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 parameter | Values | Notes |
|---|---|---|
marketId | numeric market id | required (alias market_id) |
channel | book (default), trades, bbo | alias type; l2book/l2 and trade are accepted |
instance | a (default), b | which line; both carry the same sequence space |
fromSeq | last applied sequence | resume; aliases from_seq, resume_from, cursor |
depth | levels per side in the snapshot | default is the canonical projection depth |
limit | trades in the snapshot | trades channel |
encoding / format | json (default), binary | binary is the venue's internal cold codec, decodable with the Rust SDK; use JSON unless you run that SDK |
integrityPayload | true | attach 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 }
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 + 1per connection; - after applying a
book_delta, your top of book must change exactly whenchecksumchanges — a divergence means a corrupted local book: resnapshot; - treat
bookChecksumonsnapshot_endas the identity of the book you now hold; a laterfullReplacedelta 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.