Orders

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.

post/v2/orders/intent

Request

  • Base URL
    https://execution.kairos.trade
  • Auth
    API key in header X-Client-Id and API key in header X-Api-Key and API key in header X-Api-Secret

Request body

provider'polymarket' | 'predictfun'

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.

market_idstring nullable

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.

outcomestring nullable

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 request

{
  "provider": "polymarket",
  "intent": {
    "token_id": "71360012345678901234567890123456789012345678901234567890123456",
    "side": "buy",
    "price": "0.52",
    "size": "100",
    "time_in_force": "GTC",
    "owner_address": "0x0000000000000000000000000000000000dEaD"
  },
  "outcome": "Yes"
}

Response

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.

payload_idstring uuid required

Single-use handle for this stored intent. Pass back to POST /v2/orders/submit.

eip712_digest_hexstring required

Convenience copy of unsigned_payload.eip712_digest_hex — the digest to sign.

expires_at_usinteger required

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.

Example response

{
  "unsigned_payload": {
    "domain": {
      "name": "Polymarket CTF Exchange",
      "version": "2",
      "chain_id": 137,
      "verifying_contract": "0xE111180000d2663C0091e4f400237545B87B996B"
    },
    "primary_type": "Order",
    "eip712_digest_hex": "0x9a1c2b3d4e5f60718293a4b5c6d7e8f90112233445566778899aabbccddeeff0"
  }
}

Changes

No recorded changes to this operation across all 1 revision of this API.