PumpFun Sniper Technical Deep-Dive: Architecture, Performance, and ShredStream Integration
A comprehensive technical analysis of the PumpFun Sniper bot architecture, including ShredStream integration, pre-confirmation detection, AllenHark Relay optimization, and design tradeoffs.
The PumpFun Sniper represents a significant advancement in Solana trading bot architecture. This technical deep-dive explores the engineering decisions, performance optimizations, and infrastructure integrations that let it act on a launch while the block is still being produced, before RPC confirmation.
Table of Contents
- Architecture Overview
- ShredStream Integration
- Detection Pipeline
- Transaction Execution
- AllenHark Relay Integration
- Performance Benchmarks
- Configuration Deep-Dive
- Advanced Features
Architecture Overview
The PumpFun Sniper is built on a lock-free, event-driven architecture designed to minimize latency at every stage of the detection-to-execution pipeline.
Core Components
graph LR
A[ShredStream] -->|UDP Packets| B[Shred Processor]
B -->|Parsed Transactions| C[Filter Layer]
C -->|PumpFun Creates| D[Signal Generator]
D -->|Token Mint| E[Transaction Builder]
E -->|Signed TX| F[AllenHark Relay]
F -->|QUIC/UDP| G[Validator TPU]Where the time goes, in order:
- ShredStream ingestion: receiving the entry from the shred feed
- Transaction parsing: decoding it into instructions
- Filter evaluation: program ID, discriminator and spam checks
- Signal generation: deciding to buy
- Transaction building: filling the pre-built template and signing
- Relay submission: handing the signed transaction to AllenHark Relay
Each stage is instrumented, so you can see your own budget rather than trusting ours.
Why Rust?
The bot is implemented in Rust for several critical reasons:
- Zero-cost abstractions: No runtime overhead for high-level constructs
- No garbage collection: Eliminates unpredictable GC pauses, which Node.js and Python both have
- Memory safety: Prevents crashes without runtime checks
- Lock-free concurrency: Uses atomic operations and channels for thread communication
- LLVM optimization: Produces highly optimized machine code
Transaction parsing by language
| Language | Garbage collector | Memory layout |
|---|---|---|
| Rust | None | Structs, no boxing |
| Node.js | Yes, with pauses | Objects on the heap |
| Python | Yes, with pauses | Objects on the heap |
ShredStream Integration
ShredStream provides access to raw Turbine protocol data - the UDP packets validators exchange before block finalization. This lets the bot see a transaction while its block is still being built, before any RPC reports it.
How ShredStream Works
Solana validators use the Turbine protocol to propagate blocks as "shreds" (fragments). By tapping into this stream, we can:
- Detect transactions before RPC confirmation
- Process transactions in parallel with validators
- Avoid RPC polling overhead (no waiting for the next poll)
Integration Code
The bot connects to a local ShredStream proxy via gRPC:
use tonic::transport::Channel;
use shredstream_proto::shred_stream_client::ShredStreamClient;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Connect to local ShredStream proxy
let channel = Channel::from_static("http://127.0.0.1:9999")
.connect()
.await?;
let mut client = ShredStreamClient::new(channel);
// Subscribe to transaction stream
let request = SubscribeRequest {
filter_accounts: vec![PUMPFUN_PROGRAM_ID.to_string()],
..Default::default()
};
let mut stream = client.subscribe(request).await?.into_inner();
// Process shreds in real-time
while let Some(shred) = stream.message().await? {
process_shred(shred).await;
}
Ok(())
}Performance Characteristics
- Timing: data arrives while the block is still being produced
- Throughput: the full shred stream of every slot, votes included
- Delivery: pushed to you, no polling
Detection Pipeline
The detection pipeline uses a multi-stage filter, cheapest check first, so almost no CPU is spent on transactions that are not PumpFun creates.
Stage 1: Program ID Filter
const PUMPFUN_PROGRAM: Pubkey = pubkey!("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P");
fn quick_filter(tx: &VersionedTransaction) -> bool {
// Fast path: check if PumpFun program is in account keys
tx.message.static_account_keys().contains(&PUMPFUN_PROGRAM)
}This throws away nearly every transaction (votes, transfers, other programs) with one lookup in the account keys.
Stage 2: Instruction Discriminator
const CREATE_DISCRIMINATOR: [u8; 8] = [24, 30, 200, 40, 5, 28, 7, 119];
fn is_create_instruction(ix: &CompiledInstruction) -> bool {
ix.data.len() >= 8 && ix.data[..8] == CREATE_DISCRIMINATOR
}The discriminator is computed as sha256("global:create")[..8] and identifies token creation instructions.
Stage 3: Spam Filter
The spam filter evaluates multiple heuristics to avoid rug pulls:
pub struct SpamFilter {
min_buy_sol: f64,
max_buy_sol: f64,
min_tokens: u64,
min_accounts: usize,
}
impl SpamFilter {
fn evaluate(&self, tx: &ParsedCreate) -> bool {
// Check dev buy amount
if tx.dev_buy_sol < self.min_buy_sol || tx.dev_buy_sol > self.max_buy_sol {
return false;
}
// Check token amount
if tx.token_amount < self.min_tokens {
return false;
}
// Check account count (malformed txs have fewer accounts)
if tx.accounts.len() < self.min_accounts {
return false;
}
true
}
}Stage 4: Blacklist Check
The blacklist is stored in a lock-free concurrent hash set for O(1) lookups:
use dashmap::DashSet;
static BLACKLIST: Lazy<DashSet<Pubkey>> = Lazy::new(DashSet::new);
fn is_blacklisted(creator: &Pubkey) -> bool {
BLACKLIST.contains(creator)
}The blacklist is reloaded every 10 seconds from blacklist.jsonl without blocking the hot path.
Transaction Execution
Once a token passes all filters, the bot has to build and submit a transaction before everyone else who saw the same create.
Pre-computed Transaction Template
To minimize latency, we pre-build a transaction template with placeholder accounts:
struct TransactionTemplate {
instructions: Vec<Instruction>,
recent_blockhash: Hash,
payer: Pubkey,
}
impl TransactionTemplate {
fn instantiate(&self, mint: Pubkey) -> Transaction {
// Clone template and replace mint address
let mut tx = self.clone();
tx.instructions[0].accounts[MINT_INDEX] = AccountMeta::new(mint, false);
// Sign transaction
tx.sign(&[&PAYER_KEYPAIR], self.recent_blockhash);
tx
}
}Blockhash Management
A background thread maintains a cache of the latest blockhash:
static BLOCKHASH_CACHE: Lazy<RwLock<Hash>> = Lazy::new(|| RwLock::new(Hash::default()));
async fn blockhash_updater(rpc_client: Arc<RpcClient>) {
loop {
if let Ok(hash) = rpc_client.get_latest_blockhash().await {
*BLOCKHASH_CACHE.write().unwrap() = hash;
}
tokio::time::sleep(Duration::from_millis(400)).await; // Update every slot
}
}This ensures we always have a fresh blockhash without blocking the critical path.
AllenHark Relay Integration
The AllenHark Relay provides optimized transaction submission straight to the leader's TPU.
Why Use a Relay?
Standard RPC submission has several bottlenecks:
- HTTP overhead: connection setup and headers on every request
- RPC processing: validation and forwarding by the node
- Gossip propagation: the transaction has to find the leader
- Shared path: Transactions may pass through several nodes on the way
AllenHark Relay eliminates these bottlenecks:
- QUIC protocol: a persistent connection, so no handshake when the transaction is ready
- Straight to the leader: private connections to the current leader, never public gossip
- Jito bundles: up to five transactions, atomic and in order
- Persistent connections: No connection setup overhead
QUIC Integration
use quinn::{ClientConfig, Endpoint};
async fn send_via_relay(tx: &Transaction) -> Result<(), Box<dyn std::error::Error>> {
// Create QUIC endpoint
let mut endpoint = Endpoint::client("0.0.0.0:0".parse()?)?;
endpoint.set_default_client_config(ClientConfig::with_native_roots());
// Connect to AllenHark Relay
let connection = endpoint
.connect("88.216.36.108:4433".parse()?, "relay.allenhark.com")?
.await?;
// Open bidirectional stream
let (mut send, mut recv) = connection.open_bi().await?;
// Serialize transaction
let tx_bytes = bincode::serialize(tx)?;
// Build the relay packet and send it
let packet = build_relay_packet(&tx_bytes, TIP_LAMPORTS)?;
send.write_all(&packet).await?;
send.finish().await?;
// Read response
let response = recv.read_to_end(1024).await?;
Ok(())
}Performance Comparison
| Method | Dispatch cost | Cost |
|---|---|---|
| AllenHark Relay (QUIC) | No per-request handshake — the connection is already open | Relay tip per transaction, from 0.001 SOL |
| AllenHark Relay (HTTPS) | One HTTPS request per transaction | Relay tip per transaction, from 0.001 SOL |
| Standard RPC | Validation, then gossip propagation | Free |
| Jito (Public) | Auction round before submission | Variable |
What to Measure
We don't publish latency or win-rate figures for this bot, because the numbers that matter are yours: they depend on where your box sits relative to the relay and the stake, how aggressive your filters are, and what the rest of the field is doing on the day. Anyone quoting a P99 without telling you the vantage point is selling you something.
Instrument these yourself and you'll know whether your setup is actually competitive:
- Detection: time from the entry arriving on the shred feed to your filter firing.
- End-to-end: detection → signed transaction handed to the relay.
- Landing: what fraction of attempts land in the intended slot, not just eventually.
- Filter yield: how many launches survive your filters, and how many of those you'd actually have wanted.
Configuration Deep-Dive
Trading Parameters
purchase_amount_sol = 0.001
slippage_basis_points = 100
live = truepurchase_amount_sol: Position size per snipe. Start with 0.001 SOL for testing.slippage_basis_points: 100 basis points is one hundredth of the price. Increase for volatile tokens.live: Set tofalsefor dry-run mode (no real transactions).
Dev Buy Filter
min_dev_buy_sol = 0.5
max_dev_buy_sol = 4.5Filters tokens based on the developer's initial buy:
- Too low (< 0.5 SOL): Likely spam or low-effort launch
- Too high (> 4.5 SOL): Risk of immediate dump
User Volume Accumulator
The user_volume_accumulator is a PDA required by PumpFun to track trading volume:
fn derive_accumulator(user: &Pubkey) -> Pubkey {
let pumpfun = pubkey!("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P");
let fee_program = pubkey!("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ");
Pubkey::find_program_address(
&[
b"user-volume-accumulator",
user.as_ref(),
pumpfun.as_ref(),
],
&fee_program,
).0
}Advanced Features
Automatic Blacklisting
The bot tracks creator performance and automatically blacklists unprofitable wallets:
fn update_blacklist(creator: Pubkey, profit: f64) {
if profit < -0.001 { // Lost more than 0.001 SOL
BLACKLIST.insert(creator);
append_to_file("blacklist.jsonl", &creator);
}
}Profit Tracking
After each sell, the bot calculates profit including rent recovery:
fn calculate_profit(initial_balance: u64, final_balance: u64, rent: u64) -> f64 {
let profit_lamports = (final_balance + rent) as i64 - initial_balance as i64;
profit_lamports as f64 / 1e9 // Convert to SOL
}RPC Pool Rotation
The bot rotates through multiple RPC endpoints for load balancing and redundancy:
struct RpcPool {
endpoints: Vec<String>,
current: AtomicUsize,
}
impl RpcPool {
fn next(&self) -> &str {
let idx = self.current.fetch_add(1, Ordering::Relaxed) % self.endpoints.len();
&self.endpoints[idx]
}
}Conclusion
The PumpFun Sniper demonstrates that competitive Solana trading requires:
- Low-level infrastructure access (ShredStream)
- Optimized execution (Rust, lock-free architecture)
- Direct network connectivity (AllenHark Relay, QUIC)
- Intelligent filtering (spam detection, blacklisting)
By combining these elements, the bot detects a launch while the block is still being produced — before RPC confirmation — and goes for first-block execution, which an RPC-polling bot structurally cannot do.
Download the PumpFun Sniper: Latest Release
Full Documentation: PumpFun Sniper Docs
AllenHark Services: ShredStream | Relay | Co-Location
