Docs/Realtime/WebSocket

WebSocket

One socket for live OHLCV, trades and price.

Open one multiplexed socket and subscribe to channels per token; updates push as they land.

Transport: the WebSocket upgrade is served over HTTP/1.1 (the standard for WebSockets). Browsers and the 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:true means 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 subscribe frame; 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 nextCursor you 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.