Launch Types

Bonding Curve Swap Integration

Last updated April 9, 2026

Use the Genesis SDK to read bonding curve state, compute swap quotes, execute buy and sell transactions onchain, handle slippage, and claim creator fees.

What You'll Build

This guide covers:

  • Fetching and interpreting BondingCurveBucketV2 account state
  • Checking lifecycle status with isSwappable, isSoldOut, and isGraduated
  • Getting accurate swap quotes with getSwapResult
  • Protecting users with applySlippage
  • Constructing buy and sell transactions with swapBondingCurveV2
  • Claiming creator fees from the curve and post-graduation Raydium pool

Summary

Bonding curve swaps use the Genesis SDK to interact with the BondingCurveBucketV2 onchain account — a constant product AMM that accepts SOL and returns tokens (buy) or accepts tokens and returns SOL (sell). For the underlying pricing mathematics, see Theory of Operation.

  • Quote before sending — call getSwapResult to get the exact fee-adjusted input and output amounts
  • Slippage protection — derive minAmountOutScaled with applySlippage and pass it to the instruction
  • wSOL is manual — the swap instruction does not wrap or unwrap native SOL; callers must handle the wSOL ATA themselves
  • Program IDGNS1S5J5AspKXgpjz6SvKL66kPaKWAhaGRhCqPRxii2B on Solana mainnet

Quick Start

Jump to: Installation · Setup · Fetch Curve · Lifecycle Helpers · Quote · Slippage · Execute Swap · Creator Fees · Errors · API Reference

  1. Install the packages and configure a Umi instance with the genesis() plugin
  2. Derive BondingCurveBucketV2Pda and fetch the account
  3. Check isSwappable(bucket) — abort if false
  4. Call getSwapResult(bucket, amountIn, SwapDirection.Buy) for a fee-adjusted quote
  5. Apply applySlippage(quote.amountOut, slippageBps) to get minAmountOutScaled
  6. Handle wSOL wrapping manually, then send swapBondingCurveV2 and confirm

Prerequisites

  • Node.js 18+ — required for native BigInt support
  • Solana wallet funded with SOL for transaction fees and swap input
  • A Solana RPC endpoint (mainnet-beta or devnet)
  • Familiarity with the Umi framework and async/await patterns

Tested Configuration

ToolVersion
@metaplex-foundation/genesis1.x
@metaplex-foundation/umi1.x
@metaplex-foundation/umi-bundle-defaults1.x
Node.js18+

Installation

Terminal
npm install @metaplex-foundation/genesis \
@metaplex-foundation/umi \
@metaplex-foundation/umi-bundle-defaults

Umi and Genesis Plugin Setup

Configure a Umi instance and register the genesis() plugin before calling any SDK function.

setup.ts
1import { createUmi } from '@metaplex-foundation/umi-bundle-defaults';
2import { genesis } from '@metaplex-foundation/genesis';
3import { keypairIdentity } from '@metaplex-foundation/umi';
4import { readFileSync } from 'fs';
5
6const keypairFile = JSON.parse(readFileSync('/path/to/keypair.json', 'utf-8'));
7
8const umi = createUmi('https://api.mainnet-beta.solana.com')
9 .use(genesis());
10
11const keypair = umi.eddsa.createKeypairFromSecretKey(Uint8Array.from(keypairFile));
12umi.use(keypairIdentity(keypair));

Fetching a Bonding Curve BucketV2

Three discovery strategies are available depending on what you already know.

Fetch from a Known Genesis Account

1import {
2 findBondingCurveBucketV2Pda,
3 fetchBondingCurveBucketV2,
4 genesis,
5} from '@metaplex-foundation/genesis';
6import { publicKey } from '@metaplex-foundation/umi';
7import { createUmi } from '@metaplex-foundation/umi-bundle-defaults';
8
9const umi = createUmi('https://api.mainnet-beta.solana.com').use(genesis());
10
11const genesisAccount = publicKey('YOUR_GENESIS_ACCOUNT_PUBKEY');
12
13const [bucketPda] = findBondingCurveBucketV2Pda(umi, {
14 genesisAccount,
15 bucketIndex: 0,
16});
17
18const bucket = await fetchBondingCurveBucketV2(umi, bucketPda);

Fetch from a Token Mint

fetch-from-mint.ts
1import {
2 findGenesisAccountV2Pda,
3 findBondingCurveBucketV2Pda,
4 fetchBondingCurveBucketV2,
5} from '@metaplex-foundation/genesis';
6import { publicKey } from '@metaplex-foundation/umi';
7
8const baseMint = publicKey('TOKEN_MINT_PUBKEY');
9
10const [genesisAccount] = findGenesisAccountV2Pda(umi, {
11 baseMint,
12 genesisIndex: 0,
13});
14
15const [bucketPda] = findBondingCurveBucketV2Pda(umi, {
16 genesisAccount,
17 bucketIndex: 0,
18});
19
20const bucket = await fetchBondingCurveBucketV2(umi, bucketPda);

Reading Bonding Curve BucketV2 State

FieldTypeDescription
baseTokenBalancebigintTokens remaining on the curve. Zero means sold out.
baseTokenAllocationbigintTotal tokens allocated to this curve at creation.
quoteTokenDepositTotalbigintReal SOL deposited by buyers (lamports). Starts at 0.
virtualSolbigintVirtual SOL reserve added at initialization (pricing only).
virtualTokensbigintVirtual token reserve added at initialization (pricing only).
depositFeenumberProtocol fee rate applied to the SOL side of every swap.
withdrawFeenumberProtocol fee rate applied to the SOL output side of sells.
creatorFeeAccruedbigintCreator fees accrued since the last claim (lamports).
creatorFeeClaimedbigintCumulative creator fees claimed to date (lamports).
swapStartConditionobjectCondition that must be met before trading is allowed.
swapEndConditionobjectCondition that ends trading when triggered.

virtualSol and virtualTokens exist only in pricing mathematics — they are never deposited as real assets onchain. See Theory of Operation for how virtual reserves shape the constant product curve.

Bonding Curve Lifecycle Helpers

Five helper functions inspect curve state without additional RPC calls (except isGraduated).

lifecycle-helpers.ts
1import {
2 isSwappable,
3 isFirstBuyPending,
4 isSoldOut,
5 getFillPercentage,
6 isGraduated,
7} from '@metaplex-foundation/genesis';
8
9const canSwap = isSwappable(bucket);
10const firstBuyPending = isFirstBuyPending(bucket);
11const soldOut = isSoldOut(bucket);
12const fillPercent = getFillPercentage(bucket);
13const graduated = await isGraduated(umi, bucket); // async RPC call
HelperAsyncReturnsDescription
isSwappable(bucket)Nobooleantrue when accepting public trades
isFirstBuyPending(bucket)Nobooleantrue when designated first-buy not yet done
isSoldOut(bucket)Nobooleantrue when baseTokenBalance === 0n
getFillPercentage(bucket)Nonumber0–100 percentage of allocation sold
isGraduated(umi, bucket)Yesbooleantrue when Raydium CPMM pool exists onchain

Getting a Swap Quote

getSwapResult(bucket, amountIn, swapDirection, isFirstBuy?) computes the exact fee-adjusted amounts for a swap without sending any transaction.

Returns { amountIn, fee, creatorFee, amountOut }:

  • amountIn — actual input amount after any adjustments
  • fee — protocol fee charged, in lamports
  • creatorFee — creator fee charged, in lamports (0 if no creator fee configured)
  • amountOut — tokens received (buy) or SOL received (sell)

Buy Quote (SOL to Tokens)

1import {
2 genesis,
3 findBondingCurveBucketV2Pda,
4 fetchBondingCurveBucketV2,
5 getSwapResult,
6} from '@metaplex-foundation/genesis';
7import { publicKey } from '@metaplex-foundation/umi';
8import { createUmi } from '@metaplex-foundation/umi-bundle-defaults';
9
10const umi = createUmi('https://api.mainnet-beta.solana.com').use(genesis());
11
12const genesisAccount = publicKey('YOUR_GENESIS_ACCOUNT_PUBKEY');
13const [bucketPda] = findBondingCurveBucketV2Pda(umi, { genesisAccount, bucketIndex: 0 });
14const bucket = await fetchBondingCurveBucketV2(umi, bucketPda);
15
16const SOL_IN = 1_000_000_000n; // 1 SOL in lamports
17
18const buyQuote = getSwapResult(bucket, SOL_IN, 'buy');
19
20console.log('SOL input: ', buyQuote.amountIn.toString(), 'lamports');
21console.log('Total fee: ', buyQuote.fee.toString(), 'lamports');
22console.log('Tokens out: ', buyQuote.amountOut.toString());
23
24// SOL input: 1000000000 lamports
25// Total fee: 10000000 lamports
26// Tokens out: <calculated token amount>

Sell Quote (Tokens to SOL)

1import {
2 genesis,
3 findBondingCurveBucketV2Pda,
4 fetchBondingCurveBucketV2,
5 getSwapResult,
6} from '@metaplex-foundation/genesis';
7import { publicKey } from '@metaplex-foundation/umi';
8import { createUmi } from '@metaplex-foundation/umi-bundle-defaults';
9
10const umi = createUmi('https://api.mainnet-beta.solana.com').use(genesis());
11
12const genesisAccount = publicKey('YOUR_GENESIS_ACCOUNT_PUBKEY');
13const [bucketPda] = findBondingCurveBucketV2Pda(umi, { genesisAccount, bucketIndex: 0 });
14const bucket = await fetchBondingCurveBucketV2(umi, bucketPda);
15
16const TOKENS_IN = 500_000_000_000n; // 500 tokens (9 decimals)
17
18const sellQuote = getSwapResult(bucket, TOKENS_IN, 'sell');
19
20console.log('Tokens input: ', sellQuote.amountIn.toString());
21console.log('Total fee: ', sellQuote.fee.toString(), 'lamports');
22console.log('SOL out: ', sellQuote.amountOut.toString(), 'lamports');
23
24// Tokens input: 500000000000
25// Total fee: <fee in lamports>
26// SOL out: <calculated SOL amount>

First Buy Fee Waiver

Pass true as the fourth argument to quote a first buy with fees waived:

first-buy-quote.ts
1const firstBuyQuote = getSwapResult(bucket, SOL_IN, SwapDirection.Buy, true);
2console.log('Fee (waived): ', firstBuyQuote.fee.toString()); // 0n

Current Price Helpers

current-price.ts
1import {
2 getCurrentPrice,
3 getCurrentPriceQuotePerBase,
4 getCurrentPriceComponents,
5} from '@metaplex-foundation/genesis';
6
7const tokensPerSol = getCurrentPrice(bucket); // bigint
8const lamportsPerToken = getCurrentPriceQuotePerBase(bucket); // bigint
9const { baseReserves, quoteReserves } = getCurrentPriceComponents(bucket);

Slippage Protection

applySlippage(expectedAmountOut, slippageBps) reduces the expected output by the slippage tolerance. Pass the result as minAmountOutScaled to the swap instruction — the onchain program rejects the transaction if the actual output falls below this value.

slippage.ts
1import { getSwapResult, applySlippage, SwapDirection } from '@metaplex-foundation/genesis';
2
3const quote = getSwapResult(bucket, 1_000_000_000n, SwapDirection.Buy);
4const minAmountOutScaled = applySlippage(quote.amountOut, 100); // 1% slippage

Never send a swap without minAmountOutScaled derived from applySlippage. The bonding curve price moves with every trade; without slippage protection a user can receive far fewer tokens than quoted.

Common values: 50 bps (0.5%) for stable conditions; 200 bps (2%) during volatile launches.

Constructing Swap Transactions

swapBondingCurveV2(umi, accounts) builds the swap instruction. The caller is responsible for handling wrapped SOL (wSOL) before and after the transaction.

Buy Transaction (SOL to Tokens)

1import {
2 genesis,
3 findBondingCurveBucketV2Pda,
4 fetchBondingCurveBucketV2,
5 getSwapResult,
6 applySlippage,
7 swapBondingCurveV2,
8 isSwappable,
9} from '@metaplex-foundation/genesis';
10import { findAssociatedTokenPda } from '@metaplex-foundation/mpl-toolbox';
11import { publicKey } from '@metaplex-foundation/umi';
12import { createUmi } from '@metaplex-foundation/umi-bundle-defaults';
13
14const umi = createUmi('https://api.mainnet-beta.solana.com').use(genesis());
15
16const genesisAccount = publicKey('YOUR_GENESIS_ACCOUNT_PUBKEY');
17const baseMint = publicKey('TOKEN_MINT_PUBKEY');
18const quoteMint = publicKey('So11111111111111111111111111111111111111112'); // wSOL
19
20const [bucketPda] = findBondingCurveBucketV2Pda(umi, { genesisAccount, bucketIndex: 0 });
21const bucket = await fetchBondingCurveBucketV2(umi, bucketPda);
22
23if (!isSwappable(bucket)) throw new Error('Curve is not currently accepting swaps');
24
25const SOL_IN = 1_000_000_000n; // 1 SOL in lamports
26const quote = getSwapResult(bucket, SOL_IN, 'buy');
27const minAmountOut = applySlippage(quote.amountOut, 100); // 1% slippage
28
29const [userBaseTokenAccount] = findAssociatedTokenPda(umi, { mint: baseMint, owner: umi.identity.publicKey });
30const [userQuoteTokenAccount] = findAssociatedTokenPda(umi, { mint: quoteMint, owner: umi.identity.publicKey });
31
32// NOTE: Fund the wSOL ATA before this call — see the wSOL Wrapping Note on this page.
33const result = await swapBondingCurveV2(umi, {
34 genesisAccount,
35 bucketPda,
36 baseMint,
37 quoteMint,
38 userBaseTokenAccount,
39 userQuoteTokenAccount,
40 amountIn: quote.amountIn,
41 minAmountOut,
42 direction: 'buy',
43}).sendAndConfirm(umi);
44
45console.log('Buy confirmed:', result.signature);
46
47// Buy confirmed: <base58 transaction signature>

Sell Transaction (Tokens to SOL)

1import {
2 genesis,
3 findBondingCurveBucketV2Pda,
4 fetchBondingCurveBucketV2,
5 getSwapResult,
6 applySlippage,
7 swapBondingCurveV2,
8} from '@metaplex-foundation/genesis';
9import { findAssociatedTokenPda } from '@metaplex-foundation/mpl-toolbox';
10import { publicKey } from '@metaplex-foundation/umi';
11import { createUmi } from '@metaplex-foundation/umi-bundle-defaults';
12
13const umi = createUmi('https://api.mainnet-beta.solana.com').use(genesis());
14
15const genesisAccount = publicKey('YOUR_GENESIS_ACCOUNT_PUBKEY');
16const baseMint = publicKey('TOKEN_MINT_PUBKEY');
17const quoteMint = publicKey('So11111111111111111111111111111111111111112'); // wSOL
18
19const [bucketPda] = findBondingCurveBucketV2Pda(umi, { genesisAccount, bucketIndex: 0 });
20const bucket = await fetchBondingCurveBucketV2(umi, bucketPda);
21
22const [userBaseTokenAccount] = findAssociatedTokenPda(umi, { mint: baseMint, owner: umi.identity.publicKey });
23const [userQuoteTokenAccount] = findAssociatedTokenPda(umi, { mint: quoteMint, owner: umi.identity.publicKey });
24
25const TOKENS_IN = 500_000_000_000n; // 500 tokens (9 decimals)
26const quote = getSwapResult(bucket, TOKENS_IN, 'sell');
27const minAmountOut = applySlippage(quote.amountOut, 100); // 1% slippage
28
29const result = await swapBondingCurveV2(umi, {
30 genesisAccount,
31 bucketPda,
32 baseMint,
33 quoteMint,
34 userBaseTokenAccount,
35 userQuoteTokenAccount,
36 amountIn: quote.amountIn,
37 minAmountOut,
38 direction: 'sell',
39}).sendAndConfirm(umi);
40
41// NOTE: Close the wSOL ATA after confirming to unwrap back to native SOL.
42console.log('Sell confirmed:', result.signature);
43
44// Sell confirmed: <base58 transaction signature>

wSOL Wrapping Note

Manual wSOL handling required

swapBondingCurveV2 uses wrapped SOL (wSOL) as the quote token and does not wrap or unwrap native SOL automatically.

For buys: Create a wSOL ATA, transfer the required lamports into it, and call syncNative before sending the swap.

For sells: Close the wSOL ATA after the swap confirms to unwrap back to native SOL.

Only wSOL is accepted as the quote token in the current version.

wsol-wrap-unwrap.ts
1import {
2 findAssociatedTokenPda,
3 createAssociatedTokenAccountIdempotentInstruction,
4 syncNative,
5 closeToken,
6} from '@metaplex-foundation/mpl-toolbox';
7import { transactionBuilder, sol, publicKey } from '@metaplex-foundation/umi';
8
9const wSOL = publicKey('So11111111111111111111111111111111111111112');
10const [wSolAta] = findAssociatedTokenPda(umi, { mint: wSOL, owner: umi.identity.publicKey });
11
12// --- Wrap SOL before a buy ---
13const wrapBuilder = transactionBuilder()
14 .add(createAssociatedTokenAccountIdempotentInstruction(umi, {
15 mint: wSOL,
16 owner: umi.identity.publicKey,
17 }))
18 .add(syncNative(umi, { account: wSolAta }));
19
20await wrapBuilder.sendAndConfirm(umi);
21
22// --- Unwrap SOL after a sell ---
23const unwrapBuilder = closeToken(umi, {
24 account: wSolAta,
25 destination: umi.identity.publicKey,
26 authority: umi.identity,
27});
28
29await unwrapBuilder.sendAndConfirm(umi);

Claiming Creator Fees

Creator fees are accrued in the bucket (creatorFeeAccrued) on every swap rather than transferred directly. Collect them via the permissionless claimBondingCurveCreatorFeeV2 instruction while the curve is active, and via claimRaydiumCreatorFeeV2 after graduation.

For the full claiming flow — including how to check the accrued balance and handle post-graduation Raydium LP fees — see Creator Fees.

Error Handling

ErrorCauseResolution
BondingCurveInsufficientFundsNot enough tokens (buy) or SOL (sell) remainingRe-fetch the bucket and re-quote; curve may be nearly sold out
InsufficientOutputAmountActual output fell below minAmountOutScaledIncrease slippageBps or retry immediately
InvalidSwapDirectionswapDirection value is invalidPass SwapDirection.Buy or SwapDirection.Sell from the @metaplex-foundation/genesis import
BondingCurveNotStartedswapStartCondition not yet metCheck bucket.swapStartCondition and wait
BondingCurveEndedCurve is sold out or graduatedDirect users to the Raydium CPMM pool
error-handling.ts
1async function executeBuy(bucket, amountIn: bigint, slippageBps: number) {
2 if (!isSwappable(bucket)) {
3 if (isSoldOut(bucket)) throw new Error('Token sold out. Trade on Raydium.');
4 throw new Error('Curve not yet active. Check the start time.');
5 }
6
7 const quote = getSwapResult(bucket, amountIn, SwapDirection.Buy);
8 const minAmountOutScaled = applySlippage(quote.amountOut, slippageBps);
9
10 try {
11 return await swapBondingCurveV2(umi, {
12 amount: quote.amountIn,
13 minAmountOutScaled,
14 swapDirection: SwapDirection.Buy,
15 // ... accounts
16 }).sendAndConfirm(umi);
17 } catch (err: any) {
18 if (err.message?.includes('InsufficientOutputAmount'))
19 throw new Error('Price moved. Try again with higher slippage.');
20 if (err.message?.includes('BondingCurveInsufficientFunds'))
21 throw new Error('Not enough tokens remaining. Reduce amount.');
22 throw err;
23 }
24}

Notes

  • Re-fetch the bucket before every swap in production — the price changes with every trade by any user
  • virtualSol and virtualTokens are immutable after curve creation — cache them; only real reserve fields change per swap
  • isGraduated makes an RPC call on every invocation — cache the result in your indexer
  • Between isSoldOut returning true and isGraduated returning true, the curve is sold out but Raydium is not yet funded; do not send users to Raydium until isGraduated confirms
  • For event decoding and lifecycle indexing, see Indexing & Events
  • All fee amounts are in lamports (SOL side); see Protocol Fees for current rates

API Reference

Quote and Price Functions

FunctionAsyncReturnsDescription
getSwapResult(bucket, amountIn, swapDirection, isFirstBuy?)No{ amountIn, fee, creatorFee, amountOut }Fee-adjusted swap quote
getCurrentPrice(bucket)NobigintBase tokens per SOL unit (integer division)
getCurrentPriceQuotePerBase(bucket)NobigintLamports per base token unit (integer division)
getCurrentPriceComponents(bucket)No{ baseReserves, quoteReserves }Combined virtual + real reserves as bigints

Lifecycle Functions

FunctionAsyncReturnsDescription
isSwappable(bucket)Nobooleantrue when accepting public trades
isFirstBuyPending(bucket)Nobooleantrue when designated first-buy not yet done
isSoldOut(bucket)Nobooleantrue when baseTokenBalance === 0n
getFillPercentage(bucket)Nonumber0–100 percentage of allocation sold
isGraduated(umi, bucket)Yesbooleantrue when Raydium CPMM pool exists onchain

Slippage

FunctionReturnsDescription
applySlippage(amountOut, slippageBps)bigintReduces amountOut by slippageBps / 10_000

Swap Instruction Accounts

AccountWritableSignerDescription
genesisAccountYesNoGenesis coordination PDA
bucketYesNoBondingCurveBucketV2 PDA
baseMintNoNoSPL token mint
quoteMintNoNowSOL mint
baseTokenAccountYesNoUser's base token ATA
quoteTokenAccountYesNoUser's wSOL ATA
payerYesYesTransaction fee payer

Account Discovery

FunctionReturnsDescription
findBondingCurveBucketV2Pda(umi, { genesisAccount, bucketIndex })[PublicKey, bump]Derives the bucket PDA
findGenesisAccountV2Pda(umi, { baseMint, genesisIndex })[PublicKey, bump]Derives the genesis account PDA
fetchBondingCurveBucketV2(umi, pda)BondingCurveBucketV2Fetches and deserializes the account

FAQ

What is the difference between isSwappable and isSoldOut?

isSwappable returns true only when the curve is actively accepting public trades. isSoldOut returns true the moment baseTokenBalance reaches zero, ending trading and triggering graduation. A curve can be sold out but not yet graduated.

Do I need to wrap SOL before calling swapBondingCurveV2?

Yes. The bonding curve uses wSOL as its quote token and swapBondingCurveV2 does not wrap or unwrap native SOL automatically. See wSOL Wrapping Note.

What does getSwapResult return and how does it handle fees?

getSwapResult returns { amountIn, fee, creatorFee, amountOut }. For buys, fees are deducted from SOL input before the AMM formula runs. For sells, fees are deducted from the SOL output after the AMM runs. Pass true as the fourth argument to simulate the first-buy fee waiver (all fees zeroed).

How do I protect against slippage?

Call applySlippage(quote.amountOut, slippageBps) to derive minAmountOutScaled, then pass it to swapBondingCurveV2 as the minAmountOutScaled field. The onchain program rejects the transaction if the actual output falls below this value.