<!-- Generated from the Exen API specification. Do not edit by hand. -->

# Exen Aggregation API — complete reference

> One file, the whole published contract: every endpoint, parameter, response field,
> error and warning. Written to be read in a single fetch by an agent or an LLM.
> Machine-readable equivalents: `/openapi.yaml`, `/openapi.json`. Version 1.0.0.

DEX-aggregation quote + swap API. One request prices a trade against every venue Exen indexes
on a chain — AMMs, concentrated liquidity, proprietary market makers and RFQ desks — splits it
across whichever combination pays best, and returns calldata that settles the whole route in a
single transaction through the Exen router. Built by Wraxyn.

## Getting started

One base URL for everyone: **`https://exen.wraxyn.io`**.

No signup to try it — anonymous callers get **3 requests/second**, enough to evaluate the API
and build against it. Every route works; nothing is held back behind a key.

Send an **`x-api-key`** header and that ceiling is replaced by whatever rate your key is
provisioned for. The key also carries your partner configuration — a negotiated fee policy
(which overrides any fee in the request), on-chain attribution of your fills, and access to
venues that price only for approved parties.

Keys are free. Email **admin@wraxyn.io** with what you are building, the chains you need and a
rough request rate.

## Conventions (read first)

- **All token amounts on the wire are decimal strings of base units (wei)** — never
  token-decimal floats. `"1500000"` = 1.5 USDC (6 decimals). Decimals vary by chain and are
  not guessable: BSC stablecoins are 18, not 6.
- **Addresses** are 0x-prefixed 20-byte hex, case-insensitive.
- **Native token** (ETH / BNB / AVAX / OKB) is the sentinel address
  `0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee` on both `sellToken` and `buyToken`.
- `/v1/quote` and `/v1/swap` take **`chainId` as a query parameter** (numeric EIP-155 id).
  `/v1/{chain}/sources` takes a `{chain}` **path segment**: a canonical name (`ethereum`,
  `base`, `arbitrum`, `bsc`, `avalanche`, `xlayer`), a common alias (`eth`, `bnb`, `avax`, …)
  or a numeric id. Chains not hosted by the instance return 404.
- Every field name on every published route is **camelCase**.
- Repeated query parameters use the unindexed form: `addresses=0x..&addresses=0x..`.
- Errors are JSON `{"error": "<message>"}`. Branch on the status code, never on the message
  text — there are no stable machine codes on errors. Success-path degradations DO carry
  stable codes: see `warnings[]`.

## Quote vs swap

`/v1/quote` and `/v1/swap` share the exact same parameters and price identically; `/v1/swap`
additionally requires `taker`, firms up any RFQ legs with the makers, and returns an executable
`transaction` (plus a `permit2` envelope when `funding=permit2`).

There is no quote-id handshake and **nothing is reserved** — a subsequent `/v1/swap` re-solves
at its own block and can return a different number. If you intend to execute, call `/v1/swap`
directly rather than pairing a `/v1/quote` with a later swap and expecting the same price.

## What is not here

This document is the complete published surface. The engine serves further introspection
routes; they describe how the system is built rather than what we promise, are not a contract
we intend to keep stable, and are not available on the public host.

## Endpoints

### GET /v1/quote

*Price a trade across every venue on the chain*

Solves the best route for the pair and size at the current block and returns exact integer
amounts — produced by re-simulating the chosen plan in the same integer math the settlement
enforces — plus the route breakdown with per-hop splits, gas, USD values, and the fee
breakdown if a fee applies.

Quotes are **not reserved or stored**: a subsequent `/v1/swap` re-solves at its own block and
may return a different number. `blockNumber` is the block this solve was pinned at.

| Parameter | In | Required | Type | Meaning |
|---|---|---|---|---|
| `chainId` | query | **yes** | integer (int64) | EIP-155 chain id (e.g. `1`, `8453`). Must be hosted by the instance, else 404. |
| `mode` | query | **yes** | `sell` \| `buy` | `sell` = exact-in (fix `sellAmount`, maximize output). `buy` = exact-out (fix `buyAmount`, minimize input; the solver bisects the required input and certifies the target in exact integer math). Buys carry a STRICT delivery contract: the on-chain floor is exactly the requested `buyAmount` — the recipient receives the target to the wei or the transaction reverts; `slippagePct` does not apply. |
| `sellToken` | query | **yes** | string | Token to sell. Native token = the `0xee…ee` sentinel. |
| `buyToken` | query | **yes** | string | Token to buy. Native token = the `0xee…ee` sentinel. Must differ from `sellToken`. |
| `sellAmount` | query | no | string | Exact-in input amount, decimal wei string. **Required when `mode=sell`.** |
| `buyAmount` | query | no | string | Exact-out target output, decimal wei string. **Required when `mode=buy`.** |
| `slippagePct` | query | no | number | Slippage tolerance in **percent** (`0.5` = 0.50%). Out-of-range values are clamped to `[0.00001, 50]`; default `0.5`. Determines `minBuyAmount` (the on-chain floor) for `mode=sell` only. **Ignored for `mode=buy`** (strict target; the response echoes `slippagePct: 0`). |
| `maxHops` | query | no | integer | Hop-depth override, clamped to `[1, 7]`. Default scales with trade size in USD: 2 hops (< $25k), 3 (>= $25k), 4 (>= $250k), 6 (>= $10M). When omitted, a request that finds **no route at all** at the default depth automatically retries one hop deeper, up to 4, within the request's time budget — the response's `maxHops` reports the depth that actually produced the route. An explicit `maxHops` is honored verbatim and never escalated. |
| `gasPriceGwei` | query | no | number | Gas price (gwei) used to value route gas during solving and for `gasUsd`. Omit ⇒ the chain's live feed (its head base fee, floored at the chain's minimum gas price — which is what prices chains that mine a zero base fee, e.g. BSC). If neither is available the solve runs gas-`blind` (still quotes). Priority tips are not modelled: the submitter chooses the tip, so add it yourself when comparing net-of-gas. |
| `taker` | query | no | string | Wallet holding/sending the input tokens. Optional on `/v1/quote` (echoed). |
| `recipient` | query | no | string | Wallet receiving the output. Defaults to `taker`. |
| `feeBps` | query | no | integer | Integrator volume fee in basis points, taken on the **buy token**; `buyAmount` / `minBuyAmount` are returned **net** of it. Requires `feeRecipient`. Capped at 1500 (15%). **Ignored** when the partner key carries a server-side fee override. The request-fee split is 85% to `feeRecipient` / 15% protocol. |
| `feeRecipient` | query | no | string | Where the integrator's fee slice is paid. Required when `feeBps` is set. |
| `timeLimitMs` | query | no | integer | Pathfinding + quote budget in milliseconds, stamped at request arrival (queue time counts). Below **300**, RFQ/API-quoted venues are excluded from routing entirely (their firm-up is too slow for the budget). **Unset ⇒ the maximum (5000 ms) is applied** — no request runs unbounded. Enforcement is best-effort: direct routes are always considered; deeper sampling stops at the deadline. Latency-sensitive callers (solvers) should set this explicitly. |
| `excludeRfq` | query | no | boolean | When `true`, exclude RFQ **market-maker** venues and attestation-gated hook venues from routing — every venue whose price comes from an off-chain quote rather than from on-chain state. Does **not** exclude real-world-asset venues: use `excludeRwa` for those, or set both. Independent of the time budget (a `timeLimitMs` below 300 separately drops all venues that quote out-of-band, RWA included). On `/v1/quote` this yields a **firm**, non-optimistic quote — RFQ legs are otherwise priced from indicative maker ladders that `/v1/swap` may fail to firm up. On `/v1/swap` it guarantees no maker firm-up or attestation step. Default `false`. |
| `excludeRwa` | query | no | boolean | When `true`, exclude real-world-asset venues (tokenized-equity mint/redeem) from routing, **independently** of `excludeRfq`. So a caller can keep RFQ crypto liquidity while dropping RWA, keep RWA while dropping RFQ, or set both to exclude all venues that price out-of-band. RWA venues price only for approved parties, so they are only routable for an API key that carries that approval. Default `false`. |
| `excludeVenues` | query | no | string | Restrict routing to a subset of venues. Comma-separated venue **source labels** — the exact `source` strings the response reports (e.g. `dexalot,metric,uniswap-v3`; case-insensitive; list them via `GET /v1/{chain}/sources`). `excludeVenues` routes through everything EXCEPT these. **Mutually exclusive with `includeVenues`** — sending both is a 400. Default: all venues. Unknown names are ignored. Hooked labels are hierarchical: excluding a family (`uniswap-v4`) also excludes its `uniswap-v4:*` variants, while excluding only a variant (`uniswap-v4:angstrom`) leaves the vanilla family routable. The synthetic native-wrap hop (`source: "WRAP"`) is not a venue and is never excludable. Applies to `/v1/quote` and `/v1/swap`; independent of `excludeRfq`/`excludeRwa` (those gate by class, this by venue). Note: a venue the engine has temporarily throttled (a maker that reported itself busy) is dropped from routing regardless of this param. |
| `includeVenues` | query | no | string | Comma-separated venue source labels to route through **exclusively** (all others dropped). Mutually exclusive with `excludeVenues` (both → 400). Same label semantics as `excludeVenues` (case-insensitive, hierarchical `family:variant` matching — including `uniswap-v4` admits its variants); the synthetic native-wrap hop (`"WRAP"`) is always admitted, so native-token swaps keep working under any include list. |

| Status | Meaning |
|---|---|
| `200` | Priced quote |
| `400` | Invalid request — missing/inconsistent parameters, malformed amounts, identical tokens, out-of-range `timeLimitMs`, `feeBps` without `feeRecipient`, or a `routerAddress`/`allowanceHolderAddress` override (dev-only, rejected in production). |
| `404` | Unknown/unhosted chain, or no route exists for this pair + size. Also returned with a **token-specific message** when a trade endpoint fails the on-chain transfer-integrity measurement (2026-07-27): a transfer-gated token (honeypot/blacklist); a token whose measured tax exceeds the routable cap (20%) and is therefore treated as a trap; any fee-on-transfer token in `mode=buy` (exact-out cannot be honored against an owner-mutable tax); and a universal-scope or sell-only taxed token on the bought side. Ordinary fee-on-transfer *sells* are served instead, carrying a `FEE_ON_TRANSFER` warning. Verdicts are re-measured on-chain and owner-mutable, so a token can move between served and refused — key on the message, do not cache the verdict. |
| `429` | Your caller identity exceeded its request budget. Identity is your API key when you send a registered one, and your client IP otherwise — an unrecognised key is treated exactly like no key, so rotating key values does not widen the budget. The budget is a token bucket: a sustained rate plus a burst reservoir of `min(rps × 1.5, rps + 20)`, refilling continuously at the sustained rate. Bursting is fine; sustaining above the rate is not — the reservoir is a one-time allowance, not extra throughput. Always accompanied by `Retry-After` (seconds, never 0). `X-RateLimit-Limit` and `X-RateLimit-Remaining` are returned on **every** response, including successful ones, so you can pace yourself rather than discovering the ceiling by hitting it. Retry after the indicated delay. Contact us for a higher tier — the limit travels with the key, so a change is immediate and needs no work on your side. |
| `500` | We could not serve this one — a chain still coming up after a restart, or an internal failure building the calldata. Retryable. |
| `503` | Retryable-with-backoff. Two distinct causes share this status; the `code` field tells them apart, and both mean "ask again shortly", never "no liquidity". **`code` absent — stale state.** The engine declined to price because its view of the chain is evidently stale (the committed head is older than the serving freshness bound). **This is not `no_route`** — the pair and size are probably fine, and the same request is expected to succeed once the indexer catches up. Do NOT fall back to a cached quote: a price derived from stale state is wrong, not merely old, and settling it loses the difference (which is exactly why this response exists). **`code: "overloaded"` — load shedding.** Demand momentarily outran the solver, so the request was refused rather than admitted into a queue where it would have spent its `timeLimitMs` waiting and returned a worse route. Unlike `429` this is about us, not you: it does not count against your budget and a higher tier will not prevent it. Retry after `retryAfterSeconds`. |

### GET /v1/swap

*Price a trade and return an executable transaction*

Same pricing as `/v1/quote` (identical parameters, `taker` required), then:

1. **RFQ firm-up** — if the chosen route contains RFQ market-maker legs and the time budget
   admits them, each leg is firmed with its maker (bounded by the remaining budget, capped at
   2 s) and the signed maker calldata replaces the indicative leg. Terminal firm legs re-price
   the reported amounts to the signed values. On any firm-up failure the route is **re-solved
   without those venues** and an `RFQ_FIRMUP_FAILED` warning is attached — the returned
   transaction is always executable. Attestation-gated hook venues behave the same way, with
   an `ANGSTROM_ATTESTATION_UNAVAILABLE` warning.
2. **Encoding** — the route becomes a router plan; the transaction target and funding mode
   follow the sell side (see the `Transaction` schema).

The response `transaction` can be submitted as-is for native and allowance-holder funding. For
`funding=permit2` the client must first write its 65-byte EIP-712 signature into
`transaction.data` at `permit2.signatureOffset`.

The swap enforces `minBuyAmount` on-chain; the transaction reverts past `deadline` (which is
also capped to the earliest RFQ maker expiry when RFQ legs are present). No `gasLimit` is
returned — run `eth_estimateGas` against `transaction` before sending. That is also your final
validity check: a pool that moved or a maker quote that expired surfaces there as a revert
rather than as a failed transaction you paid for.

| Parameter | In | Required | Type | Meaning |
|---|---|---|---|---|
| `chainId` | query | **yes** | integer (int64) | EIP-155 chain id (e.g. `1`, `8453`). Must be hosted by the instance, else 404. |
| `mode` | query | **yes** | `sell` \| `buy` | `sell` = exact-in (fix `sellAmount`, maximize output). `buy` = exact-out (fix `buyAmount`, minimize input; the solver bisects the required input and certifies the target in exact integer math). Buys carry a STRICT delivery contract: the on-chain floor is exactly the requested `buyAmount` — the recipient receives the target to the wei or the transaction reverts; `slippagePct` does not apply. |
| `sellToken` | query | **yes** | string | Token to sell. Native token = the `0xee…ee` sentinel. |
| `buyToken` | query | **yes** | string | Token to buy. Native token = the `0xee…ee` sentinel. Must differ from `sellToken`. |
| `sellAmount` | query | no | string | Exact-in input amount, decimal wei string. **Required when `mode=sell`.** |
| `buyAmount` | query | no | string | Exact-out target output, decimal wei string. **Required when `mode=buy`.** |
| `slippagePct` | query | no | number | Slippage tolerance in **percent** (`0.5` = 0.50%). Out-of-range values are clamped to `[0.00001, 50]`; default `0.5`. Determines `minBuyAmount` (the on-chain floor) for `mode=sell` only. **Ignored for `mode=buy`** (strict target; the response echoes `slippagePct: 0`). |
| `maxHops` | query | no | integer | Hop-depth override, clamped to `[1, 7]`. Default scales with trade size in USD: 2 hops (< $25k), 3 (>= $25k), 4 (>= $250k), 6 (>= $10M). When omitted, a request that finds **no route at all** at the default depth automatically retries one hop deeper, up to 4, within the request's time budget — the response's `maxHops` reports the depth that actually produced the route. An explicit `maxHops` is honored verbatim and never escalated. |
| `gasPriceGwei` | query | no | number | Gas price (gwei) used to value route gas during solving and for `gasUsd`. Omit ⇒ the chain's live feed (its head base fee, floored at the chain's minimum gas price — which is what prices chains that mine a zero base fee, e.g. BSC). If neither is available the solve runs gas-`blind` (still quotes). Priority tips are not modelled: the submitter chooses the tip, so add it yourself when comparing net-of-gas. |
| `taker` | query | **yes** | string | Wallet holding/sending the input tokens; the transaction's `from`. **Required on `/v1/swap`.** |
| `recipient` | query | no | string | Wallet receiving the output. Defaults to `taker`. |
| `userAddress` | query | no | string | The end user on whose behalf the swap is requested (`/v1/swap` only). Integrators whose `taker` is a shared settlement contract rather than the swapper's own wallet should set this to the real order owner. It is applied per venue: sent to RFQ makers that meter per user identity, so one busy shared taker does not trip a limit for your whole flow; makers that only screen keep the reliable `taker`. It never gates settlement — the router remains the on-chain executor — so the signed quote still settles regardless of this address. Defaults to `taker` when omitted. Ignored on `/v1/quote` (no RFQ firm-up occurs there). |
| `deadline` | query | no | integer (int64) | Unix-seconds transaction deadline (the router reverts after it). Default ≈ now + 20 minutes. When the route carries RFQ legs, the effective deadline is capped at the earliest maker-order expiry — the response reports the effective value in its top-level `deadline` field (and the capping expiry in `rfqExpiry`). |
| `funding` | query | no | `allowanceHolder` \| `permit2` | How the taker authorises the sell-token pull (ERC-20 sells only; native sells fund via `value` and ignore this). `allowanceHolder` (default): a standard ERC-20 approval to `allowanceTarget`, tx targets the holder's `exec`. `permit2`: one-time approval to canonical Permit2 + a per-swap EIP-712 signature spliced into the calldata (see the `permit2` response field). |
| `feeBps` | query | no | integer | Integrator volume fee in basis points, taken on the **buy token**; `buyAmount` / `minBuyAmount` are returned **net** of it. Requires `feeRecipient`. Capped at 1500 (15%). **Ignored** when the partner key carries a server-side fee override. The request-fee split is 85% to `feeRecipient` / 15% protocol. |
| `feeRecipient` | query | no | string | Where the integrator's fee slice is paid. Required when `feeBps` is set. |
| `timeLimitMs` | query | no | integer | Pathfinding + quote budget in milliseconds, stamped at request arrival (queue time counts). Below **300**, RFQ/API-quoted venues are excluded from routing entirely (their firm-up is too slow for the budget). **Unset ⇒ the maximum (5000 ms) is applied** — no request runs unbounded. Enforcement is best-effort: direct routes are always considered; deeper sampling stops at the deadline. Latency-sensitive callers (solvers) should set this explicitly. |
| `excludeRfq` | query | no | boolean | When `true`, exclude RFQ **market-maker** venues and attestation-gated hook venues from routing — every venue whose price comes from an off-chain quote rather than from on-chain state. Does **not** exclude real-world-asset venues: use `excludeRwa` for those, or set both. Independent of the time budget (a `timeLimitMs` below 300 separately drops all venues that quote out-of-band, RWA included). On `/v1/quote` this yields a **firm**, non-optimistic quote — RFQ legs are otherwise priced from indicative maker ladders that `/v1/swap` may fail to firm up. On `/v1/swap` it guarantees no maker firm-up or attestation step. Default `false`. |
| `excludeRwa` | query | no | boolean | When `true`, exclude real-world-asset venues (tokenized-equity mint/redeem) from routing, **independently** of `excludeRfq`. So a caller can keep RFQ crypto liquidity while dropping RWA, keep RWA while dropping RFQ, or set both to exclude all venues that price out-of-band. RWA venues price only for approved parties, so they are only routable for an API key that carries that approval. Default `false`. |
| `excludeVenues` | query | no | string | Restrict routing to a subset of venues. Comma-separated venue **source labels** — the exact `source` strings the response reports (e.g. `dexalot,metric,uniswap-v3`; case-insensitive; list them via `GET /v1/{chain}/sources`). `excludeVenues` routes through everything EXCEPT these. **Mutually exclusive with `includeVenues`** — sending both is a 400. Default: all venues. Unknown names are ignored. Hooked labels are hierarchical: excluding a family (`uniswap-v4`) also excludes its `uniswap-v4:*` variants, while excluding only a variant (`uniswap-v4:angstrom`) leaves the vanilla family routable. The synthetic native-wrap hop (`source: "WRAP"`) is not a venue and is never excludable. Applies to `/v1/quote` and `/v1/swap`; independent of `excludeRfq`/`excludeRwa` (those gate by class, this by venue). Note: a venue the engine has temporarily throttled (a maker that reported itself busy) is dropped from routing regardless of this param. |
| `includeVenues` | query | no | string | Comma-separated venue source labels to route through **exclusively** (all others dropped). Mutually exclusive with `excludeVenues` (both → 400). Same label semantics as `excludeVenues` (case-insensitive, hierarchical `family:variant` matching — including `uniswap-v4` admits its variants); the synthetic native-wrap hop (`"WRAP"`) is always admitted, so native-token swaps keep working under any include list. |

| Status | Meaning |
|---|---|
| `200` | Quote + executable transaction |
| `400` | Invalid request — missing/inconsistent parameters, malformed amounts, identical tokens, out-of-range `timeLimitMs`, `feeBps` without `feeRecipient`, or a `routerAddress`/`allowanceHolderAddress` override (dev-only, rejected in production). |
| `404` | Unknown/unhosted chain, or no route exists for this pair + size. Also returned with a **token-specific message** when a trade endpoint fails the on-chain transfer-integrity measurement (2026-07-27): a transfer-gated token (honeypot/blacklist); a token whose measured tax exceeds the routable cap (20%) and is therefore treated as a trap; any fee-on-transfer token in `mode=buy` (exact-out cannot be honored against an owner-mutable tax); and a universal-scope or sell-only taxed token on the bought side. Ordinary fee-on-transfer *sells* are served instead, carrying a `FEE_ON_TRANSFER` warning. Verdicts are re-measured on-chain and owner-mutable, so a token can move between served and refused — key on the message, do not cache the verdict. |
| `429` | Your caller identity exceeded its request budget. Identity is your API key when you send a registered one, and your client IP otherwise — an unrecognised key is treated exactly like no key, so rotating key values does not widen the budget. The budget is a token bucket: a sustained rate plus a burst reservoir of `min(rps × 1.5, rps + 20)`, refilling continuously at the sustained rate. Bursting is fine; sustaining above the rate is not — the reservoir is a one-time allowance, not extra throughput. Always accompanied by `Retry-After` (seconds, never 0). `X-RateLimit-Limit` and `X-RateLimit-Remaining` are returned on **every** response, including successful ones, so you can pace yourself rather than discovering the ceiling by hitting it. Retry after the indicated delay. Contact us for a higher tier — the limit travels with the key, so a change is immediate and needs no work on your side. |
| `500` | We could not serve this one — a chain still coming up after a restart, or an internal failure building the calldata. Retryable. |
| `503` | Retryable-with-backoff. Two distinct causes share this status; the `code` field tells them apart, and both mean "ask again shortly", never "no liquidity". **`code` absent — stale state.** The engine declined to price because its view of the chain is evidently stale (the committed head is older than the serving freshness bound). **This is not `no_route`** — the pair and size are probably fine, and the same request is expected to succeed once the indexer catches up. Do NOT fall back to a cached quote: a price derived from stale state is wrong, not merely old, and settling it loses the difference (which is exactly why this response exists). **`code: "overloaded"` — load shedding.** Demand momentarily outran the solver, so the request was refused rather than admitted into a queue where it would have spent its `timeLimitMs` waiting and returned a worse route. Unlike `429` this is about us, not you: it does not count against your budget and a higher tier will not prevent it. Retry after `retryAfterSeconds`. |

### GET /v1/{chain}/sources

*Venue labels routable on a chain*

Every venue label routable on this chain — the exact strings `route[].source` reports and
`excludeVenues` / `includeVenues` accept. This is the only place those strings are enumerated,
so read them from here rather than hard-coding a list that grows without you.

| Parameter | In | Required | Type | Meaning |
|---|---|---|---|---|
| `chain` | path | **yes** | string | Canonical chain name (`ethereum`, `base`, …), alias (`eth`, `avax`, …) or numeric id. |

| Status | Meaning |
|---|---|
| `200` | Venue list |
| `404` | Chain not hosted (plain-text body) |
| `429` | Your caller identity exceeded its request budget. Identity is your API key when you send a registered one, and your client IP otherwise — an unrecognised key is treated exactly like no key, so rotating key values does not widen the budget. The budget is a token bucket: a sustained rate plus a burst reservoir of `min(rps × 1.5, rps + 20)`, refilling continuously at the sustained rate. Bursting is fine; sustaining above the rate is not — the reservoir is a one-time allowance, not extra throughput. Always accompanied by `Retry-After` (seconds, never 0). `X-RateLimit-Limit` and `X-RateLimit-Remaining` are returned on **every** response, including successful ones, so you can pace yourself rather than discovering the ceiling by hitting it. Retry after the indicated delay. Contact us for a higher tier — the limit travels with the key, so a change is immediate and needs no work on your side. |

### GET /healthz

*Liveness*

`200` while the API is serving. `503` with `status: degraded` when a hosted chain's view of
the chain has fallen behind, which is the same condition that makes `/v1/quote` and `/v1/swap`
answer `503` for that chain.

Suitable for a status page or an uptime check. It is not a per-chain coverage report: treat
`status`, and each chain's `chain` and `healthy`, as the contract and ignore anything else in
the body.

| Status | Meaning |
|---|---|
| `200` | Live |
| `503` | A chain's committed head is stale |

## Schemas

### Address

20-byte EVM address, 0x-hex. Native token sentinel = `0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee`.

Type: string

Pattern: `^0x[0-9a-fA-F]{40}$`

### Wei

Token amount in base units (wei) as a decimal string — exact, no float precision loss.

Type: string

Pattern: `^[0-9]+$`

### HexData

0x-prefixed hex byte string.

Type: string

Pattern: `^0x[0-9a-fA-F]*$`

### ErrorBody

| Field | Type | Required | Meaning |
|---|---|---|---|
| `error` | string | **yes** | Human-readable message. Do not parse it — see `code` where present. |
| `code` | `rate_limited` \| `overloaded` | no | Stable machine code, present **only** on admission-control refusals — `rate_limited` on `429`, `overloaded` on a shed `503`. Absent on every other error, where the status alone carries the meaning. Treat an absent `code` as "not an admission refusal", never as "unknown error". |
| `retryAfterSeconds` | integer | no | Present alongside `code`; mirrors the `Retry-After` header for clients that find a body field easier to reach than a header. |

### TradeMode

Type: `sell` | `buy`

### QuoteResponse

A priced quote. `/v1/swap` returns this same body plus execution fields.

| Field | Type | Required | Meaning |
|---|---|---|---|
| `quoteId` | string (uuid) | **yes** | Unique id for this quote. Also the high 16 bytes of the on-chain `tag` emitted in the router's `Swap` event, so a settlement is attributable to this response. |
| `chainId` | integer (int64) | **yes** |  |
| `mode` | `sell` \| `buy` | **yes** |  |
| `sellToken` | string | **yes** |  |
| `buyToken` | string | **yes** |  |
| `sellAmount` | string | **yes** | Input amount. `mode=sell`: echoes the request. `mode=buy`: the **solved** input — the maximum the taker funds; unused input is swept back by the router. |
| `buyAmount` | string | **yes** | Output from exact integer re-simulation, **net of any fee** and **gross of gas** (gas is never deducted from the quote). `mode=buy`: **exactly** the requested target — delivery above it is surplus (captured by default; see `FeeBreakdown` for who receives it), below it reverts. |
| `minBuyAmount` | string | **yes** | Enforced on-chain as the output floor. `mode=sell`: `buyAmount` after the slippage tolerance. `mode=buy`: equal to `buyAmount` (the strict target). |
| `slippagePct` | number | **yes** | Slippage tolerance actually applied, percent (after clamping). Always `0` for `mode=buy` (buys have no slippage band). |
| `amountInUsd` | number \| null | no | USD value of the input at internal oracle prices; null if unpriced. |
| `amountOutUsd` | number \| null | no | USD value of the output; null if unpriced. |
| `priceImpact` | number \| null | no | `amountOutUsd / amountInUsd` — a **value-retention ratio**, not a percentage: `1.0` ≈ no value lost, `0.98` ≈ 2% lost. Null when either side is unpriced. |
| `gas` | integer (int64) | **yes** | Estimated total plan gas units (heuristic per-hop model; always present). Note: L2 calldata/data-availability fees are NOT included — L2 integrators should price those separately. |
| `gasPriceWei` | integer \| null | no | Gas price used to value gas, wei: the request override, else the chain's live feed (head base fee floored at the chain's minimum gas price). Excludes any priority tip — the submitter chooses that. May exceed 2^53; parse as a big integer where that matters. Null if unknown. |
| `gasUsd` | number \| null | no | USD value of `gas` × `gasPriceWei`; null when unpriced. |
| `gasMode` | `blind` \| `guardrail` \| `prune` | **yes** | Gas strategy the solver used. `blind` = no gas pricing available (routes NOT gas-penalized); `guardrail`/`prune` = gas-aware solving. |
| `maxHops` | integer | **yes** | Hop depth used: the request override, or the trade-size default — possibly deepened by the automatic no-route escalation (see the `maxHops` request parameter), in which case this reports the escalated depth that produced the route. |
| `blockNumber` | integer (int64) | **yes** | Block the state snapshot was pinned at for this solve. |
| `fee` | object \| null | no | Fee taken on this swap; **omitted** when no fee applies. `buyAmount` is already net. |
| `taker` | string \| null | no | Echo of the request `taker` (null on taker-less quotes). |
| `recipient` | string \| null | no | Output recipient (request `recipient`, defaulting to `taker`). |
| `allowanceTarget` | string \| null | no | Contract the taker must approve to spend `sellToken` before executing: the allowance holder (default funding) or canonical Permit2 (`funding=permit2`). **Null for native-token sells** (funded via `value`, no approval). |
| `route` | array<object> | **yes** | The chosen route: every hop of every split. Hops with the same source token show `bps` splits of that token's balance. The synthetic `WRAP` hop (native ⇄ wrapped) appears as protocol `WRAP` with a zero pool address. |
| `warnings` | array<object> | no | Route-level advisories (2026-07-27). Omitted when empty. Today's only quote-level code is `FEE_ON_TRANSFER` — a trade endpoint is a measured fee-on-transfer token and the quote models its tax (see the code's description). On `/v1/swap` these appear inside the flattened quote body, alongside (not merged with) the top-level build-time `warnings`. |

### RouteHop

| Field | Type | Required | Meaning |
|---|---|---|---|
| `poolAddress` | string | **yes** | Pool contract (zero for the synthetic WRAP hop; the singleton for V4-class venues). |
| `protocol` | string | **yes** | Protocol family label (`uniswap-v2`, `uniswap-v3`, `curve-stable-ng`, `WRAP`, …). |
| `source` | string | **yes** | Specific venue for the leg — refines `protocol` for hooked pools (`uniswap-v4:angstrom` vs `uniswap-v4`); equals `protocol` otherwise. Group per-venue volume analytics by this. |
| `hook` | string \| null | no | Uniswap-V4 hook address for hooked pools; null otherwise. |
| `tokenIn` | string | **yes** |  |
| `tokenOut` | string | **yes** |  |
| `amountIn` | string | **yes** | Exact planned input for this hop (integer re-simulation). |
| `amountOut` | string | **yes** | Exact planned output for this hop (firm RFQ legs show the signed maker amount). |
| `bps` | integer | **yes** | Fraction of the source token's balance routed into this hop, basis points (10000 = all). |

### FeeBreakdown

Volume-fee breakdown (amounts in **buy-token wei**, decimal strings). `buyAmount` is
already net of `feeAmount`. Positive slippage above the quote (surplus) is handled
separately on-chain and is never represented here: by default it is captured entirely
to the protocol, but the destination is a per-integrator setting held against the API
key (it may instead be left with the taker, or shared) — it is not a request parameter.

| Field | Type | Required | Meaning |
|---|---|---|---|
| `feeBps` | integer | **yes** | Total volume fee |
| `feeToken` | string | **yes** | Always the buy token (fees are destination-side). |
| `feeAmount` | string | **yes** |  |
| `partnerAmount` | string | **yes** |  |
| `protocolAmount` | string | **yes** |  |
| `partnerShareBps` | integer | **yes** | Partner's share of the fee |
| `partner` | string | **yes** |  |

### Transaction

The executable transaction. Target depends on the funding mode —
**native sell**: `to` = ExenRouter, `value` = `sellAmount`;
**ERC-20 + allowanceHolder** (default): `to` = allowance holder (`exec(...)`), `value` = 0;
**ERC-20 + permit2**: `to` = ExenRouter, `value` = 0, signature splice required first.
No gas-limit estimate is provided — estimate with `eth_estimateGas` before sending.

| Field | Type | Required | Meaning |
|---|---|---|---|
| `from` | string \| null | no | The request `taker`. |
| `to` | string | **yes** |  |
| `data` | string | **yes** |  |
| `value` | string | **yes** | Native value to attach (= `sellAmount` for native sells, else `0`). |

### Permit2Approval

Present only when `funding=permit2`: the EIP-712 `PermitTransferFrom` the taker must
sign. Write the 65-byte signature into `transaction.data` at byte `signatureOffset`
before submitting. The permit's nonce is unordered and derived from `quoteId`
(single-use); its deadline equals the swap deadline.

| Field | Type | Required | Meaning |
|---|---|---|---|
| `type` | `Permit2` | **yes** |  |
| `hash` | string | **yes** | The EIP-712 digest — must equal the hash of `eip712` computed client-side. |
| `eip712` | object | **yes** | EIP-712 typed data (`types` / `domain` / `message` / `primaryType`) ready for `eth_signTypedData_v4`. |
| `signatureOffset` | integer | **yes** | Byte offset into `transaction.data` (after 0x-decoding) where the 65 signature bytes go. |
| `signatureLength` | `65` | **yes** |  |

### SwapWarning

| Field | Type | Required | Meaning |
|---|---|---|---|
| `code` | `RFQ_FIRMUP_FAILED` \| `ANGSTROM_ATTESTATION_UNAVAILABLE` \| `STALE_STATE_RESOLVED` \| `FEE_ON_TRANSFER` \| `HIGH_PRICE_IMPACT` | **yes** | Stable machine code. **Every code leaves the returned transaction executable** — a warning records that the response degraded on the way, never that the calldata is unusable. - `RFQ_FIRMUP_FAILED` — an RFQ leg won routing but its maker firm-up failed; the returned route is the AMM-only fallback re-solve. Also the main cause of a slow response with no visible RFQ leg. - `ANGSTROM_ATTESTATION_UNAVAILABLE` — an attestation-gated hook leg could not fetch its per-block attestation; same contract as above, an AMM-only re-solve. - `STALE_STATE_RESOLVED` — chain state committed while the swap was being built (RFQ firm-up spends real wall time) left the originally chosen plan under its enforced minimum output; it would have reverted. The returned route was re-solved against fresher state and certified. - `FEE_ON_TRANSFER` — a token in the trade charges a transfer fee. The quote already models the measured tax plus a safety margin and the route settles through FoT-tolerant venue entrypoints; realized output may exceed the quote, and a venue-side tax change can still revert the fill at `minBuyAmount`. `detail` carries the modelled bps per side. |
| `detail` | string | **yes** | Human-readable reason. |

### SwapResponse

`/v1/swap` body — the quote fields **flattened** at the top level (not nested) plus
the execution fields.

All fields of `QuoteResponse`, flattened at the top level, plus:

| Field | Type | Required | Meaning |
|---|---|---|---|
| `transaction` | object | **yes** |  |
| `deadline` | integer (int64) | **yes** | Unix-seconds validity horizon of `transaction` — the deadline **encoded in the calldata** (the router reverts past it). This is the *effective* deadline: the request's `deadline` (default ≈ now + 20 minutes), capped at the earliest RFQ maker-order expiry when the route carries firm RFQ legs. Callers that delay submission — batchers, and solvers settling seconds after the quote — should read it instead of assuming the horizon they requested. |
| `rfqExpiry` | integer \| null (int64) | no | Earliest maker-order expiry (unix seconds) among the route's firm RFQ legs; omitted when the route has no firm RFQ leg. When present it is what capped `deadline` (always ≤ the requested horizon) — it tells the caller an RFQ leg constrains this transaction's life, independent of their own `deadline` choice. |
| `driftExposedShareBps` | integer \| null | no | Share of the gross output that can actually **drift** between quote and settlement, in bps of gross (`0` = fully pinned, `10000` = fully exposed). Output is pinned when its entire funding path from your sell token is rate-deterministic: firm RFQ legs (signed constants), wrap/convert and ERC-4626 hops, the native wrap. Present only on `mode=sell` responses whose route kept firm RFQ legs. Size your own `slippagePct` from it — a fully pinned route survives an arbitrarily tight floor, while an exposed route needs a drift budget on the exposed share. |
| `permit2` | object \| null | no | Omitted unless `funding=permit2`. |
| `warnings` | array<object> | no | Degradations hit while building this swap (in addition to the quote-level advisories carried inside the flattened quote body). Omitted when empty. |

### HealthResponse

| Field | Type | Required | Meaning |
|---|---|---|---|
| `status` | `ok` \| `degraded` | **yes** |  |
| `chains` | array<object> | **yes** |  |

### SourcesResponse

camelCase fields, same as `/v1/quote` and `/v1/swap` (2026-08-14).

| Field | Type | Required | Meaning |
|---|---|---|---|
| `chainId` | integer (int64) | **yes** |  |
| `chain` | string | **yes** |  |
| `sources` | array<string> | **yes** | Sorted venue labels (`uniswap-v3`, `curve-stable-ng`, `uniswap-v4:angstrom`, …). |

## Authentication

Header `x-api-key`.

Partner API key. Optional — without one you are served at the anonymous rate (3 req/s). Sending
a key raises your rate budget to whatever it is provisioned for and applies your partner
configuration. Request one from admin@wraxyn.io.

