Yellowstone gRPC in Python (2026): Setup, 5 Core Patterns & a Real-Time PumpFun Detector
A complete Python guide to Yellowstone gRPC: proto generation, a reusable auth helper, five working patterns from wallet watcher to memcmp filter, a full PumpFun token-launch detector, and production reconnect with exponential backoff.
On this page +
Every Yellowstone gRPC tutorial online uses TypeScript or Rust, and the Python experience has been worse: find a GitHub repo that skips proto generation entirely, spend an afternoon on setup, then debug a stream that dies silently at 90 seconds with no obvious reason why. This covers what those tutorials don't: proto generation, the five subscription patterns that actually matter, keepalive, reconnect logic, and a working PumpFun token detector as the capstone.
01Get something running in 10 lines#
Before setup instructions, here's a working slot stream. Copy it, point it at your endpoint, and watch Solana slots print in your terminal.
your Python app (asyncio)
│
▼
grpcio channel (TLS + token auth)
│
▼
Yellowstone node ──▶ Solana validator Geyser callback (~µs)
│
generated stubs: geyser_pb2.py, geyser_pb2_grpc.py
If that printed slots, your connection works. Now set up properly.
Install dependencies
Generate Python stubs
Download geyser.proto and solana-storage.proto from github.com/rpcpool/yellowstone-grpc into a ./proto directory, then:
This produces geyser_pb2.py and geyser_pb2_grpc.py. Every pattern below imports from these two files.
Reusable connection helper
Set ENDPOINT = "YOUR_ENDPOINT:10000" and TOKEN = "YOUR_TOKEN" at the top of your scripts and every example below runs without modification.
02What you're actually receiving#
Every message from stub.Subscribe() is a SubscribeUpdate. It carries exactly one payload depending on what fired. Check HasField() to know which one arrived before reading it.
SubscribeUpdate (one per message): │ ├── .account AccountInfo (pubkey, lamports, data, owner, slot) ├── .transaction TransactionInfo (signature, slot, meta, message) ├── .slot SlotInfo (slot number, parent, status) ├── .block BlockInfo (blockhash, rewards, transactions) ├── .block_meta BlockMetaInfo (slot, blockhash, block_time) ├── .entry EntryInfo (slot, index, hash, num_hashes) └── .ping / .pong keepalive signals
Commitment lives at the request level, not per filter. One commitment level applies to all filters in a single SubscribeRequest. If you need mixed commitment levels, open two connections.
03The 5 patterns that cover 90% of what you'll build#
Pattern 1: Watch a specific wallet
Subscribe to the exact pubkey. Every lamport change fires an update.
Pattern 2: Track all accounts owned by a program
Subscribe by owner instead of by address to receive every state change across an entire protocol. This fires for every account whose owner field matches. On a busy program that's hundreds of updates per slot. Use a memcmp filter (Pattern 4) to narrow to specific account types.
Pattern 3: Stream transactions for a specific program
account_include returns transactions where the program ID appears anywhere in the account list. Set failed=True if you want reverted transactions — failed sandwich attempts are signals in MEV analysis.
Pattern 4: memcmp filter — match accounts by data pattern
The first 8 bytes of each Anchor account's data are the discriminator, unique per account type. Filter on those bytes to subscribe only to the account type you want.
Compute the discriminator for any Anchor account type with:
Pattern 5: Combined subscription — accounts and transactions on one connection
One stream, two filters, no second connection. Check HasField to route each update. This is how most production bots run: one persistent connection carrying multiple subscriptions, one event loop routing updates to handlers by type.
04asyncio: the right way to run this in Python#
The synchronous stub.Subscribe() iterator blocks the calling thread. For a quick script that's fine. For anything that also submits transactions or handles signals concurrently, it isn't. We learned this the hard way: the first time we ran a synchronous subscriber alongside a transaction sender in the same process, the sender started missing slots intermittently. We spent two hours on signing latency, serialization, and network jitter before we thought to look at the subscriber. It turned out the sender was blocking for 400ms every time the iterator yielded a large update. Moving to grpc.aio fixed it in ten minutes. We should have started there.
Use grpc.aio for async-native streaming. The API is identical: only the channel creation and iteration change.
Run multiple subscriptions concurrently with asyncio.gather():
05Build the PumpFun detector#
PumpFun creates a new token every time someone calls its create instruction. That instruction has a fixed 8-byte Anchor discriminator. Subscribe to all PumpFun transactions, check each instruction's first 8 bytes, and when they match the create discriminator, you've seen a new token before most of the market.
The discriminator for any Anchor instruction is the first 8 bytes of SHA256("global:instruction_name"):
Full working detector:
06Keep it running at 3am#
Two things end production gRPC streams. Neither throws an exception you can catch at the point of failure.
The dead connection
Cloud load balancers close idle gRPC connections after 60 to 90 seconds. The stream dies silently. Your async for loop hangs. No RpcError, no timeout, just no more updates.
client sends nothing for 60+ seconds
│
▼
load balancer closes the connection
│
▼
stream silently dead
no exception raised in your loop
async for hangs waiting for a message
that will never arrive
Fix it with gRPC channel keepalive options set at connection time. This tells the gRPC layer to ping the server every 30 seconds and close the connection if no pong arrives within 10 seconds — the exception then propagates into your loop and you can reconnect cleanly.
The dropped stream with no recovery
Keepalive catches dead connections. It doesn't catch server restarts, network blips, or your provider cycling nodes. Wrap the entire subscription in a retry loop with exponential backoff.
Use it by passing your SubscribeRequest and an async handler:
07Full capability reference#
Commitment notes. PROCESSED is the earliest signal — 5 to 10% of processed slots never get confirmed, so your consumer may act on state that gets rolled back. CONFIRMED is the default for production: supermajority vote, rare rollbacks, the right tradeoff for most bots and indexers. FINALIZED is permanent but adds 32+ seconds of latency.
account_include vs account_required. account_include returns transactions touching any of the listed accounts. account_required returns only transactions touching all of them. Use account_required when you need a transaction to involve two specific parties simultaneously — a wallet interacting with a specific pool, not just any transaction touching either one.
The code above runs against any standard Yellowstone gRPC endpoint, including ours. If you're building on PumpFun, Raydium, Jupiter, or any of the other 37 programs we decode, the free account at app.nolimitnodes.com is the fastest way to skip the decoder step and get typed events directly in your Python consumer. Not sure what your workload needs? Talk to an engineer before you write a decoder you'll maintain for the next two years.
Every benchmark in this blog runs against our public endpoints.
Spin up an RPC, WebSocket, or gRPC endpoint in under a minute. Flat pricing, no request caps. Reproduce the numbers for your own workload.