associatedWallet requirement, and rate-limit buckets.
Request body
forbidNonWhitelisted). Any extra field
returns 400 invalid_request, and error.details.fields[] names the
offending property, for example:
{ "field": "foo", "constraint": "whitelistValidation", "message": "property foo should not exist" }.
Send feeRateBps with the value you signed. If the market fee changed
after you fetched it, the order is rejected with status: REJECTED,
code: stale_fee_quote and a message giving the current rate. Re-fetch
feeTakerBps, then re-sign. If you omit the field, the signature is still
checked against the current fee, and a stale fee comes back as
bad_signature.
The platform never receives the struct you signed. It rebuilds the
on-chain Order from your request fields and recovers your signature
against that rebuild. So the struct you sign must always contain:
signer= the wallet you authenticate as (your key’sassociatedWallet, or theX-User-Walletfor amulti_walletkey)maker= that wallet’s vault (VaultFactory.vaultOf(signer)), never the EOAsignatureType=1(VAULT).0(EOA) is deprecated. The platform and the exchange contract both reject it, even whenmaker == signer.salt= the bodynonce,expiration=expiry,feeRateBps= the market’sfeeTakerBps,taker=0x0,tokenId= the token foroutcome,side=0BUY /1SELLmakerAmount/takerAmountderived fromprice/quantity(CEIL on BUY notional, FLOOR on SELL)- the EIP-712 domain of the binary or the neg-risk exchange, according to
the market’s
negRiskEligible
maker field is only an ownership check. It must be your wallet
or its vault (anything else is rejected with maker_mismatch, an HTTP
401 on /place), and the platform verifies against your vault in both
cases. If the signature does not recover to your wallet, or a non-zero
expiry has already passed, the order comes back as status: REJECTED,
code: bad_signature, not as an HTTP 400. Its details show the exact
struct the platform verified and the address it expected to recover; see
Response shape.
Validation rules
Except where a row shows an HTTP status, every rule in this table is checked as a business rule. A failure comes back as a REJECTED envelope, not an HTTP error: an HTTP201 body
{ status: "REJECTED", code, message } on /place, or an entry with
success: false on /place-batch. Branch on status / code, not on
the HTTP status. (Shape errors, such as a non-numeric price or an
unknown field, fail request validation first with 400 invalid_request.)
Worked example — BUY 10 YES at 0.42
Response shape
code: bad_signature, details contains recovered (the address your
signature recovers to against the platform’s struct), expected_signer
(the acting wallet: your key’s associatedWallet, or the X-User-Wallet
on multi_wallet keys) and platform_canonical_struct (the exact Order
the platform verified). Compare it field by field with the struct you
signed. The field that differs is the cause. If recovered equals
expected_signer, the struct matched and your non-zero expiry has
already passed. See
bad_signature diagnostic hints.
The clientOrderId field is echoed verbatim when the request supplied
one — present on every shape (success, REJECTED, idempotent replay). Use
it to correlate the server-issued orderId to your own client-side handle
without a follow-up GET /api/orders/{id}. The same value is also carried
on the order_placed and order_cancelled frames of the user_orders
channel on /ws/user, so partners that drive their state machine off the WS feed get
the correlation key on both surfaces.
Batch placement
Submit up to 10 orders in one request withPOST /api/orders/place-batch.
Each entry is the exact same body as single placement; the
batch is processed sequentially in array order with independent
per-order outcomes — one rejected order never aborts the rest, and there
is no all-or-nothing atomicity. Request validation is the exception: if
any entry is malformed (for example a non-numeric price, an unknown
field or a missing signature), or the array holds 0 or more than 10
entries, the whole request fails with 400 invalid_request and no entry
is processed.
PlaceOrderResp (identical to single place) plus a success flag:
Partner trade limits accumulate across the batch. For users who
trade through a partner sign-in (
partner_sso), the partner trade cap
is charged against the running total of the entries accepted earlier in
the same batch, so a batch can never admit more exposure than the same
orders sent one at a time.Finding an order after a timeout
When a place request times out or answers 5xx you don’t have theorderId, but you hold two keys for the order: the clientOrderId you
sent, and its orderHash, the EIP-712 digest of the struct you signed
(see EIP-712 signing).
Look the order up by either one:
- Pass exactly one of the two; neither or both answers
400 invalid_request. - The answer is the same
Orderobject asGET /api/orders/{id}. clientOrderIdis matched exactly (case-sensitive, not trimmed). URL-encode it: a raw+in a query string reads as a space.orderHashis accepted in any case.- An unknown key and an order of another wallet both answer
404 order_not_found. - The server-generated
srv-*key comes back (indetails.clientOrderId) only on a singlePOST /api/orders/place503. Aplace-batch5xx carries no keys, so set your ownclientOrderIdon every batch entry, or look the order up byorderHash. - Orders that ended with no trade are archived after 30 days and are then
not found here or by
GET /api/orders/{id}: a404for an old order says nothing about it.
/place has its own limit of 20 req/sec/wallet. For the full table,
see Rate limits. KYC deposit-tier gating applies, same
as single place.
Post-only orders
SetpostOnly: true to guarantee an order only ever rests on the book
and never matches on entry. It’s the maker-side guarantee market makers need
to avoid paying taker fees. (No order of yours ever trades against your
own quotes, post-only or not: a limit order that would reach them is
rejected, and a market order cancels the ones it comes to; see
Self-trade prevention.)
gtc time-in-force (optionally with
expiry). Omit it (or send false) for today’s behavior.
Self-trade prevention
A wallet never trades with itself. When a new order would meet one of your own resting orders, the platform stops that match. What it stops depends on the new order’stype:
For a
limit order, “reaches” means a BUY priced at or above your
lowest resting SELL, or a SELL priced at or below your highest resting
BUY; an equal price counts. Quoting both sides with a spread between
them is always allowed.
- YES and NO share one book. On a binary market, buying NO at
pis the same as selling YES at1 − p, so your orders on both outcomes are checked together. A BUY YES and a BUY NO reach each other when their prices add up to1.00or more; a SELL YES and a SELL NO when they add up to1.00or less. With your BUY NO at0.40resting, a BUY YES at0.60isself_cross_blockedand a BUY YES at0.55is not. Different markets never block each other, including the questions of one multi-outcome event. - Checked first. The check runs before the post-only and
fill-or-kill checks. A
postOnlyorder that reaches your own order getsself_cross_blocked, notpost_only_would_cross, and aLIMIT + FOKorder comes backREJECTEDwithself_cross_blocked, notCANCELLED. ALIMIT + IOCorder that reaches your own order takes nothing, not even better-priced orders of other participants. - Batches.
place-batchentries are processed in array order. Alimitentry that reaches an order an earlier entry of the same batch left resting is rejected withself_cross_blocked, and the earlier entry is unaffected. Amarketentry cancels that order instead, as described above. - Pre-market. While a market is
PRE_MARKET, an order that would cross any resting order, yours or anyone else’s, is rejected earlier withcode: pre_market_crossing_order(see Market status). An entry that crosses an order placed by an earlier entry of the sameplace-batchis not caught by that check and comes back asself_cross_blockedinstead. - Just-expired orders. One of your orders whose
expiryhas just passed can still count for a few seconds, until the platform removes it from the book. - Per wallet. Orders from two different wallets are never blocked
or cancelled against each other, even when they belong to the same
multi_walletAPI key: they match like any two participants. Trading between wallets you control is still subject to market surveillance.
- Rejected
limitorder.POST /api/orders/placeanswers HTTP201withstatus: "REJECTED",code: "self_cross_blocked"and a realorderId(onplace-batch, that entry withsuccess: false). Only this response names the code. On theuser_ordersWebSocket channel you get the usualorder_placedframe, thenorder_cancelledwithreason: "matcher_rejected"(notorder_rejected), andGET /api/orders/{id}readsREJECTED. - Resting order cancelled by your
marketorder. Anorder_cancelledframe onuser_orderswithreason: "self_trade_prevention". It can arrive withoutclientOrderId,originalQuantityandremainingQty, so key it byorderId.GET /api/orders/{id}readsCANCELLED, and the order’s locked funds are released as with any cancel. Themarketorder’s own response doesn’t list the orders it cancelled.
Sell orders
Same shape;side: 'sell'. No USDC is locked for a SELL order.
Instead, the order reserves its quantity of the outcome token
(ERC-1155), which must already sit in the vault, not the EOA.
Tokens reserved by one open SELL cannot back another; cancelling or
expiry releases the reservation for the unfilled remainder. Position
resolution is keyed by maker (the vault), which is why on-chain splits (USDC → YES + NO) must be initiated by
vault.splitPosition(...) rather than by the EOA directly. See
Vaults for the split flow.
Next
Time-in-force
GTC vs IOC vs FOK behaviour.
Cancelling orders
Single-order and cancel-all flows.