Data API · Real-time

Streams

Push, don't poll. Subscribe over WebSocket and receive trades, prices, candles, launches, pools and bonding-curve updates the instant they're indexed — one hop from our own validator (~sub-second). Same API key as the REST Data API.

WebSocketwss://solindex.infinityblocks.io/stream?api-key=<API_KEY>

Connect & authenticate

Open a WebSocket to /stream with your Data API key (as ?api-key= or an x-api-key header). On connect you receive a welcome frame listing the channels and filters. Then send subscribe messages; you can hold many subscriptions on one connection.

const ws = new WebSocket("wss://solindex.infinityblocks.io/stream?api-key=<API_KEY>");

ws.onopen = () => {
  // every DegenSafe trade, ≥ $1, pushed live
  ws.send(JSON.stringify({
    action: "subscribe", id: "dgn-trades", channel: "trades",
    filters: { platform: "GKLyKXb8XbvEZqif3C4ksRyYf7BP1ASQTLbRScM8oGnB", minUsd: 1 }
  }));
};
ws.onmessage = (e) => {
  const m = JSON.parse(e.data);
  if (m.channel === "trades") console.log(m.data);   // { time, mint, direction, wallet, amountToken, amountSol, amountUsd, priceUsd, mcapUsd, signature, dex, eventId, eventIndex, poolAddress, slot }
};

Try it — live

Live streamWebSocket

Filters (JSON, all optional) — token (mint), tokens ([mints]), dex (Raydium: launchlab·raydium-cpmm·raydium-amm-v4·raydium-clmm·raydium-stableswap; Meteora: meteora-dlmm·meteora-damm-v1·meteora-damm-v2·meteora-dbc), wallet, minUsd, platform (launchpad pubkey, e.g. DegenSafe GKLyKXb8…oGnB). candles require token + interval (1m·5m·15m·30m·1h·4h·1d·1w). An example auto-fills when you change channel. Runs live in your browser; key stored only locally.

Channels

ChannelPushesNotes
tradesevery swap as it's indexedthe firehose — filterable; everything else derives from it
prices{ mint, priceUsd, mcapUsd, time, dex, amountUsd } per swaplightweight ticker — amountUsd is the value the minUsd filter checks; mcapUsd = priceUsd × total supply (server-computed from the launch registry; null for tokens outside the registry and for the first ~minute of a brand-new mint until its row is cached)
candleslive OHLCV for the current bucketrequires token + interval; emits final:false ticks then a final:true candle on rollover
launchesnew token launches + graduations"new tokens" feeds
poolsnew pools created"new pairs" feeds
bonding-curveLaunchLab graduation progressprogressPct, reserves, status

Filters

FilterTypeDescription
tokenstringone mint address
tokensstring[]several mint addresses
dexstringRaydium: launchlab · raydium-cpmm · raydium-amm-v4 · raydium-clmm · raydium-stableswap. Meteora: meteora-dlmm · meteora-damm-v1 · meteora-damm-v2 · meteora-dbc
walletstringtrades by a specific wallet
minUsdnumberdrop trades below this USD value
platformstringLaunchLab PlatformConfig pubkey — scope to one launchpad (e.g. DegenSafe)
intervalenumcandles only: 1m·5m·15m·30m·1h·4h·1d·1w

Per-channel semantics

Channels are event-driven — an event is pushed the instant it's indexed, and a channel can sit idle for a while when nothing is happening (e.g. launches only ticks when a token actually launches). A quiet channel is not a broken one. Resume support differs per channel:

ChannelFrequencysince
(time backfill)
sinceId
(cursor replay)
filters honored
tradeshigh (firehose)✓ from ClickHousetoken·tokens·dex·wallet·minUsd·platform
priceshigh (1 per swap)—¹token·tokens·dex·minUsd·platform
candlesticks per swap + on rollover✓ computed on-read from dex_trades—²requires token + interval
launcheslow (per launch/graduation)✓ from ClickHousetoken·tokens·platform
poolslow (per new pool)✓ from ClickHousetoken·tokens·dex·platform
bonding-curvemedium (graduation progress)—¹token·tokens·platform

¹ prices and bonding-curve are derived live and have no standalone history table — resume them with sinceId against the durable log (covers the live buffer). For deep price history use trades/candles since instead.   ² candles resume by replaying closed candles with since, not sinceId.

Both since and sinceId stream the gap oldest→newest, each event tagged backfill:true, terminated by a single { type:"backfill_done", count } marker, then live events flow. Use sinceId for a fast resume after a brief drop; use since to pull a deeper historical window on first connect.

Streams are at-least-once and best-effort, not exactly-once. The honest contract: the REST Data API (ClickHouse) is the source of truth; the stream is a low-latency layer on top of it, and publishing can occasionally drop an event. Dedupe client-side on eventId for trades (not signature — one signature can carry several swap legs). Events arrive in publish order, not slot order — under load a later-slot event can be published a moment before an earlier one. Corrections to already-published data (e.g. a re-derive) land only via REST, never as a stream event. Use /stream/health (replay.lagSec, indexer.freshnessSec) to decide when to reconcile a subscription against REST rather than trust the stream alone.

A since backfill returns up to 2,000 events per subscription (oldest first). If a window holds more than that, raise since (a more recent timestamp) or resume with sinceId from the last cursor you saw — for very deep history page the REST Data API, which is the source of truth and fully paginated.

Live candles

ws.send(JSON.stringify({
  action: "subscribe", id: "chart", channel: "candles",
  filters: { token: "<MINT>", interval: "1m" },
  since: Date.now() - 3600_000          // backfill the last hour, then go live
}));
// -> backfilled closed candles (backfill:true), then live { ...ohlcv, final:false } ticks,
//    and a { final:true } candle each time the 1-minute bucket rolls over.

Stream health & replay horizon

Streams are the low-latency layer; the REST Data API (ClickHouse) is the authoritative store. GET /stream/health is a plain HTTPS endpoint — no API key, no WebSocket — that tells you how current each layer is, so a client can decide whether to resume with sinceId (fast ring-buffer replay) or fall back to a REST backfill, and how deep the ring buffer currently reaches.

curl -s https://solindex.infinityblocks.io/stream/health
{
  "ok": true, "service": "stream", "clients": 42,
  "live": true,
  "replay": {
    "type": "ring-buffer",
    "oldestRetainedId": "1780160000123-0", "oldestRetainedAgeSec": 41820,
    "note": "sinceId replay is a volume-dependent ring buffer (~12h at current load, less when busier); a resume older than oldestRetainedId is unrecoverable and returns {type:gap}",
    "oldestRetainedTs": 1780160000, "oldestRetainedSlot": 423190000,
    "latestId": "1780203800456-3", "latestTs": 1780203800, "latestSlot": 423214981,
    "lagSec": 1, "entries": 812400, "retainedWindowSec": 43800
  },
  "indexer": { "latestTradeTs": 1780203799, "freshnessSec": 2, "checkedAgoSec": 6 }
}
FieldMeaning
ok, servicereachability check — always true / "stream".
clientsopen WebSocket connections right now.
livetrue when the stream layer is current (newest event <60s old). Mainnet DEX flow never pauses that long, so false means the stream pipeline — not the chain — has stalled.
replay.typealways "ring-buffer"sinceId replay comes from a bounded log, not unlimited history.
replay.oldestRetainedId, oldestRetainedAgeSecthe oldest cursor still replayable, and its age. A sinceId older than this returns { type:"gap" } instead of a silent (lossy) resume.
replay.oldestRetainedTs, oldestRetainedSlotunix-seconds timestamp and slot of that oldest retained entry — the practical replay horizon.
replay.latestId, latestTs, latestSlotcursor / timestamp / slot of the newest event seen on the live path.
replay.lagSecseconds since that newest live event landed — what live is computed from.
replay.entriescurrent length of the ring buffer.
replay.retainedWindowSeclatestTs − oldestRetainedTs — how far back sinceId can currently reach. Volume-dependent: shrinks when the chain is busier.
replay.notehuman-readable restatement of the ring-buffer contract above.
indexer.latestTradeTsnewest dex_trades row in ClickHouse (unix seconds) — the authoritative store's own recency.
indexer.freshnessSechow stale that authoritative row is right now.
indexer.checkedAgoSechow long ago this freshness figure was measured (cached ~15s server-side).

How to use it: reconnecting with a saved sinceId? Compare it (or its embedded timestamp) against replay.oldestRetainedId/oldestRetainedTs — if your cursor predates it, skip straight to a REST backfill instead of waiting on { type:"gap" }. Watch replay.lagSec and indexer.freshnessSec together: both climbing means the pipeline is actually stalled; neither climbing but a channel is quiet just means nothing has happened (these two fields are chain-wide, not per-channel).

Resume after a disconnect

Every event carries a cursor — a composite "batchId:idx" (the underlying Redis stream entry id, plus this event's position within that batch). Reconnect and pass the last cursor you saw back as sinceId for an exactly-once resume: the server rescans the boundary batch, skips everything at or before your idx, replays the rest, then goes live:

ws.send(JSON.stringify({ action: "subscribe", channel: "trades", id: "t",
  filters: { platform: "GKLyKXb8…" }, sinceId: lastCursorYouSaw }));
// -> replayed events (backfill:true) … { type:"backfill_done", count, cursor } … then live
// -> OR, if lastCursorYouSaw predates the retained window: { type:"gap", oldestRetainedId, lostWindowMs }

A bare batch id with no :idx (e.g. a cursor saved before per-event cursors existed) is treated as exclusive of the whole batch — every event in that batch replays again, which can duplicate events you already received. Always resume with the full "batchId:idx" form when you have it.

For deeper history (beyond the ring-buffer window — see Stream health below) use since (a unix-ms timestamp) instead — it backfills from ClickHouse, then resumes live.

Message reference

Bulk / enterprise consumers

The WebSocket API suits apps and bots. For high-throughput backends that want the raw firehose with durable replay and consumer-group load-balancing, the stream is also available as a durable log (Redis Streams). Ask us for a dedicated consumer endpoint — you get at-least-once delivery, replay from any point, and horizontal fan-out across workers.

Streams are best-effort live; the REST Data API remains the source of truth for history and exact backfills. Heavy per-account feeds (holders, reserves) are served via REST, not streamed.