Skip to content

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.

PurposeAddress
Candle attribution signerCNDLVYCkAw5agDbwpSDKiagdyqBdbSgxP4kmysbt24Y2
Candle bonding-curve program6wXTwJwdtA5RhNu6VyYnBzrV51TbsMpTbo5kZv2zdssG

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.

For each successful Solana transaction:

  1. Resolve the complete v0 message account list, including address lookup-table accounts.
  2. Confirm CNDLVYCkAw5agDbwpSDKiagdyqBdbSgxP4kmysbt24Y2 is a required transaction signer.
  3. Find the top-level instruction whose program ID is 6wXTwJwdtA5RhNu6VyYnBzrV51TbsMpTbo5kZv2zdssG and whose first eight data bytes are the initialize_pool discriminator [95, 180, 10, 172, 84, 174, 232, 40].
  4. Read base_mint and quote_mint from that instruction’s account list (positions 1 and 4), and the pool from position 2.
  5. Index base_mint as 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.

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.

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 Pool discriminator [241, 154, 109, 4, 17, 177, 109, 188].
  • Its decoded base_mint, quote_mint, and pool address agree with the verified initialize_pool transaction.

After migration, identify the Meteora market from Candle’s migration state and transaction rather than selecting an arbitrary external pool for the same mint.

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:

StatePool stateTerminal behavior
Trading on the curveis_migrated = false, quote_reserve < migration_thresholdQuote and execute against the Candle bonding-curve program
Graduated, migration pendingis_migrated = false, quote_reserve >= migration_thresholdStop curve trading and wait for the migration transaction
Migratedis_migrated = trueDiscover 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.

Curve trades are the two swap instructions, and each emits one SwapExecuted event carrying the full fill:

FieldMeaning
poolThe curve the trade executed against
userThe trader
amount_in, amount_outFilled amounts, in base units of the respective mints
feeFee taken, in the input mint’s base units
is_buytrue for quote to base, false for base to quote
quote_reserve, base_reservePost-trade reserves, for price and progress
migration_thresholdGraduation 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.

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.

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, decoded

The 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.

KindNameDiscriminator
Instructioninitialize_pool[95, 180, 10, 172, 84, 174, 232, 40]
Instructionswap_quote_to_base (buy)[58, 30, 190, 32, 188, 101, 164, 184]
Instructionswap_base_to_quote (sell)[19, 60, 200, 100, 34, 177, 72, 204]
Instructionwithdraw_for_migration[229, 30, 82, 104, 62, 251, 75, 69]
Instructioncollect_fees[164, 152, 207, 99, 30, 186, 19, 182]
Instructionupdate_nft_gating[195, 235, 21, 213, 78, 122, 248, 77]
AccountPool[241, 154, 109, 4, 17, 177, 109, 188]
EventPoolInitialized[100, 118, 173, 87, 12, 198, 254, 229]
EventSwapExecuted[150, 166, 26, 225, 28, 89, 38, 79]
EventMigrationWithdrawn[113, 119, 224, 21, 172, 69, 136, 249]
EventPoolMigrated[250, 204, 24, 195, 37, 253, 152, 6]
EventFeesCollected[233, 23, 117, 225, 107, 178, 254, 8]
EventNftGatingUpdated[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.

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:

OffsetSizeFieldType
08discriminator[241, 154, 109, 4, 17, 177, 109, 188]
832authoritypubkey
4032base_mintpubkey
7232quote_mintpubkey
10432base_vaultpubkey
13632quote_vaultpubkey
1688quote_reserveu64
1768base_reserveu64
1848migration_thresholdu64
1921is_migratedbool
19333nft_gating{ nft_program_id: pubkey, enabled: bool }
2261bumpu8
2272fee_bpsu16
2298accumulated_feesu64
23732fee_recipientpubkey
26916sqrt_priceu128
28516sqrt_start_priceu128
30116seg0_target_sqrt_priceu128
31716seg0_liquidityu128
33316seg1_target_sqrt_priceu128
34916seg1_liquidityu128

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.

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.