> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ostium.com/llms.txt
> Use this file to discover all available pages before exploring further.

# One-time onboarding (gasless)

> A one-time setup step, done gaslessly. Sign two EIP-712 messages offline, post the signatures here, and we relay them on chain and pay for it — the partner never sends a transaction or needs ETH.

Restricted to allowlisted partner addresses. Contact us before integrating.

Send whichever parts you still need; at least one. They are relayed as a single atomic batch, so you are never left with an allowance but no delegate. The response reads both values back from chain, so `state.missing` reflects what is actually set rather than what we submitted.

### The two signatures

**`delegation`** lets the OstiumAtomicTrading wrapper place trades for you.

- Domain: `{ name: "Ostium", version: "1", chainId, verifyingContract: <Trading> }`
- Type: `OstiumDelegation(address delegator, address delegate, uint256 nonce, uint256 expiry)`
- The verifying contract is **Trading**, but `delegate` is the **wrapper** — it is the contract that will call Trading for you, so it is what Trading checks. Sending anything else is rejected.

**`permit`** grants the USDC allowance (ERC-2612).

- Domain: `{ name, version, chainId, verifyingContract: <USDC> }`
- Type: `Permit(address owner, address spender, uint256 value, uint256 nonce, uint256 deadline)`
- `spender` is **TradingStorage**, not Trading — different contracts.
- Read `name` and `version` from the token itself; they differ between deployments and a wrong domain produces a signature that fails on chain.

Both nonces come from the chain at signing time — `Trading.delegatableNonces(partner)` and `USDC.nonces(partner)`.

### Addresses

| | Arbitrum (42161) | Arbitrum Sepolia (421614) |
| --- | --- | --- |
| Trading | `0x6D0bA1f9996DBD8885827e1b2e8f6593e7702411` | `0x2A9B9c988393f46a2537B0ff11E98c2C15a95afe` |
| TradingStorage (permit spender) | `0xccd5891083a8acd2074690f65d3024e7d13d66e7` | `0x0b9F5243B29938668c9Cfbd7557A389EC7Ef88b8` |
| USDC | `0xaf88d065e77c8cc2239327c5edb3a432268e5831` | `0xe73B11Fb1e3eeEe8AF2a23079A4410Fe1B370548` |
| OstiumAtomicTrading (delegate) | `0x5eB3960C3fd3274cD81fE5972e0de01084bDa325` | `0x32C06a3eC2A40DABf6A8f645f29321cB7236DAA3` |

### Two cases where you send only one half

**On Arbitrum Sepolia there is no permit.** The test token is a plain ERC-20 with no `permit` or `nonces`, so no permit signature exists for it. Send `approve(<TradingStorage>, <amount>)` from the wallet, then post `delegation` alone. Mainnet USDC does implement ERC-2612.

**A contract wallet can permit but not delegate.** `setDelegateWithSignature` uses `ecrecover`, which no EIP-1271 signature satisfies. Call `Trading.setDelegate(<OstiumAtomicTrading>)` from the wallet, then post `permit` alone.

### Notes

- **Keep `expiry` and `deadline` short** — 30 minutes is plenty. Until one passes, anyone holding the signature can relay it. They cannot redirect it: `delegate`, `spender` and `value` are all inside the signed bytes.
- **Numbers are decimal strings**, never JSON numbers. A uint256 nonce or a max allowance exceeds `Number.MAX_SAFE_INTEGER`, and a rounded value no longer matches what you signed.

Rate limit: 10 requests per 10 seconds per IP. Read `x-ratelimit-*` for the live budget rather than assuming this figure.



## OpenAPI

````yaml /api-reference/openapi.json post /v1/onboard
openapi: 3.1.0
info:
  title: Ostium Builder API
  version: 1.0.0
  description: >
    REST API for the Ostium Builder SDK and integrating partners: live prices,
    depth, OHLC, and market hours.


    ## Authentication


    **None required today.**


    ## Quick start


    1. **Snapshot** — `GET /v1/prices` (or `GET /v1/prices/{pair}`) for an
    initial board.

    2. **Stream** — connect to `WS /v1/prices/stream` for live ticks — see the
    **Price Stream** section.

    3. **Candles** — `POST /v1/ohlc` for historical OHLC.

    4. **Sessions** — `GET /v1/market-hours` for calendars (cache it; use tick
    flags for “open right now”). The pair list is under **Markets**.

    5. **Depth** — `GET /v1/depth/{pair}/quote` for the price a given size
    actually gets.

    6. **Status** — `GET /v1/status` to check the price feed is live before
    relying on quotes.


    Typed clients: use `@ostium/builder-sdk`, or generate one from this document
    with any OpenAPI 3.1 client generator.


    ## Rate limits


    Applied per IP. Most limits are per route; where a group of endpoints shares
    one budget the endpoint's own description says so. Every rate-limited
    response carries `x-ratelimit-limit`, `x-ratelimit-remaining`, and
    `x-ratelimit-reset`; 429s add `retry-after`. **Read the headers** — do not
    hard-code the documented figures.


    ## Errors


    Two envelopes:


    - **Framework** — `{ "error": "<Status Name>", "message": "..." }` (optional
    `issues` / `details`).

    - **Upstream proxy** — `{ "error": "<human message>" }` from OHLC failures.


    `error` means different things across the two shapes — do not switch on it
    alone until a future major version unifies them.


    Every response carries `x-request-id`. Quote it when reporting a problem.


    ## Hosts


    - Production: `https://builder.prod.bedrock.ostium.io`
servers:
  - url: https://builder.prod.bedrock.ostium.io
    description: Production
security: []
tags:
  - name: Prices
    description: Live prices and historical candles
  - name: Price Stream
    description: >-
      Live prices over a WebSocket. This section is the contract — everything
      below is what the

      server actually sends.


      ### Connecting


      ```

      wss://builder.prod.bedrock.ostium.io/v1/prices/stream?pairs=BTC-USD,ETH-USD

      ```


      `?pairs=` is optional; omit it to receive every asset. No authentication,
      though the handshake is

      capped — see **Limits**. Browser clients are subject to an origin
      allowlist; a rejected upgrade is

      closed with a bare `403`.


      ### Server messages


      One snapshot on connect, carrying the `seq` baseline for every asset:


      ```json

      { "type": "snapshot", "seq": { "BTC-USD": 41 }, "data": [{ "pair":
      "BTC-USD", "bid": 1, "mid": 1, "ask": 1 }] }

      ```


      Then one frame per tick:


      ```json

      { "type": "tick", "seq": 42, "data": { "pair": "BTC-USD", "bid": 1, "mid":
      1, "ask": 1 } }

      ```


      `data` is byte-identical to a row from `GET /v1/prices` — `seq` sits on
      the frame, not inside it,

      so the tick payload stays the same shape as REST.


      | `type` | Sent when |

      | --- | --- |

      | `snapshot` | Once, on connect |

      | `tick` | A price updates |

      | `gap` | Ticks were dropped for you — precedes the next frame you receive
      |

      | `ack` | Your `subscribe`/`unsubscribe` was applied |

      | `error` | Your message was rejected; the filter is unchanged |


      ### Client messages


      ```json

      { "type": "subscribe",   "pairs": ["EUR-USD"] }

      { "type": "unsubscribe", "pairs": ["EUR-USD"] }

      ```


      Every message gets exactly one reply — an `ack` carrying `pairCount` (the
      number of assets you now

      receive; `null` is all of them, `0` is none), or an `error` carrying a
      `code` of `malformed`,

      `unknown_type`, `invalid_pairs`, `no_filter` or `rate_limited`.


      ```json

      { "type": "ack",   "for": "subscribe", "pairCount": 2 }

      { "type": "error", "code": "no_filter", "message": "..." }

      ```


      Filters only widen. `subscribe` on an unfiltered connection is a no-op
      acked with

      `"pairCount": null` — to narrow, reconnect with `?pairs=`. `unsubscribe`
      needs a filtered

      connection and is rejected with `no_filter` otherwise. Unsubscribing your
      last asset leaves you

      receiving nothing (`"pairCount": 0`), which `subscribe` recovers without
      reconnecting.


      ### Detecting loss


      Delivery is best effort, but loss is **detectable**. If more than 1 MB is
      buffered for a slow

      client, ticks are dropped rather than queued — and you are told:


      ```json

      { "type": "gap", "dropped": 17 }

      ```


      `seq` is a per-asset counter, so a jump between consecutive `tick` frames
      for one asset means you

      missed that many. Baseline each asset from the `snapshot`, then compare.


      Two limits on what `seq` can tell you. It is **per connection** —
      process-wide, not global, so it

      does not survive a reconnect and is not comparable across replicas. And
      there is **no replay**: the

      stream tells you that you fell behind, not what you missed, so recover by
      re-reading

      `GET /v1/prices`.


      Assets with no trading schedule are omitted entirely, from the snapshot
      and from ticks. A missing

      asset is not a signal that its market is closed.


      ### Limits


      Per pod, so the effective ceiling scales with replica count. Read the
      headers on a rejected

      upgrade rather than hard-coding these.


      | Limit | Default | Rejected with |

      | --- | --- | --- |

      | Connections per IP | 20 | `429` |

      | Connections in total | 500 | `503` |

      | Upgrades per IP per minute | 60 | `429` |

      | Client messages per socket per minute | 120 | `error`, `code:
      "rate_limited"` |


      Rejected upgrades carry `Retry-After`.


      ### Heartbeat


      The server pings every 30 seconds and terminates a connection that has not
      ponged since the

      previous ping. Browsers pong automatically; other clients need a library
      that does. On shutdown the

      server closes with `1001 going away` after draining.
    x-traitTag: true
  - name: Liquidity
    description: >-
      What a given trade size actually costs, priced against the live impact
      model
  - name: Markets
    description: >-
      What is listed and when it trades.


      ### Pairs


      Every pair on Ostium. Use `id` wherever a request takes a `pairIndex`, and
      the symbol wherever

      one takes a `pair`. Do not infer the id from a row's position — read the
      column.


      A pair marked `not yet` in **Tradeable** is listed on chain but has no
      open-interest ceiling, so

      it cannot be traded and `GET /v1/depth/{pair}` answers `503` for it (the
      `/quote`

      sub-resource still prices it, with `availableNotionalUsd: 0`). It is shown
      rather than hidden so

      the ids either side of it stay right.


      Note `GET /v1/pairs` returns the subgraph's own spelling in `from`/`to`,
      which for renamed

      assets is the legacy one — `SPX` where this table says `US500`. The table
      uses the symbol the

      price and depth endpoints expect.


      <!-- MARKETS:START -->

      | id | Pair | Tradeable |

      | -- | ---- | --------- |

      | 0 | `BTC-USD` | yes |

      | 1 | `ETH-USD` | yes |

      | 2 | `EUR-USD` | yes |

      | 3 | `GBP-USD` | yes |

      | 4 | `USD-JPY` | yes |

      | 5 | `XAU-USD` | yes |

      | 6 | `XCU-USD` | yes |

      | 7 | `WTI-USD` | yes |

      | 8 | `XAG-USD` | yes |

      | 9 | `SOL-USD` | yes |

      | 10 | `US500-USD` | yes |

      | 11 | `US30-USD` | yes |

      | 12 | `US100-USD` | yes |

      | 13 | `JP225-JPY` | yes |

      | 14 | `UK100-GBP` | yes |

      | 15 | `GER40-EUR` | yes |

      | 16 | `USD-CAD` | yes |

      | 17 | `USD-MXN` | yes |

      | 18 | `NVDA-USD` | yes |

      | 19 | `GOOG-USD` | yes |

      | 20 | `AMZN-USD` | yes |

      | 21 | `META-USD` | yes |

      | 22 | `TSLA-USD` | yes |

      | 23 | `AAPL-USD` | yes |

      | 24 | `MSFT-USD` | yes |

      | 25 | `USD-CHF` | yes |

      | 26 | `AUD-USD` | yes |

      | 27 | `NZD-USD` | yes |

      | 28 | `XPD-USD` | yes |

      | 29 | `XPT-USD` | yes |

      | 30 | `HK50-HKD` | yes |

      | 31 | `COIN-USD` | yes |

      | 32 | `HOOD-USD` | yes |

      | 33 | `MSTR-USD` | yes |

      | 34 | `CRCL-USD` | yes |

      | 35 | `BMNR-USD` | yes |

      | 36 | `SBET-USD` | yes |

      | 37 | `GLXY-USD` | yes |

      | 38 | `BNB-USD` | yes |

      | 39 | `XRP-USD` | yes |

      | 40 | `TRX-USD` | yes |

      | 41 | `HYPE-USD` | yes |

      | 42 | `LINK-USD` | yes |

      | 43 | `ADA-USD` | yes |

      | 44 | `PLTR-USD` | yes |

      | 45 | `AMD-USD` | yes |

      | 46 | `NFLX-USD` | yes |

      | 47 | `ORCL-USD` | yes |

      | 48 | `RIVN-USD` | yes |

      | 49 | `COST-USD` | yes |

      | 50 | `XOM-USD` | yes |

      | 51 | `CVX-USD` | yes |

      | 52 | `URA-USD` | yes |

      | 53 | `USD-KRW` | not yet |

      | 54 | `KR2550-USD` | yes |

      | 55 | `BRENT-USD` | yes |

      | 56 | `GEV-USD` | yes |

      | 57 | `SHEL-USD` | yes |

      | 58 | `UNG-USD` | yes |

      | 59 | `XLE-USD` | yes |

      | 60 | `ARM-USD` | yes |

      | 61 | `ASML-USD` | yes |

      | 62 | `AVGO-USD` | yes |

      | 63 | `CAT-USD` | yes |

      | 64 | `INTC-USD` | yes |

      | 65 | `SMCI-USD` | yes |

      | 66 | `TSM-USD` | yes |

      | 67 | `MU-USD` | yes |

      | 68 | `SNDK-USD` | yes |

      | 69 | `HYG-USD` | yes |

      | 70 | `TLT-USD` | yes |

      | 71 | `MP-USD` | yes |

      | 72 | `DRAM-USD` | yes |

      | 73 | `REMX-USD` | yes |

      | 74 | `BB-USD` | yes |

      | 75 | `CRWV-USD` | yes |

      | 76 | `DELL-USD` | yes |

      | 77 | `MRNA-USD` | yes |

      | 78 | `MRVL-USD` | yes |

      | 79 | `NBIS-USD` | yes |

      | 80 | `LLY-USD` | yes |

      | 81 | `SKHY-USD` | yes |

      | 82 | `SPCX-USD` | yes |

      <!-- MARKETS:END -->
  - name: Portfolio
    description: A wallet's open positions, resting orders and executed history
  - name: Orders
    description: >-
      Partner-signed intents executed as a single transaction — the position
      opens or closes and fills, or the whole call reverts. Onboarding lives
      here too: it is the one-time step every order depends on.
  - name: Status
    description: Whether the API is answering usefully right now
paths:
  /v1/onboard:
    post:
      tags:
        - Orders
      summary: One-time onboarding (gasless)
      description: >-
        A one-time setup step, done gaslessly. Sign two EIP-712 messages
        offline, post the signatures here, and we relay them on chain and pay
        for it — the partner never sends a transaction or needs ETH.


        Restricted to allowlisted partner addresses. Contact us before
        integrating.


        Send whichever parts you still need; at least one. They are relayed as a
        single atomic batch, so you are never left with an allowance but no
        delegate. The response reads both values back from chain, so
        `state.missing` reflects what is actually set rather than what we
        submitted.


        ### The two signatures


        **`delegation`** lets the OstiumAtomicTrading wrapper place trades for
        you.


        - Domain: `{ name: "Ostium", version: "1", chainId, verifyingContract:
        <Trading> }`

        - Type: `OstiumDelegation(address delegator, address delegate, uint256
        nonce, uint256 expiry)`

        - The verifying contract is **Trading**, but `delegate` is the
        **wrapper** — it is the contract that will call Trading for you, so it
        is what Trading checks. Sending anything else is rejected.


        **`permit`** grants the USDC allowance (ERC-2612).


        - Domain: `{ name, version, chainId, verifyingContract: <USDC> }`

        - Type: `Permit(address owner, address spender, uint256 value, uint256
        nonce, uint256 deadline)`

        - `spender` is **TradingStorage**, not Trading — different contracts.

        - Read `name` and `version` from the token itself; they differ between
        deployments and a wrong domain produces a signature that fails on chain.


        Both nonces come from the chain at signing time —
        `Trading.delegatableNonces(partner)` and `USDC.nonces(partner)`.


        ### Addresses


        | | Arbitrum (42161) | Arbitrum Sepolia (421614) |

        | --- | --- | --- |

        | Trading | `0x6D0bA1f9996DBD8885827e1b2e8f6593e7702411` |
        `0x2A9B9c988393f46a2537B0ff11E98c2C15a95afe` |

        | TradingStorage (permit spender) |
        `0xccd5891083a8acd2074690f65d3024e7d13d66e7` |
        `0x0b9F5243B29938668c9Cfbd7557A389EC7Ef88b8` |

        | USDC | `0xaf88d065e77c8cc2239327c5edb3a432268e5831` |
        `0xe73B11Fb1e3eeEe8AF2a23079A4410Fe1B370548` |

        | OstiumAtomicTrading (delegate) |
        `0x5eB3960C3fd3274cD81fE5972e0de01084bDa325` |
        `0x32C06a3eC2A40DABf6A8f645f29321cB7236DAA3` |


        ### Two cases where you send only one half


        **On Arbitrum Sepolia there is no permit.** The test token is a plain
        ERC-20 with no `permit` or `nonces`, so no permit signature exists for
        it. Send `approve(<TradingStorage>, <amount>)` from the wallet, then
        post `delegation` alone. Mainnet USDC does implement ERC-2612.


        **A contract wallet can permit but not delegate.**
        `setDelegateWithSignature` uses `ecrecover`, which no EIP-1271 signature
        satisfies. Call `Trading.setDelegate(<OstiumAtomicTrading>)` from the
        wallet, then post `permit` alone.


        ### Notes


        - **Keep `expiry` and `deadline` short** — 30 minutes is plenty. Until
        one passes, anyone holding the signature can relay it. They cannot
        redirect it: `delegate`, `spender` and `value` are all inside the signed
        bytes.

        - **Numbers are decimal strings**, never JSON numbers. A uint256 nonce
        or a max allowance exceeds `Number.MAX_SAFE_INTEGER`, and a rounded
        value no longer matches what you signed.


        Rate limit: 10 requests per 10 seconds per IP. Read `x-ratelimit-*` for
        the live budget rather than assuming this figure.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                chainId:
                  anyOf:
                    - type: number
                      enum:
                        - 42161
                    - type: number
                      enum:
                        - 421614
                  description: 42161 (Arbitrum One) or 421614 (Arbitrum Sepolia)
                partner:
                  type: string
                  pattern: ^0x[a-fA-F0-9]{40}$
                  description: The address being onboarded, and the one that signed below.
                delegation:
                  type: object
                  properties:
                    delegate:
                      type: string
                      pattern: ^0x[a-fA-F0-9]{40}$
                      description: >-
                        Must be the OstiumAtomicTrading address for this chain —
                        see the table above. It is the contract that will place
                        your trades.
                    nonce:
                      type: string
                      pattern: ^\d+$
                      description: >-
                        Read `Trading.delegatableNonces(partner)` at signing
                        time and send that value.
                    expiry:
                      type: string
                      pattern: ^\d+$
                      description: >-
                        Unix seconds after which the signature stops working. 30
                        minutes ahead is plenty.
                    signature:
                      type: string
                      pattern: ^0x[a-fA-F0-9]{130}$
                      description: >-
                        The `OstiumDelegation` signature — 65 bytes. Must be an
                        EOA signature: the contract recovers it with
                        `ecrecover`, which no contract wallet can satisfy.
                  required:
                    - delegate
                    - nonce
                    - expiry
                    - signature
                  description: >-
                    Lets the wrapper trade for you. Omit if you have already
                    delegated.
                permit:
                  type: object
                  properties:
                    value:
                      type: string
                      pattern: ^\d+$
                      description: >-
                        USDC allowance granted to TradingStorage, scaled by 1e6
                        — 100 USDC is `"100000000"`. It is consumed as you
                        trade, so most integrations sign max uint256 and never
                        come back.
                    nonce:
                      type: string
                      pattern: ^\d+$
                      description: >-
                        Read `USDC.nonces(partner)` at signing time and send
                        that value.
                    deadline:
                      type: string
                      pattern: ^\d+$
                      description: >-
                        Unix seconds after which the signature stops working. 30
                        minutes ahead is plenty.
                    signature:
                      type: string
                      pattern: ^0x([a-fA-F0-9]{2})+$
                      description: >-
                        The ERC-2612 `Permit` signature. 65 bytes from an EOA,
                        or opaque bytes from a contract wallet — the token
                        verifies it either way.
                  required:
                    - value
                    - nonce
                    - deadline
                    - signature
                  description: >-
                    Grants the USDC allowance. Omit if you have already
                    approved, and on Arbitrum Sepolia, where the test token has
                    no `permit`.
              required:
                - chainId
                - partner
            example:
              chainId: 42161
              partner: '0x1111111111111111111111111111111111111111'
              delegation:
                delegate: '0x2222222222222222222222222222222222222222'
                nonce: '0'
                expiry: '1800000000'
                signature: '0xabcd'
              permit:
                value: '1000000000000'
                nonce: '0'
                deadline: '1800000000'
                signature: '0xabcd'
      responses:
        '200':
          description: >-
            Relayed. `status` is the terminal on-chain outcome, not an
            acknowledgement.
          headers:
            x-request-id:
              schema:
                type: string
              description: >-
                Always present, and always generated by the service — a value
                sent on the request is ignored. Use it to correlate a response
                with your own logs, and quote it when reporting a problem.
            x-ratelimit-limit:
              schema:
                type: string
              description: Requests allowed per window.
            x-ratelimit-remaining:
              schema:
                type: string
              description: Requests left in the current window.
            x-ratelimit-reset:
              schema:
                type: string
              description: Seconds until the window resets.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum:
                      - confirmed
                      - reverted
                    description: >-
                      `confirmed` means both signatures applied on chain.
                      `reverted` means neither did — the batch is atomic.
                  userOpHash:
                    type: string
                    description: >-
                      Identifies the ERC-4337 user operation we submitted for
                      you. Quote it if you need to ask us about a specific call.
                    example: >-
                      0x8c8b184c3208dd11c0275a0cd5a62d0ef0fb4578d1f9b36569b2d346048af4f7
                  transactionHash:
                    type: string
                    description: The transaction it landed in — look it up on Arbiscan.
                    example: >-
                      0x9cae473bf1c8d54da1d32138e7bbe1d2ca05213c98fe70945b402634aac13279
                  reason:
                    type: string
                    description: Why it reverted. Present only when `status` is `reverted`.
                  state:
                    type: object
                    properties:
                      onboarded:
                        type: boolean
                        description: >-
                          True when nothing is left to do — you can start
                          trading.
                      missing:
                        type: array
                        items:
                          type: string
                          enum:
                            - delegation
                            - allowance
                        description: >-
                          What is still outstanding. Empty when `onboarded` is
                          true.
                        example: []
                      delegate:
                        type: string
                        description: >-
                          The address currently allowed to trade for you. Should
                          be OstiumAtomicTrading; the zero address means no
                          delegation is set.
                        example: '0x32C06a3eC2A40DABf6A8f645f29321cB7236DAA3'
                      allowance:
                        type: string
                        description: >-
                          USDC that TradingStorage may spend, scaled by 1e6.
                          Only checked against zero here — whether it is enough
                          depends on the sizes you intend to trade.
                    required:
                      - onboarded
                      - missing
                      - delegate
                      - allowance
                    description: >-
                      Your onboarding state, read back from chain rather than
                      assumed from what was submitted. Absent if that read
                      failed; the outcome above still stands.
                required:
                  - status
                  - userOpHash
                  - transactionHash
              example:
                status: confirmed
                userOpHash: '0xaaaa'
                transactionHash: '0xbbbb'
                state:
                  onboarded: true
                  missing: []
                  delegate: '0x2222222222222222222222222222222222222222'
                  allowance: >-
                    115792089237316195423570985008687907853269984665640564039457584007913129639935
        '400':
          description: Validation failed
          headers:
            x-request-id:
              schema:
                type: string
              description: >-
                Always present, and always generated by the service — a value
                sent on the request is ignored. Use it to correlate a response
                with your own logs, and quote it when reporting a problem.
            x-ratelimit-limit:
              schema:
                type: string
              description: Requests allowed per window.
            x-ratelimit-remaining:
              schema:
                type: string
              description: Requests left in the current window.
            x-ratelimit-reset:
              schema:
                type: string
              description: Seconds until the window resets.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: Bad Request
                message: Validation failed
                issues: []
        '401':
          description: A signature does not match the partner address
          headers:
            x-request-id:
              schema:
                type: string
              description: >-
                Always present, and always generated by the service — a value
                sent on the request is ignored. Use it to correlate a response
                with your own logs, and quote it when reporting a problem.
            x-ratelimit-limit:
              schema:
                type: string
              description: Requests allowed per window.
            x-ratelimit-remaining:
              schema:
                type: string
              description: Requests left in the current window.
            x-ratelimit-reset:
              schema:
                type: string
              description: Seconds until the window resets.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: Unauthorized
                message: permit signature does not match the partner address
        '403':
          description: The address is not an allowlisted partner
          headers:
            x-request-id:
              schema:
                type: string
              description: >-
                Always present, and always generated by the service — a value
                sent on the request is ignored. Use it to correlate a response
                with your own logs, and quote it when reporting a problem.
            x-ratelimit-limit:
              schema:
                type: string
              description: Requests allowed per window.
            x-ratelimit-remaining:
              schema:
                type: string
              description: Requests left in the current window.
            x-ratelimit-reset:
              schema:
                type: string
              description: Seconds until the window resets.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: Forbidden
                message: address is not a permitted partner
        '413':
          description: Request body is larger than the parser limit
          headers:
            x-request-id:
              schema:
                type: string
              description: >-
                Always present, and always generated by the service — a value
                sent on the request is ignored. Use it to correlate a response
                with your own logs, and quote it when reporting a problem.
            x-ratelimit-limit:
              schema:
                type: string
              description: Requests allowed per window.
            x-ratelimit-remaining:
              schema:
                type: string
              description: Requests left in the current window.
            x-ratelimit-reset:
              schema:
                type: string
              description: Seconds until the window resets.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: Payload Too Large
                message: request entity too large
        '429':
          description: Rate limit exceeded
          headers:
            x-request-id:
              schema:
                type: string
              description: >-
                Always present, and always generated by the service — a value
                sent on the request is ignored. Use it to correlate a response
                with your own logs, and quote it when reporting a problem.
            x-ratelimit-limit:
              schema:
                type: string
              description: Requests allowed per window.
            x-ratelimit-remaining:
              schema:
                type: string
              description: Requests left in the current window.
            x-ratelimit-reset:
              schema:
                type: string
              description: Seconds until the window resets.
            retry-after:
              schema:
                type: string
              description: Seconds to wait before retrying.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: Too Many Requests
                message: Rate limit exceeded. Try again in 7s.
        '500':
          description: Unexpected error
          headers:
            x-request-id:
              schema:
                type: string
              description: >-
                Always present, and always generated by the service — a value
                sent on the request is ignored. Use it to correlate a response
                with your own logs, and quote it when reporting a problem.
            x-ratelimit-limit:
              schema:
                type: string
              description: Requests allowed per window.
            x-ratelimit-remaining:
              schema:
                type: string
              description: Requests left in the current window.
            x-ratelimit-reset:
              schema:
                type: string
              description: Seconds until the window resets.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: Internal Server Error
                message: Internal Server Error
components:
  schemas:
    ErrorResponse:
      type: object
      properties:
        error:
          type: string
          description: HTTP status name, e.g. "Bad Request"
          example: Bad Request
        message:
          type: string
          description: Human-readable detail
          example: Validation failed
        issues:
          type: array
          items: {}
          description: Raw zod issue array — present on validation failures only
        details:
          description: Optional structured detail from the thrower
      required:
        - error
        - message

````