Stream live blockchain data over WebSocket
Decoded blocks, swaps, transfers, and any Substreams DatabaseChanges
output — delivered in seconds. Subscribe to one or many streams across SVM, EVM,
TVM, HyperLiquid, Polymarket, and any chain Pinax supports. Resume exactly where
you left off after a disconnect.
Real-time, multi-chain
One generic decoder for every db_out-style Substreams package. Solana, Ethereum, Base, Arbitrum, TVM, HyperLiquid, Polymarket — same URL pattern.
Reorg-aware
Every payload carries block_num and module_hash. undo messages fire on chain reorganizations so subscribers can roll back deterministically.
Use the Try it button in the bottom-right corner to open a full-screen WebSocket client — connect, subscribe, and watch blocks roll in without leaving this page.
Available streams
Live snapshot of every stream this server is configured to broadcast. Click a tile to copy its network@stream selector. Data sourced from GET /streams.
Loading from /streams…
URL modes
Stream selectors are <network>@<stream>. * is a wildcard on either side. Two ways to connect:
| URL | Mode | Envelope |
|---|---|---|
WS/ws/<a> |
Single, path | raw payload |
WS/ws/<a>/<b>/... |
Multi, path | wrapped |
WS/stream?streams=<a>/<b>/... |
Combined, query | wrapped |
When wrapped, every payload is delivered as:
{ "stream": "<network>@<table>", "data": <raw payload> }
Bare /ws with no streams returns HTTP 400. Use /ws/*@* to opt into every stream explicitly. See the HTTP group in the sidebar for non-WebSocket endpoints (/streams, /version, /healthz, /SKILL.md, /llms.txt).
Connected message
Sent once per connection. Lists every configured stream and echoes this connection's resolved subscriptions.
{
"type": "session",
"status": "connected",
"client_id": 1,
"streams": [
{
"network": "solana-mainnet",
"module": "db_out",
"manifest": "https://.../svm-dex-v0.5.1.spkg",
"module_hash": "bd388f2e39f5dcc237cfbdb8d6c96d9e5678c797",
"package_name": "svm_dex",
"package_version": "v0.5.1",
"tables": ["swaps"]
}
],
"subscriptions": ["solana-mainnet@swaps"],
"wrap_envelope": false
}
Block payload
One payload per (network, table) group per block. A spkg that emits both swaps and transfers in one block produces two per-table broadcasts.
{
"network": "solana-mainnet",
"table": "swaps",
"block_num": 350000000,
"block_hash": "Gsk6...",
"timestamp": "2026-05-13 17:00:00",
"module_hash": "bd388f2e...",
"events": [
{
"input_amount": "1287000000",
"input_mint": "So11111111111111111111111111111111111111112",
"output_amount": "6848381008732",
"output_mint": "13muFY...",
"protocol": "raydium_cpmm",
"user": "F2MUE..."
}
]
}
Field rules:
- Top-level
tableidentifies the channel (= the DatabaseChanges table emitted by the spkg'sdb_out). - Per-event
@tableis dropped because the parent payload already carriestable. - Field values inside
events[*]are strings on the wire (per DatabaseChanges proto). Numeric parsing is the consumer's job. - Row-level keys that duplicate top-level meta (
block_num,block_hash,timestamp,minute) are stripped. - Upstream
ordinal,operation,pk/composite_pk,update_opare dropped.
Stream lifecycle
Same connection, distinguished by "type": "stream". Filtered by subscriptions just like block payloads.
{ "type": "stream", "status": "started", ... }
{ "type": "stream", "status": "completed", ... }
{ "type": "stream", "status": "error", ..., "message": "..." }
{ "type": "stream", "status": "fatal", ..., "message": "..." }
{ "type": "stream", "status": "undo", ..., "last_valid_block": 350000000 }
undo fires on chain reorganizations. Roll back any state materialized past last_valid_block.
SUBSCRIBE
Add to the per-connection subscription set. Idempotent.
// request
{ "method": "SUBSCRIBE",
"params": ["solana-mainnet@swaps", "ethereum-mainnet@transfers"],
"id": 1 }
// reply
{ "result": null, "id": 1 }
Wildcards accepted (["*@swaps"]). A single bad selector rejects the whole command — existing set unchanged. The wrap_envelope mode is fixed at upgrade time and is not affected by SUBSCRIBE.
UNSUBSCRIBE
Remove from the subscription set. Silent on unknown selectors.
// request
{ "method": "UNSUBSCRIBE",
"params": ["ethereum-mainnet@transfers"],
"id": 2 }
// reply
{ "result": null, "id": 2 }
To remove a wildcard subscription, pass the exact wildcard form (*@swaps removes the wildcard entry, not the individual streams it currently matches).
LIST_SUBSCRIPTIONS
Inspect the current subscription set.
// request
{ "method": "LIST_SUBSCRIPTIONS", "id": 3 }
// reply
{ "result": ["solana-mainnet@swaps", "ethereum-mainnet@*"], "id": 3 }
Insertion order preserved. Wildcards returned verbatim, not expanded.
SET_FILTER
Drop non-matching events from the wire for the given selector. params[1] is an SQE expression string (the same language as Firehose substreams run -t): field:value string equality (case-insensitive), bare value matches any column, combined with || (OR), && / whitespace (AND), ! (NOT), and ( ) grouping. Missing field on an event is a miss. Wildcard selectors are accepted and applied to every matching channel. SET_FILTER replaces the filter for that selector (not accumulate — combine with || in one expression) — read the {"result"|"error"} reply, and use LIST_FILTERS to confirm one is active.
// request
{ "method": "SET_FILTER",
"params": ["solana-mainnet@swaps", "protocol:raydium_cpmm && user:F2MUE…"],
"id": 1 }
// reply (accept)
{ "result": null, "id": 1 }
// reply (reject — previous filter left unchanged)
{ "error": "…", "id": 1 }
Replaces any existing filter for that selector. Top-level fields (block_num, network, module_hash) are not filterable — only columns inside events[*].
CLEAR_FILTER
Drop the filter for the listed selectors.
// request
{ "method": "CLEAR_FILTER", "params": ["solana-mainnet@swaps"], "id": 2 }
// reply
{ "result": null, "id": 2 }
Silently ignores selectors without a filter.
LIST_FILTERS
Inspect the current filter map for this connection.
// request
{ "method": "LIST_FILTERS", "id": 3 }
// reply
{ "result": { "solana-mainnet@swaps": "protocol:raydium_cpmm" }, "id": 3 }
Returns the selector → expression-string map. An empty {} means no filter is active.
Event filters
Per-subscription filters drop events from events[] before delivery. The filter is an SQE expression string (StreamingFast Substreams Query Expression — the same language as Firehose substreams run -t). Pass ?filter=<url-encoded-expr> (alias ?sqe=) on connect, or use the live SET_FILTER / CLEAR_FILTER / LIST_FILTERS commands.
# ?filter=protocol:raydium_cpmm ws://host/ws/solana-mainnet@swaps?filter=protocol%3Araydium_cpmm
Semantics:
field:value— string equality only. ASCII-case-insensitive (checksummed or lowercase EVM both match). No regex, range, substring.- bare
value(nofield:). Matches when any string column of the event equals it (e.g.0xWALLET). - Operators.
||(OR),&&or whitespace (AND),!(NOT),( )grouping;&&binds tighter than||. Quote values containing spaces or( ) | & ' "using'…'or"…". - OR across columns works. A wallet that may appear in any of several columns (e.g.
tx_from,maker, ortaker) is expressible:tx_from:0xW || maker:0xW || taker:0xW, or just the bare term0xW. - Missing field = miss. Events without the filtered column are dropped.
- Schema-agnostic. Server doesn't know swap / transfer schemas. Any column name works.
- Top-level fields not filterable. Only columns inside
events[*]. - Zero matches = block skipped for that client. No zero-event broadcasts.
- Filters apply to wildcard selectors too. A filter on
*@*/*@swaps/solana-mainnet@*is resolved against each outgoing(network, table)at broadcast time.
Bounds — the values cap is the total number of terms across the whole expression, not per field; over-cap (or a parse error / non-string) rejects the filter and leaves the previous one in place:
SUBSTREAMS_WEBSOCKET_MAX_FILTER_FIELDS=16 # max distinct field names SUBSTREAMS_WEBSOCKET_MAX_FILTER_VALUES=512 # max total terms in the expression
Common filter shapes per stream — server does not enforce these, just operator-friendly examples matching commonly-present columns:
# SVM swaps — Raydium CPMM by user protocol:raydium_cpmm && user:F2MUEfN1HG5mC5EiUoxhjjc7HpKi4QQnzvipnbGx6Av8 # SVM swaps — WSOL on either leg input_mint:So11111111111111111111111111111111111111112 || output_mint:So11111111111111111111111111111111111111112 # SVM SPL transfers — a wallet as source OR destination source:<wallet> || destination:<wallet> # SVM system transfers — system program touching a wallet in any role program_id:11111111111111111111111111111111 && (source:<wallet> || destination:<wallet> || fee_payer:<wallet>)
Full spec: docs/filters.md.
Reconnects (live-only)
This is a live-only feed: blocks are delivered as they arrive and there is no server-side buffer of past blocks. On reconnect you simply resume the live stream — there is no catch-up.
The query parameters ?from_timestamp= and ?from_block= are not supported and return HTTP 400 at the WebSocket upgrade. To backfill history, use Substreams directly — it resumes natively from any block, cursor, or timestamp and is the right tool for replay.
Cursor handling stays internal to the server: on restart each stream resumes from its persisted cursor so the live feed continues without operator action. The cursor is not exposed to clients.
Heartbeats
The server sends WebSocket ping frames every SUBSTREAMS_WEBSOCKET_HEARTBEAT_INTERVAL_SECS seconds (default 180s). Standard WebSocket clients pong automatically. The server closes connections that do not pong within SUBSTREAMS_WEBSOCKET_HEARTBEAT_TIMEOUT_SECS seconds (default 600s).
Limits
- Live-only, no replay. There is no on-disk replay log;
?from_timestamp=/?from_block=are rejected with HTTP 400. For any history, backfill via Substreams directly (by block, cursor, or timestamp). - One output type. Only
sf.substreams.sink.database.v1.DatabaseChanges. - Per-connection client buffer. Slow consumers are dropped per-message rather than blocking the ingest loop. Defaults to 1024 messages, configurable via
SUBSTREAMS_WEBSOCKET_CLIENT_BUFFER_SIZE.