Skip to main content

Market Feed (Snapshot + Delta)

/api/v1/feed/public is the market-data path built for quote engines: one market and one channel per connection, a full snapshot followed by sequenced deltas, a checksum on every frame, replay on resume, and two independent lines (a and b) that carry identical content from separate processes. The multiplexed /api/v1/ws/public socket is better for dashboards; this feed is better for anything that trades on it.

Written against runtime/feed_protocol.rs and handlers/public_ws_feed.rs.

Endpoints and query

wss://api.sentico-labs.xyz/api/v1/feed/public # instance a unless ?instance=b
wss://api.sentico-labs.xyz/api/v1/feed/public/a
wss://api.sentico-labs.xyz/api/v1/feed/public/b
Query parameterValuesNotes
marketIdnumeric market idrequired (alias market_id)
channelbook (default), trades, bboalias type; l2book/l2 and trade are accepted
instancea (default), bwhich line; both carry the same sequence space
fromSeqlast applied sequenceresume; aliases from_seq, resume_from, cursor
depthlevels per side in the snapshotdefault is the canonical projection depth
limittrades in the snapshottrades channel
encoding / formatjson (default), binarybinary is the venue's internal cold codec, decodable with the Rust SDK; use JSON unless you run that SDK
integrityPayloadtrueattach the canonical payload the checksum was computed over (debugging)

Frame sequence

schema → session → [status] → snapshot_start → snapshot_level × n → snapshot_end → (replay) → live frames …

Every frame is a JSON object with "type" in snake_case and camelCase fields.

schema and session

{ "type": "session", "stream": "market_feed.book.a", "streamVersion": 1, "schemaId": "market_feed_json", "schemaVersion": 1,
"schemaEncoding": "json", "instance": "a", "marketId": 3, "channel": "book",
"resumeFromSeq": null, "latestSeq": 184273, "replayRetentionMs": 60000, "maxReplayEvents": 10000,
"heartbeatMs": 20000, "snapshotDepth": 50, "alternateEndpoint": "/api/v1/feed/public/b",
"integrityMode": "top_of_book+book+payload_blake3" }

alternateEndpoint names the other line. latestSeq is the newest sequence the server holds; compare it with your fromSeq to know how much replay to expect.

status

Sent after session and whenever placement readiness changes. Connection state, not market data: no seq, never replayed, must not enter gap detection.

{ "type": "status", "sessionState": "open", "placementReady": true, "cancelOnlyAvailable": true, "reason": null, "tsMs": 1788558400818 }

Stop placing when placementReady is false; cancelOnlyAvailable says whether cancels still work. GET /api/v1/status is the REST fallback.

Snapshot

{ "type": "snapshot_start", "marketId": 3, "channel": "book", "snapshotSeq": 184270, "checksum": "…", "bookChecksum": "…", "payloadChecksum": "…", "depth": 50, "levelCount": 84 }
{ "type": "snapshot_level", "marketId": 3, "channel": "book", "snapshotSeq": 184270, "book": "spot", "side": "bid", "price": "2513937400", "qty": "40" }
{ "type": "snapshot_end", "marketId": 3, "channel": "book", "snapshotSeq": 184270, "checksum": "…", "bookChecksum": "…", "payloadChecksum": "…" }

book is spot for spot markets and yes / no for outcome markets; side is bid / ask; price and qty are micro-unit strings. Levels arrive best-first. On the bbo channel the snapshot carries at most one level per book and side; on trades it carries no levels.

Live frames

book channel:

{ "type": "book_delta", "marketId": 3, "seq": 184274, "prevSeq": 184273, "stateVersion": 545718,
"checksum": "…", "bookChecksum": null, "payloadChecksum": "…", "fullReplace": false,
"updates": [ { "book": "spot", "side": "ask", "price": "2514000000", "qty": "0" } ] }

A qty of "0" removes the level. fullReplace: true means the updates describe the entire book; drop your local book first.

trades channel:

{ "type": "trade", "marketId": 3, "seq": 184275, "prevSeq": 184274, "payloadChecksum": "…",
"trades": [ { "tradeId": "496923000001", "batchId": 496923, "book": "SPOT", "price": "2513937400", "qty": "40", "ts": 1788507827923 } ] }

bbo channel:

{ "type": "bbo", "marketId": 3, "seq": 184276, "prevSeq": 184275, "stateVersion": 545718, "checksum": "…", "payloadChecksum": "…",
"spotBid": { "price": "2513937400", "qty": "40" }, "spotAsk": { "price": "2514000000", "qty": "12" },
"yesBid": null, "yesAsk": null, "noBid": null, "noAsk": null }
Spot BBO on older builds

Builds before 2026-09-01 sent bbo frames for spot markets with empty spotBid / spotAsk. Verify the fields are populated on your deployment before quoting off this channel; the book channel is unaffected.

heartbeat

{ "type": "heartbeat", "marketId": 3, "channel": "book", "latestSeq": 184276, "tsMs": 1788558420818 }

Sent every heartbeatMs. When latestSeq equals your last applied sequence, the server has nothing pending for you — a cheap "you are current" proof. Miss three heartbeats and treat the line as dead.

gap_fill

{ "type": "gap_fill", "marketId": 3, "channel": "book", "requestedAfterSeq": 100, "oldestAvailableSeq": 174000, "newestAvailableSeq": 184276, "replayRetentionMs": 60000, "reason": "replay_window_exceeded" }

Your fromSeq is older than the retention window (or more than maxReplayEvents behind). On the book and bbo channels a fresh snapshot block follows on the same connection; on trades the stream simply continues at the new sequence and you backfill history from GET /api/v1/public/markets/{marketId}/trades?sinceId=….

Integrity

The server validates every frame before it leaves (payloadChecksum over the canonical payload, bookChecksum over a full book, checksum over the top of book) and drops frames that fail. On the client:

  • verify seq == prevSeq + 1 per connection;
  • after applying a book_delta, your top of book must change exactly when checksum changes — a divergence means a corrupted local book: resnapshot;
  • treat bookChecksum on snapshot_end as the identity of the book you now hold; a later fullReplace delta carries the new one.

Two lines

Lines a and b are served by separate processes and share the sequence space. A latency-sensitive client opens both, applies whichever frame for a given seq arrives first, and drops the duplicate. If one line stalls, the other keeps the book current without a resubscribe; the session frame's alternateEndpoint names the partner line.

Resume

Reconnect with fromSeq set to the last sequence you applied. The server replays up to maxReplayEvents retained frames (retention replayRetentionMs) and continues live; if the range is gone you get gap_fill and, on book channels, a new snapshot. Persist the cursor per market and channel.