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.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")
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:- Authenticate to your assigned regional execution node with a Kairos JWT or the API-key triple. See Authentication.
- (Optional) Price the trade with
GET /orders/fee-quoteor the WebSocket Fee Quote (RFQ) stream. POST /v2/orders/intentwith the order terms plusmarket_idandoutcome. Kairos builds the canonical EIP-712 payload and returns apayload_id, aneip712_digest_hex, and the fullunsigned_payload. No order is placed yet.- Sign locally. An EOA order signs
eip712_digest_hex(or the equivalent typed data). A Polymarket deposit-wallet order signserc7739_digest_hexwith its owner EOA and submits the resulting ERC-7739 envelope; current beacon wallets may submit the plain owner signature for server-side wrapping. POST /v2/orders/submitwithpayload_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.- Track. The submit response carries
order_idand the venue’s immediatestatus. A resting order’s later fills stream over your/wsconnection — see Fills. - Cancel or reprice as needed. Cancels need no signature; a reprice is cancel + a fresh intent. See Cancels.
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.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: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. CallPOST /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
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 (alreadyPreferkeccak256("\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-byter ‖ s ‖ v.- Wrong:
personal_sign(...)/eth_signwith the"\x19Ethereum Signed Message"prefix — produces the wrong recovery. You’ll getsignature recovered 0x… does not match owner.- Wrong:
signTypedData(...)on the digest itself — that hashes it a second time.
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:
Smart-contract wallets (Poly1271)
You are not limited to a plain EOA. Setintent.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.
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
/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).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:
saltandtimestamp_msare required on the intent, so the node rebuilds the exact digest you signed.market_idandoutcomeare required on the payload, same as REST.
provider field on the frame.
submit_signed_order
Success and error frames use the same shapes as the REST lane:
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.{"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:- Each intent is single-use (deleted on submit), so a retry needs a new intent — new
salt/timestamp→ new digest → new signature. - Repricing a resting quote is the standard cancel + new-signed-order loop.
- 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
- Overview — access, account model, and what you bring vs. what Kairos does.
- Regional Execution Nodes — where to connect, WebSocket-over-REST, and reading live positions.
- API Reference — full schemas for
/v2/orders/intent,/v2/orders/submit, and the cancel/fee-quote endpoints referenced above. - WebSocket › Order Execution and REST › Orders — the general execution surface.

