# Generate a decoder (Carbon 2.0)

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

When Carbon has no [published decoder](https://carbon.sevenlabs.org/decoders.md) 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.

### Step 1: Create a project

```sh
cargo new my-indexer
cd my-indexer
```

### Step 2: 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.

**npx**

```sh
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
```

**pnpm**

```sh
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
```

**bun**

```sh
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
```

**yarn**

```sh
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:

```text
✔ 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
```

> **Warning: parse deletes the output directory.** The CLI deletes `--out-dir` and recreates it on every run. Point it at a directory that holds only generated code, never `.`, or pass `--no-clean`.

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`):

```text
decoders/pump-amm/
├── Cargo.toml
└── src/
    ├── lib.rs           # PumpAmmDecoder, 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
```

### Step 3: Check that it compiles

```sh
cargo check --manifest-path decoders/pump-amm/Cargo.toml
```

Output:

```text
    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
```

### Step 4: 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](#generate-into-a-cargo-workspace).

`Cargo.toml`:

```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"] }
```

### Step 5: 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`:

```rust
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(())
    }
}
```

### Step 6: Run it

```sh
cargo run
```

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

Output:

```text
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`](https://carbon.sevenlabs.org/guides/load-account-state.md).

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`:

```json
{
  "address": "pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA",
  "metadata": {
    "name": "pump_amm",
    "version": "0.1.0",
    "spec": "0.1.0",
    "description": "Created with Anchor"
  },
  "instructions": [
    …
  ]
}
```

**npx**

```sh
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
```

**pnpm**

```sh
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
```

**bun**

```sh
bunx @sevenlabs-hq/carbon-cli@2.0.0 parse --idl ./pump_amm.json --out-dir decoders/pump-amm --with-postgres false --with-graphql false
```

**yarn**

```sh
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:

```text
✨ 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**

```sh
npx -y @sevenlabs-hq/carbon-cli@2.0.0 parse --idl ./codama.json --standard codama --program-id pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA --out-dir decoders/pump-amm
```

**pnpm**

```sh
pnpm dlx @sevenlabs-hq/carbon-cli@2.0.0 parse --idl ./codama.json --standard codama --program-id pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA --out-dir decoders/pump-amm
```

**bun**

```sh
bunx @sevenlabs-hq/carbon-cli@2.0.0 parse --idl ./codama.json --standard codama --program-id pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA --out-dir decoders/pump-amm
```

**yarn**

```sh
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 `eventNode`s (`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 `eventNode`s produces a decoder without events.

> **Note: --event-hints has no effect in 2.0.0.** The CLI accepts `--event-hints "BuyEvent,SellEvent"` with `-s codama` (and rejects it with `anchor`), but CLI 2.0.0 doesn't pass the hints to the generator: the output is identical with or without them. Declare events as `eventNode`s in the IDL instead.

## Options

| Flag                                | What 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-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 <v>`                | Appends `-<v>` to the crate name, for [versioned decoders](https://carbon.sevenlabs.org/guides/versioned-decoders.md).                                                                |
| `--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](https://carbon.sevenlabs.org/reference/cli.md#parse).

### Feature flags

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

`Cargo.toml`:

```toml
[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](https://carbon.sevenlabs.org/guides/postgres.md); turns on `serde` and `carbon-core/postgres` | `--with-postgres true` (default)          |
| `graphql`  | GraphQL schema types for [GraphQL](https://carbon.sevenlabs.org/guides/graphql.md); turns on `serde` and `carbon-core/graphql`                    | `--with-graphql true` (default)           |
| `base58`   | Base58 strings for `Pubkey` fields when serialized; turns on `serde`                                                                              | `--with-base58 true`                      |

See [Feature flags](https://carbon.sevenlabs.org/reference/feature-flags.md) 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`:

```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`:

```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`):

**npx**

```sh
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
```

**pnpm**

```sh
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
```

**bun**

```sh
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
```

**yarn**

```sh
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
```

```text
pump-indexer/
├── Cargo.toml        # workspace: indexer, decoder
├── .env              # RPC_URL=your-rpc-url-here
├── decoder/          # carbon-pump-amm-decoder
└── indexer/
    └── src/main.rs   # pipeline with a stub processor
```

Set the variables in `.env`, then `cargo run` in `pump-indexer/`. The [CLI reference](https://carbon.sevenlabs.org/reference/cli.md#scaffold) 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](#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

- [Decoders](https://carbon.sevenlabs.org/concepts/decoders.md): How generated decoders match discriminators and arrange accounts.
- [Stream with Yellowstone gRPC](https://carbon.sevenlabs.org/guides/stream-with-yellowstone.md): Feed the decoder real-time transactions.
- [Multiple programs](https://carbon.sevenlabs.org/guides/multiple-programs.md): Run your decoder next to others in one pipeline.
- [CLI reference](https://carbon.sevenlabs.org/reference/cli.md): Every parse and scaffold flag.

---

Carbon 2.0.0 documentation · Web: https://carbon.sevenlabs.org/guides/generate-a-decoder · Last updated 2026-11-09 · Index of all pages: https://carbon.sevenlabs.org/llms.txt
