Detect New Pump.fun Tokens and Pools in Real Time
Detect new Pump.fun tokens and PumpSwap and Raydium pools: program IDs, discriminators, working gRPC code, and why bots see launches late or miss them.
Every new Pump.fun token and every new PumpSwap or Raydium pool is one instruction in one transaction. To catch it you need three things: the program and instruction to look for, where the mint and pool sit in that instruction's accounts, and a feed that gets the transaction to you early. This post lists each launch and pool instruction with its program ID and discriminator, compares the ways to listen, goes through the reasons bots see launches late or miss them, and gives working Rust and Node.js code for Yellowstone gRPC.
Every program ID and discriminator below comes from the IDLs in our Solana IDL library, recomputed and checked against the programs' own current IDLs. The one instruction our copy lacks, CPMM's initialize_with_permission, comes from Raydium's current IDL. On October 30, 2026 we also pulled recent launches and pools from mainnet to check the account positions and to see what launches look like today. Some of what we found explains a lot of missed launches.
What a new token or pool looks like on-chain
Anchor programs pick the instruction with the first 8 bytes of its data, the discriminator: the first 8 bytes of sha256("global:<instruction_name>"). Pump.fun, PumpSwap, Raydium LaunchLab and Raydium CPMM are Anchor programs. Raydium AMM v4 is not: its first data byte is a tag, the instruction's number, and initialize2 is tag 1 in Raydium's source.
The programs:
| Program | Program ID | IDL |
|---|---|---|
| Pump.fun | 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P | Pump.fun IDL |
| PumpSwap | pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA | PumpSwap IDL |
| Raydium LaunchLab | LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj | LaunchLab IDL |
| Raydium CPMM | CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C | CPMM IDL |
| Raydium AMM v4 | 675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8 | AMM v4 IDL |
The instructions. Positions count from 0 in the instruction's own account list, in the IDL's order:
| Event | Instruction | Discriminator | Accounts to read |
|---|---|---|---|
| New Pump.fun token (SPL Token) | Pump.fun create | [24, 30, 200, 40, 5, 28, 7, 119] | mint 0, bonding curve 2, user 7 |
| New Pump.fun token (Token-2022) | Pump.fun create_v2 | [214, 144, 76, 236, 95, 139, 49, 180] | mint 0, bonding curve 2, user 5 |
| Pump.fun graduation | Pump.fun migrate, migrate_v2 | [155, 234, 231, 146, 236, 158, 162, 30], [187, 203, 18, 31, 206, 237, 254, 41] | Read the create_pool it calls, below |
| New PumpSwap pool | PumpSwap create_pool | [233, 146, 209, 142, 207, 104, 64, 188] | pool 0, base mint 3, quote mint 4 |
| New LaunchLab token | LaunchLab initialize | [175, 175, 109, 31, 13, 152, 155, 237] | pool 5, base mint 6, quote mint 7 |
| New LaunchLab token | LaunchLab initialize_v2 | [67, 153, 175, 39, 218, 16, 38, 32] | pool 5, base mint 6, quote mint 7 |
| New LaunchLab token (Token-2022) | LaunchLab initialize_with_token_2022 | [37, 190, 126, 222, 44, 154, 171, 17] | pool 5, base mint 6, quote mint 7 |
| LaunchLab graduation to CPMM | LaunchLab migrate_to_cpswap | [136, 92, 200, 103, 28, 218, 144, 140] | Read the CPMM call it makes, below |
| New CPMM pool | CPMM initialize | [175, 175, 109, 31, 13, 152, 155, 237] | pool 3, token 0 mint 4, token 1 mint 5 |
| New CPMM pool, permissioned | CPMM initialize_with_permission | [63, 55, 254, 65, 49, 178, 89, 121] | pool 4, token 0 mint 5, token 1 mint 6 |
| New AMM v4 pool | AMM v4 initialize2 | tag byte 1 | pool 4, coin mint 8, pc mint 9 |
What the table doesn't tell you:
- Check the program and the discriminator together.
initializehashes to the same 8 bytes in every Anchor program, so LaunchLab's, CPMM's and Pump.fun's own one-time setup instruction all start[175, 175, 109, 31, …]. Only the program ID tells them apart. - Pump.fun launches are
create_v2now. All 13 recent launches we pulled used it.create_v2makes a Token-2022 mint whose metadata lives on the mint itself, according to Pump's coin creation docs, so there is no Metaplex metadata account. The name, symbol and URI are in the instruction data. - Don't test the data length or the account count. Pump.fun adds optional arguments to the end of
create_v2(the IDL in our library lists 6 arguments, Pump's current one 8), and a coin priced in something other than SOL passes extra accounts after the 16 in the IDL: 3 for a quote mint, more when the quote is another Pump.fun coin (Pump's docs). We sawcreate_v2with 16, 19, 20 and 24 accounts. The discriminator and the account order haven't changed. - A graduation is two instructions. Pump.fun's
migratemoves a finished bonding curve into PumpSwap by calling PumpSwap'screate_pool(a cross-program invocation, or CPI). Pools made this way use pool index 0, per Pump's PumpSwap docs.migrateis open to anyone and does nothing on a curve that has already migrated (Pump program docs): two of the threemigrate_v2transactions we pulled succeeded and logged "Bonding curve already migrated". Key a graduation on the innercreate_pool, never onmigratealone. - LaunchLab graduations use a CPMM instruction older IDLs don't have.
migrate_to_cpswapcalls CPMM'sinitialize_with_permission. The CPMM IDL in our library predates it; Raydium's current IDL has it, with the discriminator and accounts above. Of 48 CPMM pools created on October 30 that we checked, 5 came from LaunchLab migrations this way and 2 more calledinitialize_with_permissiondirectly. A bot that only knowsinitializemisses every LaunchLab graduation into CPMM. - AMM v4 is mostly the past for launchpads. Per our Block-0 Sniper docs, LaunchLab tokens launched since 17 August 2026 graduate to CPMM, and older launches to AMM v4. New AMM v4 pools still appear;
initialize2data is 26 bytes: the tag, then a nonce, open time and the two starting amounts.
Reading the accounts: indexes, lookup tables and CPI
An instruction in a transaction is three fields: program_id_index, accounts (a list of 1-byte indexes) and data. The indexes point into the transaction's account key list, not into the IDL. Position 0 in the table above means "the key at accounts[0]".
Which list? In a legacy transaction, it's the message's account keys. In a v0 transaction, accounts can also come from address lookup tables, and the Solana docs put the full list together like this: the static keys first, then the writable addresses loaded from lookup tables, then the read-only ones. Yellowstone gRPC sends the loaded addresses in the transaction's meta (loaded_writable_addresses, loaded_readonly_addresses), so the list is those three, concatenated. The newer v1 format has no lookup tables, so for v1 the static keys are the whole list.
Signers are always static keys: the same docs say only non-signers can be loaded from a lookup table. In Pump.fun's create and create_v2, and in LaunchLab's initialize instructions, the new mint signs, so you can always read it from the static keys. The bonding curve, the pool, the quote mint and the user's token accounts can come from a lookup table. In one create_v2 transaction we pulled, 4 of its 17 account keys came from lookup tables.
Instructions made by CPI are not in the message at all. They are in the meta, in inner_instructions, grouped by the top-level instruction they ran under (index), and they use the same account key list. That's where every graduation pool is, and some launches too.
Four ways to listen, and what each one sees
| Method | What you get | When it arrives | CPI launches |
|---|---|---|---|
Polling (getSignaturesForAddress, then getTransaction) | Signatures, then the full transaction and meta | After your next poll; getTransaction does not accept processed, so confirmed at the earliest | Yes, in innerInstructions |
RPC WebSocket logsSubscribe | Signature, error and log lines; mentions takes exactly one address | When the slot reaches your commitment; the default is finalized | Yes, inner programs log too |
RPC WebSocket programSubscribe | Writes to accounts the program owns | When the slot reaches your commitment; the default is finalized | Yes: it watches accounts, not instructions |
Yellowstone gRPC transactions at processed | The full transaction and meta: inner instructions, loaded addresses, logs, error | As the node executes each transaction | Yes, in inner_instructions |
| Shreds | The block's transactions as the leader sends them | Before your node has executed anything | No: you only see top-level instructions |
From earliest to latest: shreds, then gRPC at processed, then anything that waits for confirmation, then polling. The reasons are mechanical:
- Shreds come first. The leader streams its block in shreds while it is still building it, so a shred feed has the transaction before any node has executed it. See What are Solana shreds?
- gRPC at
processedis next. The node has to receive those shreds and execute the transaction. After that, the open-source Yellowstone server sends eachprocessedtransaction as the node's Geyser plugin hands it over. It doesn't wait for the slot's status (server code). - Anything at
confirmedorfinalizedwaits for votes. Under Alpenglow the two are the same state, and the Solana Foundation puts finality at roughly 150 ms; our Alpenglow guide covers it. A slot is about 200 ms. - Polling adds your poll interval and two round trips, at
confirmedat the earliest.
The WebSocket methods have a second catch. logsSubscribe gives you log lines, not accounts. Pump.fun's create_v2 logs an Anchor event, CreateEvent, in a Program data: line that carries the mint, so you can decode it from the logs. For other programs, most bots end up calling getTransaction to find the pool, and that waits for confirmed. programSubscribe shows you a new account, but a Pump.fun bonding curve doesn't store its mint (check BondingCurve in the IDL), so you still need another lookup.
Why your bot detects pools late, or misses them
Roughly in the order we would check them.
1. It waits for confirmation
logsSubscribe and programSubscribe default to finalized, and getTransaction refuses processed. If any step in your path waits for confirmation, the launch is at least one round of votes old when you act. Subscribe at processed, and don't put an RPC call that needs confirmed between seeing the launch and sending.
2. It makes a network call before it acts
Fetching the transaction, the metadata account or the token's JSON from its URI costs a round trip; 11 of the 13 launches we pulled pointed their URI at an IPFS gateway, outside your network. Everything a buy needs is already in the instruction: the mint, the bonding curve, the user, and the name, symbol and URI as arguments. Fetch the metadata after you've acted, or in parallel, never in front.
3. It only matches create
If your filter compares the first 8 bytes against [24, 30, 200, 40, 5, 28, 7, 119] only, it misses create_v2, and in our sample that was every launch. Match both, and read the user from position 7 for create but position 5 for create_v2.
4. It only reads top-level instructions
Launches made through another program arrive as an inner instruction. Two of the 13 launches we pulled were create_v2 called from another program's instruction. Every PumpSwap graduation pool is created inside Pump.fun's migrate, and every LaunchLab graduation into CPMM inside migrate_to_cpswap. A decoder that loops over message.instructions and stops misses all of them. Walk inner_instructions too.
The filter isn't the problem: Yellowstone's account_include matches a transaction if the account appears anywhere in its account list, static or loaded from a lookup table (filter code). A program that is only called by CPI is still in that list, so the transaction reaches you. It's your decoder that has to look inside.
5. It treats migrate as the graduation
A successful migrate or migrate_v2 can be a no-op on a curve that has already migrated. Act on the PumpSwap create_pool inside it.
6. Its IDL is out of date
Programs add instructions. CPMM's initialize_with_permission is the example that matters here. Pump.fun's create_v2 also gains trailing arguments now and then. Check your discriminators against each program's current IDL, not one copied into your repo a year ago.
7. Its account indexes ignore lookup tables
If you resolve accounts[i] against the static keys only, every index past the end of the static list fails. If you append the loaded addresses in the wrong order, it reads the wrong key without an error. Append the loaded writable addresses, then the read-only ones.
8. It can't read v1 transactions
Solana's v1 transaction format went live on mainnet at the start of epoch 1035 on September 15, 2026, and the Solana Foundation's upgrade page calls it mandatory for anyone reading transactions. Nine of the 13 Pump.fun launches we pulled were v1 transactions. What breaks:
- RPC:
getTransactionandgetBlockfail on a v1 transaction with error-32015unless you passmaxSupportedTransactionVersion: 1, the integer. Passing0fails the same way, and one v1 transaction fails a wholegetBlock. - Raw decoders: a v1 transaction starts with the byte
0x81and has a new layout. A shreds or entries decoder on Solana crates from before v1 can't read it. Solana's docs list the versions that can: Agave 4.2.x-generation Rust crates,@solana/kit8.0 or later, web3.js v3, and, for reading only, web3.js 1.99.0 or later. - gRPC: nothing errors. Regenerate your stubs from the current proto (
yellowstone-grpc-proto14.0.1 in Rust has the newMessage.configfield). Our code below doesn't care about the version: a v1 transaction simply has no loaded addresses.
9. It asks for more than it needs
Subscribing to the whole Pump.fun program sends you every buy and sell on every bonding curve. Your bot has to decode all of them to find a few creates. Pump.fun's mint authority, TSLvdd1pWpHVjahSpsvCXUbgwsL3JAcvokwaKt1eokM (the PDA from the seed mint-authority, as in the IDL), is an account only create and create_v2 take. Filter on it instead of the program. Launches made by CPI still match, because the account is still in the transaction.
Raydium CPMM has an account like that for pools: both initialize and initialize_with_permission must pass the pool-creation fee account DNXgeM9EiiaAbaWvwjHj9fQQLAX5ZsfHyvmYUNRAdNC8, fixed in the program's source. Filtering on it instead of the CPMM program drops every CPMM swap from your stream.
10. It decodes or reads too slowly
JSON over WebSocket means base58 or base64 strings to parse on every message. A gRPC client that does real work inside its read loop falls behind the stream. Compare raw bytes, not base58 strings, and keep the read loop to reading: our guide to Yellowstone stream lag shows how.
11. It is far from the node
Every update crosses the network once. Put the bot in the same region as the endpoint it reads from.
12. On shreds: it trusts what it can't see
Shreds carry no execution results. A launch you decode from shreds may fail, and you can't tell until a node executes it. They also carry no inner instructions, so a launch made by CPI shows up as another program's instruction. Accounts loaded from lookup tables have to be resolved from your own cache of those tables. An address can't be used in the slot it was added to a table, but a cache you refresh every few seconds can still be behind a table that was extended a slot ago.
Symptom, cause, fix
| What you see | Likely cause | What to do |
|---|---|---|
| You see launches a slot or more after others | Waiting for confirmed or finalized somewhere in the path | Subscribe at processed; drop getTransaction from the hot path |
| Detection is fast, the buy is slow | Metadata, URI or account fetch before acting | Act on the instruction's own accounts and arguments |
| You see almost no new Pump.fun tokens | Matching create only | Add create_v2 ([214, 144, 76, 236, 95, 139, 49, 180]) |
| Some launches never show up | Top-level instructions only | Walk inner_instructions |
| No PumpSwap graduations, or false ones | Keying on migrate | Detect the inner PumpSwap create_pool |
| No LaunchLab graduations on CPMM | Matching CPMM initialize only | Add initialize_with_permission |
| Wrong mint or pool on some transactions | Indexes resolved against static keys only | Append the loaded writable, then read-only, addresses |
-32015 from getTransaction, or a decoder error on byte 0x81 | No v1 support | maxSupportedTransactionVersion: 1; update your Solana crates or SDK |
| The stream falls behind on busy launches | A whole-program filter, or work in the read loop | Filter on the mint authority; queue work off the read loop |
| A launch you saw in shreds never happened | Shreds can't show failures | Confirm with gRPC or RPC before counting the position |
Working code: Yellowstone gRPC in Rust
This subscribes at processed to transactions that touch the Pump.fun mint authority, PumpSwap, LaunchLab, CPMM or AMM v4, with votes and failed transactions dropped on the server. It checks every top-level and inner instruction against the table, and prints the event with its mint and pool. A CPI match also prints the top-level program it ran under, which tells a graduation (via Pump.fun or LaunchLab) from a launch made through someone's own program. It uses the AllenHark endpoint by default; set GRPC_ENDPOINT for any other Yellowstone node.
// Cargo.toml: yellowstone-grpc-client = "16.0.1", yellowstone-grpc-proto = "14.0.1",
// solana-pubkey = "4", five8 = "1", tokio = { version = "1", features = ["full"] },
// futures = "0.3", anyhow = "1"
use std::{collections::HashMap, time::Duration};
use futures::{SinkExt, StreamExt};
use solana_pubkey::{pubkey, Pubkey};
use yellowstone_grpc_client::GeyserGrpcClient;
use yellowstone_grpc_proto::geyser::{
subscribe_update::UpdateOneof, CommitmentLevel, SubscribeRequest,
SubscribeRequestFilterTransactions, SubscribeRequestPing, SubscribeUpdateTransaction,
};
const PUMP: Pubkey = pubkey!("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P");
// Pump.fun's mint authority PDA (seed "mint-authority"): only create and create_v2 take it
const PUMP_MINT_AUTHORITY: Pubkey = pubkey!("TSLvdd1pWpHVjahSpsvCXUbgwsL3JAcvokwaKt1eokM");
const PUMPSWAP: Pubkey = pubkey!("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA");
const LAUNCHLAB: Pubkey = pubkey!("LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj");
const CPMM: Pubkey = pubkey!("CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C");
const AMM_V4: Pubkey = pubkey!("675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8");
/// One "new token" or "new pool" instruction.
struct Event {
name: &'static str,
program: Pubkey,
/// Anchor programs: sha256("global:<name>")[..8]. Raydium AMM v4: a 1-byte tag.
tag: &'static [u8],
/// (label, position in the instruction's account list), in the IDL's order
accounts: &'static [(&'static str, usize)],
}
const LAUNCHLAB_ACCOUNTS: &[(&str, usize)] = &[("pool", 5), ("mint", 6), ("quote", 7)];
const EVENTS: &[Event] = &[
Event {
name: "pump.fun create",
program: PUMP,
tag: &[24, 30, 200, 40, 5, 28, 7, 119],
accounts: &[("mint", 0), ("curve", 2), ("user", 7)],
},
Event {
name: "pump.fun create_v2",
program: PUMP,
tag: &[214, 144, 76, 236, 95, 139, 49, 180],
accounts: &[("mint", 0), ("curve", 2), ("user", 5)],
},
Event {
name: "pumpswap create_pool",
program: PUMPSWAP,
tag: &[233, 146, 209, 142, 207, 104, 64, 188],
accounts: &[("pool", 0), ("mint", 3), ("quote", 4)],
},
Event {
name: "launchlab initialize",
program: LAUNCHLAB,
tag: &[175, 175, 109, 31, 13, 152, 155, 237],
accounts: LAUNCHLAB_ACCOUNTS,
},
Event {
name: "launchlab initialize_v2",
program: LAUNCHLAB,
tag: &[67, 153, 175, 39, 218, 16, 38, 32],
accounts: LAUNCHLAB_ACCOUNTS,
},
Event {
name: "launchlab initialize_with_token_2022",
program: LAUNCHLAB,
tag: &[37, 190, 126, 222, 44, 154, 171, 17],
accounts: LAUNCHLAB_ACCOUNTS,
},
Event {
name: "raydium cpmm initialize",
program: CPMM,
tag: &[175, 175, 109, 31, 13, 152, 155, 237],
accounts: &[("pool", 3), ("token0", 4), ("token1", 5)],
},
Event {
// What LaunchLab's migrate_to_cpswap calls; in Raydium's current IDL
name: "raydium cpmm initialize_with_permission",
program: CPMM,
tag: &[63, 55, 254, 65, 49, 178, 89, 121],
accounts: &[("pool", 4), ("token0", 5), ("token1", 6)],
},
Event {
name: "raydium amm v4 initialize2",
program: AMM_V4,
tag: &[1],
accounts: &[("pool", 4), ("coin", 8), ("pc", 9)],
},
];
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let endpoint = std::env::var("GRPC_ENDPOINT").unwrap_or("http://88.216.36.108:10001".into());
let mut client = GeyserGrpcClient::build_from_shared(endpoint)?
.x_token(std::env::var("ALLENHARK_KEY").ok())? // not needed from an allowlisted IP
.http2_keep_alive_interval(Duration::from_secs(15))
.keep_alive_timeout(Duration::from_secs(5))
.keep_alive_while_idle(true)
.max_decoding_message_size(64 * 1024 * 1024)
.connect()
.await?;
// A transaction matches if it contains any of these accounts, at the top level
// or only inside a CPI, as a static key or one loaded from a lookup table
let watch = [PUMP_MINT_AUTHORITY, PUMPSWAP, LAUNCHLAB, CPMM, AMM_V4];
let mut transactions = HashMap::new();
transactions.insert(
"new".to_string(),
SubscribeRequestFilterTransactions {
vote: Some(false),
failed: Some(false),
account_include: watch.iter().map(|k| k.to_string()).collect(),
..Default::default()
},
);
let request = SubscribeRequest {
transactions,
commitment: Some(CommitmentLevel::Processed as i32),
..Default::default()
};
let (mut sink, mut stream) = client.subscribe_with_request(Some(request)).await?;
while let Some(update) = stream.next().await {
match update?.update_oneof {
Some(UpdateOneof::Ping(_)) => {
let pong = SubscribeRequest { ping: Some(SubscribeRequestPing { id: 1 }), ..Default::default() };
sink.send(pong).await?;
}
Some(UpdateOneof::Transaction(tx)) => detect(&tx),
_ => {}
}
}
anyhow::bail!("stream ended: reconnect with from_slot")
}
fn detect(update: &SubscribeUpdateTransaction) {
let Some(info) = &update.transaction else { return };
let (Some(tx), Some(meta)) = (&info.transaction, &info.meta) else { return };
let Some(message) = &tx.message else { return };
// Account indexes point into one list: the static keys, then the writable and
// read-only addresses loaded from lookup tables (v0 only; empty otherwise)
let keys: Vec<&[u8]> = message
.account_keys
.iter()
.chain(&meta.loaded_writable_addresses)
.chain(&meta.loaded_readonly_addresses)
.map(Vec::as_slice)
.collect();
// Top-level instructions first
for ix in &message.instructions {
check(update, &keys, ix.program_id_index, &ix.accounts, &ix.data, None);
}
// Then the instructions programs made by CPI. `index` is the top-level
// instruction they ran under, so its program is the one that started it.
for group in &meta.inner_instructions {
let outer = message.instructions.get(group.index as usize);
let outer = outer.and_then(|ix| keys.get(ix.program_id_index as usize)).copied();
for ix in &group.instructions {
check(update, &keys, ix.program_id_index, &ix.accounts, &ix.data, outer);
}
}
}
fn check(
update: &SubscribeUpdateTransaction,
keys: &[&[u8]],
program: u32,
accounts: &[u8],
data: &[u8],
outer: Option<&[u8]>,
) {
let Some(&program) = keys.get(program as usize) else { return };
for event in EVENTS.iter().filter(|e| e.program.as_ref() == program && data.starts_with(e.tag)) {
let mut line = format!("slot {} {}", update.slot, event.name);
if let Some(outer) = outer.and_then(|k| Pubkey::try_from(k).ok()) {
line.push_str(&format!(" via={outer}"));
}
for &(label, position) in event.accounts {
let key = accounts.get(position).and_then(|&i| keys.get(i as usize));
if let Some(key) = key.and_then(|k| Pubkey::try_from(*k).ok()) {
line.push_str(&format!(" {label}={key}"));
}
}
if let Some(info) = &update.transaction {
line.push_str(&format!(" tx={}", signature(&info.signature)));
}
println!("{line}");
}
}
fn signature(bytes: &[u8]) -> String {
let Ok(bytes) = <&[u8; 64]>::try_from(bytes) else { return String::new() };
let mut out = [0u8; 88];
let len = five8::encode_64(bytes, &mut out) as usize;
String::from_utf8_lossy(&out[..len]).into_owned()
}Run it from an allowlisted server with cargo run, or set ALLENHARK_KEY to send your API key as x-token. Output looks like this (real transactions from October 30; token, wallet and pool addresses and signatures shortened):
slot 463376507 pump.fun create_v2 mint=4nEYa2hD…pump curve=APrhF1Q5… user=6daSSd7m… tx=24AexKqH…
slot 463376427 pump.fun create_v2 via=6Vo3245e… mint=7dUgQ8iq…pump curve=P8RQZVCB… user=9uBqhxFf… tx=4w5FSKfL…
slot 463376229 pumpswap create_pool via=6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P pool=5RLi52uD… mint=s5sMzofW…pump quote=So11111111111111111111111111111111111111112 tx=4enPLqmz…
slot 463342983 raydium cpmm initialize_with_permission via=LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj pool=66Z3N9nQ… token0=So11111111111111111111111111111111111111112 token1=EY2SaFwT… tx=2CR6Gs3f…The first is a direct launch. The second is a launch made inside another program's instruction. The third is a Pump.fun graduation, and the fourth a LaunchLab graduation into CPMM.
Before you trade on it:
- A
processedtransaction isn't final. Atprocesseda slot can have more than one candidate block, labelled bybank_id. Dedupe launches by mint, and see the Alpenglow guide if you store anything per slot. - Reconnect without gaps. The loop ends when the stream does. Resubscribe with
from_slot, as in our reconnect guide. println!is the demo part. In a bot, hand each event to the code that acts on it, off the read loop.- The stream carries more than launches. Filtering on the PumpSwap, LaunchLab, CPMM and AMM v4 programs also sends you every trade on them. Where a program has an account only its create instruction takes, like Pump.fun's mint authority or CPMM's fee account, filter on that instead.
The same in Node.js
The same filter and checks with @grpc/grpc-js. It decodes on the main thread, so keep it to detection and hand the rest to a worker.
// detect.mjs: new Pump.fun tokens and new PumpSwap/Raydium pools over Yellowstone gRPC
// npm install @grpc/grpc-js @grpc/proto-loader bs58
// geyser.proto and solana-storage.proto from rpcpool/yellowstone-grpc go in this folder.
import grpc from "@grpc/grpc-js";
import protoLoader from "@grpc/proto-loader";
import bs58 from "bs58";
const ENDPOINT = process.env.GRPC_ENDPOINT ?? "88.216.36.108:10001"; // AllenHark Frankfurt
const PUMP = "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P";
const PUMP_MINT_AUTHORITY = "TSLvdd1pWpHVjahSpsvCXUbgwsL3JAcvokwaKt1eokM"; // only create and create_v2 take it
const PUMPSWAP = "pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA";
const LAUNCHLAB = "LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj";
const CPMM = "CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C";
const AMM_V4 = "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8";
// [name, program, tag bytes, { label: account position }] in each IDL's account order
const LL = { pool: 5, mint: 6, quote: 7 };
const EVENTS = [
["pump.fun create", PUMP, [24, 30, 200, 40, 5, 28, 7, 119], { mint: 0, curve: 2, user: 7 }],
["pump.fun create_v2", PUMP, [214, 144, 76, 236, 95, 139, 49, 180], { mint: 0, curve: 2, user: 5 }],
["pumpswap create_pool", PUMPSWAP, [233, 146, 209, 142, 207, 104, 64, 188], { pool: 0, mint: 3, quote: 4 }],
["launchlab initialize", LAUNCHLAB, [175, 175, 109, 31, 13, 152, 155, 237], LL],
["launchlab initialize_v2", LAUNCHLAB, [67, 153, 175, 39, 218, 16, 38, 32], LL],
["launchlab initialize_with_token_2022", LAUNCHLAB, [37, 190, 126, 222, 44, 154, 171, 17], LL],
["raydium cpmm initialize", CPMM, [175, 175, 109, 31, 13, 152, 155, 237], { pool: 3, token0: 4, token1: 5 }],
["raydium cpmm initialize_with_permission", CPMM, [63, 55, 254, 65, 49, 178, 89, 121], { pool: 4, token0: 5, token1: 6 }],
["raydium amm v4 initialize2", AMM_V4, [1], { pool: 4, coin: 8, pc: 9 }],
].map(([name, program, tag, accounts]) => ({ name, program, tag: Buffer.from(tag), accounts }));
const def = protoLoader.loadSync("geyser.proto", { keepCase: true, longs: String, enums: String, defaults: true, oneofs: true });
const { geyser } = grpc.loadPackageDefinition(def);
const client = new geyser.Geyser(ENDPOINT, grpc.credentials.createInsecure(), {
"grpc.max_receive_message_length": 64 * 1024 * 1024,
});
const meta = new grpc.Metadata(); // empty from an allowlisted IP
if (process.env.ALLENHARK_KEY) meta.set("x-token", process.env.ALLENHARK_KEY);
const stream = client.subscribe(meta);
stream.on("data", (u) => {
if (u.update_oneof === "ping") stream.write({ ping: { id: 1 } });
else if (u.update_oneof === "transaction") detect(u.transaction);
});
stream.on("error", (err) => { console.error(`stream error ${err.code}: ${err.details}`); process.exit(1); });
stream.write({
accounts: {}, slots: {}, transactions_status: {}, blocks: {}, blocks_meta: {}, entry: {}, accounts_data_slice: [],
transactions: {
new: {
vote: false, failed: false, account_exclude: [], account_required: [],
account_include: [PUMP_MINT_AUTHORITY, PUMPSWAP, LAUNCHLAB, CPMM, AMM_V4],
},
},
commitment: "PROCESSED",
});
function detect({ slot, transaction: info }) {
const message = info.transaction.message;
// Static keys, then addresses loaded from lookup tables: one index space
const keys = [...message.account_keys, ...info.meta.loaded_writable_addresses, ...info.meta.loaded_readonly_addresses];
for (const ix of message.instructions) check(slot, info, keys, ix, null);
// CPI: group.index is the top-level instruction whose program started it
for (const group of info.meta.inner_instructions) {
const outer = keys[message.instructions[group.index]?.program_id_index];
for (const ix of group.instructions) check(slot, info, keys, ix, outer);
}
}
function check(slot, info, keys, ix, outer) {
const program = keys[ix.program_id_index];
if (!program) return;
const id = bs58.encode(program); // a real bot compares raw bytes instead
for (const e of EVENTS) {
if (id !== e.program || !ix.data.subarray(0, e.tag.length).equals(e.tag)) continue;
const found = Object.entries(e.accounts)
.filter(([, pos]) => keys[ix.accounts[pos]])
.map(([label, pos]) => `${label}=${bs58.encode(keys[ix.accounts[pos]])}`);
const via = outer ? ` via=${bs58.encode(outer)}` : "";
console.log(`slot ${slot} ${e.name}${via} ${found.join(" ")} tx=${bs58.encode(info.signature)}`);
}
}How we tested both. We pulled 25 mainnet transactions on October 30, 2026 (13 Pump.fun launches, 3 migrate_v2, 4 CPMM pools, 3 AMM v4 pools and 2 LaunchLab launches), replayed them through a local mock Yellowstone server, and ran both programs against it. Both printed the same 23 events; the two no-op migrations printed nothing. For three of the transactions, all with lookup-table accounts, we also checked the positions against the RPC node's own jsonParsed view. The Rust example passes cargo check with no warnings.
Shreds: earlier, with more work
If gRPC at processed is still too late, read shreds. The table of programs and discriminators works the same way, but the rest of the decoder changes:
- You decode the transactions yourself. AllenHark's shared shred stream already rebuilds the entries, so you get each entry's transactions as bincode-encoded
VersionedTransactions. Your Solana crates need to read v1. - There are no server-side filters. You get every transaction and filter on the program ID and the first bytes of the data yourself.
- You only see top-level instructions. A launch made by CPI shows up as another program's instruction, with the Pump.fun program and mint authority somewhere in its accounts.
- You resolve lookup tables yourself, from a cache of the tables' contents that you keep current.
- You can't see whether it worked. Confirm with gRPC or RPC before you count a position.
The shreds docs cover the feeds and decoding. Shreds vs gRPC vs RPC vs WebSockets and the Pump.fun Block-0 sniping guide cover when the extra work pays.
AllenHark for launch detection
AllenHark Shreds gives you the block while it is still being built, from Frankfurt, in four feeds: decoded entries over gRPC or UDP, raw shreds over UDP, or Jito's proxy protocol. See the shreds docs for each.
AllenHark Yellowstone gRPC runs the code above as written:
- Endpoints: Frankfurt (
88.216.36.108:10001) and Amsterdam (84.32.104.38:10001), plaintext HTTP/2. - Authentication: an IP address on your allowlist, or, on new subscriptions, your API key sent as
x-token. - Replay:
from_slotreplays about 48 hours, so a reconnect doesn't lose launches. - Alpenglow: streams carry
bank_idtoday. - Limits: on gRPC Standard, each connection is one
Subscribestream with 500 accounts per subscription; the filter in this post uses 5. Dedicated gRPC has no rate limits and no account limit.
Plans are on the pricing page, and the Transactions subscription reference covers every filter field.
If you'd rather not build it, the Solana Block-0 Sniper does the detection for you. It sees each new token in Solana's blocks as they are being made (ShredStream for launches), and uses gRPC and RPC for prices and balances and the AllenHark relay to send. Like every AllenHark bot, it detects a new token in 0.2 ms. Pump.fun is in production; Raydium LaunchLab, Bonk.fun, Meteora DBC, PumpSwap new pools and the other launchpads are Beta. A token created inside another program rather than directly, which is most often a bundle, is skipped unless you turn on "Launches made by other programs". The Block-0 Sniper guide lists every filter.
The Solana Graduation Sniper buys at the other moment in this post: its connection to the AllenHark market feed carries every graduation on the launchpads you choose, with the new pool's first state. Pump.fun graduates to PumpSwap, Raydium LaunchLab to CPMM or AMM v4, and Meteora DBC to DAMM v2 or v1 (DAMM v1 graduations are skipped for now). See its guide.
Both start in Practice mode. Most new tokens lose almost all their value, and fast detection doesn't change that. Only trade SOL you can afford to lose; see the risk disclosure.
FAQ
What is the Pump.fun create instruction discriminator?
create is [24, 30, 200, 40, 5, 28, 7, 119] and create_v2 is [214, 144, 76, 236, 95, 139, 49, 180], both on program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P. New launches use create_v2, so match both. The mint is account 0 in each.
How do I detect a Pump.fun token migrating to PumpSwap?
Watch for PumpSwap's create_pool ([233, 146, 209, 142, 207, 104, 64, 188]) inside a Pump.fun migrate or migrate_v2 transaction. It is an inner instruction, so read the transaction's inner instructions. The pool is account 0 and the token's mint account 3.
Why does my bot detect new pools late?
Usually because something in the path waits for confirmed, or makes an RPC call before acting, or the bot reads a WebSocket or polls instead of a stream. Subscribe to transactions at processed over gRPC, read the accounts straight from the instruction, and run the bot near the endpoint.
Why does my bot miss some launches completely?
Check four things: that you match create_v2, that you read inner instructions, that you resolve lookup-table accounts, and that your RPC calls and decoders can read v1 transactions. Each of these drops launches without an error.
Can I detect new tokens with logsSubscribe?
Yes, at processed, and for Pump.fun you can decode the mint from the create event in the logs. But each subscription takes one address, the default commitment is finalized, and for programs whose logs don't carry the pool, you are back to getTransaction, which waits for confirmed.
Do I need shreds?
Only if you need the launch before any node has executed it, which is what same-block sniping needs. For most detection, gRPC at processed is enough, and it gives you inner instructions, loaded addresses and the result, which shreds don't.
Related reading
- Pump.fun create instruction discriminator and IDL analysis
- Pump.fun bonding curve sniper bot: block 0 and real trading
- Why your Yellowstone gRPC stream lags or misses events
- Shreds vs gRPC vs RPC vs WebSockets
- What are Solana shreds?
Sources
Checked October 30, 2026.
- IDLs: our Solana IDL library for Pump.fun, PumpSwap, LaunchLab, CPMM and AMM v4; Pump's current pump.json and pump_amm.json; Raydium's current LaunchLab and CPMM IDLs. Anchor discriminators recomputed as
sha256("global:<name>")[..8]. - Pump: program docs (
migrate), PumpSwap docs (pool index 0), coin creation docs (create_v2accounts and arguments) - Raydium: AMM v4 instruction source (
initialize2tag and accounts) and CPMM program source (initialize_with_permission) - Solana: versioned transactions (lookup tables, v1), larger transaction sizes (v1 activation and reading), getTransaction, logsSubscribe and programSubscribe
- Yellowstone gRPC: server code (when
processedupdates are sent) and message code (which accountsaccount_includematches) - Mainnet transactions: our pull from
api.mainnet-beta.solana.comon October 30, 2026 (13 Pump.fun launches, 3migrate_v2, 48 CPMM pool creations, 3 AMM v4 pools, 2 LaunchLab launches)
