buy
Buy launchpad tokens with SOL or WSOL. The instruction supports exact-in and exact-out modes, applies protocol/creator fees, updates pool state, transfers tokens to the buyer, and emits a structured trade log (log_trade_internal) for indexers.
Program: Tradersdex Curve (Launchpad) Instruction:
buyEmits:TradeEventvia internallog_trade_internalPool status required:LIVE
Modes & Pool Types
Trade modes
Exact-in (is_exact_in = true)
Spend up to sol_amount, must receive ≥ min_tokens_out
Exact-out (is_exact_in = false)
Receive exactly min_tokens_out, must spend ≤ sol_amount
Pool types (on-chain pool_type)
pool_type)0 (Basic)
SOL
❌
1 (WithCreator)
SOL
✅
2 (WithCustomQuote)
WSOL
❌
3 (Full)
WSOL
✅
QuoteVault rule:
WSOL pools (2/3):
quote_vault = ATA(NATIVE_MINT, pool)SOL pools (0/1):
quote_vault = pool(lamports live on the pool PDA)
Arguments
#[derive(AnchorSerialize, AnchorDeserialize)]
pub struct BuyArgs {
/// SOL/WSOL to spend (exact-in) or max-in (exact-out) in lamports
pub sol_amount: u64,
/// Min tokens out (exact-in) or exact tokens out (exact-out)
pub min_tokens_out: u64,
/// true => exact-in, false => exact-out
pub is_exact_in: bool,
}Accounts
0
pool
✅
["pool", mint], owner = program
Pool PDA; custom-deserialized
1
mint
Sale token mint
2
quote_mint
For WSOL must be NATIVE_MINT
For SOL may be Pubkey::default()
3
token_vault
✅
ATA(mint, pool) & amount > 0
Pool’s base token vault
4
quote_vault
WSOL: ATA(NATIVE_MINT, pool) • SOL: pool
Pool’s quote holding
5
user_token_account
✅
ATA(mint, payer), init_if_needed
Buyer’s receiving ATA
6
user_quote_account
WSOL: ATA(NATIVE_MINT, payer)
Buyer’s WSOL ATA (unused for SOL)
7
fee_recipient
✅
Must equal one of PDA(["protocol_fee_vault",[i]])
Protocol fee receiver
8
payer
✅
Buyer
9
token_program
= anchor_spl::token::ID
SPL Token program
10
associated_token_program
For ATA ops
11
system_program
For SOL transfers
12
tradersdex_curve
= crate::ID
Self-check
13
event_authority
["event_authority"]
Signs inner log ix
14
token_creator
Must equal pool creator for fee pools
Creator pubkey
15
token_creator_ata
ATA(NATIVE_MINT, token_creator)
Needed to activate creator fee (WSOL path)
Behavior (summary)
Math
Uses virtual reserves:
vr_quote,vr_token.Exact-in: fee is taken from input;
out = floor(rt * x_eff / (rb + x_eff)), capped bytokens_for_sell. If capped, recomputes the minimal gross-in needed for the cap.Exact-out: solves minimal gross-in (after fees) to receive exactly
min_tokens_out.
Fees
fee_bps = PROTOCOL_FEE_BPS (+ CREATOR_FEE_BPS if creator path is active)Creator fee is active for pool types 1/3 and only if
token_creator_atais the correct WSOL ATA (and initialized).Split:
creator_fee = total_fee * CREATOR_FEE_BPS / fee_bps,protocol_fee = total_fee - creator_fee.
Indexer
Emits inner
log_trade_internalwithTradeLogData(pool, trader, mint, gross in, tokens out, fees, new reserves, timestamp).
Client recipe: Quote → Slippage → Build IX
1) Fetch & parse pool
import { Connection, PublicKey } from "@solana/web3.js";
import {
parsePoolAccount,
poolHasCreatorFee,
PROTOCOL_FEE_BPS,
CREATOR_FEE_BPS,
} from "./utils";
const conn = new Connection(RPC_URL);
const poolPk = new PublicKey("POOL_PDA_HERE");
const ai = await conn.getAccountInfo(poolPk);
if (!ai) throw new Error("Pool not found");
const pool = parsePoolAccount(ai.data);
const creatorFeeEligible = poolHasCreatorFee(pool);
// (Optionally also check that creator WSOL ATA exists to set creatorFeeActive=true for WSOL pools)2) Quote (pick one path)
Exact-in quote
import { quoteBuyExactInFromPool, minOutWithSlippageFromPool } from "./utils";
const grossIn = 1_000_000n; // lamports/WSOL atoms you plan to spend
const q = quoteBuyExactInFromPool({
pool,
grossIn,
protocolFeeBps: PROTOCOL_FEE_BPS,
creatorFeeBps: CREATOR_FEE_BPS,
creatorFeeActive: creatorFeeEligible, // set true only when creator ATA is valid/initialized for WSOL
});
const minTokensOut = minOutWithSlippageFromPool(q.tokensOut, 100); // 1% slippage guardExact-out quote
import { quoteBuyExactOutFromPool, maxInWithSlippageFromPool } from "./utils";
const desiredOut = 50_000n; // exact tokens you want
const q = quoteBuyExactOutFromPool({
pool,
desiredOut,
protocolFeeBps: PROTOCOL_FEE_BPS,
creatorFeeBps: CREATOR_FEE_BPS,
creatorFeeActive: creatorFeeEligible,
});
const maxSolIn = maxInWithSlippageFromPool(q.grossInRequired, 100); // 1% slippage headroom3) Build the instruction (no send)
import { Program, BN } from "@coral-xyz/anchor";
import {
derivePoolPda,
deriveEventAuthority,
deriveProtocolFeeVault,
ZERO_PUBKEY,
} from "./utils";
import {
NATIVE_MINT,
getAssociatedTokenAddressSync,
} from "@solana/spl-token";
import { SystemProgram, PublicKey } from "@solana/web3.js";
export async function buildBuyIx(
program: Program,
args: {
payer: PublicKey;
mint: PublicKey;
poolType: 0 | 1 | 2 | 3;
solAmount: bigint; // exact-in amount or max-in for exact-out
minTokensOut: bigint; // min-out for exact-in or exact-out target
isExactIn: boolean;
feeVaultIndex: number;
tokenCreator: PublicKey;
}
) {
const isWSOL = args.poolType === 2 || args.poolType === 3;
const pool = derivePoolPda(program.programId, args.mint);
const tokenVault = getAssociatedTokenAddressSync(args.mint, pool, true);
const quoteVault = isWSOL ? getAssociatedTokenAddressSync(NATIVE_MINT, pool, true) : pool;
const userTokenAta = getAssociatedTokenAddressSync(args.mint, args.payer);
const userQuoteAta = getAssociatedTokenAddressSync(NATIVE_MINT, args.payer);
const feeRecipient = deriveProtocolFeeVault(args.feeVaultIndex, program.programId);
const quoteMint = isWSOL ? NATIVE_MINT : ZERO_PUBKEY;
const tokenCreatorAta = getAssociatedTokenAddressSync(NATIVE_MINT, args.tokenCreator);
const ix = await program.methods
.buy({
solAmount: new BN(args.solAmount.toString()),
minTokensOut: new BN(args.minTokensOut.toString()),
isExactIn: args.isExactIn,
})
.accounts({
pool,
mint: args.mint,
quoteMint,
tokenVault,
quoteVault,
userTokenAccount: userTokenAta,
userQuoteAccount: userQuoteAta,
feeRecipient,
payer: args.payer,
tokenProgram: new PublicKey("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"),
associatedTokenProgram: new PublicKey("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"),
systemProgram: SystemProgram.programId,
tradersdex_curve: program.programId,
eventAuthority: deriveEventAuthority(program.programId),
tokenCreator: args.tokenCreator,
tokenCreatorAta,
})
.instruction();
return { ix };
}Putting it together
Exact-in send:
sol_amount = grossIn(your planned spend)min_tokens_out = minTokensOut(slippage protected)is_exact_in = true
Exact-out send:
sol_amount = maxSolIn(slippage padded cap)min_tokens_out = desiredOutis_exact_in = false
Fee vault PDAs
Protocol fee vaults are derived as:
protocol_fee_vault[i] = PDA(["protocol_fee_vault", [i]])Use i ∈ [0..NUM_FEE_ACCOUNTS) and pass the derived PDA as fee_recipient.
Indexer: log_trade_internal
log_trade_internalEvery successful buy triggers an inner instruction carrying TradeLogData with:
pool,trader,mintsol_amount(gross in)token_amountfee_amount,creator_fee,protocol_feeis_buy = truePost-trade:
vr_quote,vr_token,raised_quote,sold_supplytimestamp
Consume these logs to index trades reliably.
Troubleshooting
InvalidWsolAccount (WSOL pools): ensure
quote_mint == NATIVE_MINTquote_vault == ATA(NATIVE_MINT, pool)user_quote_account == ATA(NATIVE_MINT, payer)For creator-fee pools,
token_creator_ata == ATA(NATIVE_MINT, token_creator)
InvalidFeeRecipient: derive one of
protocol_fee_vault[i].SlippageExceeded: relax
min_tokens_out(exact-in) or increasesol_amountcap (exact-out).PoolNotLive: target reached or sold out.
Last updated