From a sentence
to a signed swap.
Give your app a trading API. Roque reads the intent, prepares an exact trade, and settles into your user's wallet after they sign.
One backend. Your user's wallet.
The API base is https://roque-dex.vercel.app. Your backend holds an application key and forwards your user's authenticated calls. Their EVM wallet signs the login message and each transaction. The output goes to that same wallet.
Use Sepolia ETH for gas and the test-token addresses from /v1/tokens. Public v1 accepts absolute, immediate swaps such as Swap 0.1 rUSDC for rWETH. Conditional trades, percentage amounts, smart accounts and autonomous delegation are outside this API.
Application access is provisioned by the Roque operator for beta partners. Request a key through the project's integration issue tracker; never include credentials in an issue. There is no public API-key signup endpoint. Operators use register-api-client.ts with private database configuration.
API="https://roque-dex.vercel.app"
curl --fail-with-body "$API/v1/health"
curl --fail-with-body "$API/v1/chains"
curl --fail-with-body "$API/v1/tokens?chainId=11155111"
curl --fail-with-body "$API/v1/openapi.json"Prove which wallet is trading.
Send your app key as X-API-Key. Create a challenge for the selected wallet, validate the exact message against the trusted Roque origin, and ask that wallet to sign it. Challenges expire after five minutes and can be used once.
The shell examples share variables and require Bash, curl, jq and Node.js 22.13 or newer. Set OWNER to the wallet address selected by your user. Keep the application key on your backend; forward only the challenge JSON to the browser.
# Run on your backend or in a private terminal. Requires Bash, curl and jq.
API="https://roque-dex.vercel.app"
ROQUE_API_KEY_FILE="/absolute/private/path/to/partner.key"
read -r ROQUE_API_KEY < "$ROQUE_API_KEY_FILE" || exit 1
# Set OWNER to the address selected in your user's wallet.
: "${OWNER:?Set OWNER to your connected wallet address}"
curl --fail-with-body "$API/v1/auth/challenges" \
-H "X-API-Key: $ROQUE_API_KEY" \
-H "Content-Type: application/json" \
--data "$(jq -n --arg owner "$OWNER" '{owner:$owner,chainId:11155111}')" \
> challenge.json || exit 1
jq -r '.message' challenge.json// In your browser app. challenge is the exact JSON returned by your backend.
import { createWalletClient, custom } from "viem";
import { sepolia } from "viem/chains";
import { validateWalletChallenge } from "@roque/sdk";
import type { EIP1193Provider } from "viem";
export async function signChallenge(
provider: EIP1193Provider,
challenge: { challengeId: string; message: string },
) {
const wallet = createWalletClient({ chain: sepolia, transport: custom(provider) });
const [owner] = await wallet.requestAddresses();
await wallet.switchChain({ id: sepolia.id });
validateWalletChallenge(challenge.message, owner, "https://roque-dex.vercel.app");
const signature = await wallet.signMessage({ account: owner, message: challenge.message });
return { challengeId: challenge.challengeId, owner, signature };
}
// Send the returned object to your backend. Never send it an app private key.Return challengeId, owner and signature to your backend, then exchange them for a wallet token. Keep that token private and send it as Authorization: Bearer … alongside the app key on protected calls.
# OWNER and SIGNATURE come from signChallenge above.
: "${SIGNATURE:?Set SIGNATURE to the wallet signature}"
CHALLENGE_ID="$(jq -r '.challengeId' challenge.json)"
SESSION_JSON="$(curl --fail-with-body "$API/v1/auth/sessions" \
-H "X-API-Key: $ROQUE_API_KEY" \
-H "Content-Type: application/json" \
--data "$(jq -n --arg challengeId "$CHALLENGE_ID" --arg owner "$OWNER" \
--arg signature "$SIGNATURE" '{challengeId:$challengeId,owner:$owner,signature:$signature}')")" \
|| exit 1
WALLET_TOKEN="$(printf '%s' "$SESSION_JSON" | jq -er '.token')" || exit 1
# Keep this token private. All following calls use this wallet session and app key.Use the SDK from this repository for the TypeScript examples. It validates domain, URI, chain, wallet and expiry before requesting a signature.
Send the words. Poll the result.
POST /v1/intents returns 202 with an id, status and statusUrl. Interpretation runs asynchronously. Poll the same ID through queued and interpreting; proceed only when it is ready. Stop on rejected or failed.
slippageBps: 100 means 1%; the accepted range is 0–500. Commands are limited to 500 characters. Give each new trade a stable request ID and retain it across retries.
# Choose once per intended trade. Reuse it after timeouts; do not generate a new ID.
: "${REQUEST_ID:=$(node -e 'process.stdout.write(require("node:crypto").randomUUID())')}"
INTENT_JSON="$(curl --fail-with-body "$API/v1/intents" \
-H "X-API-Key: $ROQUE_API_KEY" \
-H "Authorization: Bearer $WALLET_TOKEN" \
-H "Idempotency-Key: ${REQUEST_ID}:intent" \
-H "Content-Type: application/json" \
--data '{"command":"Swap 0.1 rUSDC for rWETH","chainId":11155111,"executionMode":"wallet","slippageBps":100}')" \
|| exit 1
INTENT_ID="$(printf '%s' "$INTENT_JSON" | jq -er '.id')" || exit 1
# Poll the same intent. Stop on ready, rejected or failed.
for attempt in $(seq 1 150); do
RESULT="$(curl --fail-with-body "$API/v1/intents/$INTENT_ID" \
-H "X-API-Key: $ROQUE_API_KEY" \
-H "Authorization: Bearer $WALLET_TOKEN")" || exit 1
STATUS="$(printf '%s' "$RESULT" | jq -r '.status')"
case "$STATUS" in
ready) break ;;
rejected|failed) printf '%s\n' "$RESULT"; exit 1 ;;
esac
sleep 2
done
[ "$STATUS" = ready ] || { echo "Still pending: $INTENT_ID. Poll it again later."; exit 1; }Prepare the exact transactions.
Quote a ready intent, then prepare its quote ID. Each write needs its own Idempotency-Key. The quote pins a canonical chain block and expires after 120 seconds. A plan contains the approval and swap calldata, recipient, input amount, minimum receive, fees, price impact and expiry.
QUOTE_JSON="$(curl --fail-with-body "$API/v1/quotes" \
-H "X-API-Key: $ROQUE_API_KEY" \
-H "Authorization: Bearer $WALLET_TOKEN" \
-H "Idempotency-Key: ${REQUEST_ID}:quote" \
-H "Content-Type: application/json" \
--data "$(jq -n --arg intentId "$INTENT_ID" '{intentId:$intentId}')")" || exit 1
QUOTE_ID="$(printf '%s' "$QUOTE_JSON" | jq -er '.quoteId')" || exit 1
curl --fail-with-body "$API/v1/swaps/prepare" \
-H "X-API-Key: $ROQUE_API_KEY" \
-H "Authorization: Bearer $WALLET_TOKEN" \
-H "Idempotency-Key: ${REQUEST_ID}:prepare" \
-H "Content-Type: application/json" \
--data "$(jq -n --arg quoteId "$QUOTE_ID" '{quoteId:$quoteId}')" \
> plan.json || exit 1
jq '{swapId,recipient,tokenInSymbol,tokenOutSymbol,amountInRaw,
minAmountOutRaw,feeBps,priceImpactBps,expiresAt,steps}' plan.jsonAmounts ending in Raw are decimal strings in the token's smallest units. For example, 100000 raw units of a six-decimal token is 0.1 token. Use the returned decimal metadata and integer arithmetic.
Review first. Sign on a separate action.
Display token addresses, spend amount, minimum receive, recipient, fees, impact and expiry. After explicit user approval, validate the plan, sign the exact allowance, wait for its successful receipt, check expiry again, simulate the swap and ask the wallet to sign it.
The SDK performs these checks and independently decodes the calldata. This example uses a same-origin backend proxy that adds the app key to /v1/* requests and preserves the wallet token and idempotency headers. The complete proxy is in the partner app below.
// Browser code, with /v1/* proxied by your own backend to Roque.
// Your proxy adds X-API-Key; the browser never receives that key.
import { RoqueClient, SubmittedTradeError, validatePlan } from "@roque/sdk";
import { createWalletClient, custom } from "viem";
import { sepolia } from "viem/chains";
import type { EIP1193Provider } from "viem";
import type { SwapPlan } from "@roque/shared";
export async function connectRoque(provider: EIP1193Provider) {
const wallet = createWalletClient({ chain: sepolia, transport: custom(provider) });
const [owner] = await wallet.requestAddresses();
await wallet.switchChain({ id: sepolia.id });
const api = new RoqueClient(location.origin, {
authOrigin: "https://roque-dex.vercel.app",
onSubmitted: hash => {
// Persist immediately so a timeout/reload cannot cause a second swap.
localStorage.setItem("roque-last-submitted-hash", hash);
},
});
await api.connect(owner, message => wallet.signMessage({ account: owner, message }));
// These calls prepare a plan; they do not send tokens.
async function prepareTrade(command: string, requestId: string) {
const intent = await api.createIntent(command, requestId + ":intent", 100);
for (let attempt = 0; attempt < 150; attempt++) {
const result = await api.intent(intent.id);
if (result.status === "ready") {
const quote = await api.quote(intent.id, requestId + ":quote");
const plan = await api.prepare(quote.quoteId, requestId + ":prepare");
validatePlan(plan, owner);
return plan; // Render the amounts, minimum receive, fees and expiry for review.
}
if (["failed", "rejected"].includes(result.status)) throw new Error(result.status);
await new Promise(resolve => setTimeout(resolve, 2000));
}
throw new Error("Still pending. Resume polling intent " + intent.id);
}
// Call only from a separate explicit "Reviewed: sign" button.
async function signReviewedTrade(plan: SwapPlan) {
try {
return await api.executeReviewedPlan(plan, wallet, owner);
} catch (error) {
if (error instanceof SubmittedTradeError) {
// The swap was sent. Retry reporting this hash, never execute it again.
await api.report(plan.swapId, error.txHash, "submission:" + plan.swapId);
return error.txHash;
}
throw error;
}
}
return { api, owner, prepareTrade, signReviewedTrade };
}
// Supply a stable crypto.randomUUID() per new trade; retain its ID across retries.
// After signing, poll api.swap(plan.swapId) until confirmed or reverted.Persist the swap ID, plan and returned transaction hash. If reporting times out after a wallet broadcast, report that same hash and poll status. Resume an existing trade after a reload; another signature would create another trade.
A hash is the start of confirmation.
Report the swap transaction hash, then poll its swap ID. Status progresses through submitted and confirming. confirmed requires a successful, matching receipt and three confirmations; reverted means the transaction failed. Confirmed output is returned in settlement.amountOutRaw.
SWAP_ID="$(jq -r '.swapId' plan.json)"
: "${TX_HASH:?Set TX_HASH to the swap hash returned by your wallet}"
curl --fail-with-body "$API/v1/swaps/$SWAP_ID/submissions" \
-H "X-API-Key: $ROQUE_API_KEY" \
-H "Authorization: Bearer $WALLET_TOKEN" \
-H "Idempotency-Key: submission:$SWAP_ID" \
-H "Content-Type: application/json" \
--data "$(jq -n --arg txHash "$TX_HASH" '{txHash:$txHash}')"
# Repeat this GET until confirmed or reverted. A broadcast is not a completed trade.
curl --fail-with-body "$API/v1/swaps/$SWAP_ID" \
-H "X-API-Key: $ROQUE_API_KEY" \
-H "Authorization: Bearer $WALLET_TOKEN"
# Disconnect when this wallet is finished. No JSON body or Content-Type is needed.
curl --fail-with-body -X DELETE "$API/v1/auth/session" \
-H "X-API-Key: $ROQUE_API_KEY" \
-H "Authorization: Bearer $WALLET_TOKEN"Intent, quote and swap records are isolated by application and wallet. Disconnecting revokes this session; an already submitted transaction continues to settle on chain.
Run an independent partner app.
@roque/sdk and @roque/shared are workspace packages in the repository; the examples do not require an npm-published SDK. The bundled partner app includes a backend proxy, injected EVM wallet, review screen and status polling. Set ROQUE_API_KEY in the server environment from your supplied private key file before starting it.
git clone https://github.com/dotmantissa/roque.git
cd roque
# Node.js >=22.13 and the workspace's pinned pnpm are required.
pnpm install --frozen-lockfile
# Bundle the browser SDK from source. @roque/sdk is a workspace package.
pnpm --filter @roque/core exec node -e 'const entry=require.resolve("tsx");const esbuild=require(require.resolve("esbuild",{paths:[entry]}));esbuild.build({entryPoints:["../../examples/partner-app/browser.ts"],bundle:true,format:"esm",platform:"browser",outfile:"../../examples/partner-app/sdk.js"})'
# Read your supplied key privately and export it only for the backend server.
ROQUE_API_KEY_FILE="/absolute/private/path/to/partner.key"
read -r ROQUE_API_KEY < "$ROQUE_API_KEY_FILE" || exit 1
export ROQUE_API_KEY
ROQUE_API_URL=https://roque-dex.vercel.app \
ROQUE_AUTH_ORIGIN=https://roque-dex.vercel.app \
node examples/partner-app/server.mjs
# Open http://localhost:3100 with a funded Sepolia wallet.Connect through OAuth and MCP.
Use the remote connector URL below in an OAuth-capable MCP client. Authorization requires an allowlisted callback, S256 PKCE, the exact resource audience and explicit wallet consent on /connect. Access tokens last one hour; reconnect after expiry because refresh grants are not enabled.
Connector URL: https://roque-dex.vercel.app/mcp
Resource: https://roque-dex.vercel.app/mcp
Discovery: https://roque-dex.vercel.app/.well-known/oauth-protected-resource/mcp
OAuth server: https://roque-dex.vercel.app/.well-known/oauth-authorization-server
Tools:
roque_list_tokens
roque_quote_swap
roque_prepare_swap
roque_request_approval
roque_get_swap_statusThe connector can prepare a trade and request an approval link. The wallet owner reviews and signs on /approve. For a custom client, ask the operator to allowlist its exact callback before registering it. The configured Claude callback is https://claude.ai/api/mcp/auth_callback.
Raw commands may also be visible in GenLayer transaction data. Actual Claude-account and embedded-wallet provider certification remain pilot acceptance checks.
The public v1 endpoints.
This table is generated from the checked-in OpenAPI specification. JSON bodies use Content-Type: application/json; GET requests and empty DELETE requests do not need it.
| Method / endpoint | Use | Authentication |
|---|---|---|
GET/v1/health | Liveness | Public |
GET/v1/chains | Sepolia deployments and wallet settlement | Public |
GET/v1/tokens | Canonical test tokens | Public |
POST/v1/auth/challenges | Issue a client-bound wallet challenge | App key |
POST/v1/auth/sessions | Consume wallet proof once | App key |
DELETE/v1/auth/session | Revoke this connection | App key + wallet token (or OAuth) |
POST/v1/intents | Queue an exact-input interpretation | App key + wallet token (or OAuth) |
GET/v1/intents/{id} | Read this connection's interpretation | App key + wallet token (or OAuth) |
POST/v1/quotes | Create immutable block-bound quote | App key + wallet token (or OAuth) |
POST/v1/swaps/prepare | Prepare fixed-recipient transaction plan | App key + wallet token (or OAuth) |
GET/v1/swaps/{id} | Read receipt-backed settlement | App key + wallet token (or OAuth) |
POST/v1/swaps/{id}/submissions | Report wallet hash for chain verification | App key + wallet token (or OAuth) |
POST/v1/swaps/{id}/approval-links | Create short-lived owner-bound wallet review URL | App key + wallet token (or OAuth) |
POST/v1/approval-links/resolve | Read the plan from an approval link | Owner proof |
POST/v1/approval-links/submissions | Report a swap signed through an approval link | Owner proof |
OAuth tokens replace the app-key/wallet-token pair for permitted scopes. Approval-link resolution and submission require the link token plus a fresh wallet-owner proof; see the specification for their request fields.
Retry the operation you already started.
Errors have error.code, error.message and error.requestId. Keep the request ID for support, and avoid logging app keys, bearer tokens or signed transaction bytes.
- 400 / 413 / 422
- Fix invalid fields, payloads over 16 KiB or unsupported trade instructions.
- 401 / 403
- Reconnect an expired or revoked wallet session; check the app key, wallet and granted scopes.
- 409
- Wait for a pending operation and retry its original key. Changed request fields need a new operation; never reuse a key for different input.
- 410
- The quote or plan expired. Obtain a fresh quote and plan with new operation keys, then review them again.
- 429 / 503
- Back off and retry the same operation key. Do not turn a timeout into a second trade.
Idempotency keys must be 8–128 characters using letters, digits, ., _, : or -. They are scoped to application, wallet, chain and operation. Keep each original request body with its key.