WebSockets
Stream decoded blocks, DEX swaps, and token transfers over a single persistent connection. Path-based subscription, server-side SQE filters, and per-message billing.
What you get
- Solana DEX swaps —
solana@swaps. Raydium, Orca, Meteora, Pump.fun and more, normalized per trade. - Solana transfers —
solana@spl_transfer,solana@system_transfer. SPL, SPL-2022, and native SOL. - EVM DEX swaps —
mainnet@swaps,base@swaps, and 6 more EVM chains. - EVM ERC-20 transfers —
mainnet@erc20_transfers, plus the same 8 EVM networks.
Live catalog at ws.pinax.network/streams. Supported networks at api.pinax.network/v1/networks.
Subscription URL
Channels are encoded in the URL path — no JSON-RPC subscribe message needed.
wss://ws.pinax.network/ws/<network>@<table>?token=<API_KEY>
Single channel, raw payload:
wss://ws.pinax.network/ws/solana@swaps?token=$PINAX_KEY
Multi-channel (wrapped envelope — { "stream": "<id>", "data": <raw> }):
wss://ws.pinax.network/ws/solana@swaps/mainnet@erc20_transfers?token=$PINAX_KEY
Wildcards work on either side — *@swaps, solana@*. Bare /ws is HTTP 400; use /ws/*@* to opt into everything.
The network side also takes a comma-separated list — subscribe to the same table across chains in one selector. The server expands it into one entry per network.
wss://ws.pinax.network/ws/solana,base,mainnet@swaps?token=$PINAX_KEY
A bare *can't be mixed with named networks, and the comma list isn't allowed on the table side. Comma lists also work in SUBSCRIBE / UNSUBSCRIBE params.
Messages vs events — billing
Two counters matter, and they are not the same number:
- Messages — one WebSocket frame per
(network, table)per block. This is the billable unit. - Events — items inside each message's
events[]array. A single message can carry dozens of swaps or transfers.
Pricing is $0.00005 per message (= $0.50 per 10K). A package that emits two tables produces two messages per block — one per table — regardless of how many events each contains. Use server-side SET_FILTER to reduce billable messages further; heartbeats and protocol frames are free.
Sample envelope (Solana swaps):
{
"network": "solana",
"table": "swaps",
"block_num": 422102644,
"block_hash": "6AEzeSFDZstnhagF1D2FsCPNVELz1YBTvHFMHDSNP4fs",
"timestamp": "2026-05-25T16:20:05Z",
"timestamp_seconds": 1779726005,
"module_hash": "411d6a46…b295",
"events": [
{ "protocol": "pumpfun_amm", "user": "B6r52E…YUbC", ... },
{ "protocol": "raydium_clmm", "user": "9KaPDQ…X4tN", ... }
]
}Authentication
Every connection needs your API key. Two equivalent options:
- Header (server libs):
Authorization: Bearer <jwt> - Query param(browsers — can't set headers on the WS upgrade):
?token=<jwt>
Same JWT as the rest of Pinax — RPC, Firehose, Token API. Quota is shared.
Reconnects (live-only)
This WebSocket is a live-only feed — it delivers blocks as they arrive and keeps no history. On reconnect you simply resume the live stream; there is no server-side catch-up. Cursor handling stays internal to the server, so the live feed keeps flowing across restarts with no client action.
For historical data or to backfill a gap, use a Substreams gRPC stream — it resumes from any block, cursor, or timestamp with an explicit start_block, which is the right tool for replay.
Server-side filtering
Drop non-matching events before they hit the wire — this cuts billable messages, not just bandwidth. The filter is an SQE expression string (the same query language as Firehose substreams run -t) scoped to a selector: params is [selector, expression].
field:value— case-insensitive equality on anevents[*]column.- bare
value— matches when any column equals it. - operators
||(OR),&&or whitespace (AND),!(NOT),( )(grouping).
Ethereum — watch one wallet in any role (sender, receiver, or tx signer):
{ "method": "SET_FILTER",
"params": ["mainnet@erc20_transfers",
"tx_from:0xd8dA…6045 || from:0xd8dA…6045 || to:0xd8dA…6045"],
"id": 1 }Solana — Raydium CPMM swaps from one wallet:
{ "method": "SET_FILTER",
"params": ["solana@swaps",
"protocol:raydium_cpmm && user:F2MUEfN1HG5mC5EiUoxhjjc7HpKi4QQnzvipnbGx6Av8"],
"id": 2 }You can also set a filter on connect with ?filter=<url-encoded-expr> (alias ?sqe=). SET_FILTER replaces the filter for a selector — combine conditions with || in one expression. Max 512 terms, 16 distinct fields. Full reference and recipes: Filters docs.
Use cases
Push DEX swaps and ERC-20 / SPL transfers into a UI the moment they confirm. One socket per network@table — no polling, no per-block REST fan-out.
Apply server-side SET_FILTER rules to drop non-matching events on the wire. Trigger Discord, Slack, or webhook notifications only on the trades you actually care about.
undo lifecycle events surface chain reorganizations explicitly. Roll back materialized state past last_valid_block instead of rebuilding from scratch.
Stream pre-parsed swaps into an agent loop with no schema work. Each message ships the protocol, the user, and the trade — ready to be summarized or acted on.
Frequently asked
module_hash in production and watch for stream lifecycle fatal messages.stream dropped frame with the count lost plus last_block / last_timestamp of where delivery resumed, so you can reconcile the gap instead of silently shipping incomplete data.ping/pong frames are protocol-level and free. The server pings every 180s; clients that don't pong within 600s are closed.decimals) to scale.tx_index, tx_nonce, tx_gas_*, tx_value, log_index, log_topics, log_data, and all call_*. On SVM: compute_units_consumed, stack_height. Kept: tx_hash, tx_from/tx_to, log_ordinal, log_address, signature, fee_payer, program_id. Any *_raw field is split on commas into a JSON array under the suffix-stripped key — signers_raw "a,b,c" → signers ["a","b","c"].start_block (it resumes from any block, cursor, or timestamp), or query Token API for indexed history.Related
- WebSockets reference — full endpoint, message, and command catalog.
- Token API guide — historical equivalents for the same data.
- Product page — pricing, calculator, live demo.