Titandocs

WebSocket

One socket per client, four in-process projections, and the exact-match filter that bites everyone once.

/v1/streamone connection per client. The BFF holds a single SSE connection to the engine (/v1/admin/events/stream, bearer ENGINE_ADMIN_TOKEN) and fans it out after filtering.

Four in-process projections recompose the raw firehose into domain frames: positions, balance, trades, orderbook snapshots, and OHLC candles.

Query params

All optional, CSV or repeated:

kinds=position.update,trade,candle.update
party=Alice::1220…,Bob::1220…
markets=BTC-USDCX
intervals=1m,5m          # narrows candle.* frames only

markets= is compared exactly. A wrong dialect (BTC/USDCx) matches nothing — and an unmatched filter subscribes you to every market rather than none. The symptom is a suspiciously busy tape, not an error.

Bootstrapping

REST for the snapshot, WS for the deltas:

Snapshot (REST)Frame (WS)
GET /v1/users/:party/positionsposition.update
GET /v1/users/:party/balancebalance.update
GET /v1/orderbook?depth=orderbook.snapshot
— (direct WS)trade
— (live only)candle.update / candle.close

Frame envelope

{
  "kind": "trade",
  "ts": "2026-05-27T13:42:01.123Z",
  "seq": 12345,
  "market": "BTC-USDCX",
  "payload": { "...": "kind-specific" }
}

Domain frames

KindDerived fromFilterable by
position.updateengine.order_filled per leg, scoped to userPartyparty, markets
balance.updateengine.order_filled per legparty
tradeengine.order_filled, taker leg only, flip legs coalescedmarkets
orderbook.snapshotPoll of engine.orderbook(depth) every WS_ORDERBOOK_POLL_MS, hash-skipped if unchangedmarkets
candle.updateIn-memory OHLCV bucket, updated per taker fillmarkets, intervals
candle.closeEmitted once when the next bucket opensmarkets, intervals

position.update

{
  "kind": "position.update",
  "seq": 12345,
  "ts": "2026-05-27T13:42:01.123Z",
  "userParty": "Alice::1220…",
  "market": "BTC-USDCX",
  "payload": {
    "positionId": "pos-1f0e9d8c7b6a5f4e3d2c1b0a99887766",
    "size": "0.5000000000",
    "entryPrice": "65010.0000000000",
    "realizedPnl": "0.0000000000",
    "asOfSeq": 12345
  }
}

positionId is an epoch identity. A flip closes one epoch and opens another, so you receive two frames: size: "0" on the old id, then a reopen under a fresh id with realizedPnl restarting at zero. Keying positions by market instead of by positionId silently merges the two and reports wrong realised PnL.

trade (public — no userParty)

{
  "kind": "trade",
  "seq": 12345,
  "market": "BTC-USDCX",
  "payload": {
    "side": "buy",
    "size": "0.5000000000",
    "price": "65010.0000000000",
    "takerOrderId": "o1",
    "makerOrderId": "o0"
  }
}

orderbook.snapshot

{
  "kind": "orderbook.snapshot",
  "market": "BTC-USDCX",
  "payload": {
    "depth": 50,
    "bids": [["65000.0000000000", "1.2500000000"]],
    "asks": [["65010.0000000000", "0.5000000000"]]
  }
}

Snapshots, not deltas — and hash-skipped when unchanged, so a quiet book costs nothing.

candle.update / candle.close

{
  "kind": "candle.update",
  "market": "BTC-USDCX",
  "interval": "1m",
  "payload": {
    "openTs": "2026-05-27T13:42:00.000Z",
    "closeTs": "2026-05-27T13:43:00.000Z",
    "open": "65010.0000000000",
    "high": "65020.0000000000",
    "low": "65005.0000000000",
    "close": "65015.0000000000",
    "volume": "1.2500000000",
    "trades": 4
  }
}

candle.close fires once per bucket, when the next one opens — that is the signal to commit the bar to history.

Engine WAL passthroughs

Raw events forwarded unchanged, for tooling and debugging:

KindMeaning
engine.tick_startMatching cycle began
inbound.depositDeposit observed on-chain
inbound.withdrawal_requestWithdrawal requested
engine.approve_submitted / engine.reject_submittedWithdrawal decision
session.authorizedNew session signed
inbound.orderOrder reached the engine
engine.order_placed / _filled / _cancelled / _rejectedRaw order lifecycle
engine.trigger_set / _cancelled / _firedTP/SL lifecycle
engine.attached_resolvedAttached tp/sl leg armed or abandoned (engine-internal, no leaf)
engine.user_batch_committedNew per-user merkle commitment
outbound.batch_committedNew global manifest

Client pattern

const ws = new WebSocket(
  streamWsUrl({
    kinds: ['orderbook.snapshot', 'trade', 'candle.update', 'candle.close'],
    markets: [market.id],   // exact engine symbol
    intervals: ['1m'],
    party,
  }),
);

ws.onmessage = (e) => {
  const frame = JSON.parse(e.data);
  switch (frame.kind) {
    case 'orderbook.snapshot': replaceBook(frame.payload); break;
    case 'trade':              appendTape(frame.payload); break;
    case 'candle.update':      updateBar(frame.payload); break;
    case 'candle.close':       commitBar(frame.payload); break;
  }
};

On reconnect, do not replay from zero. Take the last seq you processed and backfill with GET /v1/events?since=<seq> before resuming the socket.

On this page