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
BondingCurveBucketV2accounts across the program - Swap events — discriminator byte
255on 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.
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
| Field | Type | Description |
|---|---|---|
swapDirection | SwapDirection | SwapDirection.Buy (SOL in, tokens out) or SwapDirection.Sell (tokens in, SOL out) |
quoteTokenAmount | bigint | SOL amount on the swap (input for buys, gross output for sells), in lamports |
baseTokenAmount | bigint | Token amount on the swap (output for buys, input for sells) |
fee | bigint | Protocol fee charged, in lamports |
creatorFee | bigint | Creator fee charged, in lamports (0 if no creator fee configured) |
baseTokenBalance | bigint | baseTokenBalance after the swap |
quoteTokenDepositTotal | bigint | quoteTokenDepositTotal after the swap |
virtualSol | bigint | Virtual SOL reserve (immutable — useful for price calculation without fetching the account) |
virtualTokens | bigint | Virtual token reserve (immutable — same as above) |
blockTime | bigint | Unix timestamp of the block containing the swap |
Decoding from a Confirmed Transaction
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:
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.
| Event | Description | Key Fields |
|---|---|---|
| Token Created | SPL token minted, genesis account initialized | baseMint, genesisAccount |
| Bonding Curve Added | BondingCurveBucketV2 account created | bucketPda, baseTokenAllocation, virtualSol, virtualTokens |
| Finalized | Launch configuration locked, buckets activated | genesisAccount |
| Goes Live | swapStartCondition met, trading open | bucketPda, timestamp |
| Swap | Buy or sell executed | BondingCurveSwapEvent (discriminator 255) |
| Sold Out | baseTokenBalance === 0 | bucketPda, quoteTokenDepositTotal |
| Graduation Crank | Liquidity migration instruction submitted | bucketPda, raydiumCpmmPool |
| Graduated | Raydium CPMM pool funded, bonding curve closed | cpmmPoolPda, accumulated SOL |
Account Discriminators and PDA Derivation
Discriminators
| Account | Discriminator | Description |
|---|---|---|
GenesisAccountV2 | Unique per account type | Master coordination account |
BondingCurveBucketV2 | Unique per account type | Bonding curve AMM state |
BondingCurveSwapEvent | 255 (inner instruction) | Per-swap event emitted by the program |
PDA Seeds
| PDA | Seeds |
|---|---|
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
virtualSolandvirtualTokensare included in everyBondingCurveSwapEvent— no separate account fetch is required to compute price from events; they are immutable after curve creation- The
BondingCurveSwapEventdiscriminator is always byte255— any inner instruction on the Genesis program with this leading byte is a swap event - Between
isSoldOutreturningtrueandisGraduatedreturningtrue, the curve is sold out but the Raydium CPMM pool is not yet funded; do not redirect users to Raydium untilisGraduatedconfirms the pool exists isGraduatedmakes 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.
