Skip to main content

State machine

States

StateMeaningBalance effect
PENDINGOrder signed and submitted; balance locked; matcher call in flightavailable → locked for BUY notional
OPENResting in orderbooklocked remains
PARTIALSome quantity filled, remainder restinglocked reduced pro-rata at settlement
FILLEDTerminal — fully executedresidual locked refunded
CANCELLEDTerminal — user-initiated cancellocked → available (refund)
CANCELLED_BY_RESOLVETerminal — market was resolved; all open orders cleanedlocked → available (refund)
REJECTEDTerminal — rejected before matchinglocked → available (refund)
EXPIREDTerminal — expiration passedlocked → available (refund)
SETTLEMENT_FAILEDTerminal — every fill on this order reverted on-chain (MatchFailed for every leg)locked → available (refund). Rare; usually requires the maker book to have moved between off-chain match and on-chain submit.
PARTIAL is normalised to OPEN on GET /api/orders/open for client simplicity (the filledQty field carries the residual information). GET /api/orders/history returns the raw DB state, so a partially-filled-then-cancelled order surfaces there as PARTIAL (active leg) followed by CANCELLED (terminal). Code against both shapes when reconciling open vs historical views.

Trade-level settlement (after match)

Order state flips to FILLED / PARTIAL as soon as the matching engine returns fills — but the underlying trade rows then go through a separate on-chain settlement pipeline:
Trade stateMeaningLocked balance
matchedRecorded right after the matchRemains locked
settlement_pendingbatchMatchOrders tx broadcast on-chainRemains locked
settledOrderFilled event confirmed on-chainlocked → 0 (released)
settlement_failedTx reverted fully, or MatchFailed event for this specific leg (other legs in the batch can still settle)locked → available (refunded)
The most common cause of settlement_failed is MatchFailed(reason = 0xc56873ba) = OrderExpired() — a resting maker order whose expiration slipped between off-chain match and on-chain submit. Sign new orders with expiration = 0 to avoid this; see CTFExchange — MatchFailed reason codes. This is why PredictStreet uses a strict settlement model: partner-visible balances reflect only on-chain-confirmed state. Expect a few seconds (p99 < 30s on testnet) between order FILLED and the locked notional clearing. If settlement_pending exceeds 5 minutes, contact support — most likely chain congestion or a known stuck-trade case.

How partners observe settlement

The REST trade endpoints (/api/orders/{id}/fills, /api/me/trades) do not currently surface tx_hash or settlement state on the trade row — they always show the matched fill, not the on-chain status. The partner-recommended channels:
  • WebSocket /ws/useruser_activity channel — push semantics:
    • trade_matched fires at off-chain match (DB-only, NOT yet on-chain). Use this for orderbook UI updates only.
    • trade_fill fires when chain-watcher indexes the on-chain OrderFilled event. This is the balance-commit signal — available / locked totals from /api/me/balances reflect the trade after this event. Carries txHash, blockNumber, fee, vault addresses, matched amounts.
    • trade_failed fires on MatchFailed — locked balance is refunded automatically; treat the trade as never settled.
  • REST balance pollingavailable + locked totals derive from on-chain-confirmed state, so polling /api/me/balances is a backstop if WS is unavailable.
See WebSocket — User events for the full payload shapes of each push.

What actually happens on POST /api/orders/place

  1. AuthnX-Api-Key is verified (partner lookup → SHA-256 hash compare → lifecycle + IP-allowlist checks). The canonical wallet is your key’s associatedWallet. orders:write scope is enforced before the handler runs — see API keys.
  2. Pre-verify enrichment — the platform resolves:
    • your vault via VaultFactory.vaultOf(signer)
    • the on-chain tokenId for the requested outcome
    • makerAmount / takerAmount from price × quantity per side
  3. EIP-712 verify — the full on-chain Order struct (11 fields) is reconstructed; the recovered signer must match the authenticated wallet, and vaultOf(signer) must equal maker. The body’s maker is then overridden to the resolved vault before storage.
  4. Market guard — the market’s status must be OPEN; otherwise 409 market_not_open.
  5. Balance / position lock — atomic:
    • BUY: available -= notional, locked += notional
    • SELL: position lookup is keyed by vault address (not EOA); fails with insufficient_position if the vault doesn’t hold enough of the outcome token
    • the order row is recorded as PENDING
  6. Match — sent to the matching engine. Returns {status, filledQty, trades[]} synchronously.
  7. Trade persistence — trade rows + fee ledger written, balance deltas applied for both sides of every trade — atomically.
  8. Settlement enqueue — matched trades are broadcast on-chain via batchMatchOrders on CTFExchange. See the trade-state table above.
  9. Response — shape documented below.
If step 6 fails after retries, step 5 is compensated: order status → REJECTED, locked balance refunded, call returns 503 exchange_unavailable.

Response shape

interface PlaceOrderResp {
  orderId: string;
  status:
    | 'PENDING'
    | 'OPEN'
    | 'FILLED'
    | 'CANCELLED'
    | 'REJECTED'
    | 'EXPIRED'
    | 'SETTLEMENT_FAILED';
  filledQty: string;
  remainingQty: string;
  trades: Trade[];
  code?: string;
  message?: string;
}

interface Trade {
  id: string;
  orderId: string;
  userWallet: string;        // EOA — the side's authenticated wallet
  marketId: string;
  price: string;             // decimal USDC
  quantity: string;          // outcome-token qty (decimal)
  fee: string;               // taker fee charged on this fill (decimal USDC)
  side: 'buy' | 'sell';      // from this wallet's perspective
  createdAt: string;
}
The synchronous trades[] carries the matched fill view — settlement state and txHash are not on this row. To observe on-chain confirmation, subscribe to the user_activity WebSocket channel and watch for trade_fill events (they include txHash, blockNumber, and the matched amounts) — see How partners observe settlement above.

Idempotency

Include clientOrderId in the request body to make the call idempotent. If a network retry hits the server with the same (wallet, clientOrderId), the original response is replayed.

Cancellation

DELETE /api/orders/{orderId}
X-Api-Key: ps_live_<keyId>_<secret>  # integrators — needs `orders:write`
  • Matcher is called first (best-effort — NOT_FOUND is treated as idempotent success).
  • PG is the source of truth: on successful matcher.cancelOrder, we flip OPEN | PARTIAL → CANCELLED and refund residual locked balance in a single transaction.

Next

EIP-712 signing

Struct reference + signing examples.

Placing orders

Full request schema + validation.

Fees

Quadratic taker-fee curve explained.

Error codes

Complete code reference.