# Kairos Order Execution API (execution.kairos.trade) — OpenAPI 3.1 spec.
# Canonical source of truth for this service's public API reference.
#
# Consumers: services/web/scripts/build-openapi.ts publishes it (and the
# other specs in docs/api/) as /openapi/* static assets; the unified docs
# reference at /docs/api-reference renders it with live try-it panels.
#
# When you change this service's public surface, update this file in the
# same PR. Extensions the reference UI renders: x-kairos-auth,
# x-kairos-rate-limit, x-kairos-scope.
openapi: 3.1.0
info:
  title: Kairos Order Execution API
  version: 1.0.0
  summary: Order entry, cancellation, fee quotes, and live position exposure across venues.
  description: |
    The Order Execution API at `execution.kairos.trade` places and manages
    orders across every venue Kairos integrates. Two lanes:

    - **Custodial** (`POST /orders`) — Kairos signs and routes on your behalf
      (custodial signing). Async ack: a 200 means validated + enqueued; track via
      `GET /orders/{order_id}` or the WebSocket order/fill stream.
    - **Self-custody / external signing** (`POST /v2/orders/intent` +
      `POST /v2/orders/submit`) — for allow-listed institutional accounts on
      Polymarket and Predict.fun: Kairos builds the EIP-712 payload, you sign
      with your own key, submission is synchronous with the venue's verbatim
      result. `POST /v2/onchain/intent` + `POST /v2/onchain/submit` are the
      same pattern for on-chain operations (approvals, redeem, split/merge,
      unwrap) on Polygon, where you also pay the gas.

    ## Authentication

    All endpoints require a Kairos API key (`X-Client-Id` + `X-Api-Key` +
    `X-Api-Secret`) or a first-party session JWT, plus the per-operation
    scope (`trade:execute`, `trade:read`, `position:read`) where the operation
    documents one. Synthetic Books accepts any otherwise-valid account
    credential without an extra scope or allowlist. There is
    no anonymous tier. Scope checks apply to API-key credentials only; a
    session JWT carries no scope list and is treated as holding every scope.

    The custodial mutation endpoints (`POST /orders`, the three cancel
    endpoints) and Synthetic Books create/refresh/release carry an additional
    `RequireServiceToken` gate satisfied by **any** of: the full API-key triple,
    an internal `X-Service-Token`, or a valid `X-Csrf-Token`. API-key consumers
    need no extra header — but a bare `Authorization: Bearer <jwt>` alone is NOT
    sufficient on those routes. The `/v2/orders/*` and `/v2/onchain/*`
    external-signing routes have no such gate: a session JWT alone works there.

    ## Rate limits

    Order submission is capped per user (default 5 orders per **second**,
    sliding window, `ORDER_RATE_LIMIT_PER_SEC`); some credentials carry an
    `orders` override, which is enforced on its own per-credential window of
    the same duration *in addition to* the per-user window — either one
    denying is a `429`. Idempotent replays (same `client_order_id`) return the
    existing order before the limiter runs and never consume a slot. The
    limiter is Redis-backed and **fails closed**: if Redis is unreachable the
    submission is denied with `429`.

    Separately, repeated *authentication failures* from one client IP are
    throttled at 10 failures per 60 s (also fail-closed). That limiter returns
    the minimal `{"error": "Too many authentication attempts"}` body.

    **No rate-limit headers.** This service does not emit `Retry-After`,
    `X-RateLimit-*`, or any other backoff hint on a `429` — back off on your
    own schedule.

    ## Error responses

    **Six different error body shapes are in use across this service and they
    are not interchangeable.** Check the shape documented on the specific
    operation before writing a parser; a client that assumes one shape will
    read `undefined` for the reason on the others.

    1. `OrderErrorResponse` — the structured envelope (`error`, `code`,
       `error_details{code,message,details?,metadata?,actions}`) returned by
       every handler that surfaces an `ExecutionError`/`ApiError`: order
       submission, cancel-all, the CTF endpoints, the Polymarket/Opinion
       onboarding endpoints, Hyperliquid withdraw/transfer, and most of the
       deposit-wallet family. Note the two `code` fields differ in case:
       top-level `code` is PascalCase (`"AuthInsufficientScope"`), while
       `error_details.code` is the SCREAMING_SNAKE_CASE wire code
       (`"AUTH_INSUFFICIENT_SCOPE"`). Match on `error_details.code`.
    2. `OrderSimpleErrorResponse` — the minimal `{"error": "..."}` (sometimes
       with `code`) used by the auth middleware (any endpoint's
       `401`/`403`/`429`), the whole `/v2/*` external-signing and on-chain
       lane, the Kalshi and Predict.fun endpoints.
    3. `OrderMarketLinkErrorResponse` — `{"message": "..."}`, with no `error`
       and no `code`. Used by `POST /orders/market-links` **only**.
    4. **Empty or plain-text, not JSON at all.** Several deposit-wallet
       endpoints — most importantly
       the RPC signed-batch submission and
       `POST /exchanges/polymarket/imported/relay-info` — return most 4xx/5xx
       responses with a **completely empty body** and `content-type:
       text/plain`. A handful of cases carry a bare plain-text sentence
       (`batch is not an allowed withdrawal or collateral conversion`,
       `relayer rejected batch: …`). Do not attempt to JSON-parse these.
    5. Handlers whose Rust signature returns a bare `StatusCode` likewise send
       **no body at all**; those responses are marked "empty body (status code
       only)".
    6. `SyntheticBookErrorResponse` — `{"error": "stable_snake_case_code",
       "message": "...", "details"?: {...}}` on `/v1/synthetics*`. Match the
       top-level `error`; upstream definition validation may be nested under
       `details`.

    A further wrinkle inside shape 1: some provider-access checks discard the
    specific reason and return a generic `"Request failed with status 403"` /
    `AUTH_CREDENTIALS_INVALID` body, while others preserve
    `AUTH_INSUFFICIENT_SCOPE` and the real message. Do not rely on the message
    text of an access denial being stable.

    The central `ExecutionError` → HTTP mapping (`ApiError::from`) is:

    | `ExecutionError` | Status | `error_details.code` |
    |---|---|---|
    | `InsufficientBalance` | 400 | `FUNDS_INSUFFICIENT_USDC` |
    | `CollateralLocation` | 400 | `FUNDS_COLLATERAL_LOCATION` |
    | `InvalidOrder` | 400 | `VALIDATION_INVALID_ORDER` |
    | `PositionShortfall` | 400 | `FUNDS_INSUFFICIENT_BALANCE` |
    | `MarketClosed` | 400 | `EXCHANGE_POLYMARKET_MARKET_CLOSED` |
    | `OrderAlreadyCancelled` | 400 | `VALIDATION_INVALID_ORDER` |
    | `UnsupportedExchange` | 400 | `EXCHANGE_UNSUPPORTED` |
    | `SlippageExceeded` | 400 | `MARKET_FOK_NOT_FILLED` |
    | `FokNotFilled` | 400 | `MARKET_FOK_NOT_FILLED` |
    | `AuthenticationError` | 401 | `AUTH_CREDENTIALS_INVALID` |
    | `CredentialError` | 401 | `AUTH_CREDENTIALS_NOT_FOUND` |
    | `MarketNotFound` | 404 | `VALIDATION_MARKET_NOT_FOUND` |
    | `OrderNotFound` | 404 | `VALIDATION_INVALID_ORDER` |
    | `MarketNotSettledOnChain` | 409 | `VALIDATION_MARKET_NOT_SETTLED_ON_CHAIN` |
    | `OrderbookUnavailable` | 422 | `ORDERBOOK_UNAVAILABLE` |
    | `ExchangeError` (code `429`/`RATE_LIMITED`) | 429 | `EXCHANGE_POLYMARKET_RATE_LIMITED` |
    | `ExchangeError` (code `401`/`UNAUTHORIZED`) | 401 | `AUTH_POLYMARKET_API_KEY_INVALID` |
    | `SigningError` | 500 | `SIGNATURE_ERROR` |
    | `DatabaseError` | 500 | `DATABASE_ERROR` |
    | `InternalError` / `LedgerReconciliationRequired` / `SponsoredRequestWedged` / `PreTradeError` | 500 | `INTERNAL_ERROR` |
    | `NetworkError`, `ExchangeError` (any other code) | 502 | `NETWORK_ERROR` / venue-classified |
    | `LockError` | 503 | `INTERNAL_ERROR` |
    | `Timeout` | 504 | `NETWORK_TIMEOUT` |

    A venue `ExchangeError` is further classified from the venue's own message
    text before it is mapped, so an "allowance is not enough" rejection becomes
    `ALLOWANCE_CTF_NOT_SET`, a "not enough balance" rejection becomes
    `FUNDS_INSUFFICIENT_BALANCE`, "post-only mode" becomes `MARKET_NOT_READY`,
    a "no liquidity" rejection becomes `MARKET_INSUFFICIENT_LIQUIDITY`, and so
    on. Match on `error_details.code`, never on `error`.

    ## Server-level guards

    Every request is subject to a 120 s timeout (`OE_REQUEST_TIMEOUT_SECS`), a
    2 MiB request-body cap (`OE_MAX_BODY_BYTES`, over-size bodies get `413`),
    and a 1024-request global concurrency ceiling
    (`OE_MAX_CONCURRENT_REQUESTS`). Every response carries `X-Content-Type-Options:
    nosniff`, `X-Frame-Options: DENY`, HSTS, and a `default-src 'none'` CSP.

    ## Conventions

    - Prices are decimal strings on the 0–1 scale; `*_bps` fields are the
      same value in basis points (× 10000).
    - Quantities/sizes are decimal strings.
    - Structured errors carry `error_details.code`
      (SCREAMING_SNAKE_CASE) plus actionable recovery `actions`.

    ## Changelog

    **2026-09-14 — collateral routing fields on `POST /orders`.** Four optional
    request fields and one optional response field. `collateral`
    (`skip` | `check` | `fund`) **defaults to** `skip`, which is exactly today's
    path: no affordability check, no hold, no funding, zero added latency — so
    no existing caller changes behaviour without opting in. `shard_funding`
    **defaults to** `true`, which is also exactly today's behaviour: the Kalshi
    shard move is same-venue, zero-fee and already runs for every account; send
    `false` to opt out. `collateral=fund` requires both `max_bridge_fee_usdc`
    and `max_funding_wait_ms` and is rejected `400` naming the missing cap;
    either cap without `fund` is likewise a `400`. The response gains
    `funding`, `null` whenever no funding work ran. FIX sessions carry the same
    fields on `NewOrderSingle(D)` as optional tags `5701`–`5704`; a session that
    sends none of them is unchanged.
  contact:
    name: Kairos
    url: https://app.kairos.trade/docs/api-reference
  termsOfService: https://kairos.trade/terms
servers:
- url: https://execution.kairos.trade
  description: Production (central primary, us-east-1)
- url: https://eu-west-1-polymarket.executor.kairos.trade
  description: Production regional execution node — Ireland, colocated with Polymarket. Same API surface; assigned at onboarding.
- url: https://ap-northeast-1-predictfun.executor.kairos.trade
  description: Production regional execution node — Tokyo, colocated with Predict.fun. Same API surface; assigned at onboarding.
- url: https://staging-execution.kairos.trade
  description: Staging
security:
- apiKeyClientId: []
  apiKeyKey: []
  apiKeySecret: []
components:
  securitySchemes:
    apiKeyClientId:
      type: apiKey
      in: header
      name: X-Client-Id
      description: Credential client id (`kairos_ck_...`). Must be sent together with X-Api-Key and X-Api-Secret.
    apiKeyKey:
      type: apiKey
      in: header
      name: X-Api-Key
      description: 64-char hex API key.
    apiKeySecret:
      type: apiKey
      in: header
      name: X-Api-Secret
      description: 64-char hex API secret.
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: First-party Kairos session JWT (web app sessions). Not issued to API consumers.
  responses:
    ExecUnauthorized:
      description: |
        No valid credential presented — missing/invalid API-key headers or an invalid/expired session token.
      content:
        application/json:
          schema:
            type: object
            required:
            - error
            properties:
              error:
                type: string
                example: Unauthorized
  schemas:
    OrderSide:
      type: string
      description: Order/intent side. Lower-case on the wire.
      enum:
      - buy
      - sell
      example: buy
    OrderKind:
      type: string
      description: Order type. `market` orders still require a `price` (the limit you'll cross to); `limit`
        orders rest at `price` until filled or cancelled.
      enum:
      - market
      - limit
      example: limit
    OrderTimeInForce:
      type: string
      description: Time-in-force, always UPPER-CASE on the wire. `GTC`/`GTD` are resting (limit-style);
        `FOK`/`FAK`/`IOC` are immediate taker executions.
      enum:
      - GTC
      - GTD
      - FOK
      - FAK
      - IOC
      example: GTC
    OrderStatus:
      type: string
      description: |-
        Lifecycle status. `partial` is the wire spelling of a partially-filled order (NOT
        `partially_filled`).

        The internal states `queued`, `locked` and `orphaned` are **persisted as `pending`**, so
        an order read back from `GET /orders` or `GET /orders/{order_id}` reports `pending` for
        all three — they are listed here because they are part of the type and can appear on
        in-process/streamed values. `executing` is the exception: once a worker claims the order
        for submission it is persisted as `executing`, and read-back endpoints report `executing`
        until the venue acknowledges (then `live`) or the attempt fails. The one place a caller
        sees `queued` directly is the `status` field of a fresh `POST /orders` response, which is
        the literal string `"queued"`.
      enum:
      - pending
      - queued
      - locked
      - executing
      - live
      - partial
      - filled
      - cancelled
      - expired
      - failed
      - orphaned
      example: live
    Order:
      type: object
      description: Exchange-agnostic order record.
      required:
      - id
      - user_id
      - exchange_id
      - market_id
      - side
      - kind
      - quantity
      - time_in_force
      - filled_quantity
      - status
      - created_at
      - updated_at
      - gas_sponsored
      - collateral_mode
      - shard_funding
      - holding_wallet
      properties:
        id:
          type: string
          format: uuid
          description: Internal Kairos order id.
        user_id:
          type: string
          description: Owning user's id.
        exchange_id:
          type: string
          description: Venue identifier (e.g. `polymarket`, `kalshi`, `predictfun`, `hyperliquid`).
          example: polymarket
        market_id:
          type: string
          description: Market/contract identifier on the venue (condition id for Polymarket; numeric HIP-4 outcome id for Hyperliquid).
        token_id:
          type: string
          nullable: true
          description: Outcome-token id. Hyperliquid uses a side coin such as `#1010` or `#1011`.
        side:
          $ref: '#/components/schemas/OrderSide'
        kind:
          $ref: '#/components/schemas/OrderKind'
        quantity:
          type: string
          description: Order quantity (decimal string, full precision), shares/contracts.
          example: '100'
        price:
          type: string
          nullable: true
          description: Limit price as a decimal string in `[tick, 1]` (prediction-market venues).
          example: '0.52'
        price_bps:
          type: integer
          nullable: true
          description: '`price` expressed in basis points (price × 10000), for DB/analytics compatibility.'
          example: 5200
        time_in_force:
          $ref: '#/components/schemas/OrderTimeInForce'
        post_only:
          type: boolean
          description: Whether this order was submitted maker-only. A post-only order is one the venue
            was told to REJECT rather than let cross the spread and take liquidity. Always present;
            `false` for ordinary orders and for venues with no post-only concept.
          default: false
          example: false
        expires_at:
          type: string
          format: date-time
          nullable: true
          description: Expiration timestamp for GTD orders.
        filled_quantity:
          type: string
          description: Cumulative filled quantity (decimal string).
          example: '0'
        avg_fill_price:
          type: string
          nullable: true
          description: Size-weighted average fill price (decimal string, 0..1), null until any fill lands.
        avg_fill_price_bps:
          type: integer
          nullable: true
          description: '`avg_fill_price` in basis points.'
        exchange_order_id:
          type: string
          nullable: true
          description: Venue-assigned order handle. Null until the order reaches the venue.
        status:
          $ref: '#/components/schemas/OrderStatus'
        terminal_fill_verification:
          allOf:
          - $ref: '#/components/schemas/TerminalFillVerification'
          nullable: true
          description: Durable venue-terminal fill barrier. While `pending`, callers must retain
            protection even if another status field appears terminal. `complete` carries the exact
            venue cumulative that was folded before terminal publication.
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        locked_by:
          type: string
          nullable: true
          description: Worker id currently holding the execution lock, if any.
        lock_expires_at:
          type: string
          format: date-time
          nullable: true
        error_message:
          type: string
          nullable: true
          description: Failure detail when `status` is `failed`.
        failure:
          $ref: '#/components/schemas/OrderFailure'
        fee_amount:
          type: string
          nullable: true
          description: Fee charged for this order, decimal string.
        fee_currency:
          type: string
          nullable: true
        client_order_id:
          type: string
          nullable: true
          description: Caller-supplied idempotency key, if one was provided at submission.
        wallet_id:
          type: string
          nullable: true
          description: FK to the user's wallet used for this order.
        outcome:
          type: string
          nullable: true
          description: Human-readable outcome label (e.g. "Yes", "No", a team name).
        outcome_id:
          type: string
          nullable: true
          description: Outcome id — equals `token_id` on Polymarket.
        trigger_price:
          type: string
          nullable: true
          description: Trigger price for stop-loss/take-profit style orders, decimal string.
        trigger_price_bps:
          type: integer
          nullable: true
        gas_sponsored:
          type: boolean
          description: Whether Kairos sponsored gas for this order (always `false` on the self-custody
            external-signing lane).
        collateral_mode:
          type: string
          enum:
          - skip
          - check
          - fund
          description: The collateral mode resolved for this order at submit. Always present; `skip`
            for an order that asked for nothing.
          example: skip
        shard_funding:
          type: boolean
          description: Whether the Kalshi shard move was permitted for this order. Always present;
            `true` unless the caller opted out.
          example: true
        max_bridge_fee_usdc:
          type: string
          nullable: true
          description: The order's bridge-fee ceiling in USDC, decimal string. Only ever set under
            `collateral_mode=fund`.
        max_funding_wait_ms:
          type: integer
          nullable: true
          description: The order's funding-wait ceiling in milliseconds. Only ever set under
            `collateral_mode=fund`.
        gas_amount:
          type: string
          nullable: true
          description: Gas spent, decimal string, if applicable.
        maker_address:
          type: string
          nullable: true
          description: On-chain maker/signer address, for Polymarket reconciliation.
        tx_hash:
          type: string
          nullable: true
          description: On-chain transaction hash (Polygon), if the fill involved one.
        raw:
          nullable: true
          description: Full raw venue request/response payload for audit. Present on `GET /orders/{order_id}`;
            stripped to `null` on `GET /orders` (list responses) to keep list payloads small.
        bot_id:
          type: string
          nullable: true
          description: Trading-bot id that placed this order, if any (null for manual/API orders).
        source:
          type: string
          nullable: true
          description: 'Attribution for who initiated the order: `manual` (web UI), `bot`, `copytrade`,
            `api` (API-key auth), or `fastlane` (external-signing lane).'
          example: api
        metadata:
          nullable: true
          description: Exchange-specific metadata (arbitrary JSON). Present on `GET /orders/{order_id}`;
            stripped to `null` on `GET /orders`.
        holding_wallet:
          type: string
          description: On-chain wallet that holds (or will hold) the resulting shares — the EOA for legacy
            Polymarket orders, or the Safe deposit-wallet proxy for upgraded/external-signing users. Empty
            string for non-Polymarket orders.
    OrderSubmitRequest:
      type: object
      description: Body for `POST /orders` (custodial order submission).
      required:
      - exchange_id
      - market_id
      - side
      - kind
      - quantity
      properties:
        user_id:
          type: string
          deprecated: true
          description: Ignored. The authenticated caller's id is always used.
        exchange_id:
          type: string
          description: Venue identifier, must be a registered exchange (e.g. `polymarket`, `kalshi`, `predictfun`, `hyperliquid`).
          example: polymarket
        market_id:
          type: string
          description: Market/contract identifier on the venue. Hyperliquid expects the numeric HIP-4 outcome id.
        token_id:
          type: string
          nullable: true
          description: Outcome-token id. For Hyperliquid, pass the selected side coin `#<10 * market_id + side_index>`; it is required to select side 1.
        outcome:
          type: string
          nullable: true
          description: Human-readable outcome label for display/attribution (e.g. `"Yes"`, `"No"`, a team
            name).
          example: 'Yes'
        side:
          type: string
          description: '`buy` or `sell`, case-insensitive on input.'
          enum:
          - buy
          - sell
          - BUY
          - SELL
          - Buy
          - Sell
          example: buy
        kind:
          type: string
          description: '`market` or `limit`, lower-case exactly.'
          enum:
          - market
          - limit
          example: limit
        quantity:
          type: string
          description: Decimal string, shares/contracts. Must be > 0 and <= 1,000,000. A minimum order
            size (default `1.0`, `MIN_ORDER_QUANTITY`) applies to BUY orders only — SELL/close orders
            are allowed at any size so a position can be fully closed after partial fills. API-key-authenticated
            BUYs additionally require `quantity × price >= $5` notional; session-JWT callers and all
            SELLs are exempt.
          example: '100'
        price:
          type: string
          description: Decimal string, REQUIRED for every order (including market orders, where it is
            the limit you're willing to cross to). Must be > 0 and <= the venue's max price (typically
            `1.0` for prediction markets).
          example: '0.52'
        time_in_force:
          type: string
          description: '`GTC`, `GTD`, `FOK`, `FAK`, or `IOC` (case-insensitive on input, normalized to
            upper-case). Defaults to `GTC` if omitted; an unrecognized non-empty value is rejected rather
            than silently downgraded to `GTC`. Must be a TIF the target venue''s capabilities advertise.'
          default: GTC
          example: GTC
        post_only:
          type: boolean
          nullable: true
          description: 'Maker-only. When `true` the venue must REJECT the order rather than let any part
            of it cross the spread and take liquidity — it is a guarantee, never a hint, so an order that
            cannot honour it is rejected instead of being downgraded to a taker. Requires `kind=limit`
            and `time_in_force` of `GTC` or `GTD`, on a venue whose capabilities advertise post-only
            support (currently Polymarket, Kalshi, Predict.fun and Hyperliquid — the last
            expresses maker-only as its `Alo` time-in-force rather than a flag, but the request
            field is the same). Any other combination is a 400.
            Defaults to `false`.'
          default: false
          example: false
        expiration_minutes:
          type: integer
          nullable: true
          description: 'Minutes from now until expiry. Only meaningful (and validated) when `time_in_force=GTD`:
            must be in `[1, 43200]` (30 days).'
          example: 60
        trigger_price:
          type: string
          nullable: true
          description: Decimal string. Trigger price for stop-loss/take-profit style orders. Must be >
            0 and, like `price`, <= the venue's max price.
        collateral:
          type: string
          nullable: true
          enum:
          - skip
          - check
          - fund
          default: skip
          description: 'How much of the collateral router runs for this order. `skip` (the server
            default) is today''s path: no affordability check, no hold, no router call, zero added
            latency. `check` places an affordability check and a hold and refuses a known shortfall,
            but never moves money. `fund` is `check` plus cross-ledger funding inside the two caps
            below, and REQUIRES both of them. An unrecognised value is rejected `400` naming the
            three valid values; it is never downgraded to `skip`.'
          example: skip
        shard_funding:
          type: boolean
          nullable: true
          default: true
          description: 'Kalshi only. Move collateral between the account''s own Kalshi shards before
            submit. Defaults to `true` — same venue, zero fee, one REST call, and already every
            account''s behaviour — so an existing caller is unaffected. Send `false` to skip it.'
          example: true
        max_bridge_fee_usdc:
          type: string
          nullable: true
          description: 'Decimal string. The most bridge fee this order consents to pay, in USDC.
            Required when `collateral=fund` and rejected `400` otherwise; `0` is a valid cap meaning
            "fund only if it is free".'
          example: '1.50'
        max_funding_wait_ms:
          type: integer
          nullable: true
          description: 'The longest this order consents to wait for funding, in milliseconds.
            Required when `collateral=fund` and rejected `400` otherwise.'
          example: 30000
        gas_sponsored:
          type: boolean
          nullable: true
          description: Requests gas sponsorship. Currently not implemented server-side.
        client_order_id:
          type: string
          nullable: true
          description: Caller-supplied idempotency key. A retry with the same `(user, exchange_id, market_id,
            client_order_id)` returns the existing order instead of creating a duplicate, and does not
            consume a rate-limit slot. If omitted, a random id is generated server-side (so un-keyed retries
            are NOT deduped).
        orderbook_levels:
          type: array
          deprecated: true
          description: Ignored. The executor always fetches fresh orderbook data itself. Kept only for
            backwards-compatible request bodies.
          items:
            type: array
            items:
              type: string
        max_slippage:
          type: string
          nullable: true
          deprecated: true
          description: Deprecated — use `max_slippage_cents`. Decimal in `[0, 0.5]` (e.g. `0.10` = 10%).
        max_slippage_cents:
          type: integer
          nullable: true
          description: Maximum acceptable slippage in cents. Must be in `[1, 99]` if provided.
          example: 5
        max_retries:
          type: integer
          nullable: true
          description: Maximum retry attempts for transient failures. Defaults to `0`.
        bot_id:
          type: string
          nullable: true
          description: Trading-bot id this order is attributed to.
        source:
          type: string
          nullable: true
          description: Order-source attribution override. Pass `copytrade` explicitly when relevant; otherwise
            the server derives it from the auth method (API-key auth → `api`, `bot_id` present → `bot`,
            else → `manual`).
          example: api
    OrderSubmitResponse:
      type: object
      description: Response for `POST /orders`.
      required:
      - order_id
      - status
      properties:
        order_id:
          type: string
          format: uuid
          description: Internal Kairos order id. Use with `GET /orders/{order_id}` and the cancel endpoints.
        status:
          type: string
          description: '`queued` for a newly-created order; on an idempotent replay (matching `client_order_id`),
            the pre-existing order''s current status (`pending`, `live`, `partial`, `filled`, `cancelled`,
            `expired`, or `failed`).'
          example: queued
        funding:
          nullable: true
          description: 'What the collateral router did for this order. `null` whenever no funding
            work ran — every `skip` order, and every order while funding orchestration is still
            being built.'
          allOf:
          - $ref: '#/components/schemas/FundingOutcome'
    FundingOutcome:
      type: object
      description: What the collateral router did for one order. Absent sub-fields mean that part
        did not happen.
      required:
      - mode
      properties:
        mode:
          type: string
          enum:
          - skip
          - check
          - fund
          description: The resolved collateral mode this outcome describes.
        hold_id:
          type: string
          format: uuid
          description: The BUY hold the affordability check placed, when one was placed.
        intent_ref:
          type: string
          description: The router intent that moved collateral, when one was opened.
        refused_reason:
          type: string
          description: Why funding did not happen, for a mode that asked for it.
    OrderCancelResponse:
      type: object
      description: Response for `POST /orders/{order_id}/cancel`. Returned with HTTP 200 even when `success`
        is `false`.
      required:
      - success
      - order_id
      - venue_reconciled
      properties:
        success:
          type: boolean
          description: Whether cancellation and terminal fill verification are both complete.
            A venue DELETE acknowledgement alone still returns false while verification is pending.
        order_id:
          type: string
          format: uuid
        message:
          type: string
          description: Human-readable outcome detail, e.g. "Order cancelled on exchange" or "Order is
            being submitted to exchange. Please try again in a moment."
        filled_quantity:
          type: string
          nullable: true
          description: Total cumulative filled quantity, decimal string, when terminal verification
            is complete. Omitted while pending or unknown; do not infer omitted as zero. The durable
            receipt repeats this exact cumulative in `final_filled_quantity`.
        avg_fill_price:
          type: string
          nullable: true
          description: Average fill price (decimal, 0..1) of any portion that filled before the cancel
            landed. Omitted when nothing filled or unknown.
        venue_reconciled:
          type: boolean
          description: True only when a venue-final snapshot was observed and its exact cumulative
            fill has passed through Kairos' position fold. Currently authoritative for Kalshi.
        reconciled_position:
          allOf:
          - $ref: '#/components/schemas/ReconciledPositionExposure'
          nullable: true
          description: Exact post-fold position authority. A reconciled order may omit this object,
            either because nothing filled or because the durable position projection could not be
            proven within the cancel budget; the cancel itself is still confirmed. Omission is never
            proof that the portfolio is flat.
        terminal_fill_verification:
          allOf:
          - $ref: '#/components/schemas/TerminalFillVerification'
          nullable: true
          description: Pending or completed durable receipt for the exact venue-order generation.
            A pending response is not a confirmed cancellation even when the venue acknowledged DELETE.
    OrderAmendRequest:
      type: object
      description: Request body for `POST /orders/{order_id}/amend`. Reprice only — `quantity` exists
        solely to be refused.
      properties:
        price:
          type: number
          description: New limit price, strictly greater than 0 and less than 1, in the order's OWN
            outcome's terms (send a Kalshi `No` order's `No` price; do not complement it client-side).
            Omitted re-sends the price the order already has.
        quantity:
          type: number
          description: Accepted only to be REFUSED with `409` `EXCHANGE_AMEND_QUANTITY_UNSUPPORTED`,
            before the order is even read. This route reprices; it does not resize. Refused rather
            than ignored so a caller never believes an order was resized when it was not.
    OrderAmendResponse:
      type: object
      description: Response for `POST /orders/{order_id}/amend`. Returned with HTTP 200 even when
        `success` is `false`.
      required:
      - success
      - order_id
      properties:
        success:
          type: boolean
          description: Whether the venue confirmed the amend. `false` is an AMBIGUOUS outcome, not an
            error status — the amend may have been refused, or it may have reached the venue and been
            applied without us learning so. That ambiguity is why it is not a 5xx, which would invite
            a blind retry against an order that may already carry the new price. A non-amendable
            status answers 200 with a false `success` too. On any `false`, re-read the order before
            acting on it.
        order_id:
          type: string
          format: uuid
          description: The id that was addressed, always unchanged. An amend never mints a new order.
        exchange_order_id:
          type: string
          description: The venue's order id, also unchanged — Kalshi AmendOrder v2 answers with the
            same id it was given, so there is no supersession to record. Omitted only on the
            pre-submission path, where the order has no venue identity yet.
        price:
          type: string
          description: Decimal string. The new resting price on success. On an unsuccessful path it is
            the price we last recorded — the resting price when the venue explicitly refused, but only
            our last known value when the call failed in transit or came back unreadable, where the
            venue may in fact hold the new price. Omitted when unknown.
        quantity:
          type: string
          description: Decimal string, the order's total size — always the size it was placed for.
            This route never resizes. Omitted when unknown.
        message:
          type: string
          description: >-
            Human-readable outcome detail, e.g. "Order amended on exchange", "Order cannot be amended
            - status is filled", or "Could not amend on the exchange: <reason>". A success whose row
            write failed reads "Order amended on exchange (price not persisted)".
    TerminalFillVerification:
      type: object
      required:
      - state
      - exchange_order_id
      - reason
      - target_status
      - started_at
      properties:
        state:
          type: string
          enum: [pending, complete]
        exchange_order_id:
          type: string
          description: Exact venue-order generation covered by the barrier.
        reason:
          type: string
          enum: [explicit_cancel, venue_absent]
        target_status:
          type: string
          enum: [cancelled, expired]
        started_at:
          type: string
          format: date-time
        final_filled_quantity:
          type: string
          nullable: true
          description: Exact venue cumulative, including explicit `0`; required when state is complete.
        verified_at:
          type: string
          format: date-time
          nullable: true
          description: Receipt completion timestamp; required when state is complete.
    ReconciledPositionExposure:
      type: object
      required:
      - exchange_id
      - market_id
      - token_id
      - outcome
      - net_size
      - base_established
      - market_resolved
      properties:
        exchange_id:
          type: string
          description: Canonical venue identity.
          example: kalshi
        market_id:
          type: string
        token_id:
          type: string
        outcome:
          type: string
          nullable: true
        net_size:
          type: string
          description: Exact signed post-fold position size as a decimal string, including zero.
        base_established:
          type: boolean
          description: Explicit proof that the durable position base was established. A returned
            reconciliation authority always sets this to true; clients must fail closed when it is
            absent or false during rolling deployment.
        market_resolved:
          type: boolean
          nullable: true
          description: Tri-state resolution authority. Null means resolution was not proven and must
            not be interpreted as false.
    OrderCancelBatchRequest:
      type: object
      description: Body for `POST /orders/cancel-batch`.
      required:
      - order_ids
      properties:
        order_ids:
          type: array
          description: Internal order ids to cancel. Deduplicated server-side; must be non-empty and at
            most 100 entries.
          maxItems: 100
          items:
            type: string
            format: uuid
    OrderCancelBatchFailure:
      type: object
      description: One order the venue refused to cancel.
      required:
      - order_id
      - exchange_order_id
      - reason
      properties:
        order_id:
          type: string
          format: uuid
        exchange_order_id:
          type: string
          description: The venue-side order id that was rejected.
        reason:
          type: string
          description: Venue-reported reason the cancel was refused.
    OrderCancelBatchResponse:
      type: object
      description: Response for `POST /orders/cancel-batch`.
      required:
      - success
      - cancelled_count
      - noop_count
      - failed_count
      - cancelled_order_ids
      - noop_order_ids
      - failures
      properties:
        success:
          type: boolean
          description: True iff `failed_count == 0`.
        cancelled_count:
          type: integer
        noop_count:
          type: integer
          description: Orders the venue reported as a no-op (e.g. already filled/cancelled at the venue)
            — still counted as handled, not a failure.
        failed_count:
          type: integer
        cancelled_order_ids:
          type: array
          items:
            type: string
            format: uuid
        noop_order_ids:
          type: array
          items:
            type: string
            format: uuid
        failures:
          type: array
          items:
            $ref: '#/components/schemas/OrderCancelBatchFailure'
    OrderCancelAllRequest:
      type: object
      description: Body for `POST /orders/cancel-all`.
      required:
      - exchange_id
      properties:
        user_id:
          type: string
          deprecated: true
          description: Ignored. The authenticated caller's id is always used.
        exchange_id:
          type: string
          description: Venue to cancel all open orders on.
          example: polymarket
        market_id:
          type: string
          nullable: true
          description: Optionally scope the cancel-all to a single market/contract id instead of every
            open order on the exchange.
    OrderCancelAllResponse:
      type: object
      description: Response for `POST /orders/cancel-all`.
      required:
      - cancelled_count
      - failed_count
      - cancelled_order_ids
      properties:
        cancelled_count:
          type: integer
          description: Authoritative count of orders the venue reported cancelled.
        failed_count:
          type: integer
          description: Local rows that failed to update to `cancelled` after the venue confirmed the cancel.
        cancelled_order_ids:
          type: array
          description: Internal order ids (as strings) whose local status was updated. May be fewer than
            `cancelled_count` if the venue cancelled more than the locally-tracked active set.
          items:
            type: string
    OrderFeeQuoteResponse:
      type: object
      description: >-
        Response for `GET /orders/fee-quote`. All numeric fields are decimal
        strings in USDC. The WebSocket `subscribe_fee_quote` stream emits a
        near-identical frame, but it omits `venue_reserve_fee_usdc` — do not
        treat the two as interchangeable. See `execution-ws.asyncapi.yaml`.
      required:
      - avg_price_usdc
      - filled_size
      - requested_size
      - sufficient_liquidity
      - notional_usdc
      - platform_fee_usdc
      - exchange_fee_usdc
      - venue_reserve_fee_usdc
      - total_fee_usdc
      - total_cost_usdc
      - pricing_unavailable
      - is_estimate
      - funding_tier
      - bridge_fee_usdc
      - bridge_eta_p50_ms
      - bridge_eta_p95_ms
      - bridge_route_label
      - bridge_min_txs
      - bridge_eta_state
      - bridge_quote_unavailable
      - bridge_beta
      properties:
        avg_price_usdc:
          type: string
          description: Size-weighted executable price (VWAP for a market order), or the limit price for
            a limit order.
          example: '0.52'
        filled_size:
          type: string
          description: Size the book can actually fill; equals `requested_size` when liquidity suffices.
          example: '100'
        requested_size:
          type: string
          description: The size the caller asked to quote (echoes `quantity`).
          example: '100'
        sufficient_liquidity:
          type: boolean
          description: False when the book cannot fill the full `requested_size`.
        notional_usdc:
          type: string
          description: '`filled_size × avg_price_usdc`.'
        platform_fee_usdc:
          type: string
          description: Kairos platform fee, based on the caller's fee tier. `"0"` if the tier lookup failed
            (fails open, never a phantom rate).
        exchange_fee_usdc:
          type: string
          description: Venue-specific fee the caller is expected to pay. `"0"` on venues that only charge
            takers when this quote is a resting maker order.
        venue_reserve_fee_usdc:
          type: string
          description: Fee the VENUE actually reserves to accept the order — distinct from `exchange_fee_usdc`.
            Some venues (Polymarket CLOB) can't know a resting limit will stay maker, so they reserve
            the taker estimate at placement regardless; buy-affordability checks must size off this field,
            not `exchange_fee_usdc`.
        total_fee_usdc:
          type: string
          description: '`platform_fee_usdc + exchange_fee_usdc`.'
        exchange_fee_note:
          type: string
          nullable: true
          description: Human-readable note on the exchange fee, e.g. "1.8% taker fee".
        total_cost_usdc:
          type: string
          description: All-in cost — for a buy, `notional + fees`; for a sell, `proceeds = notional −
            fees`.
        pricing_unavailable:
          type: boolean
          description: '`true` when no executable price was available (no client price and no fresh orderbook)
            — every other numeric field is a placeholder `"0"` and MUST NOT be rendered as a real quote.'
        is_estimate:
          type: boolean
          description: Always `true` — this is a display estimate; the authoritative fee is computed at
            fill time.
        funding_tier:
          type: string
          enum: [t0_local, t1_prepositioned, t2_bridge, reject]
          description: Which rails run to fund this order. `t0_local` means no collateral moves; only
            `t2_bridge` carries a bridge fee. It names the movement, not whether the balance suffices.
        bridge_fee_usdc:
          type: string
          nullable: true
          description: Bridge fee in USDC. `"0"` below `t2_bridge`, and `null` exactly when
            `bridge_quote_unavailable` is `true` — an unknown fee is never rendered as `$0.00`. This fee
            is already included in `total_cost_usdc`, and buy-affordability checks must include it too.
        bridge_eta_p50_ms:
          type: integer
          nullable: true
          description: Median bridge fill time from Kairos's own completed intents, never a provider
            estimate. Present only once the route has ≥1,000 samples and its p90 ETA error is within ±10s.
        bridge_eta_p95_ms:
          type: integer
          nullable: true
          description: 95th-percentile bridge fill time from Kairos's own completed intents. Present
            under the same gate as `bridge_eta_p50_ms`.
        bridge_route_label:
          type: string
          nullable: true
          description: The route in the user's words, naming the token the rail DELIVERS, e.g. "BNB USDT
            → Polymarket pUSD via Relay". Absent when nothing bridges.
        bridge_min_txs:
          type: integer
          nullable: true
          description: On-chain transactions the bridge route needs, so a client can price the signing
            path. Absent when nothing bridges.
        bridge_eta_state:
          type: string
          nullable: true
          enum: [measured, not_yet_measured]
          description: Whether the ETA above is a measurement or an admission that the route has not
            earned one. Absent when nothing bridges.
        bridge_quote_unavailable:
          type: boolean
          description: '`true` when a bridge is needed and no quote landed. Consumers MUST render a
            "routing…" state and disable submit rather than showing any fee; `bridge_fee_usdc` is `null`
            in this state.'
        bridge_beta:
          type: boolean
          description: '`true` while tier-2 bridge funding is behind a flag, so the bridge row can be
            labelled beta.'
    OrderPositionExposure:
      type: object
      description: A single open position, from `positions[]` in the exposure response.
      required:
      - token_id
      - market_id
      - holding_wallet
      - net_size
      - available_to_sell
      - reserved
      - avg_entry_price_bps
      - realized_pnl
      - last_trade_at
      properties:
        token_id:
          type: string
        market_id:
          type: string
        outcome:
          type: string
          nullable: true
        holding_wallet:
          type: string
        net_size:
          type: string
          description: Current signed size (decimal string), positive = long.
          example: '100'
        available_to_sell:
          type: string
          description: '`net_size` minus outstanding sell reservations — what a new SELL can reserve right
            now.'
        reserved:
          type: string
          description: 'Size committed to in-flight sells: `net_size - available_to_sell`.'
        avg_entry_price_bps:
          type: integer
          description: Average entry price in basis points (price × 10000).
          example: 5200
        realized_pnl:
          type: string
          description: Realized PnL to date, decimal string.
        last_trade_at:
          type: string
          format: date-time
    OrderResolvedPositionExposure:
      type: object
      description: A held position whose market has resolved, from `resolved[]` in the exposure response.
      required:
      - token_id
      - market_id
      - holding_wallet
      - net_size
      - redeemable
      properties:
        token_id:
          type: string
        market_id:
          type: string
        outcome:
          type: string
          nullable: true
        holding_wallet:
          type: string
        net_size:
          type: string
          description: Size still on the books for this resolved token. Informational only — may lag zero
            for a settled loser.
        redeemable:
          type: boolean
          description: '`true` = won (claimable via redeem); `false` = lost.'
    OrderPositionsExposureResponse:
      type: object
      description: Response for `GET /positions/exposure`.
      required:
      - positions
      - closed_token_ids
      - closed
      - resolved
      properties:
        positions:
          type: array
          items:
            $ref: '#/components/schemas/OrderPositionExposure'
        closed_token_ids:
          type: array
          description: Token ids the store positively holds at `net_size == 0` (folded flat, e.g. a just-settled
            sell). Absence from both this list and `positions`/`resolved` means "no current opinion."
            Carries no venue, so on a cell serving several it cannot say which venue's position to
            retire — read `closed` instead.
          items:
            type: string
        closed:
          type: array
          description: The same retirements as `closed_token_ids`, each scoped to the venue that owns it.
          items:
            $ref: '#/components/schemas/OrderClosedPositionExposure'
        resolved:
          type: array
          items:
            $ref: '#/components/schemas/OrderResolvedPositionExposure'
    OrderClosedPositionExposure:
      type: object
      description: One retirement, scoped to the venue that owns it.
      required:
      - exchange_id
      - token_id
      properties:
        exchange_id:
          type: string
          description: Canonical execution venue id. Token and market ids are unique only within a venue.
          example: hyperliquid
        token_id:
          type: string
          description: Outcome-token id the venue holds at `net_size == 0`.
    OrderIntentPayload:
      type: object
      description: The unsigned order intent — `intent` field of `OrderIntentRequest`, mirrored inside
        the one-RTT WSS `submit_signed_order` command.
      required:
      - token_id
      - side
      - price
      - size
      - time_in_force
      - neg_risk
      - owner_address
      - signature_type
      properties:
        token_id:
          type: string
          description: Polymarket outcome-token id (uint256, decimal string).
          example: '71360012345678901234567890123456789012345678901234567890123456'
        side:
          $ref: '#/components/schemas/OrderSide'
        price:
          type: string
          description: Decimal string, limit price in `[tick, 1]`. Caller is responsible for snapping
            to the market's tick — the server does not re-snap, so the digest you compute locally matches
            what the server recomputes.
          example: '0.52'
        size:
          type: string
          description: Decimal string, order size in shares.
          example: '100'
        time_in_force:
          $ref: '#/components/schemas/OrderTimeInForce'
        post_only:
          type: boolean
          description: Maker-only. NOT part of the signed EIP-712 digest — it rides on the outer venue
            payload. Requires a resting time-in-force (`GTC` or `GTD`); anything else is rejected with
            `400 post_only requires a resting time-in-force (GTC or GTD), not <tif>`.
          default: false
        expiration_unix_secs:
          type: integer
          nullable: true
          description: UNIX seconds expiration. Required if (and only meaningful when) `time_in_force=GTD`
            — a GTD intent without it is rejected, and one already in the past is rejected with
            `400 expiration_unix_secs is in the past for this GTD order`. On Polymarket this value is
            NOT part of the signed `Order` struct; it travels on the outer payload.
        neg_risk:
          type: boolean
          description: Whether the market is a neg-risk market — selects the EIP-712 verifying contract.
            On Polymarket the caller is expected to know this. On Predict.fun the value is
            CROSS-CHECKED against authoritative market metadata and a mismatch is a `400`, never a
            silent correction.
        owner_address:
          type: string
          description: 0x-checksummed EOA address that will sign the digest. For `signature_type=0`
            (EOA) this MUST equal both the order maker and signer. On Predict.fun it must additionally
            be your registered Predict.fun trading wallet.
          example: '0x0000000000000000000000000000000000dEaD'
        signer_address:
          type: string
          nullable: true
          description: Signer address. On this lane it must equal `owner_address` (or be omitted); a
            different value is rejected with `400 signer_address must equal owner_address for signature_type
            0 (EOA)`.
        signature_type:
          type: integer
          description: Signature-type discriminant. Only `0` (EOA) is accepted on this lane — the intake
            validator rejects anything else with `400 only signature_type 0 (EOA) is supported on the
            external-signing lane`. (The underlying Polymarket builder also understands `2` = Poly1271,
            but the HTTP/WSS handlers never let it through, so verification here is always raw
            `ecrecover` and never EIP-1271.)
          enum:
          - 0
          example: 0
        salt:
          type: string
          nullable: true
          description: uint256 decimal string. If omitted, the server generates one and returns it in
            `unsigned_payload`. REQUIRED (along with `timestamp_ms`) if you build and sign the order fully
            client-side over the one-RTT WSS `submit_signed_order` command instead of this two-RTT REST
            flow.
        timestamp_ms:
          type: string
          nullable: true
          description: uint256 decimal string, order timestamp in milliseconds (CLOB V2 uses this in place
            of a nonce). If omitted, the server stamps it. See `salt` for when it's required.
    OrderEipDomain:
      type: object
      description: EIP-712 domain separator fields.
      required:
      - name
      - version
      - chain_id
      - verifying_contract
      properties:
        name:
          type: string
          example: Polymarket CTF Exchange
        version:
          type: string
          example: '2'
        chain_id:
          type: integer
          example: 137
        verifying_contract:
          type: string
          example: '0xE111180000d2663C0091e4f400237545B87B996B'
    OrderUnsignedPayload:
      type: object
      description: Full EIP-712 typed-data payload — pass directly to `eth_signTypedData` as an alternative
        to raw-hash-signing `eip712_digest_hex`.
      required:
      - domain
      - primary_type
      - types
      - message
      - eip712_digest_hex
      properties:
        domain:
          $ref: '#/components/schemas/OrderEipDomain'
        primary_type:
          type: string
          example: Order
        types:
          type: object
          description: The full EIP-712 type set for `eth_signTypedData`.
          additionalProperties: true
        message:
          type: object
          description: The order struct's field values, as they will be hashed.
          additionalProperties: true
        eip712_digest_hex:
          type: string
          description: 0x-prefixed 32-byte digest. Sign this raw (no EIP-191 prefix) to produce a signature
            identical to signing `message` via `eth_signTypedData`.
          example: '0x9a1c2b3d4e5f60718293a4b5c6d7e8f90112233445566778899aabbccddeeff0'
    OrderIntentRequest:
      type: object
      description: Body for `POST /v2/orders/intent`.
      required:
      - intent
      properties:
        provider:
          $ref: '#/components/schemas/OrderExternalProvider'
        intent:
          $ref: '#/components/schemas/OrderIntentPayload'
        market_id:
          type: string
          nullable: true
          description: Condition id — METADATA ONLY, not part of the signed digest, but REQUIRED in
            practice. The server resolves `intent.token_id` against it and rejects a blank value with
            `400 market_id is required for external orders`, a token that does not belong to it with
            `400 token_id does not belong to the supplied market_id`. Max 256 chars.
        outcome:
          type: string
          nullable: true
          description: REQUIRED — outcome label (e.g. `"Yes"`/`"No"`); metadata only, not signed. A
            missing/empty value is `400 outcome is required for external orders`, and a label that does
            not match the resolved token is `400 token_id does not match the supplied outcome`. Max 64
            chars.
          example: 'Yes'
    OrderExternalProvider:
      type: string
      description: Target venue for the external-signing lane. Only these two venues are implemented;
        Kalshi and Solana variants exist in the internal types but are not reachable on `/v2/orders/*`.
        The one-RTT WebSocket `submit_signed_order` command and the whole `/v2/onchain/*` lane are
        Polymarket-only and have no `provider` field.
      enum:
      - polymarket
      - predictfun
      default: polymarket
      example: polymarket
    OrderIntentResponse:
      type: object
      description: Response for `POST /v2/orders/intent`.
      required:
      - payload_id
      - unsigned_payload
      - eip712_digest_hex
      - expires_at_us
      properties:
        payload_id:
          type: string
          format: uuid
          description: Single-use handle for this stored intent. Pass back to `POST /v2/orders/submit`.
        unsigned_payload:
          $ref: '#/components/schemas/OrderUnsignedPayload'
        eip712_digest_hex:
          type: string
          description: Convenience copy of `unsigned_payload.eip712_digest_hex` — the digest to sign.
        expires_at_us:
          type: integer
          format: int64
          description: 'Wall-clock expiry, UNIX microseconds (60 s out by default,
            `EXTERNAL_SIGNING_INTENT_TTL_SECS`). The stored intent is claimed with an atomic Redis
            `GETDEL` at the very top of `/submit`, BEFORE any signature verification — so it is consumed
            by ANY submit attempt, not only a successful one. A `/submit` that fails verification burns
            the `payload_id`; retrying needs a fresh `POST /v2/orders/intent`.'
    OrderSubmitSignedRequest:
      type: object
      description: Body for `POST /v2/orders/submit`.
      required:
      - payload_id
      - signature_hex
      properties:
        payload_id:
          type: string
          format: uuid
          description: The `payload_id` returned by `POST /v2/orders/intent`. Single-use — claimed atomically
            on the first `/submit` call.
        signature_hex:
          type: string
          description: 0x-prefixed 65-byte secp256k1 signature (`r || s || v`) over `eip712_digest_hex`.
          example: 0x1234...1b
    OrderSubmitSignedResponse:
      type: object
      description: Response for `POST /v2/orders/submit` (and the equivalent one-RTT WSS `submit_signed_order`
        command).
      required:
      - order_id
      - status
      properties:
        order_id:
          type: string
          format: uuid
          description: Internal Kairos order id — usable for `GET /orders/{order_id}` and the cancel endpoints.
        exchange_order_id:
          type: string
          nullable: true
          description: Venue-assigned order handle. An EMPTY venue id is treated as a failed submission
            and returned as `400 venue returned an empty order id; order not tracked`, so on a `200` this
            is in practice always populated; it is `null` only on the idempotent-replay response path.
        status:
          type: string
          description: 'The venue''s immediate result string, passed through verbatim — Kairos does not
            normalize or validate it. Polymarket''s observed values are `matched`, `live` and `delayed`;
            `unmatched` appears on the fill-polling path. Treat this as an open string, not a closed
            enum.'
          example: matched
    OrderOnchainIntentRequest:
      type: object
      description: Body for `POST /v2/onchain/intent`. You supply the nonce and gas price yourself —
        the server makes no RPC call while building the intent (no nonce fetch, no gas quote, no balance
        preflight).
      required:
      - op
      - owner_address
      - nonce
      - gas_price_wei
      properties:
        op:
          type: string
          description: 'Which on-chain operation to build. `approvals` builds the one-time collateral +
            CTF approvals; `redeem` claims a resolved position; `split`/`merge` convert between collateral
            and a complete outcome-token set; `unwrap_wcol` unwraps wrapped collateral.'
          enum:
          - approvals
          - redeem
          - split
          - merge
          - unwrap_wcol
          example: redeem
        owner_address:
          type: string
          description: The EOA that will sign and broadcast. Must be a wallet registered to the authenticated
            caller.
        condition_id:
          type: string
          nullable: true
          description: CTF conditionId. Required for `redeem`, `split` and `merge`.
        neg_risk:
          type: boolean
          nullable: true
          description: Whether the market is a neg-risk market (selects the adapter contract).
        collateral_address:
          type: string
          nullable: true
        amount:
          type: string
          nullable: true
          description: Amount in wei, decimal string. Required for `split` and `merge`; must be > 0.
        nonce:
          type: integer
          format: int64
          description: The signing EOA's next transaction nonce. Caller-supplied — the server never
            queries the chain for it.
        gas_price_wei:
          type: string
          description: Gas price in wei, decimal string. Must be > 0. Caller-supplied.
        gas_limit:
          type: integer
          format: int64
          nullable: true
        wcol_amount:
          type: string
          nullable: true
          description: Amount in wei, decimal string. Required for `unwrap_wcol`; must be > 0.
    OrderUnsignedTx:
      type: object
      description: One pinned unsigned transaction to sign and hand back to `POST /v2/onchain/submit`.
      required:
      - description
      - to
      - data_hex
      - value
      - nonce
      - gas
      - gas_price
      - chain_id
      - signing_digest_hex
      - unsigned_rlp_hex
      properties:
        description:
          type: string
          description: Human-readable label for this leg (e.g. which approval it grants).
        to:
          type: string
        data_hex:
          type: string
        value:
          type: string
        nonce:
          type: integer
          format: int64
        gas:
          type: string
        gas_price:
          type: string
        chain_id:
          type: integer
          description: Always `137` — this lane is Polygon/Polymarket-only.
          example: 137
        signing_digest_hex:
          type: string
          description: The legacy EIP-155 sighash (`keccak256` of the unsigned RLP). Sign these raw 32
            bytes — no EIP-191 prefix. The server normalizes `v` to `35 + 2·chain_id + parity` itself.
        unsigned_rlp_hex:
          type: string
    OrderOnchainIntentResponse:
      type: object
      description: Response for `POST /v2/onchain/intent`.
      required:
      - payload_id
      - transactions
      - expires_at_us
      properties:
        payload_id:
          type: string
          format: uuid
          description: Single-use handle. Pass back to `POST /v2/onchain/submit`.
        transactions:
          type: array
          description: The transactions to sign, in the order they must be broadcast (ascending nonce).
          items:
            $ref: '#/components/schemas/OrderUnsignedTx'
        expires_at_us:
          type: integer
          format: int64
          description: Wall-clock expiry, UNIX microseconds. TTL is 300 s here (longer than the 60 s
            order-intent TTL, because signing several transactions on a hardware wallet takes longer).
        requires_followup:
          type: string
          nullable: true
          description: Reserved. Always absent on a newly-created intent.
    OrderOnchainSubmitRequest:
      type: object
      description: Body for `POST /v2/onchain/submit`.
      required:
      - payload_id
      - signatures
      properties:
        payload_id:
          type: string
          format: uuid
        signatures:
          type: array
          description: One 0x-hex 65-byte signature per transaction, in the same order as
            `transactions[]` from the intent. The count must match exactly.
          items:
            type: string
    OrderOnchainSubmitResponse:
      type: object
      description: Response for `POST /v2/onchain/submit`.
      required:
      - transaction_hashes
      - status
      properties:
        transaction_hashes:
          type: array
          items:
            type: string
        status:
          type: string
          description: Always `confirmed` on a 200 — each transaction is broadcast and waited on
            (90 s) in nonce order before the next.
          enum:
          - confirmed
    OrderHealthResponse:
      type: object
      description: Response for `GET /health`. Always returned with HTTP 200 — a degraded backing store
        flips the fields rather than the status code, because this is the load balancer's liveness probe.
      required:
      - status
      - version
      - healthy
      properties:
        status:
          type: string
          enum:
          - ok
          - degraded
        version:
          type: string
        healthy:
          type: boolean
          description: '`false` (with `status: degraded`) when the Redis ping failed.'
    OrderSupportedOrderTypes:
      type: object
      description: Which order types the venue's adapter implements.
      required:
      - market
      - limit
      - stop_loss
      - stop_limit
      - take_profit
      - trailing_stop
      properties:
        market:
          type: boolean
        limit:
          type: boolean
        stop_loss:
          type: boolean
        stop_limit:
          type: boolean
        take_profit:
          type: boolean
        trailing_stop:
          type: boolean
    OrderExchangeCapabilities:
      type: object
      description: |-
        What a venue supports. This is the authoritative source for the per-venue constraints
        `POST /orders` enforces — a `time_in_force` outside `supported_tif`, or `post_only` on a venue
        with `supports_post_only: false`, is rejected with `400`, never silently downgraded.

        Current production values:

        | Venue | `supported_tif` | `supports_post_only` | `min_tick_size` | `max_price` | `settlement_currency` |
        |---|---|---|---|---|---|
        | `polymarket` | GTC, GTD, FOK, FAK, IOC | true | 0.01 | 1 | USDC |
        | `predictfun` | GTC, GTD, FOK, FAK, IOC | true | 0.001 | 1 | USDT |
        | `kalshi` | GTC, GTD, IOC, FAK, FOK | true | 0.01 | 1 | USD |
        | `hyperliquid` | GTC, GTD, IOC, FAK, FOK | true (expressed venue-side as the `Alo` TIF) | 0.0001 | none | USDC |
        | `opinion` | GTC only | false | 0.01 | 1 | USDC |

        `kalshi_offchain` is a deprecated alias that canonicalizes to `kalshi`.
      required:
      - exchange_id
      - display_name
      - supported_order_types
      - supported_tif
      - requires_allowances
      - supports_redemption
      - has_outcome_tokens
      - supports_user_websocket
      - supports_cancel_all
      - supports_batch_orders
      - min_tick_size
      - min_order_size
      - fee_model
      - settlement_currency
      - autonomous_settlement
      - settlement_deferred_fill
      - is_active
      properties:
        exchange_id:
          type: string
          description: The CANONICAL venue id — may differ from the id you requested (`kalshi_offchain`
            resolves to `kalshi`).
        display_name:
          type: string
        supported_order_types:
          $ref: '#/components/schemas/OrderSupportedOrderTypes'
        supported_tif:
          type: array
          items:
            $ref: '#/components/schemas/OrderTimeInForce'
        supports_post_only:
          type: boolean
          default: false
        requires_allowances:
          type: boolean
        supports_redemption:
          type: boolean
        has_outcome_tokens:
          type: boolean
        supports_user_websocket:
          type: boolean
        supports_cancel_all:
          type: boolean
          description: When `false`, `POST /orders/cancel-all` is not available for this venue.
        supports_batch_orders:
          type: boolean
        min_tick_size:
          type: string
          description: Decimal string.
        min_order_size:
          type: string
          description: Decimal string.
        max_order_size:
          type: string
          nullable: true
        fee_model:
          type: string
          enum:
          - zero_fee
          - maker_rebate
          - tiered_schedule
          - fixed_bps
        maker_fee_bps:
          type: integer
          nullable: true
        taker_fee_bps:
          type: integer
          nullable: true
        chain_id:
          type: string
          nullable: true
          description: Decimal string (e.g. `"137"`), or null for off-chain venues.
        settlement_currency:
          type: string
        autonomous_settlement:
          type: boolean
        settlement_deferred_fill:
          type: boolean
        sell_quantity_decimals:
          type: integer
          nullable: true
          description: Decimal places a SELL quantity is truncated to before submission.
        max_price:
          type: string
          nullable: true
          description: Decimal string. The ceiling `price` and `trigger_price` are validated against.
        is_active:
          type: boolean
        ws_fill_authoritative:
          type: boolean
          default: false
        ws_fills_include_exchange_fee:
          type: boolean
          default: false
        supports_native_amend:
          type: boolean
          default: false
          description: Venue-native amend exists. FIX replace otherwise uses durable synthetic replacement.
    OrderExchangeInfo:
      type: object
      description: Response for `GET /exchanges/{exchange_id}`.
      required:
      - id
      - name
      - is_active
      - capabilities
      properties:
        id:
          type: string
          description: Echoes the id you asked for verbatim — NOT canonicalized. Use `capabilities.exchange_id`
            for the canonical value.
        name:
          type: string
        is_active:
          type: boolean
        capabilities:
          $ref: '#/components/schemas/OrderExchangeCapabilities'
    OrderSetAllowancesRequest:
      type: object
      description: Body for `POST /exchanges/{exchange_id}/allowances`. All three identity fields are
        required by the schema but are VALIDATED against the authenticated caller, never trusted — they
        can confirm authority, never grant it.
      required:
      - user_id
      - turnkey_org_id
      - wallet_address
      properties:
        user_id:
          type: string
        turnkey_org_id:
          type: string
        wallet_address:
          type: string
    OrderSetAllowancesResponse:
      type: object
      required:
      - success
      properties:
        usdc_tx_hash:
          type: string
          nullable: true
        ctf_tx_hash:
          type: string
          nullable: true
        success:
          type: boolean
    OrderRouteQuoteLeg:
      type: object
      required:
      - provider
      - market_id
      - token_id
      - quantity
      - limit_price
      - effective_limit
      - est_notional
      - est_fee
      - below_min_notional
      properties:
        provider:
          type: string
        market_id:
          type: string
        token_id:
          type: string
        quantity:
          type: string
        limit_price:
          type: string
        effective_limit:
          type: string
        est_notional:
          type: string
        est_fee:
          type: string
        below_min_notional:
          type: boolean
    OrderRouteQuoteResponse:
      type: object
      description: 'Response for `GET /orders/route-quote`. A quote that cannot be routed is still a `200`
        with `routed` set false — check the flag, not the status code.'
      required:
      - routed
      - notes
      - legs
      - requested
      - planned
      - unfilled
      properties:
        routed:
          type: boolean
        link_id:
          type: string
          format: uuid
          nullable: true
        link_title:
          type: string
          nullable: true
        notes:
          type: array
          items:
            type: string
        legs:
          type: array
          items:
            $ref: '#/components/schemas/OrderRouteQuoteLeg'
        requested:
          type: string
        planned:
          type: string
        unfilled:
          type: string
        blended_effective:
          type: string
          nullable: true
        stop:
          type: string
          nullable: true
          description: Why planning stopped.
          enum:
          - filled
          - exhausted
          - effective_cap
    OrderRouteFeeModelView:
      type: object
      required:
      - model
      - estimated
      properties:
        model:
          type: string
          enum:
          - curve
          - min_side_bps
          - none
        rate_ppm:
          type: integer
          format: int64
          nullable: true
        bps:
          type: integer
          nullable: true
        estimated:
          type: boolean
    OrderRouteFeesResponse:
      type: object
      description: 'Response for `GET /orders/route-fees`. An unknown link is a `200` with `routed` set
        false and an empty `legs`.'
      required:
      - routed
      - legs
      properties:
        routed:
          type: boolean
          description: '`true` only when at least two routable legs were resolved.'
        legs:
          type: array
          items:
            type: object
            required:
            - provider
            - market_id
            - stream_key
            properties:
              provider:
                type: string
              market_id:
                type: string
              stream_key:
                type: string
              'yes':
                oneOf:
                - $ref: '#/components/schemas/OrderRouteFeeModelView'
                - type: 'null'
              'no':
                oneOf:
                - $ref: '#/components/schemas/OrderRouteFeeModelView'
                - type: 'null'
    OrderRouteBuyRequest:
      type: object
      description: Body for `POST /orders/route-buy`.
      required:
      - provider
      - market_id
      - quantity
      properties:
        provider:
          type: string
        market_id:
          type: string
        side:
          type: string
          nullable: true
          description: '`yes` or `no`. Supply this or `outcome`.'
          enum:
          - 'yes'
          - 'no'
        outcome:
          type: string
          nullable: true
          description: Venue-native outcome label, as an alternative to `side`.
        quantity:
          type: string
          description: Decimal string, must be > 0.
        slippage_cents:
          type: integer
          nullable: true
          description: Must be in `[1, 99]` if provided.
    OrderRouteBuyLegResult:
      type: object
      required:
      - provider
      - planned_qty
      - planned_limit_price
      properties:
        provider:
          type: string
        order_id:
          type: string
          format: uuid
          nullable: true
          description: Null when this leg failed to submit — see `error`.
        planned_qty:
          type: string
        planned_limit_price:
          type: string
        error:
          type: string
          nullable: true
          description: Per-leg submission failure, reported at HTTP 200. An opaque debug rendering of the
            underlying order error — do not parse it.
    OrderRouteBuyResponse:
      type: object
      description: Response for `POST /orders/route-buy`. Individual legs can fail while the request
        succeeds — always inspect `legs[].error`.
      required:
      - route_order_id
      - status
      - requested
      - planned
      - legs
      - notes
      properties:
        route_order_id:
          type: string
          format: uuid
        status:
          type: string
          description: Always `submitting` — children are dispatched asynchronously. Poll `GET /orders/routes`.
          enum:
          - submitting
        requested:
          type: string
        planned:
          type: string
        legs:
          type: array
          items:
            $ref: '#/components/schemas/OrderRouteBuyLegResult'
        notes:
          type: array
          items:
            type: string
    OrderRouteCloseRequest:
      type: object
      description: Body for `POST /orders/route-close`.
      required:
      - provider
      - market_id
      properties:
        provider:
          type: string
        market_id:
          type: string
        side:
          type: string
          nullable: true
          enum:
          - 'yes'
          - 'no'
        outcome:
          type: string
          nullable: true
        slippage_cents:
          type: integer
          nullable: true
          description: Defaults to `5`. Must be in `[1, 99]` if provided.
          default: 5
        leg_caps:
          type: array
          nullable: true
          description: 'Per-venue size caps. FAIL-CLOSED: when `leg_caps` is present, any leg with no
            matching entry is EXCLUDED from the close entirely rather than closed in full.'
          items:
            type: object
            required:
            - provider
            - quantity
            properties:
              provider:
                type: string
              quantity:
                type: string
    OrderRouteCloseResponse:
      type: object
      description: Response for `POST /orders/route-close`.
      required:
      - route_order_id
      - status
      - closing
      - legs
      - notes
      properties:
        route_order_id:
          type: string
          format: uuid
        status:
          type: string
          enum:
          - submitting
        closing:
          type: string
        legs:
          type: array
          items:
            type: object
            required:
            - provider
            - holdings
            properties:
              provider:
                type: string
              order_id:
                type: string
                format: uuid
                nullable: true
              holdings:
                type: string
              floor_price:
                type: string
                nullable: true
              error:
                type: string
                nullable: true
                description: 'Per-leg failure at HTTP 200, e.g. `no bid available for floor — leg not
                  sold` or `no floor computed`.'
        notes:
          type: array
          items:
            type: string
    OrderRouteView:
      type: object
      description: One routed parent order, from `GET /orders/routes`.
      required:
      - id
      - provider
      - market_id
      - action
      - side
      - requested_qty
      - status
      - filled_qty
      - created_at
      - legs
      properties:
        id:
          type: string
          format: uuid
        market_link_id:
          type: string
          format: uuid
          nullable: true
        provider:
          type: string
        market_id:
          type: string
        action:
          type: string
          enum:
          - buy
          - sell
        side:
          type: string
        requested_qty:
          type: string
        status:
          type: string
        filled_qty:
          type: string
        avg_fill_price_bps:
          type: integer
          nullable: true
        created_at:
          type: string
          description: Naive local timestamp — serialized WITHOUT a timezone offset, so it is not an
            RFC 3339 instant. Unlike `Order.created_at`.
        completed_at:
          type: string
          nullable: true
          description: Naive local timestamp, same caveat as `created_at`.
        legs:
          type: array
          items:
            type: object
            required:
            - provider
            - planned_qty
            - planned_limit_price_bps
            properties:
              provider:
                type: string
              market_id:
                type: string
                nullable: true
              planned_qty:
                type: string
              planned_limit_price_bps:
                type: integer
              order_id:
                type: string
                format: uuid
                nullable: true
              order_status:
                type: string
                nullable: true
              filled_size:
                type: string
                nullable: true
              avg_fill_price_bps:
                type: integer
                nullable: true
              token_id:
                type: string
                nullable: true
              outcome:
                type: string
                nullable: true
              error:
                type: string
                nullable: true
    OrderCredentialsInvalidateRequest:
      type: object
      description: Body for `POST /credentials/invalidate`.
      required:
      - user_id
      properties:
        user_id:
          type: string
          description: Must equal the authenticated caller's id — a mismatch is a `403`.
        exchange_id:
          type: string
          nullable: true
          description: Omit to invalidate the cached credentials for EVERY registered exchange. NOT
            canonicalized, so a deprecated alias such as `kalshi_offchain` may `404`.
    OrderCredentialsInvalidateResponse:
      type: object
      required:
      - success
      properties:
        success:
          type: boolean
          description: Always `true` on the success path.
    OrderComboExecuteRequest:
      type: object
      description: 'Body for `POST /combo/execute`. NOTE: the whole `/combo/*` family uses **camelCase**
        field names, where the Orders endpoints are snake_case. Do not assume one convention across this
        API.'
      required:
      - legPositionIds
      - notionalUsd
      properties:
        legPositionIds:
          type: array
          description: The YES-outcome position id of each leg market. Minimum 2, maximum 10 legs.
          minItems: 2
          maxItems: 10
          items:
            type: string
        notionalUsd:
          type: number
          format: double
          description: USD notional to spend on the combo (e.g. `10.0` = $10). Must be finite and > 0,
            and large enough to round to a non-zero six-decimal amount.
          example: 10.0
        maxPriceCents:
          type: integer
          nullable: true
          description: Reject the quote if the blended price exceeds this, in cents per share (e.g. `60`
            = $0.60). Strongly recommended — without it you accept whatever the maker quotes.
          example: 60
    OrderComboExecuteResponse:
      type: object
      description: Response for `POST /combo/execute`.
      required:
      - rfqId
      - quoteId
      - yesPositionId
      - blendedPriceE6
      - totalRequiredE6
      - status
      properties:
        rfqId:
          type: string
        quoteId:
          type: string
        conditionId:
          type: string
          nullable: true
        yesPositionId:
          type: string
          description: The combo's synthetic YES outcome position id.
        blendedPriceE6:
          type: string
          description: Blended maker price as a six-decimal fixed-point integer string (e.g. `"16393"`
            = 0.016393). NOT a decimal — divide by 1,000,000.
          example: '16393'
        totalRequiredE6:
          type: string
          description: pUSD spent, six-decimal fixed-point integer string.
        status:
          type: string
          description: The RFQ gateway's settlement status, passed through verbatim.
        txHash:
          type: string
          nullable: true
    OrderComboQuoteRequest:
      type: object
      description: Body for `POST /combo/quote`.
      required:
      - legPositionIds
      - notionalUsd
      properties:
        legPositionIds:
          type: array
          minItems: 2
          maxItems: 10
          items:
            type: string
        notionalUsd:
          type: number
          format: double
          example: 10.0
    OrderComboQuoteResponse:
      type: object
      description: Response for `POST /combo/quote`.
      required:
      - yesPositionId
      - blendedPriceE6
      - totalRequiredE6
      properties:
        yesPositionId:
          type: string
        conditionId:
          type: string
          nullable: true
        blendedPriceE6:
          type: string
          description: Six-decimal fixed-point integer string.
        totalRequiredE6:
          type: string
          description: pUSD the taker must hold, six-decimal fixed-point integer string.
    OrderComboLeg:
      type: object
      required:
      - legPositionId
      - outcomeLabel
      - currentPrice
      - legStatus
      - title
      - slug
      - outcome
      - imageUrl
      properties:
        legPositionId:
          type: string
        outcomeLabel:
          type: string
        currentPrice:
          type: string
          description: Live leg price in `0..1`, as an upstream-provided string.
        legStatus:
          type: string
          description: Authoritative per-leg resolution. Use this — never a price heuristic — to mark a
            leg won/lost/pending.
          enum:
          - OPEN
          - RESOLVED_WIN
          - RESOLVED_LOSS
        title:
          type: string
        slug:
          type: string
        outcome:
          type: string
        imageUrl:
          type: string
    OrderComboPosition:
      type: object
      required:
      - comboConditionId
      - comboPositionId
      - sharesBalance
      - entryCostUsdc
      - totalCostUsdc
      - realizedPayoutUsdc
      - status
      - redeemable
      - firstEntryAt
      - legsTotal
      - legsResolved
      - legsPending
      - legs
      properties:
        comboConditionId:
          type: string
          description: 31-byte hex. Pass this as `comboConditionId` to `POST /combo/redeem`.
        comboPositionId:
          type: string
        sharesBalance:
          type: string
          description: Shares held (decimal string). A winning combo redeems 1:1 at $1, so this is also
            the maximum payout in USD.
        entryCostUsdc:
          type: string
        totalCostUsdc:
          type: string
        realizedPayoutUsdc:
          type: string
        status:
          type: string
          description: 'Combo-level status from upstream. NOTE: it stays `OPEN` for a won-but-unredeemed
            combo — do NOT infer redeemability from it. Use `redeemable`.'
        redeemable:
          type: boolean
          description: The single source of truth for "can redeem now" — `true` only for a resolved WIN
            with a non-zero balance. Covers "not resolved", "lost" and "already redeemed" in one flag.
        firstEntryAt:
          type: string
        legsTotal:
          type: integer
        legsResolved:
          type: integer
        legsPending:
          type: integer
        legs:
          type: array
          items:
            $ref: '#/components/schemas/OrderComboLeg'
    OrderComboPositionsResponse:
      type: object
      description: Response for `GET /combo/positions`.
      required:
      - combos
      properties:
        combos:
          type: array
          description: Capped at 50 combos, newest/most-valuable first. There is no pagination parameter.
          items:
            $ref: '#/components/schemas/OrderComboPosition'
    OrderComboCashOutQuoteRequest:
      type: object
      description: Body for `POST /combo/cash-out-quote`.
      required:
      - legPositionIds
      - shares
      properties:
        legPositionIds:
          type: array
          description: The combo's leg position ids, from the position's `legs[].legPositionId`.
          minItems: 2
          maxItems: 10
          items:
            type: string
        shares:
          type: number
          format: double
          description: Shares to price. Must be finite and > 0. Pass the full `sharesBalance` to preview
            a complete close.
    OrderComboCashOutQuoteResponse:
      type: object
      description: Response for `POST /combo/cash-out-quote`. All amounts are six-decimal fixed-point
        integer strings.
      required:
      - rfqId
      - proceedsE6
      - feeE6
      - netProceedsE6
      - blendedPriceE6
      properties:
        rfqId:
          type: string
        proceedsE6:
          type: string
          description: GROSS pUSD proceeds from the maker quote. The `minProceedsUsd` floor on
            `POST /combo/cash-out` is checked against this, not against the net.
        feeE6:
          type: string
          description: Estimated Kairos platform fee on the proceeds.
        netProceedsE6:
          type: string
          description: Proceeds after the platform fee — what actually lands. This is the number to show
            in a cash-out preview.
        blendedPriceE6:
          type: string
    OrderComboCashOutRequest:
      type: object
      description: Body for `POST /combo/cash-out`.
      required:
      - legPositionIds
      - shares
      properties:
        legPositionIds:
          type: array
          minItems: 2
          maxItems: 10
          items:
            type: string
        shares:
          type: number
          format: double
          description: Shares to sell. Must be finite and > 0.
        minProceedsUsd:
          type: number
          format: double
          nullable: true
          description: Reject the cash-out if GROSS proceeds fall below this USD floor (the platform fee
            is charged after the floor check). Recommended.
    OrderComboCashOutResponse:
      type: object
      description: Response for `POST /combo/cash-out`.
      required:
      - rfqId
      - sharesSoldE6
      - proceedsE6
      - blendedPriceE6
      - status
      properties:
        rfqId:
          type: string
        sharesSoldE6:
          type: string
        proceedsE6:
          type: string
        blendedPriceE6:
          type: string
        status:
          type: string
        txHash:
          type: string
          nullable: true
    OrderComboRedeemRequest:
      type: object
      description: Body for `POST /combo/redeem`.
      required:
      - comboConditionId
      properties:
        comboConditionId:
          type: string
          description: The resolved combo's `comboConditionId` (31-byte hex) from `GET /combo/positions`.
            The amount redeemed is always the position's real on-chain share balance — never caller-supplied
            — so a wrong or stale value cannot over- or under-redeem.
        outcomeIndex:
          type: integer
          nullable: true
          description: Winning outcome side; `0` = YES (all legs won). Defaults to `0`.
          default: 0
    OrderComboRedeemResponse:
      type: object
      description: Response for `POST /combo/redeem`.
      required:
      - batchTxId
      - amountE6
      - status
      properties:
        batchTxId:
          type: string
          description: The Polymarket relayer batch id for the redemption.
        amountE6:
          type: string
          description: Amount redeemed, six-decimal fixed-point integer string.
        status:
          type: string
    OrderMarketLinkLegRef:
      type: object
      required:
      - provider
      - market_id
      properties:
        provider:
          type: string
          description: Routable venue. Only `polymarket` and `predictfun` are accepted.
          enum:
          - polymarket
          - predictfun
        market_id:
          type: string
          description: Either the venue-native executor id or the stream-side alias (the web terminal's
            Polymarket ids are the numeric Gamma form) — both are matched against the index.
    OrderCreateMarketLinkRequest:
      type: object
      description: Body for `POST /orders/market-links`.
      required:
      - legs
      properties:
        legs:
          type: array
          description: Exactly two legs, on two DIFFERENT venues.
          minItems: 2
          maxItems: 2
          items:
            $ref: '#/components/schemas/OrderMarketLinkLegRef'
        title:
          type: string
          nullable: true
          description: Optional display title. Trimmed, truncated to 512 characters; a blank value falls
            back to the first leg's market name.
        similarity:
          type: number
          format: double
          nullable: true
          description: Matcher confidence, carried through for provenance only.
        confirm:
          type: boolean
          description: '`false` (the default) returns a verified preview and writes nothing. `true` inserts
            the link, and additionally requires `fingerprint`.'
          default: false
        fingerprint:
          type: string
          nullable: true
          description: REQUIRED when `confirm` is `true` — the `verification_fingerprint` from the preview.
            The confirm re-resolves against fresh metadata and requires the result to match what you
            reviewed, so index drift becomes a `409` rather than a silently different link.
    OrderMarketLinkLegPreview:
      type: object
      required:
      - provider
      - market_id
      - stream_key
      - outcome_yes_label
      - outcome_no_label
      - market_name
      - expires_at
      properties:
        provider:
          type: string
        market_id:
          type: string
          description: Executor-convention market id (the condition id on Polymarket).
        stream_key:
          type: string
          description: The stream-side id clients hold (the numeric Gamma id on Polymarket).
        outcome_yes_label:
          type: string
        outcome_no_label:
          type: string
        market_name:
          type: string
        expires_at:
          type: string
          description: '`YYYY-MM-DD HH:MM:SS`, as indexed — not RFC 3339.'
    OrderMarketLinkResponse:
      type: object
      description: Response for `POST /orders/market-links`.
      required:
      - status
      - legs
      - warnings
      - verification_fingerprint
      properties:
        status:
          type: string
          description: '`preview` (nothing written), `created` (inserted), or `exists` (a link the caller
            can already route).'
          enum:
          - preview
          - created
          - exists
        link_id:
          type: string
          format: uuid
          nullable: true
          description: Null on a `preview`.
        scope:
          type: string
          nullable: true
          description: On `exists`, whether the existing link is `global` or the caller's own `user` link.
            Always `user` on `created`; null on `preview`.
          enum:
          - global
          - user
        title:
          type: string
          nullable: true
          description: Null on an `exists` response.
        legs:
          type: array
          items:
            $ref: '#/components/schemas/OrderMarketLinkLegPreview'
        warnings:
          type: array
          description: Non-blocking advisories (venue expiry gaps, resolution-source divergence). Always
            includes the standing "venues may resolve on different data sources" notice.
          items:
            type: string
        verification_fingerprint:
          type: string
          description: 16-hex-character hash of the verified identity material (ids, tokens, case-folded
            outcome labels). Echo it back as `fingerprint` on confirm. Change detection, not security.
    OrderMarketLinkErrorResponse:
      type: object
      description: 'Error body for `POST /orders/market-links` ONLY. The key is `message`, NOT `error`
        — this endpoint uses a fourth error shape found nowhere else in this API, and a client written
        against `OrderErrorResponse` or `OrderSimpleErrorResponse` will silently read `undefined` for the
        reason. There is no `code` field.'
      required:
      - message
      properties:
        message:
          type: string
          example: exactly two legs required
    OrderPrepareWalletRequest:
      type: object
      description: Body for `POST /exchanges/{exchange_id}/prepare-wallet`. All three identity fields are
        mandatory and are VALIDATED against the authenticated caller — they can confirm authority, never
        grant it.
      required:
      - user_id
      - turnkey_org_id
      - wallet_address
      properties:
        user_id:
          type: string
        turnkey_org_id:
          type: string
        wallet_address:
          type: string
          description: Must be a wallet owned by the authenticated caller.
        wait_for_confirmation:
          type: boolean
          nullable: true
          description: '**Defaults to `true`** — the call blocks until gas sponsorship and allowances are
            confirmed. Pass `false` to fire-and-forget: the response returns immediately with
            `confirmation_pending: true`, `success: true` and zeroed result fields, and a background task
            does the work. A background failure is only logged; the caller is never told.'
          default: true
    OrderPrepareWalletResponse:
      type: object
      description: Response for `POST /exchanges/{exchange_id}/prepare-wallet`.
      required:
      - gas_sponsored
      - allowances_set
      - confirmation_pending
      - success
      properties:
        gas_sponsored:
          type: boolean
          description: Always `false` when `confirmation_pending` is `true` — the work had not run yet.
        gas_tx_hash:
          type: string
          nullable: true
        gas_amount:
          type: string
          nullable: true
          description: Present only when gas was actually sponsored.
        allowances_set:
          type: integer
          description: Number of allowances set. Always `0` when `confirmation_pending` is `true`.
        confirmation_pending:
          type: boolean
          description: '`true` when the caller passed `wait_for_confirmation: false` and the preparation
            is still running in the background.'
        success:
          type: boolean
          description: Always `true` on a 200, INCLUDING the fire-and-forget path where nothing has been
            attempted yet. It is not evidence the wallet is ready.
    OrderPolymarketEnableTradingRequest:
      type: object
      required:
      - wallet_address
      properties:
        wallet_address:
          type: string
          description: The wallet to enable. Must be a wallet owned by the authenticated caller.
    OrderPolymarketEnableTradingResponse:
      type: object
      required:
      - success
      - message
      properties:
        success:
          type: boolean
        message:
          type: string
        usdc_tx_hash:
          type: string
          description: OMITTED entirely (not null) when no USDC approval was needed.
        ctf_tx_hash:
          type: string
          description: OMITTED entirely (not null) when no CTF approval was needed.
    OrderPolymarketEnableImportedTradingRequest:
      type: object
      description: Body for `POST /exchanges/polymarket/enable-imported-trading`. Neither field is
        trusted — both are checked against the authenticated caller before any credential is minted.
      required:
      - wallet_address
      - turnkey_org_id
      properties:
        wallet_address:
          type: string
          description: The imported Polymarket EOA. Must already live in the caller's Turnkey sub-org.
        turnkey_org_id:
          type: string
          description: Sub-org id holding the imported key. Must equal the caller's own org.
    OrderPolymarketEnableImportedTradingResponse:
      type: object
      required:
      - success
      - wallet_address
      properties:
        success:
          type: boolean
        wallet_address:
          type: string
    OrderPredictfunEnableTradingRequest:
      type: object
      required:
      - wallet_address
      properties:
        wallet_address:
          type: string
          description: BSC EOA to enable. Must be owned by the authenticated caller.
    OrderPredictfunEnableTradingResponse:
      type: object
      required:
      - success
      - message
      - tx_hashes
      properties:
        success:
          type: boolean
        message:
          type: string
        tx_hashes:
          type: object
          description: 'Per-variant approval transaction hashes, keyed by variant label (e.g.
            `plain-binary`, `yield-negrisk`). A variant already approved is a no-op and contributes no
            entry.'
          additionalProperties:
            type: string
    OrderOpinionEnableTradingRequest:
      type: object
      required:
      - wallet_address
      properties:
        wallet_address:
          type: string
    OrderOpinionEnableTradingResponse:
      type: object
      required:
      - success
      - message
      properties:
        success:
          type: boolean
        message:
          type: string
        usdt_tx_hash:
          type: string
          description: OMITTED entirely (not null) when no USDT approval was needed.
    OrderKalshiEnableTradingRequest:
      type: object
      description: Body for `POST /exchanges/kalshi/enable-trading`. Kalshi trades against the user's
        OWN Kalshi account, so the caller supplies their own Kalshi API credentials — Kairos does not
        provision them.
      required:
      - api_key_id
      - private_key_pem
      properties:
        api_key_id:
          type: string
          description: Kalshi API Key ID (the public identifier). Must be non-blank.
        private_key_pem:
          type: string
          description: The matching RSA private key, PEM-encoded. Both PKCS#1 (`BEGIN RSA PRIVATE KEY`)
            and PKCS#8 (`BEGIN PRIVATE KEY`) are accepted, and a key pasted as a single line with
            literal `\n` escape sequences is normalized server-side.
    OrderKalshiEnableTradingResponse:
      type: object
      required:
      - success
      - message
      properties:
        success:
          type: boolean
        message:
          type: string
    OrderPredictfunAccountReferral:
      type: object
      required:
      - status
      properties:
        code:
          type: string
          nullable: true
        status:
          type: string
          description: '`LOCKED` or `UNLOCKED` per Predict.fun''s spec. Kept as an open string so a new
            upstream state does not break deserialization — do not treat it as a closed enum.'
    OrderPredictfunAccountPoints:
      type: object
      required:
      - total
      properties:
        total:
          type: number
          format: double
    OrderPredictfunAccountResponse:
      type: object
      description: Response for `GET /exchanges/predictfun/account` — the `data` block of Predict.fun's
        own `GET /v1/account`, forwarded verbatim. Upstream may add fields.
      required:
      - name
      - address
      - referral
      - points
      properties:
        name:
          type: string
        address:
          type: string
        image_url:
          type: string
          nullable: true
        referral:
          $ref: '#/components/schemas/OrderPredictfunAccountReferral'
        points:
          $ref: '#/components/schemas/OrderPredictfunAccountPoints'
    OrderCheckResolutionRequest:
      type: object
      required:
      - condition_id
      properties:
        condition_id:
          type: string
          description: 'Polymarket condition id. Two forms are accepted and routed differently upstream:
            a `0x…` hex condition id, or a numeric Gamma market id.'
        token_id:
          type: string
          nullable: true
          description: Optional outcome token id, to check a specific outcome's redeemability.
    OrderCheckResolutionResponse:
      type: object
      required:
      - condition_id
      - resolved
      - redeemable
      properties:
        condition_id:
          type: string
        resolved:
          type: boolean
        winning_outcome:
          type: integer
          description: Index of the winning outcome. OMITTED entirely (not null) when unresolved or
            unknown.
        resolution_time:
          type: string
          description: OMITTED entirely (not null) when unavailable.
        redeemable:
          type: boolean
    OrderHyperliquidWithdrawPrepareRequest:
      type: object
      description: Body for `POST /exchanges/hyperliquid/withdraw/prepare`. This call is PURE — it writes
        nothing and has no side effects; it only builds the typed data for you to sign.
      required:
      - wallet_address
      - amount
      properties:
        wallet_address:
          type: string
          description: The Hyperliquid main wallet. Must be owned by the authenticated caller.
        amount:
          type: string
          description: USDC amount as a plain decimal string. Only ASCII digits and a single `.` not in
            first position are accepted, and it must parse to a finite value > 0 — `1e5`, `.5`, `-1`,
            `inf` and `NaN` are all rejected. Kairos enforces NO minimum and NO maximum; Hyperliquid's
            own withdrawal minimum and flat fee are applied venue-side.
        destination:
          type: string
          nullable: true
          description: Arbitrum destination address. **Defaults to `wallet_address`** (withdraw to self)
            when omitted. Kairos does NOT validate this address — see the endpoint description.
    OrderHyperliquidWithdrawPrepareResponse:
      type: object
      required:
      - typed_data
      - time
      - destination
      - amount
      properties:
        typed_data:
          type: object
          description: The EIP-712 typed data for Hyperliquid's `withdraw3` action. Sign this with the
            main wallet.
          additionalProperties: true
        time:
          type: integer
          format: int64
          description: Millisecond timestamp that is ALSO the action's nonce. Pass it back unchanged to
            `POST /exchanges/hyperliquid/withdraw`.
        destination:
          type: string
        amount:
          type: string
    OrderHyperliquidWithdrawRequest:
      type: object
      required:
      - wallet_address
      - amount
      - time
      - signature
      properties:
        wallet_address:
          type: string
        amount:
          type: string
        time:
          type: integer
          format: int64
          description: The `time` from prepare. Doubles as the nonce — Hyperliquid's replay protection is
            the only thing preventing a duplicate withdrawal.
        signature:
          type: string
          description: 0x-prefixed 65-byte signature over the prepared typed data, produced by the main
            wallet. Kairos never signs a withdrawal.
        destination:
          type: string
          nullable: true
          description: Defaults to `wallet_address`. Must match what you signed, or Hyperliquid rejects it.
    OrderHyperliquidActionResponse:
      type: object
      description: Response for `POST /exchanges/hyperliquid/withdraw` and `POST /exchanges/hyperliquid/transfer`.
      required:
      - success
      - message
      properties:
        success:
          type: boolean
        message:
          type: string
          description: '`Withdrawal submitted` or `Transfer submitted`. Submission only — not a
            confirmation of settlement.'
    OrderHyperliquidTransferPrepareRequest:
      type: object
      description: Body for `POST /exchanges/hyperliquid/transfer/prepare`. Pure — writes nothing.
      required:
      - wallet_address
      - amount
      - to_perp
      properties:
        wallet_address:
          type: string
        amount:
          type: string
          description: USDC amount, same plain-decimal validation as withdraw. No Kairos-side minimum or
            maximum.
        to_perp:
          type: boolean
          description: '`true` moves spot → perp; `false` moves perp → spot. Funds never leave the
            account.'
    OrderHyperliquidTransferPrepareResponse:
      type: object
      required:
      - typed_data
      - time
      - amount
      - to_perp
      properties:
        typed_data:
          type: object
          description: EIP-712 typed data for Hyperliquid's `usdClassTransfer` action.
          additionalProperties: true
        time:
          type: integer
          format: int64
          description: Millisecond timestamp, doubles as the nonce.
        amount:
          type: string
        to_perp:
          type: boolean
    OrderHyperliquidTransferRequest:
      type: object
      required:
      - wallet_address
      - amount
      - time
      - to_perp
      - signature
      properties:
        wallet_address:
          type: string
        amount:
          type: string
        time:
          type: integer
          format: int64
        to_perp:
          type: boolean
        signature:
          type: string
          description: 0x-prefixed 65-byte signature from the MAIN wallet. Hyperliquid forbids agent
            wallets from class transfers, so a Kairos-held agent key cannot sign this.
    OrderDepositWalletIdentityRequest:
      type: object
      description: The common identity body shared by the deposit-wallet endpoints. All three fields are
        checked against the authenticated caller and can only ever CONFIRM authority, never grant it —
        omitting them is not an option here, but supplying someone else's is a `403`.
      required:
      - user_id
      - turnkey_org_id
      - owner_address
      properties:
        user_id:
          type: string
          format: uuid
        turnkey_org_id:
          type: string
        owner_address:
          type: string
          description: The owner EOA. The deposit-wallet address itself is resolved server-side from
            `UserIdentity`, never taken from the body.
    OrderDepositWalletOnboardResponse:
      type: object
      description: Response for `POST /exchanges/polymarket/deposit-wallet/onboard`.
      required:
      - deposit_wallet_address
      - batch_tx_id
      - success
      properties:
        deposit_wallet_address:
          type: string
          description: The deterministic (CREATE2) ERC-1967 proxy address for this owner.
        deploy_tx_id:
          type: string
          nullable: true
          description: Relayer transaction id for the proxy deployment. `null` when the wallet was
            already deployed — the relayer is idempotent and that case is treated as success.
        batch_tx_id:
          type: string
          description: Relayer transaction id for the confirmed trading-approval batch.
            Empty when no batch was submitted (approvals already present or optional approvals skipped).
        success:
          type: boolean
    OrderDepositWalletBatchCall:
      type: object
      description: One call inside a signed `Batch`.
      required:
      - target
      - value
      - data
      properties:
        target:
          type: string
          description: 0x contract address.
        value:
          type: string
          description: Native value as a DECIMAL string — hex is rejected for any non-zero value. The
            allow-list requires this to be zero on every permitted batch shape.
        data:
          type: string
          description: 0x-prefixed calldata.
    OrderImportedRelayInfoRequest:
      type: object
      required:
      - user_id
      - turnkey_org_id
      - owner_address
      - sig_type
      properties:
        user_id:
          type: string
          format: uuid
        turnkey_org_id:
          type: string
        owner_address:
          type: string
        sig_type:
          $ref: '#/components/schemas/OrderImportedSigType'
    OrderImportedSigType:
      type: string
      description: Which imported Polymarket wallet model this is. Lower-case on the wire.
      enum:
      - proxy
      - safe
    OrderImportedRelayInfoResponse:
      type: object
      required:
      - nonce
      properties:
        nonce:
          type: string
          description: Relayer nonce as a decimal U256 string.
        relay:
          type: string
          nullable: true
          description: The GSN relay address, needed to build the PROXY digest. **Always `null` when
            `sig_type` is `safe`.**
    OrderErrorActionType:
      type: string
      description: Machine-readable recovery-action identifier, snake_case on the wire.
      enum:
      - enable_trading
      - reenable_trading
      - update_policies
      - regenerate_api_key
      - contact_support
      - retry
      - review_order
      - adjust_price
      - adjust_size
      - browse_markets
      - refresh_market
      - create_new_order
      - add_funds
      - reduce_size
      - swap_usdc
      - add_matic
      - approve_usdc
      - approve_ctf
      - retry_approval
      - wait_and_retry
      - check_status
      - use_limit_order
      - update_kalshi_credentials
      - enable_bridge_funding
    OrderErrorAction:
      type: object
      description: An actionable recovery step a client can surface to the end user.
      required:
      - action
      - label
      - primary
      properties:
        action:
          $ref: '#/components/schemas/OrderErrorActionType'
        label:
          type: string
          description: Human-readable button/link label.
          example: Add Funds
        url:
          type: string
          nullable: true
        primary:
          type: boolean
          description: Whether this is the primary/recommended action.
    OrderFailure:
      type: object
      description: |-
        Structured failure detail attached to a `failed` order (`GET /orders`,
        `GET /orders/{order_id}`, and the `order_update` WebSocket event). OMITTED
        entirely (not `null`) when the order has no failure.

        `classification` is the **authoritative retry policy**:
        `expected_user_rejection` is a well-formed request the venue or the user's own
        inputs rejected — do not retry without changing the order; `retryable` is a
        transient condition that may succeed on retry; `non_retryable` cannot succeed
        by retrying the same order.

        `details.actions` is a **UI affordance only**. It is derived from `code`
        independently of `classification` and may include `retry` (even as `primary`)
        on a `non_retryable` failure, because the suggested buttons target an end user
        who may be able to change something first. Do NOT build an automatic retry
        loop from `actions`; branch on `classification`. A client that is not rendering
        buttons can ignore `actions` entirely.
      required:
      - code
      - classification
      - details
      properties:
        code:
          type: string
          description: |-
            Machine-readable failure code. This is NOT the same namespace as
            `details.code`: `code` is the execution error's wire code (e.g.
            `FOK_NOT_FILLED`), while `details.code` is the `OrderErrorCode`
            used for incident grouping (e.g. `MARKET_FOK_NOT_FILLED`). For a
            venue `ExchangeError` the two coincide.
          example: FOK_NOT_FILLED
        classification:
          type: string
          enum:
          - expected_user_rejection
          - retryable
          - non_retryable
          description: Authoritative retry policy for this failure.
        details:
          $ref: '#/components/schemas/OrderErrorDetails'
    OrderErrorCode:
      type: string
      description: Machine-readable error code, SCREAMING_SNAKE_CASE on the wire.
      enum:
      - AUTH_CREDENTIALS_NOT_FOUND
      - AUTH_CREDENTIALS_INVALID
      - AUTH_POLICY_OUTDATED
      - AUTH_INSUFFICIENT_SCOPE
      - AUTH_POLYMARKET_API_KEY_INVALID
      - AUTH_TURNKEY_WALLET_NOT_FOUND
      - AUTH_TURNKEY_AUTH_FAILED
      - AUTH_TURNKEY_SIGNING_FAILED
      - VALIDATION_INVALID_ORDER
      - VALIDATION_INVALID_PRICE
      - VALIDATION_INVALID_SIZE
      - VALIDATION_MARKET_NOT_FOUND
      - VALIDATION_TOKEN_NOT_FOUND
      - VALIDATION_ORDER_EXPIRED
      - VALIDATION_MARKET_NOT_SETTLED_ON_CHAIN
      - AUTH_IDENTITY_MISMATCH
      - FUNDS_INSUFFICIENT_USDC
      - FUNDS_INSUFFICIENT_GAS
      - FUNDS_INSUFFICIENT_TOTAL
      - FUNDS_INSUFFICIENT_BALANCE
      - FUNDS_COLLATERAL_LOCATION
      - ALLOWANCE_USDC_NOT_SET
      - ALLOWANCE_CTF_NOT_SET
      - ALLOWANCE_APPROVAL_FAILED
      - NETWORK_RPC_ERROR
      - NETWORK_TIMEOUT
      - NETWORK_HTTP_ERROR
      - NETWORK_ERROR
      - EXCHANGE_POLYMARKET_API_ERROR
      - EXCHANGE_POLYMARKET_UNAUTHORIZED
      - EXCHANGE_POLYMARKET_MARKET_CLOSED
      - EXCHANGE_POLYMARKET_RATE_LIMITED
      - EXCHANGE_KALSHI_API_ERROR
      - EXCHANGE_KALSHI_UNAUTHORIZED
      - EXCHANGE_KALSHI_MARKET_CLOSED
      - EXCHANGE_ERROR
      - EXCHANGE_UNSUPPORTED
      - SIGNATURE_EIP712_FAILED
      - SIGNATURE_ERROR
      - GAS_ESTIMATION_FAILED
      - TRANSACTION_FAILED
      - GAS_SPONSORSHIP_FAILED
      - TRANSACTION_TIMEOUT
      - MARKET_NOT_READY
      - MARKET_PAUSED
      - MARKET_INSUFFICIENT_LIQUIDITY
      - ORDERBOOK_UNAVAILABLE
      - MARKET_FOK_NOT_FILLED
      - MARKET_ORDER_SIZE_EXCEEDS_MAX
      - DATABASE_ERROR
      - DATABASE_CREDENTIAL_DECRYPTION_FAILED
      - INTERNAL_ERROR
      example: FUNDS_INSUFFICIENT_USDC
    OrderErrorDetails:
      type: object
      description: Structured, machine-readable error payload shared across order-execution error responses.
      required:
      - code
      - message
      - actions
      properties:
        code:
          $ref: '#/components/schemas/OrderErrorCode'
        message:
          type: string
          description: Human-readable error message.
        details:
          type: string
          nullable: true
          description: Optional extended explanation.
        metadata:
          nullable: true
          description: 'Error-type-specific structured metadata (shape varies by `code` — e.g. `{type:
            "insufficient_funds", required, available, currency, shortfall}` or `{type: "rate_limit",
            retry_after_seconds, limit, window}`). Untagged union; treat as opaque unless you match on
            the embedded `type` field.'
          additionalProperties: true
        actions:
          type: array
          description: Zero or more actionable recovery steps a client can surface.
          items:
            $ref: '#/components/schemas/OrderErrorAction'
    OrderErrorResponse:
      type: object
      description: Standard structured error body returned by every endpoint that surfaces an `ExecutionError`/`ApiError`
        (order submission, cancel-all, and the other endpoints noted as returning this schema). Endpoints
        noted as "empty body (status code only)" do NOT use this shape.
      required:
      - error
      - error_details
      properties:
        error:
          type: string
          description: Same text as `error_details.message`, kept for backwards-compatible consumers.
        code:
          type: string
          nullable: true
          description: Debug-formatted copy of `error_details.code` (e.g. `"FundsInsufficientUsdc"` —
            PascalCase, NOT the SCREAMING_SNAKE_CASE wire code in `error_details.code`).
        error_details:
          $ref: '#/components/schemas/OrderErrorDetails'
    OrderSimpleErrorResponse:
      type: object
      description: 'Minimal error body (`{"error": "..."}`, optionally `{"code": "..."}`) used by the
        auth middleware (401/403/429 on any endpoint), the external-signing `/v2/orders/*` lane, and the
        Kalshi-offchain balance endpoint. Distinct from `OrderErrorResponse` — these endpoints do not
        emit the full structured `error_details` shape.'
      required:
      - error
      properties:
        error:
          type: string
          description: Human-readable error message.
          example: Insufficient scope
        code:
          type: string
          nullable: true
          description: 'Present only on the Kalshi-offchain endpoints (omitted entirely, not null, when
            absent). Observed values: `PLATFORM_API_ACCESS_DENIED`, `PLATFORM_API_ACCESS_DISABLED`,
            `INSUFFICIENT_SCOPE`, `NOT_CONFIGURED`, `NO_CREDENTIALS`, `INVALID_USER_ID`,
            `MISSING_API_KEY_ID`, `INVALID_PRIVATE_KEY`, `INVALID_CREDENTIALS`, `CONFIG_ERROR`,
            `ENCRYPTION_ERROR`, `DB_ERROR`, `INTERNAL_ERROR`, `KALSHI_API_ERROR`.'
    OrderCtfSplitMergeRequest:
      type: object
      required:
      - condition_id
      - amount
      properties:
        condition_id:
          type: string
          description: CTF conditionId of the market (0x…).
          example: '0x1234abcd'
        market_id:
          type:
          - string
          - 'null'
          description: OPTIONAL. Only enriches the synthetic BUY/SELL trade-log legs with the market's
            YES/NO token ids — the on-chain split/merge itself needs only `condition_id` + `amount`. When
            omitted the market context is resolved from `condition_id`; if it cannot be resolved either
            way the on-chain action still succeeds and only the best-effort trade-log enrichment is
            skipped.
          example: '570362'
        amount:
          type: string
          description: >-
            Decimal string. Units of collateral (split) or complete sets
            (merge). Must be > 0. An unquoted JSON number is also accepted, but
            it round-trips through a binary float — send the string form to
            preserve every digit.
          example: '100'
        user_id: &id001
          type:
          - string
          - 'null'
          description: Optional. Must match the authenticated caller when present — identity is resolved
            server-side and these fields can never widen scope to another user.
        turnkey_org_id: &id002
          type:
          - string
          - 'null'
          description: Optional. Must match the authenticated caller when present.
        wallet_address: &id003
          type:
          - string
          - 'null'
          description: Optional wallet override; honored only if the wallet is owned by the caller. Omit
            to let the server resolve the signer wallet.
    OrderCtfSplitResponse:
      type: object
      required:
      - amount
      - condition_id
      - action
      - success
      properties:
        tx_hash:
          type:
          - string
          - 'null'
          description: On-chain transaction hash. `null` when the action completed without producing one
            — always check `success`.
          example: '0xabc123'
        amount:
          type: string
          description: Decimal string — `rust_decimal` always serializes as a string.
          example: '100'
        condition_id:
          type: string
          example: '0x1234abcd'
        action:
          type: string
          enum:
          - split
        success:
          type: boolean
    OrderCtfMergeResponse:
      type: object
      required:
      - amount
      - condition_id
      - action
      - success
      properties:
        tx_hash:
          type:
          - string
          - 'null'
          description: On-chain transaction hash. `null` when the action completed without producing one
            — always check `success`.
          example: '0xabc123'
        amount:
          type: string
          description: Decimal string — `rust_decimal` always serializes as a string.
          example: '290'
        condition_id:
          type: string
          example: '0x1234abcd'
        action:
          type: string
          enum:
          - merge
        success:
          type: boolean
    OrderCtfRedeemRequest:
      type: object
      required:
      - condition_id
      properties:
        condition_id:
          type: string
          description: CTF conditionId of the RESOLVED market.
          example: '0x1234abcd'
        position_id:
          type:
          - string
          - 'null'
          description: Position row to mark redeemed after success.
        market_id:
          type:
          - string
          - 'null'
          description: Locates the position when position_id is omitted.
        token_id:
          type:
          - string
          - 'null'
          description: Locates the position when position_id is omitted.
        user_id: *id001
        turnkey_org_id: *id002
        wallet_address: *id003
    OrderCtfRedeemResponse:
      type: object
      required:
      - amount_redeemed
      - success
      properties:
        tx_hash:
          type:
          - string
          - 'null'
          description: On-chain transaction hash. `null` when no transaction was produced — always check
            `success`.
          example: '0xabc123'
        amount_redeemed:
          type: string
          description: Decimal string — `rust_decimal` always serializes as a string.
          example: '100'
        success:
          type: boolean
        db_update_failed:
          type: boolean
          description: OMITTED entirely when false (`skip_serializing_if`). Present and `true` only when
            the on-chain redeem succeeded but the bookkeeping write failed after retries — funds are safe,
            retry to fix bookkeeping.
    SyntheticBookLeg:
      type: object
      required: [venue, contract_id, weight]
      properties:
        venue:
          type: string
          example: polymarket
        contract_id:
          type: string
          description: Provider market/contract identifier used by the Kairos market-data stream.
        token_id:
          type: string
          description: Provider outcome-token identifier. Required except for merged-book venues that accept outcome_index.
        outcome_index:
          type: integer
          minimum: 0
          description: Outcome index for a merged-book venue when token_id is omitted.
        weight:
          type: string
          pattern: '^-?[0-9]+(?:\.[0-9]{1,9})?$'
          example: '-1'
          description: Signed decimal quantity weight, encoded as a string with at most nine decimals.
    SyntheticBookDefinitionRequest:
      type: object
      required: [legs, output_mode, depth]
      properties:
        legs:
          type: array
          minItems: 1
          maxItems: 15
          items:
            $ref: '#/components/schemas/SyntheticBookLeg'
        output_mode:
          type: string
          enum: [bbo, aggregated_l2, decomposed_l2]
        depth:
          type: integer
          minimum: 1
          maximum: 50
          example: 50
        classification:
          type: string
          default: arbitrary_basket
          enum: [arbitrary_basket, relative_value, exact_equivalence, complement, partition, implication, range, guaranteed_payout]
          description: Reviewed economic metadata; Kairos does not infer it from the formula.
    SyntheticBookCreateResponse:
      type: object
      required: [synthetic_id, subscription_id, created, fees_updated, sources_subscribed, expires_at_ms, ttl_ms]
      properties:
        synthetic_id:
          type: string
          pattern: '^syn_[0-9a-f]{16}$'
          example: syn_0123456789abcdef
        subscription_id:
          type: string
          format: uuid
          description: Opaque account-owned public lease id. This is not the materializer's private numeric handle.
        created:
          type: boolean
          description: True when this request created the canonical materialization; false when it joined an equivalent one.
        fees_updated:
          type: boolean
        sources_subscribed:
          type: integer
          minimum: 0
        expires_at_ms:
          type: integer
          format: int64
        ttl_ms:
          type: integer
          format: int64
          example: 600000
    SyntheticBookRefreshResponse:
      type: object
      required: [refreshed, subscription_id, synthetic_id, expires_at_ms, ttl_ms]
      properties:
        refreshed: {type: boolean, const: true}
        subscription_id: {type: string, format: uuid}
        synthetic_id: {type: string, pattern: '^syn_[0-9a-f]{16}$'}
        expires_at_ms: {type: integer, format: int64}
        ttl_ms: {type: integer, format: int64, example: 600000}
    SyntheticBookReleaseResponse:
      type: object
      required: [released, subscription_id]
      properties:
        released: {type: boolean, const: true}
        subscription_id: {type: string, format: uuid}
    SyntheticBookErrorResponse:
      type: object
      required: [error, message]
      properties:
        error:
          type: string
          example: synthetic_books_unavailable
        message:
          type: string
        details:
          type: object
          additionalProperties: true
          description: Private materializer validation details; present only for rejected definitions.
          nullable: true
paths:
  /v1/synthetics:
    post:
      operationId: createSyntheticBook
      summary: Create or join a Synthetic Book definition
      tags: [Synthetic Books]
      description: |
        Creates an account-owned ten-minute lease for a canonical weighted book and returns the `synthetic_id` to subscribe to at `wss://stream.kairos.trade` with provider/topic `synthetic`.

        Uses the caller's existing Kairos credential triple; no separate Synthetic Books credential, scope, or account allowlist is required. The full API-key triple also satisfies the mutation gate automatically.

        Equivalent canonical definitions share a data-plane materialization but receive distinct account-owned lease UUIDs. Maximum 15 legs, depth 50, 25 active leases per user, and 30 create requests/minute/user by default.
      x-kairos-auth: API key triple, or first-party session JWT plus X-Csrf-Token
      x-kairos-rate-limit: 120 total requests/minute/user, 30 creates/minute/user, and 25 active leases/user by default
      x-kairos-scope: none
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/SyntheticBookDefinitionRequest'}
      responses:
        '200':
          description: Lease created and canonical synthetic id returned.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/SyntheticBookCreateResponse'}
        '400':
          description: Invalid formula, leg, mode, classification, or depth.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/SyntheticBookErrorResponse'}
        '401':
          description: Missing or invalid account credentials.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/OrderSimpleErrorResponse'}
        '403':
          description: Invalid mutation credentials (including an invalid API-key triple).
          content:
            application/json:
              schema: {$ref: '#/components/schemas/OrderSimpleErrorResponse'}
        '409':
          description: The account already has 25 active leases.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/SyntheticBookErrorResponse'}
        '413':
          description: Request body exceeds 64 KiB.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/SyntheticBookErrorResponse'}
        '415':
          description: Request body is not JSON.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/SyntheticBookErrorResponse'}
        '429':
          description: Creation rate limit exceeded.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/SyntheticBookErrorResponse'}
        '502':
          description: Private materializer unavailable or returned an invalid response.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/SyntheticBookErrorResponse'}
        '503':
          description: Synthetic Books is not configured on this execution node.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/SyntheticBookErrorResponse'}
  /v1/synthetics/subscriptions/{subscription_id}/refresh:
    post:
      operationId: refreshSyntheticBookSubscription
      summary: Refresh a Synthetic Book lease
      tags: [Synthetic Books]
      description: Refreshes an unexpired lease owned by the authenticated user. Refresh every five minutes; a lease expires after ten minutes and an expired lease must be recreated.
      x-kairos-auth: API key triple, or first-party session JWT plus X-Csrf-Token
      x-kairos-rate-limit: 120 requests/minute/user by default
      x-kairos-scope: none
      parameters:
      - name: subscription_id
        in: path
        required: true
        schema: {type: string, format: uuid}
      responses:
        '200':
          description: Lease refreshed.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/SyntheticBookRefreshResponse'}
        '401':
          description: Missing or invalid account credentials.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/OrderSimpleErrorResponse'}
        '403':
          description: Invalid mutation credentials.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/OrderSimpleErrorResponse'}
        '404':
          description: Lease does not exist or is owned by another account.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/SyntheticBookErrorResponse'}
        '410':
          description: Lease expired in the control plane or materializer.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/SyntheticBookErrorResponse'}
        '429':
          description: Synthetic Books request rate limit exceeded.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/SyntheticBookErrorResponse'}
        '502':
          description: Private materializer unavailable or returned an invalid response.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/SyntheticBookErrorResponse'}
        '503':
          description: Synthetic Books is not configured on this execution node.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/SyntheticBookErrorResponse'}
  /v1/synthetics/subscriptions/{subscription_id}:
    delete:
      operationId: releaseSyntheticBookSubscription
      summary: Release a Synthetic Book lease
      tags: [Synthetic Books]
      description: Releases an account-owned lease. The canonical materialization remains live while any other lease references it and otherwise enters its reclamation TTL.
      x-kairos-auth: API key triple, or first-party session JWT plus X-Csrf-Token
      x-kairos-rate-limit: 120 requests/minute/user by default
      x-kairos-scope: none
      parameters:
      - name: subscription_id
        in: path
        required: true
        schema: {type: string, format: uuid}
      responses:
        '200':
          description: Lease released (also returned when the private handle was already gone).
          content:
            application/json:
              schema: {$ref: '#/components/schemas/SyntheticBookReleaseResponse'}
        '401':
          description: Missing or invalid account credentials.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/OrderSimpleErrorResponse'}
        '403':
          description: Invalid mutation credentials.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/OrderSimpleErrorResponse'}
        '404':
          description: Lease does not exist or is owned by another account.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/SyntheticBookErrorResponse'}
        '429':
          description: Synthetic Books request rate limit exceeded.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/SyntheticBookErrorResponse'}
        '502':
          description: Private materializer unavailable or returned an invalid response.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/SyntheticBookErrorResponse'}
        '503':
          description: Synthetic Books is not configured on this execution node.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/SyntheticBookErrorResponse'}
  /v1/synthetics/{synthetic_id}:
    get:
      operationId: getSyntheticBookDefinition
      summary: Inspect a Synthetic Book definition
      tags: [Synthetic Books]
      description: Returns materialization metadata only when the authenticated user owns an active lease for this canonical id. Cross-account subscriber counts are intentionally omitted.
      x-kairos-auth: API key triple or first-party session JWT
      x-kairos-rate-limit: 120 requests/minute/user by default
      x-kairos-scope: none
      parameters:
      - name: synthetic_id
        in: path
        required: true
        schema: {type: string, pattern: '^syn_[0-9a-f]{16}$'}
      responses:
        '200':
          description: Current materialization metadata and canonical definition echo.
          content:
            application/json:
              schema: {type: object, additionalProperties: true}
        '400':
          description: synthetic_id is not canonical.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/SyntheticBookErrorResponse'}
        '401':
          description: Missing or invalid account credentials.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/OrderSimpleErrorResponse'}
        '404':
          description: No active account-owned lease or unknown materialization.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/SyntheticBookErrorResponse'}
        '429':
          description: Synthetic Books request rate limit exceeded.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/SyntheticBookErrorResponse'}
        '502':
          description: Private materializer unavailable or returned an invalid response.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/SyntheticBookErrorResponse'}
        '503':
          description: Synthetic Books is not configured on this execution node.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/SyntheticBookErrorResponse'}
  /orders:
    post:
      operationId: submitOrder
      summary: Submit a new order
      description: |-
        Submits a custodial order: Kairos signs and routes it to the venue on the caller's behalf (custodial signing). This is the standard order-entry path used by both the web app and API-key consumers who have not been allow-listed onto the self-custody "external-signing" lane (`POST /v2/orders/intent` + `POST /v2/orders/submit`).
        **Ownership.** The order is always created for the authenticated caller; any `user_id` in the body is accepted for backwards compatibility but is ignored — the JWT/API-key identity wins.
        **Ack semantics (async).** A 200 response means the order was validated, persisted, and enqueued — it does NOT mean the order is live on the venue yet. The response `status` is `"queued"` (or, on an idempotent replay, the current status of the previously-created order). Track the order via `GET /orders/{order_id}`, `GET /orders` polling, or (lowest latency) the `/ws` socket, which pushes `order_update` / `fill` events as the worker submits to the venue and fills arrive.
        **Idempotency.** Pass `client_order_id` to make retries safe: a second submit with the same `(user, exchange_id, market_id, client_order_id)` tuple returns the existing order instead of creating a duplicate, and does **not** consume a rate-limit slot. If omitted, the server generates a random one (i.e. retries without a client-supplied id are NOT deduped).
        **Provider routing.** `exchange_id` selects the venue-specific executor (`polymarket`, `kalshi`, `predictfun`, `opinion`, `hyperliquid`, …; `kalshi_offchain` is a deprecated alias of `kalshi`). Each venue advertises its own capabilities (supported `time_in_force` values, max price, order types) — a request that is well-formed in general but unsupported by the specific venue is rejected with `400 EXCHANGE_UNSUPPORTED` / `400 VALIDATION_INVALID_ORDER`.
        **Hyperliquid HIP-4.** Use the numeric outcome id as `market_id`, the selected display label as `outcome`, and the side coin from the market-data `OrderbookSnapshot.token_ids` as `token_id` (for example `#1010` or `#1011`). Quantities are whole shares. Hyperliquid maps `GTC`/`GTD` to venue GTC and `IOC`/`FAK`/`FOK` to venue IOC. Single-order and selected-order batch cancellation are supported; cancel-all and fee quotes are unavailable.
        **Order types.** `kind="market"` is a marketable taker order — pair it with `time_in_force` `FOK` (fill-or-kill) or `FAK`/`IOC` (fill-and-kill / immediate-or-cancel, partials allowed, remainder cancelled). `kind="limit"` is a resting quote — pair it with `GTC` (good-til-cancelled) or `GTD` (good-til-date, requires `expiration_minutes`). `price` is REQUIRED for every order, including market orders — for a marketable order it is the limit you are willing to cross to, not a market-price sentinel.
        **Circuit breakers & gating.** Before persisting, the order passes through, in order: the per-user order rate limit, size/price bounds, the venue time-in-force capability gate, a global + per-exchange execution kill-switch, the custodial trading-enabled check, the post-only capability gate, side/kind/TIF-specific breakers, and the API-key minimum-notional rule. Kill-switch and restriction rejections are `503` with `error_details.code = MARKET_PAUSED`; a disabled or identity-less account is `403` with `AUTH_CREDENTIALS_INVALID`. Designated canary users bypass both breaker checks.
        **Rate limiting.** A Redis sliding-window limiter caps order submissions per authenticated user (default 5 orders per second, `ORDER_RATE_LIMIT_PER_SEC`). API keys with an `orders` override are checked against BOTH their own per-credential window and the aggregate per-user window (either denying is a `429`); idempotent replays are resolved before the limiter and never consume a slot. The limiter fails closed — an unreachable Redis denies with `429`. The `429` body carries `error_details.code = VALIDATION_INVALID_ORDER` with the message `Order rate limit exceeded` (there is no dedicated rate-limit code on this path), and no `Retry-After` header.
        **Auth & scope.** Requires `trade:execute`. This mutation endpoint is additionally gated by a service-token/CSRF/API-key check (`RequireServiceToken`), which any request bearing `X-Client-Id` / `X-Api-Key` / `X-Api-Secret` satisfies automatically — no extra header is needed for API-key consumers.
      tags:
      - Orders
      x-kairos-auth: api-key
      x-kairos-scope: trade:execute
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderSubmitRequest'
            examples:
              hyperliquid:
                summary: Hyperliquid HIP-4 limit buy on side 1
                value:
                  exchange_id: hyperliquid
                  market_id: '101'
                  token_id: '#1011'
                  outcome: 'No'
                  side: buy
                  kind: limit
                  quantity: 25
                  price: 0.42
                  time_in_force: GTC
      responses:
        '200':
          description: Order accepted, persisted, and enqueued for execution. Does not imply the order
            is live on the venue yet — poll or subscribe to `/ws` for the terminal state.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderSubmitResponse'
        '400':
          description: |-
            Validation or admission failure. `error_details.code` distinguishes them:
            - `VALIDATION_INVALID_ORDER` — unparseable `side`/`kind`, `time_in_force` present but unrecognized (never silently downgraded to `GTC`), `expiration_minutes` outside `[1, 43200]` for a GTD order, `max_slippage` outside `[0, 0.5]`, `max_slippage_cents` outside `[1, 99]`, `max_retries` > 20, a TIF or `post_only` combination the venue's capabilities do not advertise, an unrecognized `collateral` mode, `collateral=fund` without `max_bridge_fee_usdc` or `max_funding_wait_ms` (the message names the missing cap), either cap sent with a mode other than `fund`, an API-key BUY under the `$5` minimum notional.
            - `VALIDATION_INVALID_SIZE` — quantity not positive, below `MIN_ORDER_QUANTITY` (BUY only), or above `1,000,000`.
            - `VALIDATION_INVALID_PRICE` — `price` missing (it is required for EVERY order, including market orders), not positive, or above the venue's max price; same bounds for `trigger_price`.
            - `EXCHANGE_UNSUPPORTED` — `exchange_id` is not a registered exchange.
            - `FUNDS_INSUFFICIENT_USDC` / `FUNDS_INSUFFICIENT_BALANCE` — a pre-trade balance check or a venue balance rejection. NOTE: an insufficient-balance reject is a `400`, not a `502`.
            - `MARKET_FOK_NOT_FILLED` — a FOK/marketable order could not be filled, or realized slippage exceeded `max_slippage_cents`.
            - `EXCHANGE_POLYMARKET_MARKET_CLOSED` — the market is closed / trading halted.
            - `ALLOWANCE_CTF_NOT_SET` / `ALLOWANCE_USDC_NOT_SET` — a missing on-chain approval, classified from the venue's rejection text.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '401':
          $ref: '#/components/responses/ExecUnauthorized'
        '403':
          description: Missing `trade:execute` scope (`API key missing trade:execute scope`), API-key access
            to this provider disabled (`API-key access to <provider> is disabled`), or trading not enabled
            / no Turnkey identity for the user's custodial signing identity (`AUTH_CREDENTIALS_INVALID`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '404':
          description: '`VALIDATION_MARKET_NOT_FOUND` — the market/contract could not be resolved on the
            venue.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '422':
          description: '`ORDERBOOK_UNAVAILABLE` — a market order could not be priced because the live
            orderbook is missing or went stale between pricing and submission. Retryable by the caller
            (a fresh book may arrive); never auto-retried server-side within the same attempt.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '429':
          description: Per-user (and, for keys with an `orders` override, per-credential) order-submission
            rate limit exceeded, or the limiter's Redis was unreachable (fails closed). Body carries
            `error_details.code = VALIDATION_INVALID_ORDER`, message `Order rate limit exceeded`. A venue-side
            rate limit instead surfaces as `EXCHANGE_POLYMARKET_RATE_LIMITED`. No `Retry-After` header.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '500':
          description: '`INTERNAL_ERROR` (capabilities unavailable, trading-status read failed, idempotency
            service unavailable, pre-trade preparation failed), `DATABASE_ERROR`, or `SIGNATURE_ERROR`
            (custodial signing failed).'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '502':
          description: The venue call failed — a network/RPC error (`NETWORK_ERROR`) or an unclassified
            exchange rejection (`EXCHANGE_ERROR`). Balance, allowance and market-closed rejections are
            classified out of this bucket into `400`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '503':
          description: Execution disabled by a circuit breaker (global or per-exchange halt, side/kind/TIF
            restriction) — `MARKET_PAUSED`; or an execution lock could not be acquired (`INTERNAL_ERROR`,
            "System is busy").
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '504':
          description: '`NETWORK_TIMEOUT` — the venue call timed out. The order may still have reached
            the venue; verify with `GET /orders/{order_id}` before resubmitting.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
    get:
      operationId: listOrders
      summary: List the authenticated user's orders
      description: |-
        Returns orders belonging to the authenticated caller, most recent first, with optional status filtering and offset pagination. When `status` is omitted, every status (including terminal ones) is returned.
        **Response is trimmed.** To keep this endpoint cheap for polling UIs, the heavy `raw` (full venue response, can be ~100 KB/order) and `metadata` fields are stripped from every row (`raw: null`, `metadata: null`); the `outcome` label is preserved by falling back to `metadata.outcome` before stripping. Use `GET /orders/{order_id}` for the full row including `raw`.
        **Auth & scope.** Requires `trade:read`. For API-key callers without an `exchange_id` filter, every distinct provider the user has orders on is checked for API-key access before the list is returned.
      tags:
      - Orders
      x-kairos-auth: api-key
      x-kairos-scope: trade:read
      parameters:
      - name: exchange_id
        in: query
        required: false
        description: Filter to a single venue (e.g. `polymarket`, `kalshi`, `hyperliquid`).
        schema:
          type: string
          example: polymarket
      - name: status
        in: query
        required: false
        description: 'A single status or a comma-separated set, e.g. `filled` or `open,pending,live,partial`.
          Omit to return all statuses. Matched against the RAW stored status strings, which are not
          identical to the `OrderStatus` enum: `queued`/`locked`/`executing`/`orphaned` are all stored as
          `pending`, and the stored set additionally includes `open`, `processing` and `delayed`.'
        schema:
          type: string
          example: live,partial
      - name: active_only
        in: query
        required: false
        description: When `true`, return only working orders — stored status in `pending`, `queued`,
          `locked`, `processing`, `executing`, `live`, `open`, `partial` or `delayed`, excluding a
          `partial` market/FAK/IOC/FOK order (which is terminal in practice).
        schema:
          type: boolean
          default: false
      - name: limit
        in: query
        required: false
        description: Maximum rows to return. Clamped to at most 500 server-side. There is no lower clamp,
          so a zero or negative value is passed through to the query — send a positive value.
        schema:
          type: integer
          format: int64
          default: 50
          maximum: 500
          example: 50
      - name: offset
        in: query
        required: false
        description: Rows to skip, for paginating order history. Clamped to >= 0. **Ignored when `before_id`
          is set** — the two pagination modes are mutually exclusive.
        schema:
          type: integer
          format: int64
          default: 0
          minimum: 0
          example: 0
      - name: before_id
        in: query
        required: false
        description: Keyset cursor on `(submitted_at, id)` — return rows strictly older than this order.
          Preferred over `offset` for deep pagination; takes precedence over it.
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: The caller's orders (raw/metadata stripped), newest first.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Order'
        '401':
          $ref: '#/components/responses/ExecUnauthorized'
        '403':
          description: Missing `trade:read` scope, or API-key access to one of the returned providers
            is disabled. Empty body (status code only).
        '500':
          description: Database read failed. Empty body (status code only).
        '503':
          description: For an API-key caller with no `exchange_id` filter, the lookup of which providers
            the user has orders on failed, so per-provider access could not be checked (fails closed).
            Empty body (status code only).
  /orders/{order_id}:
    get:
      operationId: getOrder
      summary: Get a single order by internal id
      description: Returns the full order row (including `raw`, the unredacted venue response, and `metadata`)
        for a single order the caller owns. Use this after `POST /orders` to poll a specific order's terminal
        state, or after `GET /orders` (which strips `raw`/`metadata`) to drill into one row.
      tags:
      - Orders
      x-kairos-auth: api-key
      x-kairos-scope: trade:read
      parameters:
      - name: order_id
        in: path
        required: true
        description: Internal Kairos order id (the `order_id` returned by `POST /orders` or `POST /v2/orders/submit`).
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: The order, including `raw` and `metadata`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
        '401':
          $ref: '#/components/responses/ExecUnauthorized'
        '400':
          description: '`order_id` is not a valid UUID (framework-level path rejection — plain-text body,
            not JSON).'
        '403':
          description: Order belongs to another user, missing `trade:read` scope, or API-key access to
            the order's provider is disabled. Empty body (status code only).
        '404':
          description: No order with that id. Empty body (status code only).
        '500':
          description: Database read failed. Empty body (status code only).
        '503':
          description: API-key access to the order's provider could not be checked (access-cache read
            failed — fails closed). Empty body (status code only).
  /orders/{order_id}/cancel:
    post:
      operationId: cancelOrder
      summary: Cancel a single order
      description: |-
        Cancels one order by internal id. Polymarket cancellation is an L2 API-key (HMAC) operation on the venue side, so no EOA/wallet signature is required — this works identically for custodial and external-signing orders.
        **This endpoint almost always returns `200`, even on failure to cancel.** `success: false` with a `message` covers: the order is already terminal (filled/cancelled/expired/failed), or the venue currently refuses the cancel (still live — retry). Non-2xx status codes are reserved for auth/ownership/not-found/infrastructure failures, not "the cancel didn't happen."
        **Cancel-before-submit race.** If the order hasn't reached the exchange yet (still `pending`/`queued`), it is cancelled locally via an atomic conditional update that races safely against the in-flight submission. If the submission wins that race (order mid-submission), the response is `success: false` asking the caller to retry shortly.
        **Fill-during-cancel.** A venue acknowledgement is followed by terminal fill verification. Until the response carries `terminal_fill_verification.state: complete`, it returns `success: false` and consumers must keep protection active. The completed receipt's `final_filled_quantity` is the authoritative venue cumulative (never the latest fill delta), including explicit zero, and is published only after the corresponding Trade/Position fold. Kalshi may complete this barrier synchronously; other venues normally complete through the polling worker and expose the retained receipt on retry or `GET /orders/{order_id}`.
        **Auth & scope.** Requires `trade:execute` and ownership of the order (`403` otherwise).
      tags:
      - Orders
      x-kairos-auth: api-key
      x-kairos-scope: trade:execute
      parameters:
      - name: order_id
        in: path
        required: true
        description: Internal Kairos order id.
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: Cancel outcome. Check `success` — a `200` does not guarantee the order was actually
            cancelled (see description).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderCancelResponse'
        '401':
          $ref: '#/components/responses/ExecUnauthorized'
        '400':
          description: No executor is registered for the order's exchange. Empty body (status code only).
        '403':
          description: 'Order belongs to another user, missing `trade:execute` scope, or API-key access
            to the order''s provider is disabled. Note this endpoint is behind the service-token/CSRF/API-key
            mutation gate, whose own rejections are `403` with a minimal `error` body. Otherwise empty
            body (status code only).'
        '404':
          description: No order with that id, or the order disappeared mid-cancel. Empty body (status
            code only).
        '500':
          description: Database read/write failed, credentials could not be loaded, or the cancel outcome
            could not be confirmed at all. Empty body (status code only).
        '503':
          description: API-key access to the order's provider could not be checked (fails closed). Empty
            body (status code only).
  /orders/{order_id}/amend:
    post:
      operationId: amendOrder
      summary: Reprice a resting limit order in place
      description: |-
        Moves a resting limit order's price in one venue round trip. The order keeps its Kairos `order_id` AND its venue `exchange_order_id` — that is the property this endpoint exists to provide: no supersession to record, and no second order whose fill could be booked twice. The cancel-and-replace it replaces leaves the book empty of the order for the whole cancel round trip and mints a second id to reconcile.
        **Venue support.** Only venues advertising `supports_native_amend` in `GET /exchanges/{exchange_id}/capabilities` — today `kalshi` alone. Anything else is `409` `EXCHANGE_AMEND_UNSUPPORTED`, answered before the order's own state is inspected so the verdict is a permanent property of the venue rather than of this moment. There is deliberately NO internal cancel-and-replace fallback: that substitution changes queue position, order identity and the caller's failure modes, so it is the caller's decision.
        **Reprice only.** `quantity` is accepted solely to be refused with `409` `EXCHANGE_AMEND_QUANTITY_UNSUPPORTED`, before the order is read — refused rather than silently ignored so a caller never believes it resized. Limit orders only; `price` must be strictly between 0 and 1, in the order's own outcome's terms.
        **This endpoint almost always returns `200`.** `success: false` is an ambiguous venue outcome, never a transport failure — the same convention `POST /orders/{order_id}/cancel` uses, because a 5xx invites a retry against an order that may already have been amended. It covers a terminal order, an order not yet at the venue, and a venue that refused; on every one of those the order is untouched and still resting on its old price. Branch on `success`, not on the status code.
        **Queue position.** Kalshi preserves it only when an amend DECREASES size; a reprice forfeits it — but so does the cancel/replace this replaces, so nothing is lost. The win is the closed off-book window, not priority.
        **Admission.** Runs the same checks a submit does — kill switch, side/kind/TIF circuit breakers evaluated against the order's own side and kind (an amend changes neither), and the order rate limit. An immediate fill from a crossing amend is booked through the normal poller path, not from this response.
        **Auth & scope.** Requires `trade:execute` and ownership of the order.
      tags:
      - Orders
      x-kairos-auth: api-key
      x-kairos-scope: trade:execute
      parameters:
      - name: order_id
        in: path
        required: true
        description: Internal Kairos order id.
        schema:
          type: string
          format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderAmendRequest'
      responses:
        '200':
          description: Amend outcome. Check `success` — a `200` does not guarantee the order was
            actually amended (see description).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderAmendResponse'
        '400':
          description: The order is not a limit order, the requested price is not strictly between
            0 and 1, or the price is off the market's tick grid — checked before any venue request,
            against the same grid a submit is held to, with the message naming the tick
            (`VALIDATION_INVALID_ORDER` / `VALIDATION_INVALID_PRICE`). A non-UUID `{order_id}`
            or a missing/unparseable JSON body is rejected before the handler runs and answers `400`
            with a plain-text body rather than the `OrderErrorResponse` envelope. Both
            representations are modelled below; branch on the response `Content-Type` and do not
            assume `error_details` is present on every `400` from this path.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
            text/plain:
              schema:
                type: string
                description: Rejection emitted before the handler runs — a malformed `{order_id}` or
                  a missing/unparseable JSON body. Carries no `error_details`; the text is not a
                  stable contract, so branch on the status and media type, never on its wording.
                example: 'Invalid URL: Cannot parse `order_id` with value `not-a-uuid` to a `Uuid`'
        '401':
          $ref: '#/components/responses/ExecUnauthorized'
        '403':
          description: Missing `trade:execute` scope, or API-key access to the order's venue is
            disabled (`AUTH_INSUFFICIENT_SCOPE`). This endpoint is also behind the service-token/CSRF/API-key
            mutation gate, whose own rejections are `403` with a minimal `error` body.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '404':
          description: No order with that id, or the order belongs to another user — both answer the
            same way (`VALIDATION_INVALID_ORDER`) so an id is not a probe.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '409':
          description: The venue has no native amend or is unknown (`EXCHANGE_AMEND_UNSUPPORTED`), or
            the request carried a `quantity` (`EXCHANGE_AMEND_QUANTITY_UNSUPPORTED`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '429':
          description: Order rate limit exceeded — an amend charges the same limiter a submit does.
            Carries `VALIDATION_INVALID_ORDER`; match the status, not the code.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '500':
          description: The order could not be read (`INTERNAL_ERROR`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '503':
          description: Execution disabled by a circuit breaker (`MARKET_PAUSED`), no executor
            registered for the venue (`EXCHANGE_ERROR`), venue credentials unavailable
            (`AUTH_CREDENTIALS_NOT_FOUND`), or the API-key provider-access lookup was unavailable
            (reported with `AUTH_INSUFFICIENT_SCOPE` — match the status here, not the code).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
  /orders/cancel-batch:
    post:
      operationId: cancelBatchOrders
      summary: Cancel a specific set of orders atomically at the selection layer
      description: |-
        Validates the ENTIRE selection before sending any venue request — if any requested order is missing, not owned by the caller, already terminal, on a mismatched exchange, or has no exchange order id yet (pre-submission), the whole request is rejected with no venue call made and no partial cancels. Once validation passes, the venue's native batch-cancel endpoint determines the per-order result; a venue-refused cancel is reported in `failures` without marking the local row cancelled.
        All selected orders must belong to the SAME `exchange_id` — a mixed-venue batch is rejected with `400`.
        **Auth & scope.** Requires `trade:execute`.
      tags:
      - Orders
      x-kairos-auth: api-key
      x-kairos-scope: trade:execute
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderCancelBatchRequest'
      responses:
        '200':
          description: Per-order batch-cancel outcome.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderCancelBatchResponse'
        '400':
          description: Empty `order_ids` (after de-duplication), more than 100 ids, the selected orders
            span more than one `exchange_id`, no executor is registered for that exchange, or the venue
            rejected the batch-cancel as an unsupported request. Empty body (status code only).
        '401':
          $ref: '#/components/responses/ExecUnauthorized'
        '403':
          description: One of the selected orders belongs to another user, missing `trade:execute` scope,
            or API-key access to the provider is disabled. Empty body (status code only).
        '404':
          description: One of the requested order ids does not exist. Empty body (status code only).
        '409':
          description: One of the requested orders is already terminal, or has no exchange order id yet
            (pre-submission). Empty body (status code only).
        '500':
          description: Database read/write failed, or the venue's batch-cancel call failed unexpectedly.
            Empty body (status code only).
  /orders/cancel-all:
    post:
      operationId: cancelAllOrders
      summary: Cancel all of the authenticated user's open orders on an exchange
      description: |-
        Kill-switch endpoint: cancels every open order the caller has on `exchange_id` (optionally scoped further to a single `market_id`) via the venue's native cancel-all call, then reconciles local `Order` rows up to however many the venue actually reported cancelled. If the venue reports `0` cancellations, no local rows are touched — a mismatch between "0 cancelled" and locally-tracked active orders is reconciled automatically in the background rather than being blindly marked cancelled.
        Unlike `POST /orders/{order_id}/cancel` and `POST /orders/cancel-batch`, input errors here (e.g. an unsupported `exchange_id`) surface as a real `400`, not a `success: false` 200 — this is the kill-switch path, so a caller-input problem must be visibly distinct from "the venue kept orders resting."
        **Auth & scope.** Requires `trade:execute`.
      tags:
      - Orders
      x-kairos-auth: api-key
      x-kairos-scope: trade:execute
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderCancelAllRequest'
      responses:
        '200':
          description: Cancel-all result. `cancelled_count` is the venue's authoritative count; `cancelled_order_ids`
            lists the local rows actually updated (may be fewer than `cancelled_count` if some local rows
            lag).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderCancelAllResponse'
        '400':
          description: 'No executor is registered for `exchange_id` — `VALIDATION_INVALID_ORDER`, message
            `unknown exchange_id ''<id>''` — or the venue rejected the cancel-all call as an invalid request
            (e.g. bad `market_id`).'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '401':
          description: The user's venue credentials could not be loaded (`AUTH_CREDENTIALS_NOT_FOUND`).
            NOTE this is a `401`, not a `500`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '403':
          description: Missing `trade:execute` scope (`AUTH_INSUFFICIENT_SCOPE`), or API-key access to
            this provider is disabled.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '500':
          description: An unclassified internal failure. Per-order local-row update failures are NOT
            errors — they only increment `failed_count` in a `200` response.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '502':
          description: The venue's cancel-all call failed (network/exchange error).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '503':
          description: API-key access to this provider could not be checked (`INTERNAL_ERROR`, fails closed).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '504':
          description: '`NETWORK_TIMEOUT` — the venue''s cancel-all call timed out.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
  /orders/fee-quote:
    get:
      operationId: getFeeQuote
      summary: Get a combined platform + exchange fee quote for a prospective trade
      description: |-
        Prices a trade you are considering WITHOUT submitting it: for a market order (`order_type=market`, `price` omitted) the live orderbook is walked for `quantity` to compute a size-weighted executable price; for a limit order (`order_type=limit`) `price` is required and used as the resting quote. Shares the exact computation used by the WebSocket RFQ stream (`subscribe_fee_quote`), so the two surfaces can never price a trade differently.
        Quotes are display-only, degrade-open estimates (`is_estimate: true`) — the authoritative fee is computed at fill time. If no fresh orderbook is available, every numeric field is a `"0"` placeholder and `pricing_unavailable: true`; callers MUST check that flag rather than rendering the zeros as a real quote.
        Supported `exchange_id` values: `polymarket`, `kalshi`, `predictfun`.
        **Auth & scope.** Requires `trade:read`.
      tags:
      - Orders
      x-kairos-auth: api-key
      x-kairos-scope: trade:read
      parameters:
      - name: exchange_id
        in: query
        required: true
        description: One of `polymarket`, `kalshi`, `predictfun`.
        schema:
          type: string
          enum:
          - polymarket
          - kalshi
          - predictfun
          example: polymarket
      - name: market_id
        in: query
        required: false
        description: Condition id (Polymarket). Required for Polymarket if `token_id` is omitted.
        schema:
          type: string
      - name: token_id
        in: query
        required: false
        description: CLOB token id (Polymarket) — preferred over `market_id` for book/fee lookup.
        schema:
          type: string
          example: '71360012345678901234567890123456789012345678901234567890123456'
      - name: quantity
        in: query
        required: true
        description: Decimal string. Number of shares/contracts to quote. Must be > 0.
        schema:
          type: string
          example: '100'
      - name: side
        in: query
        required: true
        description: The quote is side-aware (walks the ask side for buy, the bid side for sell).
        schema:
          type: string
          enum:
          - buy
          - sell
          example: buy
      - name: price
        in: query
        required: false
        description: Decimal string in `(0, 1]`. Required when `order_type=limit` (the resting price).
          Omit for `order_type=market` to have the server price off the live book.
        schema:
          type: string
          example: '0.52'
      - name: order_type
        in: query
        required: true
        description: '`market` (taker, priced from the book) or `limit` (maker, priced at `price`).'
        schema:
          type: string
          enum:
          - market
          - limit
          example: market
      responses:
        '200':
          description: Combined platform + exchange fee quote.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderFeeQuoteResponse'
        '400':
          description: Unsupported `exchange_id`, invalid `order_type`/`side`, non-positive `quantity`,
            `price` out of `(0, 1]`, a limit order missing `price`, or a Polymarket quote missing both
            `token_id` and `market_id`. Empty body (status code only) — the specific validation failure
            is logged server-side but never returned.
        '401':
          $ref: '#/components/responses/ExecUnauthorized'
        '403':
          description: Missing `trade:read` scope, or API-key access to this provider is disabled. Empty
            body (status code only).
        '503':
          description: API-key access to this provider could not be checked (fails closed). Empty body
            (status code only).
  /positions/exposure:
    get:
      operationId: getPositionsExposure
      summary: Get the authenticated user's current live position exposure
      description: |-
        Returns the caller's current positions from the lowest-latency source available, which is always at least as fresh as the synchronous fill path. This is the RECOMMENDED source of truth for "what do I currently hold" while actively trading — prefer it over any aggregate that may lag behind live fills.
        Three buckets, each token appears in at most one:
          - `positions` — open positions (`net_size != 0`), with `available_to_sell`
            (net size minus outstanding sell reservations) and `reserved`.
          - `closed_token_ids` — tokens the store just folded flat (within a
            10-minute window); callers may use presence here to zero any
            stale cached position for that token.
          - `resolved` — held tokens whose MARKET HAS RESOLVED, split out so a
            resolved loser is never confused with an open position; `redeemable`
            is `true` for a won (claimable) outcome, `false` for a loss.

        Absence from every bucket means the store has no current opinion on that token (neither "open" nor "just closed") — leave any previously-cached value as-is.
        **Central fallback.** On a deployment without a cell-local position store the response is built from central Postgres instead. In that mode `available_to_sell` always equals `net_size`, `reserved` is always `0`, and `closed_token_ids` is always empty — the reservation view is a regional-node feature.
        **Auth & scope.** Requires `position:read`. Takes no query, path or body parameters.
      tags:
      - Orders
      x-kairos-auth: api-key
      x-kairos-scope: position:read
      responses:
        '200':
          description: Current position exposure.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderPositionsExposureResponse'
        '401':
          $ref: '#/components/responses/ExecUnauthorized'
        '403':
          description: Missing `position:read` scope, or API-key access to one of the returned providers
            is disabled. Empty body (status code only).
        '500':
          description: Backing store read failed. Empty body (status code only).
        '503':
          description: API-key access to one of the returned providers could not be checked (fails closed).
            Empty body (status code only).
  /v2/orders/intent:
    post:
      operationId: createOrderIntent
      summary: Build an unsigned EIP-712 order payload for self-custody signing (step 1 of 2)
      description: |-
        The first step of the "external-signing" / bring-your-own-key institutional lane (Polymarket and Predict.fun, EOA only): the server builds the canonical EIP-712 typed-data message for the order you describe and stashes it single-use in Redis under a fresh `payload_id` (60 s TTL). NO order is placed and NOTHING is signed by Kairos — the response gives you a 32-byte digest (or the full typed-data payload) to sign yourself, off your own key, then hand back to `POST /v2/orders/submit`. This removes the custodial signing hop from the order path for approved market makers / institutional desks.
        **Venue.** `provider` selects the venue: `polymarket` (default) or `predictfun`. On Predict.fun you supply nothing venue-specific — the server resolves the market's `isYieldBearing`/`isNegRisk` pair (which selects one of four verifying contracts), its `feeRateBps` (part of the signed struct) and its price tick from authoritative metadata, and cross-checks your `intent.neg_risk` against the market. Predict.fun's signed `Order` struct is NOT the same shape as Polymarket's.
        `market_id` and `outcome` are METADATA ONLY — they are never part of the signed digest, so they can't be tampered with post-signing — but both are REQUIRED: the server resolves `intent.token_id` against `market_id` and rejects a mismatch, a blank `market_id`, or a missing `outcome` with `400`.
        Recommendation: omit `intent.salt` and `intent.timestamp_ms` and let the server stamp them — this guarantees a fresh digest per intent. Only set them yourself if you need to reproduce a digest deterministically (or if you're building the order fully client-side over the one-RTT WebSocket `submit_signed_order` command instead of this two-RTT REST flow, which REQUIRES you to set both — and is Polymarket-only).
        **Allowlist.** Restricted to `User.executionFastlaneEnabled` accounts, flipped by a Kairos admin during onboarding — an unapproved account gets `403 external-signing execution is not enabled for this account`, never a bare `401`, so a valid-but-unapproved caller gets an unambiguous signal. The gate fails closed on a DB error, which is also a `403` (`external-signing authorization check failed`) rather than a `500`. The flag is read through a short-lived cache, so a revocation takes effect within a couple of seconds rather than instantly.
        **Only `signature_type: 0` (EOA) is accepted** on this lane — Poly1271 / smart-wallet signature types are rejected at intake (`400`) rather than failing later at `/submit` with an opaque signer mismatch, and `signer_address` (if sent) must equal `owner_address`.
        **Admission.** The same size/price bounds, time-in-force and post-only capability gates, circuit breakers and per-user order rate limit that guard `POST /orders` are applied here, gated by the `FASTLANE_ADMISSION` deployment setting (`off` | `shadow` | `enforce`). Under `off`/`shadow` a narrower gate still runs: the execution circuit breakers and the post-only capability check. A rate-limit slot is charged HERE, on `/intent`, not on `/submit`.
        **Errors are the minimal `{"error": "..."}` shape** on this whole lane — no `error_details`, no `code`.
        **Auth & scope.** Requires `trade:execute` for the selected provider, AND the fastlane allowlist above. Unlike `POST /orders`, this endpoint does NOT require a service token / CSRF token — a session JWT alone is sufficient.
      tags:
      - Orders
      x-kairos-auth: api-key
      x-kairos-scope: trade:execute
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderIntentRequest'
      responses:
        '200':
          description: Unsigned EIP-712 payload + digest. Sign `eip712_digest_hex` as a raw 32-byte hash,
            or run `unsigned_payload` through `eth_signTypedData` — both yield an identical signature.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderIntentResponse'
        '400':
          description: |-
            Intake validation, metadata resolution, admission or payload-build failure. Exact `error` strings:
            - `only signature_type 0 (EOA) is supported on the external-signing lane`
            - `signer_address must equal owner_address for signature_type 0 (EOA)`
            - `outcome is required for external orders (e.g. "Yes" / "No")` / `outcome too long (max 64 chars)`
            - `market_id is required for external orders` / `market_id too long (max 256 chars)` / `token_id too long (max 256 chars)`
            - `token_id does not belong to the supplied market_id` / `token_id does not match the supplied outcome`
            - `expiration_unix_secs is in the past for this GTD order`
            - `invalid intent: …` — wraps the venue payload builder: price outside `(0, 1]`, size not positive or over 2 decimal places, GTD without `expiration_unix_secs`, `post_only` with a non-resting TIF, unparseable `token_id`, amount/notional overflow
            - Predict.fun only: `neg_risk mismatch: market <id> is neg_risk=<x>, intent said <y>`, `could not load predict.fun market <id>`, `expiration_unix_secs is required for time_in_force=GTD`, `invalid price: …` / `invalid size: …` / `invalid amounts: …`
            - Under `FASTLANE_ADMISSION=enforce`, the shared admission rejections: `Quantity must be positive`, `Minimum order quantity is N shares`, `Maximum order quantity is 1000000 shares`, `Price must be positive`, `Price is required for all orders`, `Price must be <= 1 for <venue>`, `Minimum $5 notional value required for API buy orders, got $N`, and the TIF/post-only capability messages.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderSimpleErrorResponse'
        '401':
          $ref: '#/components/responses/ExecUnauthorized'
        '403':
          description: |-
            `API key missing trade:execute scope`; `API-key access to <provider> is disabled`;
            `external-signing execution is not enabled for this account` (not allow-listed);
            `external-signing authorization check failed` (allowlist DB read failed — fails closed as a
            403, not a 500); or, under `FASTLANE_ADMISSION=enforce`, `Trading not enabled for user`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderSimpleErrorResponse'
        '429':
          description: '`Order rate limit exceeded` — the per-user (and per-credential, if your key has
            an `orders` override) order window. Only charged under `FASTLANE_ADMISSION=enforce`. No
            `Retry-After` header is sent.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderSimpleErrorResponse'
        '500':
          description: '`internal server error` — failed to resolve the outcome, serialize or store the
            intent, or a `payload_id` collision (astronomically unlikely). Under enforce-mode admission
            also `Exchange capabilities unavailable` and `Failed to get trading status`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderSimpleErrorResponse'
        '503':
          description: '`Execution disabled by circuit breaker: <reason>` or `Order rejected by circuit
            breaker: <reason>` (global/per-exchange halt, or a side/kind/TIF restriction — canary users
            bypass both); `API-key access to <provider> is unavailable` (access cache read failed);
            `predictfun execution is not available on this node`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderSimpleErrorResponse'
  /v2/orders/submit:
    post:
      operationId: submitSignedOrder
      summary: Submit your externally-signed order signature (step 2 of 2)
      description: |-
        Completes the self-custody order flow started by `POST /v2/orders/intent`: hand back `payload_id` and the 65-byte signature you produced over the returned digest. The server atomically claims the stored intent with a Redis `GETDEL` (single-use — a replayed or concurrent second submit for the same `payload_id` gets `400 payload_id not found or expired`), recomputes the EIP-712 digest itself (never trusting anything in this request beyond the signature), `ecrecover`s the signer, confirms it matches the intent's declared owner AND is a wallet registered to the authenticated caller, then forwards the assembled signed order to the venue CLOB.
        **The claim happens BEFORE any verification**, so the `payload_id` is consumed even when the request goes on to fail — a bad signature burns the intent.
        **Synchronous result.** `status` is the venue's immediate result string, passed through verbatim. For a marketable order that crossed immediately this IS your fill confirmation; for a resting order it confirms the order is live. `exchange_order_id` is the venue's handle — alongside the internal `order_id` it is the durable identifier for cancels.
        **Async fills.** A resting order's later fills are NOT returned here — they stream over your authenticated `/ws` connection as `partially_filled` / `filled` / `status_changed` events (deduped on the venue trade id; automatic reconciliation ensures fills are never silently lost, just occasionally a beat slower without `/ws`). Poll `GET /orders/{order_id}` if you are not holding a WebSocket open.
        **A persistence failure does NOT fail the request.** If the order lands at the venue but the Kairos row cannot be written, the `200` is still returned and the failure is logged — treat the venue as authoritative.
        **Retries.** The claimed intent is single-use and deleted on claim — a retry or a reprice requires a brand-new `POST /v2/orders/intent` (fresh `salt`/`timestamp_ms` → fresh digest → fresh signature). There is no silent server-side re-sign. The rate-limit slot was already charged at `/intent` and is not charged again here.
        **Auth & scope.** Requires the fastlane allowlist (re-checked here, independent of the check at `/intent`) and `trade:execute` for the provider recorded on the stored intent. No service token / CSRF token is required.
      tags:
      - Orders
      x-kairos-auth: api-key
      x-kairos-scope: trade:execute
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderSubmitSignedRequest'
      responses:
        '200':
          description: Venue's synchronous placement result.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderSubmitSignedResponse'
        '400':
          description: |-
            Exact `error` strings:
            - `payload_id not found or expired` — wrong / stale / already-claimed id
            - `payload_id expired` — the id resolved but its wall-clock `expires_at_us` has passed
            - `payload_id does not belong to authenticated user`
            - `invalid signature hex` — not decodable hex
            - `invalid signature: signature must be 65 bytes (got N)` / `invalid signature: failed to parse signature: …` / `invalid signature: ecrecover failed: …`
            - `signature recovered 0x… does not match owner 0x…` — usually the EIP-191-vs-raw-digest mistake
            - `recovered signer is not a registered wallet for this user` — also returned when the wallet lookup itself errors
            - `polymarket credentials not configured`
            - Predict.fun only: `predict.fun trading is not enabled for this account — no venue signing wallet is registered`; `this predict.fun account is a Kernel smart account, which the external-signing lane does not support yet …`; `owner_address is not your registered predict.fun trading wallet`
            - `venue returned an empty order id; order not tracked`
            - Under `FASTLANE_ADMISSION=enforce`, the re-run admission rejections (same set as `/intent`, minus the rate limit).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderSimpleErrorResponse'
        '401':
          $ref: '#/components/responses/ExecUnauthorized'
        '403':
          description: '`external-signing execution is not enabled for this account`; `external-signing
            authorization check failed`; `API key missing trade:execute scope`; `API-key access to <provider>
            is disabled`; or (enforce-mode admission) `Trading not enabled for user`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderSimpleErrorResponse'
        '500':
          description: '`internal server error` — Redis claim failure, stored intent no longer
            deserializes or rebuilds, the recomputed digest differs from the stored one (refused), or an
            internal failure assembling the order. The order may already be LIVE on the venue — verify
            via `GET /orders/{order_id}` or `/ws` before retrying.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderSimpleErrorResponse'
        '502':
          description: '`CLOB error: <venue message>` — the venue rejected the signed order (balance/allowance/tick);
            the venue''s message is passed through verbatim.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderSimpleErrorResponse'
        '503':
          description: '`API-key access to <provider> is unavailable` (access cache read failed);
            `verification temporarily unavailable` (an EIP-1271 RPC timeout — reachable only on a lane
            that accepted a non-EOA signature type, so not expected today); or an enforce-mode circuit-breaker
            rejection. NOTE: the `payload_id` was already consumed, so a retry needs a fresh intent.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderSimpleErrorResponse'
  /health:
    get:
      operationId: getHealth
      summary: Shallow liveness probe
      description: |-
        The load balancer's liveness probe. **Unauthenticated** — the only endpoint on this service that is. Always returns `200`; a failed Redis ping is reported as `status: "degraded"` / `healthy: false` in the body rather than as a non-2xx status, so the probe never removes an instance that is still serving.
        The deeper probes (`GET /health/deep`, `GET /executor/dead_letters`, `GET /internal/canary-health`) are gated by internal service tokens and are not reachable with an API key or a session JWT.
      tags:
      - Orders
      x-kairos-auth: public
      security: []
      responses:
        '200':
          description: Liveness state.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderHealthResponse'
  /exchanges:
    get:
      operationId: listExchanges
      summary: List the venue ids this deployment has registered
      description: |-
        Returns the raw registry ids (e.g. `polymarket`, `kalshi`, `predictfun`, `hyperliquid`, `opinion`) usable as `exchange_id` elsewhere in this API. Requires authentication but **no scope** — it exposes no user data.
        Infallible: it cannot return an error status.
      tags:
      - Exchanges
      x-kairos-auth: api-key
      responses:
        '200':
          description: Registered venue ids.
          content:
            application/json:
              schema:
                type: array
                items:
                  type: string
              example:
              - polymarket
              - kalshi
              - predictfun
              - hyperliquid
        '401':
          $ref: '#/components/responses/ExecUnauthorized'
  /exchanges/{exchange_id}:
    get:
      operationId: getExchange
      summary: Get a venue's display info and full capability set
      description: |-
        Returns the venue's display name, active flag, and the complete `capabilities` object that drives per-venue order validation. Requires authentication but **no scope**.
        The lookup canonicalizes aliases (`kalshi_offchain` resolves to `kalshi`), but the response's top-level `id` echoes what you asked for — read `capabilities.exchange_id` for the canonical id.
      tags:
      - Exchanges
      x-kairos-auth: api-key
      parameters:
      - name: exchange_id
        in: path
        required: true
        schema:
          type: string
          example: polymarket
      responses:
        '200':
          description: Venue info.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderExchangeInfo'
        '401':
          $ref: '#/components/responses/ExecUnauthorized'
        '404':
          description: No such venue on this deployment. Empty body (status code only).
  /exchanges/{exchange_id}/capabilities:
    get:
      operationId: getExchangeCapabilities
      summary: Get a venue's capability set
      description: |-
        The `capabilities` sub-object of `GET /exchanges/{exchange_id}`, on its own. This is the endpoint to consult before submitting an order: it tells you the venue's `supported_tif`, whether it honours `post_only`, its `min_tick_size` / `min_order_size` / `max_price`, and whether `POST /orders/cancel-all` is available (`supports_cancel_all`). Requires authentication but **no scope**.
      tags:
      - Exchanges
      x-kairos-auth: api-key
      parameters:
      - name: exchange_id
        in: path
        required: true
        schema:
          type: string
          example: polymarket
      responses:
        '200':
          description: The venue's capabilities.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderExchangeCapabilities'
        '401':
          $ref: '#/components/responses/ExecUnauthorized'
        '404':
          description: No such venue on this deployment. Empty body (status code only).
  /exchanges/{exchange_id}/allowances:
    post:
      operationId: setAllowances
      summary: Set the on-chain allowances the venue needs
      description: |-
        Grants the venue's contracts the token approvals they need, signed custodially with your delegated key. Idempotent in effect: an already-sufficient allowance is not re-sent, and the corresponding `*_tx_hash` comes back `null`.
        The three identity fields in the body are required by the schema but are validated against the authenticated caller — they can confirm authority, never grant it. A mismatch, or a wallet you do not own, is a `403`.
        Self-custody callers on the external-signing lane should use `POST /v2/onchain/intent` with `op: "approvals"` instead, and sign the approvals themselves.
        **Auth & scope.** Requires `trade:execute`, the mutation gate (API-key triple satisfies it), and a current Turnkey policy version.
      tags:
      - Exchanges
      x-kairos-auth: api-key
      x-kairos-scope: trade:execute
      parameters:
      - name: exchange_id
        in: path
        required: true
        schema:
          type: string
          example: polymarket
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderSetAllowancesRequest'
      responses:
        '200':
          description: Approvals submitted (or already sufficient).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderSetAllowancesResponse'
        '401':
          $ref: '#/components/responses/ExecUnauthorized'
        '403':
          description: Missing `trade:execute` scope, API-key access to this provider disabled, an identity
            field that does not match the authenticated caller (`AUTH_IDENTITY_MISMATCH`), a wallet you
            do not own, or installed Turnkey policies behind the required version (`AUTH_POLICY_OUTDATED`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '404':
          description: No allowance manager registered for this venue.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '500':
          description: Credentials or signing authority could not be resolved, or a non-retryable approval
            submission failed. Submission failures return `ALLOWANCE_APPROVAL_FAILED` without exposing
            signer or custody internals.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '502':
          description: An upstream approval dependency failed. Returns `ALLOWANCE_APPROVAL_FAILED`;
            re-read allowance state before retrying.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '503':
          description: Approval submission is temporarily busy, or API-key access to this provider could
            not be checked (fails closed).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '504':
          description: Approval submission or confirmation timed out. Returns `ALLOWANCE_APPROVAL_FAILED`;
            re-read allowance state because the transaction may have landed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
  /orders/route-quote:
    get:
      operationId: getRouteQuote
      summary: Price a size across both legs of an approved cross-venue market link
      description: |-
        Plans a buy across the two venues of an approved market link and returns the per-leg sizes, limit prices and fee estimates it would use, without submitting anything. `POST /orders/route-buy` executes the same plan.
        **A plan that cannot be built is still a `200`** with `routed: false` — no approved link for this market, sources unavailable, or fewer than two legs with a live book. Only caller-input errors are `400`.
        **Auth & scope.** Requires `trade:read` for `provider`.
      tags:
      - Routing
      x-kairos-auth: api-key
      x-kairos-scope: trade:read
      parameters:
      - name: provider
        in: query
        required: true
        description: The venue you are quoting from. Routable venues are `polymarket` and `predictfun`.
        schema:
          type: string
          example: polymarket
      - name: market_id
        in: query
        required: true
        schema:
          type: string
      - name: side
        in: query
        required: true
        description: '`yes` or `no`, case-insensitive.'
        schema:
          type: string
          enum:
          - 'yes'
          - 'no'
      - name: quantity
        in: query
        required: true
        description: Decimal string, must be > 0.
        schema:
          type: string
          example: '100'
      - name: slippage_cents
        in: query
        required: false
        description: Must be in `[1, 99]` if provided.
        schema:
          type: integer
      responses:
        '200':
          description: The routing plan (check `routed`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderRouteQuoteResponse'
        '400':
          description: '`slippage_cents` outside `[1, 99]`, invalid `side`, non-positive `quantity`, or
            a malformed authenticated user id. Empty body (status code only).'
        '401':
          $ref: '#/components/responses/ExecUnauthorized'
        '403':
          description: Missing `trade:read` scope, or API-key access to `provider` is disabled. Empty body
            (status code only).
        '503':
          description: API-key access to `provider` could not be checked (fails closed). Empty body (status
            code only).
  /orders/route-fees:
    get:
      operationId: getRouteFees
      summary: Get the per-leg fee model for an approved cross-venue market link
      description: |-
        Returns the fee model each leg of the link charges per side, so a client can show the routing cost before quoting a size. Legs on non-routable venues are silently omitted. An unrecognized link is a `200` with `routed: false` and an empty `legs`.
        **Auth & scope.** Requires `trade:read` for `provider`.
      tags:
      - Routing
      x-kairos-auth: api-key
      x-kairos-scope: trade:read
      parameters:
      - name: provider
        in: query
        required: true
        schema:
          type: string
          example: polymarket
      - name: market_id
        in: query
        required: true
        schema:
          type: string
      responses:
        '200':
          description: Per-leg fee models.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderRouteFeesResponse'
        '400':
          description: Malformed authenticated user id. Empty body (status code only).
        '401':
          $ref: '#/components/responses/ExecUnauthorized'
        '403':
          description: Missing `trade:read` scope, or API-key access to `provider` is disabled. Empty body
            (status code only).
        '503':
          description: API-key access to `provider` could not be checked (fails closed). Empty body (status
            code only).
  /orders/route-buy:
    post:
      operationId: routeBuy
      summary: Buy a size split across both legs of an approved cross-venue market link
      description: |-
        Executes the plan `GET /orders/route-quote` previews: creates a parent route row and submits one child order per leg. Children go through the same validation, admission and rate limiting as `POST /orders`.
        **Partial success is a `200`.** `status` is always `submitting`; a leg that failed to submit has `order_id: null` and a populated `legs[].error`. Track the parent via `GET /orders/routes` and the children via `GET /orders/{order_id}`.
        **Feature-gated.** Disabled unless `ROUTED_EXECUTION_ENABLED` is set on the deployment; otherwise every call is a `403`.
        **Auth & scope.** Requires the mutation gate (API-key triple satisfies it). This handler enforces no scope of its own — each child order carries the normal `trade:execute` and per-provider checks.
      tags:
      - Routing
      x-kairos-auth: api-key
      x-kairos-scope: trade:execute
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderRouteBuyRequest'
      responses:
        '200':
          description: Route created and children dispatched. Inspect `legs[].error` for per-leg failures.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderRouteBuyResponse'
        '400':
          description: '`slippage_cents` outside `[1, 99]`, non-positive `quantity`, an unresolvable
            `side`/`outcome`, or a malformed authenticated user id. Empty body (status code only).'
        '401':
          $ref: '#/components/responses/ExecUnauthorized'
        '403':
          description: Routed execution is disabled on this deployment. Empty body (status code only).
        '422':
          description: No approved market link for this market, no routable sources, or the planner
            produced no legs. Empty body (status code only).
        '500':
          description: The parent route row could not be created. Empty body (status code only).
        '503':
          description: Route sources could not be assembled. Empty body (status code only).
  /orders/route-close:
    post:
      operationId: routeClose
      summary: Close a routed position across both legs of a market link
      description: |-
        Sells the holdings on each leg of an approved market link, at a per-leg floor price derived from that leg's best bid minus `slippage_cents` (never below `0.01`). Holdings are rounded down to the venue's share precision, and a leg with less than `0.01` sellable is skipped.
        **`leg_caps` is fail-closed:** if you send it, a leg with no matching entry is excluded from the close entirely rather than closed in full.
        Like `route-buy`, this is a `200` with `status: submitting` and per-leg `error` strings; it is gated by `ROUTED_EXECUTION_ENABLED`.
        **Auth & scope.** Requires the mutation gate; per-leg `trade:execute` and provider checks happen on each child order.
      tags:
      - Routing
      x-kairos-auth: api-key
      x-kairos-scope: trade:execute
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderRouteCloseRequest'
      responses:
        '200':
          description: Close dispatched. Inspect `legs[].error`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderRouteCloseResponse'
        '400':
          description: '`slippage_cents` outside `[1, 99]`, an unresolvable `side`/`outcome`, or a
            malformed authenticated user id. Empty body (status code only).'
        '401':
          $ref: '#/components/responses/ExecUnauthorized'
        '403':
          description: Routed execution is disabled on this deployment. Empty body (status code only).
        '422':
          description: No approved market link, or nothing sellable after rounding and `leg_caps` filtering.
            Empty body (status code only).
        '500':
          description: The position read or the parent route insert failed. Empty body (status code only).
  /orders/routes:
    get:
      operationId: listRoutes
      summary: List the caller's routed parent orders and their legs
      description: |-
        Returns routed parents (from `route-buy` / `route-close`) newest first, each with its legs and the child order id, status and fill state. Available regardless of the `ROUTED_EXECUTION_ENABLED` flag, so history stays readable after routing is switched off.
        **Note on timestamps:** `created_at` / `completed_at` here are naive local timestamps serialized without a timezone offset — unlike `Order.created_at`, which is RFC 3339.
        **Auth.** Authentication only — this endpoint enforces no scope.
      tags:
      - Routing
      x-kairos-auth: api-key
      parameters:
      - name: limit
        in: query
        required: false
        description: Maximum routes to return. Defaults to 50 and is **not clamped** server-side.
        schema:
          type: integer
          format: int64
          default: 50
      responses:
        '200':
          description: The caller's routes, newest first.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/OrderRouteView'
        '400':
          description: Malformed authenticated user id. Empty body (status code only).
        '401':
          $ref: '#/components/responses/ExecUnauthorized'
        '500':
          description: Database read failed. Empty body (status code only).
  /credentials/invalidate:
    post:
      operationId: invalidateCredentials
      summary: Drop the service's cached copy of your venue credentials
      description: |-
        Evicts the cached credential set for one venue (or all of them) so the next order re-reads it from storage. Use after rotating a venue API key or re-running an enable-trading flow, when the executor would otherwise keep using the stale credential for the cache's lifetime.
        Does not delete or modify any stored credential.
        **Auth.** Authentication and self-ownership only — `user_id` must equal the authenticated caller. No scope is enforced and no mutation gate applies, so a bare session JWT is sufficient.
      tags:
      - Exchanges
      x-kairos-auth: api-key
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderCredentialsInvalidateRequest'
      responses:
        '200':
          description: Cache evicted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderCredentialsInvalidateResponse'
        '401':
          $ref: '#/components/responses/ExecUnauthorized'
        '403':
          description: '`user_id` does not match the authenticated caller. Empty body (status code only).'
        '404':
          description: An `exchange_id` was supplied but no credential provider is registered for it.
            Empty body (status code only).
  /combo/quote:
    post:
      operationId: quoteCombo
      summary: Price a Polymarket combo (parlay) without executing it
      description: |-
        Returns the real maker-quoted blended price for a multi-leg Polymarket combo. A naive product of the individual leg prices does NOT match what the RFQ gateway quotes, so this is the only correct way to price a parlay before placing it.
        Read-only: it opens an RFQ, takes the quote, drops the connection, and commits nothing. Polymarket-only — combos exist on no other venue.
        **camelCase.** The whole `/combo/*` family uses camelCase request and response field names, where every Orders endpoint is snake_case — check the schema for the family you are calling rather than assuming one convention across this API. Prices and amounts are six-decimal fixed-point INTEGER strings (`"16393"` = 0.016393), not decimals.
        **Requires connected Polymarket credentials.** Every wallet type can quote (quoting never signs), including imported wallet types that cannot yet execute a combo.
        **Auth & scope.** Requires `trade:read` for `polymarket`. No mutation gate, so a session JWT alone works. Not rate limited.
      tags:
      - Combo
      x-kairos-auth: api-key
      x-kairos-scope: trade:read
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderComboQuoteRequest'
      responses:
        '200':
          description: The maker's blended quote.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderComboQuoteResponse'
        '400':
          description: |-
            `VALIDATION_INVALID_ORDER`. Triggers: fewer than 2 or more than 10 legs; `notionalUsd` not finite or not positive, or too small to round to a non-zero six-decimal amount; Polymarket credentials not configured; an imported wallet whose signature type could not be determined. Also the gateway outcomes, which are deliberately mapped to plain-English messages that never leak gateway status codes:
            - no maker is pricing the parlay right now (can happen while a leg is in play)
            - no maker will take a parlay this size
            - the quote expired before it could be filled
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '401':
          $ref: '#/components/responses/ExecUnauthorized'
        '403':
          description: Missing `trade:read` scope, or API-key access to `polymarket` is disabled.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '500':
          description: '`INTERNAL_ERROR` — credential load failed, the credential set is not Polymarket,
            RFQ gateway authentication failed, or any other unmapped gateway error.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '503':
          description: API-key access to `polymarket` could not be checked (fails closed).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
  /combo/execute:
    post:
      operationId: executeCombo
      summary: Buy a Polymarket combo (parlay) via the RFQ gateway
      description: |-
        Runs the whole requester flow server-side in one call: creates the RFQ from your chosen leg position ids, accepts the best quote with a signed order, and returns the on-chain result. It is one call rather than two precisely to beat the quote expiry — there is no separate accept step.
        **Set `maxPriceCents`.** Without it you accept whatever blended price the maker returns. With it, a quote worse than your ceiling is refused and nothing trades (a `400`).
        **One-time collateral approval.** Combos settle against Polymarket's v3 exchange, which ordinary trading never approves. The first execute transparently sets that approval — directly for an EOA, or through the Polymarket relayer for a Kairos deposit wallet. **Imported smart-account wallets (Safe / imported Poly1271) cannot execute combos yet** and get a `400` explaining so; quoting still works for them.
        **Fees.** The Kairos platform fee is resolved from your tier BEFORE the irreversible RFQ is submitted (a tier-lookup outage stops the trade rather than making it free), then accrued against the pUSD spent through the same ledger and sweep as ordinary fills.
        **Auth & scope.** Requires `trade:execute` for `polymarket` plus the service-token/CSRF/API-key mutation gate, which the API-key triple satisfies automatically.
      tags:
      - Combo
      x-kairos-auth: api-key
      x-kairos-scope: trade:execute
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderComboExecuteRequest'
      responses:
        '200':
          description: The combo settled on-chain.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderComboExecuteResponse'
        '400':
          description: |-
            `VALIDATION_INVALID_ORDER`. All the `POST /combo/quote` triggers, plus: the wallet is an imported smart-account type that cannot execute combos yet; and the slippage refusal — the maker's quote came back above `maxPriceCents`, so nothing traded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '401':
          $ref: '#/components/responses/ExecUnauthorized'
        '403':
          description: Missing `trade:execute` scope, or API-key access to `polymarket` is disabled.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '500':
          description: '`INTERNAL_ERROR` — credential load failed, no regional signing authority on the
            credentials, the v3 collateral approval failed, the relayer is not configured, the fee-tier
            lookup failed (fails closed — the trade is refused rather than made free), gateway auth failed,
            or any other unmapped gateway error.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '503':
          description: API-key access to `polymarket` could not be checked (fails closed).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
  /combo/positions:
    get:
      operationId: listComboPositions
      summary: List the authenticated user's Polymarket combo positions
      description: |-
        Returns the combos held by the caller's Polymarket maker wallet, newest/most-valuable first, each with its per-leg resolution state. Capped at 50 combos; there is no pagination parameter and no query parameters at all.
        **At the combo level, read `redeemable`, not `status`.** A won-but-unredeemed combo keeps `status: "OPEN"`, so inferring redeemability from `status` — or from leg prices — is wrong. `redeemable` is the authoritative flag and is `true` only for a resolved win with a non-zero balance; it covers "not resolved", "lost" and "already redeemed" in one check.
        **At the leg level, `legStatus` IS the authoritative field.** Use `legStatus` (`OPEN` / `RESOLVED_WIN` / `RESOLVED_LOSS`) to mark a leg won, lost or pending. Never infer a leg's outcome from `currentPrice` — the price is display data and a heuristic over it will be wrong. The caution above is about the combo-level `status` field specifically, and does not extend to `legStatus`.
        A parlay is all-or-nothing: one `RESOLVED_LOSS` leg makes the whole combo worth $0 even while sibling legs are still pending.
        **Auth & scope.** Requires `position:read` for `polymarket`. No mutation gate.
      tags:
      - Combo
      x-kairos-auth: api-key
      x-kairos-scope: position:read
      responses:
        '200':
          description: The caller's combo positions.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderComboPositionsResponse'
        '400':
          description: '`VALIDATION_INVALID_ORDER` — Polymarket credentials not configured, or an imported
            wallet whose signature type could not be determined.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '401':
          $ref: '#/components/responses/ExecUnauthorized'
        '403':
          description: Missing `position:read` scope, or API-key access to `polymarket` is disabled.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '500':
          description: '`INTERNAL_ERROR` — credential load failed, or the upstream combo-positions read
            failed or returned an unparseable body.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '503':
          description: API-key access to `polymarket` could not be checked (fails closed).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
  /combo/cash-out-quote:
    post:
      operationId: quoteComboCashOut
      summary: Price a combo cash-out (SELL) without executing it
      description: |-
        Returns the real maker-quoted proceeds for selling an open combo, and subtracts the Kairos platform fee so the preview matches what actually lands in the wallet. A client-side `shares × ∏(leg price)` estimate ignores the maker's spread and systematically overstates the payout — show `netProceedsE6`, not a local calculation.
        The platform-fee lookup **fails closed**: a tier-lookup error rejects the quote rather than displaying an optimistic zero fee.
        **Fully-resolved and dead combos are rejected up front.** No maker quotes a settled binary, so the RFQ would simply hang; a combo that has fully resolved, or that has any single `RESOLVED_LOSS` leg, gets an immediate `400` instead. A won, fully-resolved combo is redeemed via `POST /combo/redeem`, not cashed out.
        **Rate limited** at 20 quotes/minute per user (`COMBO_QUOTE_RATE_LIMIT_PER_MIN`), fail-closed, because each call holds an external RFQ websocket open until the gateway timeout. The `429` carries `error_details.code = EXCHANGE_POLYMARKET_RATE_LIMITED`. No `Retry-After` header.
        **Auth & scope.** Requires `position:read` for `polymarket` AND the mutation gate (it is a POST behind `RequireServiceToken` despite being read-only), so an API-key triple or CSRF token is needed — a bare Bearer JWT is not enough.
      tags:
      - Combo
      x-kairos-auth: api-key
      x-kairos-scope: position:read
      x-kairos-rate-limit: 20/minute per user
      x-kairos-bucket: combo-quote
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderComboCashOutQuoteRequest'
      responses:
        '200':
          description: Gross proceeds, platform fee, and net proceeds.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderComboCashOutQuoteResponse'
        '400':
          description: '`VALIDATION_INVALID_ORDER` — fewer than 2 or more than 10 legs; `shares` not finite
            or not positive; credentials not configured; the parlay has a lost leg and can no longer pay
            out; the parlay has fully resolved (redeem it instead); or a gateway no-quote / size / expiry
            outcome.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '401':
          $ref: '#/components/responses/ExecUnauthorized'
        '403':
          description: Missing `position:read` scope, or API-key access to `polymarket` is disabled.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '429':
          description: Combo-quote rate limit exceeded, or its Redis was unreachable (fails closed).
            `error_details.code = EXCHANGE_POLYMARKET_RATE_LIMITED`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '500':
          description: '`INTERNAL_ERROR` — credential load failed, the upstream positions read failed,
            the fee-tier lookup failed (fails closed), gateway auth failed, or another unmapped gateway
            error.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '503':
          description: API-key access to `polymarket` could not be checked (fails closed).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
  /combo/cash-out:
    post:
      operationId: cashOutCombo
      summary: Sell an open combo position back to pUSD
      description: |-
        Sells `shares` of an open combo through the RFQ gateway. Preview it first with `POST /combo/cash-out-quote`.
        **`minProceedsUsd` is checked against GROSS proceeds**, before the platform fee is deducted — so the amount that lands can be below the floor you set by the fee amount. Size the floor accordingly.
        Same up-front rejection as the quote endpoint: a combo with a lost leg, or one that has fully resolved, cannot be cashed out (no maker quotes a worthless or settled combo) and fails fast rather than hanging. Unlike the quote endpoint this is not rate limited — selling a position is economically self-limiting.
        **Auth & scope.** Requires `trade:execute` for `polymarket` plus the mutation gate.
      tags:
      - Combo
      x-kairos-auth: api-key
      x-kairos-scope: trade:execute
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderComboCashOutRequest'
      responses:
        '200':
          description: The cash-out settled.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderComboCashOutResponse'
        '400':
          description: '`VALIDATION_INVALID_ORDER` — the same set as `POST /combo/cash-out-quote`, plus
            the floor refusal: the maker''s quote came back below `minProceedsUsd`, so nothing traded.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '401':
          $ref: '#/components/responses/ExecUnauthorized'
        '403':
          description: Missing `trade:execute` scope, or API-key access to `polymarket` is disabled.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '500':
          description: '`INTERNAL_ERROR` — credential load failed, the upstream positions read failed,
            the fee-tier lookup failed, gateway auth failed, or another unmapped gateway error.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '503':
          description: API-key access to `polymarket` could not be checked (fails closed).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
  /combo/redeem:
    post:
      operationId: redeemCombo
      summary: Redeem a resolved, winning combo back to pUSD
      description: |-
        The settlement path a cash-out cannot cover: once every leg has resolved there is no maker to sell to, so a winning combo is redeemed on-chain instead. Executed as a Polymarket relayer `WALLET` batch (approve the combo Router as operator, then redeem), signed by the deposit wallet's owner EOA through the user's delegated Turnkey key.
        **Kairos deposit wallets only.** Imported and plain-EOA makers get a `400` — the direct redemption path for them is not built yet.
        **Idempotent by construction.** The amount comes from the position's real on-chain share balance, never from the request, and the call is validated against `GET /combo/positions` first. A retry after a successful redeem sees a zero balance and is rejected, rather than firing a second batch that would revert on-chain while the client reads "redeemed".
        **Auth & scope.** Requires `trade:execute` for `polymarket`, the mutation gate, and a current Turnkey policy version for the combo-redeem capability specifically (`403 AUTH_POLICY_OUTDATED` if stale).
      tags:
      - Combo
      x-kairos-auth: api-key
      x-kairos-scope: trade:execute
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderComboRedeemRequest'
      responses:
        '200':
          description: The redemption batch was mined.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderComboRedeemResponse'
        '400':
          description: '`VALIDATION_INVALID_ORDER` — the wallet is not a Kairos deposit wallet; the
            `comboConditionId` is malformed; no combo with that condition id exists on this wallet; the
            parlay is not redeemable (not resolved as a win yet, or already redeemed); or it has no
            redeemable share balance.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '401':
          $ref: '#/components/responses/ExecUnauthorized'
        '403':
          description: Missing `trade:execute` scope, API-key access to `polymarket` disabled, or installed
            Turnkey policies behind the version the combo-redeem capability requires (`AUTH_POLICY_OUTDATED`,
            action `update_policies`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '500':
          description: '`INTERNAL_ERROR` — credential load failed, the upstream positions read failed,
            the relayer is not configured, or the redemption batch failed.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '503':
          description: API-key access to `polymarket` could not be checked (fails closed).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
  /orders/market-links:
    post:
      operationId: createMarketLink
      summary: Create (or preview) a self-serve cross-venue market link
      description: |-
        Declares that two markets on two different venues are the same real-world contract, making them routable together by `GET /orders/route-quote`, `POST /orders/route-buy` and `POST /orders/route-close`.
        **Two-step by design.** `confirm: false` (the default) returns a verified preview and writes nothing. Show the user the resolved pairing, then call again with `confirm: true` AND the preview's `verification_fingerprint` as `fingerprint`. The confirm re-resolves against fresh metadata and requires the result to fingerprint-match what was reviewed, so index drift between the two calls becomes a `409` ("review the pairing again") rather than a silently different link.
        **You attest; the server verifies.** Both legs are resolved against indexed market metadata and the link is REFUSED unless the venues' yes/no outcome labels pair exactly (case-insensitive). Outcome inversion is the one catastrophic failure mode here, so it is gated deterministically rather than by the user's click — a `Yes/No` vs `Trump/Harris` pair may well be the same market, but the mapping is not machine-provable and is refused rather than guessed.
        **Scope of a created link.** Links are approved but USER-SCOPED: routable only by their creator, so a bad self-link's blast radius is the creator's own wallet. Capped at 25 links per user, enforced inside the insert transaction so concurrent confirms cannot overshoot it.
        Expiry gaps and resolution-source divergence come back as `warnings`, not errors — for a self-scoped link they are the creator's risk to accept.
        **Rate limited** at 15 requests/minute per user (`MARKET_LINK_RATE_LIMIT_PER_MIN`), fail-closed. Previews count: every call costs two indexed-metadata lookups plus duplicate checks, and the 25-link cap only bounds completed inserts.
        > **Error shape.** This endpoint returns `{"message": "..."}` — NOT the `{"error": ...}` used everywhere else on this service, and with no `code` field. See `OrderMarketLinkErrorResponse`.
        **Auth.** Requires authentication and the mutation gate (API-key triple, `X-Service-Token`, or CSRF token). It enforces **no scope** — any authenticated credential that clears the mutation gate can create links.
      tags:
      - Routing
      x-kairos-auth: api-key
      x-kairos-rate-limit: 15/minute per user
      x-kairos-bucket: market-link
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderCreateMarketLinkRequest'
      responses:
        '200':
          description: A verified preview, a newly created link, or an existing routable link — read
            `status` to tell which.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderMarketLinkResponse'
        '400':
          description: '`invalid user id`; `exactly two legs required`; or `confirm requires the preview''s
            verification fingerprint`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderMarketLinkErrorResponse'
        '401':
          $ref: '#/components/responses/ExecUnauthorized'
        '409':
          description: '`one of these markets is already claimed by another link`; `one of these markets
            was just claimed by another link` (lost insert race); or `market metadata changed since the
            preview — review the pairing again` (fingerprint mismatch).'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderMarketLinkErrorResponse'
        '422':
          description: |-
            The pairing is not verifiable. Messages: `<provider> is not a routable venue`; `legs must be on two different venues`; `<provider>: market <id> is not indexed`; `<provider>: market is settled|closed|resolved`; `<provider>: self-serve linking not supported`; `polymarket: not a binary market (N outcomes)`; `polymarket: outcome labels unavailable` / `token ids unavailable` / `condition id unavailable`; `predictfun: yes token unavailable` / `no token unavailable` / `outcome labels unavailable`; `outcome labels differ (A/B vs C/D) — the side mapping can't be verified`; `link limit reached (25 per user)`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderMarketLinkErrorResponse'
        '429':
          description: '`too many link requests — wait a moment and try again`. No `Retry-After` header.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderMarketLinkErrorResponse'
        '500':
          description: '`link lookup failed` or `link insert failed`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderMarketLinkErrorResponse'
        '502':
          description: '`market metadata lookup failed` — the indexed-metadata query failed.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderMarketLinkErrorResponse'
  /exchanges/{exchange_id}/prepare-wallet:
    post:
      operationId: prepareWallet
      summary: Sponsor gas and set the venue's required allowances on a wallet
      description: |-
        The one-time onboarding step behind "enable trading": tops the wallet up with sponsored gas if it needs it, then sets every token allowance the venue's contracts require. Both legs are signed custodially with the user's delegated key.
        **`wait_for_confirmation` defaults to `true`.** The call then blocks until the work completes and the response reports what actually happened. Passing `false` returns immediately with `confirmation_pending: true`, `success: true`, `gas_sponsored: false` and `allowances_set: 0` — **`success: true` there means "accepted", not "done"**, and a background failure is only logged, never surfaced. Poll the RPC query `exchange.getAllowances` to find out whether it worked.
        The three identity fields are mandatory but are validated against the authenticated caller and the wallets it owns; they can confirm authority, never grant it.
        Self-custody callers on the external-signing lane should use `POST /v2/onchain/intent` with `op: "approvals"` instead and pay their own gas.
        **Auth & scope.** Requires `trade:execute`, the mutation gate (this endpoint spends gas-station funds, so a stolen JWT alone must not reach it), wallet ownership, and a current Turnkey policy version.
      tags:
      - Exchanges
      x-kairos-auth: api-key
      x-kairos-scope: trade:execute
      parameters:
      - name: exchange_id
        in: path
        required: true
        schema:
          type: string
          example: polymarket
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderPrepareWalletRequest'
      responses:
        '200':
          description: Wallet prepared, or — when `wait_for_confirmation` was sent as false — preparation
            accepted and running in the background.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderPrepareWalletResponse'
        '400':
          description: '`user_id` is not a valid UUID, or an identity field is otherwise unusable.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '401':
          $ref: '#/components/responses/ExecUnauthorized'
        '403':
          description: Missing `trade:execute` scope, API-key access to this provider disabled, an identity
            field that does not match the authenticated caller (`AUTH_IDENTITY_MISMATCH`), a wallet the
            caller does not own, or installed Turnkey policies behind the required version
            (`AUTH_POLICY_OUTDATED`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '404':
          description: No pre-trade service is registered for this venue.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '500':
          description: Credentials or regional signing authority could not be resolved, or the wallet
            preparation itself failed. Only reachable on the blocking path — a background failure never
            surfaces to the caller.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '503':
          description: API-key access to this provider could not be checked (fails closed).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
  /exchanges/polymarket/enable-trading:
    post:
      operationId: enablePolymarketTrading
      summary: Provision Polymarket CLOB credentials and set on-chain approvals
      description: |-
        The one-time onboarding step for Polymarket. In a single call the server derives CLOB API credentials for your wallet (signing the ClobAuth EIP-712 message with your delegated key), encrypts and stores them, and sets the token approvals the venue needs. Until this succeeds, `POST /orders` on `polymarket` will fail.
        **Credentials are never returned.** The derived API key/secret/passphrase are stored server-side only — deliberately kept out of HTTP responses so they cannot leak through logs, proxies or browser devtools.
        Legacy-EOA users get gas sponsorship plus approvals here. Deposit-wallet users had their approvals set by the onboarding batch (`POST /exchanges/polymarket/deposit-wallet/onboard`), so the approval legs are skipped and the `*_tx_hash` fields are omitted.
        **Auth & scope.** Requires `trade:execute` for `polymarket`, wallet ownership, and the mutation gate — this provisions signing credentials and is in the same risk class as order submission.
      tags:
      - Onboarding
      x-kairos-auth: api-key
      x-kairos-scope: trade:execute
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderPolymarketEnableTradingRequest'
      responses:
        '200':
          description: Credentials provisioned and approvals set.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderPolymarketEnableTradingResponse'
        '400':
          description: '`ValidationInvalidOrder` — `wallet_address` is not a valid address, or the
            authenticated user id is not a UUID.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '401':
          $ref: '#/components/responses/ExecUnauthorized'
        '403':
          description: '`AuthCredentialsInvalid` — "This API key lacks the trade:execute scope";
            `AuthInsufficientScope` / API-key access to `polymarket` disabled; or `AuthTurnkeyWalletNotFound`
            — "Wallet not found or does not belong to you".'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '500':
          description: Database read failed, credential derivation against Polymarket failed, or the
            approval transactions failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '503':
          description: API-key access to `polymarket` could not be checked (fails closed).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
  /exchanges/polymarket/enable-imported-trading:
    post:
      operationId: enablePolymarketImportedTrading
      summary: Provision CLOB credentials for an imported Polymarket wallet
      description: |-
        The equivalent of `POST /exchanges/polymarket/enable-trading` for a wallet imported from polymarket.com. Derives and stores CLOB credentials against the imported EOA. No approval legs run — an imported wallet already carries its on-chain approvals from its prior polymarket.com activity.
        Neither `wallet_address` nor `turnkey_org_id` is trusted input: both are checked against the authenticated caller before anything is minted, so a stolen session cannot provision credentials against a wallet or sub-org it has no relationship to.
        **Rate limited** at 5 requests/minute per user (`CLOB_PROVISIONING_RATE_LIMIT_PER_MIN`, bucket `clob-provisioning`), fail-closed — each allowed call makes a real CLOB round-trip and writes a credential row. The `429` carries `error_details.code = VALIDATION_INVALID_ORDER` with the message `Too many import attempts — please wait a moment and try again`.
        **Auth & scope.** Requires `trade:execute` for `polymarket` and the mutation gate.
      tags:
      - Onboarding
      x-kairos-auth: api-key
      x-kairos-scope: trade:execute
      x-kairos-rate-limit: 5/minute per user
      x-kairos-bucket: clob-provisioning
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderPolymarketEnableImportedTradingRequest'
      responses:
        '200':
          description: Imported-wallet credentials provisioned.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderPolymarketEnableImportedTradingResponse'
        '400':
          description: Malformed wallet address or user id.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '401':
          $ref: '#/components/responses/ExecUnauthorized'
        '403':
          description: Missing `trade:execute` scope, API-key access to `polymarket` disabled, the
            supplied `turnkey_org_id` is not the caller's org, or the wallet is not the caller's.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '429':
          description: CLOB-provisioning rate limit exceeded (or its Redis was unreachable — fails
            closed). No `Retry-After` header.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '500':
          description: Credential derivation or persistence failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '503':
          description: API-key access to `polymarket` could not be checked (fails closed).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
  /exchanges/predictfun/enable-trading:
    post:
      operationId: enablePredictfunTrading
      summary: Set the on-chain approvals Predict.fun needs
      description: |-
        Predict.fun has no server-side credentials to provision — orders are signed at submission time. This endpoint does the one-time on-chain setup instead: it approves USDT and ConditionalTokens across all four `(yieldBearing × negRisk)` market variants, gas-sponsored on BSC. A wallet must run this once before its first Predict.fun order; the executor does not approve per trade.
        **Idempotent.** Variants already approved are no-ops. A wallet that was only partially approved re-submits just the missing variants on the next call, so retrying is safe.
        > **Error shape.** This endpoint returns the minimal `{"error": "...", "code": "..."}` body (`OrderSimpleErrorResponse`), NOT the structured `error_details` envelope used by the Polymarket and Opinion equivalents. Codes: `INSUFFICIENT_SCOPE`, `PLATFORM_API_ACCESS_DISABLED`, `RATE_LIMITED`, `INVALID_USER_ID`, `DATABASE_ERROR`.
        **Rate limited** at 5 requests/minute per user (bucket `clob-provisioning`, shared with the other heavy-provisioning endpoints), fail-closed, and checked BEFORE the ownership query so a flood cannot load the database. Each allowed call can launch up to 12 sponsored BSC approvals.
        **Auth & scope.** Requires `trade:execute` for `predictfun`, wallet ownership, and the mutation gate.
      tags:
      - Onboarding
      x-kairos-auth: api-key
      x-kairos-scope: trade:execute
      x-kairos-rate-limit: 5/minute per user
      x-kairos-bucket: clob-provisioning
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderPredictfunEnableTradingRequest'
      responses:
        '200':
          description: Approvals set (or already present).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderPredictfunEnableTradingResponse'
        '400':
          description: '`INVALID_USER_ID`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderSimpleErrorResponse'
        '401':
          $ref: '#/components/responses/ExecUnauthorized'
        '403':
          description: '`INSUFFICIENT_SCOPE` ("This API key lacks the trade:execute scope"),
            `PLATFORM_API_ACCESS_DISABLED`, or the wallet is not owned by the caller.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderSimpleErrorResponse'
        '429':
          description: '`RATE_LIMITED` — "Too many enable-trading attempts — please wait a moment and
            try again". No `Retry-After` header.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderSimpleErrorResponse'
        '500':
          description: '`DATABASE_ERROR`, or an approval transaction failed.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderSimpleErrorResponse'
        '503':
          description: API-key access to `predictfun` could not be checked (fails closed).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderSimpleErrorResponse'
  /exchanges/opinion/enable-trading:
    post:
      operationId: enableOpinionTrading
      summary: Provision Opinion credentials and set the USDT allowance
      description: |-
        One-time onboarding for Opinion: auto-provisions Opinion API credentials through their builder API, encrypts and stores them, then sets the USDT allowance on BSC (gas-sponsored).
        **Known recovery case.** If Opinion already has the wallet on file but Kairos has lost the original API key, and Opinion's recovery endpoint is unavailable, the call returns `400` with `error_details.code = AUTH_CREDENTIALS_NOT_FOUND` and actions pointing at Kairos support (email + Discord). That state cannot be resolved by retrying — the Opinion account must be reset manually.
        **Auth & scope.** Requires `trade:execute` for `opinion`, wallet ownership, the mutation gate, and a current Turnkey policy version.
      tags:
      - Onboarding
      x-kairos-auth: api-key
      x-kairos-scope: trade:execute
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderOpinionEnableTradingRequest'
      responses:
        '200':
          description: Credentials provisioned and USDT approved.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderOpinionEnableTradingResponse'
        '400':
          description: '`ValidationInvalidOrder` — invalid user id; or `AuthCredentialsNotFound` — the
            manual-reset recovery case described above.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '401':
          $ref: '#/components/responses/ExecUnauthorized'
        '403':
          description: Missing `trade:execute` scope, API-key access to `opinion` disabled, wallet not
            owned by the caller, or installed Turnkey policies behind the required version
            (`AUTH_POLICY_OUTDATED`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '500':
          description: '`DatabaseError`, or credential provisioning / the allowance transaction failed.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '503':
          description: API-key access to `opinion` could not be checked (fails closed).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
  /exchanges/kalshi/enable-trading:
    post:
      operationId: enableKalshiTrading
      summary: Connect your own Kalshi account by storing its API credentials
      description: |-
        Kalshi is the one venue where Kairos does NOT custody or provision an identity — you trade against your own Kalshi account, so you supply your own Kalshi API key and RSA private key here. The server validates the PEM, proves the credentials work by calling Kalshi's balance endpoint, then encrypts them (AES-256-GCM) and stores them.
        **You are handing over a live private key.** It is stored encrypted and used only to sign Kalshi requests on your behalf. Rotate it at Kalshi and re-run this endpoint to replace it.
        Both PKCS#1 and PKCS#8 PEM encodings are accepted, and a key pasted as a single line with literal `\n` escapes is normalized before parsing.
        `POST /exchanges/kalshi_offchain/enable-trading` is a deprecated alias of this route, kept for clients that are not deployed in lockstep.
        > **Error shape.** Minimal `{"error": "...", "code": "..."}` (`OrderSimpleErrorResponse`), not the structured envelope.
        **Auth & scope.** Requires `trade:execute` for `kalshi` and the mutation gate.
      tags:
      - Onboarding
      x-kairos-auth: api-key
      x-kairos-scope: trade:execute
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderKalshiEnableTradingRequest'
      responses:
        '200':
          description: Credentials validated against Kalshi and stored.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderKalshiEnableTradingResponse'
        '400':
          description: '`INVALID_USER_ID`; `MISSING_API_KEY_ID` ("API Key ID is required");
            `INVALID_PRIVATE_KEY` ("Invalid RSA private key. Must be a valid PEM-encoded RSA key.");
            or `INVALID_CREDENTIALS` — the key parsed but Kalshi rejected it.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderSimpleErrorResponse'
        '401':
          $ref: '#/components/responses/ExecUnauthorized'
        '403':
          description: '`INSUFFICIENT_SCOPE` or `PLATFORM_API_ACCESS_DISABLED`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderSimpleErrorResponse'
        '500':
          description: '`CONFIG_ERROR` ("Internal configuration error" / "Encryption configuration
            error"), `ENCRYPTION_ERROR` ("Failed to encrypt credentials"), or `DB_ERROR` ("Failed to
            save credentials").'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderSimpleErrorResponse'
  /exchanges/predictfun/account:
    get:
      operationId: getPredictfunAccount
      summary: Get the caller's Predict.fun account profile
      description: |-
        Authenticates against Predict.fun with the caller's wallet and returns the `data` block of their `GET /v1/account` — display name, address, referral state and points. The upstream shape is forwarded verbatim, so new upstream fields appear without a Kairos change; treat the response as open.
        > **Error shape.** Minimal `{"error": "...", "code": "..."}` (`OrderSimpleErrorResponse`).
        **Auth.** Authentication only — this endpoint enforces **no scope** and no per-provider access check. Any authenticated credential reaches it.
      tags:
      - Onboarding
      x-kairos-auth: api-key
      responses:
        '200':
          description: The caller's Predict.fun profile.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderPredictfunAccountResponse'
        '401':
          $ref: '#/components/responses/ExecUnauthorized'
        '404':
          description: '`NO_CREDENTIALS` — "No Predict.fun wallet found for this user".'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderSimpleErrorResponse'
        '500':
          description: '`INTERNAL_ERROR` — the registered executor or credential set has an unexpected
            type.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderSimpleErrorResponse'
        '503':
          description: '`NOT_CONFIGURED` — Predict.fun (or its credential provider) is not configured on
            this deployment.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderSimpleErrorResponse'
  /polymarket/check-resolution:
    post:
      operationId: checkPolymarketResolution
      summary: Check whether a Polymarket market has resolved
      description: |-
        Queries Polymarket's Gamma API for a market's resolution state and reports whether positions on it are redeemable. Accepts either a `0x…` condition id or a numeric Gamma market id.
        Note the path has no `/exchanges` prefix — it sits at the service root.
        **Auth.** Authentication only — this endpoint enforces **no scope** and no per-provider access check. It requires auth purely to prevent anonymous scraping of Polymarket data through Kairos.
      tags:
      - Onboarding
      x-kairos-auth: api-key
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderCheckResolutionRequest'
      responses:
        '200':
          description: Resolution state.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderCheckResolutionResponse'
        '401':
          $ref: '#/components/responses/ExecUnauthorized'
        '404':
          description: '`VALIDATION_MARKET_NOT_FOUND` — Gamma returned a non-success status, or the
            condition-id query matched no market.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '500':
          description: '`INTERNAL_ERROR` — could not build the lookup URL, or could not parse Gamma''s
            response.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '502':
          description: '`EXCHANGE_POLYMARKET_API_ERROR` — the Gamma request itself failed.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
  /exchanges/hyperliquid/withdraw/prepare:
    post:
      operationId: prepareHyperliquidWithdraw
      summary: Build the typed data for a Hyperliquid withdrawal (step 1 of 2)
      description: |-
        Builds the EIP-712 `withdraw3` payload for a USDC withdrawal from Hyperliquid to an Arbitrum address. **Kairos never signs a withdrawal** — your main wallet does, and this endpoint only hands you the bytes.
        **This call is pure.** It writes nothing, reserves nothing, and has no side effects. Never calling `POST /exchanges/hyperliquid/withdraw` afterwards has zero consequence.
        **`destination` defaults to `wallet_address`.** Omit it and you withdraw to yourself. If you set it, be certain: Kairos validates only the *amount*, never the destination address, and a signed withdrawal to a wrong address is irreversible.
        **No server-side binding between prepare and submit.** The submit endpoint accepts any well-formed `{amount, time, destination, signature}` — correctness rests entirely on Hyperliquid re-verifying your signature. Prepare is a convenience, not a control.
        **Auth & scope.** Requires `trade:execute` for `hyperliquid`, wallet ownership, and the mutation gate. Not rate limited.
      tags:
      - Hyperliquid
      x-kairos-auth: api-key
      x-kairos-scope: trade:execute
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderHyperliquidWithdrawPrepareRequest'
      responses:
        '200':
          description: Typed data to sign, plus the `time` nonce to echo back.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderHyperliquidWithdrawPrepareResponse'
        '400':
          description: '`ValidationInvalidOrder` — invalid user id, or `Invalid withdraw amount: …`
            (not a plain positive decimal).'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '401':
          $ref: '#/components/responses/ExecUnauthorized'
        '403':
          description: '"This API key lacks the trade:execute scope", API-key access to `hyperliquid`
            disabled, or "Wallet not found or does not belong to you".'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '500':
          description: '`DatabaseError`, or `InternalError` — "Hyperliquid signing misconfigured".'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '503':
          description: API-key access to `hyperliquid` could not be checked (fails closed).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
  /exchanges/hyperliquid/withdraw:
    post:
      operationId: submitHyperliquidWithdraw
      summary: Submit a signed Hyperliquid withdrawal (step 2 of 2)
      description: |-
        Forwards your main wallet's signed `withdraw3` action to Hyperliquid. Kairos verifies the signature parses and then passes it through; the venue re-verifies it covers `destination`, `amount` and `time`, so a field altered after signing is rejected upstream.
        **Irreversible.** A withdrawal that Hyperliquid accepts cannot be recalled. Kairos does not validate the destination address.
        **No Kairos-side idempotency.** There is no in-flight tracking and no dedupe: calling this twice with the same `time` sends the same signed action twice, and the ONLY protection against a double withdrawal is Hyperliquid rejecting the replayed nonce. Do not build a blind retry loop on this endpoint.
        **A `400` conflates two very different outcomes.** `Hyperliquid rejected withdrawal: …` is returned both for a genuine venue rejection AND for an HTTP/network failure reaching the venue — the underlying error type collapses them. On that error you cannot tell whether the withdrawal landed; query Hyperliquid directly before retrying.
        Hyperliquid's own withdrawal minimum and flat fee are enforced venue-side and surface through that same `400`.
        **Auth & scope.** Requires `trade:execute` for `hyperliquid`, wallet ownership, and the mutation gate. Not rate limited.
      tags:
      - Hyperliquid
      x-kairos-auth: api-key
      x-kairos-scope: trade:execute
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderHyperliquidWithdrawRequest'
      responses:
        '200':
          description: The withdrawal was submitted to Hyperliquid. Submission only — not a settlement
            confirmation.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderHyperliquidActionResponse'
        '400':
          description: '`ValidationInvalidOrder` — invalid user id, `Invalid withdraw amount: …`, or
            `Invalid withdrawal signature: …`. **Also** `InternalError` with message `Hyperliquid rejected
            withdrawal: …`, which covers venue rejection and network failure indistinguishably.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '401':
          $ref: '#/components/responses/ExecUnauthorized'
        '403':
          description: Missing scope, provider access disabled, or wallet not owned by the caller.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '500':
          description: '`DatabaseError`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '503':
          description: API-key access to `hyperliquid` could not be checked (fails closed).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
  /exchanges/hyperliquid/transfer/prepare:
    post:
      operationId: prepareHyperliquidTransfer
      summary: Build the typed data for a Hyperliquid spot↔perp transfer (step 1 of 2)
      description: |-
        Builds the EIP-712 `usdClassTransfer` payload for moving USDC between your own Hyperliquid spot and perp balances. **Funds never leave your account** — there is no destination and nothing to mis-address.
        Your MAIN wallet must sign: Hyperliquid forbids agent wallets from class transfers, so a Kairos-held agent key cannot do this for you.
        **This call is pure** — same properties as the withdraw prepare: no writes, no reservation, no binding to the submit step.
        **Auth & scope.** Requires `trade:execute` for `hyperliquid`, wallet ownership, and the mutation gate. Not rate limited.
      tags:
      - Hyperliquid
      x-kairos-auth: api-key
      x-kairos-scope: trade:execute
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderHyperliquidTransferPrepareRequest'
      responses:
        '200':
          description: Typed data to sign, plus the `time` nonce to echo back.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderHyperliquidTransferPrepareResponse'
        '400':
          description: '`ValidationInvalidOrder` — invalid user id, or `Invalid withdraw amount: …`
            (the withdraw-worded message is reused for transfers).'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '401':
          $ref: '#/components/responses/ExecUnauthorized'
        '403':
          description: Missing scope, provider access disabled, or wallet not owned by the caller.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '500':
          description: '`DatabaseError`, or `InternalError` — "Hyperliquid signing misconfigured".'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '503':
          description: API-key access to `hyperliquid` could not be checked (fails closed).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
  /exchanges/hyperliquid/transfer:
    post:
      operationId: submitHyperliquidTransfer
      summary: Submit a signed Hyperliquid spot↔perp transfer (step 2 of 2)
      description: |-
        Forwards your signed `usdClassTransfer` to Hyperliquid. Because the move is internal to your own account, the blast radius is far smaller than a withdrawal — but the same mechanics apply: no Kairos-side idempotency, `time` doubles as the nonce, and replay protection is Hyperliquid's alone.
        The `400 Hyperliquid rejected transfer: …` conflates venue rejection with network failure, exactly as on withdraw.
        **Auth & scope.** Requires `trade:execute` for `hyperliquid`, wallet ownership, and the mutation gate. Not rate limited.
      tags:
      - Hyperliquid
      x-kairos-auth: api-key
      x-kairos-scope: trade:execute
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderHyperliquidTransferRequest'
      responses:
        '200':
          description: The transfer was submitted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderHyperliquidActionResponse'
        '400':
          description: '`ValidationInvalidOrder` — invalid user id, `Invalid withdraw amount: …`, or
            `Invalid transfer signature: …`. **Also** `InternalError` with message `Hyperliquid rejected
            transfer: …` (venue rejection or network failure, indistinguishable).'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '401':
          $ref: '#/components/responses/ExecUnauthorized'
        '403':
          description: Missing scope, provider access disabled, or wallet not owned by the caller.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '500':
          description: '`DatabaseError`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '503':
          description: API-key access to `hyperliquid` could not be checked (fails closed).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
  /exchanges/polymarket/deposit-wallet/onboard:
    post:
      operationId: onboardDepositWallet
      summary: Deploy the caller's Polymarket deposit wallet and set its trading approvals
      description: |-
        Creates the user's **deposit wallet** — a per-user ERC-1967 proxy on Polygon, deployed through Polymarket's relayer at a deterministic CREATE2 address derived from the owner EOA. Two relayer operations run: the deployment, then a signed batch setting the CTF/collateral approvals the venue needs.
        **Idempotent on the deployment leg.** Re-submitting for an already-deployed owner is treated as a no-op success and comes back with `deploy_tx_id: null`.
        **It does not move user funds** — it deploys a contract and grants approvals. The approval batch is signed server-side under the user's delegated key; the batch deadline window is one hour, and each relayer transaction is waited on for up to 120 s.
        A successful response means the submitted approval batch has confirmed. `batch_tx_id` is empty when no batch was submitted.
        **Auth & scope.** Requires `trade:execute` for `polymarket`, identity ownership, and the mutation gate.
      tags:
      - Deposit wallet
      x-kairos-auth: api-key
      x-kairos-scope: trade:execute
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderDepositWalletIdentityRequest'
      responses:
        '200':
          description: Wallet deployed (or already present) and approvals submitted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderDepositWalletOnboardResponse'
        '400':
          description: '`ValidationInvalidOrder` — `user_id` is not a valid UUID.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '401':
          $ref: '#/components/responses/ExecUnauthorized'
        '403':
          description: '`AuthInsufficientScope` — "This API key lacks the trade:execute scope"; or
            `AuthIdentityMismatch` — a `user_id` / `turnkey_org_id` in the body that is not the
            authenticated account (omit them to avoid this). Note that the provider-access check on this
            endpoint returns a GENERIC body (see the description of `OrderErrorResponse`).'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '500':
          description: '`DatabaseError` — ownership could not be verified; or `InternalError` — the
            relayer client is unavailable or the onboarding itself failed. The underlying relayer reason
            is logged server-side and NOT returned.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '503':
          description: API-key access to `polymarket` could not be checked (fails closed).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
  /exchanges/polymarket/imported/relay-info:
    post:
      operationId: getImportedRelayInfo
      summary: Read the relayer nonce (and GSN relay) for an imported Polymarket wallet
      description: |-
        The imported-wallet analogue of the deposit-wallet nonce endpoint: returns the relayer nonce and, for a legacy ProxyWallet (`sig_type: proxy`), the GSN relay address needed to construct the digest the browser signs. For a Gnosis Safe (`sig_type: safe`) `relay` is always `null`.
        Fetched server-side purely because Polymarket's relayer is CORS-blocked from browsers. The response is public information; the ownership check is belt-and-braces, not a secrecy boundary.
        > **Error shape.** Most 4xx/5xx here have an **empty `text/plain` body**; the `502` cases carry a plain-text reason (`relay-payload fetch failed: …` / `nonce fetch failed: …`). Scope and identity failures return the structured envelope.
        **Auth & scope.** Requires `trade:execute` for `polymarket` and identity + wallet ownership. No mutation gate, so a bare session JWT reaches it.
      tags:
      - Deposit wallet
      x-kairos-auth: api-key
      x-kairos-scope: trade:execute
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderImportedRelayInfoRequest'
      responses:
        '200':
          description: Relayer nonce, plus the GSN relay for `proxy`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderImportedRelayInfoResponse'
        '400':
          description: Malformed or unparseable `owner_address`. Empty body (`text/plain`). An identity
            mismatch instead returns the structured `AuthIdentityMismatch` body.
        '401':
          $ref: '#/components/responses/ExecUnauthorized'
        '403':
          description: Missing `trade:execute` scope, API-key access to `polymarket` disabled, or an
            identity mismatch. Structured body.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '500':
          description: The relayer client is unavailable. Empty body (`text/plain`).
        '502':
          description: 'Plain-text `relay-payload fetch failed: <reason>` (proxy) or `nonce fetch failed:
            <reason>` (safe).'
        '503':
          description: API-key access to `polymarket` could not be checked (fails closed). Structured
            body.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
  /v2/onchain/intent:
    post:
      operationId: createOnchainIntent
      summary: Build pinned unsigned on-chain transactions for self-signing (step 1 of 2)
      description: |-
        The on-chain counterpart to `POST /v2/orders/intent`, for the operations an order cannot do off-chain: one-time approvals, redemptions of resolved positions, CTF split/merge, and unwrapping wrapped collateral. Kairos builds the exact calldata, pins it under a single-use `payload_id` (300 s TTL), and hands you the EIP-155 signing digest for each transaction. **You** sign with your own EOA and **you** pay the gas; Kairos only broadcasts what you signed via `POST /v2/onchain/submit`.
        **Polygon / Polymarket only.** `chain_id` is pinned to `137`; there is no `provider` field.
        **No RPC at build time.** You supply `nonce` and `gas_price_wei`; the server does not fetch a nonce, quote gas, or preflight your balance. A stale nonce or an under-priced transaction surfaces at broadcast, not here.
        **Multi-leg.** `op=approvals` returns several transactions; sign each `signing_digest_hex` raw (no EIP-191 prefix) and submit the signatures in the same order.
        **Auth & scope.** Requires `trade:execute` for `polymarket` AND the `User.executionFastlaneEnabled` allowlist — the same gate as the order lane. `owner_address` must be a wallet registered to the authenticated caller. There is no service-token, admission, circuit-breaker or rate-limit gate on this route.
      tags:
      - Orders
      x-kairos-auth: api-key
      x-kairos-scope: trade:execute
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderOnchainIntentRequest'
      responses:
        '200':
          description: Pinned unsigned transactions plus the digests to sign.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderOnchainIntentResponse'
        '400':
          description: |-
            Exact `error` strings: `invalid owner_address`; `owner_address is not a registered wallet for
            this user`; `invalid gas_price_wei (expected a wei decimal string)`; `gas_price_wei must be > 0`;
            `condition_id is required for op=redeem`; `condition_id is required for op=<op>`;
            `amount (wei) is required for op=<op>`; `invalid amount (expected a wei decimal string)`;
            `amount must be > 0`; `wcol_amount (wei) is required for op=unwrap_wcol`;
            `invalid wcol_amount (expected a wei decimal string)`; `wcol_amount must be > 0`;
            `unsupported op: <other>`; or the calldata builder's own message.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderSimpleErrorResponse'
        '401':
          $ref: '#/components/responses/ExecUnauthorized'
        '403':
          description: '`API key missing trade:execute scope`; `API-key access to polymarket is disabled`;
            `external-signing execution is not enabled for this account`; `external-signing authorization
            check failed`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderSimpleErrorResponse'
        '500':
          description: '`internal server error` — failed to serialize or store the intent, or a `payload_id`
            collision.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderSimpleErrorResponse'
        '503':
          description: '`API-key access to polymarket is unavailable` — the API-key access cache read failed.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderSimpleErrorResponse'
  /v2/onchain/submit:
    post:
      operationId: submitOnchainSigned
      summary: Broadcast your externally-signed on-chain transactions (step 2 of 2)
      description: |-
        Hand back the `payload_id` and one signature per transaction, in the order `POST /v2/onchain/intent` returned them. The server claims the stored intent atomically (single-use Redis `GETDEL` — a replay gets `400`), re-verifies each signature recovers to the declared owner AND to a wallet registered to you, reassembles the signed transactions, and broadcasts them **in nonce order, waiting up to 90 s for each to confirm** before sending the next.
        **Partial failure is possible and is reported.** If leg `i` fails to broadcast or revert-checks, the response is a `502` whose message lists the transaction hashes already mined (`already broadcast: [...]`). Earlier legs stay on chain; re-intent only the remainder.
        **Auth & scope.** Identical gate to `POST /v2/onchain/intent`.
      tags:
      - Orders
      x-kairos-auth: api-key
      x-kairos-scope: trade:execute
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderOnchainSubmitRequest'
      responses:
        '200':
          description: All transactions broadcast and confirmed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderOnchainSubmitResponse'
        '400':
          description: |-
            Exact `error` strings: `payload_id not found, expired, or already claimed`;
            `payload_id does not belong to authenticated user`; `payload_id expired`;
            `expected N signature(s), got M`; `tx <i>: invalid signature hex`;
            `tx <i>: <verification error>` (e.g. wrong length, or the signature does not recover to the
            declared owner); `tx <i>: signer is not a registered wallet`; `tx <i>: <reassembly error>`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderSimpleErrorResponse'
        '401':
          $ref: '#/components/responses/ExecUnauthorized'
        '403':
          description: Same set as `POST /v2/onchain/intent`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderSimpleErrorResponse'
        '500':
          description: '`internal server error` — Redis claim failure, the stored intent no longer
            deserializes, or the Polygon RPC is not configured on this deployment.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderSimpleErrorResponse'
        '502':
          description: '`tx <i> failed to broadcast: <error> (already broadcast: [...])` — earlier
            transactions in the batch may already be mined.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderSimpleErrorResponse'
        '503':
          description: '`API-key access to polymarket is unavailable`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderSimpleErrorResponse'
  /exchanges/{exchange_id}/ctf/split:
    post:
      operationId: ctfSplit
      summary: Split collateral into a complete outcome-token set
      tags:
      - CTF
      x-kairos-auth: api-key
      x-kairos-scope: trade:execute
      description: Converts `amount` of collateral into `amount` YES **and** `amount` NO tokens on-chain,
        without going through the order book. Does not open a market position — it mints equal tokens
        on every outcome; both legs are recorded as synthetic BUY fills at price 0.5 so history and PnL
        stay consistent. NegRisk (multi-outcome) markets are detected from the conditionId and routed
        through the NegRiskAdapter automatically. Transactions are custodially signed; deposit-wallet users
        execute gaslessly via the relayer. Identity (`user_id`/`turnkey_org_id`/`wallet_address`) is resolved
        server-side from the authenticated caller — body fields are optional and must match when present.
        Because this signs under the user's delegated key it is additionally gated on the installed Turnkey
        policy version (`403 AUTH_POLICY_OUTDATED` if stale) and, like the other mutation endpoints, on the
        service-token/CSRF/API-key check that the API-key triple satisfies automatically. Failures return
        the structured `OrderErrorResponse` envelope — match on `error_details.code`.
      parameters:
      - &id004
        name: exchange_id
        in: path
        required: true
        description: Venue. Split/merge require an on-chain CTF (polymarket, predictfun). Kalshi settles
          off-exchange — redeem only.
        schema:
          type: string
          enum:
          - polymarket
          - predictfun
          - kalshi
          - opinion
        example: polymarket
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderCtfSplitMergeRequest'
            example:
              condition_id: '0x1234abcd'
              market_id: '570362'
              amount: 100
      responses:
        '200':
          description: Split executed on-chain.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderCtfSplitResponse'
        '400': &id005
          description: 'Invalid `condition_id`, non-positive `amount`, an exchange that doesn''t support
            the action (e.g. split/merge on Kalshi → `EXCHANGE_UNSUPPORTED`), or insufficient collateral
            (split) / outcome tokens (merge) → `FUNDS_INSUFFICIENT_USDC` or `FUNDS_INSUFFICIENT_BALANCE`.
            An insufficient-funds reject on this service is a `400`; there is no `402` anywhere in this
            API.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '401': &id006
          $ref: '#/components/responses/ExecUnauthorized'
        '403': &id008
          description: Identity fields conflict with the authenticated caller (`AUTH_IDENTITY_MISMATCH`),
            wallet not owned by the caller, missing `trade:execute` scope, API-key access to this provider
            disabled, or installed Turnkey policies behind the version this path requires
            (`AUTH_POLICY_OUTDATED`, action `update_policies` — split, merge and redeem all enforce it).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '404': &id010
          description: '`VALIDATION_MARKET_NOT_FOUND` — the condition/market could not be resolved.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '500': &id011
          description: '`INTERNAL_ERROR` / `DATABASE_ERROR` / `SIGNATURE_ERROR` — bookkeeping or custodial
            signing failed.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '502': &id009
          description: On-chain submission or the RPC call failed (`NETWORK_ERROR` / `EXCHANGE_ERROR`)
            — generally safe to retry.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '504':
          description: '`NETWORK_TIMEOUT` — the on-chain call timed out; the transaction may still land.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
  /exchanges/{exchange_id}/ctf/merge:
    post:
      operationId: ctfMerge
      summary: Merge a complete outcome-token set back into collateral
      tags:
      - CTF
      x-kairos-auth: api-key
      x-kairos-scope: trade:execute
      description: Burns `amount` YES **and** `amount` NO tokens and returns `amount` collateral — recover
        capital from matched inventory without waiting for resolution. Requires holding at least `amount`
        of every outcome token. Recorded as two synthetic SELL fills at price 0.5. NegRisk routing, custodial
        signing, the Turnkey policy-version gate, the service-token/CSRF/API-key mutation gate, and
        server-side identity resolution behave exactly as on split.
      parameters:
      - *id004
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderCtfSplitMergeRequest'
            example:
              condition_id: '0x1234abcd'
              market_id: '570362'
              amount: 290
      responses:
        '200':
          description: Merge executed on-chain.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderCtfMergeResponse'
        '400': *id005
        '401': *id006
        '403': *id008
        '404': *id010
        '500': *id011
        '502': *id009
  /exchanges/{exchange_id}/redeem:
    post:
      operationId: ctfRedeem
      summary: Redeem winning outcome tokens after resolution
      tags:
      - CTF
      x-kairos-auth: api-key
      x-kairos-scope: trade:execute
      description: 'After a market resolves on-chain, converts the winning outcome tokens into collateral.
        Only the winning side is needed (losing shares are worthless). NegRisk redeems unwrap wrapped
        collateral back to the base asset automatically. `db_update_failed: true` in the response means
        the on-chain redeem SUCCEEDED but the position-state bookkeeping write failed — funds are safe;
        retry the call to fix the bookkeeping. Failures return the structured `OrderErrorResponse`
        envelope — match on `error_details.code`.'
      parameters:
      - *id004
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderCtfRedeemRequest'
            example:
              condition_id: '0x1234abcd'
              market_id: '570362'
      responses:
        '200':
          description: Redeem executed on-chain.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderCtfRedeemResponse'
        '400': *id005
        '401': *id006
        '403': *id008
        '404': *id010
        '409':
          description: 'Venue shows resolved but on-chain payouts are still all zero
            (`VALIDATION_MARKET_NOT_SETTLED_ON_CHAIN`). Transient — wait for the dispute window and
            retry; primary action is `retry`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '500':
          description: Unexpected fault, or a sponsored redeem that needs manual review
            (`INTERNAL_ERROR`; may include `contact_support`). Do not blindly retry wedged sponsored
            requests.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '502': *id009
