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.
wss://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
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
| Channel | Pushes | Notes |
|---|---|---|
trades | every swap as it's indexed | the firehose — filterable; everything else derives from it |
prices | { mint, priceUsd, mcapUsd, time, dex, amountUsd } per swap | lightweight 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) |
candles | live OHLCV for the current bucket | requires token + interval; emits final:false ticks then a final:true candle on rollover |
launches | new token launches + graduations | "new tokens" feeds |
pools | new pools created | "new pairs" feeds |
bonding-curve | LaunchLab graduation progress | progressPct, reserves, status |
Filters
| Filter | Type | Description |
|---|---|---|
token | string | one mint address |
tokens | string[] | several mint addresses |
dex | string | Raydium: launchlab · raydium-cpmm · raydium-amm-v4 · raydium-clmm · raydium-stableswap. Meteora: meteora-dlmm · meteora-damm-v1 · meteora-damm-v2 · meteora-dbc |
wallet | string | trades by a specific wallet |
minUsd | number | drop trades below this USD value |
platform | string | LaunchLab PlatformConfig pubkey — scope to one launchpad (e.g. DegenSafe) |
interval | enum | candles 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:
| Channel | Frequency | since(time backfill) | sinceId(cursor replay) | filters honored |
|---|---|---|---|---|
trades | high (firehose) | ✓ from ClickHouse | ✓ | token·tokens·dex·wallet·minUsd·platform |
prices | high (1 per swap) | —¹ | ✓ | token·tokens·dex·minUsd·platform |
candles | ticks per swap + on rollover | ✓ computed on-read from dex_trades | —² | requires token + interval |
launches | low (per launch/graduation) | ✓ from ClickHouse | ✓ | token·tokens·platform |
pools | low (per new pool) | ✓ from ClickHouse | ✓ | token·tokens·dex·platform |
bonding-curve | medium (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.
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 }
}
| Field | Meaning |
|---|---|
ok, service | reachability check — always true / "stream". |
clients | open WebSocket connections right now. |
live | true 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.type | always "ring-buffer" — sinceId replay comes from a bounded log, not unlimited history. |
replay.oldestRetainedId, oldestRetainedAgeSec | the oldest cursor still replayable, and its age. A sinceId older than this returns { type:"gap" } instead of a silent (lossy) resume. |
replay.oldestRetainedTs, oldestRetainedSlot | unix-seconds timestamp and slot of that oldest retained entry — the practical replay horizon. |
replay.latestId, latestTs, latestSlot | cursor / timestamp / slot of the newest event seen on the live path. |
replay.lagSec | seconds since that newest live event landed — what live is computed from. |
replay.entries | current length of the ring buffer. |
replay.retainedWindowSec | latestTs − oldestRetainedTs — how far back sinceId can currently reach. Volume-dependent: shrinks when the chain is busier. |
replay.note | human-readable restatement of the ring-buffer contract above. |
indexer.latestTradeTs | newest dex_trades row in ClickHouse (unix seconds) — the authoritative store's own recency. |
indexer.freshnessSec | how stale that authoritative row is right now. |
indexer.checkedAgoSec | how 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
- subscribe (client → server) —
{ action, channel, id?, filters?, since?, sinceId? }→{ type:"subscribed", sub, channel, filters } - unsubscribe (client → server) —
{ action:"unsubscribe", id }→{ type:"unsubscribed", sub } - ping (client → server) —
{ action:"ping" }→{ type:"pong" }(the server also sends WebSocket-protocol pings every 30s) - welcome (server → client, sent once on connect) —
{ type:"welcome", channels: [...], filters: [...] }, listing everything your key can subscribe to - event (server → client) —
{ channel, sub, cursor, data, backfill? } - backfill_done (server → client) —
{ type:"backfill_done", sub, count, cursor? }, marking the end of asince/sinceIdcatch-up (live events follow) - gap (server → client) —
{ type:"gap", sub, channel, requested, oldestRetainedId, lostWindowMs }, sent when asinceIdpredates the oldest retained ring-buffer entry (see Stream health) — that window is unrecoverable from the stream; backfill it via REST instead - error (server → client) —
{ type:"error", error, sub? }— invalid JSON, unknown channel, bad filter, too many subscriptions, or a failed backfill/replay
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.