Geyser Yellowstone gRPC: Real-Time Account Feeds for Solana Bots
Polling RPC nodes for account updates introduces 50-200 ms of unnecessary latency compared to streaming directly from a Geyser-compatible plugin like Yellowstone. This tutorial walks through subscribing to transaction and account streams over gRPC, filtering by program ID, and parsing Protobuf messages inside a Rust or TypeScript bot.
Geyser Yellowstone gRPC gives your Solana bot a direct firehose into the validator's account update pipeline — no polling, no cache misses, no RPC overhead. If you are still calling getAccountInfo on a timer or leaning on onAccountChange websockets, you are leaving 50-200 ms on the table every single slot, and in competitive on-chain environments that lag costs you fills, copy-trade opportunities, and arb edges.
What the Geyser Plugin Interface Actually Is
Solana validators expose an internal plugin interface called Geyser. When a plugin is loaded, the validator calls it synchronously on every account write, transaction, slot update, and block notification — before those events are even available to the RPC layer. Yellowstone gRPC (maintained by Triton One) is the most widely deployed implementation: it wraps that plugin interface in a persistent gRPC stream you can connect to from any language with a Protobuf client.
The protocol is defined in a single .proto file (geyser.proto). The key service is Geyser with a single bidirectional streaming RPC called Subscribe. You send a SubscribeRequest with your filters, and the server streams back SubscribeUpdate messages — which can be account updates, transactions, slots, blocks, or pings — in real time.
Connecting and Filtering by Program ID
To subscribe to all account changes owned by a specific program (say, Raydium AMM v4 or Pump.fun's bonding curve program), you construct a SubscribeRequestFilterAccounts map entry with the owner field set to the program's public key.
Rust example using yellowstone-grpc-client:
use yellowstone_grpc_client::GeyserGrpcClient;
use yellowstone_grpc_proto::prelude::*;
use std::collections::HashMap;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let mut client = GeyserGrpcClient::connect(
"https://your-yellowstone-endpoint:10000",
Some("your-x-token"),
None,
).await?;
let mut accounts_filter: HashMap<String, SubscribeRequestFilterAccounts> = HashMap::new();
accounts_filter.insert("pump_accounts".to_string(), SubscribeRequestFilterAccounts {
account: vec![],
owner: vec!["6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P".to_string()], // Pump.fun program
filters: vec![],
});
let request = SubscribeRequest {
accounts: accounts_filter,
transactions: HashMap::new(),
slots: HashMap::new(),
..Default::default()
};
let (mut _sink, mut stream) = client.subscribe_with_request(Some(request)).await?;
while let Some(msg) = stream.next().await {
match msg?.update_oneof {
Some(subscribe_update::UpdateOneof::Account(acc_update)) => {
// acc_update.account contains pubkey, lamports, data, owner, slot
println!("Account updated: slot {}", acc_update.slot);
}
_ => {}
}
}
Ok(())
}
For transaction filtering (e.g., to catch every swap on a specific DEX), use SubscribeRequestFilterTransactions with account_include set to the AMM address. You can combine account and transaction filters in a single subscription — the server multiplexes them onto one stream.
Parsing Protobuf Account Data
The raw data field in an account update is just bytes — the same on-chain data layout you would get from getAccountInfo. You still need to deserialize it against your program's IDL or account struct.
In Rust with Anchor, this is straightforward:
use anchor_lang::AccountDeserialize;
let mut data: &[u8] = &acc_update.account.unwrap().data;
// Skip the 8-byte Anchor discriminator
data = &data[8..];
let pool_state = PoolState::try_deserialize(&mut data)?;
In TypeScript with @coral-xyz/anchor, you pass the raw Buffer to program.coder.accounts.decode("PoolState", data.slice(8)). Either way, once you have the struct, your bot logic runs on fresh state — not state that arrived via RPC two slots later.
One critical detail: Yellowstone sends updates at the account-write granularity, which can be multiple times per transaction when a program writes multiple accounts. Filter by slot and deduplicate if you are aggregating across a transaction.
TypeScript Subscription Boilerplate
The @triton-one/yellowstone-grpc npm package ships pre-generated TypeScript clients:
import Client, { CommitmentLevel } from "@triton-one/yellowstone-grpc";
const client = new Client("https://your-endpoint:10000", "your-x-token");
const stream = await client.subscribe();
stream.on("data", (chunk) => {
if (chunk.account) {
const { pubkey, data, slot } = chunk.account.account;
// pubkey and data are Uint8Array — decode as needed
processAccountUpdate(pubkey, Buffer.from(data), slot);
}
});
await new Promise((resolve, reject) => {
stream.write({
accounts: {
myFilter: {
account: [],
owner: ["RVKd61ztZW9GUwhRbbLoYVRE5Xf1B2tVscKqwZqXgEr"], // Raydium v4
filters: [],
},
},
transactions: {},
slots: {},
commitment: CommitmentLevel.PROCESSED,
}, resolve);
});
Set commitment to PROCESSED for the fastest updates. CONFIRMED and FINALIZED add one and 31+ slots of latency respectively — only use them if your strategy requires finality guarantees.
Latency Reality and Where Geyser Fits
In production, a Geyser stream on a well-connected Triton One or self-hosted node delivers account updates in 5-30 ms after the slot leader processes the transaction. Compare that to:
getAccountInfopolling at 100 ms intervals: average 50-150 ms staleness, burst 200+ ms- WebSocket
accountSubscribevia standard RPC: typically 40-100 ms, with connection instability under load - Geyser gRPC (Yellowstone): 5-30 ms, persistent stream, server-side filtering before bytes hit the wire
The difference compounds on strategies where you react to state changes — copytrading bots, snipers, and any logic keying on pool reserve ratios. A 100 ms delay versus a 10 ms delay is the difference between landing in the same block and chasing a move that already printed.
For the full infrastructure picture — RPC selection, co-location, and custom indexers that sit above the Geyser layer — the infra and data pipelines service covers how these pieces connect in a production trading stack.
Operational Concerns and Trade-offs
A few things that burn people in production:
- Backpressure. The server sends updates as fast as the chain produces them. If your consumer is slow (heavy deserialization, blocking I/O), the stream buffer fills and you start dropping messages. Use a bounded channel between the stream reader and your processing logic; if the channel is full, log and skip — don't block the reader.
- Reconnect logic. gRPC streams drop. Yellowstone endpoints can restart. Build exponential backoff with jitter and re-subscribe on every reconnect. Track the last
slotyou processed and discard any updates below it after reconnect. - Endpoint access. Public Yellowstone endpoints are rate-limited and unreliable for production use. For serious bots, run your own
yellowstone-grpcplugin on a private validator node or use a paid Triton One subscription. The cost is meaningless compared to the edge you lose on a flaky public endpoint. - Slot vs. confirmation gap.
PROCESSEDcommitment means the validator applied the transaction, but it could still be rolled back in a fork. For strategies that take action on account state, consider waiting for one confirmation if the cost of acting on a forked state exceeds the latency benefit.
Geyser is not a silver bullet — it is the right tool for latency-sensitive reactions to on-chain state. If your strategy operates on confirmed data or aggregates across many accounts, a custom indexer built on top of Geyser (writing to Postgres or Redis) often makes more sense than reacting to the raw stream directly. The Solana sniper bot architecture we ship uses Geyser for detection and a lightweight in-memory state cache so the execution path touches zero RPC calls.
If you want a production-grade Geyser integration built into your Solana bot — whether that is a sniper, a copy-trader, or a custom indexer — get in touch and we can scope it out.
Need a bot like this built?
We design, build and run trading bots on Solana, Hyperliquid and Polymarket.
Start a projectMore from the blog
Public vs Paid RPC: How to Run a Fair Benchmark
A fair comparison of public and paid RPC endpoints must account for quotas, workload, freshness and support guarantees.
Read articleRPC Benchmark Percentiles Explained: p50, p95 and p99
Why averages hide the latency spikes that break trading bots, wallets and production blockchain applications.
Read articleHyperliquid REST vs WebSocket Benchmark: What to Measure
A benchmark plan for Hyperliquid market data that separates request latency from streaming freshness and recovery.
Read article