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:
buy
Emits:TradeEvent
via internallog_trade_internal
Pool 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_ata
is 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_internal
withTradeLogData
(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 guard
Exact-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 headroom
3) 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 = desiredOut
is_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_internal
Every successful buy triggers an inner instruction carrying TradeLogData
with:
pool
,trader
,mint
sol_amount
(gross in)token_amount
fee_amount
,creator_fee
,protocol_fee
is_buy = true
Post-trade:
vr_quote
,vr_token
,raised_quote
,sold_supply
timestamp
Consume these logs to index trades reliably.
Troubleshooting
InvalidWsolAccount (WSOL pools): ensure
quote_mint == NATIVE_MINT
quote_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_amount
cap (exact-out).PoolNotLive: target reached or sold out.
Last updated