Docs · Agents
Wallet in, swap out.
The whole path from “I have a wallet and a goal” to “I have a signed, broadcast swap”, written for a program. No key, no account, no signup — an agent can price a swap holding nothing at all, and only needs funds when it decides to trade.
What you need, and what we never do#
| The agent holds | SwapsPro provides | Nobody provides |
|---|---|---|
| A funded wallet on the sell chain, plus native gas on that chain for the approval and the swap. | Pricing across every venue, and a payload that is ready to sign. | Custody. Key handling. A hosted signer. There is no SwapsPro account to fund and nothing to withdraw. |
Concretely: SwapsPro never sees a private key, never takes possession of funds, and cannot move anything on its own. A quote is data. Signing it is entirely yours, and if you never broadcast, nothing happened.
Gas is the one that catches agents out
The five steps#
- Discover what can be priced —
/chains, then/tokens. - Quote the pair —
/quote. Free, keyless. - Branch on which of the three execution shapes came back.
- Sign and broadcast with your own key.
- Track to settlement — which means something different per shape.
1. Discover#
Both endpoints are static config, cached an hour at the edge with a day of stale-while-revalidate, and neither is rate limited. Cache them on your side and you will essentially never call them again.
request
# What can SwapsPro price at all?
curl "https://www.swaps.pro/api/sdk/v1/chains"
# What can it price on Base?
curl "https://www.swaps.pro/api/sdk/v1/tokens?chainId=8453"200 OK — real response (abridged to four chains)
{
"chains": [
{ "id": "ETH", "name": "Ethereum", "nativeSymbol": "ETH", "caip2": "eip155:1", "type": "evm", "chainId": 1 },
{ "id": "BTC", "name": "Bitcoin", "nativeSymbol": "BTC", "caip2": "bip122:0000…", "type": "utxo" },
{ "id": "BASE", "name": "Base", "nativeSymbol": "ETH", "caip2": "eip155:8453", "type": "evm", "chainId": 8453 },
{ "id": "BSC", "name": "BNB Smart Chain", "nativeSymbol": "BNB", "caip2": "eip155:56", "type": "evm", "chainId": 56 }
]
}id is the symbol every other endpoint accepts, and EVM chains also carry the numeric chainId — either form works as sellChain. The token list is curated (natives plus blue chips), not an index of the chain; you can quote any ERC-20 by passing its contract address instead of a symbol.
2. Quote#
One GET. Required: sellChain, sellToken, buyChain, buyToken, amount, address. Cross-chain also needs recipient. amount is in human units — 0.1, never 100000000000000000. Send a partner id so the traffic is attributable to you.
request
curl "https://www.swaps.pro/api/sdk/v1/quote\
?sellChain=8453&sellToken=ETH\
&buyChain=8453&buyToken=USDC\
&amount=0.1\
&address=0x21c9a94AF76B59b171b32fD125A4edF0e9A2Ad3e\
&partner=docs-agents"200 OK — real response (calldata truncated)
{
"provider": "lifi",
"sellChain": "BASE",
"buyChain": "BASE",
"sellToken": { "caip": "eip155:8453/slip44:60", "symbol": "ETH" },
"buyToken": { "caip": "eip155:8453/erc20:0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "symbol": "USDC" },
"sellAmount": "0.1",
"buyAmount": "242.868948",
"minBuyAmount": "241.654603",
"rate": 2428.6894799999995,
"tx": {
"chainId": 8453,
"to": "0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE",
"data": "0x736eac0b24238ec7374b4b60b30b33aca5ab8341f288aec869a8732be54e4baf2ada5a0c…",
"value": "0x16345785d8a0000",
"gasLimit": "0x7212bc"
},
"expiresAt": "2026-08-23T16:03:11.867Z",
"partner": "docs-agents",
"partnerFee": {
"requestedBps": 0,
"collectedBps": 0,
"collected": false,
"note": "No partner fee was requested."
}
}provider names the venue that won the price race. expiresAt is real and usually about a minute out — re-quote rather than sign something stale. Every failure is { "error": "…", "code": "…" } with a matching status; the API page lists every code.
3. Branch on the execution shape#
This is the step an agent most often gets wrong. A quote comes back in exactly one of three shapes, decided by which venue won, and they are not interchangeable — one is a transaction, one is a signature, one is a payment. Check the shape before you do anything else.
| Field present | What it is | How it settles |
|---|---|---|
tx | A ready-to-sign EVM transaction. Plus approval when selling an ERC-20. | You broadcast it. Settled at the receipt. |
order | A CoW Protocol order: an appData document, EIP-712 typed data, and a URL to POST to. | You sign and post — no gas, no broadcast. A solver fills it in a batch auction, or it expires. |
depositAddress | A THORChain-style deposit route, with a memo that encodes the whole trade. | You send the sell asset to the address with the memo attached. Settled when the destination chain pays out. |
branching
type Quote = Awaited<ReturnType<typeof getQuote>>;
// Exactly one of these three is present. Branch on it FIRST — everything
// after this point differs, including whether a transaction exists at all.
function shapeOf(q: Quote) {
if (q.tx) return "evm-transaction" as const; // sign and broadcast
if (q.order) return "cow-order" as const; // sign and POST, no gas
if (q.depositAddress) return "deposit-memo" as const; // send funds + memo
throw new Error(`unrecognised quote shape from ${q.provider}`);
}Shape one — an EVM transaction#
The common case, and the only one that is a transaction. value and gasLimit arrive as 0x-hex quantities, ready for eth_sendTransaction. If approval is present, grant exactly amountWei of token to spender and wait for it to mine before sending the swap — an unmined approval is the single most common cause of a reverted swap.
quote → approve → swap → receipt
import { createWalletClient, http, publicActions, erc20Abi } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { base } from "viem/chains";
const account = privateKeyToAccount(process.env.AGENT_KEY as `0x${string}`);
const wallet = createWalletClient({ account, chain: base, transport: http() }).extend(publicActions);
const url = new URL("https://www.swaps.pro/api/sdk/v1/quote");
url.search = new URLSearchParams({
sellChain: "8453", sellToken: "ETH",
buyChain: "8453", buyToken: "USDC",
amount: "0.1",
address: account.address,
partner: "my-agent",
}).toString();
const res = await fetch(url);
if (!res.ok) {
const { code, error } = await res.json(); // { error, code } on every failure
throw new Error(`${code}: ${error}`);
}
const quote = await res.json();
if (!quote.tx) throw new Error(`expected an EVM tx, got a ${quote.order ? "CoW order" : "deposit route"}`);
// 1. Approve first if we are selling an ERC-20. Native sells have no approval.
if (quote.approval) {
const hash = await wallet.writeContract({
address: quote.approval.token,
abi: erc20Abi,
functionName: "approve",
args: [quote.approval.spender, BigInt(quote.approval.amountWei)],
});
await wallet.waitForTransactionReceipt({ hash }); // MUST mine before the swap
}
// 2. Broadcast the swap. `value` and `gasLimit` arrive as 0x-hex quantities.
const hash = await wallet.sendTransaction({
to: quote.tx.to,
data: quote.tx.data,
value: BigInt(quote.tx.value ?? "0x0"),
gas: quote.tx.gasLimit ? BigInt(quote.tx.gasLimit) : undefined,
});
// 3. Track. A same-chain route is settled when the receipt says so.
const receipt = await wallet.waitForTransactionReceipt({ hash });
console.log(receipt.status, hash);Shape two — a CoW order#
CoW settles by signature rather than by transaction, so the response carries order and no tx. It costs no gas, which is genuinely useful for an agent, at the price of being asynchronous: solvers batch it and it can expire unfilled. Do not treat the POST as settlement.
register appData → sign → post
// A CoW route is gasless: nothing is broadcast, an order is signed and posted.
// Solvers fill it in a batch auction — usually inside a minute, and it CAN
// expire unfilled, so an agent must treat this as asynchronous.
// 0. One-time approval, exactly as above, but the spender is CoW's vault relayer.
// 1. Register the fee metadata document so the appData hash resolves.
await fetch(quote.order.appDataUrl, {
method: "PUT",
headers: { "content-type": "application/json" },
body: JSON.stringify({ fullAppData: quote.order.appData.fullAppData }),
});
// 2. Sign the typed data verbatim, from the address that asked for the quote.
const signature = await account.signTypedData(quote.order.typedData);
// 3. Post it. The reply is the orderUid you poll for a fill.
const posted = await fetch(quote.order.postUrl, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ ...quote.order.body, signature, signingScheme: quote.order.signingScheme }),
});
const orderUid = await posted.json();Shape three — deposit and memo#
Cross-chain routes through THORChain-style protocols settle by depositing. There is nothing to sign on our side at all: send sellAmount of the sell asset to depositAddress with memo attached, from a wallet for the sell chain. The memo encodes the destination, the recipient and the limit price, so it must be attached verbatim — a deposit without its memo is a donation.
request
curl "https://www.swaps.pro/api/sdk/v1/quote\
?sellChain=BTC&sellToken=BTC\
&buyChain=ETH&buyToken=ETH\
&amount=0.01\
&address=bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq\
&recipient=0x21c9a94AF76B59b171b32fD125A4edF0e9A2Ad3e"200 OK — real response (abridged)
{
"provider": "thorchain",
"sellChain": "BTC",
"buyChain": "ETH",
"sellAmount": "0.01",
"buyAmount": "0.31509635",
"minBuyAmount": "0.30564345",
"depositAddress": "bc1q2nfxrvvg67nhey0gk0cc8ke2ea4akge8kskyyq",
"memo": "=:ETH.ETH:0x21c9a94AF76B59b171b32fD125A4edF0e9A2Ad3e:30564345:keep/thor1ujdj4360n835r49yzuvvsyu80hv28k9frlqeuh:15/15",
"expiresAt": "2026-08-23T15:39:02.000Z"
}Cross-chain needs a recipient
recipient is optional same-chain (it defaults to address) and required cross-chain, because the address that pays on Bitcoin cannot receive on Ethereum. Omitting it is a BAD_REQUEST, not a default.Rate limits — and why quoting is free#
Quoting costs nothing and is not going to start costing something. 60 requests per minute per IP, no key, no payment, no account. SwapsPro earns from the routing fee inside a swap that actually executes, so a quote that leads to a trade has already paid for itself and a quote that does not is the top of our funnel. Charging a cent to find out what a swap costs would tax exactly the loop an agent has to run to decide whether to trade at all.
What does have a marginal cost is capacity: every quote fans out to upstream venues whose quota we do not own. So headroom — not calls — is the only thing we would ever sell.
How the number is counted, and whether you can trust it#
On this deployment the limit is best-effort — read that literally
The count lives in memory inside each serverless instance, and requests spread across however many are warm. So the effective ceiling is 60 times the instance count, and a 429 tells you which instance you landed on rather than what your own request rate is. Measured against production: 80 sequential requests were cut off at exactly 60, while 120 issued in parallel got 92 through. Do not design a backoff around the published figure.
Which is also why nothing is sold here. A paid tier would be charging for a lift on a number we cannot hold ourselves to, so /api/sdk/v1/access answers 503 until a shared counter is configured. Quoting is unaffected, and free either way.
The x402 handshake, exactly#
Not live on this deployment
/api/sdk/v1/access answers 503 with ACCESS_NOT_CONFIGURED rather than the 402 below — we do not sell a lift on a limit we cannot meter. It is documented here so you can write the client once and have it work when the endpoint turns on./api/sdk/v1/access speaks both x402 wire versions at once, because most deployed agent clients still speak v1 while the spec has moved to v2. The 402 carries the v1 requirements in the JSON body and the v2 requirements base64'd in the PAYMENT-REQUIRED header, so whichever your client reads, it finds terms that match.
Step 1 — ask, get a 402#
request
$ curl -s -D- "https://www.swaps.pro/api/sdk/v1/access"
HTTP/2 402
payment-required: eyJ4NDAyVmVyc2lvbiI6MiwiZXJyb3IiOiJQQVlNRU5ULVNJR05BVFVSRSAo…
access-control-allow-origin: *402 body — x402 v1, verbatim
{
"x402Version": 1,
"error": "X-PAYMENT header is required",
"accepts": [
{
"scheme": "exact",
"network": "base",
"maxAmountRequired": "1000000",
"resource": "https://www.swaps.pro/api/sdk/v1/access",
"description": "SwapsPro API rate-limit elevation — raises GET /api/sdk/v1/quote from 60 to 600 requests per minute for 24 hours. Quotes themselves are free and always will be; this sells headroom, not calls. Pay 1.00 USDC on Base and the response carries a bearer token to send as X-SwapsPro-Access on subsequent quote requests.",
"mimeType": "application/json",
"payTo": "0xAccF0dB4b6B55Ba692467988D0a1188f26428C2b",
"maxTimeoutSeconds": 300,
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"extra": { "name": "USD Coin", "version": "2" }
}
]
}Decoding the header gives a v2 client the same terms in its own shape:
base64-decoded PAYMENT-REQUIRED — x402 v2
{
"x402Version": 2,
"error": "PAYMENT-SIGNATURE (or X-PAYMENT) header is required",
"resource": {
"url": "https://www.swaps.pro/api/sdk/v1/access",
"description": "SwapsPro API rate-limit elevation — raises GET /api/sdk/v1/quote from 60 to 600 requests per minute for 24 hours. …",
"mimeType": "application/json",
"serviceName": "SwapsPro"
},
"accepts": [
{
"scheme": "exact",
"network": "eip155:8453",
"amount": "1000000",
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"payTo": "0xAccF0dB4b6B55Ba692467988D0a1188f26428C2b",
"maxTimeoutSeconds": 300,
"extra": { "name": "USD Coin", "version": "2" }
}
]
}Step 2 — pay#
Sign an EIP-3009 TransferWithAuthorization over Base USDC for exactly 1000000 atomic units (1.00 USDC) to payTo, using the EIP-712 domain in extra and the USDC contract in asset as the verifying contract. Base64 the payload and retry.
| Header | Direction | Meaning |
|---|---|---|
PAYMENT-REQUIRED | ← from us | Base64 JSON. The x402 v2 requirements, on the 402. |
PAYMENT-SIGNATURE | → to us | Base64 JSON. The signed x402 v2 payment payload. |
X-PAYMENT | → to us | The x402 v1 equivalent. Send one or the other, not both. |
PAYMENT-RESPONSE | ← from us | Base64 JSON settlement receipt, incl. the tx hash (v1: X-PAYMENT-RESPONSE). |
X-SwapsPro-Access | → to us | The granted token, on subsequent /quote calls. Authorization: Bearer works too. |
We verify and settle through an x402 facilitator before issuing anything — the USDC has actually moved by the time you hold a token. The grant is bound to the payer the facilitator confirms, never to an address you name.
Step 3 — hold the token, use the headroom#
200 OK — the grant
{
"token": "swp1.eyJzdWIiOiIweDIxYzlhOTRB…",
"header": "X-SwapsPro-Access",
"subject": "0x21c9a94AF76B59b171b32fD125A4edF0e9A2Ad3e",
"via": "x402",
"limit": { "requestsPerMinute": 600, "previously": 60 },
"expiresAt": "2026-08-24T16:03:11.867Z",
"ttlSeconds": 86400,
"usage": "Send \"X-SwapsPro-Access: <token>\" (or \"Authorization: Bearer <token>\") on GET /api/sdk/v1/quote."
}using it
curl -H "X-SwapsPro-Access: swp1.eyJzdWIiOiIweDIxYzlhOTRB…" \
"https://www.swaps.pro/api/sdk/v1/quote?sellChain=8453&sellToken=ETH&buyChain=8453&buyToken=USDC&amount=0.1&address=0x21c9…"It is a bearer token, and it is one bucket
A Pro Pass holder pays nothing#
Holding a SwapsPro Pass earns the same elevation for free. Ownership is read from chain rather than asserted — but reading the chain only proves a pass exists at an address, not that you are it, and a token minted for an address is usable by whoever holds it. So the endpoint also asks you to sign a short, timestamped challenge.
challenge → sign → claim
# 1. Ask for the exact bytes to sign.
curl "https://www.swaps.pro/api/sdk/v1/access?address=0xYourPassWallet&challenge=1"
{
"message": "SwapsPro API access\n\nI control this address and claim SwapsPro Pass rate-limit elevation for it.\naddress: 0xyourpasswallet\nissued: 2026-08-23T16:03:11.867Z",
"issued": "2026-08-23T16:03:11.867Z",
"signWith": "personal_sign / eth_sign (EIP-191). Send the 0x signature back verbatim.",
"expiresInSeconds": 600
}
# 2. personal_sign it, then claim. Same token body, "via": "pro-pass".
curl "https://www.swaps.pro/api/sdk/v1/access?address=0xYourPassWallet&issued=2026-08-23T16%3A03%3A11.867Z&signature=0x…"EOAs only on this path
When elevation is switched off#
Elevation needs two things: a signing secret, and a shared counter. The second is the one that matters — a secret only lets us mint a token, whereas a shared counter is what makes the thing that token buys real. Without one the limit is per-instance and unmeterable, so we do not offer it.
With either missing, /api/sdk/v1/access answers 503 with ACCESS_NOT_CONFIGURED, names the missing config in missingConfig, and describes the real free tier in the body. Nothing else changes: /quote serves every caller exactly as it always has, and a token presented to it is ignored rather than rejected. There is no configuration in which quoting breaks.
Reading us without asking#
SwapsPro publishes the llms.txt convention: /llms.txt is a short index with a note per link, and /llms-full.txt inlines the content so a model can answer without following anything. Both are CORS-open plain text, generated from the same source as these docs, and every page here is linked from them.
the machine-readable map
curl "https://www.swaps.pro/llms.txt" # the index
curl "https://www.swaps.pro/llms-full.txt" # the same map, content inlinedEvery page also carries a Link: </llms.txt>; rel="describedby" response header, which is the discovery mechanism the llms.txt spec itself recommends. There is deliberately no .well-known entry: no registered convention covers this, the llms.txt spec explicitly rejects .well-known for it, and x402 discovery lives on the facilitator rather than on our origin. We would rather publish nothing than publish an invented path.
Where to go next#
- HTTP API — every parameter, every error code, every response field.
- SDK — the same endpoints with types, if you are in a JavaScript runtime.
- Fees — what a swap costs and which venues can carry a partner fee.
- Free and Pro — including buying a Pass over x402, which is the other agent-shaped endpoint we run.
