Pump now has three v2 trade instructions, two quote assets, and a separate migration step into
PumpSwap. A decoder that only recognizes legacy buy and sell can keep receiving healthy
transactions while silently missing the newer path.
The right monitor tracks a lifecycle, not a keyword:
v2 trade observed
-> transaction executed successfully
-> bonding curve reaches complete state
-> migrate executes successfully
-> PumpSwap pool and migration event are recordedEach arrow has a different proof requirement. Collapsing them into one "graduated" flag creates duplicate alerts and false pool records.
Start from Pump's public contracts
The Pump program runs at:
6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6PPump's public repository documents the unified v2 trade interface:
buy_v2sell_v2buy_exact_quote_in_v2
All accounts are mandatory and use one order across the supported quote paths. The interface adds
quote_mint, quote-side token accounts, sharing configuration, and volume accumulators. For an
existing SOL-paired coin, the caller passes wrapped SOL as quote_mint, while the trade still uses
native SOL.
Pump's fee documentation says token creators could select USDC as the paired token from May 21, 2026. That makes quote-mint decoding part of the business contract, not an optional display field.
The instruction name identifies the operation. The quote mint identifies the market.
Filter by program, then decode every instruction layer
A Yellowstone transaction filter can remove unrelated traffic at the server:
import { CommitmentLevel, type SubscribeRequest } from "@triton-one/yellowstone-grpc";
const PUMP_PROGRAM_ID = "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P";
export const request: SubscribeRequest = {
accounts: {},
slots: {},
transactions: {
pump: {
vote: false,
failed: false,
signature: undefined,
accountInclude: [PUMP_PROGRAM_ID],
accountExclude: [],
accountRequired: [],
},
},
transactionsStatus: {},
blocks: {},
blocksMeta: {},
entry: {},
accountsDataSlice: [],
commitment: CommitmentLevel.PROCESSED,
ping: undefined,
};accountInclude means the transaction mentions the program account. It does not tell you which Pump
instruction ran. It also does not replace transaction-status validation.
The decoder must inspect:
- Static message account keys.
- Addresses loaded through lookup tables.
- Outer compiled instructions.
- Inner instructions emitted by CPI.
- Transaction metadata, including the execution error.
Missing inner instructions is a common source of partial histories. A router can invoke another program, which then invokes Pump. If the consumer reads only the top-level message, the trade never reaches its decoder.
Match the eight-byte discriminator before parsing arguments
Anchor instructions begin with an eight-byte discriminator. The May 18 public IDL defines these values:
| Instruction | Discriminator bytes |
|---|---|
buy_v2 | 184, 23, 238, 97, 103, 197, 211, 61 |
sell_v2 | 93, 246, 130, 60, 231, 233, 64, 178 |
buy_exact_quote_in_v2 | 194, 171, 28, 70, 104, 77, 91, 47 |
migrate | 155, 234, 231, 146, 236, 158, 162, 30 |
Keep classification small and testable:
const PUMP_PROGRAM_ID = "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P";
const PUMP_IX = new Map<string, string>([
[hex([184, 23, 238, 97, 103, 197, 211, 61]), "buy_v2"],
[hex([93, 246, 130, 60, 231, 233, 64, 178]), "sell_v2"],
[hex([194, 171, 28, 70, 104, 77, 91, 47]), "buy_exact_quote_in_v2"],
[hex([155, 234, 231, 146, 236, 158, 162, 30]), "migrate"],
]);
function hex(bytes: number[] | Uint8Array): string {
return Buffer.from(bytes).toString("hex");
}
export function classifyPumpInstruction(
programId: string,
data: Uint8Array,
): string | null {
if (programId !== PUMP_PROGRAM_ID || data.byteLength < 8) return null;
return PUMP_IX.get(hex(data.subarray(0, 8))) ?? null;
}Classification should fail closed. An unknown discriminator is a version signal, not permission to guess an account layout from token balance changes.
After classification, decode arguments and accounts with the pinned IDL or Pump SDK. The current IDL
still contains argument names such as max_sol_cost and min_sol_output, while the v2 account model
supports another quote mint. Treat the decoded quote_mint and quote-side amounts as authoritative
for asset identity.
Completion and migration are separate states
Pump's program documentation
defines a completed curve as complete == true with real_token_reserves == 0. At that point, the
permissionless migrate instruction can move liquidity into PumpSwap. The documentation describes
migrate as idempotent, so another caller may complete it before your worker acts.
Model at least these states:
type PumpAssetState =
| { kind: "bonding_curve"; mint: string; quoteMint: string }
| { kind: "migration_eligible"; mint: string; quoteMint: string; completedAtSlot: bigint }
| { kind: "pumpswap"; mint: string; quoteMint: string; pool: string; migratedAtSlot: bigint };Do not mark an asset as migrated because a transaction contains the migrate discriminator. Require
all of the following:
- The transaction finished without an execution error.
- The decoded mint and bonding curve match the asset record.
- The
CompletePumpAmmMigrationEventor verified PumpSwap pool identifies the resulting pool. - A replay of the same transaction updates the record without creating a second pool row.
The current public IDL gives CompletePumpAmmMigrationEvent fields for the user, mint, token and
quote amounts, migration fee, bonding curve, timestamp, pool, and quote mint. Decode the official event
decoder rather than hand-counting log offsets.
Persist a replay-safe event key
Yellowstone reconnect and replay can deliver an observation more than once. Processed data may also return before the transaction reaches a stronger commitment.
Persist one key per decoded instruction or event:
type EventLocation = {
signature: string;
outerInstructionIndex: number;
innerInstructionIndex: number | null;
eventIndex: number | null;
};
export function pumpEventKey(location: EventLocation): string {
return [
location.signature,
location.outerInstructionIndex,
location.innerInstructionIndex ?? "outer",
location.eventIndex ?? "instruction",
].join(":");
}Upsert by that key. Store the first observed slot and receive time, then update commitment and decoded fields as stronger evidence arrives. Notifications, orders, and analytics counters must use the same idempotency record before producing a side effect.
The recovery state machine in our Yellowstone reconnect and replay guide covers checkpoint overlap and reconciliation. Pump-specific decoding sits after that transport boundary.
Verify the monitor with captured transactions
Make the Pump Lifecycle Fixture Set the release gate. Capture and retain successful examples for:
- One SOL-paired
buy_v2and onesell_v2. - One USDC-paired v2 trade.
- One
buy_exact_quote_in_v2. - One curve-completion transaction.
- One successful PumpSwap migration.
- One failed v2 trade and one unknown discriminator.
- One replayed update for an already stored signature.
For each fixture, assert the program ID, discriminator, resolved account keys, quote mint, amounts, execution result, event key, and final lifecycle state. A decoder upgrade is safe only when old fixtures remain stable and new upstream fields are handled deliberately.
Do not build fixtures from explorer screenshots. Retain raw transaction responses or serialized Yellowstone updates with their software and IDL versions.
Keep the decoder narrow
This monitor can power activity feeds, wallet alerts, migration notifications, analytics, and bot inputs. It does not prove that a trade should be copied or that a newly migrated pool is safe.
Keep market policy outside the decoder. The decoder answers what executed, for which mint and quote asset, and where the asset sits in the Pump lifecycle. Risk controls decide whether the application does anything with that fact.
Start with the Pump Lifecycle Fixture Set and one filtered stream. To compare the workload against a second Yellowstone endpoint, review the RPCEdge plans.