State machine
States
| State | Meaning | Balance effect |
|---|---|---|
PENDING | Order signed and submitted; balance locked; matcher call in flight | available → locked for BUY notional |
OPEN | Resting in orderbook | locked remains |
PARTIAL | Some quantity filled, remainder resting | locked reduced pro-rata at settlement |
FILLED | Terminal — fully executed | residual locked refunded |
CANCELLED | Terminal — user-initiated cancel | locked → available (refund) |
CANCELLED_BY_RESOLVE | Terminal — market was resolved; all open orders cleaned | locked → available (refund) |
REJECTED | Terminal — rejected before matching | locked → available (refund) |
EXPIRED | Terminal — expiration passed | locked → available (refund) |
SETTLEMENT_FAILED | Terminal — 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. |
Trade-level settlement (after match)
Order state flips toFILLED / PARTIAL as soon as the matching
engine returns fills — but the underlying trade rows then go
through a separate on-chain settlement pipeline:
| Trade state | Meaning | Locked balance |
|---|---|---|
matched | Recorded right after the match | Remains locked |
settlement_pending | batchMatchOrders tx broadcast on-chain | Remains locked |
settled | OrderFilled event confirmed on-chain | locked → 0 (released) |
settlement_failed | Tx reverted fully, or MatchFailed event for this specific leg (other legs in the batch can still settle) | locked → available (refunded) |
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/user→user_activitychannel — push semantics:trade_matchedfires at off-chain match (DB-only, NOT yet on-chain). Use this for orderbook UI updates only.trade_fillfires when chain-watcher indexes the on-chainOrderFilledevent. This is the balance-commit signal —available/lockedtotals from/api/me/balancesreflect the trade after this event. CarriestxHash,blockNumber,fee, vault addresses, matched amounts.trade_failedfires onMatchFailed— locked balance is refunded automatically; treat the trade as never settled.
- REST balance polling —
available+lockedtotals derive from on-chain-confirmed state, so polling/api/me/balancesis a backstop if WS is unavailable.
What actually happens on POST /api/orders/place
- Authn —
X-Api-Keyis verified (partner lookup → SHA-256 hash compare → lifecycle + IP-allowlist checks). The canonical wallet is your key’sassociatedWallet.orders:writescope is enforced before the handler runs — see API keys. - Pre-verify enrichment — the platform resolves:
- your vault via
VaultFactory.vaultOf(signer) - the on-chain
tokenIdfor the requestedoutcome makerAmount/takerAmountfromprice × quantityper side
- your vault via
- EIP-712 verify — the full on-chain
Orderstruct (11 fields) is reconstructed; the recovered signer must match the authenticated wallet, andvaultOf(signer)must equalmaker. The body’smakeris then overridden to the resolved vault before storage. - Market guard — the market’s
statusmust beOPEN; otherwise409 market_not_open. - Balance / position lock — atomic:
- BUY:
available -= notional,locked += notional - SELL: position lookup is keyed by vault address (not EOA);
fails with
insufficient_positionif the vault doesn’t hold enough of the outcome token - the order row is recorded as
PENDING
- BUY:
- Match — sent to the matching engine. Returns
{status, filledQty, trades[]}synchronously. - Trade persistence — trade rows + fee ledger written, balance deltas applied for both sides of every trade — atomically.
- Settlement enqueue — matched trades are broadcast on-chain via
batchMatchOrdersonCTFExchange. See the trade-state table above. - Response — shape documented below.
REJECTED, locked balance refunded, call returns 503
exchange_unavailable.
Response shape
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
IncludeclientOrderId 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
- Matcher is called first (best-effort —
NOT_FOUNDis treated as idempotent success). - PG is the source of truth: on successful
matcher.cancelOrder, we flipOPEN | PARTIAL → CANCELLEDand 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.