Swap Endpoints¶
Request Swap Quote¶
Authentication: None
Request Body¶
{
"from_token": "0x965d4b4546716e416e950bc30467d128455d2d0e",
"to_token": "0xef64d15ed6c371545eb6dcd6c026c17dfb6c440f",
"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": "0x965d4b4546716e416e950bc30467d128455d2d0e",
"outputToken": "0xef64d15ed6c371545eb6dcd6c026c17dfb6c440f",
"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": "0x965d4b4546716e416e950bc30467d128455d2d0e",
"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": "0x965d4b4546716e416e950bc30467d128455d2d0e"
},
"eip712": {
"domain": {
"name": "USD Coin", "version": "2",
"chainId": 11155111,
"verifyingContract": "0x965d4b4546716e416e950bc30467d128455d2d0e"
},
"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 wheninitialDepositAmount > 0. Ifpermit_supported=true, sign the returnedeip712payload. The nonce is read from the chain; the quote does not reserve a sequence of future nonces. Submission checks the chain nonce again and atomically claims that signed nonce. A changed/claimed nonce may require a fresh quote. Pure-equity quotes (initialDepositAmount=0) havepermit=null.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
Invalid Intent signatures or missing required permit fields fail before the quote is claimed. Concurrent or repeated valid submissions use the identity rules below; they do not uniformly return 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 is unsupported and allowance is insufficient, approve the quoted spender using the transaction helpers, sign locally and broadcast with POST /tx/send. Wait for confirmation and obtain a fresh quote before signing/submitting the swap. An already sufficient allowance needs no new approve.
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": "0x965d4b4546716e416e950bc30467d128455d2d0e",
"to_token": "0xef64d15ed6c371545eb6dcd6c026c17dfb6c440f",
"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. Typed upstream codes are forwarded verbatim, including quote-specific errors. Codes shared with execution, such as AMOUNT_BELOW_MIN and NO_LIQUIDITY, can use the same handling; clients must also handle unknown codes.
rejectionCategory | When |
|---|---|
| Typed code | Forwarded verbatim; quote-specific codes need not belong to the execution error vocabulary. |
SWAP_GAS_PRICE_UNAVAILABLE | Valid USD prices needed to convert gas cost are unavailable. The batch remains HTTP 200 with an ok:false item; message can be empty. Retry later. |
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 | Untyped upstream 5xx / unreachable accounting service; typed codes take precedence. |
quote_timeout | Per-pair compute exceeded the server-side per-pair budget (default 10 s) |
internal_error | Unhandled server error |
A single quote or Exact Output quote can instead return generic HTTP 503 for unavailable gas pricing; do not require SWAP_GAS_PRICE_UNAVAILABLE in its cleaned response. Batch sizes and signatures also obey the whole-body limit.
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 | When permit_required && permit_supported | Sign the returned permit.eip712 payload; supports 64/65-byte signatures. For unsupported tokens, complete approve and omit this field. |
permit_deadline | integer | With permit_signature | Deadline signed in the permit; use the quote suggestion and submit both fields together. |
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 for categorized business rejections. Other 4xx responses can contain a string detail. A categorized response has this shape:
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 | Confirm the original request cannot execute, then obtain a fresh quote and authorization |
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 | Reconcile the original execution, then request a fresh quote if it cannot execute |
NO_LIQUIDITY | Route has no executable depth at the quoted price; the public quote API has no client slippage parameter | 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 | Reconcile the original execution and retain request identity for any retry; see below |
Error Cases¶
| Status | Cause |
|---|---|
400 | Invalid request, signature mismatch, missing required permit fields, or non-executable quote (minOutputAmount = 0) |
409 | A pre-admission stale permit nonce can return a string detail and leave the quote unconsumed. Other quote conflicts may be typed. Inspect the response and current order state; do not assume every 409 has the same cause or consumption state. |
410 | Missing quote, expired unclaimed quote, or result no longer retained; a retained identical successful request can still return its cached result |
429 | Wallet rate limit exceeded |
503 | Service temporarily unavailable; retry after the Retry-After header |
Quote Display Fields¶
Single and batch quote successes can also include route_metadata.leg_count and quote_breakdown:
Field in quote_breakdown | Description |
|---|---|
gas_mode, input_token, output_token | Gas choice and token identity |
input_amount_before_gas, input_amount_after_gas | Input amounts before/after gas, in raw input-token units |
output_amount_before_gas, output_amount_after_gas | Output amounts before/after gas, in raw output-token units |
gas_cost_input_token, gas_cost_output_token | Gas cost in the corresponding raw token units |
gas_cost_usd | USD cost as a decimal string |
These are display estimates, not confirmed settlement amounts. Sign route_params unchanged. Batch quote requests are charged by the number of requested quotes, with a quote-group budget of 50 per second when public throttling is enforced.
To specify a required output amount, use the separate Exact Output API. Setting pay_more on this exact-input quote is not the same API contract.
Swap Error Envelope¶
Categorized submission failures use { "detail": { "detail": "Swap execution failed", "error_code": "NO_LIQUIDITY" } }. Codes include ALLOWANCE_INSUFFICIENT, INTENT_DEADLINE_EXPIRED, SLIPPAGE_EXCEEDED, NO_LIQUIDITY, QUOTE_STALE, AMOUNT_BELOW_MIN, STP_BLOCKED, COMPLIANCE_BLOCKED and TRANSIENT_SETTLEMENT_FAILURE. Missing signatures, invalid request models and other preflight failures can use a string detail instead. A rejectionCategory in a batch quote is a per-item quote error, not proof that a swap was submitted.
Submission Identity and Retries¶
Persist uuid, signature, permit_signature and permit_deadline before the first submission. These values identify the request; preserve the original signature encoding and permit values when retrying.
| Existing quote state | Same request | Different request |
|---|---|---|
| Successful result retained | HTTP 200 with the original result; no second execution | 409 conflict |
| Submission in progress | Recover a known result when available; otherwise 409 in progress | 409 conflict |
| Rejected | 409 with the rejection code, or 403 for a compliance rejection, while retained | Not a new quote |
| Missing or expired before claim | 410 | 410 |
After a timeout or 503, retry the identical signed body to recover the stored result. Once a response supplies trade_id, use that ID for GET /orders/{order_id} with the owner API key. A 409 saying submission is in progress is not terminal failure. Request a fresh quote only after confirming the prior attempt is rejected or cannot execute. Retention is finite; indefinite replay is not guaranteed after cleanup. Exact Output has its own status routes keyed by quote_uuid.
Keep the Three Swap Identifiers Separate¶
| Identifier | Use |
|---|---|
Quote response uuid | String sent as POST /swap request uuid; identifies the stored quote and retry body |
route_params.uuid | Composite uint256 signed inside the Intent |
Execution response trade_id | Persisted order ID for GET /orders/{order_id} and /fills/{order_id} |
The persisted order ID is generated separately. Ordinary Swap does not expose an Exact Output-style status-by-quote endpoint; a lost initial response is recovered by an identical POST /swap retry, not by substituting the quote ID into an order URL.