
Shredstream
- 191 installs
- 257 repo stars
- Updated June 24, 2026
- agiprolabs/claude-trading-skills
shredstream is a Claude Code skill that teaches how to receive pre-execution Solana transaction data via Jito ShredStream, Shyft RabbitStream, or Triton Deshred.
About
shredstream is a Claude Code skill that documents how to receive Solana transaction data before the validator executes the block, using Jito ShredStream, Shyft RabbitStream, or Triton Deshred. It explains the shred/Turbine mechanics, how to run and configure Jito's open-source proxy, and how to consume decoded gRPC entries. A developer uses it when building latency-sensitive Solana trading bots that need transaction intent earlier than standard Yellowstone gRPC.
- Pre-execution Solana transaction data ~100-500ms before block execution
- Covers Jito ShredStream Proxy, Shyft RabbitStream, and Triton Deshred
- Rust/gRPC consumer examples plus proxy deploy config
Shredstream by the numbers
- 191 all-time installs (skills.sh)
- Ranked #485 of 1,106 Finance & Trading skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
shredstream capabilities & compatibility
Jito proxy free during beta; Shyft RabbitStream from $199/mo; Triton Deshred ~$2,900+/mo.
- Capabilities
- solana rpc · transaction streaming · low latency data · mempool monitoring
- Works with
- docker · github
- Use cases
- trading
- Runs
- Runs locally
- Pricing
- Freemium
- Requires keys
- JITOAUTHKEYPAIRWHITELISTED · SHYFTGRPCTOKENRABBITSTREAMOPTION
What shredstream says it does
ShredStream gives you transaction data **before the validator executes the block** — typically 100-500ms earlier than standard Yellowstone gRPC.
This is the fastest path to Solana data for time-critical trading strategies.
npx skills add https://github.com/agiprolabs/claude-trading-skills --skill shredstreamAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 191 |
|---|---|
| repo stars | ★ 257 |
| Last updated | June 24, 2026 |
| Repository | agiprolabs/claude-trading-skills ↗ |
What it does
Stream pre-execution Solana transaction data via ShredStream for latency-sensitive trading bots.
Who is it for?
Building latency-sensitive Solana trading bots that need transaction intent before block execution.
Skip if: Getting confirmed transaction results such as success/failure status, balance changes, or logs, which require post-execution data.
When should I use this skill?
You need Solana transaction data 100-500ms earlier than standard gRPC for time-critical strategies.
What you get
A running pre-execution data pipeline delivering decoded transaction intent 100-500ms earlier than standard RPC.
- A running ShredStream proxy forwarding shreds and gRPC entries
- Code that decodes pre-execution transaction entries
By the numbers
- Compares 3 providers (Jito, Shyft, Triton)
- ~1,228-byte shreds sized for UDP MTU
- 100-500ms earlier than standard gRPC
Files
ShredStream — Pre-Execution Solana Data
ShredStream gives you transaction data before the validator executes the block — typically 100-500ms earlier than standard Yellowstone gRPC. You see transaction intent, not confirmed results.
This is the fastest path to Solana data for time-critical trading strategies.
How It Works
Solana validators produce blocks by serializing transactions into shreds (~1,228 bytes each, sized for UDP MTU). Shreds propagate through Turbine (Solana's fanout protocol, 2-3 hops). ShredStream bypasses Turbine by receiving shreds directly from leader validators via Jito's Block Engine.
Leader Validator
│
├── Turbine (standard, 2-3 hops, 200-500ms)
│ └── Your RPC Node → Yellowstone gRPC (post-execution)
│
└── Jito Block Engine (direct)
└── ShredStream Proxy (your server)
├── UDP shreds → Your RPC/Validator (faster block building)
└── gRPC entries → Your Trading Bot (decoded transactions)What You Get vs. What You Don't
| Available (Pre-Execution) | NOT Available (Needs Execution) |
|---|---|
| Transaction signatures | Success/failure status |
| Account keys (pubkeys) | Balance changes (pre/post) |
| Instructions (program, accounts, data) | Log messages |
| Address lookup table references | Inner instructions (CPI) |
| Slot number | Token balance changes |
| Compute units consumed |
Key tradeoff: Speed for completeness. You see what's about to happen but can't confirm it actually succeeded. Some transactions you see will ultimately fail.
Three Ways to Get Pre-Execution Data
| Provider | Product | Latency | Access | Cost |
|---|---|---|---|---|
| Jito | ShredStream Proxy | ~10-50ms from leader | Apply + auth keypair | Free (beta) |
| Shyft | RabbitStream | ~15-100ms faster than gRPC | Shyft gRPC plan | From $199/mo |
| Triton | Deshred (SubscribeDeshred) | ~6.3ms p50 from shred | Triton customer | ~$2,900+/mo |
See references/providers_compared.md for detailed comparison.
Option 1: Jito ShredStream Proxy
The most direct approach — run Jito's open-source proxy on your own server.
Get Access
1. Generate a Solana keypair: solana-keygen new -o shred_auth.json 2. Apply at Jito's form with your public key 3. Wait for approval (your keypair gets whitelisted) 4. No staking requirement, free during beta
Run the Proxy
# Clone and build
git clone https://github.com/jito-labs/shredstream-proxy.git --recurse-submodules
cd shredstream-proxy
# Run with gRPC enabled (key flag: --grpc-service-port)
RUST_LOG=info cargo run --release --bin jito-shredstream-proxy -- shredstream \
--block-engine-url https://mainnet.block-engine.jito.wtf \
--auth-keypair /path/to/shred_auth.json \
--desired-regions ny,amsterdam \
--dest-ip-ports 127.0.0.1:8001 \
--grpc-service-port 7777Docker (host networking required for UDP):
docker run -d --name shredstream-proxy --rm \
--network host \
-e RUST_LOG=info \
-e BLOCK_ENGINE_URL=https://mainnet.block-engine.jito.wtf \
-e AUTH_KEYPAIR=/app/shred_auth.json \
-e DESIRED_REGIONS=ny,amsterdam \
-e DEST_IP_PORTS=127.0.0.1:8001 \
-e GRPC_SERVICE_PORT=7777 \
-v /path/to/shred_auth.json:/app/shred_auth.json \
jitolabs/jito-shredstream-proxy shredstreamConfiguration
| Parameter | Description | Example |
|---|---|---|
BLOCK_ENGINE_URL | Jito block engine endpoint | https://mainnet.block-engine.jito.wtf |
AUTH_KEYPAIR | Path to whitelisted Solana keypair | shred_auth.json |
DESIRED_REGIONS | Max 2, comma-separated | ny,amsterdam |
DEST_IP_PORTS | Where to forward raw shreds (UDP) | 127.0.0.1:8001 |
GRPC_SERVICE_PORT | Enable gRPC entry streaming | 7777 |
SRC_BIND_PORT | Incoming shred UDP port | 20000 |
Available regions: amsterdam, dublin, frankfurt, london, ny, salt-lake-city, singapore, tokyo
Verify It's Working
# Check shreds are arriving via UDP
sudo tcpdump 'udp and dst port 20000'
# Should see many ~1200-byte packets continuouslyConsume via gRPC
use jito_protos::shredstream::{
shredstream_proxy_client::ShredstreamProxyClient,
SubscribeEntriesRequest,
};
let mut client = ShredstreamProxyClient::connect("http://127.0.0.1:7777").await?;
let mut stream = client
.subscribe_entries(SubscribeEntriesRequest {})
.await?
.into_inner();
while let Some(entry) = stream.message().await? {
let entries: Vec<solana_entry::entry::Entry> =
bincode::deserialize(&entry.entries)?;
for e in &entries {
for tx in &e.transactions {
let sig = tx.signatures[0];
let msg = tx.message();
// Parse instructions, accounts, etc.
}
}
println!("Slot {}: {} entries, {} transactions",
entry.slot,
entries.len(),
entries.iter().map(|e| e.transactions.len()).sum::<usize>()
);
}Option 2: Shyft RabbitStream
Drop-in replacement for Yellowstone gRPC — same SubscribeRequest format, just a different endpoint. Easiest way to get pre-execution data without running infrastructure.
export GRPC_ENDPOINT="https://rabbitstream.ny.shyft.to"
export GRPC_TOKEN="your-shyft-x-token"# Same code as yellowstone-grpc, just different endpoint
import grpc
endpoint = "rabbitstream.ny.shyft.to"
token = os.environ["GRPC_TOKEN"]
# ... standard Yellowstone connection code ...
# Subscribe to transactions — same filter format
request = SubscribeRequest(
transactions={
"pumpfun": SubscribeRequestFilterTransactions(
account_include=["6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"],
vote=False,
failed=False,
)
},
commitment=CommitmentLevel.PROCESSED,
)Limitations: Only transaction filters work. No account, slot, or block subscriptions. The meta field is empty (no execution results).
Regional endpoints: rabbitstream.{ny,va,ams,fra}.shyft.to
Option 3: Triton Deshred
Lowest latency (~6.3ms p50) via Triton's SubscribeDeshred RPC. Same Yellowstone client, different method.
// Requires yellowstone-grpc-client with Deshred support
let (mut tx, mut stream) = client.subscribe_deshred().await?;
tx.send(SubscribeDeshredRequest {
deshred_transactions: hashmap!{
"pumpfun".to_string() => SubscribeRequestFilterDeshredTransactions {
vote: Some(false),
account_include: vec!["6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P".into()],
..Default::default()
}
},
..Default::default()
}).await?;Access: Triton customers only, paid beta, requires their custom Agave validator fork.
Parsing Pre-Execution Transactions
Without execution metadata, parsing is simpler but requires program-specific knowledge.
Identify the Program
# From a raw VersionedTransaction (post-deserialization)
account_keys = [str(k) for k in tx.message.account_keys]
for ix in tx.message.instructions:
program_id = account_keys[ix.program_id_index]
if program_id == "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P":
# This is a PumpFun instruction
discriminator = ix.data[:8]
# Decode instruction data per PumpFun IDLInstruction Discriminators
Most Solana programs use 8-byte discriminators (Anchor SHA256 hash of the instruction name). Match instruction.data[:8] against known values for each program.
# Common approach
PUMPFUN_CREATE = bytes.fromhex("181ec828051c0777")
PUMPFUN_BUY = bytes.fromhex("66063d1201daebea")
PUMPFUN_SELL = bytes.fromhex("33e685a4017f83ad")
disc = ix.data[:8]
if disc == PUMPFUN_BUY:
# Parse buy parameters from remaining bytes
...Warning: Discriminator values are program-specific and can change between program versions. Always verify against the current program IDL. See the pumpfun-mechanics skill for PumpFun-specific parsing.
Common Architecture: ShredStream + Yellowstone
Most production systems use both:
ShredStream (pre-execution) Yellowstone gRPC (post-execution)
│ │
▼ ▼
Intent Detection Confirmation + Reconciliation
"Wallet X is buying token Y" "Buy succeeded, wallet now holds Z"
│ │
▼ ▼
Pre-compute Response Execute / Update State
(route, sign, prepare bundle) (record PnL, update positions)This gives you the speed advantage of ShredStream for signal detection while using Yellowstone for reliable state management.
Deployment Requirements
- Public IP required — NAT breaks UDP shred delivery
- Host networking — Docker bridge mode drops shred packets
- UDP port 20000 open for incoming shreds
- Co-locate near validators — Frankfurt, NY, Amsterdam, London, Tokyo recommended
- Bare metal preferred — Cloud VMs add 1-5ms jitter from shared NICs
See references/deployment.md for full infrastructure guide.
Files
References
references/providers_compared.md— Jito ShredStream vs Shyft RabbitStream vs Triton Deshredreferences/deployment.md— Infrastructure requirements, region selection, firewall configurationreferences/proto_reference.md— ShredStream protobuf definitions and Entry parsing
Scripts
scripts/parse_shredstream_entries.py— Decode and analyze ShredStream gRPC entriesscripts/rabbitstream_monitor.py— Connect to Shyft RabbitStream for pre-execution transaction monitoring
ShredStream — Deployment & Infrastructure Guide
Requirements
Network
- Public IP — Required. NAT is not supported (shreds arrive via UDP)
- UDP port 20000 — Open for incoming shreds (configurable via
SRC_BIND_PORT) - Host networking — Docker bridge mode drops UDP multicast. Use
--network host - Low-jitter connection — Bare metal preferred over cloud VMs
Hardware (Recommended for HFT)
- CPU: Modern AMD (EPYC 9354/9005 series) or Intel Xeon
- RAM: 32GB+ (proxy itself is lightweight, but co-located services need more)
- NIC: 10Gbps+ with kernel bypass (DPDK/AF_XDP) for lowest latency
- Storage: NVMe SSD (for any logging/persistence)
Software
- Rust toolchain (for building from source) or Docker
- Solana CLI (for keypair generation):
sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)"
Region Selection
Choose regions closest to your server and to major validator clusters.
| Region | Code | Best For |
|---|---|---|
| New York | ny | US East coast, most Solana validators |
| Amsterdam | amsterdam | Europe, good validator density |
| Frankfurt | frankfurt | Europe, major datacenter hub |
| London | london | Europe |
| Salt Lake City | salt-lake-city | US West |
| Singapore | singapore | Asia-Pacific |
| Tokyo | tokyo | Asia-Pacific |
| Dublin | dublin | Europe |
Max 2 regions per proxy instance. Run multiple instances for more regions.
Recommended Co-Location
For lowest latency, co-locate in datacenters near Solana validator clusters:
- Frankfurt: OVH, Equinix FR5
- New York: Equinix NY, TeraSwitch
- Amsterdam: Equinix AM, Interxion
- Tokyo: Equinix TY
Co-location trims 20-50ms vs. random cloud placement.
Firewall Configuration
Each Jito region uses specific IP ranges for shred delivery. You must allow UDP traffic from these IPs on your shred receive port (default 20000).
Check the current IP allowlist at: https://docs.jito.wtf/lowlatencytxnfeed/
General rule:
# Allow UDP from Jito Block Engine IPs (example, verify current IPs in docs)
sudo ufw allow proto udp from <JITO_IP> to any port 20000Also allow outbound HTTPS (443) for gRPC connection to the Block Engine.
Running the Proxy
Build from Source
git clone https://github.com/jito-labs/shredstream-proxy.git --recurse-submodules
cd shredstream-proxy
cargo build --releaseSystemd Service (Production)
[Unit]
Description=Jito ShredStream Proxy
After=network.target
[Service]
Type=simple
User=solana
Environment=RUST_LOG=info
ExecStart=/opt/shredstream/target/release/jito-shredstream-proxy shredstream \
--block-engine-url https://mainnet.block-engine.jito.wtf \
--auth-keypair /opt/shredstream/shred_auth.json \
--desired-regions ny,amsterdam \
--dest-ip-ports 127.0.0.1:8001 \
--grpc-service-port 7777
Restart=always
RestartSec=5
LimitNOFILE=65535
[Install]
WantedBy=multi-user.targetsudo cp shredstream.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now shredstream
sudo journalctl -u shredstream -f # monitor logsDocker (Host Networking)
docker run -d --name shredstream \
--network host \
--restart unless-stopped \
-e RUST_LOG=info \
-e BLOCK_ENGINE_URL=https://mainnet.block-engine.jito.wtf \
-e AUTH_KEYPAIR=/app/shred_auth.json \
-e DESIRED_REGIONS=ny,amsterdam \
-e DEST_IP_PORTS=127.0.0.1:8001 \
-e GRPC_SERVICE_PORT=7777 \
-v /opt/shredstream/shred_auth.json:/app/shred_auth.json:ro \
jitolabs/jito-shredstream-proxy shredstreamFly.io Deployment
For distributed setups, run the proxy on Fly.io near your target region:
# fly.toml
app = "my-shredstream"
primary_region = "ewr" # Newark (close to NY)
[build]
image = "jitolabs/jito-shredstream-proxy"
[env]
RUST_LOG = "info"
BLOCK_ENGINE_URL = "https://mainnet.block-engine.jito.wtf"
DESIRED_REGIONS = "ny"
GRPC_SERVICE_PORT = "7777"
[[services]]
internal_port = 7777
protocol = "tcp"
[[services.ports]]
port = 7777
[[services]]
internal_port = 20000
protocol = "udp"
[[services.ports]]
port = 20000Note: Fly.io may not support raw UDP well on all regions. Test thoroughly.
Verification
Check Shred Receipt
# Should see continuous stream of ~1200-byte UDP packets
sudo tcpdump -i any 'udp and dst port 20000' -c 10
# Count packets per second
sudo tcpdump -i any 'udp and dst port 20000' -w /dev/null 2>&1 | head -1Check gRPC Output
# If you have grpcurl installed
grpcurl -plaintext localhost:7777 shredstream.ShredstreamProxy/SubscribeEntriesCheck Proxy Logs
# Look for successful heartbeat responses and shred counts
journalctl -u shredstream --since "5 minutes ago" | grep -E "heartbeat|shred|entries"Monitoring
| Metric | How to Check | Alert On |
|---|---|---|
| Shreds/second | tcpdump packet count | < 100/sec (stream may be dead) |
| Heartbeat TTL | Proxy logs | Missed heartbeats (reconnection needed) |
| gRPC clients connected | Proxy logs | Unexpected disconnections |
| Entry deserialization errors | Proxy logs | Non-zero error rate |
| Slot gaps | Compare slots in entries | Gaps > 2 consecutive slots |
Common Issues
No Shreds Arriving
1. Check keypair is whitelisted (re-apply if needed) 2. Verify UDP port is open: sudo ufw status | grep 20000 3. Check Block Engine URL is reachable: curl -s https://mainnet.block-engine.jito.wtf 4. Ensure no NAT between you and the internet 5. Try different regions
High Latency
1. Move closer to validators (Frankfurt/NY/Amsterdam) 2. Switch from cloud VM to bare metal 3. Check for CPU contention: htop 4. Verify NIC is not saturated
Docker Shred Loss
- Use host networking (
--network host), not bridge mode - Bridge mode fragments UDP multicast and drops packets
- If host networking is unavailable, use
SRC_BIND_PORTand explicit port mapping
ShredStream — Protobuf & Data Reference
Source: jito-labs/mev-protos
Proto Definitions
ShredStream Service (Block Engine ↔ Proxy)
service Shredstream {
rpc SendHeartbeat(Heartbeat) returns (HeartbeatResponse) {}
}
message Heartbeat {
shared.Socket socket = 1; // IP must match incoming packet (anti-spoofing)
repeated string regions = 2; // desired regions, max 2
}
message HeartbeatResponse {
uint32 ttl_ms = 1; // must send next heartbeat within this window
}The proxy sends heartbeats to the Block Engine to maintain the shred stream. If a heartbeat is missed (exceeds ttl_ms), the stream stops.
ShredStream Proxy Service (Proxy ↔ Your Code)
service ShredstreamProxy {
rpc SubscribeEntries(SubscribeEntriesRequest) returns (stream Entry) {}
}
message SubscribeEntriesRequest {
// Currently no filters — you get everything
}
message Entry {
uint64 slot = 1;
bytes entries = 2; // bincode-serialized Vec<solana_entry::entry::Entry>
}Shared Types
message Socket {
string ip = 1;
int64 port = 2;
}Trace Shred (Debugging)
message TraceShred {
string region = 1;
google.protobuf.Timestamp created_at = 2;
uint32 seq_num = 3; // monotonically increases, resets on restart
}Solana Entry Structure
The Entry.entries field contains bincode-serialized Vec<solana_entry::entry::Entry>:
/// A Solana entry — a batch of transactions with a PoH hash
pub struct Entry {
pub num_hashes: u64, // PoH hashes since previous entry
pub hash: Hash, // resulting PoH hash (32 bytes)
pub transactions: Vec<VersionedTransaction>,
}
/// A versioned transaction (v0 or legacy)
pub struct VersionedTransaction {
pub signatures: Vec<Signature>, // first is the tx signature
pub message: VersionedMessage, // legacy or v0
}
/// Transaction message (v0 with ALT support)
pub struct v0::Message {
pub header: MessageHeader,
pub account_keys: Vec<Pubkey>, // static account keys
pub recent_blockhash: Hash,
pub instructions: Vec<CompiledInstruction>,
pub address_table_lookups: Vec<MessageAddressTableLookup>,
}
pub struct CompiledInstruction {
pub program_id_index: u8, // index into account_keys
pub accounts: Vec<u8>, // indices into account_keys
pub data: Vec<u8>, // instruction data (program-specific)
}Parsing Flow
Step 1: Receive Entry from gRPC
let entry_msg: Entry = stream.message().await?.unwrap();
let slot = entry_msg.slot;Step 2: Deserialize Entries
let entries: Vec<solana_entry::entry::Entry> =
bincode::deserialize(&entry_msg.entries)
.expect("failed to deserialize entries");Step 3: Extract Transactions
for entry in &entries {
for tx in &entry.transactions {
let signature = tx.signatures[0];
let message = tx.message();
// Static account keys
let account_keys = message.static_account_keys();
// Instructions
for ix in message.instructions() {
let program_id = account_keys[ix.program_id_index as usize];
let accounts: Vec<Pubkey> = ix.accounts
.iter()
.map(|&i| account_keys[i as usize])
.collect();
let data = &ix.data;
// Match by program ID
if program_id == pumpfun_program_id {
let discriminator = &data[..8];
// Parse instruction-specific data
}
}
}
}Step 4: Handle Address Lookup Tables (v0 Transactions)
v0 transactions may reference accounts via Address Lookup Tables (ALTs). These accounts are NOT in static_account_keys(). To resolve them:
// Check if transaction uses ALTs
if let VersionedMessage::V0(msg) = &tx.message {
for lookup in &msg.address_table_lookups {
// lookup.account_key = the ALT address
// lookup.writable_indexes = indices into ALT for writable accounts
// lookup.readonly_indexes = indices into ALT for readonly accounts
// To resolve: fetch the ALT account data via RPC
// let alt_data = rpc.get_account(&lookup.account_key).await?;
// Parse addresses from ALT data at the specified indices
}
}Important: Without resolving ALTs, you may miss some accounts referenced in instructions. For pre-execution use cases where speed matters, you can: 1. Pre-cache frequently used ALTs (Jupiter, Raydium, etc.) 2. Skip ALT resolution and work only with static keys (misses some accounts) 3. Resolve lazily after initial signal detection
What You Can Extract (Without Execution)
| Data | Available? | How |
|---|---|---|
| Transaction signature | Yes | tx.signatures[0] |
| Signer (fee payer) | Yes | account_keys[0] |
| Programs called | Yes | account_keys[ix.program_id_index] |
| Instruction data | Yes | ix.data (decode per program IDL) |
| Static account keys | Yes | message.static_account_keys() |
| ALT-referenced accounts | Partial | Need ALT data to resolve indices |
| Success/failure | No | Transaction hasn't executed yet |
| Balance changes | No | Need execution results |
| Log messages | No | Need execution results |
| Inner instructions (CPI) | No | Need execution results |
| Token balance changes | No | Need execution results |
| Compute units | No | Need execution results |
Comparison with Yellowstone gRPC Data
| Field | ShredStream Entry | Yellowstone SubscribeUpdateTransaction |
|---|---|---|
| Signature | tx.signatures[0] | info.signature |
| Slot | entry.slot | tx_update.slot |
| Account keys | msg.static_account_keys() | msg.account_keys (all resolved) |
| Instructions | msg.instructions() | msg.instructions |
| ALT resolution | Manual (need ALT data) | Automatic (loaded addresses in meta) |
| Execution status | Not available | meta.err |
| Balances | Not available | meta.pre_balances, meta.post_balances |
| Token balances | Not available | meta.pre_token_balances, meta.post_token_balances |
| Logs | Not available | meta.log_messages |
| Inner instructions | Not available | meta.inner_instructions |
| Compute units | Not available | meta.compute_units_consumed |
| Block position | Entry order within slot | info.index |
Pre-Execution Streaming — Provider Comparison
Overview
Three products offer pre-execution Solana data (transactions before block confirmation):
| Jito ShredStream | Shyft RabbitStream | Triton Deshred | |
|---|---|---|---|
| How it works | Raw shreds from Block Engine via UDP proxy | Shred-level extraction, Yellowstone-compatible format | Blockstore tap before Replay stage |
| Latency | 10-50ms from leader | 15-100ms faster than standard gRPC | ~6.3ms p50, ~20ms p90 |
| Self-hosted? | Yes (run proxy yourself) | No (managed service) | No (Triton infrastructure) |
| API format | Custom gRPC (SubscribeEntries) | Yellowstone gRPC (Subscribe) | Yellowstone gRPC (SubscribeDeshred) |
| Filtering | None (all entries) | Transaction filters only | Transaction filters (vote, accounts) |
| Cost | Free (beta) + server costs | From $199/mo (Shyft plan) | ~$2,900+/mo (Triton dedicated) |
| ALT resolution | No (raw transactions) | Yes | Yes |
Jito ShredStream (Direct)
Best for: Teams running their own infrastructure who want maximum control and lowest cost.
Pros
- Free during beta (no Jito fees)
- Open-source proxy (jito-labs/shredstream-proxy)
- Direct from Block Engine — no intermediary
- Can also feed shreds to your own RPC node for faster block building
- gRPC mode eliminates need for a full Solana node
Cons
- Requires public IP with UDP access (no NAT)
- Must run and maintain the proxy yourself
- No server-side filtering — you get everything, must filter client-side
- Approval process required (keypair whitelisting)
- Entry deserialization is bincode, not protobuf (slightly more work)
- Limited to 2 regions per proxy instance
Access
1. Generate keypair: solana-keygen new -o shred_auth.json 2. Submit public key at: https://web.miniextensions.com/WV3gZjFwqNqITsMufIEp 3. Wait for approval 4. Run proxy with --grpc-service-port to enable gRPC streaming
Regions
amsterdam, dublin, frankfurt, london, ny, salt-lake-city, singapore, tokyo
Shyft RabbitStream
Best for: Teams already using Shyft who want pre-execution data with minimal code changes.
Pros
- Drop-in replacement for Yellowstone gRPC — same
SubscribeRequestformat - No infrastructure to run — managed service
- Same x-token auth as regular Shyft gRPC
- Server-side transaction filtering (account_include, account_exclude, etc.)
- ALT resolution included
- Multiple regions available
Cons
- Only transaction filters work (no account/slot/block subscriptions)
- No
metafield (no execution results, as expected for pre-execution) - Requires Shyft Build plan or higher ($199+/mo)
- Slightly higher latency than direct ShredStream (goes through Shyft infrastructure)
Endpoints
rabbitstream.ny.shyft.to(New York)rabbitstream.va.shyft.to(Virginia)rabbitstream.ams.shyft.to(Amsterdam)rabbitstream.fra.shyft.to(Frankfurt)
Connection
Same as Yellowstone gRPC — just change the endpoint URL:
endpoint = "rabbitstream.ny.shyft.to" # instead of grpc.ny.shyft.to
# Everything else identicalTriton Deshred
Best for: HFT teams who need absolute lowest latency and can afford Triton pricing.
Pros
- Lowest measured latency: ~6.3ms p50, ~20ms p90
- Integrated into Yellowstone client (same library, different RPC method)
- Server-side filtering: vote, account_include, account_exclude, account_required
- ALT resolution included
- Historical data via Old Faithful
Cons
- Paid beta, limited availability (waitlist)
- Requires Triton dedicated node (~$2,900+/mo)
- Requires Triton's custom Agave validator fork (not stock Agave)
- New API surface (
SubscribeDeshredvsSubscribe)
RPC Method
rpc SubscribeDeshred(stream SubscribeDeshredRequest) returns (stream SubscribeUpdateDeshred) {}Filter Definition
message SubscribeRequestFilterDeshredTransactions {
optional bool vote = 1;
repeated string account_include = 2;
repeated string account_exclude = 3;
repeated string account_required = 4;
}Decision Matrix
| If you need... | Use... |
|---|---|
| Lowest cost, willing to run infra | Jito ShredStream (free + server) |
| Minimal code changes from Yellowstone | Shyft RabbitStream |
| Absolute lowest latency | Triton Deshred |
| No self-hosted infrastructure | Shyft RabbitStream or Triton Deshred |
| Server-side filtering | Shyft RabbitStream or Triton Deshred |
| Feed shreds to your own validator | Jito ShredStream |
Bundled Provider Access
Some RPC providers include ShredStream automatically:
- Chainstack — ShredStream on all Solana nodes
- RPC Fast — ShredStream gRPC as free add-on to dedicated nodes
- Everstake — Connect ($199/mo), Pro ($499/mo), Enterprise (custom)
- QuickNode — gRPC products on ShredStream-enabled leader nodes
#!/usr/bin/env python3
"""Parse and analyze entries from Jito ShredStream gRPC proxy.
Connects to a locally-running ShredStream proxy's gRPC endpoint, receives
decoded entries containing pre-execution transactions, and prints analysis
including program identification, signer detection, and transaction counts.
This demonstrates how to consume ShredStream data for early signal detection.
Usage:
python scripts/parse_shredstream_entries.py
Dependencies:
uv pip install grpcio grpcio-tools protobuf base58 solders
Environment Variables:
SHREDSTREAM_GRPC_URL: ShredStream proxy gRPC endpoint (default: localhost:7777)
Setup:
1. Run jito-shredstream-proxy with --grpc-service-port 7777
2. Generate Python stubs from jito-labs/mev-protos:
git clone https://github.com/jito-labs/mev-protos.git
python -m grpc_tools.protoc \\
-I./mev-protos/ \\
--python_out=./generated \\
--pyi_out=./generated \\
--grpc_python_out=./generated \\
./mev-protos/shredstream.proto ./mev-protos/shared.proto
Note: If you don't have a ShredStream proxy running, this script will
demonstrate the parsing logic with mock data when run with --demo flag.
"""
import os
import sys
import time
import struct
from typing import Optional
from dataclasses import dataclass, field
import base58
# ── Configuration ───────────────────────────────────────────────────
SHREDSTREAM_URL = os.getenv("SHREDSTREAM_GRPC_URL", "localhost:7777")
DEMO_MODE = "--demo" in sys.argv
# Known Solana program IDs for identification
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",
"Vote111111111111111111111111111111111111111": "Vote",
}
# Programs we care about for trading signals
TRADING_PROGRAMS = {
"6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P", # PumpFun
"PSwapMdSai8tjrEXcxFeQth87xC4rRsa4VA5mhGhXkP", # PumpSwap
"675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8", # Raydium AMM
"CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK", # Raydium CLMM
"whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc", # Orca
"LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo", # Meteora
"JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4", # Jupiter
}
# ── Data Structures ────────────────────────────────────────────────
@dataclass
class PreExecTransaction:
"""A pre-execution transaction parsed from ShredStream."""
signature: str
slot: int
signer: str
programs: list[str]
instruction_count: int
has_trading_program: bool
raw_instructions: list[dict] = field(default_factory=list)
@dataclass
class SlotStats:
"""Aggregated stats for a single slot."""
slot: int
entry_count: int = 0
tx_count: int = 0
trading_tx_count: int = 0
programs_seen: dict = field(default_factory=dict)
first_seen: float = 0.0
last_seen: float = 0.0
# ── Transaction Parsing ────────────────────────────────────────────
def parse_transaction_from_bytes(
tx_bytes: bytes, slot: int
) -> Optional[PreExecTransaction]:
"""Parse a serialized VersionedTransaction into a PreExecTransaction.
This is a simplified parser that handles the common case of legacy
transactions. Full v0 transaction parsing with ALT resolution requires
the solders library.
Args:
tx_bytes: Serialized VersionedTransaction bytes.
slot: The slot this transaction belongs to.
Returns:
Parsed PreExecTransaction, or None if parsing fails.
"""
try:
from solders.transaction import VersionedTransaction as SoldersVersionedTx
tx = SoldersVersionedTx.from_bytes(tx_bytes)
sig = str(tx.signatures[0])
msg = tx.message
account_keys = [str(k) for k in msg.account_keys()]
programs = []
instructions = []
has_trading = False
for ix in msg.instructions():
program_id = account_keys[ix.program_id_index]
label = KNOWN_PROGRAMS.get(program_id, program_id[:12] + "...")
programs.append(label)
if program_id in TRADING_PROGRAMS:
has_trading = True
disc = ix.data[:8].hex() if len(ix.data) >= 8 else ix.data.hex()
instructions.append({
"program": program_id,
"label": label,
"discriminator": disc,
"data_len": len(ix.data),
"account_count": len(ix.accounts),
})
signer = account_keys[0] if account_keys else "unknown"
return PreExecTransaction(
signature=sig,
slot=slot,
signer=signer,
programs=list(set(programs)),
instruction_count=len(instructions),
has_trading_program=has_trading,
raw_instructions=instructions,
)
except Exception as e:
return None
def parse_entry_batch(entries_bytes: bytes, slot: int) -> list[PreExecTransaction]:
"""Parse a batch of entries from ShredStream.
The entries field is bincode-serialized Vec<Entry>. Each Entry contains
a list of VersionedTransactions. This function uses solders for
deserialization.
Args:
entries_bytes: Raw bytes from the Entry.entries field.
slot: The slot number for context.
Returns:
List of parsed PreExecTransactions.
"""
try:
from solders.entry import Entry as SoldersEntry
entries = SoldersEntry.from_bytes_vec(entries_bytes)
transactions = []
for entry in entries:
for tx in entry.transactions:
parsed = parse_transaction_from_bytes(bytes(tx), slot)
if parsed:
transactions.append(parsed)
return transactions
except ImportError:
print("WARNING: solders not installed. Install with: uv pip install solders")
print("Falling back to raw byte analysis.")
return []
except Exception as e:
print(f"Entry parse error: {e}")
return []
# ── Statistics Tracking ─────────────────────────────────────────────
class StreamAnalyzer:
"""Tracks and reports statistics from the ShredStream."""
def __init__(self) -> None:
self.total_entries = 0
self.total_txs = 0
self.total_trading_txs = 0
self.program_counts: dict[str, int] = {}
self.slots_seen: dict[int, SlotStats] = {}
self.start_time = time.time()
def process_slot_entry(
self, slot: int, transactions: list[PreExecTransaction]
) -> SlotStats:
"""Process a batch of transactions from a slot entry.
Args:
slot: The slot number.
transactions: Parsed pre-execution transactions.
Returns:
Updated SlotStats for this slot.
"""
now = time.time()
if slot not in self.slots_seen:
self.slots_seen[slot] = SlotStats(slot=slot, first_seen=now)
stats = self.slots_seen[slot]
stats.entry_count += 1
stats.last_seen = now
for tx in transactions:
stats.tx_count += 1
self.total_txs += 1
if tx.has_trading_program:
stats.trading_tx_count += 1
self.total_trading_txs += 1
for prog in tx.programs:
self.program_counts[prog] = self.program_counts.get(prog, 0) + 1
stats.programs_seen[prog] = stats.programs_seen.get(prog, 0) + 1
self.total_entries += 1
# Prune old slots (keep last 100)
if len(self.slots_seen) > 100:
oldest = sorted(self.slots_seen.keys())[:-100]
for s in oldest:
del self.slots_seen[s]
return stats
def summary(self) -> str:
"""Generate a summary of stream statistics."""
elapsed = time.time() - self.start_time
tps = self.total_txs / elapsed if elapsed > 0 else 0
trading_pct = (
self.total_trading_txs / self.total_txs * 100
if self.total_txs > 0
else 0
)
lines = [
f"\n{'='*60}",
f"ShredStream Analysis Summary",
f"{'='*60}",
f"Duration: {elapsed:.1f}s",
f"Entries: {self.total_entries}",
f"Transactions: {self.total_txs} ({tps:.1f}/s)",
f"Trading TXs: {self.total_trading_txs} ({trading_pct:.1f}%)",
f"Slots seen: {len(self.slots_seen)}",
f"\nTop Programs:",
]
for prog, count in sorted(
self.program_counts.items(), key=lambda x: -x[1]
)[:10]:
lines.append(f" {prog}: {count}")
return "\n".join(lines)
# ── Demo Mode ───────────────────────────────────────────────────────
def run_demo() -> None:
"""Run a demonstration with synthetic data to show parsing logic."""
print("Running in DEMO mode (no live ShredStream connection)")
print("This demonstrates the parsing and analysis logic.\n")
analyzer = StreamAnalyzer()
# Simulate some pre-execution transactions
demo_txs = [
PreExecTransaction(
signature="5wHu1" + "A" * 83,
slot=300_000_000,
signer="Whale1" + "1" * 38,
programs=["PumpFun", "Token", "System"],
instruction_count=4,
has_trading_program=True,
),
PreExecTransaction(
signature="3kXp2" + "B" * 83,
slot=300_000_000,
signer="Trader" + "2" * 38,
programs=["Jupiter-V6", "Raydium-AMM", "Token"],
instruction_count=7,
has_trading_program=True,
),
PreExecTransaction(
signature="7mNq4" + "C" * 83,
slot=300_000_000,
signer="User33" + "3" * 38,
programs=["System"],
instruction_count=1,
has_trading_program=False,
),
]
stats = analyzer.process_slot_entry(300_000_000, demo_txs)
print(f"Slot {stats.slot}: {stats.tx_count} txs, {stats.trading_tx_count} trading")
for tx in demo_txs:
marker = " [TRADE]" if tx.has_trading_program else ""
print(f" {tx.signature[:12]}... | {tx.signer[:12]}... | {', '.join(tx.programs)}{marker}")
print(analyzer.summary())
print("\nTo run against a live ShredStream proxy:")
print(" 1. Start jito-shredstream-proxy with --grpc-service-port 7777")
print(" 2. Run this script without --demo flag")
# ── Live Mode ───────────────────────────────────────────────────────
def run_live() -> None:
"""Connect to ShredStream proxy and process live entries."""
try:
import grpc
from generated import shredstream_pb2, shredstream_pb2_grpc # type: ignore
except ImportError:
print("ERROR: Generated protobuf stubs not found.")
print("Generate from jito-labs/mev-protos (see docstring for instructions).")
print("\nOr run with --demo flag to see parsing logic without live data.")
sys.exit(1)
print(f"Connecting to ShredStream proxy at {SHREDSTREAM_URL}...")
channel = grpc.insecure_channel(
SHREDSTREAM_URL,
options=[("grpc.max_receive_message_length", 64 * 1024 * 1024)],
)
stub = shredstream_pb2_grpc.ShredstreamProxyStub(channel)
analyzer = StreamAnalyzer()
entry_count = 0
try:
request = shredstream_pb2.SubscribeEntriesRequest()
stream = stub.SubscribeEntries(request)
print("Connected. Receiving entries...\n")
for entry_msg in stream:
slot = entry_msg.slot
transactions = parse_entry_batch(entry_msg.entries, slot)
stats = analyzer.process_slot_entry(slot, transactions)
# Print trading transactions immediately
for tx in transactions:
if tx.has_trading_program:
programs = ", ".join(tx.programs)
print(
f"[{slot}] {tx.signature[:16]}... "
f"| {tx.signer[:12]}... "
f"| {programs}"
)
entry_count += 1
if entry_count % 1000 == 0:
elapsed = time.time() - analyzer.start_time
print(
f"\n--- {entry_count} entries, "
f"{analyzer.total_txs} txs, "
f"{analyzer.total_trading_txs} trading, "
f"{elapsed:.0f}s ---\n"
)
except grpc.RpcError as e:
print(f"\ngRPC error: {e}")
except KeyboardInterrupt:
pass
finally:
print(analyzer.summary())
# ── Main ────────────────────────────────────────────────────────────
if __name__ == "__main__":
if DEMO_MODE:
run_demo()
else:
run_live()
#!/usr/bin/env python3
"""Monitor pre-execution transactions via Shyft RabbitStream.
RabbitStream uses the same Yellowstone gRPC protocol but delivers transactions
from the shred level — before execution. This script connects to RabbitStream,
filters for DEX-related transactions, and prints early trading signals.
The key advantage: same code as standard Yellowstone gRPC, just a different
endpoint. No infrastructure to run.
Usage:
python scripts/rabbitstream_monitor.py
Dependencies:
uv pip install grpcio grpcio-tools protobuf base58 python-dotenv
Environment Variables:
RABBITSTREAM_ENDPOINT: RabbitStream endpoint (default: rabbitstream.ny.shyft.to)
GRPC_TOKEN: Your Shyft x-token (same token as regular gRPC)
TARGET_PROGRAMS: Comma-separated program IDs to filter (optional)
Setup:
Generate Yellowstone protobuf stubs first (same as yellowstone-grpc skill):
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
"""
import os
import sys
import time
import queue
import threading
from datetime import datetime, timezone
from typing import Optional
import base58
import grpc
# ── Configuration ───────────────────────────────────────────────────
RABBITSTREAM_ENDPOINT = os.getenv(
"RABBITSTREAM_ENDPOINT", "rabbitstream.ny.shyft.to"
)
GRPC_TOKEN = os.getenv("GRPC_TOKEN", "")
if not GRPC_TOKEN:
print("Set GRPC_TOKEN environment variable (your Shyft x-token)")
print(" export GRPC_TOKEN='your-shyft-x-token'")
sys.exit(1)
# Default: watch PumpFun. Override with TARGET_PROGRAMS env var.
DEFAULT_PROGRAMS = [
"6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P", # PumpFun
]
TARGET_PROGRAMS_STR = os.getenv("TARGET_PROGRAMS", "")
TARGET_PROGRAMS = (
[p.strip() for p in TARGET_PROGRAMS_STR.split(",") if p.strip()]
if TARGET_PROGRAMS_STR
else DEFAULT_PROGRAMS
)
MAX_QUEUE_SIZE = 10_000
MAX_RECONNECT_DELAY = 60.0
# Program labels for display
PROGRAM_LABELS = {
"6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P": "PumpFun",
"PSwapMdSai8tjrEXcxFeQth87xC4rRsa4VA5mhGhXkP": "PumpSwap",
"675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8": "Raydium-AMM",
"CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK": "Raydium-CLMM",
"CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C": "Raydium-CPMM",
"whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc": "Orca",
"LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo": "Meteora-DLMM",
"JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4": "Jupiter-V6",
}
# ── Stub Import ─────────────────────────────────────────────────────
try:
from generated import geyser_pb2, geyser_pb2_grpc # type: ignore
except ImportError:
print("ERROR: Yellowstone protobuf stubs not found.")
print("Generate them first (see docstring for instructions).")
sys.exit(1)
# ── Connection ──────────────────────────────────────────────────────
def create_channel(endpoint: str, token: str) -> grpc.Channel:
"""Create authenticated TLS channel for RabbitStream.
Args:
endpoint: RabbitStream endpoint (without https://).
token: Shyft x-token.
Returns:
Configured gRPC channel.
"""
clean = endpoint.replace("https://", "").replace("http://", "")
auth = grpc.metadata_call_credentials(
lambda ctx, cb: cb((("x-token", token),), None)
)
return grpc.secure_channel(
clean,
grpc.composite_channel_credentials(grpc.ssl_channel_credentials(), auth),
options=[("grpc.max_receive_message_length", 64 * 1024 * 1024)],
)
# ── Transaction Parsing (Pre-Execution) ────────────────────────────
def parse_pre_exec_tx(tx_update) -> Optional[dict]:
"""Parse a RabbitStream transaction update.
RabbitStream transactions have the same structure as Yellowstone, but the
meta field is empty (no execution results). We extract what's available:
signature, signer, programs called, and instruction discriminators.
Args:
tx_update: SubscribeUpdateTransaction from RabbitStream.
Returns:
Dict with pre-execution transaction details, or None on error.
"""
try:
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]
signer = account_keys[0] if account_keys else "unknown"
programs_called = []
instructions = []
for ix in msg.instructions:
program_id = account_keys[ix.program_id_index]
label = PROGRAM_LABELS.get(program_id, program_id[:12] + "...")
programs_called.append(label)
disc = ix.data[:8].hex() if len(ix.data) >= 8 else ix.data.hex()
instructions.append({
"program": program_id,
"label": label,
"discriminator": disc,
"data_len": len(ix.data),
"num_accounts": len(ix.accounts),
})
# Note: meta fields are empty for pre-execution data
# No balance changes, no logs, no inner instructions
return {
"signature": sig,
"slot": slot,
"signer": signer,
"programs": list(dict.fromkeys(programs_called)), # deduplicate
"instructions": instructions,
"is_pre_execution": True,
"received_at": time.time(),
}
except Exception as e:
return None
# ── Streaming ───────────────────────────────────────────────────────
def stream_rabbitstream(
endpoint: str,
token: str,
programs: list[str],
msg_queue: queue.Queue,
) -> None:
"""Stream pre-execution transactions from RabbitStream.
Args:
endpoint: RabbitStream endpoint.
token: Shyft x-token.
programs: Program IDs to filter.
msg_queue: Queue for parsed transaction dicts.
"""
delay = 0.1
last_slot: Optional[int] = None
while True:
try:
channel = create_channel(endpoint, token)
stub = geyser_pb2_grpc.GeyserStub(channel)
# Same subscription format as Yellowstone — that's the beauty
request = geyser_pb2.SubscribeRequest(
transactions={
"target": geyser_pb2.SubscribeRequestFilterTransactions(
account_include=programs,
vote=False,
failed=False,
)
},
commitment=geyser_pb2.CommitmentLevel.PROCESSED,
)
if last_slot:
request.from_slot = last_slot - 32
labels = [PROGRAM_LABELS.get(p, p[:12]) for p in programs]
print(f"Connecting to RabbitStream ({endpoint})...")
print(f"Filtering: {', '.join(labels)}")
stream = stub.Subscribe(iter([request]))
delay = 0.1
print("Connected. Streaming pre-execution transactions...\n")
for update in stream:
if update.HasField("transaction"):
last_slot = update.transaction.slot
parsed = parse_pre_exec_tx(update.transaction)
if parsed:
try:
msg_queue.put_nowait(parsed)
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)
# ── Display ─────────────────────────────────────────────────────────
def format_pre_exec_signal(tx: dict) -> str:
"""Format a pre-execution transaction as a readable signal.
Args:
tx: Parsed transaction dict from parse_pre_exec_tx.
Returns:
Formatted string for terminal display.
"""
ts = datetime.now(timezone.utc).strftime("%H:%M:%S.%f")[:-3]
sig = tx["signature"][:16]
signer = tx["signer"][:12]
programs = ", ".join(p for p in tx["programs"]
if p not in ("System", "ComputeBudget", "Token", "ATA"))
lines = [f"[{ts}] PRE-EXEC | Slot {tx['slot']} | {sig}..."]
lines.append(f" Signer: {signer}... | Programs: {programs}")
# Show instruction details for trading programs
for ix in tx["instructions"]:
if ix["program"] in PROGRAM_LABELS:
lines.append(
f" -> {ix['label']}: disc={ix['discriminator']} "
f"({ix['data_len']}B, {ix['num_accounts']} accounts)"
)
return "\n".join(lines)
# ── Main ────────────────────────────────────────────────────────────
def main() -> None:
"""Entry point: stream pre-execution transactions and display signals."""
print("=" * 60)
print("RabbitStream Pre-Execution Monitor")
print("=" * 60)
print(f"Endpoint: {RABBITSTREAM_ENDPOINT}")
print(f"Programs: {len(TARGET_PROGRAMS)}")
for p in TARGET_PROGRAMS:
label = PROGRAM_LABELS.get(p, "Unknown")
print(f" {label}: {p}")
print()
print("NOTE: These are PRE-EXECUTION signals. Transactions may")
print("ultimately fail. Use Yellowstone gRPC for confirmation.")
print("=" * 60)
print()
msg_queue: queue.Queue = queue.Queue(maxsize=MAX_QUEUE_SIZE)
seen: set = set()
count = 0
start = time.time()
reader = threading.Thread(
target=stream_rabbitstream,
args=(RABBITSTREAM_ENDPOINT, GRPC_TOKEN, TARGET_PROGRAMS, msg_queue),
daemon=True,
)
reader.start()
try:
while True:
try:
tx = msg_queue.get(timeout=30.0)
except queue.Empty:
elapsed = time.time() - start
print(f"[{elapsed:.0f}s] No signals in 30s. Total: {count}")
continue
if tx["signature"] in seen:
continue
seen.add(tx["signature"])
if len(seen) > 50_000:
seen.clear()
count += 1
print(format_pre_exec_signal(tx))
print()
except KeyboardInterrupt:
elapsed = time.time() - start
rate = count / elapsed if elapsed > 0 else 0
print(f"\n{count} pre-execution signals in {elapsed:.1f}s ({rate:.1f}/s)")
if __name__ == "__main__":
main()
Related skills
FAQ
What data does ShredStream give you before execution?
Transaction signatures, account keys, instructions, address lookup table references, and slot number. It does not include success/failure status, balance changes, logs, or compute units consumed.
How much faster is ShredStream than standard gRPC?
Typically 100-500ms earlier than standard Yellowstone gRPC, since it receives shreds directly from leader validators via Jito's Block Engine instead of through Turbine.