Filters
Filters drop non-matching events[*] rows server-side, before they ever reach the wire — so they cut both bandwidth and billable messages (a block whose every event is filtered out produces no message for you). Filtering is the main lever for turning a firehose like mainnet@erc20_transfers into just the transfers you care about.
SQE expressions
A filter is an SQE expression string — StreamingFast Substreams Query Expression, the same language as Firehose substreams run -t:
field:value— case-insensitive string equality on anevents[*]column.- bare
value(nofield:) — matches when anycolumn equals it. Great for "this wallet in any role". - operators
||(OR),&&or whitespace (AND),!(NOT),( )(grouping).&&binds tighter than||; quote values containing spaces or operators. - only
events[*]columns are filtered — top-levelblock_num/network/module_hashare not.
Two ways to set a filter
- At connect time —
?filter=<url-encoded-expr>(alias?sqe=) on the upgrade URL. An invalid expression is rejected withHTTP 400at the upgrade. - Live — the
SET_FILTER/CLEAR_FILTER/LIST_FILTERScommands, scoped pernetwork@tableselector.SET_FILTERreplaces the filter for a selector — combine conditions with||in one expression.
wss://ws.pinax.network/ws/solana@swaps?filter=protocol%3Araydium_cpmm # the filter param is a URL-encoded SQE expression — here: protocol:raydium_cpmm
Semantics
- Case-insensitive equality. A checksummed or lowercased EVM address both match; supply the exact value for case-significant keys like Solana base58.
- Missing field = miss.An event without the named column doesn't match that term.
- Event-level.You receive only the matching events; a block whose every event is filtered out isn't sent to you at all.
- Wildcard selectors (
*@swaps,*@*) are filterable too — a filter applies to every(network, table)the selector matches.
events[*] row. Connect, read one block payload, and filter on whatever keys you see — the server is schema-agnostic and never validates column names.Example — Ethereum ERC-20 transfers (mainnet@erc20_transfers)
Useful columns to filter a transfer row on:
| log_addressstring | The token contract that emitted the |
| from, tostring | Sender / recipient addresses — watch a wallet's inflows or outflows. |
Match a token by its log_addresscontract, not a symbol — the stream carries addresses, and amounts aren't a useful equality target.
Only USDC transfers landing on one wallet:
{ "method": "SET_FILTER", "params": [ "mainnet@erc20_transfers", "log_address:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 && to:0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" ], "id": 1 }
Any of several stablecoins (by contract) sent from one wallet — OR the contracts, AND the sender:
{ "method": "SET_FILTER", "params": [ "mainnet@erc20_transfers", "(log_address:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 || log_address:0xdAC17F958D2ee523a2206206994597C13D831ec7 || log_address:0x6B175474E89094C44Da98b954EedeAC495271d0F) && from:0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" ], "id": 2 }
Example — Solana swaps (solana@swaps)
Common columns on a swap row:
| protocolstring | DEX protocol — e.g. |
| input_mint, output_mintstring | Mint addresses of the tokens swapped. Wrapped SOL is |
| user, fee_payerstring | Swapper wallet / transaction fee payer. |
| amm, program_idstring | Pool / on-chain program addresses. |
Raydium CPMM swaps that spend wrapped SOL, from one wallet:
{ "method": "SET_FILTER", "params": [ "solana@swaps", "protocol:raydium_cpmm && input_mint:So11111111111111111111111111111111111111112 && user:F2MUEfN1HG5mC5EiUoxhjjc7HpKi4QQnzvipnbGx6Av8" ], "id": 3 }
Swaps on either of two protocols (OR), across all wallets:
{ "method": "SET_FILTER", "params": [ "solana@swaps", "protocol:raydium_cpmm || protocol:pumpfun_amm" ], "id": 4 }
SUBSTREAMS_WEBSOCKET_MAX_FILTER_VALUES / …_MAX_FILTER_FIELDS, defaults 512 / 16). An expression over a cap, with a parse error, or that isn't a string returns an error reply and leaves the previous filter unchanged — the socket stays open. See SET_FILTER for the live-command envelope.