Data flow
Follow one order from keypress to on-chain leaf, and one price tick from a CEX to the order book.
An order, end to end
Browser builds canonical bytes
lib/auth/canonical.ts serialises the order into a byte-exact blob. Not JSON — a fixed field
order with fixed encodings, because the engine reproduces the same bytes to verify.
Session key signs
@noble/curves Ed25519 over those bytes. The wallet is not involved — it signed once, at session
open.
BFF proxies
POST /v1/orders with X-User / X-Session-Id / X-Session-Sig forwarded verbatim to the
engine. The BFF adds nothing and strips nothing.
Engine validates then matches
Grid rules first (invalid_size, invalid_price, below_min_notional), then the book. Rejections
come back as HTTP 200 with ok: false — a 200 does not mean the order rested.
WAL entry written
The engine appends a signed entry: engine.order_placed, then engine.order_filled per leg.
Two consumers read the WAL
The indexer persists it into Postgres. The BFF receives the same events over SSE and re-derives domain frames for live clients. Same source, two latencies.
Merkle commitment
Fills accumulate into a per-user tree. On cadence, the engine commits a UserBatch root per user
and a global BatchManifest on-chain.
A rejection is a 200. POST /v1/orders returning HTTP 200 with {"ok": false, "code": "0x0C"} is the normal rejection path. Code that only checks res.ok will report a rejected
order as filled.
Snapshot + delta
Never poll for what the stream already pushes. Every live surface boots from REST and then follows the WebSocket.
| Snapshot (REST) | Delta (WS frame) |
|---|---|
GET /v1/users/:party/positions | position.update |
GET /v1/users/:party/balance | balance.update |
GET /v1/orderbook?depth= | orderbook.snapshot |
| — (live only) | trade |
| — (live only) | candle.update / candle.close |
There is no REST trade history feed for the tape: trade frames are live-only, deduplicated to
the taker leg, and flip legs are coalesced into a single print.
A price tick
Binance ─┐
OKX ─┼──► oracle (WS) ──► median/robust index ──► Redis ──► engine
Bybit ─┘ │
├─► mark price
├─► funding rate
└─► liquidation checksThe oracle publishes an index price per asset to Redis. The engine consumes it for mark price, funding, and liquidation — it never reads a CEX directly.
The engine names oracle assets BTC / ETH / SOL, while the token registry keys the Canton
wrappers CBTC / CETH / CSOL. One alias layer (resolveTokenKey) reconciles them — see
Markets & tokens.
Positions are epochs, not rows
A positionId (pos-<32 hex>) identifies an epoch, not a running position. A flip closes one
epoch and opens another, so the stream emits two frames:
{"kind":"position.update","payload":{"positionId":"pos-1f0e…","size":"0"}}
{"kind":"position.update","payload":{"positionId":"pos-9b3a…","size":"-0.25","realizedPnl":"0"}}realizedPnl restarts at zero under the new id. UI code that keys positions by market instead
of by positionId will silently merge two epochs and show wrong realised PnL.