# Exen aggregation API — public OpenAPI 3.1 specification.
#
# GENERATED from the Exen API specification. Do not edit this file by hand: it is overwritten
# on every regeneration, and the engine is the source of truth for the wire contract.
# Questions, or a partner key: admin@wraxyn.io
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
    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.
  contact:
    name: Wraxyn
    email: admin@wraxyn.io
    url: https://wraxyn.io
servers:
- url: https://exen.wraxyn.io
  description: Public — 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
        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.
      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:

        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.
      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 — 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.
      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
        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.
      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 — 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.
  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`, …), alias (`eth`, `avax`, …) 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
        `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:
      name: sellToken
      in: query
      required: true
      schema:
        $ref: '#/components/schemas/Address'
      description: Token to sell. Native token = the `0xee…ee` sentinel.
    buyToken:
      name: buyToken
      in: query
      required: true
      schema:
        $ref: '#/components/schemas/Address'
      description: Token to buy. Native token = the `0xee…ee` 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: 1.0e-05
        maximum: 50
      description: |
        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:
      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:
        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:
      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 ⇒ 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.
    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
        `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).
    excludeVenues:
      name: excludeVenues
      in: query
      required: false
      schema:
        type: string
      description: |
        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:
      name: includeVenues
      in: query
      required: false
      schema:
        type: string
      description: |
        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.
    deadline:
      name: deadline
      in: query
      required: false
      schema:
        type: integer
        format: int64
      description: |
        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:
      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
        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:
      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` /
        `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:
      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
        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:
      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
        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:
      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,
        **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`.
  responses:
    BadRequest:
      description: |
        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).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorBody'
    NotFound:
      description: |
        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.
      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
        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.
      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 — a chain still coming up after a restart, or an internal failure
        building the calldata. Retryable.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorBody'
    StaleState:
      description: |
        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`.
      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 — 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 — see `code` where present.
        code:
          type: string
          enum:
          - rate_limited
          - overloaded
          description: |
            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:
          type: integer
          minimum: 1
          description: |
            Present alongside `code`; mirrors the `Retry-After` header for clients that find a
            body field easier to reach than a header.
    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
            the router's `Swap` event, so a settlement is attributable to this response.
        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 —
            the maximum the taker funds; unused input is swept back by the router.
        buyAmount:
          allOf:
          - $ref: '#/components/schemas/Wei'
          description: |
            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:
          allOf:
          - $ref: '#/components/schemas/Wei'
          description: |
            Enforced on-chain as the output floor. `mode=sell`: `buyAmount` after the
            slippage tolerance. `mode=buy`: equal to `buyAmount` (the strict target).
        slippagePct:
          type: number
          description: |
            Slippage tolerance actually applied, percent (after clamping). Always `0` for
            `mode=buy` (buys have no slippage band).
        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` — a **value-retention ratio**, not a percentage:
            `1.0` ≈ no value lost, `0.98` ≈ 2% lost. Null when either side is unpriced.
        gas:
          type: integer
          format: int64
          description: |
            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:
          type:
          - integer
          - 'null'
          description: |
            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:
          type:
          - number
          - 'null'
          description: USD value of `gas` × `gasPriceWei`; null when unpriced.
        gasMode:
          type: string
          enum:
          - blind
          - guardrail
          - prune
          description: |
            Gas strategy the solver used. `blind` = no gas pricing available (routes NOT
            gas-penalized); `guardrail`/`prune` = gas-aware solving.
        maxHops:
          type: integer
          description: |
            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:
          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:
            the allowance holder (default funding) or canonical Permit2 (`funding=permit2`).
            **Null for native-token sells** (funded via `value`, no approval).
        route:
          type: array
          items:
            $ref: '#/components/schemas/RouteHop'
          description: |
            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:
          type: array
          items:
            $ref: '#/components/schemas/SwapWarning'
          description: |
            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:
      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`,
            …).
        source:
          type: string
          description: |
            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:
          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
        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.
      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 —
        **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.
      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
        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.
      required:
      - type
      - hash
      - eip712
      - signatureOffset
      - signatureLength
      properties:
        type:
          type: string
          const: Permit2
        hash:
          allOf:
          - $ref: '#/components/schemas/HexData'
          description: The EIP-712 digest — 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** — 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:
          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` — 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:
            type:
            - integer
            - 'null'
            format: int64
            description: |
              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:
            type:
            - integer
            - 'null'
            minimum: 0
            maximum: 10000
            description: |
              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:
            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
              advisories carried inside the flattened quote body). Omitted when empty.
      description: |
        `/v1/swap` body — the quote fields **flattened** at the top level (not nested) plus
        the execution fields.
    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`,
            …).
