interface PlaceOrderReq { marketId: string; // app-level symbol, e.g. "NC26-BIN-83479265" side: 'buy' | 'sell'; // lowercase outcome: string; // "0" = YES (first), "1" = NO (second) price: string; // decimal USDC, e.g. "0.42" quantity: string; // decimal outcome qty, e.g. "2" nonce: string; // decimal-string of the salt used in the EIP-712 struct expiry: number; // unix seconds (= Order.expiration); use 0 for no on-chain expiry maker: string; // VAULT address (= VaultFactory.vaultOf(signer)) signature: string; // 0x-prefixed EIP-712 signature clientOrderId?: string; // idempotency key (optional, scoped per associatedWallet) type?: 'limit' | 'market'; // default 'limit' timeInForce?: 'gtc' | 'ioc' | 'fok'; // default 'gtc' for limit, 'ioc' for market}
The request body is whitelisted (forbidNonWhitelisted); any extra
field returns 400 validation_failed.maker is the vault address. The signing EOA is recovered from
signature and must satisfy vaultFactory.vaultOf(signer) == maker.
The backend re-runs that check before storing the order, so passing the
EOA in maker instead of the vault returns 400 bad_signature.
0 < p < 1 (strict — p >= 1 is rejected to block the fee = k × P × (1−P) sign flip at P > 1). Tick size is 0.01 — at most 2 decimal places (integer cents, 99 distinct levels). 0.505 returns invalid_amounts with “price tick size is 0.01”.
400 invalid_amounts
quantity
> 0, at most 6 decimal places (USDC scale).
400 invalid_amounts
type + timeInForce
MARKET orders require ioc or fok; MARKET+GTC rejected
400 invalid_tif
signature
EIP-712 recover (full on-chain Order struct) must equal an EOA whose vault == maker
400 bad_signature
expiry
0 (no expiry, recommended) or unix-seconds in the future. There is no minimum TTL.
400 expired
outcome
"0" or "1". The platform maps this to the on-chain tokenId for the market.
interface PlaceOrderResp { orderId: string; // server-issued UUID, '' when status=REJECTED status: 'PENDING' | 'OPEN' | 'FILLED' | 'CANCELLED' | 'REJECTED' | 'EXPIRED' | 'SETTLEMENT_FAILED'; filledQty: string; // cumulative fill at response time (decimal) remainingQty: string; // quantity - filledQty trades: TradeFill[]; // synchronous fills produced by IOC/FOK or aggressive LIMIT code?: string; // present when status=REJECTED (insufficient_funds, …) message?: string; // human-readable explanation; pairs with code clientOrderId?: string; // echoed from request when supplied}
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 events.order_placed and events.order_cancelled frames over
/ws/user, so partners that drive their state machine off the WS feed get
the correlation key on both surfaces.
Submit up to 10 orders in one request with POST /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.
POST /api/orders/place-batchX-Api-Key: ps_live_<keyId>_<secret> # needs `orders:write` scopeContent-Type: application/json{ "orders": [ /* 1–10 PlaceOrderReq objects, same shape as single place */ ] }
The response is an array aligned to the request by index. Each item is a
PlaceOrderResp (identical to single place) plus a success flag:
Per-wallet limits accumulate across the batch. KYC deposit-tier,
responsible-gambling, and partner trade caps are charged against the
running total of accepted entries, so a batch can never admit more
exposure than the same orders sent one at a time.
Business reject vs infrastructure failure. A rejected order (bad
signature, insufficient funds, …) is a per-entry success: false with
HTTP 200 — the rest of the batch still processes. An infrastructure
failure (matcher / verifier / DB unavailable) fails the whole request
with a 5xx. On a 5xx, retry the whole batch: every forwarded entry carries
a clientOrderId (the server mints one when you omit it), so the replay
returns the already-committed result via idempotency rather than placing
duplicates.
Rate limited at 5 req/sec/wallet (up to 50 orders/sec). KYC deposit-tier
gating applies, same as single place.
Set postOnly: 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 or accidentally crossing their own quotes.
Only valid for LIMIT orders with gtc time-in-force (optionally with
expiry). Omit it (or send false) for today’s behavior.
Two rejection modes.
post_only_invalid_order_type (HTTP 400) — postOnly was sent with
a MARKET order or an ioc / fok time-in-force. The combination is
invalid; nothing is placed.
post_only_would_cross — the order was marketable at entry: its
price meets or crosses the best price on the opposite side of the book
(at-touch equality counts as crossing). It returns
{ "status": "REJECTED", "code": "post_only_would_cross" } with no
fills — reprice inside the spread and resubmit.
Same shape; side: 'sell'. Balance is not locked for SELL orders —
the outcome token (ERC-1155) must already sit in the vault, not
the EOA. 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.