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

# Build an unsigned EIP-712 order payload for self-custody signing (step 1 of 2)

> 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.



## OpenAPI

````yaml /openapi/execution.yaml post /v2/orders/intent
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: []
paths:
  /v2/orders/intent:
    post:
      tags:
        - Orders
      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.
      operationId: createOrderIntent
      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'
components:
  schemas:
    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'
    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`.
    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`.
    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
    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.
    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'
    OrderSide:
      type: string
      description: Order/intent side. Lower-case on the wire.
      enum:
        - buy
        - sell
      example: buy
    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
    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'
  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
  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.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.