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.

Both of these run behind a CDN on api.sentico-labs.xyz. If latency matters, Direct Market Data carries the same stream without that hop — measured at 1.80 ms against 8.55 ms from inside the venue's facility.

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 book and side in the snapshotdefault 20, clamped to 1..100
limittrades in the snapshottrades channel
encoding / formatbinary (default), jsonselect encoding=json explicitly for the JSON examples below; binary clients must decode the feed codec
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 …

With encoding=json, every frame is a JSON object with "type" in snake_case and camelCase fields. Omitting encoding selects binary WebSocket frames.

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": 20, "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": 20, "levelCount": 40 }
{ "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.