All articles
Solana·March 22, 2026·6 min read

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:

  • getAccountInfo polling at 100 ms intervals: average 50-150 ms staleness, burst 200+ ms
  • WebSocket accountSubscribe via 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 slot you 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-grpc plugin 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. PROCESSED commitment 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 project
#Solana#Geyser#gRPC#Yellowstone#Trading Bots#Rust#TypeScript#Low Latency