Launch Types

Bonding Curve — Indexing & Events

Last updated April 9, 2026

Index the complete Genesis Bonding Curve lifecycle — discover curves via GPA, decode per-swap events, and track price and state changes without polling.

Summary

The Genesis program emits a BondingCurveSwapEvent inner instruction on every confirmed swap. Indexers can combine this with GPA queries and lifecycle instruction tracking to reconstruct full curve state without fetching accounts after every trade.

  • GPA discovery — find all BondingCurveBucketV2 accounts across the program
  • Swap events — discriminator byte 255 on inner instructions; contains direction, amounts, fees, and post-swap reserves
  • Price from events — derive current price from event data without additional RPC calls
  • Lifecycle tracking — eight distinct events from token creation through Raydium graduation

Program ID: GNS1S5J5AspKXgpjz6SvKL66kPaKWAhaGRhCqPRxii2B

Discovering All Bonding Curves (GPA)

Use the GPA builder to retrieve every BondingCurveBucketV2 account on the program — useful for dashboards, aggregators, and indexers. For the full account field reference, see Advanced Internals.

discover-all-curves.ts
1import { getBondingCurveBucketV2GpaBuilder } from '@metaplex-foundation/genesis';
2
3const allCurves = await getBondingCurveBucketV2GpaBuilder(umi)
4 .whereField('discriminator', /* BondingCurveBucketV2 discriminator */)
5 .get();
6
7for (const curve of allCurves) {
8 console.log('Bucket PDA: ', curve.publicKey.toString());
9 console.log('Base token balance: ', curve.data.baseTokenBalance.toString());
10}

Decoding Swap Events

Every confirmed swap emits a BondingCurveSwapEvent as an inner instruction with discriminator byte 255. Decode it from the transaction to get exact post-swap reserve state, fee breakdown, and direction.

BondingCurveSwapEvent Fields

FieldTypeDescription
swapDirectionSwapDirectionSwapDirection.Buy (SOL in, tokens out) or SwapDirection.Sell (tokens in, SOL out)
quoteTokenAmountbigintSOL amount on the swap (input for buys, gross output for sells), in lamports
baseTokenAmountbigintToken amount on the swap (output for buys, input for sells)
feebigintProtocol fee charged, in lamports
creatorFeebigintCreator fee charged, in lamports (0 if no creator fee configured)
baseTokenBalancebigintbaseTokenBalance after the swap
quoteTokenDepositTotalbigintquoteTokenDepositTotal after the swap
virtualSolbigintVirtual SOL reserve (immutable — useful for price calculation without fetching the account)
virtualTokensbigintVirtual token reserve (immutable — same as above)
blockTimebigintUnix timestamp of the block containing the swap

Decoding from a Confirmed Transaction

decode-swap-event.ts
1import { getBondingCurveSwapEventSerializer, SwapDirection } from '@metaplex-foundation/genesis';
2
3const GENESIS_PROGRAM_ID = 'GNS1S5J5AspKXgpjz6SvKL66kPaKWAhaGRhCqPRxii2B';
4const SWAP_EVENT_DISCRIMINATOR = 255;
5
6async function decodeSwapEvent(signature: string) {
7 const tx = await umi.rpc.getTransaction(signature, {
8 commitment: 'confirmed',
9 });
10
11 if (!tx) throw new Error('Transaction not found');
12
13 const serializer = getBondingCurveSwapEventSerializer();
14
15 for (const innerIx of tx.meta?.innerInstructions ?? []) {
16 for (const ix of innerIx.instructions) {
17 const programId = tx.transaction.message.accountKeys[ix.programIdIndex];
18
19 if (programId.toString() !== GENESIS_PROGRAM_ID) continue;
20
21 const data = ix.data; // Uint8Array
22 if (data[0] !== SWAP_EVENT_DISCRIMINATOR) continue;
23
24 // Slice off the discriminator byte, then deserialize.
25 const [event] = serializer.deserialize(data.slice(1));
26
27 const isBuy = event.swapDirection === SwapDirection.Buy;
28 console.log('Direction: ', isBuy ? 'buy' : 'sell');
29 console.log('Quote token amount: ', event.quoteTokenAmount.toString(), 'lamports');
30 console.log('Base token amount: ', event.baseTokenAmount.toString());
31 console.log('Protocol fee: ', event.fee.toString(), 'lamports');
32 console.log('Creator fee: ', event.creatorFee.toString(), 'lamports');
33 console.log('Base balance: ', event.baseTokenBalance.toString());
34 console.log('Quote deposit total: ', event.quoteTokenDepositTotal.toString());
35
36 return event;
37 }
38 }
39
40 return null; // No swap event found in this transaction.
41}

Tracking Current Price from Events

Derive the current price from the post-swap reserve state included in each BondingCurveSwapEvent rather than fetching the account after every trade:

price-from-event.ts
1function getPriceFromEvent(event: BondingCurveSwapEvent, bucket: BondingCurveBucketV2) {
2 // totalTokens = virtualTokens + post-swap baseTokenBalance (included in the event)
3 const totalTokens = bucket.virtualTokens + event.baseTokenBalance;
4 // totalSol = virtualSol + post-swap quoteTokenDepositTotal (included in the event)
5 const totalSol = bucket.virtualSol + event.quoteTokenDepositTotal;
6 // Price: tokens per SOL (lamports per base token unit as bigint)
7 return totalSol > 0n ? totalTokens / totalSol : 0n;
8}

virtualSol and virtualTokens are included in every BondingCurveSwapEvent — no separate account fetch is needed to compute price from an event. They are immutable after curve creation.

Lifecycle Events

Track the full lifecycle of a bonding curve by listening for Genesis program instructions and inner instruction events. For executing swap transactions using the SDK, see Bonding Curve Swap Integration.

EventDescriptionKey Fields
Token CreatedSPL token minted, genesis account initializedbaseMint, genesisAccount
Bonding Curve AddedBondingCurveBucketV2 account createdbucketPda, baseTokenAllocation, virtualSol, virtualTokens
FinalizedLaunch configuration locked, buckets activatedgenesisAccount
Goes LiveswapStartCondition met, trading openbucketPda, timestamp
SwapBuy or sell executedBondingCurveSwapEvent (discriminator 255)
Sold OutbaseTokenBalance === 0bucketPda, quoteTokenDepositTotal
Graduation CrankLiquidity migration instruction submittedbucketPda, raydiumCpmmPool
GraduatedRaydium CPMM pool funded, bonding curve closedcpmmPoolPda, accumulated SOL

Account Discriminators and PDA Derivation

Discriminators

AccountDiscriminatorDescription
GenesisAccountV2Unique per account typeMaster coordination account
BondingCurveBucketV2Unique per account typeBonding curve AMM state
BondingCurveSwapEvent255 (inner instruction)Per-swap event emitted by the program

PDA Seeds

PDASeeds
GenesisAccountV2["genesis_account_v2", baseMint, genesisIndex (u8)]
BondingCurveBucketV2["bonding_curve_bucket_v2", genesisAccount, bucketIndex (u8)]

Derive PDAs in TypeScript with findGenesisAccountV2Pda and findBondingCurveBucketV2Pda from the Genesis SDK.

Notes

  • virtualSol and virtualTokens are included in every BondingCurveSwapEvent — no separate account fetch is required to compute price from events; they are immutable after curve creation
  • The BondingCurveSwapEvent discriminator is always byte 255 — any inner instruction on the Genesis program with this leading byte is a swap event
  • Between isSoldOut returning true and isGraduated returning true, the curve is sold out but the Raydium CPMM pool is not yet funded; do not redirect users to Raydium until isGraduated confirms the pool exists
  • isGraduated makes an RPC call on every invocation — cache the result in your indexer rather than calling it on every render

FAQ

How do I decode a BondingCurveSwapEvent?

Find inner instructions on the Genesis program (GNS1S5J5AspKXgpjz6SvKL66kPaKWAhaGRhCqPRxii2B) where the first data byte is 255. Slice off that byte and pass the remainder to getBondingCurveSwapEventSerializer().deserialize(data.slice(1)). The returned object contains swapDirection, quoteTokenAmount, baseTokenAmount, fee, creatorFee, and post-swap reserve state (baseTokenBalance, quoteTokenDepositTotal, virtualSol, virtualTokens, blockTime).

What is the difference between isSoldOut and isGraduated?

isSoldOut is a synchronous local check — it returns true as soon as baseTokenBalance is 0n. isGraduated is an async RPC call that verifies whether the Raydium CPMM pool has been created and funded onchain. There is a window between sell-out and graduation where isSoldOut is true but isGraduated is false. Do not redirect users to Raydium until isGraduated confirms the pool exists.