# Index token transfers (Carbon 2.0)

> Decode SPL Token and Token-2022 transfers of one mint with the token decoders, and turn raw amounts into token amounts.

Every token movement on Solana is an instruction of the SPL Token program or of Token-2022, often nested inside a swap or another program's call. This guide indexes the transfers of one mint from both programs with the published decoders, and prints each one with its token amount.

**You need:** Rust 1.96.1 or newer. The example uses Solana's public RPC, so no account is needed.

## Which instructions move tokens

| Instruction                       | In                    | Has the mint?        | Notes                                                                                                                                                                        |
| --------------------------------- | --------------------- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TransferChecked`                 | SPL Token, Token-2022 | Yes, plus `decimals` | What wallets and most programs send today. Use it to attribute a transfer to a mint.                                                                                         |
| `Transfer`                        | SPL Token, Token-2022 | No                   | Legacy. The mint is only known from the token accounts; see [Plain `Transfer` has no mint](#plain-transfer-has-no-mint).                                                     |
| `TransferCheckedWithFee`          | Token-2022            | Yes                  | For mints with a transfer fee. Its fields decode wrongly in 2.0.0; see the [Token-2022 decoder](https://carbon.sevenlabs.org/decoders/token-2022.md#transfercheckedwithfee). |
| `MintTo`, `Burn` (and `*Checked`) | Both                  | Yes                  | Supply changes rather than transfers; handle them the same way if you need balances.                                                                                         |

A mint belongs to one program: classic tokens such as USDC and JUP use SPL Token, newer ones may use Token-2022. Register both decoders and your indexer works for any mint.

### Step 1: Create a project

```sh
cargo new token-transfers
cd token-transfers
cargo add carbon-core@2.0.0 carbon-rpc-transaction-crawler-datasource@2.0.0 carbon-token-program-decoder@2.0.0 carbon-token-2022-decoder@2.0.0 carbon-log-metrics@2.0.0 solana-pubkey@4.2.1
cargo add tokio --features full
cargo add env_logger
```

### Step 2: Write the indexer

The [RPC transaction crawler](https://carbon.sevenlabs.org/datasources/rpc-transaction-crawler.md) fetches every transaction that references the mint's address. Both decoders feed the same processor, which keeps `TransferChecked` instructions for the mint and converts the raw `amount` with the instruction's own `decimals`.

`src/main.rs`:

```rust
use {
    carbon_core::{
        error::CarbonResult, instruction::InstructionProcessorInputType, pipeline::Pipeline,
        processor::Processor,
    },
    carbon_log_metrics::LogMetrics,
    carbon_rpc_transaction_crawler_datasource::{ConnectionConfig, Filters, RpcTransactionCrawler},
    carbon_token_2022_decoder::{instructions::Token2022Instruction, Token2022Decoder},
    carbon_token_program_decoder::{instructions::TokenProgramInstruction, TokenProgramDecoder},
    solana_pubkey::{pubkey, Pubkey},
    std::sync::Arc,
};

/// The mint whose transfers we index: JUP.
const MINT: Pubkey = pubkey!("JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN");

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

    let rpc_url = std::env::var("RPC_URL")
        .unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string());

    // Every transaction that references the mint, newest first, then new ones as they land.
    let datasource = RpcTransactionCrawler::new(
        rpc_url,
        MINT,
        ConnectionConfig::default(),
        Filters::new(None, None, None),
        None,
    );

    Pipeline::builder()
        .datasource(datasource)
        .metrics(Arc::new(LogMetrics::new()))
        // A mint belongs to one token program; register both so the code works for any mint.
        .instruction(TokenProgramDecoder, TransferLogger)
        .instruction(Token2022Decoder, TransferLogger)
        .build()?
        .run()
        .await
}

struct TransferLogger;

fn print_transfer(amount: u64, decimals: u8, mint: Pubkey, from: Pubkey, to: Pubkey) {
    if mint != MINT {
        return; // a transfer of another token in the same transaction
    }
    let ui_amount = amount as f64 / 10f64.powi(decimals as i32);
    println!("{ui_amount:>14.6} from {from} to {to}");
}

impl Processor<InstructionProcessorInputType<'_, TokenProgramInstruction>> for TransferLogger {
    async fn process(
        &mut self,
        input: &InstructionProcessorInputType<'_, TokenProgramInstruction>,
    ) -> CarbonResult<()> {
        if let TokenProgramInstruction::TransferChecked { data, accounts, .. } = input.decoded_instruction {
            print_transfer(data.amount, data.decimals, accounts.mint, accounts.source, accounts.destination);
        }
        Ok(())
    }
}

impl Processor<InstructionProcessorInputType<'_, Token2022Instruction>> for TransferLogger {
    async fn process(
        &mut self,
        input: &InstructionProcessorInputType<'_, Token2022Instruction>,
    ) -> CarbonResult<()> {
        if let Token2022Instruction::TransferChecked { data, accounts, .. } = input.decoded_instruction {
            print_transfer(data.amount, data.decimals, accounts.mint, accounts.source, accounts.destination);
        }
        Ok(())
    }
}
```

### Step 3: Run it

```sh
cargo run
```

Output:

```text
     15.817487 from CcXcW9Ad7DpvcWjv7iDz6RUUdmBSP8dXZS9EbQktQzXq to DqHAep7hD2VJ4rsWSxkLoBSRMiLkdw7y8DLx1htrryts
     15.817487 from DqHAep7hD2VJ4rsWSxkLoBSRMiLkdw7y8DLx1htrryts to 4gPSQHK96TttxoK3AmAVKk6mJvcDDNhAiRmbZh1sDkyA
     13.616322 from CcXcW9Ad7DpvcWjv7iDz6RUUdmBSP8dXZS9EbQktQzXq to CugEgUFKcEE8CayShReXpkQjfYVJXEGqasidZr99ViiJ
     13.616322 from CugEgUFKcEE8CayShReXpkQjfYVJXEGqasidZr99ViiJ to 4gPSQHK96TttxoK3AmAVKk6mJvcDDNhAiRmbZh1sDkyA
    210.276046 from CcXcW9Ad7DpvcWjv7iDz6RUUdmBSP8dXZS9EbQktQzXq to 47cA3idUSGNbJ6tuKEx8UwPLmPkPZRvBkVjeqTbtDYkZ
…
```

Transfers often come in pairs or chains: a swap routed through an aggregator moves the tokens from the user to an intermediate account and on to the pool. Each hop is its own instruction, nested inside the aggregator's call, and Carbon decodes [nested instructions](https://carbon.sevenlabs.org/concepts/instructions.md) like top-level ones. Use `input.metadata.stack_height` and `input.metadata.absolute_path` to tell a direct transfer from one inside another program's call.

## Index a different mint

Change `MINT`. For a Token-2022 mint nothing else changes. For very busy addresses, such as the USDC mint, Solana's public endpoint can return months-old signatures; use your own RPC endpoint with `RPC_URL`, or stream instead of crawling (below).

## Stream transfers in real time

The crawler polls. For production, subscribe to transactions that include the mint with a Geyser stream, using the same processor:

- [Yellowstone gRPC](https://carbon.sevenlabs.org/guides/stream-with-yellowstone.md): a transaction filter with `account_include: vec![MINT.to_string()]`.
- [Helius LaserStream](https://carbon.sevenlabs.org/guides/stream-with-laserstream.md): the same filter.

## Plain `Transfer` has no mint

`Transfer` lists only the source account, the destination and the authority. To attribute one to a mint, look up the source token account's mint: from the transaction's token balances (`input.metadata.transaction_metadata.meta.pre_token_balances`, which give each token account's mint by account index), or from your own table of token accounts filled by an [account processor](https://carbon.sevenlabs.org/concepts/processors.md). `TransferChecked` avoids the lookup, which is why most programs send it.

## Common problems

### Nothing prints for a while

The public endpoint is rate limited, and the crawler skips failed transactions. Busy mints print within a few seconds; quiet mints may have no transfers in the last minutes. Set `RPC_URL` to your own endpoint for steady throughput.

### The same transfer prints twice

If you also run a second datasource, deduplicate with [`DeduplicationFilter`](https://carbon.sevenlabs.org/concepts/filters.md#deduplicationfilter). Two lines with different accounts are two hops of a routed swap, not duplicates.

## Next steps

- [SPL Token decoder](https://carbon.sevenlabs.org/decoders/token-program.md): Every instruction and account of the token program.
- [Store transfers in Postgres](https://carbon.sevenlabs.org/guides/postgres.md): Typed tables and idempotent writes.
- [Index multiple programs](https://carbon.sevenlabs.org/guides/multiple-programs.md): Several decoders in one pipeline.

---

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