1. Get started

Quickstart

Build an indexer that decodes live Meteora DLMM swaps from Solana mainnet in about five minutes. No account or API key needed.

On this page

You'll build a small Rust program that reads a Solana program's transactions, decodes each instruction into a typed Rust struct, and prints swaps as they happen. It uses Solana's public RPC endpoint, so you don't need an account or an API key.

You need: Rust 1.96.1 or newer, an internet connection, and about 3 GB of free disk space for the first build.

Check your Rust version#

Terminal
rustc --version
Output
rustc 1.99.0 (b940084d7 2026-09-28)

Any version from 1.96.1 up works. If yours is older, run rustup update stable. If you don't have Rust, install it from rustup.rs.

Create a project#

Terminal
cargo new carbon-quickstart
cd carbon-quickstart

Add Carbon#

Add the core crate, a datasource, a decoder for the program you want to read, and a metrics exporter. All Carbon crates in one project must have the same version.

Terminal
cargo add carbon-core@2.0.0 carbon-rpc-transaction-crawler-datasource@2.0.0 carbon-meteora-dlmm-decoder@2.0.0 carbon-log-metrics@2.0.0
cargo add tokio --features full
cargo add env_logger log

Your [dependencies] now look like this (patch versions of tokio, env_logger and log may be newer):

Cargo.toml
[dependencies]
carbon-core = "2.0.0"
carbon-log-metrics = "2.0.0"
carbon-meteora-dlmm-decoder = "2.0.0"
carbon-rpc-transaction-crawler-datasource = "2.0.0"
env_logger = "0.11.11"
log = "0.4.35"
tokio = { version = "1.54.1", features = ["full"] }

Write the pipeline#

Replace src/main.rs with this program. It connects three parts: a datasource that fetches the program's transactions, a decoder that turns Meteora DLMM instructions into Rust types, and a processor, SwapLogger, that prints them.

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

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

    // Solana's public endpoint works for this example. Set RPC_URL to use your own.
    let rpc_url = std::env::var("RPC_URL")
        .unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string());

    // Fetches the program's recent transactions, newest first, then keeps polling for new ones.
    let datasource = RpcTransactionCrawler::new(
        rpc_url,
        PROGRAM_ID,
        ConnectionConfig::default(),
        Filters::new(None, None, None),
        None,
    );

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

struct SwapLogger;

impl Processor<InstructionProcessorInputType<'_, MeteoraDlmmInstruction>> for SwapLogger {
    async fn process(
        &mut self,
        input: &InstructionProcessorInputType<'_, MeteoraDlmmInstruction>,
    ) -> CarbonResult<()> {
        let signature = input.metadata.transaction_metadata.signature;
        match input.decoded_instruction {
            MeteoraDlmmInstruction::Swap { data, accounts, .. } => println!(
                "swap  pool={} amount_in={} min_amount_out={}",
                accounts.lb_pair, data.amount_in, data.min_amount_out,
            ),
            MeteoraDlmmInstruction::Swap2 { data, accounts, .. } => println!(
                "swap2 pool={} amount_in={} min_amount_out={}",
                accounts.lb_pair, data.amount_in, data.min_amount_out,
            ),
            // Every other instruction is decoded too; print its name.
            other => println!("{} in {signature}", variant_name(other)),
        }
        Ok(())
    }
}

/// The variant name of a decoded instruction, e.g. "AddLiquidityByStrategy2".
fn variant_name(instruction: &MeteoraDlmmInstruction) -> String {
    format!("{instruction:?}").split([' ', '{', '(']).next().unwrap_or_default().to_string()
}

Run it#

Terminal
cargo run

The first build compiles the Solana dependencies and takes one to three minutes. Then decoded instructions start printing, newest transactions first. On Solana's public endpoint the first line can take a minute or more, because it rate-limits the crawler; see Nothing prints for a minute or more.

Output
swap2 pool=6e7V9eegCHw997T72MxgwwJipZ6GJyZF8NvjkzT1rvpN amount_in=84225603 min_amount_out=0
CpiEvent in 5oc7rbKmGEti7jEfvCt77cWWTG8gUvQn5breYsMwpA396gn3FJAozKjUvKE6Nqma3SHZbKg4kXSi8G9pzSfTL8cA
CpiEvent in 5oc7rbKmGEti7jEfvCt77cWWTG8gUvQn5breYsMwpA396gn3FJAozKjUvKE6Nqma3SHZbKg4kXSi8G9pzSfTL8cA
swap  pool=GMgh4NtWrGaUf1RR2kcXD7LY1jou1qFAuSsQeKp5ow4a amount_in=14968688 min_amount_out=0
swap2 pool=4ju8phDxUU4WSJP1wWPLZ8R2KRCqwP3VZFXAqhT9cfVy amount_in=56765035 min_amount_out=1
…

CpiEvent lines are the events Meteora emits through self-CPI after each swap; Carbon decodes them as instructions too. Press Ctrl+C to stop.

What you built#

  • The datasource, RpcTransactionCrawler, asks the RPC node for the program's signatures (getSignaturesForAddress), fetches each transaction (getTransaction), skips failed ones, and sends the rest into the pipeline. It keeps polling every five seconds for new transactions.
  • The decoder, MeteoraDlmmDecoder from carbon-meteora-dlmm-decoder, matches each instruction's discriminator and returns a MeteoraDlmmInstruction with typed data and named accounts. It decodes instructions nested inside other programs' calls too, such as a Jupiter route that swaps through a DLMM pool.
  • The processor, SwapLogger, implements Processor for the decoder's output. Carbon calls process once per decoded instruction, with the instruction's metadata (signature, slot, position in the transaction).
  • The pipeline wires them together and runs until you stop it. See how a pipeline runs.

See the pipeline's metrics#

LogMetrics writes throughput and queue depth to the log every five seconds. Turn the log on with RUST_LOG:

Terminal
RUST_LOG=info cargo run

Every five seconds you get a summary line followed by each metric's value. An excerpt:

Output
[2026-11-09T00:02:47Z INFO  carbon_log_metrics] 00:00:50 (+5.006587213s) | 7 processed (100%), 7 successful, 0 failed (0%), 0 in queue
[2026-11-09T00:02:47Z INFO  carbon_log_metrics]   carbon_updates_received_total: 7
[2026-11-09T00:02:47Z INFO  carbon_log_metrics]   transaction_crawler_signatures_fetched_total: 1000
[2026-11-09T00:02:47Z INFO  carbon_log_metrics]   transaction_crawler_transactions_fetched_total: 7
…

The metrics reference lists every metric.

Use your own RPC endpoint#

The public endpoint is rate limited, so the example fetches only a few transactions per second. Any Solana RPC provider's URL works in its place:

Terminal
RPC_URL="https://your-rpc-provider.example/?api-key=…" cargo run

For production, stream with Geyser gRPC instead of polling: see Stream with Yellowstone gRPC. The processor doesn't change.

Troubleshooting#

rustc 1.95.0 is not supported by the following packages#

Output
error: rustc 1.95.0 is not supported by the following packages:
  carbon-core@2.0.0 requires rustc 1.96.1

Your Rust toolchain is older than Carbon 2.0 needs. Run rustup update stable, then cargo run again.

Nothing prints for a minute or more#

The public RPC endpoint limits how many requests you can make, and the crawler waits and retries when it hits the limit. It also skips failed transactions, so a burst of failed bot transactions prints nothing. To see whether it's working, run with metrics:

Terminal
RUST_LOG=info cargo run

If transaction_crawler_signatures_fetched_total grows while transaction_crawler_transactions_fetched_total stays low, the crawler has found transactions and is being rate limited while it fetches them. Output follows; use your own endpoint with RPC_URL for steady throughput.

Failed to fetch transaction … after 3 retries in the log#

The RPC node kept rejecting requests, usually with HTTP 429 (Too Many Requests). The crawler logs the error, skips that transaction and continues. Lower the request rate with a custom ConnectionConfig, or use your own endpoint.

the trait InstructionDecoder is not implemented for a decoder#

A Carbon 1 crate is mixed with Carbon 2, so two versions of carbon-core are compiled; the compiler adds the note there are multiple different versions of crate carbon_core. Make every carbon-* dependency 2.0.0, then run cargo update. See Installation.

More problems and fixes are in Troubleshooting.

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.