Swap Endpoints¶
Request Swap Quote¶
Authentication: None
Request Body¶
{
"from_token": "0xDcAEcdd8Db64f4316A11917Ad0162DEBD935285b",
"to_token": "0xd3BdB2CE9cD98566EFc2e2977448c40578371779",
"from_amount": "1000000",
"owner_address": "0x...",
"recipient": "0x...",
"expiration": 1713254400,
"gas_mode": "receive_less"
}
| Field | Type | Required | Description |
|---|---|---|---|
from_token | address | Yes | ERC-20 input token address |
to_token | address | Yes | ERC-20 output token address |
from_amount | string | Yes | Raw input amount as a positive uint256 string |
owner_address | address | Yes | Wallet that will sign and fund the swap |
recipient | address | Yes | Final token recipient. May differ from owner_address — set to any address to deliver the output to a third-party wallet. The signed Intent locks this value, and the contract enforces that the final leg pays exactly this address. |
expiration | integer | Yes | Unix timestamp used as the Intent deadline; must satisfy now < expiration <= now + 365 days - 300 seconds |
gas_mode | string | No | receive_less or pay_more; defaults to receive_less |
from_token and to_token must differ.
If from_amount is below the token floor from GET /tokens, the endpoint returns HTTP 400 with structured detail.code = "AMOUNT_BELOW_MIN" and the minimum raw / decimal amount that must be met.
Response¶
{
"uuid": "6d0ad60d-c5d5-4b71-b0ca-9e8d2ae1bca4",
"route_params": {
"taker": "0x...",
"inputToken": "0xDcAEcdd8Db64f4316A11917Ad0162DEBD935285b",
"outputToken": "0xd3BdB2CE9cD98566EFc2e2977448c40578371779",
"maxInputAmount": "1000000",
"minOutputAmount": "923450",
"recipient": "0x...",
"initialDepositAmount": "1000000",
"uuid": "6431994229952211760403847975151123456789012345678901234567",
"deadline": 1713254400
},
"fee_breakdown": {
"gas_cost_usd": "0.12",
"gas_cost_from_token": "0.120000"
},
"expires_at": 1713250830,
"permit": {
"permit_supported": true,
"permit_required": true,
"token": "0xDcAEcdd8Db64f4316A11917Ad0162DEBD935285b",
"spender": "0x...",
"owner": "0x...",
"value_raw": "1000000",
"current_allowance_raw": "1000000",
"nonce": 4,
"suggested_deadline": 1713254400,
"domain": {
"name": "USD Coin",
"version": "2",
"chainId": 11155111,
"verifyingContract": "0xDcAEcdd8Db64f4316A11917Ad0162DEBD935285b"
},
"eip712": {
"domain": {
"name": "USD Coin", "version": "2",
"chainId": 11155111,
"verifyingContract": "0xDcAEcdd8Db64f4316A11917Ad0162DEBD935285b"
},
"primaryType": "Permit",
"types": {
"Permit": [
{ "name": "owner", "type": "address" },
{ "name": "spender", "type": "address" },
{ "name": "value", "type": "uint256" },
{ "name": "nonce", "type": "uint256" },
{ "name": "deadline", "type": "uint256" }
]
},
"message": {
"owner": "0x...",
"spender": "0x...",
"value": "1000000",
"nonce": 4,
"deadline": 1713254400
}
}
}
}
Response Notes¶
uuidis the quote record ID used inPOST /swap.route_params.uuidis the composite uint256 bound into the signed Intent.expires_atis short-lived. The current implementation stores quotes for roughly 30 seconds.fee_breakdowncovers gas costs only; the underlying pricing inputs are not part of the response contract.permitis non-null on every wallet-deposit swap quote (anything whereroute_params.initialDepositAmount > 0), even when the wallet already holds a standing allowance covering the spend. The server always re-signs per quote so back-to-back swaps cannot race the same allowance to zero. Theeip712sub-object is shaped exactly assignTypedData(domain, types, message)expects — pass it straight to the wallet without assembling the struct.permit.nonceis the wallet's expected next EIP-2612 nonce after any swap from the same wallet already accepted by the server but not yet mined, so two back-to-back swaps queue in order at the relayer.current_allowance_rawis informational only and does not control whether a permit is required. Only equity-only swaps (initialDepositAmount = 0) leavepermitasnull.permit_supported = falseindicates the input token does not implement EIP-2612 — fall back toPOST /approve(noeip712block in this branch).
Gas Modes¶
| Mode | Behavior |
|---|---|
receive_less | Preserve spend and let gas reduce output |
pay_more | Preserve output target by increasing total input budget |
Note
A quote is consumed only when a fully valid signed submission arrives. Signature mismatches, missing required permit fields, and minOutputAmount = 0 all return 400 without burning the quote, so a benign error doesn't force you to re-quote. If another valid caller wins the race first, you receive 410.
Note
Some quotes intentionally return route_params.minOutputAmount = "0" when there is no executable route at the requested size. Those quotes are informational only and POST /swap will reject them.
Note
If permit.permit_supported is false, fall back to POST /approve with the live sor_address from GET /config, sign the returned tx locally, broadcast it via POST /tx/send, and then submit POST /swap.
Request Multiple Swap Quotes (Batched)¶
Authentication: None
Request up to 50 swap quotes in a single round-trip. Useful for market-makers and traders that need to price many pairs at once — collapses N sequential /swap/quote calls into one HTTP request with bounded concurrency at the server.
Request Body¶
{
"quotes": [
{
"from_token": "0xDcAEcdd8Db64f4316A11917Ad0162DEBD935285b",
"to_token": "0xd3BdB2CE9cD98566EFc2e2977448c40578371779",
"from_amount": "1000000",
"owner_address": "0x...",
"recipient": "0x...",
"expiration": 1713254400,
"gas_mode": "receive_less"
}
]
}
| Field | Type | Required | Description |
|---|---|---|---|
quotes | array | Yes | 1–50 quote requests; each item has the same shape as POST /swap/quote's body |
Per-item field validation is identical to POST /swap/quote. A request that fails Pydantic validation on any item (invalid address, self-trade, bad gas_mode, etc.) returns 422 and does not invoke the handler. Per-item runtime errors (oracle, OA, timeout) are surfaced as a per-item error envelope instead — the request itself returns 200.
Response¶
{
"items": [
{
"ok": true,
"quote": {
"uuid": "6d0ad60d-c5d5-4b71-b0ca-9e8d2ae1bca4",
"route_params": { "...": "..." },
"fee_breakdown": { "gas_cost_usd": "0.12", "gas_cost_from_token": "0.120000" },
"expires_at": 1713250830,
"permit": null
},
"error": null
},
{
"ok": false,
"quote": null,
"error": {
"rejectionCategory": "no_liquidity",
"message": "No liquidity"
}
}
]
}
items[i] corresponds positionally to quotes[i] — same order, same length. Each item is either a successful quote (ok: true) or a typed error envelope (ok: false).
Error Envelope¶
rejectionCategory is a string. When the underlying error carries a typed code (the same code field used by POST /swap's typed error envelope — e.g., AMOUNT_BELOW_MIN, NO_LIQUIDITY, SLIPPAGE_EXCEEDED, INTENT_DEADLINE_EXPIRED, QUOTE_STALE), it is forwarded verbatim so the same client switch can handle both endpoints. Batch-runtime concerns surface their own categories:
rejectionCategory | When |
|---|---|
Public typed code (e.g. AMOUNT_BELOW_MIN, NO_LIQUIDITY, SLIPPAGE_EXCEEDED) | Forwarded from the pricing engine; same vocabulary as POST /swap's typed error_code. See the Swap error envelope above for the action table. |
no_liquidity | Pricing returned no liquidity in the legacy string form (lowercase variant; treat the same as the typed NO_LIQUIDITY) |
invalid_quote | Other 4xx from the pricing engine that didn't carry a typed code |
upstream_unavailable | Order-accounting unreachable / 5xx |
quote_timeout | Per-pair compute exceeded the server-side per-pair budget (default 10 s) |
internal_error | Unhandled server error |
Operational Notes¶
- Cap: 50 quotes per request. Larger batches return
422from request validation. Submit further pairs in subsequent calls. - Concurrency: the server caps in-flight per-pair compute to a fixed concurrency (default 20) so 50 simultaneous quotes never burst more than 20 concurrent OA round-trips.
- Per-pair timeout: each pair's compute is bounded so one slow pair cannot stall the rest of the batch. Slow pairs return
quote_timeout; siblings still succeed. - Pricing snapshot: quotes are computed independently per pair — there is no cross-pair coherent snapshot. If you need atomic pricing across pairs, this endpoint is not the right tool.
- No batch idempotency: every successful item mints a fresh quote
uuid. Retrying a batch produces new quote UUIDs. - Quote UUIDs returned per item are independent — submit each via
POST /swapwith its own signed Intent, just as you would forPOST /swap/quoteresults.
Execute Swap¶
Authentication: EIP-712 Intent signature carried in the request body
Request Body¶
{
"uuid": "6d0ad60d-c5d5-4b71-b0ca-9e8d2ae1bca4",
"signature": "0x...",
"permit_signature": "0x...",
"permit_deadline": 1713254400
}
| Field | Type | Required | Description |
|---|---|---|---|
uuid | string | Yes | Quote ID returned by POST /swap/quote |
signature | hex string | Yes | Signature over quote.route_params |
permit_signature | hex string | Yes when permit != null | EIP-2612 permit signature obtained by passing quote.permit.eip712.{domain, types, message} to wallet.signTypedData(...). Both standard 65-byte and compact 64-byte hex encodings are accepted. Omit only on equity-only swaps where quote.permit == null |
permit_deadline | integer | Yes when permit != null | The deadline value signed into the permit (= quote.permit.eip712.message.deadline). Required alongside permit_signature |
Response¶
{
"success": true,
"trade_id": "85c92fcb-21b9-43ba-bb36-01d7b21eaa8d",
"status": "pending",
"fee_breakdown": {
"gas_cost_usd": "0.12",
"gas_cost_from_token": "0.120000"
}
}
trade_id is the order ID you can later inspect through GET /orders/{order_id} or filter through GET /orders?type=swap.
fee_breakdown is optional and may be omitted on some failure or degraded-response paths.
Intent Type To Sign¶
const types = {
Intent: [
{ name: 'taker', type: 'address' },
{ name: 'inputToken', type: 'address' },
{ name: 'outputToken', type: 'address' },
{ name: 'maxInputAmount', type: 'uint256' },
{ name: 'minOutputAmount', type: 'uint256' },
{ name: 'recipient', type: 'address' },
{ name: 'initialDepositAmount', type: 'uint256' },
{ name: 'uuid', type: 'uint256' },
{ name: 'deadline', type: 'uint48' }
]
};
Sign quote.route_params exactly as returned. Do not regenerate or normalize fields client-side.
Error Envelope¶
POST /swap returns a typed envelope on every 4xx failure. The wire shape is:
The inner detail is a curated, human-readable string. The error_code carries the categorical signal — clients should branch on error_code and ignore the human string for routing logic.
error_code | When it surfaces | Client action |
|---|---|---|
ALLOWANCE_INSUFFICIENT | ERC-20 allowance to the spender is insufficient at transfer time, or the permit signature is invalid/expired | Re-permit or re-approve and retry |
INTENT_DEADLINE_EXPIRED | Signed Intent's deadline passed before the tx landed | Request a fresh quote |
SLIPPAGE_EXCEEDED | The matching engine rejected the swap because no crossing liquidity exists at the signed price | Widen slippage and re-quote |
NO_LIQUIDITY | Route has no executable depth at the quoted price; widening slippage will not help | Reduce size or try a different pair |
QUOTE_STALE | Quote snapshot or signed deadline expired before submission | Request a fresh quote |
AMOUNT_BELOW_MIN | Amount is below the pair's configured minimum | Increase amount |
STP_BLOCKED | Self-trade prevention — the swap would cross the caller's own resting order | Cancel the resting order or revise the side |
TRANSIENT_SETTLEMENT_FAILURE | Catch-all for non-actionable infrastructure failures and counterparty-state changes | Retry; if it persists, escalate operationally |
Error Cases¶
| Status | Cause |
|---|---|
400 | Invalid request, signature mismatch, missing required permit fields, or non-executable quote (minOutputAmount = 0) |
409 | Typed envelope with error_code = "QUOTE_STALE" — the chain nonce or server-side queue advanced between the quote and this submit (e.g. another swap from this wallet landed first), or the upstream rejected the planned quote. The quote is not consumed; clients should silently re-quote and re-submit. |
410 | Quote expired, not found, or already consumed |
429 | Wallet rate limit exceeded |
503 | Service temporarily unavailable; retry after the Retry-After header |