Skip to content

Swap Endpoints

Request Swap Quote

POST /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

  • uuid is the quote record ID used in POST /swap.
  • route_params.uuid is the composite uint256 bound into the signed Intent.
  • expires_at is short-lived. The current implementation stores quotes for roughly 30 seconds.
  • fee_breakdown covers gas costs only; the underlying pricing inputs are not part of the response contract.
  • permit is non-null when initialDepositAmount > 0. If permit_supported=true, sign the returned eip712 payload. 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) have permit=null.
  • permit_supported = false indicates the input token does not implement EIP-2612 — fall back to POST /approve (no eip712 block 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)

POST /swap/quote/batch

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 422 from 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 /swap with its own signed Intent, just as you would for POST /swap/quote results.

Execute Swap

POST /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:

{
  "detail": {
    "detail": "Swap execution failed",
    "error_code": "NO_LIQUIDITY"
  }
}

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.