
Yellowstone Grpc
- 197 installs
- 257 repo stars
- Updated June 24, 2026
- agiprolabs/claude-trading-skills
yellowstone-grpc is a Claude Code skill that streams real-time Solana transactions, accounts, slots, and blocks over Yellowstone gRPC (a Geyser plugin) at low latency.
About
A Claude Code skill for streaming Solana data in real time using Yellowstone gRPC, a Geyser plugin exposed by RPC providers. It shows how to connect an authenticated gRPC channel and subscribe to filtered streams of transactions, accounts, slots, and blocks at roughly 5ms slot latency in Python, Rust, or TypeScript. Developers use it to build latency-sensitive Solana trading systems instead of REST polling.
- Streams Solana transactions and account updates in real time at ~5ms slot latency
- Subscribes to filtered gRPC streams via the Yellowstone Geyser plugin
- Python, Rust, and TypeScript setup with provider comparison
Yellowstone Grpc by the numbers
- 197 all-time installs (skills.sh)
- Ranked #476 of 1,106 Finance & Trading skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
yellowstone-grpc capabilities & compatibility
Needs a paid gRPC provider; entry prices cited from Chainstack $49/mo to Triton One ~$2,900/mo, with Shyft $199/mo noted as best value
- Capabilities
- solana streaming · grpc subscription · dex monitoring · copy trading feed · whale tracking
- Use cases
- data analysis · trading · orchestration
- Pricing
- Bring your own API key
What yellowstone-grpc says it does
Stream every transaction, account update, slot, and block on Solana in real-time using Yellowstone gRPC.
This is the foundation for any latency-sensitive Solana trading system — replacing REST polling with push-based streaming at ~5ms slot latency.
Yellowstone is a Geyser plugin that exposes Solana validator data over gRPC.
npx skills add https://github.com/agiprolabs/claude-trading-skills --skill yellowstone-grpcAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 197 |
|---|---|
| repo stars | ★ 257 |
| Last updated | June 24, 2026 |
| Repository | agiprolabs/claude-trading-skills ↗ |
What it does
Stream filtered Solana transactions and account updates in real time via Yellowstone gRPC for a trading system.
Who is it for?
Building latency-sensitive Solana trading systems on a push-based real-time data feed.
Skip if: Historical lookups or non-Solana chains.
When should I use this skill?
You need real-time streaming of Solana transactions or account changes instead of REST polling.
What you get
A working low-latency gRPC subscription to filtered Solana streams.
- An authenticated gRPC connection with filtered subscriptions to Solana streams
By the numbers
- ~5ms slot latency (p90) vs ~150ms+ for REST polling
- 7 subscription types (transactions, accounts, slots, blocks, blocks_meta, entry, transactions_status)
Files
Yellowstone gRPC — Real-Time Solana Streaming
Stream every transaction, account update, slot, and block on Solana in real-time using Yellowstone gRPC. This is the foundation for any latency-sensitive Solana trading system — replacing REST polling with push-based streaming at ~5ms slot latency.
Why Yellowstone gRPC
| Method | Slot Latency (p90) | Use Case |
|---|---|---|
REST polling (getTransaction) | ~150ms+ | Historical lookups |
WebSocket (onLogs) | ~10ms | Simple notifications |
| Yellowstone gRPC | ~5ms | Production trading systems |
Yellowstone is a Geyser plugin that exposes Solana validator data over gRPC. Every major RPC provider runs it. You subscribe to filtered streams of transactions, account changes, slots, blocks, and entries — and the data pushes to you.
Quick Start
1. Get Access
You need a gRPC-enabled RPC provider. See references/providers.md for full comparison.
| Provider | gRPC Entry Price | Notes |
|---|---|---|
| Shyft | $199/mo | Best value, 7 regions, unlimited bandwidth |
| Helius | $999/mo | LaserStream, DAS APIs included |
| Triton One | ~$2,900/mo | Created Yellowstone, lowest latency |
| QuickNode | Plan-dependent | Marketplace add-on |
| Chainstack | $49/mo (1 stream) | Budget option, limited filters |
| Alchemy | Free tier available | Compute-unit metered |
2. Install Dependencies
# Python
uv pip install grpcio grpcio-tools protobuf base58 solders python-dotenv
# Generate Python stubs from proto files
git clone https://github.com/rpcpool/yellowstone-grpc.git
python -m grpc_tools.protoc \
-I./yellowstone-grpc/yellowstone-grpc-proto/proto/ \
--python_out=./generated \
--pyi_out=./generated \
--grpc_python_out=./generated \
./yellowstone-grpc/yellowstone-grpc-proto/proto/*.proto# Rust — Cargo.toml
[dependencies]
yellowstone-grpc-client = "6.0.0"
yellowstone-grpc-proto = "6.0.0"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
futures = "0.3"
bs58 = "0.5"# TypeScript
npm install @triton-one/yellowstone-grpc @solana/web3.js3. Environment Setup
export GRPC_ENDPOINT="https://grpc.ny.shyft.to" # your provider endpoint
export GRPC_TOKEN="your-x-token-here" # from provider dashboard4. Connect and Subscribe
import grpc
import os
from generated import geyser_pb2, geyser_pb2_grpc
endpoint = os.environ["GRPC_ENDPOINT"].replace("https://", "")
token = os.environ["GRPC_TOKEN"]
# Authenticated TLS channel
auth_creds = grpc.metadata_call_credentials(
lambda ctx, cb: cb((("x-token", token),), None)
)
channel = grpc.secure_channel(
endpoint,
grpc.composite_channel_credentials(
grpc.ssl_channel_credentials(), auth_creds
),
options=[("grpc.max_receive_message_length", 64 * 1024 * 1024)],
)
stub = geyser_pb2_grpc.GeyserStub(channel)Core Concepts
Subscription Types
| Type | What You Get | Use Case |
|---|---|---|
transactions | Full transaction with metadata | DEX swap monitoring, copy trading |
accounts | Account data on change | Pool reserve tracking, token supply |
slots | Slot progression events | Block timing, confirmation tracking |
blocks | Full block contents | Block-level analysis |
blocks_meta | Block metadata only | Lightweight block tracking |
entry | Block entries (shred groups) | Low-level validator data |
transactions_status | Tx status without full data | Lightweight confirmation |
Filter Logic
- Multiple filter types (transactions + accounts) = AND — you get updates matching any type
- Values within arrays (multiple addresses in
account_include) = OR - Named filters let you distinguish which filter matched in the response
- Sending a new
SubscribeRequestreplaces all previous filters
Commitment Levels
| Level | Speed | Safety | Use For |
|---|---|---|---|
PROCESSED | Fastest | May be rolled back | Time-critical signals |
CONFIRMED | ~400ms slower | Supermajority voted | Most trading use cases |
FINALIZED | ~6-12s slower | Irreversible | Settlement verification |
Common Subscription Patterns
Watch All Swaps on a DEX Program
# Filter: all non-vote, non-failed transactions involving PumpFun
request = geyser_pb2.SubscribeRequest(
transactions={
"pumpfun": geyser_pb2.SubscribeRequestFilterTransactions(
account_include=["6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"],
vote=False,
failed=False,
)
},
commitment=geyser_pb2.CommitmentLevel.PROCESSED,
)Track Specific Wallets
request = geyser_pb2.SubscribeRequest(
transactions={
"whales": geyser_pb2.SubscribeRequestFilterTransactions(
account_include=[
"WalletAddress1...",
"WalletAddress2...",
],
vote=False,
failed=False,
)
},
commitment=geyser_pb2.CommitmentLevel.CONFIRMED,
)Monitor Pool Reserves (Account Subscription)
request = geyser_pb2.SubscribeRequest(
accounts={
"raydium_pools": geyser_pb2.SubscribeRequestFilterAccounts(
account=["PoolAddress1...", "PoolAddress2..."],
)
},
commitment=geyser_pb2.CommitmentLevel.PROCESSED,
)Reduce Bandwidth with Data Slicing
# Only get the first 40 bytes of account data (e.g., just the discriminator + key fields)
request = geyser_pb2.SubscribeRequest(
accounts={
"token_accounts": geyser_pb2.SubscribeRequestFilterAccounts(
owner=["TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"],
filters=[
geyser_pb2.SubscribeRequestFilterAccountsFilter(
token_account_state=True
)
],
)
},
accounts_data_slice=[
geyser_pb2.SubscribeRequestAccountsDataSlice(offset=0, length=40)
],
)Parsing Transaction Updates
When you receive a SubscribeUpdateTransaction, extract:
for update in stream:
if update.HasField("transaction"):
tx = update.transaction
info = tx.transaction
sig = base58.b58encode(info.signature).decode()
slot = tx.slot
msg = info.transaction.message
account_keys = [base58.b58encode(k).decode() for k in msg.account_keys]
# Instructions
for ix in msg.instructions:
program = account_keys[ix.program_id_index]
accounts = [account_keys[i] for i in ix.accounts]
data = ix.data # bytes — decode per program IDL
# Token balance changes (post-execution)
meta = info.meta
for tb in meta.post_token_balances:
mint = tb.mint
owner = tb.owner
amount = tb.ui_token_amount.ui_amountSee references/proto_reference.md for complete field documentation.
Production Architecture
[gRPC Stream] → [Bounded Channel] → [Processing Workers]
(1K-100K cap) ├─ Parse instructions
├─ Update state / DB
└─ Trigger actionsCritical patterns:
- Decouple I/O from processing — never block the gRPC stream
- Reconnect with exponential backoff (100ms → 60s cap)
- Use
from_slotto resume after disconnection (subtract ~32 slots for reorg safety) - Ping every 15-30 seconds to keep connection alive
- Filter
vote: falsealways — vote transactions are ~70% of all traffic - Set
max_receive_message_lengthto 64MB+ (default 4MB is too small)
See references/performance.md for full production checklist.
Key Program IDs for Trading
| Program | Address | What It Does |
|---|---|---|
| PumpFun | 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P | Token launches, bonding curve trades |
| PumpSwap | PSwapMdSai8tjrEXcxFeQth87xC4rRsa4VA5mhGhXkP | PumpFun graduated token swaps |
| Raydium AMM | 675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8 | Legacy AMM swaps |
| Raydium CLMM | CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK | Concentrated liquidity |
| Raydium CPMM | CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C | Constant product MM |
| Orca Whirlpool | whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc | Concentrated liquidity |
| Meteora DLMM | LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo | Dynamic liquidity MM |
| Jupiter V6 | JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 | Swap aggregator |
| Token Program | TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA | SPL token operations |
Files
References
references/providers.md— Provider comparison: endpoints, pricing, auth, featuresreferences/subscription_filters.md— Complete filter reference with examples for every filter typereferences/proto_reference.md— Key protobuf message definitions and field documentationreferences/performance.md— Connection management, reconnection, backpressure, production checklist
Scripts
scripts/subscribe_transactions.py— Stream and parse transactions filtered by program IDscripts/monitor_wallets.py— Watch specific wallets for on-chain activity
Yellowstone gRPC — Performance & Production Guide
Latency Benchmarks
| Method | Slot Latency (p90) | Account Latency (p90) | Notes |
|---|---|---|---|
| REST polling | ~150ms+ | N/A | Polling overhead + HTTP |
WebSocket (onLogs) | ~10ms | ~374ms | Event-based, limited filtering |
| Yellowstone gRPC | ~5ms | ~215ms | Push-based, rich filtering |
| Deshred (Triton only) | ~20ms (p90) | N/A | Pre-execution, ~6.3ms p50 |
Data Volume Expectations
- All Solana transactions: ~2,000-4,000 TPS (including votes)
- Vote transactions: ~70% of total — always filter with
vote: false - PumpFun alone: ~20,000+ trades/hour during active periods
- Single DEX program filter: ~100-1,000 updates/second depending on activity
- Single wallet filter: ~0.01-10 updates/second depending on activity
Connection Management
Max Message Size
The default gRPC max message size (4MB) is too small for Solana. Set it higher:
# Python
options = [("grpc.max_receive_message_length", 64 * 1024 * 1024)] # 64MB
# For block subscriptions, go higher
options = [("grpc.max_receive_message_length", 1024 * 1024 * 1024)] # 1GB// Rust
.max_decoding_message_size(64 * 1024 * 1024)// TypeScript
{ "grpc.max_receive_message_length": 64 * 1024 * 1024 }Keep-Alive / Ping
Send pings every 15-30 seconds to prevent connection timeout:
import time, threading
def ping_loop(request_iterator):
while True:
time.sleep(15)
request_iterator.send(SubscribeRequest(
ping=SubscribeRequestPing(id=int(time.time()))
))Handle pong responses — if you receive a SubscribeUpdatePing from the server, respond:
if update.HasField("ping"):
request_iterator.send(SubscribeRequest(
ping=SubscribeRequestPing(id=update.ping.id)
))Reconnection with Exponential Backoff
import time, random
def connect_with_backoff(max_delay: float = 60.0):
delay = 0.1 # start at 100ms
last_slot = None
while True:
try:
stream = subscribe(from_slot=last_slot)
delay = 0.1 # reset on success
for update in stream:
last_slot = extract_slot(update)
process(update)
except grpc.RpcError as e:
jitter = random.uniform(0, delay * 0.1)
print(f"Disconnected: {e.code()}. Reconnecting in {delay:.1f}s")
time.sleep(delay + jitter)
delay = min(delay * 2, max_delay)Resume After Disconnection
Use from_slot to replay missed data. Subtract ~32 slots for reorg safety:
request = SubscribeRequest(
transactions={...},
from_slot=last_processed_slot - 32,
)Important: from_slot replay may produce duplicate updates. Deduplicate by transaction signature.
Replay depth varies by provider:
- Shyft: ~150 slots
- QuickNode: ~3,000 slots
- Helius: ~24 hours
Backpressure Architecture
Never process messages in the gRPC receive loop. Decouple I/O from business logic:
[gRPC Stream Thread]
│
▼
[Bounded Channel/Queue] ← backpressure point (1K-100K capacity)
│
▼
[Processing Worker(s)]
├─ Parse instructions
├─ Update state
├─ Trigger signals
└─ Batch DB writesPython Implementation
import queue
import threading
msg_queue = queue.Queue(maxsize=10_000)
def grpc_reader(stub, request):
"""Dedicated thread: reads gRPC stream into queue."""
stream = stub.Subscribe(iter([request]))
for update in stream:
try:
msg_queue.put(update, timeout=1.0)
except queue.Full:
print("WARNING: queue full, dropping message")
def processor():
"""Dedicated thread: processes messages from queue."""
while True:
update = msg_queue.get()
handle_update(update)
threading.Thread(target=grpc_reader, args=(stub, request), daemon=True).start()
threading.Thread(target=processor, daemon=True).start()Rust Implementation
use tokio::sync::mpsc;
let (tx, mut rx) = mpsc::channel(100_000);
// Spawn gRPC reader
tokio::spawn(async move {
while let Some(msg) = stream.next().await {
if tx.send(msg.unwrap()).await.is_err() { break; }
}
});
// Process in main task
while let Some(update) = rx.recv().await {
match update.update_oneof {
Some(UpdateOneof::Transaction(tx)) => handle_transaction(tx),
_ => {}
}
}Database Write Batching
Don't write every update to the database individually. Batch writes:
batch = []
last_flush = time.time()
for update in process_stream():
batch.append(to_row(update))
if len(batch) >= 1000 or (time.time() - last_flush) > 1.0:
db.executemany("INSERT INTO events ...", batch)
batch.clear()
last_flush = time.time()Multi-Connection Patterns
For high-throughput programs, split across multiple gRPC connections:
Connection 1: PumpFun transactions → Worker pool A
Connection 2: Raydium + Orca swaps → Worker pool B
Connection 3: Tracked wallet activity → Worker pool C
Connection 4: Pool account updates → Worker pool DEach connection runs on its own thread/task with independent reconnection logic.
Monitoring
Track these metrics in production:
| Metric | Alert Threshold | Why |
|---|---|---|
| Updates received/sec | < expected baseline | Stream may be stalled |
| Queue depth | > 80% capacity | Processing can't keep up |
| Time since last update | > 30 seconds | Connection likely dead |
| Reconnection count | > 5/hour | Provider instability |
| Processing latency | > 100ms p99 | Bottleneck in handlers |
| Dropped messages | > 0 | Queue overflow |
Production Checklist
- [ ]
vote: falseon all transaction filters - [ ]
max_receive_message_lengthset to 64MB+ - [ ] Exponential backoff reconnection (100ms → 60s cap)
- [ ]
from_slotresume after disconnection - [ ] Ping/pong every 15-30 seconds
- [ ] Bounded channel between I/O and processing
- [ ] Database write batching (1000 rows or 1s flush)
- [ ] Duplicate detection on replay (by tx signature)
- [ ] Monitoring: updates/sec, queue depth, reconnection count
- [ ] Graceful shutdown (drain queue before exit)
- [ ] Error classification (retriable vs fatal gRPC errors)
- [ ] Separate connections for independent data streams
Yellowstone gRPC — Protobuf Reference
Source: rpcpool/yellowstone-grpc
Service Definition
service Geyser {
rpc Subscribe(stream SubscribeRequest) returns (stream SubscribeUpdate) {}
rpc Ping(PingRequest) returns (PongResponse) {}
rpc GetLatestBlockhash(GetLatestBlockhashRequest) returns (GetLatestBlockhashResponse) {}
rpc GetBlockHeight(GetBlockHeightRequest) returns (GetBlockHeightResponse) {}
rpc GetSlot(GetSlotRequest) returns (GetSlotResponse) {}
rpc IsBlockhashValid(IsBlockhashValidRequest) returns (IsBlockhashValidResponse) {}
rpc GetVersion(GetVersionRequest) returns (GetVersionResponse) {}
}Subscribe is bidirectional streaming — you send SubscribeRequest messages and receive SubscribeUpdate messages continuously.
SubscribeRequest
message SubscribeRequest {
map<string, SubscribeRequestFilterAccounts> accounts = 1;
map<string, SubscribeRequestFilterSlots> slots = 2;
map<string, SubscribeRequestFilterTransactions> transactions = 3;
map<string, SubscribeRequestFilterTransactions> transactions_status = 10;
map<string, SubscribeRequestFilterBlocks> blocks = 4;
map<string, SubscribeRequestFilterBlocksMeta> blocks_meta = 5;
map<string, SubscribeRequestFilterEntry> entry = 8;
optional CommitmentLevel commitment = 6;
repeated SubscribeRequestAccountsDataSlice accounts_data_slice = 7;
optional SubscribeRequestPing ping = 9;
optional uint64 from_slot = 11;
}Map keys are user-defined labels. They appear in SubscribeUpdate.filters to identify which subscription matched.
SubscribeUpdate (Response)
message SubscribeUpdate {
repeated string filters = 1; // which named filters matched
oneof update_oneof {
SubscribeUpdateAccount account = 2;
SubscribeUpdateSlot slot = 3;
SubscribeUpdateTransaction transaction = 4;
SubscribeUpdateTransactionStatus transaction_status = 10;
SubscribeUpdateBlock block = 5;
SubscribeUpdatePing ping = 6;
SubscribeUpdatePong pong = 9;
SubscribeUpdateBlockMeta block_meta = 7;
SubscribeUpdateEntry entry = 8;
}
google.protobuf.Timestamp created_at = 11;
}Transaction Update
message SubscribeUpdateTransaction {
SubscribeUpdateTransactionInfo transaction = 1;
uint64 slot = 2;
}
message SubscribeUpdateTransactionInfo {
bytes signature = 1; // 64 bytes, base58-encode for display
bool is_vote = 2;
Transaction transaction = 3; // from solana-storage.proto
TransactionStatusMeta meta = 4; // execution results
uint64 index = 5; // position within the block
}Transaction (Inner)
message Transaction {
repeated bytes signatures = 1; // first is the tx signature
Message message = 2;
}
message Message {
MessageHeader header = 1;
repeated bytes account_keys = 2; // all account pubkeys (32 bytes each)
bytes recent_blockhash = 3;
repeated CompiledInstruction instructions = 4;
bool versioned = 5;
repeated MessageAddressTableLookup address_table_lookups = 6;
}
message MessageHeader {
uint32 num_required_signatures = 1;
uint32 num_readonly_signed_accounts = 2;
uint32 num_readonly_unsigned_accounts = 3;
}
message CompiledInstruction {
uint32 program_id_index = 1; // index into account_keys
bytes accounts = 2; // indices into account_keys
bytes data = 3; // instruction data (program-specific)
}TransactionStatusMeta (Post-Execution)
message TransactionStatusMeta {
TransactionError err = 1;
uint64 fee = 2;
repeated uint64 pre_balances = 3; // SOL balances before (lamports)
repeated uint64 post_balances = 4; // SOL balances after (lamports)
repeated InnerInstructions inner_instructions = 5;
repeated string log_messages = 6;
repeated TokenBalance pre_token_balances = 7;
repeated TokenBalance post_token_balances = 8;
repeated Reward rewards = 9;
repeated bytes loaded_writable_addresses = 12;
repeated bytes loaded_readonly_addresses = 13;
ReturnData return_data = 14;
optional uint64 compute_units_consumed = 15;
}Token Balance
message TokenBalance {
uint32 account_index = 1; // index into account_keys
string mint = 2; // token mint address (base58)
UiTokenAmount ui_token_amount = 3;
string owner = 4; // token account owner (base58)
}
message UiTokenAmount {
double ui_amount = 1; // human-readable amount
uint32 decimals = 2;
string amount = 3; // raw amount as string
}Inner Instructions
message InnerInstructions {
uint32 index = 1; // which top-level instruction generated these
repeated InnerInstruction instructions = 2;
}
message InnerInstruction {
uint32 program_id_index = 1;
bytes accounts = 2;
bytes data = 3;
optional uint32 stack_height = 4;
}Account Update
message SubscribeUpdateAccount {
SubscribeUpdateAccountInfo account = 1;
uint64 slot = 2;
optional bool is_startup = 3;
}
message SubscribeUpdateAccountInfo {
bytes pubkey = 1; // 32 bytes
uint64 lamports = 2;
bytes owner = 3; // program that owns this account (32 bytes)
bool executable = 4;
uint64 rent_epoch = 5;
bytes data = 6; // account data (variable length)
uint64 write_version = 7;
optional bytes txn_signature = 8; // which tx caused this update
}Commitment Levels
enum CommitmentLevel {
PROCESSED = 0; // fastest, may be rolled back
CONFIRMED = 1; // supermajority voted
FINALIZED = 2; // irreversible
}Slot Status
enum SlotStatus {
SLOT_PROCESSED = 0;
SLOT_CONFIRMED = 1;
SLOT_FINALIZED = 2;
SLOT_FIRST_SHRED_RECEIVED = 3;
SLOT_COMPLETED = 4;
SLOT_CREATED_BANK = 5;
SLOT_DEAD = 6;
}Parsing Checklist
When processing a SubscribeUpdateTransaction:
1. Signature: base58.b58encode(info.signature).decode() 2. Account keys: Decode each 32-byte entry in message.account_keys to base58 3. Instructions: For each CompiledInstruction:
- Program =
account_keys[program_id_index] - Accounts =
[account_keys[i] for i in accounts] - Data = raw bytes, first 8 bytes are typically the instruction discriminator
4. Inner instructions: CPI calls generated during execution — same structure as top-level 5. Token changes: Compare pre_token_balances vs post_token_balances for swap amounts 6. SOL changes: Compare pre_balances vs post_balances for fee and SOL transfer analysis 7. Logs: meta.log_messages contain program logs (useful for debugging) 8. Compute: meta.compute_units_consumed for gas analysis
Yellowstone gRPC — Provider Comparison
Shyft
- Entry price: $199/mo (Build plan)
- Endpoints:
grpc.{region}.shyft.to— NY, VA, MIA, AMS, FRA, LON, SGP - Auth: x-token from dashboard, or IP whitelisting
- Bandwidth: Unlimited (no metering)
- Connections: 10 (Build), 20 (Grow/$349), 50 (Accelerate/$649)
- Dedicated nodes: From $1,800/mo (unlimited connections, 5-10ms latency advantage)
- Historical replay: Up to 150 slots lookback
- Unique feature: RabbitStream — pre-execution shred-level data (transactions before confirmation, no metadata)
- DeFi parsing examples: github.com/Shyft-to/solana-defi — Raydium, PumpFun, Orca, Meteora parsers
- Connection management: Clear stale connections via
https://grpc.{region}.shyft.to/clear-connections?xtoken=YOUR_TOKEN - Best for: Price-sensitive teams, PumpFun/DEX trading, good regional coverage
Helius (LaserStream)
- Entry price: $999/mo (Professional plan, mainnet gRPC)
- Endpoints:
- US East:
laserstream-mainnet-ewr.helius-rpc.com - US West:
laserstream-mainnet-slc.helius-rpc.com - Europe:
laserstream-mainnet-fra.helius-rpc.com - Asia:
laserstream-mainnet-tyo.helius-rpc.com - Devnet:
laserstream-devnet-ewr.helius-rpc.com - Auth: API key as x-token
- Bandwidth: Credit-based + data add-ons ($500/5TB, scaling tiers)
- Historical replay: Up to 24 hours
- Unique features: DAS API (token/NFT metadata), enhanced webhooks, auto-reconnect, multi-node failover
- Best for: Teams already using Helius RPC/DAS, need rich Solana APIs beyond streaming
Triton One (Dragon's Mouth)
- Entry price: ~$2,900/mo (dedicated nodes)
- Bandwidth: ~$0.08/GB
- Auth: x-token
- Created Yellowstone: Triton built and open-sources the Yellowstone gRPC plugin
- Unique features:
- Deshred (
SubscribeDeshred): Pre-execution transaction streaming (~6.3ms p50, ~20ms p90). Paid beta, limited availability - Fumarole: Multi-node HA aggregator for persistent streaming
- Old Faithful: Historical data replay (full archive)
- Whirligig: Enhanced WebSocket proxy over Yellowstone
- Best for: HFT/MEV, lowest raw latency, enterprise infrastructure
QuickNode
- Entry price: Marketplace add-on (price depends on base plan)
- Port: 10000 (separate from RPC)
- Endpoint format:
your-endpoint.solana-mainnet.quiknode.pro:10000 - Auth: Built into endpoint URL
- Rate limits: Tied to plan RPS (e.g., 125 RPS on Accelerate)
- Historical replay: Up to 3000 slots via
from_slot - Best for: Teams already on QuickNode, flexible add-on model
Chainstack
- Entry price: $49/mo (1 stream)
- Tiers: $49/1 stream, $149/5 streams, $449/25 streams
- Limits: Up to 50 accounts per stream, 5 concurrent filters of same type
- Features: Jito ShredStream enabled by default on all nodes
- Best for: Budget option, teams needing ShredStream bundled
Alchemy
- Entry price: Free tier (30M compute units/mo)
- Bandwidth: ~$0.08/GB for gRPC
- Pay-as-you-go: $5 per 11M compute units
- Best for: Experimentation, low-volume testing
GetBlock
- Entry price: Included with Dedicated Solana Node subscription
- Auth: Access token created after node deployment
- Features: Single TLS endpoint (no separate port config)
- Best for: Teams with existing GetBlock dedicated nodes
Provider Selection Guide
| Priority | Recommended |
|---|---|
| Lowest cost with gRPC | Chainstack ($49/mo, limited) or Shyft ($199/mo, full) |
| Best value for trading | Shyft ($199-649/mo, unlimited bandwidth, 7 regions) |
| Rich Solana APIs + gRPC | Helius ($999/mo, DAS + webhooks + gRPC) |
| Lowest latency / HFT | Triton One (Deshred: ~6ms p50) |
| Free experimentation | Alchemy (free tier, CU-metered) |
| Already on QuickNode | QuickNode add-on |
Authentication Code (All Providers)
All providers use the same x-token pattern:
import grpc, os
endpoint = os.environ["GRPC_ENDPOINT"].replace("https://", "")
token = os.environ["GRPC_TOKEN"]
auth = grpc.metadata_call_credentials(
lambda ctx, cb: cb((("x-token", token),), None)
)
channel = grpc.secure_channel(
endpoint,
grpc.composite_channel_credentials(grpc.ssl_channel_credentials(), auth),
options=[("grpc.max_receive_message_length", 64 * 1024 * 1024)],
)use yellowstone_grpc_client::GeyserGrpcClient;
let mut client = GeyserGrpcClient::connect(endpoint, Some(token), None).await?;import Client from "@triton-one/yellowstone-grpc";
const client = new Client(endpoint, token, {
"grpc.max_receive_message_length": 64 * 1024 * 1024,
});Yellowstone gRPC — Subscription Filters Reference
How Filters Work
A SubscribeRequest contains named filter maps. Each map key is a label you choose — it appears in the response's filters field so you know which subscription matched.
- Multiple filter types (transactions + accounts in same request) run independently
- Values within arrays (
account_include: [A, B, C]) are logical OR - Sending a new
SubscribeRequestreplaces all previous filters entirely - To unsubscribe, send empty maps for all types
Transaction Filters
Filter real-time transactions by involved accounts, vote status, and failure status.
message SubscribeRequestFilterTransactions {
optional bool vote = 1; // include vote transactions?
optional bool failed = 2; // include failed transactions?
optional string signature = 5; // watch a specific signature
repeated string account_include = 3; // tx must involve ANY of these
repeated string account_exclude = 4; // tx must NOT involve ANY of these
repeated string account_required = 6;// tx must involve ALL of these
}Filter by Program ID
Subscribe to all successful transactions involving a DEX program:
transactions={
"raydium_swaps": SubscribeRequestFilterTransactions(
account_include=["675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8"],
vote=False,
failed=False,
)
}Filter by Multiple Programs
OR logic — matches transactions involving ANY listed program:
transactions={
"all_dex": SubscribeRequestFilterTransactions(
account_include=[
"6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P", # PumpFun
"675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8", # Raydium AMM
"whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc", # Orca
],
vote=False,
failed=False,
)
}Filter by Wallet (Copy Trading)
Watch specific wallets for any on-chain activity:
transactions={
"tracked_wallets": SubscribeRequestFilterTransactions(
account_include=[
"WalletPubkey1...",
"WalletPubkey2...",
],
vote=False,
failed=False,
)
}Exclude Known Programs
Exclude noisy programs to reduce volume:
transactions={
"clean_feed": SubscribeRequestFilterTransactions(
account_include=["TargetWallet..."],
account_exclude=[
"Vote111111111111111111111111111111111111111",
"ComputeBudget111111111111111111111111111111",
],
vote=False,
failed=False,
)
}Require Multiple Accounts (AND logic)
Match only transactions that involve ALL listed accounts:
transactions={
"wallet_on_pumpfun": SubscribeRequestFilterTransactions(
account_required=[
"6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P", # PumpFun
"SpecificWalletAddress...", # Target wallet
],
vote=False,
failed=False,
)
}Watch a Specific Transaction
Track confirmation of a submitted transaction:
transactions={
"my_tx": SubscribeRequestFilterTransactions(
signature="5wHu1qwD7q8ZHqDRYBiHMk2aJ4C8tNqVNK5...",
)
}Account Filters
Stream account data whenever it changes on-chain. Useful for tracking pool reserves, token balances, and program state.
message SubscribeRequestFilterAccounts {
repeated string account = 2; // specific account pubkeys
repeated string owner = 3; // accounts owned by these programs
repeated SubscribeRequestFilterAccountsFilter filters = 4;
optional bool nonempty_txn_signature = 5;
}Watch Specific Accounts
accounts={
"pool_reserves": SubscribeRequestFilterAccounts(
account=["PoolAccountPubkey1...", "PoolAccountPubkey2..."],
)
}Watch All Accounts Owned by a Program
accounts={
"all_token_accounts": SubscribeRequestFilterAccounts(
owner=["TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"],
filters=[
SubscribeRequestFilterAccountsFilter(token_account_state=True)
],
)
}Warning: Subscribing to all accounts owned by the Token Program produces enormous data volume. Always add filters to narrow the stream.
Memcmp Filter (Match Bytes at Offset)
Filter accounts by specific bytes at a given offset in account data:
# Match token accounts for a specific mint (mint pubkey at offset 0)
accounts={
"sol_token_accounts": SubscribeRequestFilterAccounts(
owner=["TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"],
filters=[
SubscribeRequestFilterAccountsFilter(
memcmp=SubscribeRequestFilterAccountsFilterMemcmp(
offset=0,
base58="So11111111111111111111111111111111111111112",
)
)
],
)
}Data Size Filter
Match accounts by exact data length:
accounts={
"sized_accounts": SubscribeRequestFilterAccounts(
owner=["YourProgramId..."],
filters=[
SubscribeRequestFilterAccountsFilter(datasize=165) # SPL token account size
],
)
}Lamports Filter
Filter by SOL balance:
accounts={
"whales": SubscribeRequestFilterAccounts(
owner=["11111111111111111111111111111111"],
filters=[
SubscribeRequestFilterAccountsFilter(
lamports=SubscribeRequestFilterAccountsFilterLamports(
gt=1_000_000_000_000 # > 1000 SOL in lamports
)
)
],
)
}Data Slicing
Reduce bandwidth by requesting only specific byte ranges of account data:
# Only first 40 bytes (discriminator + first key field)
accounts_data_slice=[
SubscribeRequestAccountsDataSlice(offset=0, length=40)
]Multiple slices are supported — you get concatenated results.
Slot Filters
slots={
"slot_updates": SubscribeRequestFilterSlots(
filter_by_commitment=True,
)
}Slot status values: PROCESSED, CONFIRMED, FINALIZED, FIRST_SHRED_RECEIVED, COMPLETED, CREATED_BANK, DEAD.
Block and Block Meta Filters
# Full blocks (high bandwidth)
blocks={
"full_blocks": SubscribeRequestFilterBlocks(
account_include=["ProgramId..."], # optional: only blocks with this program
include_transactions=True,
include_accounts=False,
include_entries=False,
)
}
# Block metadata only (low bandwidth)
blocks_meta={
"block_meta": SubscribeRequestFilterBlocksMeta()
}Multiple Named Filters
Use different labels to multiplex subscriptions on one connection:
request = SubscribeRequest(
transactions={
"pumpfun": SubscribeRequestFilterTransactions(
account_include=["6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"],
vote=False, failed=False,
),
"whales": SubscribeRequestFilterTransactions(
account_include=["Whale1...", "Whale2..."],
vote=False, failed=False,
),
},
accounts={
"pools": SubscribeRequestFilterAccounts(
account=["Pool1...", "Pool2..."],
),
},
commitment=CommitmentLevel.PROCESSED,
)
# In the response, update.filters tells you which matched: ["pumpfun"], ["whales"], ["pools"]Historical Replay
Resume from a specific slot (useful after disconnection):
request = SubscribeRequest(
transactions={...},
from_slot=last_seen_slot - 32, # subtract for reorg safety
)Replay depth varies by provider: Shyft ~150 slots, QuickNode ~3000 slots, Helius ~24 hours.
#!/usr/bin/env python3
"""Monitor specific Solana wallets for on-chain activity via Yellowstone gRPC.
Watches a list of wallet addresses for any transaction activity, parses the
transactions to identify what programs were called and what token balances
changed, and logs the activity with timestamps.
Useful for: copy trading signal generation, whale watching, wallet profiling.
Usage:
python scripts/monitor_wallets.py
# Watch specific wallets (comma-separated)
WATCH_WALLETS="addr1,addr2,addr3" python scripts/monitor_wallets.py
Dependencies:
uv pip install grpcio grpcio-tools protobuf base58 python-dotenv
Environment Variables:
GRPC_ENDPOINT: Your Yellowstone gRPC endpoint (e.g., https://grpc.ny.shyft.to)
GRPC_TOKEN: Your x-token for authentication
WATCH_WALLETS: Comma-separated list of wallet addresses to monitor
Setup:
Generate protobuf stubs first (see subscribe_transactions.py for instructions).
"""
import os
import sys
import time
import json
import queue
import threading
from datetime import datetime, timezone
from typing import Optional
import base58
import grpc
# ── Configuration ───────────────────────────────────────────────────
GRPC_ENDPOINT = os.getenv("GRPC_ENDPOINT", "")
GRPC_TOKEN = os.getenv("GRPC_TOKEN", "")
if not GRPC_ENDPOINT or not GRPC_TOKEN:
print("Set GRPC_ENDPOINT and GRPC_TOKEN environment variables")
sys.exit(1)
# Wallets to watch — comma-separated in env var, or hardcode for testing
WATCH_WALLETS_STR = os.getenv("WATCH_WALLETS", "")
if not WATCH_WALLETS_STR:
print("Set WATCH_WALLETS environment variable (comma-separated addresses)")
print(" export WATCH_WALLETS='addr1,addr2,addr3'")
sys.exit(1)
WATCH_WALLETS = [w.strip() for w in WATCH_WALLETS_STR.split(",") if w.strip()]
if not WATCH_WALLETS:
print("No valid wallet addresses found in WATCH_WALLETS")
sys.exit(1)
MAX_RECONNECT_DELAY = 60.0
MAX_QUEUE_SIZE = 10_000
# Known program labels for readable output
KNOWN_PROGRAMS = {
"6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P": "PumpFun",
"PSwapMdSai8tjrEXcxFeQth87xC4rRsa4VA5mhGhXkP": "PumpSwap",
"675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8": "Raydium-AMM",
"CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK": "Raydium-CLMM",
"CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C": "Raydium-CPMM",
"whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc": "Orca",
"LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo": "Meteora-DLMM",
"JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4": "Jupiter-V6",
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA": "Token",
"11111111111111111111111111111111": "System",
"ComputeBudget111111111111111111111111111111": "ComputeBudget",
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL": "ATA",
}
# ── gRPC Setup ──────────────────────────────────────────────────────
def create_channel(endpoint: str, token: str) -> grpc.Channel:
"""Create an authenticated TLS gRPC channel."""
clean_endpoint = endpoint.replace("https://", "").replace("http://", "")
auth_creds = grpc.metadata_call_credentials(
lambda ctx, cb: cb((("x-token", token),), None)
)
return grpc.secure_channel(
clean_endpoint,
grpc.composite_channel_credentials(grpc.ssl_channel_credentials(), auth_creds),
options=[("grpc.max_receive_message_length", 64 * 1024 * 1024)],
)
try:
from generated import geyser_pb2, geyser_pb2_grpc # type: ignore
except ImportError:
print("ERROR: Generated protobuf stubs not found.")
print("See subscribe_transactions.py for setup instructions.")
sys.exit(1)
# ── Wallet Activity Parsing ────────────────────────────────────────
def identify_programs(tx_info) -> list[str]:
"""Identify which known programs a transaction interacted with.
Args:
tx_info: SubscribeUpdateTransactionInfo protobuf.
Returns:
List of program labels (e.g., ["PumpFun", "Token"]).
"""
msg = tx_info.transaction.message
account_keys = [base58.b58encode(k).decode() for k in msg.account_keys]
programs = set()
for ix in msg.instructions:
program_id = account_keys[ix.program_id_index]
label = KNOWN_PROGRAMS.get(program_id, None)
if label and label not in ("System", "ComputeBudget", "Token", "ATA"):
programs.add(label)
# Also check inner instructions (CPI calls)
for inner in tx_info.meta.inner_instructions:
for ix in inner.instructions:
if ix.program_id_index < len(account_keys):
program_id = account_keys[ix.program_id_index]
label = KNOWN_PROGRAMS.get(program_id, None)
if label and label not in ("System", "ComputeBudget", "Token", "ATA"):
programs.add(label)
return sorted(programs) if programs else ["Unknown"]
def extract_sol_change(tx_info, wallet: str) -> float:
"""Calculate SOL balance change for a specific wallet.
Args:
tx_info: SubscribeUpdateTransactionInfo protobuf.
wallet: Base58 wallet address.
Returns:
SOL change in lamports (positive = received, negative = spent).
"""
msg = tx_info.transaction.message
account_keys = [base58.b58encode(k).decode() for k in msg.account_keys]
try:
idx = account_keys.index(wallet)
except ValueError:
return 0.0
pre = tx_info.meta.pre_balances[idx] if idx < len(tx_info.meta.pre_balances) else 0
post = tx_info.meta.post_balances[idx] if idx < len(tx_info.meta.post_balances) else 0
return (post - pre) / 1e9 # Convert lamports to SOL
def extract_token_changes(tx_info, wallet: str) -> list[dict]:
"""Extract token balance changes for a specific wallet.
Args:
tx_info: SubscribeUpdateTransactionInfo protobuf.
wallet: Base58 wallet address.
Returns:
List of dicts with mint, delta, and post_amount for each changed token.
"""
changes = []
pre_map = {}
for tb in tx_info.meta.pre_token_balances:
if tb.owner == wallet:
pre_map[tb.mint] = float(tb.ui_token_amount.ui_amount)
for tb in tx_info.meta.post_token_balances:
if tb.owner == wallet:
post_amount = float(tb.ui_token_amount.ui_amount)
pre_amount = pre_map.get(tb.mint, 0.0)
delta = post_amount - pre_amount
if abs(delta) > 0:
changes.append({
"mint": tb.mint,
"delta": delta,
"post_amount": post_amount,
})
return changes
def parse_wallet_activity(tx_update, wallets: list[str]) -> Optional[dict]:
"""Parse a transaction update for wallet-relevant activity.
Args:
tx_update: SubscribeUpdateTransaction protobuf.
wallets: List of watched wallet addresses.
Returns:
Activity dict if a watched wallet was involved, None otherwise.
"""
info = tx_update.transaction
sig = base58.b58encode(info.signature).decode()
slot = tx_update.slot
msg = info.transaction.message
account_keys = [base58.b58encode(k).decode() for k in msg.account_keys]
# Find which watched wallets are involved
involved = [w for w in wallets if w in account_keys]
if not involved:
return None
# Is this wallet the signer (initiator)?
signer = account_keys[0] if account_keys else None
programs = identify_programs(info)
activity = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"signature": sig,
"slot": slot,
"programs": programs,
"wallets": [],
}
for wallet in involved:
sol_change = extract_sol_change(info, wallet)
token_changes = extract_token_changes(info, wallet)
is_signer = wallet == signer
activity["wallets"].append({
"address": wallet,
"is_signer": is_signer,
"sol_change": sol_change,
"token_changes": token_changes,
})
return activity
# ── Streaming ───────────────────────────────────────────────────────
def stream_wallet_activity(
endpoint: str,
token: str,
wallets: list[str],
msg_queue: queue.Queue,
) -> None:
"""Stream transactions for watched wallets with reconnection."""
delay = 0.1
last_slot: Optional[int] = None
while True:
try:
channel = create_channel(endpoint, token)
stub = geyser_pb2_grpc.GeyserStub(channel)
request = geyser_pb2.SubscribeRequest(
transactions={
"wallets": geyser_pb2.SubscribeRequestFilterTransactions(
account_include=wallets,
vote=False,
failed=False,
)
},
commitment=geyser_pb2.CommitmentLevel.CONFIRMED,
)
if last_slot:
request.from_slot = last_slot - 32
print(f"Connecting to {endpoint}...")
stream = stub.Subscribe(iter([request]))
delay = 0.1
print(f"Monitoring {len(wallets)} wallets...\n")
for update in stream:
if update.HasField("transaction"):
last_slot = update.transaction.slot
try:
msg_queue.put_nowait(update.transaction)
except queue.Full:
pass
except grpc.RpcError as e:
status = e.code() if hasattr(e, "code") else "UNKNOWN"
print(f"\nDisconnected: {status}. Reconnecting in {delay:.1f}s...")
time.sleep(delay)
delay = min(delay * 2, MAX_RECONNECT_DELAY)
except KeyboardInterrupt:
return
except Exception as e:
print(f"\nError: {e}. Reconnecting in {delay:.1f}s...")
time.sleep(delay)
delay = min(delay * 2, MAX_RECONNECT_DELAY)
# ── Main ────────────────────────────────────────────────────────────
def format_activity(activity: dict) -> str:
"""Format a wallet activity event for display.
Args:
activity: Parsed activity dict from parse_wallet_activity.
Returns:
Formatted string for terminal output.
"""
lines = []
ts = activity["timestamp"][:19]
programs = ", ".join(activity["programs"])
sig = activity["signature"][:20]
lines.append(f"[{ts}] {sig}... | {programs}")
for w in activity["wallets"]:
addr = w["address"][:12] + "..."
role = "SIGNER" if w["is_signer"] else "participant"
sol = w["sol_change"]
sol_str = f"{sol:+.6f} SOL" if abs(sol) > 0.000001 else ""
lines.append(f" {addr} ({role}) {sol_str}")
for tc in w["token_changes"]:
mint = tc["mint"][:12] + "..."
lines.append(f" Token {mint}: {tc['delta']:+.6f}")
return "\n".join(lines)
def main() -> None:
"""Entry point: monitor wallets and print activity."""
print(f"Wallet Monitor — watching {len(WATCH_WALLETS)} addresses")
for w in WATCH_WALLETS:
print(f" {w}")
print()
msg_queue: queue.Queue = queue.Queue(maxsize=MAX_QUEUE_SIZE)
seen_sigs: set = set()
event_count = 0
reader = threading.Thread(
target=stream_wallet_activity,
args=(GRPC_ENDPOINT, GRPC_TOKEN, WATCH_WALLETS, msg_queue),
daemon=True,
)
reader.start()
try:
while True:
try:
tx_update = msg_queue.get(timeout=60.0)
except queue.Empty:
print(f"[{datetime.now(timezone.utc).isoformat()[:19]}] No activity (60s)")
continue
activity = parse_wallet_activity(tx_update, WATCH_WALLETS)
if not activity:
continue
if activity["signature"] in seen_sigs:
continue
seen_sigs.add(activity["signature"])
if len(seen_sigs) > 50_000:
seen_sigs.clear()
event_count += 1
print(format_activity(activity))
print()
except KeyboardInterrupt:
print(f"\nMonitored {event_count} events. Shutting down.")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Stream and parse Solana transactions in real-time via Yellowstone gRPC.
Connects to a Yellowstone gRPC provider, subscribes to transactions for a
specified program ID (default: PumpFun), and prints parsed transaction data
including signatures, involved accounts, instruction data, and token balance
changes.
Usage:
python scripts/subscribe_transactions.py
Dependencies:
uv pip install grpcio grpcio-tools protobuf base58 python-dotenv
Environment Variables:
GRPC_ENDPOINT: Your Yellowstone gRPC endpoint (e.g., https://grpc.ny.shyft.to)
GRPC_TOKEN: Your x-token for authentication
Setup:
Before first run, generate Python stubs from proto files:
git clone https://github.com/rpcpool/yellowstone-grpc.git
python -m grpc_tools.protoc \\
-I./yellowstone-grpc/yellowstone-grpc-proto/proto/ \\
--python_out=./generated \\
--pyi_out=./generated \\
--grpc_python_out=./generated \\
./yellowstone-grpc/yellowstone-grpc-proto/proto/*.proto
Then set PYTHONPATH to include the generated directory, or copy the
generated files into your project.
"""
import os
import sys
import time
import queue
import threading
from typing import Optional
import base58
import grpc
# ── Configuration ───────────────────────────────────────────────────
GRPC_ENDPOINT = os.getenv("GRPC_ENDPOINT", "")
GRPC_TOKEN = os.getenv("GRPC_TOKEN", "")
if not GRPC_ENDPOINT or not GRPC_TOKEN:
print("Set GRPC_ENDPOINT and GRPC_TOKEN environment variables")
print(" export GRPC_ENDPOINT='https://grpc.ny.shyft.to'")
print(" export GRPC_TOKEN='your-x-token'")
sys.exit(1)
# Program to filter (default: PumpFun)
TARGET_PROGRAM = os.getenv(
"TARGET_PROGRAM", "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"
)
# Reconnection settings
MAX_RECONNECT_DELAY = 60.0
INITIAL_RECONNECT_DELAY = 0.1
PING_INTERVAL_SECONDS = 15
MAX_QUEUE_SIZE = 10_000
# ── gRPC Setup ──────────────────────────────────────────────────────
def create_channel(endpoint: str, token: str) -> grpc.Channel:
"""Create an authenticated TLS gRPC channel.
Args:
endpoint: gRPC endpoint URL (https:// prefix is stripped).
token: x-token for authentication.
Returns:
Configured gRPC secure channel.
"""
clean_endpoint = endpoint.replace("https://", "").replace("http://", "")
auth_creds = grpc.metadata_call_credentials(
lambda context, callback: callback((("x-token", token),), None)
)
ssl_creds = grpc.ssl_channel_credentials()
combined = grpc.composite_channel_credentials(ssl_creds, auth_creds)
return grpc.secure_channel(
clean_endpoint,
combined,
options=[
("grpc.max_receive_message_length", 64 * 1024 * 1024),
("grpc.keepalive_time_ms", 10_000),
("grpc.keepalive_timeout_ms", 5_000),
],
)
# ── Stub Generation Check ──────────────────────────────────────────
try:
# Attempt to import generated protobuf stubs
# Users must generate these from the yellowstone-grpc proto files
from generated import geyser_pb2, geyser_pb2_grpc # type: ignore
except ImportError:
print("ERROR: Generated protobuf stubs not found.")
print()
print("Generate them first:")
print(" git clone https://github.com/rpcpool/yellowstone-grpc.git")
print(" mkdir -p generated")
print(" python -m grpc_tools.protoc \\")
print(" -I./yellowstone-grpc/yellowstone-grpc-proto/proto/ \\")
print(" --python_out=./generated \\")
print(" --pyi_out=./generated \\")
print(" --grpc_python_out=./generated \\")
print(" ./yellowstone-grpc/yellowstone-grpc-proto/proto/*.proto")
print()
print("Then ensure 'generated/' is in your PYTHONPATH or working directory.")
sys.exit(1)
# ── Transaction Parsing ────────────────────────────────────────────
def parse_transaction(tx_update) -> dict:
"""Parse a SubscribeUpdateTransaction into a readable dict.
Args:
tx_update: A SubscribeUpdateTransaction protobuf message.
Returns:
Dict with signature, slot, accounts, instructions, and token changes.
"""
info = tx_update.transaction
sig = base58.b58encode(info.signature).decode()
slot = tx_update.slot
msg = info.transaction.message
account_keys = [base58.b58encode(k).decode() for k in msg.account_keys]
# Parse top-level instructions
instructions = []
for ix in msg.instructions:
program_id = account_keys[ix.program_id_index]
ix_accounts = [account_keys[i] for i in ix.accounts]
discriminator = ix.data[:8].hex() if len(ix.data) >= 8 else ix.data.hex()
instructions.append({
"program": program_id,
"accounts": ix_accounts,
"discriminator": discriminator,
"data_len": len(ix.data),
})
# Parse token balance changes
token_changes = []
pre_balances = {tb.account_index: tb for tb in info.meta.pre_token_balances}
for post in info.meta.post_token_balances:
pre = pre_balances.get(post.account_index)
pre_amount = float(pre.ui_token_amount.ui_amount) if pre else 0.0
post_amount = float(post.ui_token_amount.ui_amount)
delta = post_amount - pre_amount
if abs(delta) > 0:
token_changes.append({
"mint": post.mint,
"owner": post.owner,
"delta": delta,
"post_amount": post_amount,
})
return {
"signature": sig,
"slot": slot,
"index": info.index,
"fee": info.meta.fee,
"compute_units": info.meta.compute_units_consumed,
"accounts": account_keys,
"instructions": instructions,
"token_changes": token_changes,
"num_logs": len(info.meta.log_messages),
}
# ── Streaming Logic ────────────────────────────────────────────────
def build_subscribe_request(
program_id: str, from_slot: Optional[int] = None
) -> geyser_pb2.SubscribeRequest:
"""Build a subscription request for a specific program.
Args:
program_id: Base58 program address to filter transactions.
from_slot: Optional slot to replay from (for reconnection).
Returns:
Configured SubscribeRequest.
"""
request = geyser_pb2.SubscribeRequest(
transactions={
"target": geyser_pb2.SubscribeRequestFilterTransactions(
account_include=[program_id],
vote=False,
failed=False,
)
},
commitment=geyser_pb2.CommitmentLevel.PROCESSED,
)
if from_slot is not None:
request.from_slot = from_slot
return request
def stream_with_reconnection(
endpoint: str,
token: str,
program_id: str,
msg_queue: queue.Queue,
) -> None:
"""Connect to gRPC and stream messages with automatic reconnection.
Args:
endpoint: gRPC endpoint URL.
token: Authentication token.
program_id: Program to filter.
msg_queue: Queue to push parsed updates into.
"""
delay = INITIAL_RECONNECT_DELAY
last_slot: Optional[int] = None
while True:
try:
channel = create_channel(endpoint, token)
stub = geyser_pb2_grpc.GeyserStub(channel)
from_slot = (last_slot - 32) if last_slot else None
request = build_subscribe_request(program_id, from_slot)
print(f"Connecting to {endpoint}...")
print(f"Filtering program: {program_id}")
if from_slot:
print(f"Replaying from slot: {from_slot}")
stream = stub.Subscribe(iter([request]))
delay = INITIAL_RECONNECT_DELAY # reset on successful connect
print("Connected. Streaming transactions...\n")
for update in stream:
if update.HasField("transaction"):
tx = update.transaction
last_slot = tx.slot
try:
msg_queue.put_nowait(tx)
except queue.Full:
pass # drop oldest if queue is full
elif update.HasField("ping"):
pass # server ping, connection is alive
except grpc.RpcError as e:
status = e.code() if hasattr(e, "code") else "UNKNOWN"
print(f"\nDisconnected: {status}. Reconnecting in {delay:.1f}s...")
time.sleep(delay)
delay = min(delay * 2, MAX_RECONNECT_DELAY)
except KeyboardInterrupt:
print("\nShutting down...")
return
except Exception as e:
print(f"\nUnexpected error: {e}. Reconnecting in {delay:.1f}s...")
time.sleep(delay)
delay = min(delay * 2, MAX_RECONNECT_DELAY)
# ── Main ────────────────────────────────────────────────────────────
def main() -> None:
"""Entry point: start streaming and print parsed transactions."""
msg_queue: queue.Queue = queue.Queue(maxsize=MAX_QUEUE_SIZE)
seen_sigs: set = set()
tx_count = 0
start_time = time.time()
# Start gRPC reader in background thread
reader_thread = threading.Thread(
target=stream_with_reconnection,
args=(GRPC_ENDPOINT, GRPC_TOKEN, TARGET_PROGRAM, msg_queue),
daemon=True,
)
reader_thread.start()
# Process messages in main thread
try:
while True:
try:
tx_update = msg_queue.get(timeout=30.0)
except queue.Empty:
elapsed = time.time() - start_time
print(f"No updates in 30s. Total: {tx_count} txs in {elapsed:.0f}s")
continue
parsed = parse_transaction(tx_update)
# Deduplicate (from_slot replay can produce duplicates)
if parsed["signature"] in seen_sigs:
continue
seen_sigs.add(parsed["signature"])
# Keep seen_sigs bounded
if len(seen_sigs) > 100_000:
seen_sigs.clear()
tx_count += 1
# Print summary
print(f"[{parsed['slot']}] {parsed['signature'][:20]}...")
print(f" Fee: {parsed['fee']} lamports | CU: {parsed['compute_units']}")
print(f" Instructions: {len(parsed['instructions'])}")
for ix in parsed["instructions"]:
prog_short = ix["program"][:8] + "..."
print(f" {prog_short} disc={ix['discriminator']} ({ix['data_len']}B)")
if parsed["token_changes"]:
print(f" Token changes:")
for tc in parsed["token_changes"]:
mint_short = tc["mint"][:8] + "..."
print(f" {mint_short}: {tc['delta']:+.6f}")
print()
except KeyboardInterrupt:
elapsed = time.time() - start_time
rate = tx_count / elapsed if elapsed > 0 else 0
print(f"\nProcessed {tx_count} transactions in {elapsed:.1f}s ({rate:.1f} tx/s)")
if __name__ == "__main__":
main()
Related skills
FAQ
Why use gRPC over WebSocket or REST?
Yellowstone gRPC reaches ~5ms slot latency versus ~10ms for WebSocket and ~150ms+ for REST polling, and supports rich filtering.
What can you subscribe to?
Transactions, accounts, slots, blocks, block metadata, entries, and transaction status, with named filters and commitment levels.