Skip to content

Swap Endpoints

Request Swap Quote

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

  • 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 on every wallet-deposit swap quote (anything where route_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. The eip712 sub-object is shaped exactly as signTypedData(domain, types, message) expects — pass it straight to the wallet without assembling the struct. permit.nonce is 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_raw is informational only and does not control whether a permit is required. Only equity-only swaps (initialDepositAmount = 0) leave permit as 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

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)

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": "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 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 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:

{
  "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 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