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 a Substreams cursor 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>@<stream>", "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, /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": [
{
"stream": "swaps",
"network": "solana-mainnet",
"module": "db_out",
"manifest": "https://.../svm-dex-v0.5.1.spkg",
"module_hash": "bd388f2e39f5dcc237cfbdb8d6c96d9e5678c797"
}
],
"subscriptions": ["solana-mainnet@swaps"],
"wrap_envelope": false
}
Block payload
One message per non-empty block. Empty blocks are skipped; their cursor is still persisted server-side.
{
"stream": "swaps",
"network": "solana-mainnet",
"block_num": 350000000,
"block_hash": "Gsk6...",
"timestamp": "2026-05-13 17:00:00",
"cursor": "Mloz_-WpoBoZ...",
"module_hash": "bd388f2e...",
"events": [
{
"@table": "swaps",
"input_amount": "1287000000",
"input_mint": "So11111111111111111111111111111111111111112",
"output_amount": "6848381008732",
"output_mint": "13muFY...",
"protocol": "raydium_cpmm",
"user": "F2MUE..."
}
]
}
Field rules:
@tableidentifies the source table per row. Prefixed so a real column literally namedtablecan't shadow it.- 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.
Cursors
Every block payload includes cursor. To resume from an exact position across reconnects, persist the last cursor you processed and reconnect to a Substreams gRPC endpoint directly with it — this server has no replay buffer.
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
- No replay buffer. Disconnected clients miss broadcasts during the gap.
- 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.