Skip to main content
Put your key in COINVERSA_API_KEY and run any of these as it stands (the Browser client takes the key in a placeholder instead, because a page cannot read the environment). There is no unauthenticated mode to fall back on — without the key the upgrade is refused before a socket exists, which every client below handles. Each client is a working subscriber, not a connect line: it reconnects with jittered backoff and resubscribes, resets the backoff once the connected frame lands, checks sequence ordering per stream, sends a heartbeat ping, and tells the three handshake refusals apart from a drop — never retrying an invalid key. They subscribe to two coins on one connection deliberately, because that is the case where gap detection keyed on channel alone quietly falls apart.

Check your key in one command

Before writing code, prove the handshake with a raw upgrade. A 101 means the key was accepted; a 401, 429 or 503 is the refusal it names.
curl
Test the browser path from a browser or with the raw handshake above. Bun’s WebSocket client (observed on 1.2.2) closes a correctly negotiated bearer handshake with 1002 Mismatch client protocol; it is a client quirk, not a refusal, and a Bun or Node process should send the Authorization header anyway.

Clients

These four are generated from one module — the same one the developer portal exports from — and are re-synced by scripts/sync-websocket-samples.mjs. They are not edited by hand.

What each client does

  • Authenticates on the upgrade. Node, Python and Go send Authorization: Bearer; the Browser client offers ['bearer', key] as the subprotocol. Neither ever puts the key in the URL.
  • Subscribes after open, one frame per subscription, and re-sends them on every reconnect.
  • Checks that seq increases. On the feeds these clients subscribe to, seq is the source block height, so a forward jump is a block that matched nothing, not a lost frame. A value at or below the last one means frames were replayed or reordered — the only real fault. A data frame carries no subscription id, so per-stream tracking keys on the channel plus the coin or wallet of the frame’s first fill.
  • Reconnects with backoff — 1 s doubling to 30 s, with jitter so a fleet does not reconnect in lockstep — and resets once live. An invalid key is never retried, and the process exits non-zero on it so a supervisor sees a failure.
  • Sends a heartbeat every 30 seconds, which keeps an idle connection warm and doubles as a liveness check.
Three things in the generated code are true of the feeds it subscribes to and not of the catalog as a whole. Adjust all three before you point a client at another feed.
  • The comment reading seq is the SOURCE BLOCK HEIGHT, the same on every feed holds for tradesByCoin and l4Book. It does not hold for leverageUpdates, twapOrders, their by-user and list variants, or candle/allCandles on source: "mark": there seq is the replica block’s Unix timestamp in nanoseconds — around 1.79 × 10¹⁸ against a height’s 1.1 × 10⁹, about nine orders of magnitude larger. Never hold one cursor across both kinds — see sequence numbers.
  • streamKey branches on tradesByCoin and tradesByUser and falls through to msg.channel for everything else, which is exactly the keying the protocol page warns against when two subscriptions share a channel. Add a branch for whatever you subscribe to.
  • The fill loop assumes data is an array of [wallet, fill] pairs — for (const [wallet, fill] of msg.data) and its Python and Go equivalents. That shape is the fill feeds’ alone. On candle and on a twapStatuses snapshot data is an object, and a live twapStatuses frame is an array of bare status events rather than of pairs — so the loop throws or mis-destructures on the very first frame. Read the feed’s own data shape before reusing the handler.

Following a list of wallets

The generated clients subscribe to two coins, but the change to follow a set of wallets is one frame. The five …ByUsers feeds take a users JSON array of strings — never a comma-separated string — of up to 1,000 distinct wallets, and the whole list counts as one subscription against your cap. tradesByUsers and twapSliceFillsByUsers are Starter and above; the liquidation, leverage and TWAP-order lists are Pro.
Swap that in for the two tradesByCoin frames a client sends on open. Two things then differ from the samples as generated:
  • streamKey needs a branch for it, and it cannot be the wallet: the wallet in the first entry changes from frame to frame. Hold at most one list subscription of a given type per connection and key it on msg.channel alone — why.
  • Reconnect resubscribes the same set. The list is hashed as a set, so re-sending it in any order rejoins the same subscription and the sequence cursor you recorded still applies.

Order book

The l4Book feed needs one thing the fill clients do not: a handler that keeps the book’s height and gates every update on prev. These four are the same generator with that handler spliced in — a fills-only client is byte-for-byte what it was — and they are re-synced by the same script. They subscribe to ZEC rather than BTC on purpose: the first message on a book subscription is the whole book, and ZEC’s snapshot is small (about 1.4 MB) where BTC’s is about 30 MB. Swap the coin once you have seen it work, spelling it as the node does. The feed is Pro and above.
What the book handler adds, on top of everything above:
  • Raises the read limit where the library has one — Python’s max_size and Go’s SetReadLimit go to 64 MiB, since their defaults (1 MiB and 32 KiB) would close the socket on the snapshot. A browser WebSocket has no limit; Node’s ws defaults to 100 MiB, which a 30 MB snapshot fits. Expect JSON.parse to take a moment on a large book.
  • Applies the client rule: the snapshot sets the height, each update is applied only if its prev is the last applied height, and any mismatch — or partial: true — unsubscribes and resubscribes for a fresh snapshot rather than jumping to live.
  • Leaves book failures to the ordinary error channel. A dropped book subscription arrives as {"channel":"error","data":"<reason>"} with no coin on it, so the client cannot attribute it when several books share a socket — the generated handler logs it and leaves the decision to you. unknown coin is a spelling problem and must not be retried as is.
  • Forgets the book on every new connection, because a new socket starts with a new snapshot, and dispatches l4Book frames before the seq tracking that the fill feeds use.