NoLimitNodes
PricingDocsBlogAbout
SupportContact
Log in
Blog/Guides

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.

N
NoLimitNodes Engineering
Infrastructure Team
Jun 19, 202618 min read
On this page +
  • 01Get something running in 10 lines
  • 02What you're actually receiving
  • 03The 5 patterns that cover 90% of what you'll build
  • 04asyncio: the right way to run this in Python
  • 05Build the PumpFun detector
  • 06Keep it running at 3am
  • 07Full capability reference

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. If you still need an endpoint, compare the managed options on our Yellowstone gRPC nodespage before wiring this into a production bot.

Python client → Yellowstone → validator

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
Fig. 1: grpcio handles TLS and auth. Generated stubs translate your Python objects into protobuf on the wire.
quickstart.py
python
import grpc
import geyser_pb2, geyser_pb2_grpc

creds = grpc.composite_channel_credentials(
    grpc.ssl_channel_credentials(),
    grpc.metadata_call_credentials(
        lambda ctx, cb: cb([("x-token", "YOUR_TOKEN")], None)
    )
)
stub = geyser_pb2_grpc.GeyserStub(
    grpc.secure_channel("YOUR_ENDPOINT:10000", creds)
)
req = geyser_pb2.SubscribeRequest(
    slots={"s": geyser_pb2.SubscribeRequestFilterSlots()}
)
for update in stub.Subscribe(iter([req])):
    print(f"slot {update.slot.slot}  status {update.slot.status}")

If that printed slots, your connection works. Now set up properly.

Install dependencies

requirements.txt
text
# requirements.txt
grpcio==1.63.0
grpcio-tools==1.63.0
protobuf==5.26.1
base58==2.1.1
bash
pip install -r requirements.txt

Generate Python stubs

Download geyser.proto and solana-storage.proto from github.com/rpcpool/yellowstone-grpc into a ./proto directory, then:

compile-protos.sh
bash
python -m grpc_tools.protoc \
  -I./proto \
  --python_out=. \
  --grpc_python_out=. \
  ./proto/geyser.proto \
  ./proto/solana-storage.proto

This produces geyser_pb2.py and geyser_pb2_grpc.py. Every pattern below imports from these two files.

Reusable connection helper

client.py
python
import grpc
import geyser_pb2_grpc

def get_stub(endpoint: str, token: str) -> geyser_pb2_grpc.GeyserStub:
    creds = grpc.composite_channel_credentials(
        grpc.ssl_channel_credentials(),
        grpc.metadata_call_credentials(
            lambda ctx, cb: cb([("x-token", token)], None)
        )
    )
    return geyser_pb2_grpc.GeyserStub(
        grpc.secure_channel(endpoint, creds)
    )

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 shape

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
Fig. 2: One connection, one stream, multiple payload types. HasField() routes each update to the right handler.

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.

pattern1-wallet-watcher.py
python
import geyser_pb2
import base58

stub = get_stub(ENDPOINT, TOKEN)

req = geyser_pb2.SubscribeRequest(
    accounts={
        "wallet": geyser_pb2.SubscribeRequestFilterAccounts(
            account=["WALLET_PUBKEY_BASE58"]
        )
    },
    commitment=geyser_pb2.CommitmentLevel.CONFIRMED
)
for update in stub.Subscribe(iter([req])):
    if update.HasField("account"):
        acct = update.account.account
        pubkey = base58.b58encode(bytes(acct.pubkey)).decode()
        sol = acct.lamports / 1e9
        print(f"{pubkey}: {sol:.6f} SOL  (slot {update.account.slot})")

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.

pattern2-program-accounts.py
python
req = geyser_pb2.SubscribeRequest(
    accounts={
        "protocol": geyser_pb2.SubscribeRequestFilterAccounts(
            owner=["PROGRAM_ID_BASE58"]
        )
    },
    commitment=geyser_pb2.CommitmentLevel.CONFIRMED
)
for update in stub.Subscribe(iter([req])):
    if update.HasField("account"):
        acct = update.account.account
        pubkey = base58.b58encode(bytes(acct.pubkey)).decode()
        print(f"account {pubkey} updated  data_len={len(acct.data)}")

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. For decoded DEX events instead of raw transactions, see the Enhanced Streams catalog.

pattern3-program-transactions.py
python
req = geyser_pb2.SubscribeRequest(
    transactions={
        "prog_txns": geyser_pb2.SubscribeRequestFilterTransactions(
            vote=False,
            failed=False,
            account_include=["PROGRAM_ID_BASE58"]
        )
    },
    commitment=geyser_pb2.CommitmentLevel.CONFIRMED
)
for update in stub.Subscribe(iter([req])):
    if update.HasField("transaction"):
        tx = update.transaction.transaction
        sig = base58.b58encode(bytes(tx.signature)).decode()
        slot = update.transaction.slot
        print(f"tx {sig}  slot {slot}")

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.

pattern4-memcmp.py
python
# Anchor discriminator for Raydium AMM v4 PoolState
# sha256("account:PoolState")[:8] -> base58
POOL_STATE_DISCRIMINATOR_B58 = "Q6XprfkF8RQQKoQVG33xT88H7wi8uy6mZpvKA6BSCym"

req = geyser_pb2.SubscribeRequest(
    accounts={
        "pools": geyser_pb2.SubscribeRequestFilterAccounts(
            owner=["RAYDIUM_PROGRAM_ID"],
            filters=[
                geyser_pb2.SubscribeRequestFilterAccountsFilter(
                    memcmp=geyser_pb2.SubscribeRequestFilterAccountsFilterMemcmp(
                        offset=0,
                        base58=POOL_STATE_DISCRIMINATOR_B58
                    )
                )
            ]
        )
    }
)

Compute the discriminator for any Anchor account type with:

discriminator.py
python
import hashlib

def account_discriminator(name: str) -> bytes:
    return hashlib.sha256(f"account:{name}".encode()).digest()[:8]

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.

pattern5-combined.py
python
req = geyser_pb2.SubscribeRequest(
    accounts={
        "vault": geyser_pb2.SubscribeRequestFilterAccounts(
            account=["VAULT_PUBKEY_BASE58"]
        )
    },
    transactions={
        "protocol": geyser_pb2.SubscribeRequestFilterTransactions(
            vote=False,
            failed=False,
            account_include=["PROTOCOL_PROGRAM_ID_BASE58"]
        )
    },
    commitment=geyser_pb2.CommitmentLevel.CONFIRMED
)
for update in stub.Subscribe(iter([req])):
    if update.HasField("account"):
        print(f"vault balance: {update.account.account.lamports / 1e9:.4f} SOL")
    elif update.HasField("transaction"):
        sig = base58.b58encode(
            bytes(update.transaction.transaction.signature)
        ).decode()
        print(f"protocol tx: {sig}")

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.

asyncio-stream.py
python
import asyncio
import grpc
from grpc import aio
import geyser_pb2, geyser_pb2_grpc

async def stream_slots(endpoint: str, token: str):
    creds = grpc.composite_channel_credentials(
        grpc.ssl_channel_credentials(),
        grpc.metadata_call_credentials(
            lambda ctx, cb: cb([("x-token", token)], None)
        )
    )
    async with aio.secure_channel(endpoint, creds) as channel:
        stub = geyser_pb2_grpc.GeyserStub(channel)
        req = geyser_pb2.SubscribeRequest(
            slots={"s": geyser_pb2.SubscribeRequestFilterSlots()}
        )
        async for update in stub.Subscribe(iter([req])):
            if update.HasField("slot"):
                print(f"slot {update.slot.slot}")

asyncio.run(stream_slots(ENDPOINT, TOKEN))

Run multiple subscriptions concurrently with asyncio.gather():

asyncio-gather.py
python
async def main():
    await asyncio.gather(
        stream_slots(ENDPOINT, TOKEN),
        watch_wallet(ENDPOINT, TOKEN, "WALLET_PUBKEY"),
    )

asyncio.run(main())

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"):

discriminator.py
python
import hashlib

def ix_discriminator(name: str) -> bytes:
    return hashlib.sha256(f"global:{name}".encode()).digest()[:8]

CREATE_IX = ix_discriminator("create")

Full working detector:

pumpfun-detector.py
python
import asyncio
import hashlib
import base58
import grpc
from grpc import aio
import geyser_pb2, geyser_pb2_grpc

PUMPFUN_PROGRAM = "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"
CREATE_IX = hashlib.sha256(b"global:create").digest()[:8]

async def detect_launches(endpoint: str, token: str):
    creds = grpc.composite_channel_credentials(
        grpc.ssl_channel_credentials(),
        grpc.metadata_call_credentials(
            lambda ctx, cb: cb([("x-token", token)], None)
        )
    )
    req = geyser_pb2.SubscribeRequest(
        transactions={
            "pumpfun": geyser_pb2.SubscribeRequestFilterTransactions(
                vote=False,
                failed=False,
                account_include=[PUMPFUN_PROGRAM]
            )
        },
        commitment=geyser_pb2.CommitmentLevel.CONFIRMED
    )
    async with aio.secure_channel(endpoint, creds) as channel:
        stub = geyser_pb2_grpc.GeyserStub(channel)
        async for update in stub.Subscribe(iter([req])):
            if not update.HasField("transaction"):
                continue
            tx = update.transaction.transaction
            for ix in tx.message.instructions:
                data = bytes(ix.data)
                if len(data) < 8 or data[:8] != CREATE_IX:
                    continue
                keys = tx.message.account_keys
                mint = base58.b58encode(bytes(keys[0])).decode()
                sig  = base58.b58encode(bytes(tx.signature)).decode()
                print(f"new token : {mint}")
                print(f"signature : {sig}")
                print(f"slot      : {update.transaction.slot}")
                print()

asyncio.run(detect_launches(ENDPOINT, TOKEN))
NOTE / The decoder you'd rather skip
What you just wrote is an instruction decoder. It works. It will also break the next time PumpFun modifies its account layout or instruction structure, quietly, with no error in your logs. At NoLimitNodes, we maintain decoders for 37 programs including PumpFun. A PumpFunTrade event arrives at your Python consumer already typed with named fields, no discriminator parsing required.

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.

silent connection death

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
Fig. 3: No keepalive ping means the load balancer closes the connection with no exception raised in your loop.

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.

keepalive-options.py
python
KEEPALIVE_OPTIONS = [
    ("grpc.keepalive_time_ms", 30_000),
    ("grpc.keepalive_timeout_ms", 10_000),
    ("grpc.keepalive_permit_without_calls", True),
    ("grpc.http2.max_pings_without_data", 0),
    ("grpc.http2.min_time_between_pings_ms", 30_000),
]

async with aio.secure_channel(endpoint, creds, options=KEEPALIVE_OPTIONS) as channel:
    ...

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. Provider behavior differs here; we break down the operational tradeoffs in ourYellowstone gRPC providers comparison.

subscribe-with-retry.py
python
async def subscribe_with_retry(endpoint: str, token: str, request, handler):
    delay = 1
    while True:
        try:
            creds = grpc.composite_channel_credentials(
                grpc.ssl_channel_credentials(),
                grpc.metadata_call_credentials(
                    lambda ctx, cb: cb([("x-token", token)], None)
                )
            )
            async with aio.secure_channel(
                endpoint, creds, options=KEEPALIVE_OPTIONS
            ) as channel:
                stub = geyser_pb2_grpc.GeyserStub(channel)
                delay = 1  # reset on successful connection
                async for update in stub.Subscribe(iter([request])):
                    await handler(update)
        except grpc.RpcError as e:
            print(f"stream error ({e.code()}), reconnecting in {delay}s")
            await asyncio.sleep(delay)
            delay = min(delay * 2, 60)
        except Exception as e:
            print(f"unexpected error: {e}, reconnecting in {delay}s")
            await asyncio.sleep(delay)
            delay = min(delay * 2, 60)

Use it by passing your SubscribeRequest and an async handler:

retry-usage.py
python
async def handle(update):
    if update.HasField("transaction"):
        sig = base58.b58encode(
            bytes(update.transaction.transaction.signature)
        ).decode()
        print(f"tx: {sig}")

asyncio.run(
    subscribe_with_retry(ENDPOINT, TOKEN, your_request, handle)
)
TIP / Backoff behaviour
This pattern runs indefinitely. It reconnects on any error, backs off up to 60 seconds between retries, and resets the backoff on any successful message so a brief blip doesn't permanently slow your reconnect speed.

07Full capability reference#

SubscriptionSubscribeRequest fieldKey filter optionsUse case
Account updatesaccountsaccount (pubkeys), owner (program), filters (memcmp, datasize)Wallet watcher, protocol monitor, liquidation trigger
Transactionstransactionsvote, failed, account_include, account_exclude, account_requiredDEX monitor, MEV searcher, event indexer
Slot statusslotsfilter_by_commitmentBlock timing, fork detection, slot tracking
Block metadatablocks_metaaccount_includeBlock-level indexer, rewards tracking
Full blocksblocksinclude_transactions, include_accounts, include_entriesArchive node, full block indexer
PoH entriesentry(none)Latency measurement, entry-level analysis
Account data sliceaccount_data_sliceoffset, lengthRead partial account data without full state
Commitment levelcommitment (request-level)PROCESSED, CONFIRMED, FINALIZEDApplies to all filters in the request
Table 1: Everything Yellowstone can stream. If a pattern above covers your use case, you don't need this table.

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.

///Related resources
Yellowstone gRPC nodes

Managed gRPC endpoints for low-latency Solana streams.

Enhanced Streams

Decoded Solana events for bots, dashboards, and analytics.

PumpFun data

Launches, trades, graduations, and creator-wallet signals.

Solana RPC nodes

Private Solana RPC endpoints for builders and trading systems.

#yellowstone#grpc#python#solana#pumpfun#asyncio#streaming
N
NoLimitNodes Engineering
Infrastructure Team

The team that runs our RPC, WebSocket, gRPC, and streaming fleet. We write about what we operate: validators, Geyser pipelines, and the request paths in between.

On this page
  • 01Get something running in 10 lines
  • 02What you're actually receiving
  • 03The 5 patterns that cover 90% of what you'll build
  • 04asyncio: the right way to run this in Python
  • 05Build the PumpFun detector
  • 06Keep it running at 3am
  • 07Full capability reference
↑ back to top
///Read next
GuidesMay 19, 2026

Yellowstone gRPC vs WebSockets: choosing a real-time Solana data pipeline

Both transports stream the same chain, but they come from different places inside the validator and fail in different ways. A field guide to choosing and operating the right pipeline for your workload.

#yellowstone#grpc#websocket
16 min read
GuidesMay 15, 2025

Solana Meme Coin Trading with NoLimitNodes API: Ride the Solana Pump Fun with Price Alerts and Token Launch Insights

Solana meme coin trading rewards the people who see the data first. Charts alone won't get you there.

#solana#pumpfun#trading
5 min read
← Older
Yellowstone gRPC Providers Compared (2026): Latency, Decoded Streams & What Nobody Tells You Before You Go Live
Run it yourself

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.

See pricing

Ready to get started?

Get your free API key and start building in under 30 seconds.

Talk to Sales
NoLimitNodes

Solana RPC infrastructure built for performance and scale.

RPC Access
  • HTTP RPC
  • WebSocket
  • gRPC
Infrastructure
  • Compute Platform
  • VPS
  • VDS
  • Bare Metal
  • Geyser Plugin Hosting
Enhanced Streams
  • PumpFun
  • PumpSwap
  • Raydium
  • Orca
  • Meteora
  • System Events
  • Browse All →
Program Streams
  • PumpFun
  • PumpSwap
  • Raydium CLMM
  • Orca Whirlpool
  • Meteora DLMM
  • Jupiter Swap
  • Jupiter Perps
  • Kamino Lending
  • Browse All 37 →
Trading
  • EZWallet
Analytics
  • Historical Datasets
  • Historical Raw Blocks
Company
  • About
Resources
  • Pricing
  • Custom Development
  • Documentation
  • Blog
  • Support
  • Contact Sales
Compare
  • Yellowstone gRPC vs LaserStream
  • Triton vs Helius
  • Raydium API vs Helius
  • PumpSwap API vs Bitquery
  • QuickNode Streams vs NLN
  • All comparisons →
Legal
  • Terms & Conditions
  • Privacy Policy
© 2026 CLR3 Inc., operating as NoLimitNodes. Registered in Ontario, Canada. All rights reserved.solana mainnet