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
- Create a project
- Generate the decoder crate
- Check that it compiles
- Add it to the project
- Use the decoder in a pipeline
- Run it
- Verify the decoder
- From an Anchor IDL file
- From a Codama IDL
- Options
- Feature flags
- Generate into a Cargo workspace
- Scaffold a whole project
- Common problems
- Error: RPC URL is required when using a program address
- Error: No Anchor IDL found for program address.
- Error: Failed to fetch Anchor IDL: … 429 or … 413 … You have used your data allowance
- Error: Program ID is required. IDL missing or invalid address field
- Error: Event hints can only be used with Codama standard
- Failed to format code warning
- error: multiple workspace roots found in the same workspace
- Instructions you expect never reach the processor
- Next steps
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,bunoryarn. - Rust 1.96.1 or newer, with
rustfmt(rustup component add rustfmt). The CLI runscargo fmton 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#
cargo new my-indexer
cd my-indexerGenerate 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.
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 falsepnpm dlx @sevenlabs-hq/carbon-cli@2.0.0 parse --idl pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA --url mainnet-beta --out-dir decoders/pump-amm --with-postgres false --with-graphql falsebunx @sevenlabs-hq/carbon-cli@2.0.0 parse --idl pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA --url mainnet-beta --out-dir decoders/pump-amm --with-postgres false --with-graphql falseyarn dlx @sevenlabs-hq/carbon-cli@2.0.0 parse --idl pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA --url mainnet-beta --out-dir decoders/pump-amm --with-postgres false --with-graphql falseAfter a banner, the CLI prints:
✔ 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.comThe crate is named carbon-<, where <name> is the IDL's program name in kebab case (pump_amm gives carbon-pump-amm-decoder):
- 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#
cargo check --manifest-path decoders/pump-amm/Cargo.toml 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.56sAdd 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.
[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 .. 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.
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#
cargo runThe first build takes one to three minutes. Then decoded trades print, newest transactions first:
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 . 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/ on anchor build; for other programs, use the IDL their authors publish. The CLI reads the program ID from the IDL's address field:
{
"address": "pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA",
"metadata": {
"name": "pump_amm",
"version": "0.1.0",
"spec": "0.1.0",
"description": "Created with Anchor"
},
"instructions": [
…
]
}npx -y @sevenlabs-hq/carbon-cli@2.0.0 parse --idl ./pump_amm.json --out-dir decoders/pump-amm --with-postgres false --with-graphql falsepnpm dlx @sevenlabs-hq/carbon-cli@2.0.0 parse --idl ./pump_amm.json --out-dir decoders/pump-amm --with-postgres false --with-graphql falsebunx @sevenlabs-hq/carbon-cli@2.0.0 parse --idl ./pump_amm.json --out-dir decoders/pump-amm --with-postgres false --with-graphql falseyarn dlx @sevenlabs-hq/carbon-cli@2.0.0 parse --idl ./pump_amm.json --out-dir decoders/pump-amm --with-postgres false --with-graphql false✨ 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:
npx -y @sevenlabs-hq/carbon-cli@2.0.0 parse --idl ./codama.json --standard codama --program-id pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA --out-dir decoders/pump-ammpnpm dlx @sevenlabs-hq/carbon-cli@2.0.0 parse --idl ./codama.json --standard codama --program-id pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA --out-dir decoders/pump-ammbunx @sevenlabs-hq/carbon-cli@2.0.0 parse --idl ./codama.json --standard codama --program-id pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA --out-dir decoders/pump-ammyarn dlx @sevenlabs-hq/carbon-cli@2.0.0 parse --idl ./codama.json --standard codama --program-id pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA --out-dir decoders/pump-ammEvents in a Codama IDL come from its eventNodes (program.). 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 < sets the CPI envelope; it defaults to Anchor's e445a52e51cb9a1d. An IDL without eventNodes produces a decoder without events.
Options#
| Flag | What it changes |
|---|---|
--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-graphql | true by default. Each adds generated modules and a Cargo feature of the same name. |
--with-serde | Adds the serde feature (derives Serialize and Deserialize on every type). On by default when both flags above are false. |
--with-base58 | Adds a base58 feature that serializes Pubkey fields as base58 strings. |
--standalone false | Removes the [workspace] table and writes every dependency as { workspace = true }, for a crate that lives in a Cargo workspace. See below. |
--version-name < | Appends -<v> to the crate name, for versioned decoders. |
--no-clean | Writes over --out-dir instead of deleting it first. |
-c, --as-crate | Accepted, 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:
[dependencies]
carbon-pump-amm-decoder = { path = "decoders/pump-amm", features = ["serde"] }| Feature | Enables | Exists when generated with |
|---|---|---|
serde | Serialize and Deserialize on instructions, accounts, events and types | --with-serde true, or any of the others |
postgres | Row types, migrations and processors for Postgres; turns on serde and carbon-core/ | --with-postgres true (default) |
graphql | GraphQL schema types for GraphQL; turns on serde and carbon-core/ | --with-graphql true (default) |
base58 | Base58 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:
[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_, 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:
[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):
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 falsepnpm 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 falsebunx @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 falseyarn 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- 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 <.
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.