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
BondingCurveBucketV2account state - Checking lifecycle status with
isSwappable,isSoldOut, andisGraduated - 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
getSwapResultto get the exact fee-adjusted input and output amounts - Slippage protection — derive
minAmountOutScaledwithapplySlippageand 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 ID —
GNS1S5J5AspKXgpjz6SvKL66kPaKWAhaGRhCqPRxii2Bon Solana mainnet
Quick Start
Jump to: Installation · Setup · Fetch Curve · Lifecycle Helpers · Quote · Slippage · Execute Swap · Creator Fees · Errors · API Reference
- Install the packages and configure a Umi instance with the
genesis()plugin - Derive
BondingCurveBucketV2Pdaand fetch the account - Check
isSwappable(bucket)— abort if false - Call
getSwapResult(bucket, amountIn, SwapDirection.Buy)for a fee-adjusted quote - Apply
applySlippage(quote.amountOut, slippageBps)to getminAmountOutScaled - Handle wSOL wrapping manually, then send
swapBondingCurveV2and 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
| Tool | Version |
|---|---|
@metaplex-foundation/genesis | 1.x |
@metaplex-foundation/umi | 1.x |
@metaplex-foundation/umi-bundle-defaults | 1.x |
| Node.js | 18+ |
Installation
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.
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);
1mplx genesis bucket fetch <GENESIS_ACCOUNT> --type bonding-curve
Fetch from a Token Mint
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
| Field | Type | Description |
|---|---|---|
baseTokenBalance | bigint | Tokens remaining on the curve. Zero means sold out. |
baseTokenAllocation | bigint | Total tokens allocated to this curve at creation. |
quoteTokenDepositTotal | bigint | Real SOL deposited by buyers (lamports). Starts at 0. |
virtualSol | bigint | Virtual SOL reserve added at initialization (pricing only). |
virtualTokens | bigint | Virtual token reserve added at initialization (pricing only). |
depositFee | number | Protocol fee rate applied to the SOL side of every swap. |
withdrawFee | number | Protocol fee rate applied to the SOL output side of sells. |
creatorFeeAccrued | bigint | Creator fees accrued since the last claim (lamports). |
creatorFeeClaimed | bigint | Cumulative creator fees claimed to date (lamports). |
swapStartCondition | object | Condition that must be met before trading is allowed. |
swapEndCondition | object | Condition 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).
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
| Helper | Async | Returns | Description |
|---|---|---|---|
isSwappable(bucket) | No | boolean | true when accepting public trades |
isFirstBuyPending(bucket) | No | boolean | true when designated first-buy not yet done |
isSoldOut(bucket) | No | boolean | true when baseTokenBalance === 0n |
getFillPercentage(bucket) | No | number | 0–100 percentage of allocation sold |
isGraduated(umi, bucket) | Yes | boolean | true 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 adjustmentsfee— protocol fee charged, in lamportscreatorFee— 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>
1# Get a buy quote without executing a swap (--info flag)
2mplx genesis swap <GENESIS_ACCOUNT> --info --buyAmount 1000000000
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>
1# Get a sell quote without executing a swap (--info flag)
2mplx genesis swap <GENESIS_ACCOUNT> --info --sellAmount 500000000000
First Buy Fee Waiver
Pass true as the fourth argument to quote a first buy with fees waived:
1const firstBuyQuote = getSwapResult(bucket, SOL_IN, SwapDirection.Buy, true);
2console.log('Fee (waived): ', firstBuyQuote.fee.toString()); // 0n
Current Price Helpers
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.
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>
1# Buy tokens — SOL is wrapped automatically
2# --buyAmount is in lamports (1 SOL = 1000000000)
3mplx genesis swap <GENESIS_ACCOUNT> --buyAmount 1000000000
4
5# With custom slippage (100 bps = 1%)
6mplx genesis swap <GENESIS_ACCOUNT> --buyAmount 1000000000 --slippage 100
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>
1# Sell tokens — SOL is unwrapped automatically
2# --sellAmount is in base token units (with decimals)
3mplx genesis swap <GENESIS_ACCOUNT> --sellAmount 500000000000
4
5# With custom slippage (100 bps = 1%)
6mplx genesis swap <GENESIS_ACCOUNT> --sellAmount 500000000000 --slippage 100
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.
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
| Error | Cause | Resolution |
|---|---|---|
BondingCurveInsufficientFunds | Not enough tokens (buy) or SOL (sell) remaining | Re-fetch the bucket and re-quote; curve may be nearly sold out |
InsufficientOutputAmount | Actual output fell below minAmountOutScaled | Increase slippageBps or retry immediately |
InvalidSwapDirection | swapDirection value is invalid | Pass SwapDirection.Buy or SwapDirection.Sell from the @metaplex-foundation/genesis import |
BondingCurveNotStarted | swapStartCondition not yet met | Check bucket.swapStartCondition and wait |
BondingCurveEnded | Curve is sold out or graduated | Direct users to the Raydium CPMM pool |
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
virtualSolandvirtualTokensare immutable after curve creation — cache them; only real reserve fields change per swapisGraduatedmakes an RPC call on every invocation — cache the result in your indexer- Between
isSoldOutreturningtrueandisGraduatedreturningtrue, the curve is sold out but Raydium is not yet funded; do not send users to Raydium untilisGraduatedconfirms - 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
| Function | Async | Returns | Description |
|---|---|---|---|
getSwapResult(bucket, amountIn, swapDirection, isFirstBuy?) | No | { amountIn, fee, creatorFee, amountOut } | Fee-adjusted swap quote |
getCurrentPrice(bucket) | No | bigint | Base tokens per SOL unit (integer division) |
getCurrentPriceQuotePerBase(bucket) | No | bigint | Lamports per base token unit (integer division) |
getCurrentPriceComponents(bucket) | No | { baseReserves, quoteReserves } | Combined virtual + real reserves as bigints |
Lifecycle Functions
| Function | Async | Returns | Description |
|---|---|---|---|
isSwappable(bucket) | No | boolean | true when accepting public trades |
isFirstBuyPending(bucket) | No | boolean | true when designated first-buy not yet done |
isSoldOut(bucket) | No | boolean | true when baseTokenBalance === 0n |
getFillPercentage(bucket) | No | number | 0–100 percentage of allocation sold |
isGraduated(umi, bucket) | Yes | boolean | true when Raydium CPMM pool exists onchain |
Slippage
| Function | Returns | Description |
|---|---|---|
applySlippage(amountOut, slippageBps) | bigint | Reduces amountOut by slippageBps / 10_000 |
Swap Instruction Accounts
| Account | Writable | Signer | Description |
|---|---|---|---|
genesisAccount | Yes | No | Genesis coordination PDA |
bucket | Yes | No | BondingCurveBucketV2 PDA |
baseMint | No | No | SPL token mint |
quoteMint | No | No | wSOL mint |
baseTokenAccount | Yes | No | User's base token ATA |
quoteTokenAccount | Yes | No | User's wSOL ATA |
payer | Yes | Yes | Transaction fee payer |
Account Discovery
| Function | Returns | Description |
|---|---|---|
findBondingCurveBucketV2Pda(umi, { genesisAccount, bucketIndex }) | [PublicKey, bump] | Derives the bucket PDA |
findGenesisAccountV2Pda(umi, { baseMint, genesisIndex }) | [PublicKey, bump] | Derives the genesis account PDA |
fetchBondingCurveBucketV2(umi, pda) | BondingCurveBucketV2 | Fetches 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.
