# Decoders (Carbon 2.0)

> How AccountDecoder and InstructionDecoder turn raw bytes into Rust types, how discriminators and ArrangeAccounts work, and how decoder crates are laid out.

A decoder recognizes the accounts or instructions of one program and turns their bytes into Rust types. Every account and instruction pipe has one. This page covers the two decoder traits, how decoding works, how a generated decoder crate is organized and where decoders come from.

## The decoder traits

```rust
pub trait AccountDecoder<'a> {
    type AccountType;

    fn decode_account(
        &self,
        account: &'a solana_account::Account,
    ) -> Option<DecodedAccount<Self::AccountType>>;
}

pub trait InstructionDecoder<'a> {
    type InstructionType;

    fn decode_instruction(
        &self,
        instruction: &'a solana_instruction::Instruction,
    ) -> Option<Self::InstructionType>;
}
```

Both return `None` for data that isn't theirs. The pipe then skips the update and the processor isn't called, so a pipe with a Meteora decoder ignores every non-Meteora instruction in a transaction. A decoder that returns `Some` for everything sees every update of its kind.

`DecodedAccount<T>` carries the decoded data plus the account header:

```rust
pub struct DecodedAccount<T> {
    pub lamports: u64,
    pub data: T,
    pub owner: Pubkey,
    pub executable: bool,
    pub rent_epoch: u64,
}
```

The `instruction` passed to `decode_instruction` is a `solana_instruction::Instruction` with the program ID, the account metas (pubkey, signer, writable) and the data. Carbon builds it from the transaction and resolves address lookup tables first, so account keys are complete. Pass a decoder to the pipeline by value: `.instruction(MeteoraDlmmDecoder, processor)` or `.account(MeteoraDlmmDecoder, processor)`.

## How decoding works

### Discriminators

Programs put a discriminator at the start of their data to say what it is:

- **Anchor programs** use 8 bytes: the first 8 bytes of the SHA-256 of `global:<instruction_name>` for instructions, `account:<AccountName>` for accounts and `event:<EventName>` for events. Meteora DLMM's `swap` instruction starts with `[248, 198, 158, 145, 225, 117, 135, 200]`.
- **Native and SPL programs** use their own layouts. SPL Token instructions start with a one-byte tag (`Transfer` is `3`), and token accounts have no discriminator: the decoder recognizes them by size.

A generated decoder gives each account, instruction and event type a `decode(data: &[u8]) -> Option<Self>` method that checks the discriminator and deserializes the rest with Borsh, ignoring trailing bytes. The program's `decode_instruction` first checks `instruction.program_id == PROGRAM_ID`, then tries each instruction type in turn; `decode_account` first checks `account.owner == PROGRAM_ID`, then tries each account type. The first match wins.

### `CarbonDeserialize`

`carbon_core::deserialize::CarbonDeserialize` is the trait form of the same idea, for types you write yourself:

```rust
pub trait CarbonDeserialize
where
    Self: Sized + borsh::BorshDeserialize,
{
    const DISCRIMINATOR: &'static [u8];

    fn deserialize(data: &[u8]) -> Option<Self>;
}
```

[`decode_log_events`](https://carbon.sevenlabs.org/concepts/instructions.md#log-events) needs it to decode events from transaction logs. Generated decoders in 2.0.0 don't implement it; they use the inherent `decode` methods above. The same module provides `extract_discriminator(length, data)`, which splits data into `(discriminator, rest)`.

### `ArrangeAccounts`

An instruction's accounts arrive as a list in the order the program expects them. `ArrangeAccounts` maps that list to a struct with named fields:

```rust
pub trait ArrangeAccounts {
    type ArrangedAccounts: Clone + Send + Sync + std::fmt::Debug;

    fn arrange_accounts(accounts: &[solana_instruction::AccountMeta]) -> Option<Self::ArrangedAccounts>;
}
```

Generated instruction types implement it. Optional accounts in the IDL become `Option<Pubkey>`, and any accounts beyond the IDL's list land in a `remaining: Vec<AccountMeta>` field. If the instruction has fewer accounts than it requires, `arrange_accounts` returns `None` and the instruction isn't decoded.

## Generated decoder crates

Every published decoder, and every decoder the CLI generates, has the same layout. For Meteora DLMM (`carbon-meteora-dlmm-decoder`):

```text
src/
├── lib.rs           MeteoraDlmmDecoder, PROGRAM_ID
├── accounts/        one module per account type, MeteoraDlmmAccount, AccountDecoder impl
├── instructions/    one module per instruction, MeteoraDlmmInstruction, InstructionDecoder impl
├── events/          one module per event type, each with decode()
├── types/           structs and enums the IDL defines and the others use
└── graphql/         behind the graphql feature
```

| Item                                           | What it is                                                                                                                                                                                                        |
| ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `MeteoraDlmmDecoder`                           | A unit struct that implements both `AccountDecoder` and `InstructionDecoder`.                                                                                                                                     |
| `PROGRAM_ID`                                   | The program's address, a `const Pubkey`. Use it to configure datasources.                                                                                                                                         |
| `accounts::MeteoraDlmmAccount`                 | One variant per account type, such as `LbPair(Box<LbPair>)`.                                                                                                                                                      |
| `instructions::MeteoraDlmmInstruction`         | One struct variant per instruction: `Swap { program_id, data, accounts }`, where `data` holds the arguments (`Swap { amount_in, min_amount_out }`) and `accounts` the named accounts (`SwapInstructionAccounts`). |
| `events::swap::SwapEvent`                      | An event payload, with its own `decode`.                                                                                                                                                                          |
| `accounts::postgres`, `instructions::postgres` | Row types and migrations, behind the `postgres` feature.                                                                                                                                                          |

The account and instruction enums derive `Debug`, `Clone` and `PartialEq`, plus `Serialize` and `Deserialize` with the `serde` feature. Decoders generated by CLI 2.0.0 for programs with Anchor events also export `EVENT_CPI_DISCRIMINATOR`; the published 2.0.0 crates predate that and don't. The [decoder catalog](https://carbon.sevenlabs.org/decoders.md) documents each published crate.

## Events and CPI events

Anchor programs publish events in one of two ways, and Carbon decodes each differently:

- **Event CPI** (`emit_cpi!`): the program calls itself with an instruction whose data is the 8-byte event-CPI tag `[228, 69, 165, 46, 81, 203, 154, 29]`, the event's discriminator and the event. A generated decoder decodes this inner instruction as a `CpiEvent` variant: `MeteoraDlmmInstruction::CpiEvent { data: CpiEvent::Swap(SwapEvent { .. }), .. }`. It reaches your instruction processor like any other instruction, and also appears among the parent instruction's nested instructions.
- **Log events** (`emit!`): the program writes a `Program data: <base64>` log line. Carbon doesn't turn these into instructions; decode them from the instruction's metadata with `decode_log_events`.

[Instructions](https://carbon.sevenlabs.org/concepts/instructions.md#event-cpi-instructions) shows both in a processor.

## Where decoders come from

- **Published crates.** Carbon 2.0 publishes 63 decoder crates, `carbon-<program>-decoder`, for SPL Token, Token-2022, System, Jupiter, Raydium, Orca, Meteora, Pump.fun, Kamino and more. See the [catalog](https://carbon.sevenlabs.org/decoders.md).
- **Generated with the CLI.** `carbon-cli parse` turns an Anchor or Codama IDL, or an on-chain IDL fetched by program address, into a decoder crate with this layout. See [Generate a decoder](https://carbon.sevenlabs.org/guides/generate-a-decoder.md).
- **Written by hand.** For programs without an IDL, or when you only need one or two types, implement the traits yourself. See [Write a custom decoder](https://carbon.sevenlabs.org/guides/custom-decoder.md) and the second example below.

## Examples

### Decode an instruction with a generated decoder

This program builds a Meteora DLMM `swap` instruction by hand and decodes it. In a pipeline, the instruction comes from a transaction instead.

```sh
cargo add carbon-core@2.0.0 carbon-meteora-dlmm-decoder@2.0.0
cargo add solana-instruction@3.4.1 solana-pubkey@4.2.1
```

`src/main.rs`:

```rust
use {
    carbon_core::instruction::InstructionDecoder,
    carbon_meteora_dlmm_decoder::{
        instructions::MeteoraDlmmInstruction, MeteoraDlmmDecoder, PROGRAM_ID,
    },
    solana_instruction::{AccountMeta, Instruction},
    solana_pubkey::Pubkey,
};

fn main() {
    // Anchor discriminator of `swap`, then the Borsh-encoded arguments.
    let mut data = vec![248, 198, 158, 145, 225, 117, 135, 200];
    data.extend_from_slice(&1_000_000u64.to_le_bytes()); // amount_in
    data.extend_from_slice(&990_000u64.to_le_bytes()); // min_amount_out

    let accounts = (0..15)
        .map(|_| AccountMeta::new(Pubkey::new_unique(), false))
        .collect();
    let instruction = Instruction { program_id: PROGRAM_ID, accounts, data };

    match MeteoraDlmmDecoder.decode_instruction(&instruction) {
        Some(MeteoraDlmmInstruction::Swap { data, accounts, .. }) => println!(
            "swap: amount_in={} min_amount_out={} lb_pair={}",
            data.amount_in, data.min_amount_out, accounts.lb_pair
        ),
        Some(_) => println!("another DLMM instruction"),
        None => println!("not a DLMM instruction"),
    }
}
```

Output:

```text
swap: amount_in=1000000 min_amount_out=990000 lb_pair=11157t3sqMV725NVRLrVQbAu98Jjfk1uCKehJnXXQs
```

### Write an account decoder by hand

This decoder reads a `CounterState` account of a hypothetical Anchor program: an 8-byte discriminator followed by a Borsh-encoded authority and count.

```sh
cargo add carbon-core@2.0.0 solana-account@4.3.2 solana-pubkey@4.2.1
cargo add borsh --features derive
```

`src/main.rs`:

```rust
use {
    borsh::BorshDeserialize,
    carbon_core::{
        account::{AccountDecoder, DecodedAccount},
        deserialize::CarbonDeserialize,
    },
    solana_account::Account,
    solana_pubkey::Pubkey,
};

const PROGRAM_ID: Pubkey = Pubkey::from_str_const("Counter111111111111111111111111111111111111");

/// On-chain layout: 8-byte discriminator, then Borsh fields.
#[derive(Debug, BorshDeserialize)]
pub struct CounterState {
    pub authority: Pubkey,
    pub count: u64,
}

impl CarbonDeserialize for CounterState {
    // The first 8 bytes of sha256("account:CounterState")
    const DISCRIMINATOR: &'static [u8] = &[98, 23, 205, 159, 14, 202, 79, 139];

    fn deserialize(data: &[u8]) -> Option<Self> {
        let mut body = data.strip_prefix(Self::DISCRIMINATOR)?;
        // `deserialize` (not `try_from_slice`) tolerates trailing padding bytes.
        <Self as BorshDeserialize>::deserialize(&mut body).ok()
    }
}

pub struct CounterDecoder;

impl<'a> AccountDecoder<'a> for CounterDecoder {
    type AccountType = CounterState;

    fn decode_account(&self, account: &'a Account) -> Option<DecodedAccount<CounterState>> {
        if account.owner != PROGRAM_ID {
            return None;
        }
        Some(DecodedAccount {
            lamports: account.lamports,
            data: <CounterState as CarbonDeserialize>::deserialize(&account.data)?,
            owner: account.owner,
            executable: account.executable,
            rent_epoch: account.rent_epoch,
        })
    }
}

fn main() {
    let mut data = CounterState::DISCRIMINATOR.to_vec();
    data.extend_from_slice(&[7; 32]); // authority
    data.extend_from_slice(&42u64.to_le_bytes()); // count
    data.extend_from_slice(&[0; 16]); // padding

    let account = Account {
        lamports: 1_224_960,
        data,
        owner: PROGRAM_ID,
        executable: false,
        rent_epoch: u64::MAX,
    };

    let decoded = CounterDecoder.decode_account(&account).expect("a CounterState account");
    println!("{:?}", decoded.data);
}
```

Output:

```text
CounterState { authority: US517G5965aydkZ46HS38QLi7UQiSojurfbQfKCELFx, count: 42 }
```

`CounterDecoder` works in a pipeline like a generated one: `.account(CounterDecoder, processor)`.

## Next steps

- [Generate a decoder](https://carbon.sevenlabs.org/guides/generate-a-decoder.md): Create a decoder crate from your program's IDL.
- [Processors](https://carbon.sevenlabs.org/concepts/processors.md): Receive decoded accounts and instructions in your code.
- [Instructions](https://carbon.sevenlabs.org/concepts/instructions.md): Nested instructions, CPIs and events.

---

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