Buy a basket within a budget
pump.fun
Status: Tested locally in LiteSVM against pump.fun's bonding-curve program and three live curves copied from mainnet; not yet run on devnet or mainnet.
Cost: Ballista's own work took 13,226 of the tested three-coin transaction's 255,496compute units; pump.fun took the rest. Ballista charges no fee; see what it costs.
What it does
Buys up to seven pump.fun coins in one transaction, each on its bonding curve, and fails the whole transaction if together they cost more than budget.
pump.fun's buy caps one coin's cost with its max_sol_cost. Nothing caps the total. Each coin's price moves with every trade that lands before yours, so the only total a plain transaction can promise is the sum of every buy's worst case. The template measures what each buy actually took from the buyer while the transaction runs, and keeps a running total.
For each coin, the template:
- requires the coin's token account to belong to the buyer (
tokensGoToTheBuyer). pump.fun checks only the account's mint, so a run built by someone else could otherwise send the coins anywhere; - requires the coin's curve not to have graduated (
curveNotGraduated), by itscompleteflag. A graduated coin trades on PumpSwap, and pump.fun would refuse the buy with its ownBondingCurveComplete(6005); - buys
amountbase units for at mostmaxSolCostlamports, which pump.fun enforces; - adds what the buyer's balance dropped by to the total: the price, pump.fun's and the creator's fees, and any rent pump.fun charged;
- requires the total to be at most
budget(withinBudget).
Any failure reverts the whole basket. A graduated coin fails the run rather than being skipped, so a basket that lands holds every coin you asked for.
It does not guard against another set of coins. The rows name the mints, so a run built by someone else could buy other coins within your budget.
Template
import {
SYSTEM_PROGRAM_ADDRESS_BYTES,
TOKEN_2022_PROGRAM_ADDRESS_BYTES,
account,
compileTemplate,
data,
defineTemplate,
expression,
step,
} from '@jac0xb/ballista';
import {
PUMP_BONDING_CURVE,
PUMP_FEES,
PUMP_FUN,
PUMP_FUN_BUY,
PUMP_TRACK_VOLUME,
TOKEN_ACCOUNT_LENGTH,
TOKEN_ACCOUNT_OWNER_OFFSET,
addressBytes,
} from './shared.js';
const buyer = account.fixed('buyer');
const row = (name: string) => account.iteration(name);
const buyersLamports = expression.accountField(buyer, 'lamports');
export const pumpFunBuyBasket = defineTemplate({
inputs: {
/** The most the whole basket may take from the buyer, in lamports: fees and rent included. */
budget: { type: 'u64' },
},
accounts: {
pumpProgram: { executable: true, address: addressBytes(PUMP_FUN) },
global: {},
/** Pays for every coin, and receives every coin in its own token accounts. */
buyer: { signer: true, writable: true },
systemProgram: { executable: true, address: SYSTEM_PROGRAM_ADDRESS_BYTES },
/** pump.fun mints every coin `create_v2` makes with Token-2022. */
tokenProgram: { executable: true, address: TOKEN_2022_PROGRAM_ADDRESS_BYTES },
eventAuthority: {},
globalVolumeAccumulator: { writable: true },
/** The buyer's volume record. pump.fun creates it on the buyer's first buy, at the buyer's cost. */
userVolumeAccumulator: { writable: true },
feeConfig: {},
feeProgram: { executable: true, address: addressBytes(PUMP_FEES) },
buybackFeeRecipient: { writable: true },
},
batch: {
// Each buy makes eight calls (pump.fun, its fee program, a token transfer, four SOL transfers
// and its event log), and a transaction holds at most 64. Seven rows use 60 at most.
maxIterations: 7,
minIterations: 1,
row: {
mint: {},
/** Owner-pinned, so `complete` is read from an account pump.fun wrote. */
bondingCurve: {
writable: true,
owner: addressBytes(PUMP_FUN),
minDataLength: PUMP_BONDING_CURVE.minLength,
},
/** The curve's own token account, which the coins come from. */
curveTokenAccount: { writable: true },
/** Must belong to the buyer (`tokensGoToTheBuyer`). */
buyerTokenAccount: {
writable: true,
owner: TOKEN_2022_PROGRAM_ADDRESS_BYTES,
minDataLength: TOKEN_ACCOUNT_LENGTH,
},
creatorVault: { writable: true },
bondingCurveV2: {},
/** An ordinary fee recipient, or a reserved one for a mayhem-mode coin. */
feeRecipient: { writable: true },
},
rowInputs: {
/** Base units of the coin to buy. */
amount: { type: 'u64' },
/** The most this buy may cost, in lamports, fees included. pump.fun enforces it. */
maxSolCost: { type: 'u64' },
},
},
steps: [
step.let('spent', expression.u64(0)),
step.forEach(
[
// pump.fun checks only the mint of the account it pays, so a run built by someone else
// could otherwise send the coins anywhere.
step.require(
expression.equal(
expression.accountData(row('buyerTokenAccount'), TOKEN_ACCOUNT_OWNER_OFFSET, 'pubkey'),
expression.accountField(buyer, 'key'),
),
'tokensGoToTheBuyer',
),
// A graduated coin trades on PumpSwap. Say so here, before pump.fun refuses the buy with
// its own `BondingCurveComplete` (6005).
step.require(
expression.not(expression.accountData(row('bondingCurve'), PUMP_BONDING_CURVE.complete, 'bool')),
'curveNotGraduated',
),
step.let('lamportsBefore', buyersLamports, 'readLamportsBefore'),
step.invoke({
program: account.fixed('pumpProgram'),
accounts: [
{ account: account.fixed('global'), signer: false, writable: false },
{ account: row('feeRecipient'), signer: false, writable: true },
{ account: row('mint'), signer: false, writable: false },
{ account: row('bondingCurve'), signer: false, writable: true },
{ account: row('curveTokenAccount'), signer: false, writable: true },
{ account: row('buyerTokenAccount'), signer: false, writable: true },
{ account: buyer, signer: true, writable: true },
{ account: account.fixed('systemProgram'), signer: false, writable: false },
{ account: account.fixed('tokenProgram'), signer: false, writable: false },
{ account: row('creatorVault'), signer: false, writable: true },
{ account: account.fixed('eventAuthority'), signer: false, writable: false },
{ account: account.fixed('pumpProgram'), signer: false, writable: false },
{ account: account.fixed('globalVolumeAccumulator'), signer: false, writable: true },
{ account: account.fixed('userVolumeAccumulator'), signer: false, writable: true },
{ account: account.fixed('feeConfig'), signer: false, writable: false },
{ account: account.fixed('feeProgram'), signer: false, writable: false },
{ account: row('bondingCurveV2'), signer: false, writable: false },
{ account: account.fixed('buybackFeeRecipient'), signer: false, writable: true },
],
data: [
data.literal(PUMP_FUN_BUY),
data.encode('u64', expression.rowInput('amount')),
data.encode('u64', expression.rowInput('maxSolCost')),
data.literal(PUMP_TRACK_VOLUME),
],
label: 'buyOnTheCurve',
}),
// What the buy took from the buyer: the price, pump.fun's and the creator's fees, and any
// rent it charged.
step.assign(
'spent',
expression.add(
expression.variable('spent'),
expression.subtract(expression.variable('lamportsBefore'), buyersLamports),
),
'addWhatItCost',
),
step.require(
expression.lessThanOrEqual(expression.variable('spent'), expression.input('budget')),
'withinBudget',
),
],
{ carry: ['spent'], label: 'everyCoin' },
),
],
});
export const compiled = compileTemplate(pumpFunBuyBasket);/// Buy several pump.fun coins on their bonding curves, the total within a budget.
pub fn pump_fun_buy_basket() -> Template {
let row = account::iteration;
Template::new()
// The most the whole basket may take from the buyer, in lamports: fees and rent included.
.input("budget", Type::U64)
.account("pumpProgram", account::program(PUMP_FUN))
.account("global", account::readonly())
.account("buyer", account::signer().writable())
.account("systemProgram", account::program(SYSTEM_PROGRAM_ID))
.account("tokenProgram", account::program(TOKEN_2022_PROGRAM_ID))
.account("eventAuthority", account::readonly())
.account("globalVolumeAccumulator", account::writable())
.account("userVolumeAccumulator", account::writable())
.account("feeConfig", account::readonly())
.account("feeProgram", account::program(PUMP_FEES))
.account("buybackFeeRecipient", account::writable())
.batch(
// Each buy makes eight calls, and a transaction holds at most 64.
Batch::new(7)
.min_iterations(1)
.account("mint", account::readonly())
.account(
"bondingCurve",
account::writable()
.owner(PUMP_FUN)
.min_data_length(PUMP_CURVE_MIN_LENGTH),
)
.account("curveTokenAccount", account::writable())
.account("buyerTokenAccount", token_2022_account())
.account("creatorVault", account::writable())
.account("bondingCurveV2", account::readonly())
.account("feeRecipient", account::writable())
// Base units of the coin to buy.
.input("amount", Type::U64)
// The most this buy may cost, in lamports, fees included. pump.fun enforces it.
.input("maxSolCost", Type::U64),
)
.step(step::let_("spent", u64(0)))
.step(
step::for_each()
// pump.fun checks only the mint of the account it pays.
.step(
step::require(
account_data(
row("buyerTokenAccount"),
TOKEN_ACCOUNT_OWNER_OFFSET,
ReadType::Pubkey,
)
.eq(key("buyer")),
)
.label("tokensGoToTheBuyer"),
)
// A graduated coin trades on PumpSwap; say so before pump.fun refuses the buy.
.step(
step::require(
account_data(row("bondingCurve"), PUMP_CURVE_COMPLETE, ReadType::Bool)
.not(),
)
.label("curveNotGraduated"),
)
.step(step::let_("lamportsBefore", lamports("buyer")).label("readLamportsBefore"))
.step(
step::invoke("pumpProgram")
.readonly("global")
.writable(row("feeRecipient"))
.readonly(row("mint"))
.writable(row("bondingCurve"))
.writable(row("curveTokenAccount"))
.writable(row("buyerTokenAccount"))
.writable_signer("buyer")
.readonly("systemProgram")
.readonly("tokenProgram")
.writable(row("creatorVault"))
.readonly("eventAuthority")
.readonly("pumpProgram")
.writable("globalVolumeAccumulator")
.writable("userVolumeAccumulator")
.readonly("feeConfig")
.readonly("feeProgram")
.readonly(row("bondingCurveV2"))
.writable("buybackFeeRecipient")
.data(data::literal(pump_fun_buy()))
.data(data::u64(row_input("amount")))
.data(data::u64(row_input("maxSolCost")))
.data(data::literal(PUMP_TRACK_VOLUME))
.label("buyOnTheCurve"),
)
// What the buy took from the buyer: the price, the fees, and any rent.
.step(
step::assign(
"spent",
var("spent") + (var("lamportsBefore") - lamports("buyer")),
)
.label("addWhatItCost"),
)
.step(step::require(var("spent").lte(input("budget"))).label("withinBudget"))
.carry("spent")
.label("everyCoin"),
)
}import type { Address, Instruction } from '@solana/kit';
import { buildKitRunInstruction } from '@jac0xb/ballista/kit';
import { compiled } from '../pump-fun-buy-basket.js';
import { PUMP_FEES, PUMP_FUN } from '../shared.js';
import { SYSTEM_PROGRAM, at, pinned } from './programs.js';
import {
PUMP_BUYBACK_FEE_RECIPIENT,
PUMP_EVENT_AUTHORITY,
PUMP_FEE_CONFIG,
PUMP_GLOBAL,
PUMP_GLOBAL_VOLUME_ACCUMULATOR,
TOKEN_2022_PROGRAM,
pumpCoinAccounts,
pumpUserVolumeAccumulator,
token2022Ata,
type PumpCoin,
} from './pump-fun.js';
/** The template's most rows: each buy makes eight of the 64 calls a transaction holds. */
export const MAX_COINS_PER_RUN = 7;
export interface PumpBuy {
coin: PumpCoin;
/** Base units to buy. */
amount: bigint;
/** The most this buy may cost, in lamports, fees included. */
maxSolCost: bigint;
}
/**
* One row per coin, bought into the buyer's own associated token accounts, which must exist.
* `budget` caps what the whole basket takes from the buyer. Put a compute-budget instruction
* first: a buy costs pump.fun about 75,000 compute units.
*/
export async function buildPumpBuyBasketRun(input: {
templateAddress: Address;
buyer: Address;
buys: readonly PumpBuy[];
budget: bigint;
}): Promise<Instruction> {
if (input.buys.length === 0 || input.buys.length > MAX_COINS_PER_RUN) {
throw new Error(`A basket holds 1 to ${MAX_COINS_PER_RUN} coins, not ${input.buys.length}`);
}
const rows = await Promise.all(
input.buys.map(async (buy) => {
const coin = await pumpCoinAccounts(buy.coin);
return {
mint: at(coin.mint),
bondingCurve: at(coin.bondingCurve),
curveTokenAccount: at(coin.curveTokenAccount),
buyerTokenAccount: at(await token2022Ata(input.buyer, coin.mint)),
creatorVault: at(coin.creatorVault),
bondingCurveV2: at(coin.bondingCurveV2),
feeRecipient: at(coin.feeRecipient),
};
}),
);
return buildKitRunInstruction({
compiled,
templateAddress: input.templateAddress,
inputs: { budget: input.budget },
accounts: {
pumpProgram: pinned(PUMP_FUN),
global: at(PUMP_GLOBAL),
buyer: at(input.buyer),
systemProgram: pinned(SYSTEM_PROGRAM),
tokenProgram: pinned(TOKEN_2022_PROGRAM),
eventAuthority: at(PUMP_EVENT_AUTHORITY),
globalVolumeAccumulator: at(PUMP_GLOBAL_VOLUME_ACCUMULATOR),
userVolumeAccumulator: at(await pumpUserVolumeAccumulator(input.buyer)),
feeConfig: at(PUMP_FEE_CONFIG),
feeProgram: pinned(PUMP_FEES),
buybackFeeRecipient: at(PUMP_BUYBACK_FEE_RECIPIENT),
},
// One row of accounts and one of inputs per coin, in the same order.
batchRows: rows,
batchInputs: input.buys.map((buy) => ({ amount: buy.amount, maxSolCost: buy.maxSolCost })),
});
}/// One coin of the basket.
pub struct PumpBuy {
pub coin: PumpCoin,
/// Base units to buy.
pub amount: u64,
/// The most this buy may cost, in lamports, fees included.
pub max_sol_cost: u64,
}
/// 1 to 7 rows, one per coin, bought into the buyer's own associated token accounts, which must
/// exist. `budget` caps what the whole basket takes from the buyer. Put a compute-budget
/// instruction first: a buy costs pump.fun about 75,000 compute units. Without a lookup table, two
/// coins fit a transaction; see the page for more.
pub fn run_pump_buy_basket(
template: Pubkey,
buyer: Pubkey,
buys: &[PumpBuy],
budget: u64,
) -> Result<Instruction, Box<dyn Error>> {
let instruction = templates::pump_fun_buy_basket()
.compile()?
.run(template)
.input("budget", budget)
.account("pumpProgram", PUMP_FUN)
.account("global", PUMP_GLOBAL)
.account("buyer", buyer)
.account("systemProgram", SYSTEM_PROGRAM_ID)
.account("tokenProgram", TOKEN_2022_PROGRAM_ID)
.account("eventAuthority", PUMP_EVENT_AUTHORITY)
.account("globalVolumeAccumulator", PUMP_GLOBAL_VOLUME_ACCUMULATOR)
.account(
"userVolumeAccumulator",
pump_user_volume_accumulator(&buyer),
)
.account("feeConfig", PUMP_FEE_CONFIG)
.account("feeProgram", PUMP_FEES)
.account("buybackFeeRecipient", PUMP_BUYBACK_FEE_RECIPIENT)
.rows(buys.iter().map(|buy| {
let coin = &buy.coin;
Row::new()
.account("mint", coin.mint)
.account("bondingCurve", coin.bonding_curve())
.account("curveTokenAccount", coin.curve_token_account())
.account("buyerTokenAccount", token_2022_ata(&buyer, &coin.mint))
.account("creatorVault", coin.creator_vault())
.account("bondingCurveV2", coin.bonding_curve_v2())
.account("feeRecipient", coin.fee_recipient())
.input("amount", buy.amount)
.input("maxSolCost", buy.max_sol_cost)
}))
.instruction()?;
Ok(instruction)
}complete is the byte at offset 48 of a BondingCurve, after Anchor's discriminator and five u64s. The owner pin on bondingCurve means pump.fun wrote it, and pump.fun's own seeds check ties it to the row's mint. The Rust template takes its program addresses, offsets and token_2022_account() from the shared helpers.
Run it
The coins form a batch. The Run tabs pass the 11 declared accounts in order, pumpProgram, global, buyer, systemProgram, tokenProgram, eventAuthority, globalVolumeAccumulator, userVolumeAccumulator, feeConfig, feeProgram and buybackFeeRecipient, then one row of seven accounts per coin. The inputs are budget, then each row's amount and maxSolCost. A row is:
mint,bondingCurveandcurveTokenAccount, the curve's own token account;buyerTokenAccount, the buyer's associated token account, which must already exist;creatorVault, derived from the creator the curve records;bondingCurveV2, a PDA pump.fun has taken since its April2026upgrade, whether or not it exists;feeRecipient: an ordinary fee recipient, or a reserved one for a mayhem-mode coin.
Deriving pump.fun's accounts
import { address, getAddressDecoder, getAddressEncoder, getProgramDerivedAddress, type Address } from '@solana/kit';
import { ASSOCIATED_TOKEN_PROGRAM_ADDRESS_BYTES, TOKEN_2022_PROGRAM_ADDRESS_BYTES } from '@jac0xb/ballista';
import { PUMP_FUN } from '../shared.js';
const decoder = getAddressDecoder();
const encoder = getAddressEncoder();
export const TOKEN_2022_PROGRAM = decoder.decode(TOKEN_2022_PROGRAM_ADDRESS_BYTES);
const ASSOCIATED_TOKEN_PROGRAM = decoder.decode(ASSOCIATED_TOKEN_PROGRAM_ADDRESS_BYTES);
/** pump.fun's `Global`, `["global"]`. */
export const PUMP_GLOBAL = address('4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf');
/** Anchor's event authority, `["__event_authority"]`: pump.fun logs each trade by calling itself. */
export const PUMP_EVENT_AUTHORITY = address('Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1');
/** `["global_volume_accumulator"]`. */
export const PUMP_GLOBAL_VOLUME_ACCUMULATOR = address('Hq2wp8uJ9jCPsYgNHex8RtqdvMPfVGoYwjvF1ATiwn2Y');
/** Pump Fees' `["fee_config", pump.fun]`. */
export const PUMP_FEE_CONFIG = address('8Wf5TiAheLUqBrKXeYg2JtAFFMWtKdG2BSFgqUcPVwTt');
/** `Global.fee_recipient`, which an ordinary coin's trades may pay. `Global.fee_recipients` lists seven more. */
export const PUMP_FEE_RECIPIENT = address('62qc2CNXwrYqQScmEdiZFFAnJR262PxWEuNQtxfafNgV');
/** `Global.reserved_fee_recipient`, which a mayhem-mode coin's trades may pay instead. */
export const PUMP_RESERVED_FEE_RECIPIENT = address('GesfTA3X2arioaHp8bbKdjG9vJtskViWACZoYvxp4twS');
/** One of `Global.buyback_fee_recipients`, which every trade pays. */
export const PUMP_BUYBACK_FEE_RECIPIENT = address('9M4giFFMxmFGXtc3feFzRai56WbBqehoSeRE5GK7gf7');
/** A pump.fun coin, as its bonding curve describes it. */
export interface PumpCoin {
mint: Address;
/** `BondingCurve.creator`: the 32 bytes at offset 49 of the curve's data. */
creator: Address;
/** `BondingCurve.is_mayhem_mode`: the byte at offset 81. */
mayhem: boolean;
}
/** Reads a coin's creator and mode from its curve's account data. */
export function pumpCoin(mint: Address, curveData: Uint8Array): PumpCoin {
return { mint, creator: decoder.decode(curveData.subarray(49, 81)), mayhem: curveData[81] === 1 };
}
const pumpPda = async (seeds: Parameters<typeof getProgramDerivedAddress>[0]['seeds']) =>
(await getProgramDerivedAddress({ programAddress: address(PUMP_FUN), seeds }))[0];
/** `owner`'s associated token account for the Token-2022 mint `mint`. */
export async function token2022Ata(owner: Address, mint: Address): Promise<Address> {
const [ata] = await getProgramDerivedAddress({
programAddress: ASSOCIATED_TOKEN_PROGRAM,
seeds: [encoder.encode(owner), TOKEN_2022_PROGRAM_ADDRESS_BYTES, encoder.encode(mint)],
});
return ata;
}
/** `["user_volume_accumulator", user]`. pump.fun creates it on the user's first buy. */
export function pumpUserVolumeAccumulator(user: Address): Promise<Address> {
return pumpPda(['user_volume_accumulator', encoder.encode(user)]);
}
/** The accounts pump.fun's `buy` and `sell` take for one coin. */
export async function pumpCoinAccounts(coin: PumpCoin) {
const bondingCurve = await pumpPda(['bonding-curve', encoder.encode(coin.mint)]);
return {
mint: coin.mint,
bondingCurve,
/** The curve's own token account, which coins are bought from and sold into. */
curveTokenAccount: await token2022Ata(bondingCurve, coin.mint),
/** `["creator-vault", creator]`: the creator's fees accrue here. */
creatorVault: await pumpPda(['creator-vault', encoder.encode(coin.creator)]),
/** `["bonding-curve-v2", mint]`, which `buy` and `sell` take whether or not it exists. */
bondingCurveV2: await pumpPda(['bonding-curve-v2', encoder.encode(coin.mint)]),
/** A fee recipient of the kind the coin's mode needs. */
feeRecipient: coin.mayhem ? PUMP_RESERVED_FEE_RECIPIENT : PUMP_FEE_RECIPIENT,
};
}/// pump.fun's `Global`, `["global"]`.
pub const PUMP_GLOBAL: Pubkey = pubkey!("4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf");
/// Anchor's event authority, `["__event_authority"]`: pump.fun logs each trade by calling itself.
pub const PUMP_EVENT_AUTHORITY: Pubkey = pubkey!("Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1");
/// `["global_volume_accumulator"]`.
pub const PUMP_GLOBAL_VOLUME_ACCUMULATOR: Pubkey =
pubkey!("Hq2wp8uJ9jCPsYgNHex8RtqdvMPfVGoYwjvF1ATiwn2Y");
/// Pump Fees' `["fee_config", pump.fun]`.
pub const PUMP_FEE_CONFIG: Pubkey = pubkey!("8Wf5TiAheLUqBrKXeYg2JtAFFMWtKdG2BSFgqUcPVwTt");
/// `Global.fee_recipient`, which an ordinary coin's trades may pay. `Global.fee_recipients` lists
/// seven more.
pub const PUMP_FEE_RECIPIENT: Pubkey = pubkey!("62qc2CNXwrYqQScmEdiZFFAnJR262PxWEuNQtxfafNgV");
/// `Global.reserved_fee_recipient`, which a mayhem-mode coin's trades may pay instead.
pub const PUMP_RESERVED_FEE_RECIPIENT: Pubkey =
pubkey!("GesfTA3X2arioaHp8bbKdjG9vJtskViWACZoYvxp4twS");
/// One of `Global.buyback_fee_recipients`, which every trade pays.
pub const PUMP_BUYBACK_FEE_RECIPIENT: Pubkey =
pubkey!("9M4giFFMxmFGXtc3feFzRai56WbBqehoSeRE5GK7gf7");
/// A pump.fun coin, as its bonding curve describes it.
pub struct PumpCoin {
pub mint: Pubkey,
/// `BondingCurve.creator`: the 32 bytes at offset 49 of the curve's data.
pub creator: Pubkey,
/// `BondingCurve.is_mayhem_mode`: the byte at offset 81.
pub mayhem: bool,
}
fn pump_pda(seeds: &[&[u8]]) -> Pubkey {
Pubkey::find_program_address(seeds, &PUMP_FUN).0
}
/// `owner`'s associated token account for the Token-2022 mint `mint`.
pub fn token_2022_ata(owner: &Pubkey, mint: &Pubkey) -> Pubkey {
Pubkey::find_program_address(
&[
owner.as_ref(),
TOKEN_2022_PROGRAM_ID.as_ref(),
mint.as_ref(),
],
&ASSOCIATED_TOKEN_PROGRAM_ID,
)
.0
}
/// `["user_volume_accumulator", user]`. pump.fun creates it on the user's first buy.
pub fn pump_user_volume_accumulator(user: &Pubkey) -> Pubkey {
pump_pda(&[b"user_volume_accumulator", user.as_ref()])
}
impl PumpCoin {
/// `["bonding-curve", mint]`.
pub fn bonding_curve(&self) -> Pubkey {
pump_pda(&[b"bonding-curve", self.mint.as_ref()])
}
/// The curve's own token account, which coins are bought from and sold into.
pub fn curve_token_account(&self) -> Pubkey {
token_2022_ata(&self.bonding_curve(), &self.mint)
}
/// `["creator-vault", creator]`: the creator's fees accrue here.
pub fn creator_vault(&self) -> Pubkey {
pump_pda(&[b"creator-vault", self.creator.as_ref()])
}
/// `["bonding-curve-v2", mint]`, which `buy` and `sell` take since pump.fun's April 2026
/// upgrade, whether or not it exists.
pub fn bonding_curve_v2(&self) -> Pubkey {
pump_pda(&[b"bonding-curve-v2", self.mint.as_ref()])
}
/// A fee recipient of the kind the coin's mode needs.
pub fn fee_recipient(&self) -> Pubkey {
if self.mayhem {
PUMP_RESERVED_FEE_RECIPIENT
} else {
PUMP_FEE_RECIPIENT
}
}
}Two coins fit a plain transaction. Three limits set how many more fit:
- Compute. A buy costs pump.fun about
75,000compute units, so more than two coins need a compute-budget instruction first. - Transaction size. A coin whose accounts are not in a lookup table adds
215 bytes, so two fit without one. With pump.fun's shared accounts in a table, four fit. With each coin's accounts in one too, all seven fit. - Calls. Each buy makes eight calls: pump.fun, its fee program, a token transfer, four SOL transfers and its event log. A transaction holds at most
64,so the template stops at seven coins.
The template pins Token-2022, which mints every coin pump.fun creates with create_v2. Older coins use SPL Token and need that pin changed. It calls pump.fun's buy, which trades SOL-paired coins only. buy_v2 also trades coins paired with USDC, but it takes about ten accounts per coin, more than a batch row holds.
What has been tested
- In LiteSVM.
tests/protocols/tests/pump_fun_buy_basket.rsbuys three coins in one run: two ordinary coins and one in mayhem mode. Every coin arrived in full, and the buyer paid exactly what the same three buys cost through pump.fun alone:160,822,897 lamports, including the rent for the buyer's volume record. With pump.fun's shared accounts in a lookup table, the run took255,496compute units and924 bytes. - The budget. A budget equal to the total lands. One lamport less fails at
withinBudget, and every buy reverts. A budget that covers only the first coin stops the run at the second. - Limits. Two coins land in a plain transaction, in
180,958compute units and1,004 bytes. Seven land with every account in lookup tables, in590,735compute units and492 bytes, and their calls fill60of the transaction's64. - Failures. A graduated coin fails at
curveNotGraduated, and the coin before it reverts; pump.fun alone refuses the same buy withBondingCurveComplete. A stranger's token account fails attokensGoToTheBuyer, though pump.fun alone pays the stranger. The buyer's account of another coin fails in pump.fun with Anchor'sConstraintTokenMint(2014). AmaxSolCostone lamport short fails in pump.fun withTooMuchSolRequired(6002). - Accounts. The semantics test checks
buy's accounts and writable flags against a mainnet transaction, and the run helpers' derived addresses against mainnet's.
Check before you upload
pump.fun changes often: its April 2026 upgrade added two accounts to every buy and sell. Check the accounts against pump.fun's current IDL and a recent transaction before you upload.