1. Guides

Generate a decoder

Generate a Carbon decoder crate from an Anchor IDL, a Codama IDL or a program address with carbon-cli parse, then use it in a pipeline.

On this page

When Carbon has no published decoder for the program you want to index, generate one from the program's IDL with carbon-cli parse. You get a Rust crate with typed instructions, accounts and events that plugs into any pipeline. This guide generates a decoder for the Pump.fun AMM program and runs it against mainnet.

You need:

  • Node.js 20.18.0 or newer, to run the CLI with npx, pnpm, bun or yarn.
  • Rust 1.96.1 or newer, with rustfmt (rustup component add rustfmt). The CLI runs cargo fmt on the generated code and only warns if it's missing.
  • The program's IDL: a program address whose Anchor IDL is stored on chain (as in this guide), an Anchor IDL file, or a Codama IDL file. No account or API key is needed.

Create a project#

Terminal
cargo new my-indexer
cd my-indexer

Generate the decoder crate#

Pump.fun AMM stores its Anchor IDL on chain, so the CLI can read it from the program address. --url tells it which RPC endpoint to read from; mainnet-beta is short for Solana's public endpoint. --with-postgres false --with-graphql false leaves out the Postgres and GraphQL modules, which you don't need to decode.

Terminal
npx -y @sevenlabs-hq/carbon-cli@2.0.0 parse --idl pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA --url mainnet-beta --out-dir decoders/pump-amm --with-postgres false --with-graphql false
Terminal
pnpm dlx @sevenlabs-hq/carbon-cli@2.0.0 parse --idl pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA --url mainnet-beta --out-dir decoders/pump-amm --with-postgres false --with-graphql false
Terminal
bunx @sevenlabs-hq/carbon-cli@2.0.0 parse --idl pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA --url mainnet-beta --out-dir decoders/pump-amm --with-postgres false --with-graphql false
Terminal
yarn dlx @sevenlabs-hq/carbon-cli@2.0.0 parse --idl pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA --url mainnet-beta --out-dir decoders/pump-amm --with-postgres false --with-graphql false

After a banner, the CLI prints:

Output
✔ Decoder generated
✔ Code formatted successfully

✨ Decoder Generated Successfully! ✨
  • Output: /home/you/my-indexer/decoders/pump-amm
  • Source: program-address (anchor) @ https://api.mainnet-beta.solana.com

The crate is named carbon-<name>-decoder, where <name> is the IDL's program name in kebab case (pump_amm gives carbon-pump-amm-decoder):

Files
  • decoders/pump-amm/
    • Cargo.toml
    • src/
      • lib.rsPumpAmmDecoder, PROGRAM_ID, EVENT_CPI_DISCRIMINATOR
      • accounts/PumpAmmAccount enum, one file per account type
      • instructions/PumpAmmInstruction enum, one file per instruction, cpi_event.rs
      • events/one file per event
      • types/the IDL's defined types

Check that it compiles#

Terminal
cargo check --manifest-path decoders/pump-amm/Cargo.toml
Output
    Checking carbon-core v2.0.0
    Checking carbon-pump-amm-decoder v0.1.0 (/home/you/my-indexer/decoders/pump-amm)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 19.56s

Add it to the project#

Depend on the crate by path, next to the Carbon crates. The generated Cargo.toml has its own empty [workspace] table, so it builds as a separate package. That works in a single-package project like this one; inside a Cargo workspace, see Generate into a Cargo workspace.

Cargo.toml
[package]
name = "my-indexer"
version = "0.1.0"
edition = "2024"

[dependencies]
carbon-core = "2.0.0"
carbon-log-metrics = "2.0.0"
carbon-pump-amm-decoder = { path = "decoders/pump-amm" }
carbon-rpc-transaction-crawler-datasource = "2.0.0"
env_logger = "0.11.11"
log = "0.4.35"
tokio = { version = "1.54.1", features = ["full"] }

Use the decoder in a pipeline#

The generated PumpAmmDecoder implements InstructionDecoder (and AccountDecoder), so it goes straight into .instruction(). Each instruction is a struct variant of PumpAmmInstruction with typed data and named accounts. Events that the program emits through a self-CPI arrive as the CpiEvent variant.

src/main.rs
use {
    carbon_core::{
        error::CarbonResult, instruction::InstructionProcessorInputType, pipeline::Pipeline,
        processor::Processor,
    },
    carbon_log_metrics::LogMetrics,
    carbon_pump_amm_decoder::{
        instructions::{CpiEvent, PumpAmmInstruction},
        PumpAmmDecoder, PROGRAM_ID,
    },
    carbon_rpc_transaction_crawler_datasource::{ConnectionConfig, Filters, RpcTransactionCrawler},
    std::sync::Arc,
};

#[tokio::main]
async fn main() -> CarbonResult<()> {
    env_logger::init();

    let datasource = RpcTransactionCrawler::new(
        "https://api.mainnet-beta.solana.com".to_string(),
        PROGRAM_ID,
        ConnectionConfig::default(),
        Filters::new(None, None, None),
        None,
    );

    Pipeline::builder()
        .datasource(datasource)
        .metrics(Arc::new(LogMetrics::new()))
        .instruction(PumpAmmDecoder, PumpAmmLogger)
        .build()?
        .run()
        .await
}

struct PumpAmmLogger;

impl Processor<InstructionProcessorInputType<'_, PumpAmmInstruction>> for PumpAmmLogger {
    async fn process(
        &mut self,
        input: &InstructionProcessorInputType<'_, PumpAmmInstruction>,
    ) -> CarbonResult<()> {
        let signature = input.metadata.transaction_metadata.signature;
        match input.decoded_instruction {
            PumpAmmInstruction::Buy { data, .. } => {
                println!("buy  {signature}: base_amount_out={}", data.base_amount_out);
            }
            PumpAmmInstruction::Sell { data, .. } => {
                println!("sell {signature}: base_amount_in={}", data.base_amount_in);
            }
            PumpAmmInstruction::CpiEvent { data: CpiEvent::BuyEvent(event), .. } => {
                println!("  buy event: quote_amount_in={}", event.quote_amount_in);
            }
            PumpAmmInstruction::CpiEvent { data: CpiEvent::SellEvent(event), .. } => {
                println!("  sell event: quote_amount_out={}", event.quote_amount_out);
            }
            _ => {}
        }
        Ok(())
    }
}

Run it#

Terminal
cargo run

The first build takes one to three minutes. Then decoded trades print, newest transactions first:

Output
sell QKcP663tvCWRqXgndwxYz5TZ5Vq42QW5g9wvLGeht4LhAwd7fY9Nj2RnsX4f6q8pdzxQsLHzswbyFaTu4PasJdU: base_amount_in=7038649829
  sell event: quote_amount_out=79229627543477
buy  QKcP663tvCWRqXgndwxYz5TZ5Vq42QW5g9wvLGeht4LhAwd7fY9Nj2RnsX4f6q8pdzxQsLHzswbyFaTu4PasJdU: base_amount_out=7000317342
  buy event: quote_amount_in=78796291039434
sell 52CfxhHXHoqx62yFsNy4AeXz8TxjGn8Zo6CJKJdcieJZBBAaRXUcZ5GcEVQrWPvAT6c9tFmR53zxoTgNfCvQGRzP: base_amount_in=10549842158
  sell event: quote_amount_out=339272578
…

An event line without a buy or sell line above it comes from another instruction that emits the same event, such as BuyExactQuoteIn. Press Ctrl+C to stop.

Verify the decoder#

Run the pipeline against the program and compare a few decoded values with a block explorer: pick a signature from the output, open the transaction, and check the instruction's arguments and accounts. Accounts decode the same way: register the decoder with .account(PumpAmmDecoder, processor) and a datasource that emits account updates, such as rpc-gpa.

If an instruction never shows up, the IDL may not match the deployed program: an outdated IDL decodes old instructions and returns None for new ones, which the pipeline skips silently. Regenerate from the IDL that matches the deployed version.

From an Anchor IDL file#

Pass a path ending in .json instead of an address. Anchor writes your own program's IDL to target/idl/<program>.json on anchor build; for other programs, use the IDL their authors publish. The CLI reads the program ID from the IDL's address field:

pump_amm.json
{
  "address": "pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA",
  "metadata": {
    "name": "pump_amm",
    "version": "0.1.0",
    "spec": "0.1.0",
    "description": "Created with Anchor"
  },
  "instructions": [
    …
  ]
}
Terminal
npx -y @sevenlabs-hq/carbon-cli@2.0.0 parse --idl ./pump_amm.json --out-dir decoders/pump-amm --with-postgres false --with-graphql false
Terminal
pnpm dlx @sevenlabs-hq/carbon-cli@2.0.0 parse --idl ./pump_amm.json --out-dir decoders/pump-amm --with-postgres false --with-graphql false
Terminal
bunx @sevenlabs-hq/carbon-cli@2.0.0 parse --idl ./pump_amm.json --out-dir decoders/pump-amm --with-postgres false --with-graphql false
Terminal
yarn dlx @sevenlabs-hq/carbon-cli@2.0.0 parse --idl ./pump_amm.json --out-dir decoders/pump-amm --with-postgres false --with-graphql false
Output
✨ Decoder Generated Successfully! ✨
  • Output: /home/you/my-indexer/decoders/pump-amm
  • Source: file (anchor)

The crate is identical to the one generated from the address. Program addresses work only for Anchor programs that stored their IDL on chain with anchor idl init; --url is required with them and takes a full URL or the aliases mainnet-beta and devnet.

From a Codama IDL#

Pass -s codama for a Codama IDL (a JSON file whose root has "kind": "rootNode"). The CLI looks for the program ID only in a top-level address field, which Codama IDLs don't have, so pass it with --program-id:

Terminal
npx -y @sevenlabs-hq/carbon-cli@2.0.0 parse --idl ./codama.json --standard codama --program-id pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA --out-dir decoders/pump-amm
Terminal
pnpm dlx @sevenlabs-hq/carbon-cli@2.0.0 parse --idl ./codama.json --standard codama --program-id pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA --out-dir decoders/pump-amm
Terminal
bunx @sevenlabs-hq/carbon-cli@2.0.0 parse --idl ./codama.json --standard codama --program-id pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA --out-dir decoders/pump-amm
Terminal
yarn dlx @sevenlabs-hq/carbon-cli@2.0.0 parse --idl ./codama.json --standard codama --program-id pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA --out-dir decoders/pump-amm

Events in a Codama IDL come from its eventNodes (program.events). The generator decodes events whose discriminators are an event-CPI prefix followed by the event discriminator, or a single discriminator repeated as a hidden prefix on the event data. For the hidden-prefix form, --event-cpi-discriminator <hex> sets the CPI envelope; it defaults to Anchor's e445a52e51cb9a1d. An IDL without eventNodes produces a decoder without events.

Options#

FlagWhat it changes
--name <name>Crate and type names. --name my-amm gives the crate carbon-my-amm-decoder with MyAmmDecoder, MyAmmInstruction and MyAmmAccount. Without it, the IDL's program name is used.
--with-postgres, --with-graphqltrue by default. Each adds generated modules and a Cargo feature of the same name.
--with-serdeAdds the serde feature (derives Serialize and Deserialize on every type). On by default when both flags above are false.
--with-base58Adds a base58 feature that serializes Pubkey fields as base58 strings.
--standalone falseRemoves the [workspace] table and writes every dependency as { workspace = true }, for a crate that lives in a Cargo workspace. See below.
--version-name <v>Appends -<v> to the crate name, for versioned decoders.
--no-cleanWrites over --out-dir instead of deleting it first.
-c, --as-crateAccepted, but has no effect: the output is always a crate.

Boolean flags take a value: --with-postgres false. Every flag is in the CLI reference.

Feature flags#

The generated crate has default = [], and each feature is opt-in from the crate that depends on it:

Cargo.toml
[dependencies]
carbon-pump-amm-decoder = { path = "decoders/pump-amm", features = ["serde"] }
FeatureEnablesExists when generated with
serdeSerialize and Deserialize on instructions, accounts, events and types--with-serde true, or any of the others
postgresRow types, migrations and processors for Postgres; turns on serde and carbon-core/postgres--with-postgres true (default)
graphqlGraphQL schema types for GraphQL; turns on serde and carbon-core/graphql--with-graphql true (default)
base58Base58 strings for Pubkey fields when serialized; turns on serde--with-base58 true

See Feature flags for the Carbon crates' own features.

Generate into a Cargo workspace#

With --standalone false, the crate inherits edition, rust-version and its dependencies from the workspace. Add it to members, and define every dependency it uses in the workspace root:

Cargo.toml
[workspace]
members = ["indexer", "decoders/pump-amm"]
resolver = "2"

[workspace.package]
edition = "2021"
rust-version = "1.96.1"

[workspace.dependencies]
carbon-core = { version = "2.0.0", default-features = false }
carbon-test-utils = "2.0.0"
borsh = { version = "1.5.1", features = ["derive"] }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.145"
solana-account = "4.3.2"
solana-instruction = { version = "3.4.1", default-features = false }
solana-pubkey = "4.2.1"

A crate generated with Postgres or GraphQL also needs sqlx, sqlx_migrator, async-trait and juniper there.

To keep a standalone crate (the default) inside a workspace directory instead, add it to the workspace's exclude list, and depend on it by path from your members:

Cargo.toml
[workspace]
members = ["indexer"]
exclude = ["decoders/pump-amm"]
resolver = "2"

Scaffold a whole project#

carbon-cli scaffold generates a Cargo workspace with the decoder in decoder/ and a runnable indexer in indexer/, wired to one of five datasources (rpc-transaction-crawler, rpc-block-subscribe, rpc-program-subscribe, yellowstone-grpc, helius-laserstream):

Terminal
npx -y @sevenlabs-hq/carbon-cli@2.0.0 scaffold --name pump-indexer --out-dir . --idl pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA --idl-url mainnet-beta --idl-standard anchor --data-source rpc-transaction-crawler --with-postgres false --with-graphql false
Terminal
pnpm dlx @sevenlabs-hq/carbon-cli@2.0.0 scaffold --name pump-indexer --out-dir . --idl pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA --idl-url mainnet-beta --idl-standard anchor --data-source rpc-transaction-crawler --with-postgres false --with-graphql false
Terminal
bunx @sevenlabs-hq/carbon-cli@2.0.0 scaffold --name pump-indexer --out-dir . --idl pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA --idl-url mainnet-beta --idl-standard anchor --data-source rpc-transaction-crawler --with-postgres false --with-graphql false
Terminal
yarn dlx @sevenlabs-hq/carbon-cli@2.0.0 scaffold --name pump-indexer --out-dir . --idl pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA --idl-url mainnet-beta --idl-standard anchor --data-source rpc-transaction-crawler --with-postgres false --with-graphql false
Files
  • pump-indexer/
    • Cargo.tomlworkspace: indexer, decoder
    • .envRPC_URL=your-rpc-url-here
    • decoder/carbon-pump-amm-decoder
    • indexer/
      • src/main.rspipeline with a stub processor

Set the variables in .env, then cargo run in pump-indexer/. The CLI reference lists the flags, what each datasource template needs and the known issues.

Common problems#

Error: RPC URL is required when using a program address#

--idl is a program address and --url is missing. Add --url mainnet-beta, --url devnet or a full RPC URL.

Error: No Anchor IDL found for program address.#

The program never stored its IDL on chain with anchor idl init, or it isn't an Anchor program. Get the IDL file from the program's repository or authors and pass its path.

Error: Failed to fetch Anchor IDL: … 429 or … 413 … You have used your data allowance#

The public RPC endpoint rate-limited your IP. Wait a minute, or pass your own RPC URL with --url.

Error: Program ID is required. IDL missing or invalid address field#

The IDL has no top-level address (all Codama IDLs, and old Anchor IDLs that keep it elsewhere). Pass --program-id <address>.

Error: Event hints can only be used with Codama standard#

--event-hints was passed without -s codama. Anchor IDLs carry their events; remove the flag.

Failed to format code warning#

cargo fmt isn't installed. The crate is still generated, unformatted. Run rustup component add rustfmt and generate again.

error: multiple workspace roots found in the same workspace#

A standalone decoder crate (with its own [workspace] table) is inside a Cargo workspace's directory, either in members or as a path dependency. Add it to the workspace's exclude list, or regenerate it with --standalone false and add it to members. See Generate into a Cargo workspace.

Instructions you expect never reach the processor#

The decoder returns None for instructions whose discriminator or account count doesn't match the IDL, and the pipeline skips them. Check that the IDL matches the deployed program, and that your datasource delivers that program's transactions.

Next steps#

Your endpoints

Code samples use these instead of the placeholders, so you can copy and run them as they are. They’re stored only in this browser and never sent anywhere.