Skip to main content
This page is the end-to-end integration guide for placing an order with your own key on the external execution lane. Read it when you are writing the code that signs and submits EOA or Polymarket deposit-wallet orders; it covers the intent → sign → submit flow, the one-round-trip WebSocket path, order types, fills, and cancels. The full path of an externally-signed order is build → sign → submit → track → fill: Kairos hands you an EIP-712 digest, you sign it locally with your own key, and you send the signature back — you never implement the venue’s order encoding yourself.

Read this before you write any signing code

Two mistakes account for almost every failed first integration on this lane. Both produce errors that look like a key problem and are not.

1. The EIP-712 domain keys are snake_case on our wire — your library needs camelCase

⚠️ unsigned_payload.domain comes back as { name, version, chain_id, verifying_contract }. viem and ethers require chainId and verifyingContract. Pass our object straight through and your library silently ignores the two keys it does not recognise, hashes a domain separator over { name, version } only, and produces a different digest. The submit then fails with signature recovered 0x… does not match owner 0x… and you will spend a day suspecting your private key.
Rename the two keys before you sign: name and version map straight across. The full conversion is in Option B below. This applies only to the signTypedData path — if you raw-sign eip712_digest_hex you never touch the domain object and cannot hit this.

2. market_id and outcome are required on every order — even though neither is signed

⚠️ Neither field enters the signed digest, and both are required anyway. They tag the persisted order and position rows so fills attribute to the right market, which makes them easy to leave out of a request built from the EIP-712 struct alone — and every such request is a 400.
  • Missing market_id → 400 market_id is required for external orders
  • Missing outcome → 400 outcome is required for external orders (e.g. "Yes" / "No")
Both are required on POST /v2/orders/intent and on the WebSocket submit_signed_order payload. The node also resolves your token_id against market_id and rejects a mismatch with 400 token_id does not belong to the supplied market_id.

The flow, start to finish

Mode A (assisted, two round-trips) is the path most integrations take. End to end:
  1. Authenticate to your assigned regional execution node with a Kairos JWT or the API-key triple. See Authentication.
  2. (Optional) Price the trade with GET /orders/fee-quote or the WebSocket Fee Quote (RFQ) stream.
  3. POST /v2/orders/intent with the order terms plus market_id and outcome. Kairos builds the canonical EIP-712 payload and returns a payload_id, an eip712_digest_hex, and the full unsigned_payload. No order is placed yet.
  4. Sign locally. An EOA order signs eip712_digest_hex (or the equivalent typed data). A Polymarket deposit-wallet order signs erc7739_digest_hex with its owner EOA and submits the resulting ERC-7739 envelope; current beacon wallets may submit the plain owner signature for server-side wrapping.
  5. POST /v2/orders/submit with payload_id + signature_hex, within the intent’s TTL (60 s by default). Kairos verifies EOA signatures directly. For deposit wallets it binds the wallet to your account, wraps eligible plain owner signatures, and forwards the envelope for the Polymarket CLOB’s ERC-1271 verification.
  6. Track. The submit response carries order_id and the venue’s immediate status. A resting order’s later fills stream over your /ws connection — see Fills.
  7. Cancel or reprice as needed. Cancels need no signature; a reprice is cancel + a fresh intent. See Cancels.
Mode B (self-built, one round-trip) collapses steps 3–5 into a single WebSocket frame — see Submit over WebSocket.

Intent states

The intent is claimed and deleted at the very top of /submit, before any verification. A submit that fails signature verification still burns the payload_id; the recovery is a new intent, never a retry of the same one.

Authentication

Base URL is your assigned regional execution node. All requests require authentication (JWT, SIWE session, scoped API key, or delegation); the lane is open to any authenticated account and needs no allow-list approval.
REST accepts the header above or a scoped API key issued at onboarding; on the WebSocket the JWT rides Sec-WebSocket-Protocol as authorization, Bearer_<base64url-no-pad(token)> instead. The /v2/* endpoints on this page take the JWT (or the API-key triple) on its own. The shared order endpoints — POST /orders and the three cancel routes — carry an extra gate that a bare Authorization: Bearer header does not satisfy: send the full API-key triple (X-Client-Id + X-Api-Key + X-Api-Secret) on those, which satisfies it automatically.

Partner attribution

If you resell or front Kairos access — a builder, a smart-account provider, a trading platform integrating on behalf of its users — tag every request with your partner id so Kairos can attribute the traffic to you:
On REST it is an optional request header; on the WebSocket one-round-trip path (submit_signed_order) send the same value as a top-level partner_id field in the payload, since WebSocket frames carry no headers. The id is validated once: it is trimmed, lower-cased, capped at 64 characters, and may contain only A-Z a-z 0-9 . _ - :. A malformed value is a 400 rather than a silent drop, so you never believe traffic was attributed when it was not.
This is attribution, not authorization. The partner id never grants access — you are still authenticated as usual and the lane is open to any authenticated account. It is recorded against the request (per-partner txs/RPS and latency) and stamped onto the persisted order so volume can be attributed after the fact. It is separate from your authenticated user_id: one partner fronts many end accounts.

Signing modes

Mode A — assisted, two round-trips (simplest). Kairos builds the canonical EIP-712 payload for you, so you never implement the venue’s exact order encoding — you just sign a 32-byte digest. Call POST /v2/orders/intent, sign the returned digest locally, then call POST /v2/orders/submit. Mode B — self-built, one round-trip (lowest latency, over WebSocket). If you implement the venue’s EIP-712 yourself, skip the intent step: build and sign the order locally, then send the whole thing in one WebSocket frame. Kairos verifies and forwards — zero server-side signing. See Submit over WebSocket.

Build the order intent

Builds the EIP-712 payload for an order and stashes it single-use under a payload_id. No order is placed yet — nothing reaches the venue until you submit a signature. Auth: Kairos JWT, SIWE session, scoped API key, or delegation. An API key needs the trade:execute scope, and the target provider must not be disabled for that key. Pricing a trade first? GET /orders/fee-quote (or the WebSocket Fee Quote (RFQ) stream) returns an executable price and fee breakdown before you build an intent — it works identically for external-signing accounts.

Request

Example

Response

Errors

Every error on this lane is the minimal shape {"error": "..."} — there is no code field and no structured error_details.

Sign the digest locally

Warning — sign the raw 32 bytes. This is the #1 integration mistake after the domain-key one. Kairos gives you the final EIP-712 digest (already keccak256("\x19\x01" ‖ domainSeparator ‖ hashStruct(order))). You must produce a raw secp256k1 ECDSA signature over those 32 bytes — not a re-hash, not an EIP-191-prefixed message.
  • Correct: secp256k1_sign(digest_bytes) → 65-byte r ‖ s ‖ v.
  • Wrong: personal_sign(...) / eth_sign with the "\x19Ethereum Signed Message" prefix — produces the wrong recovery. You’ll get signature recovered 0x… does not match owner.
  • Wrong: signTypedData(...) on the digest itself — that hashes it a second time.
Prefer signTypedData? You don’t have to sign the raw digest. The intent response also returns the full EIP-712 typed-data object as unsigned_payload ({ domain, primary_type, types, message }). Feed that to signTypedData and your library computes the identical digest — after you camelCase chain_id → chainId and verifying_contract → verifyingContract, as shown in Option B. Use whichever your stack supports; Kairos verifies the result the same way. Signature format: for signature_type 0, 65 bytes, 0x-prefixed hex (r (32) ‖ s (32) ‖ v (1)). v is the recovery id — Kairos accepts 27/28 and normalizes 0/1 (add 27 if your library returns 0/1). For signature_type 3 send either the owner EOA’s plain 65-byte signature over erc7739_digest_hex (Kairos builds the ERC-7739 envelope) or the wallet’s already ERC-7739-wrapped blob (see below). TypeScript — viem. Option A raw-hash signs the digest; Option B signs the typed data. Both produce an identical signature:
Python — eth_keys (raw-hash sign):

Smart-contract wallets (Poly1271)

You are not limited to a plain EOA. Set intent.signature_type to 3 (Poly1271) to trade from an EIP-1271 contract wallet — a MetaMask Smart Account, a Safe, or a Polymarket proxy wallet. This is the shape a smart-account provider’s users expect, and it needs no bundler or userOp: Kairos binds the wallet to your account, and the Polymarket CLOB calls the contract’s isValidSignature to verify. What changes versus an EOA order: The signing flow is otherwise identical to the EOA path: request an intent, sign erc7739_digest_hex with the owner EOA, and submit the plain signature — Kairos wraps it into the ERC-7739 envelope (sig ‖ appDomainSeparator ‖ contentsHash ‖ contentsType ‖ len) and forwards it for the CLOB’s isValidSignature check. Signing eip712_digest_hex instead will fail verification: that is the bare order digest the wallet validates, not what its owner signs. If you use a smart-account SDK that already produces the ERC-7739 envelope, post that blob as signature_hex — anything longer than 65 bytes is forwarded verbatim. The server-side wrap applies only when owner_address is a current beacon-derived Polymarket deposit wallet of one of your registered signing wallets — a pre-linked legacy wallet must submit the complete envelope; any other EIP-1271 account (an EIP-7702 delegated EOA, an ERC-7579 account) receives your 65-byte signature untouched and validates it under its own scheme.
Polymarket deposit-wallet orders are blocked upstream (Sept 2026). Polymarket’s CLOB binds an L2 API key only to the owner EOA, never the deposit wallet, and rejects the order with the order signer address has to be the address of the API KEY (Polymarket/py-clob-client-v2 #64/#70/#75/#77). The lane builds, wraps and verifies the order correctly; it settles once Polymarket fixes the binding.
For a Polymarket deposit wallet specifically, see Deposit wallet.
Supported signer shapes are 0 and 3 only. Polymarket’s Proxy (1) and Gnosis Safe (2) signature types are imported/custodial shapes and are rejected at intake. ERC-4337 userOp/bundler submission is deliberately out of scope — the lane verifies a signature you present, it does not sponsor or bundle a transaction. See GET /v2/providers for the per-venue signature_schemes and account_abstraction flags, and /external-execution/partner-auth for the full account model.

Submit the signature

Submits your signature. The node recomputes the order digest and verifies EOA signatures directly. For a Polymarket deposit wallet it binds the wallet to your account and forwards an ERC-7739 envelope for the CLOB’s ERC-1271 verification. Auth: same as /intent. The scope checks (and the allow-list, when the kill-switch is on) are re-run independently here, so an in-flight payload_id does not carry an earlier authorization forward.

Request

Example

Response

The order is now a first-class tracked row — fills, fees, positions, and cancel-by-id all work.

Errors

Gotcha: every submit consumes the intent. The payload_id is claimed and deleted before verification runs, so a submit that fails signature verification burns it just as thoroughly as a successful one. There is no “retry the same signature”. Recovery is always a new POST /v2/orders/intent.

Order types

time_in_force selects the order type. Both buy and sell are supported for all of them.
Case matters. Wire values for time_in_force are UPPER-case; side is lower-case (buy/sell).
A market order is a marketable taker order with FOK/FAK — price is still required (it’s the limit you’re willing to cross to). See Time in force for the general model.

Submit over WebSocket (one round-trip)

The lowest-latency path: keep one authenticated /ws socket open, build the EIP-712 order and sign it locally, then submit it in a single frame. Kairos verifies (ecrecover + the signer must be your registered wallet) and forwards — zero server-side signing, one round-trip. Two extra requirements apply here, both because there is no server-built intent to fall back on:
  • salt and timestamp_ms are required on the intent, so the node rebuilds the exact digest you signed.
  • market_id and outcome are required on the payload, same as REST.
This path is Polymarket-only — there is no provider field on the frame.

submit_signed_order

Success and error frames use the same shapes as the REST lane:
Keep one authenticated WebSocket connection open per node and both submit and listen on it. The socket stays warm (no per-order TLS setup) and fills arrive as events instead of on a poll tick. See Regional Execution Nodes › Execute over WebSocket, not REST.

Fills

POST /v2/orders/submit (and the WebSocket order_response) returns the venue’s immediate result — for a marketable order that crossed, that’s your fill confirmation; for a resting order it confirms the order is live. A resting maker order fills later. Those fills stream back over your /ws connection as flat, type-tagged events. The three you care about on this lane:
Gotcha: there is no order_update or fill frame type, and no ts_us field on any of these. Correlate on order_id and use your own receive timestamp.
A terminal failure arrives as {"type": "failed", …}; position changes as {"type": "position_updated", …}. Fills become tracked trades, update your position, and run post-trade fee capture. A stale-order-recovery sweep is the backstop, so fills are never lost — just occasionally a beat slower on REST. Listen on the WebSocket for the fastest fill detection.

Cancels — no signature required

Polymarket order cancellation is an L2 API-key operation (HMAC over the request) — it does not need your EOA signature. Cancels work on this lane exactly like the standard path: Cancel-by-internal-order_id works because external orders are persisted with a real order_id (returned from /submit). You can also cancel by the venue’s exchange_order_id, or use cancel-all. Full request/response schemas for all three are in the API Reference and REST › Orders.

Retries & repricing

Because you hold the key, a retry or reprice is a fresh signature:
  1. Each intent is single-use (deleted on submit), so a retry needs a new intent — new salt/timestamp → new digest → new signature.
  2. Repricing a resting quote is the standard cancel + new-signed-order loop.
  3. Over the WebSocket a re-sign is a single message round-trip on an already-open socket — cheap enough to reprice at quote frequency.

Predict.fun

Send "provider": "predictfun" on the intent. The request and response shapes are otherwise identical to Polymarket’s, and you sign the returned digest exactly the same way — only the EIP-712 domain differs (BSC, chain 56, and one of four CTFExchange addresses). You do not supply any venue-specific values. Kairos resolves the market’s isYieldBearing / isNegRisk pair, its feeRateBps and its price tick from authoritative market metadata and builds the domain and the signed struct from those. This is deliberate: feeRateBps is part of the signed order and the (isYieldBearing, isNegRisk) pair selects the verifying contract, so letting a client assert them would produce orders the venue rejects with opaque errors. intent.neg_risk is still required and is cross-checked against the market — a mismatch is a 400, not a silently corrected value. Current limits on this venue:

Provider coverage

Polymarket is the live reference venue for bring-your-own-signer orders (pure EOA, signatureType = 0, EIP-712 Order on the CTF Exchange V2; neg-risk markets use a different verifying contract, so a neg-risk signature can’t be replayed on a standard market) — the signing and API examples above are written against it. Coming soon: additional venues (including Hyperliquid, which uses an agent-wallet action-signing model) are in development and not yet reachable end-to-end on this lane; this page documents Polymarket only.

See also