Skip to main content
A non-custodial order is a standard limit order authorized by an EIP-712 signature. You build the typed Order, sign it with your EOA, and post it to Calibri, which validates the signature and places the order on the book. The resting order locks your locked_safe pool.
Orders are placed at POST /api/v2/atlas/account/orders so the signature can be validated and attached, and read back at GET /api/v2/pythia/orders. There is no unsigned placement path — every order on Calibri carries your signature.

1. Read the market’s CTF config

GET /api/v2/atlas/account/wallet/markets/:id Returns your funding config plus the market’s condition_id, yes_token_id, and no_token_id. Returns 404 if the market is custodial-only (not CTF-registered). The config gives you every value needed to build and sign the order: exchange_address, chain_id, domain_name, proxy_address (your Safe / the maker), owner_address (what owns the Safe), signature_type, session_keys_enabled, safe_status, and the token ids.
The YES/NO token ids are also published on the public market payload as ctf_yes_token_id / ctf_no_token_id, so a client that already holds a market can build an order without a per-market authenticated call.
Check safe_status before signing. setup_required means approvals are still outstanding: an order placed then will rest on the book and then fail to settle, which is worse than being rejected. Only ready is safe to trade on.
domain_name is served by the API — it is the EIP-712 domain name of Calibri’s exchange. You must sign with the value the API returns, not a copied constant, or the signature will not recover.

2. The EIP-712 domain

3. The Order struct

EIP-712 field names are camelCase:
YES vs NO is carried only by tokenId. side is always 0 (BUY) — both legs are buys that mint a complete set.

Signature types

Your signature type determines how you sign an order. Read it from signature_type on GET /api/v2/atlas/account/wallet and sign the way your type requires — an order signed the wrong way is rejected.
  • 2 — sign the order with your wallet key. Set maker to your Safe and signer to your wallet address.
  • 3 — sign the SafeMessage, not the order hash, using a signing key you added. Set both maker and signer to your Safe. In the browser your passkey does this for you. Your type follows from what owns your Safe: a wallet you hold the key for (2) or a passkey (3) — so it is fixed by how you set your wallet up. The names below are the exchange contract’s enum and mean the same as the numbers:
For POLY_1271 the exchange does no address derivation — it requires signer == maker, that maker has code, and that isValidSignatureNow(maker, hash, sig) passes. That is exactly what allows a contract to own the Safe. A passkey order sent as type 2 cannot work: that path ECDSA-recovers a key which does not exist.
For type 2 the signature is your EOA signing the typed data above. Type 3 has no EOA key by default — a passkey owns the Safe. Adding your own wallet as a signing key gives it one, and that is what makes type 3 scriptable: the type does not change, only the signature. See Your wallet options.

Type 3: three signature shapes

A POLY_1271 order carries one of three signature shapes, and the server tells them apart by length alone — never by a client-supplied flag: The third is the only one a server-side client can produce: a passkey signs only in the browser that registered it, and session keys are generated non-extractable. Add your own wallet as a signing key and your code signs as an owner of the Safe. It signs the SafeMessage, not the order hash. Safe re-wraps the hash before checking it, so signing orderHash directly recovers to a different address and the Safe rejects it — reported as 422 signed_order signature was not accepted by the maker wallet, on a signature that verifies perfectly if you check it yourself. The full recipe, computable without a node, is under type 3 in Authentication. The grant id is read out of the envelope’s first word, so a client cannot name a live grant while signing with an expired key. A session-signed order is additionally checked against its grant’s expiry at intake; an expired or revoked grant is rejected with 422 Your trading key has expired or been revoked — approve with your passkey. See Trading without a prompt per order.

4. Amount and unit rules

CTF_UNIT_SCALE = 1_000_000. USDC and CTF outcome tokens are both 6-decimal — 1 contract = 1 outcome token = 1e6 base units.
  • takerAmount == round(volume * 1e6) exactly.
  • makerAmount == round(legPrice * volume * 1e6), where legPrice is the order’s own side price — a NO order signs the NO’s own price, not an inverted YES probability.
  • The implied price makerAmount / takerAmount must equal price within 1e-4.
Example — buy 100 YES contracts at 0.60:

5. Sign and build the wire object

Sign the typed data with your EOA key (wallet.signTypedData(domain, { Order: [...] }, order)), then assemble the signed_order wire object (snake_case). Every big-integer field is a decimal string — including fee_rate_bps; side and signature_type are numbers:

6. Place the order

POST /api/v2/atlas/account/orders Places a non-custodial (signed) order. Calibri validates the signed_order, places it as a self-custody order (custody = "self"), and attaches the signature to the order record. Returns the order object, 201. Request body:
ioc (immediate-or-cancel) is for a market-style order: a limit at a price ceiling, signed with a short expiration. With ioc: true, whatever the book can’t fill when the order arrives is refunded instead of resting at the ceiling. It is recorded on the order, so a wallet’s market orders can be told apart from its limit orders (both are ord_type: "limit"). Ignored if post_only is set. If attaching the signature fails, the order is cancelled so no unsignable orphan is left behind.

Server-side validation

Calibri fully validates the signed order before anything is placed, rejecting with 422 unless:
  • it is a limit order with a valid side and price ∈ (0, 1);
  • the market is CTF-registered and token_id matches the side’s token;
  • side == 0, taker == 0x0, fee_rate_bps == 0;
  • expiration is non-zero, unexpired, and within 365 days;
  • amounts match the volume and price (see the unit rules above);
  • the maker is a Safe registered to you and settlement-ready;
  • for type 2, signer equals your member EOA and the signature ECDSA-recovers to signer;
  • for type 3, signer == maker and the Safe itself verifies the signature (EIP-1271) — plus, for a session-key envelope, the grant is still within its expiry.

Expiry is enforced

The engine keeps each order’s expiration and never lets a signature lapse between a match and its on-chain settlement:
  • an order is never matched within 30 seconds of its expiration;
  • a resting order reaching that point is cancelled and refunded (its order update arrives as cancel);
  • an order that arrives with less than 30 seconds left is rejected with market.order.expired.
Sign resting limit orders with a long expiration (up to 365 days) and market-style orders with a short one — the app uses 120 seconds. A check that cannot be completed — blockchain service unreachable, Safe readiness unknown — fails closed: the order is rejected rather than placed unverified. Common errors: 422 signed_order with signature is required for non-custodial orders, 422 Member safe is not settlement-ready, 422 signed_order signature does not match the signer. Engine 4xx responses are passed through; upstream failures return 502.