WebSocket subscriptions (v1)
Both /rpc/v1/ws (the node) and /api/v1/ws
(the indexer) speak the same envelope. One socket carries any number of
channels; every server frame has a per-connection seq
(monotonic from 1) and the block height it belongs to, so a
client can tell exactly what it received and what it missed.
Client → server
{"op":"subscribe","channels":["blocks","account:<hex>","pair:BTC-KUSD","deposits:BTC:<hex>","book:BTC-KUSD?depth=20"],"from_height":120}
{"op":"unsubscribe","channels":["pair:BTC-KUSD"]}
{"op":"ping"}subscribe replaces the channel set.
from_height replays the event channels
(account:, pair:, deposits:) from
that height up to the tip before live delivery starts (at most the last
10,000 blocks on the node). A connection that never subscribes receives
blocks (node) or blocks and txs
(indexer), which is what the earlier firehose sent.
Channels
| channel | where | what arrives |
|---|---|---|
blocks |
node, indexer | block: the node sends the whole block
(timestamp, state_hash, receipts,
events, books top of book per pair); the
indexer sends its block summary |
txs |
indexer | tx: one transaction summary per transaction |
account:<hex> |
node, indexer | node: event with data.kind = receipt for
every receipt signed by or touching the address (transfers to it, trades
it is in, deposits credited to it, ...) and
data.kind = event for end-of-block events about it;
indexer: tx summaries signed by it |
pair:<PAIR> |
node | event per order event on that pair
(OrderAccepted, OrderFilled,
OrderCancelled, OrderRejected, with
tx_id) and book_top after each block
(best_bid, best_ask,
last_price) |
deposits:<CHAIN>:<hex> |
node | event per deposit-side event for that owner on that
chain: DepositAddressAssigned,
DepositCredited, WithdrawalQueued |
book:<PAIR>?depth=N |
node | book_snapshot on subscribe (bids,
asks as [price, size], best first, at most N
levels a side), then book_delta per block with only the
changed levels: changes of
[side, price, size], size "0" = level
gone |
Server → client
| type | fields | meaning |
|---|---|---|
subscribed / unsubscribed |
channels |
the channel set now in force |
block |
see above | one finalized (node) or indexed (indexer) block |
event |
channel, data |
one receipt or event on an event channel |
book_top, book_snapshot,
book_delta |
pair, ... |
order-book streams |
gap |
missed, from_height,
to_height, resync |
this connection fell behind and frames were dropped; nothing between
from_height and to_height was delivered.
resync names the REST routes to read them from; book
channels get a fresh book_snapshot right after |
heartbeat |
every 15 s, with the current height |
|
pong |
answer to ping |
|
error |
message |
a bad op or channel; the subscription set is unchanged |
A client that sees seq jump has missed frames too (a
proxy or its own buffer dropped them) and should treat it as a
gap. The SDK's KeelSocket does both: it raises
a synthetic gap on a sequence hole, and on a dropped
connection it reconnects with backoff and re-subscribes with
from_height set to the last height it saw.
Example
import { KeelSocket } from "@keelchain/sdk";
const ws = new KeelSocket("wss://testnet.keelchain.com/rpc/v1/ws");
ws.on("event", (f) => console.log(f.channel, f.data));
ws.on("book_delta", (f) => applyDelta(f.pair, f.changes));
ws.on("gap", async (f) => {
for (let h = f.from_height; h <= f.to_height; h++) await reconcile(h); // GET /v1/blocks/{h}/receipts
});
await ws.subscribe([`account:${me}`, "book:BTC-KUSD?depth=10"]);