{
  "openapi": "3.1.0",
  "info": {
    "title": "Exen Aggregation API",
    "version": "1.0.0",
    "description": "DEX-aggregation quote + swap API. One request prices a trade against every venue Exen indexes\non a chain \u2014 AMMs, concentrated liquidity, proprietary market makers and RFQ desks \u2014 splits it\nacross whichever combination pays best, and returns calldata that settles the whole route in a\nsingle transaction through the Exen router. Built by Wraxyn.\n\n## Getting started\n\nOne base URL for everyone: **`https://exen.wraxyn.io`**.\n\nNo signup to try it \u2014 anonymous callers get **3 requests/second**, enough to evaluate the API\nand build against it. Every route works; nothing is held back behind a key.\n\nSend an **`x-api-key`** header and that ceiling is replaced by whatever rate your key is\nprovisioned for. The key also carries your partner configuration \u2014 a negotiated fee policy\n(which overrides any fee in the request), on-chain attribution of your fills, and access to\nvenues that price only for approved parties.\n\nKeys are free. Email **admin@wraxyn.io** with what you are building, the chains you need and a\nrough request rate.\n\n## Conventions (read first)\n\n- **All token amounts on the wire are decimal strings of base units (wei)** \u2014 never\n  token-decimal floats. `\"1500000\"` = 1.5 USDC (6 decimals). Decimals vary by chain and are\n  not guessable: BSC stablecoins are 18, not 6.\n- **Addresses** are 0x-prefixed 20-byte hex, case-insensitive.\n- **Native token** (ETH / BNB / AVAX / OKB) is the sentinel address\n  `0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee` on both `sellToken` and `buyToken`.\n- `/v1/quote` and `/v1/swap` take **`chainId` as a query parameter** (numeric EIP-155 id).\n  `/v1/{chain}/sources` takes a `{chain}` **path segment**: a canonical name (`ethereum`,\n  `base`, `arbitrum`, `bsc`, `avalanche`, `xlayer`), a common alias (`eth`, `bnb`, `avax`, \u2026)\n  or a numeric id. Chains not hosted by the instance return 404.\n- Every field name on every published route is **camelCase**.\n- Repeated query parameters use the unindexed form: `addresses=0x..&addresses=0x..`.\n- Errors are JSON `{\"error\": \"<message>\"}`. Branch on the status code, never on the message\n  text \u2014 there are no stable machine codes on errors. Success-path degradations DO carry\n  stable codes: see `warnings[]`.\n\n## Quote vs swap\n\n`/v1/quote` and `/v1/swap` share the exact same parameters and price identically; `/v1/swap`\nadditionally requires `taker`, firms up any RFQ legs with the makers, and returns an executable\n`transaction` (plus a `permit2` envelope when `funding=permit2`).\n\nThere is no quote-id handshake and **nothing is reserved** \u2014 a subsequent `/v1/swap` re-solves\nat its own block and can return a different number. If you intend to execute, call `/v1/swap`\ndirectly rather than pairing a `/v1/quote` with a later swap and expecting the same price.\n\n## What is not here\n\nThis document is the complete published surface. The engine serves further introspection\nroutes; they describe how the system is built rather than what we promise, are not a contract\nwe intend to keep stable, and are not available on the public host.\n",
    "contact": {
      "name": "Wraxyn",
      "email": "admin@wraxyn.io",
      "url": "https://wraxyn.io"
    }
  },
  "servers": [
    {
      "url": "https://exen.wraxyn.io",
      "description": "Public \u2014 no API key. Metered per IP."
    }
  ],
  "tags": [
    {
      "name": "trading",
      "description": "Price a trade and build the transaction"
    },
    {
      "name": "metadata",
      "description": "Which venues a chain can route through"
    },
    {
      "name": "ops",
      "description": "Is the API serving"
    }
  ],
  "security": [
    {},
    {
      "ApiKey": []
    }
  ],
  "paths": {
    "/v1/quote": {
      "get": {
        "tags": [
          "trading"
        ],
        "operationId": "getQuote",
        "summary": "Price a trade across every venue on the chain",
        "description": "Solves the best route for the pair and size at the current block and returns exact integer\namounts \u2014 produced by re-simulating the chosen plan in the same integer math the settlement\nenforces \u2014 plus the route breakdown with per-hop splits, gas, USD values, and the fee\nbreakdown if a fee applies.\n\nQuotes are **not reserved or stored**: a subsequent `/v1/swap` re-solves at its own block and\nmay return a different number. `blockNumber` is the block this solve was pinned at.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/chainId"
          },
          {
            "$ref": "#/components/parameters/mode"
          },
          {
            "$ref": "#/components/parameters/sellToken"
          },
          {
            "$ref": "#/components/parameters/buyToken"
          },
          {
            "$ref": "#/components/parameters/sellAmount"
          },
          {
            "$ref": "#/components/parameters/buyAmount"
          },
          {
            "$ref": "#/components/parameters/slippagePct"
          },
          {
            "$ref": "#/components/parameters/maxHops"
          },
          {
            "$ref": "#/components/parameters/gasPriceGwei"
          },
          {
            "$ref": "#/components/parameters/takerOptional"
          },
          {
            "$ref": "#/components/parameters/recipient"
          },
          {
            "$ref": "#/components/parameters/feeBps"
          },
          {
            "$ref": "#/components/parameters/feeRecipient"
          },
          {
            "$ref": "#/components/parameters/timeLimitMs"
          },
          {
            "$ref": "#/components/parameters/excludeRfq"
          },
          {
            "$ref": "#/components/parameters/excludeRwa"
          },
          {
            "$ref": "#/components/parameters/excludeVenues"
          },
          {
            "$ref": "#/components/parameters/includeVenues"
          }
        ],
        "responses": {
          "200": {
            "description": "Priced quote",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuoteResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/StaleState"
          }
        }
      }
    },
    "/v1/swap": {
      "get": {
        "tags": [
          "trading"
        ],
        "operationId": "getSwap",
        "summary": "Price a trade and return an executable transaction",
        "description": "Same pricing as `/v1/quote` (identical parameters, `taker` required), then:\n\n1. **RFQ firm-up** \u2014 if the chosen route contains RFQ market-maker legs and the time budget\n   admits them, each leg is firmed with its maker (bounded by the remaining budget, capped at\n   2 s) and the signed maker calldata replaces the indicative leg. Terminal firm legs re-price\n   the reported amounts to the signed values. On any firm-up failure the route is **re-solved\n   without those venues** and an `RFQ_FIRMUP_FAILED` warning is attached \u2014 the returned\n   transaction is always executable. Attestation-gated hook venues behave the same way, with\n   an `ANGSTROM_ATTESTATION_UNAVAILABLE` warning.\n2. **Encoding** \u2014 the route becomes a router plan; the transaction target and funding mode\n   follow the sell side (see the `Transaction` schema).\n\nThe response `transaction` can be submitted as-is for native and allowance-holder funding. For\n`funding=permit2` the client must first write its 65-byte EIP-712 signature into\n`transaction.data` at `permit2.signatureOffset`.\n\nThe swap enforces `minBuyAmount` on-chain; the transaction reverts past `deadline` (which is\nalso capped to the earliest RFQ maker expiry when RFQ legs are present). No `gasLimit` is\nreturned \u2014 run `eth_estimateGas` against `transaction` before sending. That is also your final\nvalidity check: a pool that moved or a maker quote that expired surfaces there as a revert\nrather than as a failed transaction you paid for.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/chainId"
          },
          {
            "$ref": "#/components/parameters/mode"
          },
          {
            "$ref": "#/components/parameters/sellToken"
          },
          {
            "$ref": "#/components/parameters/buyToken"
          },
          {
            "$ref": "#/components/parameters/sellAmount"
          },
          {
            "$ref": "#/components/parameters/buyAmount"
          },
          {
            "$ref": "#/components/parameters/slippagePct"
          },
          {
            "$ref": "#/components/parameters/maxHops"
          },
          {
            "$ref": "#/components/parameters/gasPriceGwei"
          },
          {
            "$ref": "#/components/parameters/takerRequired"
          },
          {
            "$ref": "#/components/parameters/recipient"
          },
          {
            "$ref": "#/components/parameters/userAddress"
          },
          {
            "$ref": "#/components/parameters/deadline"
          },
          {
            "$ref": "#/components/parameters/funding"
          },
          {
            "$ref": "#/components/parameters/feeBps"
          },
          {
            "$ref": "#/components/parameters/feeRecipient"
          },
          {
            "$ref": "#/components/parameters/timeLimitMs"
          },
          {
            "$ref": "#/components/parameters/excludeRfq"
          },
          {
            "$ref": "#/components/parameters/excludeRwa"
          },
          {
            "$ref": "#/components/parameters/excludeVenues"
          },
          {
            "$ref": "#/components/parameters/includeVenues"
          }
        ],
        "responses": {
          "200": {
            "description": "Quote + executable transaction",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SwapResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/StaleState"
          }
        }
      }
    },
    "/v1/{chain}/sources": {
      "get": {
        "tags": [
          "metadata"
        ],
        "operationId": "getSources",
        "summary": "Venue labels routable on a chain",
        "description": "Every venue label routable on this chain \u2014 the exact strings `route[].source` reports and\n`excludeVenues` / `includeVenues` accept. This is the only place those strings are enumerated,\nso read them from here rather than hard-coding a list that grows without you.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/chainPath"
          }
        ],
        "responses": {
          "200": {
            "description": "Venue list",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SourcesResponse"
                }
              }
            }
          },
          "404": {
            "description": "Chain not hosted (plain-text body)"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/healthz": {
      "get": {
        "tags": [
          "ops"
        ],
        "operationId": "healthz",
        "summary": "Liveness",
        "description": "`200` while the API is serving. `503` with `status: degraded` when a hosted chain's view of\nthe chain has fallen behind, which is the same condition that makes `/v1/quote` and `/v1/swap`\nanswer `503` for that chain.\n\nSuitable for a status page or an uptime check. It is not a per-chain coverage report: treat\n`status`, and each chain's `chain` and `healthy`, as the contract and ignore anything else in\nthe body.\n",
        "security": [
          {}
        ],
        "responses": {
          "200": {
            "description": "Live",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HealthResponse"
                }
              }
            }
          },
          "503": {
            "description": "A chain's committed head is stale",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HealthResponse"
                }
              }
            }
          }
        },
        "parameters": []
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKey": {
        "type": "apiKey",
        "in": "header",
        "name": "x-api-key",
        "description": "Partner API key. Optional \u2014 without one you are served at the anonymous rate (3 req/s). Sending\na key raises your rate budget to whatever it is provisioned for and applies your partner\nconfiguration. Request one from admin@wraxyn.io.\n"
      }
    },
    "parameters": {
      "chainId": {
        "name": "chainId",
        "in": "query",
        "required": true,
        "schema": {
          "type": "integer",
          "format": "int64"
        },
        "description": "EIP-155 chain id (e.g. `1`, `8453`). Must be hosted by the instance, else 404.",
        "example": 8453
      },
      "chainPath": {
        "name": "chain",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string"
        },
        "description": "Canonical chain name (`ethereum`, `base`, \u2026), alias (`eth`, `avax`, \u2026) or numeric id.",
        "example": "base"
      },
      "mode": {
        "name": "mode",
        "in": "query",
        "required": true,
        "schema": {
          "type": "string",
          "enum": [
            "sell",
            "buy"
          ]
        },
        "description": "`sell` = exact-in (fix `sellAmount`, maximize output). `buy` = exact-out (fix\n`buyAmount`, minimize input; the solver bisects the required input and certifies the\ntarget in exact integer math). Buys carry a STRICT delivery contract: the on-chain\nfloor is exactly the requested `buyAmount` \u2014 the recipient receives the target to the\nwei or the transaction reverts; `slippagePct` does not apply.\n"
      },
      "sellToken": {
        "name": "sellToken",
        "in": "query",
        "required": true,
        "schema": {
          "$ref": "#/components/schemas/Address"
        },
        "description": "Token to sell. Native token = the `0xee\u2026ee` sentinel."
      },
      "buyToken": {
        "name": "buyToken",
        "in": "query",
        "required": true,
        "schema": {
          "$ref": "#/components/schemas/Address"
        },
        "description": "Token to buy. Native token = the `0xee\u2026ee` sentinel. Must differ from `sellToken`."
      },
      "sellAmount": {
        "name": "sellAmount",
        "in": "query",
        "required": false,
        "schema": {
          "$ref": "#/components/schemas/Wei"
        },
        "description": "Exact-in input amount, decimal wei string. **Required when `mode=sell`.**"
      },
      "buyAmount": {
        "name": "buyAmount",
        "in": "query",
        "required": false,
        "schema": {
          "$ref": "#/components/schemas/Wei"
        },
        "description": "Exact-out target output, decimal wei string. **Required when `mode=buy`.**"
      },
      "slippagePct": {
        "name": "slippagePct",
        "in": "query",
        "required": false,
        "schema": {
          "type": "number",
          "minimum": 1e-05,
          "maximum": 50
        },
        "description": "Slippage tolerance in **percent** (`0.5` = 0.50%). Out-of-range values are clamped to\n`[0.00001, 50]`; default `0.5`. Determines `minBuyAmount` (the on-chain floor) for\n`mode=sell` only. **Ignored for `mode=buy`** (strict target; the response echoes\n`slippagePct: 0`).\n"
      },
      "maxHops": {
        "name": "maxHops",
        "in": "query",
        "required": false,
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 7
        },
        "description": "Hop-depth override, clamped to `[1, 7]`. Default scales with trade size in USD:\n2 hops (< $25k), 3 (>= $25k), 4 (>= $250k), 6 (>= $10M). When omitted, a request that\nfinds **no route at all** at the default depth automatically retries one hop deeper, up\nto 4, within the request's time budget \u2014 the response's `maxHops` reports the depth that\nactually produced the route. An explicit `maxHops` is honored verbatim and never escalated.\n"
      },
      "gasPriceGwei": {
        "name": "gasPriceGwei",
        "in": "query",
        "required": false,
        "schema": {
          "type": "number",
          "exclusiveMinimum": 0
        },
        "description": "Gas price (gwei) used to value route gas during solving and for `gasUsd`. Omit \u21d2 the\nchain's live feed (its head base fee, floored at the chain's minimum gas price \u2014 which\nis what prices chains that mine a zero base fee, e.g. BSC). If neither is available the\nsolve runs gas-`blind` (still quotes). Priority tips are not modelled: the submitter\nchooses the tip, so add it yourself when comparing net-of-gas.\n"
      },
      "takerOptional": {
        "name": "taker",
        "in": "query",
        "required": false,
        "schema": {
          "$ref": "#/components/schemas/Address"
        },
        "description": "Wallet holding/sending the input tokens. Optional on `/v1/quote` (echoed)."
      },
      "takerRequired": {
        "name": "taker",
        "in": "query",
        "required": true,
        "schema": {
          "$ref": "#/components/schemas/Address"
        },
        "description": "Wallet holding/sending the input tokens; the transaction's `from`. **Required on `/v1/swap`.**"
      },
      "recipient": {
        "name": "recipient",
        "in": "query",
        "required": false,
        "schema": {
          "$ref": "#/components/schemas/Address"
        },
        "description": "Wallet receiving the output. Defaults to `taker`."
      },
      "userAddress": {
        "name": "userAddress",
        "in": "query",
        "required": false,
        "schema": {
          "$ref": "#/components/schemas/Address"
        },
        "description": "The end user on whose behalf the swap is requested (`/v1/swap` only). Integrators whose\n`taker` is a shared settlement contract rather than the swapper's own wallet should set this\nto the real order owner.\n\nIt is applied per venue: sent to RFQ makers that meter per user identity, so one busy shared\ntaker does not trip a limit for your whole flow; makers that only screen keep the reliable\n`taker`. It never gates settlement \u2014 the router remains the on-chain executor \u2014 so the signed\nquote still settles regardless of this address. Defaults to `taker` when omitted. Ignored on\n`/v1/quote` (no RFQ firm-up occurs there).\n"
      },
      "excludeVenues": {
        "name": "excludeVenues",
        "in": "query",
        "required": false,
        "schema": {
          "type": "string"
        },
        "description": "Restrict routing to a subset of venues. Comma-separated venue **source labels** \u2014 the exact\n`source` strings the response reports (e.g. `dexalot,metric,uniswap-v3`; case-insensitive;\nlist them via `GET /v1/{chain}/sources`). `excludeVenues` routes through everything EXCEPT\nthese. **Mutually exclusive with `includeVenues`** \u2014 sending both is a 400. Default: all\nvenues. Unknown names are ignored. Hooked labels are hierarchical: excluding a family\n(`uniswap-v4`) also excludes its `uniswap-v4:*` variants, while excluding only a variant\n(`uniswap-v4:angstrom`) leaves the vanilla family routable. The synthetic native-wrap hop\n(`source: \"WRAP\"`) is not a venue and is never excludable. Applies to `/v1/quote` and\n`/v1/swap`; independent of `excludeRfq`/`excludeRwa` (those gate by class, this by venue).\nNote: a venue the engine has temporarily throttled (a maker that reported itself busy) is\ndropped from routing regardless of this param.\n"
      },
      "includeVenues": {
        "name": "includeVenues",
        "in": "query",
        "required": false,
        "schema": {
          "type": "string"
        },
        "description": "Comma-separated venue source labels to route through **exclusively** (all others dropped).\nMutually exclusive with `excludeVenues` (both \u2192 400). Same label semantics as\n`excludeVenues` (case-insensitive, hierarchical `family:variant` matching \u2014 including\n`uniswap-v4` admits its variants); the synthetic native-wrap hop (`\"WRAP\"`) is always\nadmitted, so native-token swaps keep working under any include list.\n"
      },
      "deadline": {
        "name": "deadline",
        "in": "query",
        "required": false,
        "schema": {
          "type": "integer",
          "format": "int64"
        },
        "description": "Unix-seconds transaction deadline (the router reverts after it). Default \u2248 now + 20\nminutes. When the route carries RFQ legs, the effective deadline is capped at the\nearliest maker-order expiry \u2014 the response reports the effective value in its\ntop-level `deadline` field (and the capping expiry in `rfqExpiry`).\n"
      },
      "funding": {
        "name": "funding",
        "in": "query",
        "required": false,
        "schema": {
          "type": "string",
          "enum": [
            "allowanceHolder",
            "permit2"
          ],
          "default": "allowanceHolder"
        },
        "description": "How the taker authorises the sell-token pull (ERC-20 sells only; native sells fund\nvia `value` and ignore this). `allowanceHolder` (default): a standard ERC-20 approval\nto `allowanceTarget`, tx targets the holder's `exec`. `permit2`: one-time approval to\ncanonical Permit2 + a per-swap EIP-712 signature spliced into the calldata (see the\n`permit2` response field).\n"
      },
      "feeBps": {
        "name": "feeBps",
        "in": "query",
        "required": false,
        "schema": {
          "type": "integer",
          "minimum": 0,
          "maximum": 1500
        },
        "description": "Integrator volume fee in basis points, taken on the **buy token**; `buyAmount` /\n`minBuyAmount` are returned **net** of it. Requires `feeRecipient`. Capped at 1500\n(15%). **Ignored** when the partner key carries a server-side fee override. The\nrequest-fee split is 85% to `feeRecipient` / 15% protocol.\n"
      },
      "feeRecipient": {
        "name": "feeRecipient",
        "in": "query",
        "required": false,
        "schema": {
          "$ref": "#/components/schemas/Address"
        },
        "description": "Where the integrator's fee slice is paid. Required when `feeBps` is set."
      },
      "timeLimitMs": {
        "name": "timeLimitMs",
        "in": "query",
        "required": false,
        "schema": {
          "type": "integer",
          "minimum": 50,
          "maximum": 5000
        },
        "description": "Pathfinding + quote budget in milliseconds, stamped at request arrival (queue time\ncounts). Below **300**, RFQ/API-quoted venues are excluded from routing entirely\n(their firm-up is too slow for the budget). **Unset \u21d2 the maximum (5000 ms) is\napplied** \u2014 no request runs unbounded. Enforcement is best-effort: direct routes are\nalways considered; deeper sampling stops at the deadline. Latency-sensitive callers\n(solvers) should set this explicitly.\n"
      },
      "excludeRfq": {
        "name": "excludeRfq",
        "in": "query",
        "required": false,
        "schema": {
          "type": "boolean",
          "default": false
        },
        "description": "When `true`, exclude RFQ **market-maker** venues and attestation-gated hook venues from\nrouting \u2014 every venue whose price comes from an off-chain quote rather than from on-chain\nstate. Does **not** exclude real-world-asset venues: use `excludeRwa` for those, or set both.\n\nIndependent of the time budget (a `timeLimitMs` below 300 separately drops all venues that\nquote out-of-band, RWA included).\n\nOn `/v1/quote` this yields a **firm**, non-optimistic quote \u2014 RFQ legs are otherwise priced\nfrom indicative maker ladders that `/v1/swap` may fail to firm up. On `/v1/swap` it guarantees\nno maker firm-up or attestation step. Default `false`.\n"
      },
      "excludeRwa": {
        "name": "excludeRwa",
        "in": "query",
        "required": false,
        "schema": {
          "type": "boolean",
          "default": false
        },
        "description": "When `true`, exclude real-world-asset venues (tokenized-equity mint/redeem) from routing,\n**independently** of `excludeRfq`. So a caller can keep RFQ crypto liquidity while dropping\nRWA, keep RWA while dropping RFQ, or set both to exclude all venues that price out-of-band.\nRWA venues price only for approved parties, so they are only routable for an API key that\ncarries that approval. Default `false`.\n"
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Invalid request \u2014 missing/inconsistent parameters, malformed amounts, identical\ntokens, out-of-range `timeLimitMs`, `feeBps` without `feeRecipient`, or a\n`routerAddress`/`allowanceHolderAddress` override (dev-only, rejected in production).\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorBody"
            }
          }
        }
      },
      "NotFound": {
        "description": "Unknown/unhosted chain, or no route exists for this pair + size.\n\nAlso returned with a **token-specific message** when a trade endpoint fails the on-chain\ntransfer-integrity measurement (2026-07-27): a transfer-gated token (honeypot/blacklist);\na token whose measured tax exceeds the routable cap (20%) and is therefore treated as a\ntrap; any fee-on-transfer token in `mode=buy` (exact-out cannot be honored against an\nowner-mutable tax); and a universal-scope or sell-only taxed token on the bought side.\nOrdinary fee-on-transfer *sells* are served instead, carrying a `FEE_ON_TRANSFER` warning.\nVerdicts are re-measured on-chain and owner-mutable, so a token can move between served\nand refused \u2014 key on the message, do not cache the verdict.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorBody"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Your caller identity exceeded its request budget. Identity is your API key when you send\na registered one, and your client IP otherwise \u2014 an unrecognised key is treated exactly\nlike no key, so rotating key values does not widen the budget.\n\nThe budget is a token bucket: a sustained rate plus a burst reservoir of\n`min(rps \u00d7 1.5, rps + 20)`, refilling continuously at the sustained rate. Bursting is\nfine; sustaining above the rate is not \u2014 the reservoir is a one-time allowance, not extra\nthroughput.\n\nAlways accompanied by `Retry-After` (seconds, never 0). `X-RateLimit-Limit` and\n`X-RateLimit-Remaining` are returned on **every** response, including successful ones, so\nyou can pace yourself rather than discovering the ceiling by hitting it.\n\nRetry after the indicated delay. Contact us for a higher tier \u2014 the limit travels with\nthe key, so a change is immediate and needs no work on your side.\n",
        "headers": {
          "Retry-After": {
            "schema": {
              "type": "integer",
              "minimum": 1
            },
            "description": "Seconds to wait before retrying."
          },
          "X-RateLimit-Limit": {
            "schema": {
              "type": "integer"
            },
            "description": "Your sustained requests-per-second budget."
          },
          "X-RateLimit-Remaining": {
            "schema": {
              "type": "integer"
            },
            "description": "Whole tokens left in your burst allowance."
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorBody"
            }
          }
        }
      },
      "InternalError": {
        "description": "We could not serve this one \u2014 a chain still coming up after a restart, or an internal failure\nbuilding the calldata. Retryable.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorBody"
            }
          }
        }
      },
      "StaleState": {
        "description": "Retryable-with-backoff. Two distinct causes share this status; the `code` field tells\nthem apart, and both mean \"ask again shortly\", never \"no liquidity\".\n\n**`code` absent \u2014 stale state.** The engine declined to price because its view of the\nchain is evidently stale (the committed head is older than the serving freshness bound).\n**This is not `no_route`** \u2014 the pair and size are probably fine, and the same request is\nexpected to succeed once the indexer catches up. Do NOT fall back to a cached quote: a\nprice derived from stale state is wrong, not merely old, and settling it loses the\ndifference (which is exactly why this response exists).\n\n**`code: \"overloaded\"` \u2014 load shedding.** Demand momentarily outran the solver, so the\nrequest was refused rather than admitted into a queue where it would have spent its\n`timeLimitMs` waiting and returned a worse route. Unlike `429` this is about us, not you:\nit does not count against your budget and a higher tier will not prevent it. Retry after\n`retryAfterSeconds`.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorBody"
            }
          }
        }
      }
    },
    "schemas": {
      "Address": {
        "type": "string",
        "pattern": "^0x[0-9a-fA-F]{40}$",
        "description": "20-byte EVM address, 0x-hex. Native token sentinel = `0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee`.",
        "examples": [
          "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"
        ]
      },
      "Wei": {
        "type": "string",
        "pattern": "^[0-9]+$",
        "description": "Token amount in base units (wei) as a decimal string \u2014 exact, no float precision loss.",
        "examples": [
          "2500000000000000000"
        ]
      },
      "HexData": {
        "type": "string",
        "pattern": "^0x[0-9a-fA-F]*$",
        "description": "0x-prefixed hex byte string."
      },
      "ErrorBody": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable message. Do not parse it \u2014 see `code` where present."
          },
          "code": {
            "type": "string",
            "enum": [
              "rate_limited",
              "overloaded"
            ],
            "description": "Stable machine code, present **only** on admission-control refusals \u2014 `rate_limited`\non `429`, `overloaded` on a shed `503`. Absent on every other error, where the status\nalone carries the meaning. Treat an absent `code` as \"not an admission refusal\", never\nas \"unknown error\".\n"
          },
          "retryAfterSeconds": {
            "type": "integer",
            "minimum": 1,
            "description": "Present alongside `code`; mirrors the `Retry-After` header for clients that find a\nbody field easier to reach than a header.\n"
          }
        }
      },
      "TradeMode": {
        "type": "string",
        "enum": [
          "sell",
          "buy"
        ]
      },
      "QuoteResponse": {
        "type": "object",
        "description": "A priced quote. `/v1/swap` returns this same body plus execution fields.",
        "required": [
          "quoteId",
          "chainId",
          "mode",
          "sellToken",
          "buyToken",
          "sellAmount",
          "buyAmount",
          "minBuyAmount",
          "slippagePct",
          "gas",
          "gasMode",
          "maxHops",
          "blockNumber",
          "route"
        ],
        "properties": {
          "quoteId": {
            "type": "string",
            "format": "uuid",
            "description": "Unique id for this quote. Also the high 16 bytes of the on-chain `tag` emitted in\nthe router's `Swap` event, so a settlement is attributable to this response.\n"
          },
          "chainId": {
            "type": "integer",
            "format": "int64"
          },
          "mode": {
            "$ref": "#/components/schemas/TradeMode"
          },
          "sellToken": {
            "$ref": "#/components/schemas/Address"
          },
          "buyToken": {
            "$ref": "#/components/schemas/Address"
          },
          "sellAmount": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Wei"
              }
            ],
            "description": "Input amount. `mode=sell`: echoes the request. `mode=buy`: the **solved** input \u2014\nthe maximum the taker funds; unused input is swept back by the router.\n"
          },
          "buyAmount": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Wei"
              }
            ],
            "description": "Output from exact integer re-simulation, **net of any fee** and **gross of gas**\n(gas is never deducted from the quote). `mode=buy`: **exactly** the requested\ntarget \u2014 delivery above it is surplus (captured by default; see `FeeBreakdown`\nfor who receives it), below it reverts.\n"
          },
          "minBuyAmount": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Wei"
              }
            ],
            "description": "Enforced on-chain as the output floor. `mode=sell`: `buyAmount` after the\nslippage tolerance. `mode=buy`: equal to `buyAmount` (the strict target).\n"
          },
          "slippagePct": {
            "type": "number",
            "description": "Slippage tolerance actually applied, percent (after clamping). Always `0` for\n`mode=buy` (buys have no slippage band).\n"
          },
          "amountInUsd": {
            "type": [
              "number",
              "null"
            ],
            "description": "USD value of the input at internal oracle prices; null if unpriced."
          },
          "amountOutUsd": {
            "type": [
              "number",
              "null"
            ],
            "description": "USD value of the output; null if unpriced."
          },
          "priceImpact": {
            "type": [
              "number",
              "null"
            ],
            "description": "`amountOutUsd / amountInUsd` \u2014 a **value-retention ratio**, not a percentage:\n`1.0` \u2248 no value lost, `0.98` \u2248 2% lost. Null when either side is unpriced.\n"
          },
          "gas": {
            "type": "integer",
            "format": "int64",
            "description": "Estimated total plan gas units (heuristic per-hop model; always present). Note:\nL2 calldata/data-availability fees are NOT included \u2014 L2 integrators should price\nthose separately.\n"
          },
          "gasPriceWei": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Gas price used to value gas, wei: the request override, else the chain's live feed\n(head base fee floored at the chain's minimum gas price). Excludes any priority tip\n\u2014 the submitter chooses that. May exceed 2^53; parse as a big integer where that\nmatters. Null if unknown.\n"
          },
          "gasUsd": {
            "type": [
              "number",
              "null"
            ],
            "description": "USD value of `gas` \u00d7 `gasPriceWei`; null when unpriced."
          },
          "gasMode": {
            "type": "string",
            "enum": [
              "blind",
              "guardrail",
              "prune"
            ],
            "description": "Gas strategy the solver used. `blind` = no gas pricing available (routes NOT\ngas-penalized); `guardrail`/`prune` = gas-aware solving.\n"
          },
          "maxHops": {
            "type": "integer",
            "description": "Hop depth used: the request override, or the trade-size default \u2014 possibly deepened\nby the automatic no-route escalation (see the `maxHops` request parameter), in which\ncase this reports the escalated depth that produced the route.\n"
          },
          "blockNumber": {
            "type": "integer",
            "format": "int64",
            "description": "Block the state snapshot was pinned at for this solve."
          },
          "fee": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/FeeBreakdown"
              },
              {
                "type": "null"
              }
            ],
            "description": "Fee taken on this swap; **omitted** when no fee applies. `buyAmount` is already net."
          },
          "taker": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Address"
              },
              {
                "type": "null"
              }
            ],
            "description": "Echo of the request `taker` (null on taker-less quotes)."
          },
          "recipient": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Address"
              },
              {
                "type": "null"
              }
            ],
            "description": "Output recipient (request `recipient`, defaulting to `taker`)."
          },
          "allowanceTarget": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Address"
              },
              {
                "type": "null"
              }
            ],
            "description": "Contract the taker must approve to spend `sellToken` before executing:\nthe allowance holder (default funding) or canonical Permit2 (`funding=permit2`).\n**Null for native-token sells** (funded via `value`, no approval).\n"
          },
          "route": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RouteHop"
            },
            "description": "The chosen route: every hop of every split. Hops with the same source token show\n`bps` splits of that token's balance. The synthetic `WRAP` hop (native \u21c4 wrapped)\nappears as protocol `WRAP` with a zero pool address.\n"
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SwapWarning"
            },
            "description": "Route-level advisories (2026-07-27). Omitted when empty. Today's only quote-level\ncode is `FEE_ON_TRANSFER` \u2014 a trade endpoint is a measured fee-on-transfer token\nand the quote models its tax (see the code's description). On `/v1/swap` these\nappear inside the flattened quote body, alongside (not merged with) the top-level\nbuild-time `warnings`.\n"
          }
        }
      },
      "RouteHop": {
        "type": "object",
        "required": [
          "poolAddress",
          "protocol",
          "source",
          "tokenIn",
          "tokenOut",
          "amountIn",
          "amountOut",
          "bps"
        ],
        "properties": {
          "poolAddress": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Address"
              }
            ],
            "description": "Pool contract (zero for the synthetic WRAP hop; the singleton for V4-class venues)."
          },
          "protocol": {
            "type": "string",
            "description": "Protocol family label (`uniswap-v2`, `uniswap-v3`, `curve-stable-ng`, `WRAP`, \u2026)."
          },
          "source": {
            "type": "string",
            "description": "Specific venue for the leg \u2014 refines `protocol` for hooked pools\n(`uniswap-v4:angstrom` vs `uniswap-v4`); equals `protocol` otherwise. Group\nper-venue volume analytics by this.\n"
          },
          "hook": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Address"
              },
              {
                "type": "null"
              }
            ],
            "description": "Uniswap-V4 hook address for hooked pools; null otherwise."
          },
          "tokenIn": {
            "$ref": "#/components/schemas/Address"
          },
          "tokenOut": {
            "$ref": "#/components/schemas/Address"
          },
          "amountIn": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Wei"
              }
            ],
            "description": "Exact planned input for this hop (integer re-simulation)."
          },
          "amountOut": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Wei"
              }
            ],
            "description": "Exact planned output for this hop (firm RFQ legs show the signed maker amount)."
          },
          "bps": {
            "type": "integer",
            "description": "Fraction of the source token's balance routed into this hop, basis points (10000 = all)."
          }
        }
      },
      "FeeBreakdown": {
        "type": "object",
        "description": "Volume-fee breakdown (amounts in **buy-token wei**, decimal strings). `buyAmount` is\nalready net of `feeAmount`. Positive slippage above the quote (surplus) is handled\nseparately on-chain and is never represented here: by default it is captured entirely\nto the protocol, but the destination is a per-integrator setting held against the API\nkey (it may instead be left with the taker, or shared) \u2014 it is not a request parameter.\n",
        "required": [
          "feeBps",
          "feeToken",
          "feeAmount",
          "partnerAmount",
          "protocolAmount",
          "partnerShareBps",
          "partner"
        ],
        "properties": {
          "feeBps": {
            "type": "integer",
            "description": "Total volume fee",
            "bps of the output.": null
          },
          "feeToken": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Address"
              }
            ],
            "description": "Always the buy token (fees are destination-side)."
          },
          "feeAmount": {
            "$ref": "#/components/schemas/Wei"
          },
          "partnerAmount": {
            "$ref": "#/components/schemas/Wei"
          },
          "protocolAmount": {
            "$ref": "#/components/schemas/Wei"
          },
          "partnerShareBps": {
            "type": "integer",
            "description": "Partner's share of the fee",
            "bps.": null
          },
          "partner": {
            "$ref": "#/components/schemas/Address"
          }
        }
      },
      "Transaction": {
        "type": "object",
        "description": "The executable transaction. Target depends on the funding mode \u2014\n**native sell**: `to` = ExenRouter, `value` = `sellAmount`;\n**ERC-20 + allowanceHolder** (default): `to` = allowance holder (`exec(...)`), `value` = 0;\n**ERC-20 + permit2**: `to` = ExenRouter, `value` = 0, signature splice required first.\nNo gas-limit estimate is provided \u2014 estimate with `eth_estimateGas` before sending.\n",
        "required": [
          "to",
          "data",
          "value"
        ],
        "properties": {
          "from": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Address"
              },
              {
                "type": "null"
              }
            ],
            "description": "The request `taker`."
          },
          "to": {
            "$ref": "#/components/schemas/Address"
          },
          "data": {
            "$ref": "#/components/schemas/HexData"
          },
          "value": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Wei"
              }
            ],
            "description": "Native value to attach (= `sellAmount` for native sells, else `0`)."
          }
        }
      },
      "Permit2Approval": {
        "type": "object",
        "description": "Present only when `funding=permit2`: the EIP-712 `PermitTransferFrom` the taker must\nsign. Write the 65-byte signature into `transaction.data` at byte `signatureOffset`\nbefore submitting. The permit's nonce is unordered and derived from `quoteId`\n(single-use); its deadline equals the swap deadline.\n",
        "required": [
          "type",
          "hash",
          "eip712",
          "signatureOffset",
          "signatureLength"
        ],
        "properties": {
          "type": {
            "type": "string",
            "const": "Permit2"
          },
          "hash": {
            "allOf": [
              {
                "$ref": "#/components/schemas/HexData"
              }
            ],
            "description": "The EIP-712 digest \u2014 must equal the hash of `eip712` computed client-side."
          },
          "eip712": {
            "type": "object",
            "description": "EIP-712 typed data (`types` / `domain` / `message` / `primaryType`) ready for `eth_signTypedData_v4`."
          },
          "signatureOffset": {
            "type": "integer",
            "description": "Byte offset into `transaction.data` (after 0x-decoding) where the 65 signature bytes go."
          },
          "signatureLength": {
            "type": "integer",
            "const": 65
          }
        }
      },
      "SwapWarning": {
        "type": "object",
        "required": [
          "code",
          "detail"
        ],
        "properties": {
          "code": {
            "type": "string",
            "enum": [
              "RFQ_FIRMUP_FAILED",
              "ANGSTROM_ATTESTATION_UNAVAILABLE",
              "STALE_STATE_RESOLVED",
              "FEE_ON_TRANSFER",
              "HIGH_PRICE_IMPACT"
            ],
            "description": "Stable machine code. **Every code leaves the returned transaction executable** \u2014 a warning\nrecords that the response degraded on the way, never that the calldata is unusable.\n\n- `RFQ_FIRMUP_FAILED` \u2014 an RFQ leg won routing but its maker firm-up failed; the returned\n  route is the AMM-only fallback re-solve. Also the main cause of a slow response with no\n  visible RFQ leg.\n- `ANGSTROM_ATTESTATION_UNAVAILABLE` \u2014 an attestation-gated hook leg could not fetch its\n  per-block attestation; same contract as above, an AMM-only re-solve.\n- `STALE_STATE_RESOLVED` \u2014 chain state committed while the swap was being built (RFQ firm-up\n  spends real wall time) left the originally chosen plan under its enforced minimum output;\n  it would have reverted. The returned route was re-solved against fresher state and certified.\n- `FEE_ON_TRANSFER` \u2014 a token in the trade charges a transfer fee. The quote already models\n  the measured tax plus a safety margin and the route settles through FoT-tolerant venue\n  entrypoints; realized output may exceed the quote, and a venue-side tax change can still\n  revert the fill at `minBuyAmount`. `detail` carries the modelled bps per side.\n"
          },
          "detail": {
            "type": "string",
            "description": "Human-readable reason."
          }
        }
      },
      "SwapResponse": {
        "allOf": [
          {
            "$ref": "#/components/schemas/QuoteResponse"
          },
          {
            "type": "object",
            "required": [
              "transaction",
              "deadline"
            ],
            "properties": {
              "transaction": {
                "$ref": "#/components/schemas/Transaction"
              },
              "deadline": {
                "type": "integer",
                "format": "int64",
                "description": "Unix-seconds validity horizon of `transaction` \u2014 the deadline **encoded in the calldata** (the\nrouter reverts past it). This is the *effective* deadline: the request's `deadline` (default\n\u2248 now + 20 minutes), capped at the earliest RFQ maker-order expiry when the route carries firm\nRFQ legs. Callers that delay submission \u2014 batchers, and solvers settling seconds after the\nquote \u2014 should read it instead of assuming the horizon they requested.\n"
              },
              "rfqExpiry": {
                "type": [
                  "integer",
                  "null"
                ],
                "format": "int64",
                "description": "Earliest maker-order expiry (unix seconds) among the route's firm RFQ legs;\nomitted when the route has no firm RFQ leg. When present it is what capped\n`deadline` (always \u2264 the requested horizon) \u2014 it tells the caller an RFQ leg\nconstrains this transaction's life, independent of their own `deadline` choice.\n"
              },
              "driftExposedShareBps": {
                "type": [
                  "integer",
                  "null"
                ],
                "minimum": 0,
                "maximum": 10000,
                "description": "Share of the gross output that can actually **drift** between quote and settlement, in bps\nof gross (`0` = fully pinned, `10000` = fully exposed). Output is pinned when its entire\nfunding path from your sell token is rate-deterministic: firm RFQ legs (signed constants),\nwrap/convert and ERC-4626 hops, the native wrap. Present only on `mode=sell` responses whose\nroute kept firm RFQ legs. Size your own `slippagePct` from it \u2014 a fully pinned route survives\nan arbitrarily tight floor, while an exposed route needs a drift budget on the exposed share.\n"
              },
              "permit2": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/Permit2Approval"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "Omitted unless `funding=permit2`."
              },
              "warnings": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/SwapWarning"
                },
                "description": "Degradations hit while building this swap (in addition to the quote-level\nadvisories carried inside the flattened quote body). Omitted when empty.\n"
              }
            }
          }
        ],
        "description": "`/v1/swap` body \u2014 the quote fields **flattened** at the top level (not nested) plus\nthe execution fields.\n"
      },
      "HealthResponse": {
        "type": "object",
        "required": [
          "status",
          "chains"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok",
              "degraded"
            ]
          },
          "chains": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "chain",
                "healthy"
              ],
              "properties": {
                "chain": {
                  "type": "string"
                },
                "healthy": {
                  "type": "boolean"
                }
              },
              "description": "Additional fields may be present; only `chain` and `healthy` are contractual."
            }
          }
        }
      },
      "SourcesResponse": {
        "type": "object",
        "description": "camelCase fields, same as `/v1/quote` and `/v1/swap` (2026-08-14).",
        "required": [
          "chainId",
          "chain",
          "sources"
        ],
        "properties": {
          "chainId": {
            "type": "integer",
            "format": "int64"
          },
          "chain": {
            "type": "string"
          },
          "sources": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Sorted venue labels (`uniswap-v3`, `curve-stable-ng`, `uniswap-v4:angstrom`, \u2026)."
          }
        }
      }
    }
  }
}
