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.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 fromsignature_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. Setmakerto your Safe andsignerto your wallet address.3— sign the SafeMessage, not the order hash, using a signing key you added. Set bothmakerandsignerto 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 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
APOLY_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), wherelegPriceis the order’s own side price — a NO order signs the NO’s own price, not an inverted YES probability.- The implied price
makerAmount / takerAmountmust equalpricewithin1e-4.
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 with422 unless:
- it is a limit order with a valid side and
price ∈ (0, 1); - the market is CTF-registered and
token_idmatches the side’s token; side == 0,taker == 0x0,fee_rate_bps == 0;expirationis 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,signerequals your member EOA and the signature ECDSA-recovers tosigner; - for type
3,signer == makerand 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’sexpiration 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.
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.
Related
- The Safe — make the Safe trade-ready before signing
- Redeeming winnings — claim payouts after a market resolves