WebSocket
One socket for live OHLCV, trades and price.
Open one multiplexed socket and subscribe to channels per token; updates push as they land.
ws Node library negotiate this automatically — just use wss://www.quanaris.com/ws. If you probe the URL with an HTTP/2-only client (e.g. curl --http2) you'll get a 404: that's the h2 edge refusing the upgrade, not a broken endpoint. Force HTTP/1.1 (curl --http1.1) to test by hand.// wss://www.quanaris.com/ws const ws = new WebSocket("wss://www.quanaris.com/ws"); ws.onopen = () => ws.send(JSON.stringify({ type: "subscribe", channel: "ohlcv", params: { chain: "solana", address: MINT, interval: "1m" } })); ws.onmessage = (e) => console.log(JSON.parse(e.data));
Authentication
The socket works anonymously for the public per-mint channels (that's how the embeddable charts stream). To use the account stream and get your plan's connection allowance, present an API key — either wss://www.quanaris.com/ws?key=YOUR_KEY or the bearer subprotocol (new WebSocket(url, ["bearer", key])). Each authenticated connection counts against your plan's wsConnections; anonymous connections are capped generously per IP. On connect you get { type:"welcome", authenticated, plan, wsConnections }.
Channels
Per-mint (anonymous OK) — params:{ chain, address, interval? }:
ohlcv— data frame{ channel, address, interval, data:{ t,o,h,l,c,v } }trades—{ side, signer, dex, priceUsd, volumeUsd }price—{ priceUsd, t }backfill— live indexing progress{ data:{ event, state, pct, swapsIngested } }(a freshly-subscribed mint streams its backfill until candles land).stats— holder stats{ data:{ holders:{ count, countIsFloor, updatedAt } } }, pushed each time a subscribed mint's holders are refreshed (the scheduled keeper cycle — default every 15 min — plus the lazy on-request refresh).countIsFloor:truemeans the count is a lower bound, exactly as on Holders.
Account stream
One socket for ALL your subscribed mints — no need to enumerate addresses (requires a key). Subscribe { type:"subscribe", channel:"account", params:{ interval? } }; every frame is tagged with its mint:
{ channel:"account", scope:"account", mint, kind:"trade"|"candle"|"backfill"|"stats", data }
Control & errors
Unsubscribe with { type:"unsubscribe", channel, params:{ address, interval } }. Acks: { type:"subscribed"|"unsubscribed" }. Errors: { error:"bad_channel"|"bad_address"|"bad_interval"|"too_many_subscriptions"|"auth_required"|"ws_limit"|"ws_busy"|"lagged" }.
Keepalive & reconnect contract
The server sends a WebSocket ping every 30s; a compliant client (browsers, the ws library) answers with a pong automatically — you don't write code for it. If two pings go unanswered the server terminates the socket as dead, so keep your event loop unblocked.
- Resubscribe on reconnect is mandatory. Subscriptions are per-connection and are not restored after a drop. When your socket closes, reconnect and re-send every
subscribeframe; nothing is replayed until you do. - Replay-on-gap. After resubscribing, backfill the disconnect window over REST — refetch OHLCV for the affected buckets (the tail is reconciled, so a re-pull is safe and idempotent) and page Trades with the
nextCursoryou last held. Cursors are opaque tokens — pass them back verbatim, never parse or synthesize them. - Backpressure /
lagged. If a client falls too far behind and its send buffer overflows, the server sends one{ error:"lagged" }frame and closes with code 1009 instead of buffering unboundedly. Treat 1009 like any other drop: reconnect, resubscribe, replay-on-gap.