Docs
    Guide · Beta

    WebSockets

    Stream decoded blocks, DEX swaps, and token transfers over a single persistent connection. Path-based subscription, server-side filters, replay on reconnect, 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 & replay

    The server retains a 3600s (1h) window per package on disk. On reconnect, resume with one of two mutually exclusive cursors — every block past it is replayed oldest-first before the live stream resumes:

    • ?from_timestamp=<n> (epoch seconds or ISO-8601) — chain-agnostic, works for any selector including wildcards. Replays timestamp_seconds > n.
    • ?from_block=<n> — per-chain block number, replays block_num > n. Single concrete network@table only; wildcard or multi-selector returns 400.
    wss://ws.pinax.network/ws/solana@swaps?from_timestamp=1715619600
    wss://ws.pinax.network/ws/solana@swaps?from_timestamp=2026-05-25T16:00:00Z
    wss://ws.pinax.network/ws/solana@swaps?from_block=350000001

    If the cursor falls below the oldest retained value, a gap lifecycle message fires (fields match the cursor unit) — backfill via Substreams gRPC with an explicit start_block. Wildcard selectors (*@swaps, *@*) replay too on ?from_timestamp=, expanding into concrete per-network@table frames.

    Server-side filtering

    Drop non-matching events before they hit the wire — this cuts billable messages, not just bandwidth. params is positional: [selector, filter]. String equality only; keys are AND'd, values within a key are OR'd (pass an array); a missing column counts as a miss.

    Ethereum — only USDC transfers landing on one wallet (log_address = token contract):

    { "method": "SET_FILTER",
      "params": ["mainnet@erc20_transfers",
                 { "log_address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
                   "to": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" }],
      "id": 1 }

    Solana — Raydium CPMM swaps spending wrapped SOL, from one wallet:

    { "method": "SET_FILTER",
      "params": ["solana@swaps",
                 { "protocol": "raydium_cpmm",
                   "input_mint": "So11111111111111111111111111111111111111112",
                   "user": "F2MUEfN1HG5mC5EiUoxhjjc7HpKi4QQnzvipnbGx6Av8" }],
      "id": 2 }

    Per-selector. Wildcards always pass through. Max 16 keys, 64 values per filter. Top-level fields (block_num, network) are not filterable — only keys inside events[*]. Full column reference and recipes: Filters docs.

    Use cases

    Live trade & transfer feeds

    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.

    Real-time alerting

    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.

    Reorg-aware indexers

    undo lifecycle events surface chain reorganizations explicitly. Roll back materialized state past last_valid_block instead of rebuilding from scratch.

    AI agents & MCP

    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

    Why is this marked Beta?
    Stream protocol is stable but channel names, payload field naming, and the replay window may evolve. Pin a module_hash in production and watch for stream lifecycle fatal messages.
    How many connections can I open?
    Beta caps per-key concurrency at 4 connections. Multiplex many channels over a single socket using the multi-channel URL form instead of opening one per stream.
    What happens to slow consumers?
    Each connection has a 1024-message buffer. If you fall behind, messages are dropped per-message rather than blocking ingest — so a sluggish consumer never backpressures other subscribers. When you catch up, the server emits a 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.
    Are heartbeats billable?
    No. WebSocket ping/pong frames are protocol-level and free. The server pings every 180s; clients that don't pong within 600s are closed.
    Are values in events[] strings or numbers?
    Strings on the wire — DatabaseChanges proto convention. Numeric parsing is the consumer's job. Use the field's known decimals (e.g. token decimals) to scale.
    Which event fields are dropped on the wire?
    ClickHouse-backfill provenance columns are stripped at decode time. On EVM: 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"].
    How do I get older history?
    The replay window is 3600s (1h). For deeper backfills, use Substreams gRPC directly with a start_block, or query Token API for indexed history.

    Related