Geyser · Solana Streaming
Filters & data slices
The point of Geyser is to make the server do the narrowing. The tighter your filters, the less data crosses the wire — which keeps you comfortably inside flat fair-use. Account filters, transaction filters, and accountsDataSlice are the three levers, and they stack.
Account filters
An entry under accounts narrows by four things, ANDed together:
- ·
account — a list of explicit pubkeys. Watch a handful of known accounts. - ·
owner — the program that owns the account. Stream every account a program controls. - ·
filters with memcmp ({ offset, base58/bytes }) to match bytes at a position, and datasize to match exact account length.
SPL token accounts make a clean example. Every one is owned by the Token program and is exactly 165 bytes, with the mint pubkey at offset 0. A memcmp at offset 0 plus a 165-byte datasize gives you a live feed of just one mint's token accounts.
1import Client, {
2 CommitmentLevel,
3 SubscribeRequest,
4} from "@triton-one/yellowstone-grpc";
5
6const client = new Client(
7 "https://geyser.therpc.io:443",
8 "<YOUR_API_KEY>",
9 undefined
10);
11
12const USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
13
14const request: SubscribeRequest = {
15 accounts: {
16 usdc_token_accounts: {
17 account: [],
18 // SPL Token program owns every token account
19 owner: ["TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"],
20 filters: [
21 // token account layout: mint pubkey sits in the first 32 bytes
22 { memcmp: { offset: 0, base58: USDC_MINT } },
23 // and the struct is exactly 165 bytes
24 { datasize: 165 },
25 ],
26 nonemptyTxnSignature: undefined,
27 },
28 },
29 slots: {},
30 transactions: {},
31 transactionsStatus: {},
32 blocks: {},
33 blocksMeta: {},
34 entry: {},
35 accountsDataSlice: [],
36 commitment: CommitmentLevel.PROCESSED,
37};
Trim the payload with accountsDataSlice
Matching the right accounts is half the job; you rarely need every byte they carry. accountsDataSlice takes an array of { offset, length } ranges and the server returns only those byte windows. For a token account you might want the owner (offset 32, 32 bytes) and the amount (offset 64, 8 bytes) and nothing else — 40 bytes instead of 165 on every update.
1const request: SubscribeRequest = {
2 accounts: {
3 usdc_token_accounts: {
4 account: [],
5 owner: ["TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"],
6 filters: [
7 { memcmp: { offset: 0, base58: USDC_MINT } },
8 { datasize: 165 },
9 ],
10 nonemptyTxnSignature: undefined,
11 },
12 },
13 // ship only the owner (offset 32, 32 bytes) and amount (offset 64, 8 bytes)
14 // instead of all 165 bytes of every token account
15 accountsDataSlice: [
16 { offset: 32, length: 32 },
17 { offset: 64, length: 8 },
18 ],
19 slots: {},
20 transactions: {},
21 transactionsStatus: {},
22 blocks: {},
23 blocksMeta: {},
24 entry: {},
25 commitment: CommitmentLevel.PROCESSED,
26};
Transaction filters
Entries under transactions select by the accounts a transaction touches and by two boolean flags:
- ·
accountInclude — the tx must reference at least one of these keys (a program id works here). - ·
accountExclude — drop the tx if it touches any of these. - ·
accountRequired — every listed key must be present, not just one. - ·
vote and failed — set both to false to drop the vote chatter and errored attempts most consumers ignore.
Streaming every successful transaction that hits a given program is the common case:
1const PUMP_FUN = "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P";
2
3const request: SubscribeRequest = {
4 accounts: {},
5 slots: {},
6 transactions: {
7 pump_fun_trades: {
8 vote: false, // skip vote transactions
9 failed: false, // skip transactions that errored
10 signature: undefined,
11 accountInclude: [PUMP_FUN], // must touch this program
12 accountExclude: [],
13 accountRequired: [], // every listed key must appear
14 },
15 },
16 transactionsStatus: {},
17 blocks: {},
18 blocksMeta: {},
19 entry: {},
20 accountsDataSlice: [],
21 commitment: CommitmentLevel.CONFIRMED,
22};
transactionsStatus vs transactions
Both maps take the same filter shape, but they answer different questions. transactions ships the whole transaction and its metadata — instructions, inner instructions, balances, logs. transactionsStatus ships the signature and whether it succeeded, and not much more. If you are confirming that a transaction you already know about landed, the status feed costs a sliver of the bandwidth.
1const request: SubscribeRequest = {
2 accounts: {},
3 slots: {},
4 transactions: {},
5 // same filter shape, a fraction of the payload — signature + status only
6 transactionsStatus: {
7 my_program_acks: {
8 vote: false,
9 failed: false,
10 signature: undefined,
11 accountInclude: [PUMP_FUN],
12 accountExclude: [],
13 accountRequired: [],
14 },
15 },
16 blocks: {},
17 blocksMeta: {},
18 entry: {},
19 accountsDataSlice: [],
20 commitment: CommitmentLevel.CONFIRMED,
21};
Staying inside fair-use
Narrow filters and a tight accountsDataSlice are how you keep volume sane on flat pricing. A full blocks subscription is the highest-bandwidth thing you can ask for — prefer blocksMeta or a filtered transactions feed unless you genuinely need every byte of every block. Operational limits and packs live on the product page.