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.
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.
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./place has its own limit of 50 req/sec/wallet. For the full table,
see Rate limits. KYC deposit-tier gating applies, same
as single place.
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.
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.