Solana market identification
Trading terminals and indexers can identify a current Candle launch from its creation transaction. Use the Candle attribution signer for fast discovery, then verify the Candle launch instruction before classifying the mint.
Current mainnet identifiers
Section titled “Current mainnet identifiers”| Purpose | Address |
|---|---|
| Candle attribution signer | CNDLVYCkAw5agDbwpSDKiagdyqBdbSgxP4kmysbt24Y2 |
| Candle bonding-curve program | 6wXTwJwdtA5RhNu6VyYnBzrV51TbsMpTbo5kZv2zdssG |
The attribution address is a signer, not the token creator, fee payer, or mint authority. It is deliberately included as a read-only signer in launch transactions so indexers such as Axiom, GMGN, and Fomo.family can attribute the launch to Candle.
Recommended detection rule
Section titled “Recommended detection rule”For each successful Solana transaction:
- Resolve the complete v0 message account list, including address lookup-table accounts.
- Confirm
CNDLVYCkAw5agDbwpSDKiagdyqBdbSgxP4kmysbt24Y2is a required transaction signer. - Find the top-level instruction whose program ID is
6wXTwJwdtA5RhNu6VyYnBzrV51TbsMpTbo5kZv2zdssGand whose first eight data bytes are theinitialize_pooldiscriminator[95, 180, 10, 172, 84, 174, 232, 40]. - Read
base_mintandquote_mintfrom that instruction’s account list (positions 1 and 4), and the pool from position 2. - Index
base_mintas the launched token and the pool as its pre-graduation market.
This rule covers current Open and Exclusive launches and all supported Solana quote assets. Launch tier and quote asset change the curve configuration; they do not change the attribution signer or launch program.
Enumerate every Candle curve
Section titled “Enumerate every Candle curve”Discovery does not have to start from a transaction. Every curve is a Pool account owned by the bonding-curve program, and Anchor prefixes each account with an eight-byte type discriminator, so a single call returns the complete set:
const pools = await connection.getProgramAccounts( new PublicKey("6wXTwJwdtA5RhNu6VyYnBzrV51TbsMpTbo5kZv2zdssG"), { filters: [{ memcmp: { offset: 0, bytes: bs58.encode(Buffer.from([241, 154, 109, 4, 17, 177, 109, 188])) } }] },)This is the recommended way to backfill history or reconcile an index, because it does not depend on transaction retention. Pair it with the detection rule above when you need to confirm provenance for one specific mint.
Validate a candidate mint or pool
Section titled “Validate a candidate mint or pool”For a candidate mint, locate its successful creation transaction and apply the signer and initialize_pool checks above. A CNDL vanity suffix, token name, symbol, metadata URI, creator address, or Candle API label is not proof of provenance.
For a candidate pre-graduation pool, require all of the following:
- The account is owned by the canonical Candle bonding-curve program.
- Its first eight bytes are the
Pooldiscriminator[241, 154, 109, 4, 17, 177, 109, 188]. - Its decoded
base_mint,quote_mint, and pool address agree with the verifiedinitialize_pooltransaction.
After migration, identify the Meteora market from Candle’s migration state and transaction rather than selecting an arbitrary external pool for the same mint.
Lifecycle indexing
Section titled “Lifecycle indexing”Creation is only the first step. A terminal should continue to read the Candle pool state and route by lifecycle. The pool’s is_migrated boolean, together with quote_reserve against migration_threshold, is the authoritative source:
| State | Pool state | Terminal behavior |
|---|---|---|
| Trading on the curve | is_migrated = false, quote_reserve < migration_threshold | Quote and execute against the Candle bonding-curve program |
| Graduated, migration pending | is_migrated = false, quote_reserve >= migration_threshold | Stop curve trading and wait for the migration transaction |
| Migrated | is_migrated = true | Discover and route to the resulting Meteora market |
Graduation is not a single instruction. The curve stops accepting swaps once the threshold is reached, and the migration is performed by withdraw_for_migration, which emits MigrationWithdrawn and PoolMigrated. There is no graduate instruction to watch for.
Do not infer the active venue only from token age, liquidity, or the existence of an external pair. See Solana lifecycle and migration for state transitions and Terminals and indexers for the complete integration checklist.
Indexing trades
Section titled “Indexing trades”Curve trades are the two swap instructions, and each emits one SwapExecuted event carrying the full fill:
| Field | Meaning |
|---|---|
pool | The curve the trade executed against |
user | The trader |
amount_in, amount_out | Filled amounts, in base units of the respective mints |
fee | Fee taken, in the input mint’s base units |
is_buy | true for quote to base, false for base to quote |
quote_reserve, base_reserve | Post-trade reserves, for price and progress |
migration_threshold | Graduation target, so progress is computable from the event alone |
Anchor emits events as a CPI to the program’s event_authority, so they appear in inner instructions rather than only in log strings. Match the eight-byte event discriminator from the table below at the start of the event data.
Token metadata
Section titled “Token metadata”Metadata follows the Metaplex Token Metadata standard. The uri set at launch points to a JSON document with name, description, image, symbol, and an extensions object holding social links.
Images are referenced from inside that JSON and are never part of the transaction. Metadata content is not proof of provenance; only the program and instruction checks above are.
Program interface
Section titled “Program interface”The program publishes its Anchor IDL on chain, so explorers decode its instructions automatically and an Anchor client can build itself with no vendored interface file:
const provider = new AnchorProvider(connection, wallet, {})const idl = await Program.fetchIdl(new PublicKey("6wXTwJwdtA5RhNu6VyYnBzrV51TbsMpTbo5kZv2zdssG"), provider)const program = new Program(idl, provider)
const pools = await program.account.pool.all() // every Candle curve, decodedThe IDL account is at J3jFy9rBfH7YTQJabNrUAFb6Nj9pgf1godrUWkF2GkGY, derived as
createWithSeed(findProgramAddress([], programId)[0], "anchor:idl", programId). anchor idl fetch 6wXTwJwdtA5RhNu6VyYnBzrV51TbsMpTbo5kZv2zdssG prints the same document. Its update authority is the
same multisig that holds the program’s upgrade authority.
Everything below is the same interface expressed as raw bytes, for clients that do not use Anchor.
Discriminators are the first eight bytes of sha256("global:<instruction>") for instructions,
sha256("account:<Account>") for accounts, and sha256("event:<Event>") for events.
| Kind | Name | Discriminator |
|---|---|---|
| Instruction | initialize_pool | [95, 180, 10, 172, 84, 174, 232, 40] |
| Instruction | swap_quote_to_base (buy) | [58, 30, 190, 32, 188, 101, 164, 184] |
| Instruction | swap_base_to_quote (sell) | [19, 60, 200, 100, 34, 177, 72, 204] |
| Instruction | withdraw_for_migration | [229, 30, 82, 104, 62, 251, 75, 69] |
| Instruction | collect_fees | [164, 152, 207, 99, 30, 186, 19, 182] |
| Instruction | update_nft_gating | [195, 235, 21, 213, 78, 122, 248, 77] |
| Account | Pool | [241, 154, 109, 4, 17, 177, 109, 188] |
| Event | PoolInitialized | [100, 118, 173, 87, 12, 198, 254, 229] |
| Event | SwapExecuted | [150, 166, 26, 225, 28, 89, 38, 79] |
| Event | MigrationWithdrawn | [113, 119, 224, 21, 172, 69, 136, 249] |
| Event | PoolMigrated | [250, 204, 24, 195, 37, 253, 152, 6] |
| Event | FeesCollected | [233, 23, 117, 225, 107, 178, 254, 8] |
| Event | NftGatingUpdated | [112, 176, 134, 6, 63, 230, 66, 215] |
Both swap instructions take amount_in: u64 and minimum_amount_out: u64, and share an account order whose first seven entries are payer, pool, pool_authority, base_vault, quote_vault, user_base_account, user_quote_account. A buy carries one extra optional account, mint_record in position 7, required only while NFT gating is enabled and the pool has not migrated.
The pool account is a PDA of the bonding-curve program derived from the seeds ["pool", base_mint, quote_mint], and pool_authority from ["pool_authority", pool]. A terminal can therefore compute a market address from its mint pair without an index.
Gated curves and the Believer NFT
Section titled “Gated curves and the Believer NFT”A curve whose nft_gating.enabled is true only accepts buys from a wallet that holds a Believer NFT. The proof is a MintRecord PDA of the Believer NFT program vwJ5JhNiReriKEysmKsvKW2TYfhNMUGmbwDLiRpVkR3, derived from the buyer’s wallet:
const [mintRecord] = PublicKey.findProgramAddressSync( [Buffer.from("mint_record"), buyer.toBuffer()], new PublicKey("vwJ5JhNiReriKEysmKsvKW2TYfhNMUGmbwDLiRpVkR3"),)Pass that as mint_record on swap_quote_to_base. Whether the account exists is also the cheapest eligibility check: no MintRecord means the wallet cannot buy into a gated curve, so a terminal can disable the buy path before building a transaction that would fail. The record itself carries wallet, nftMint, tier, scoreAtMint, and mintedAt.
That program publishes its IDL on chain too, so Program.fetchIdl gives a decoding client for it on the same terms as the curve. Sell (swap_base_to_quote) is never gated, and gating stops applying once the pool has migrated.
The Pool account is borsh-encoded with no padding, so a client that does not use Anchor can read it at fixed offsets. Total length is 365 bytes:
| Offset | Size | Field | Type |
|---|---|---|---|
| 0 | 8 | discriminator | [241, 154, 109, 4, 17, 177, 109, 188] |
| 8 | 32 | authority | pubkey |
| 40 | 32 | base_mint | pubkey |
| 72 | 32 | quote_mint | pubkey |
| 104 | 32 | base_vault | pubkey |
| 136 | 32 | quote_vault | pubkey |
| 168 | 8 | quote_reserve | u64 |
| 176 | 8 | base_reserve | u64 |
| 184 | 8 | migration_threshold | u64 |
| 192 | 1 | is_migrated | bool |
| 193 | 33 | nft_gating | { nft_program_id: pubkey, enabled: bool } |
| 226 | 1 | bump | u8 |
| 227 | 2 | fee_bps | u16 |
| 229 | 8 | accumulated_fees | u64 |
| 237 | 32 | fee_recipient | pubkey |
| 269 | 16 | sqrt_price | u128 |
| 285 | 16 | sqrt_start_price | u128 |
| 301 | 16 | seg0_target_sqrt_price | u128 |
| 317 | 16 | seg0_liquidity | u128 |
| 333 | 16 | seg1_target_sqrt_price | u128 |
| 349 | 16 | seg1_liquidity | u128 |
A memcmp filter at offset 192 for a single byte therefore separates live curves from migrated ones without decoding.
PoolInitialized carries pool, authority, base_mint, quote_mint, migration_threshold, nft_gating_enabled, and fee_bps, which is enough to register a new market from the event alone.
Operational stability
Section titled “Operational stability”The program ID is the protocol identity. The attribution signer is an operational discovery aid and may be rotated if its key-management requirements change. Integrators should keep both values configurable and monitor this deployment reference for changes rather than hard-coding the signer as an immutable protocol constant.
Recent transactions involving the attribution signer can be inspected on Solscan.