# Index swaps across DEXs (Carbon 2.0)

> Turn Raydium CPMM, PumpSwap and Meteora DLMM swaps into one record with the amounts that executed, read from each program's events, in one pipeline.

Every DEX describes a swap in its own types, but each one also records the amounts that executed in an event. This guide indexes swaps from Raydium CPMM, PumpSwap and Meteora DLMM into one `Swap` record in a single pipeline, using each program's events. The same pattern extends to any program in the [DEX and AMM decoders](https://carbon.sevenlabs.org/decoders/dex-and-amm.md).

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

## Where each DEX records a swap

| DEX                                                                   | Event                                                                                                                                                  | How it is emitted       | Pool      | Amount in                                        | Amount out                                         |
| --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------- | --------- | ------------------------------------------------ | -------------------------------------------------- |
| [Raydium CPMM](https://carbon.sevenlabs.org/decoders/raydium-cpmm.md) | [`SwapEvent`](https://carbon.sevenlabs.org/decoders/raydium-cpmm.md#swapevent)                                                                         | `emit!`, a log line     | `pool_id` | `input_amount`                                   | `output_amount`                                    |
| [PumpSwap](https://carbon.sevenlabs.org/decoders/pump-swap.md)        | [`BuyEvent`](https://carbon.sevenlabs.org/decoders/pump-swap.md#buyevent), [`SellEvent`](https://carbon.sevenlabs.org/decoders/pump-swap.md#sellevent) | `emit_cpi!`, a self-CPI | `pool`    | `quote_amount_in` (buy), `base_amount_in` (sell) | `base_amount_out` (buy), `quote_amount_out` (sell) |
| [Meteora DLMM](https://carbon.sevenlabs.org/decoders/meteora-dlmm.md) | [`Swap`](https://carbon.sevenlabs.org/decoders/meteora-dlmm.md#swap-1)                                                                                 | `emit_cpi!`, a self-CPI | `lb_pair` | `amount_in`                                      | `amount_out`                                       |

Read events rather than instruction arguments. The arguments are limits the trader set: Raydium CPMM's `SwapBaseInput` carries `amount_in` and `minimum_amount_out`, and `SwapBaseOutput` carries `max_amount_in` and `amount_out`. The events hold what actually moved.

Anchor programs emit events in one of two ways, and you read them differently:

- **`emit_cpi!`** makes the program call itself with the event as data. The decoder returns it as the `CpiEvent` instruction, so it reaches your processor like any other instruction. PumpSwap and Meteora DLMM do this.
- **`emit!`** writes the event to the transaction's log as a `Program data:` line. Nothing calls your processor for it: decode it from the swap instruction's logs with [`decode_log_events`](https://carbon.sevenlabs.org/concepts/instructions.md#log-events). Raydium CPMM does this, so its `CpiEvent` never arrives.

If a program's `CpiEvent` never shows up, look for `Program data:` lines in one of its transactions: it logs its events.

### Step 1: Create a project

```sh
cargo new dex-swaps
cd dex-swaps
cargo add carbon-core@2.0.0 carbon-rpc-transaction-crawler-datasource@2.0.0 carbon-raydium-cpmm-decoder@2.0.0 carbon-pump-swap-decoder@2.0.0 carbon-meteora-dlmm-decoder@2.0.0 carbon-log-metrics@2.0.0 solana-pubkey@4.2.1
cargo add tokio --features full
cargo add env_logger borsh
```

`borsh` is for the small wrapper that lets `decode_log_events` read Raydium CPMM's logged event.

### Step 2: Write the indexer

Three [RPC transaction crawlers](https://carbon.sevenlabs.org/datasources/rpc-transaction-crawler.md), one per program, feed one pipeline. One `SwapIndexer` implements a processor for each program's instructions and turns that program's swap event into the shared `Swap` record.

`src/main.rs`:

```rust
use {
    borsh::BorshDeserialize,
    carbon_core::{
        deserialize::CarbonDeserialize,
        error::CarbonResult,
        filter::{DeduplicationFilter, Filter},
        instruction::InstructionProcessorInputType,
        pipeline::Pipeline,
        processor::Processor,
    },
    carbon_log_metrics::LogMetrics,
    carbon_meteora_dlmm_decoder::{
        MeteoraDlmmDecoder,
        instructions::{CpiEvent as DlmmEvent, MeteoraDlmmInstruction},
    },
    carbon_pump_swap_decoder::{
        PumpSwapDecoder,
        instructions::{CpiEvent as PumpSwapEvent, PumpSwapInstruction},
    },
    carbon_raydium_cpmm_decoder::{
        RaydiumCpmmDecoder, events::swap_event::SwapEventEvent,
        instructions::RaydiumCpmmInstruction,
    },
    carbon_rpc_transaction_crawler_datasource::{ConnectionConfig, Filters, RpcTransactionCrawler},
    solana_pubkey::Pubkey,
    std::{sync::Arc, time::Duration},
};

#[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());
    // One crawler per program, all feeding the same pipeline. One request in flight each
    // keeps the three of them under the public endpoint's per-IP rate limit.
    let crawler = |program: Pubkey| {
        RpcTransactionCrawler::new(
            rpc_url.clone(),
            program,
            ConnectionConfig {
                max_concurrent_requests: 1,
                ..ConnectionConfig::default()
            },
            Filters::new(None, None, None),
            None,
        )
    };

    Pipeline::builder()
        .datasource(crawler(carbon_raydium_cpmm_decoder::PROGRAM_ID))
        .datasource(crawler(carbon_pump_swap_decoder::PROGRAM_ID))
        .datasource(crawler(carbon_meteora_dlmm_decoder::PROGRAM_ID))
        .metrics(Arc::new(LogMetrics::new()))
        .instruction_with_filters(RaydiumCpmmDecoder, SwapIndexer, dedupe())
        .instruction_with_filters(PumpSwapDecoder, SwapIndexer, dedupe())
        .instruction_with_filters(MeteoraDlmmDecoder, SwapIndexer, dedupe())
        .build()?
        .run()
        .await
}

/// A transaction that calls two of the programs is fetched by both crawlers. This drops the
/// second copy of each instruction, keyed by signature and position in the transaction.
fn dedupe() -> Vec<Box<dyn Filter>> {
    vec![Box::new(DeduplicationFilter::new(Duration::from_secs(600)))]
}

/// One executed swap, the same shape for every DEX. Amounts are raw token units.
struct Swap {
    dex: &'static str,
    pool: Pubkey,
    amount_in: u64,
    amount_out: u64,
}

struct SwapIndexer;

impl SwapIndexer {
    fn record(&self, signature: impl std::fmt::Display, swap: Swap) {
        // Replace with a database write; see /guides/postgres.
        println!(
            "{:<12} pool={} in={} out={} tx={signature}",
            swap.dex, swap.pool, swap.amount_in, swap.amount_out,
        );
    }
}

/// Raydium CPMM publishes `SwapEvent` with Anchor's `emit!`, as a `Program data:` log line,
/// not as a self-CPI. Wrapping the generated type lets `decode_log_events` read it.
#[derive(BorshDeserialize)]
struct LoggedCpmmSwap(SwapEventEvent);

impl CarbonDeserialize for LoggedCpmmSwap {
    const DISCRIMINATOR: &'static [u8] = &[64, 198, 205, 232, 38, 8, 113, 226];

    fn deserialize(data: &[u8]) -> Option<Self> {
        SwapEventEvent::decode(data).map(LoggedCpmmSwap)
    }
}

impl Processor<InstructionProcessorInputType<'_, RaydiumCpmmInstruction>> for SwapIndexer {
    async fn process(
        &mut self,
        input: &InstructionProcessorInputType<'_, RaydiumCpmmInstruction>,
    ) -> CarbonResult<()> {
        if !matches!(
            input.decoded_instruction,
            RaydiumCpmmInstruction::SwapBaseInput { .. }
                | RaydiumCpmmInstruction::SwapBaseOutput { .. }
        ) {
            return Ok(());
        }
        // The swap instruction's own log lines hold its SwapEvent.
        for LoggedCpmmSwap(e) in input.metadata.decode_log_events::<LoggedCpmmSwap>() {
            let swap = Swap {
                dex: "raydium-cpmm",
                pool: e.pool_id,
                amount_in: e.input_amount,
                amount_out: e.output_amount,
            };
            self.record(input.metadata.transaction_metadata.signature, swap);
        }
        Ok(())
    }
}

impl Processor<InstructionProcessorInputType<'_, PumpSwapInstruction>> for SwapIndexer {
    async fn process(
        &mut self,
        input: &InstructionProcessorInputType<'_, PumpSwapInstruction>,
    ) -> CarbonResult<()> {
        let PumpSwapInstruction::CpiEvent { data, .. } = input.decoded_instruction else {
            return Ok(());
        };
        // A buy pays the quote token (usually SOL) for the base token; a sell is the reverse.
        let swap = match data {
            PumpSwapEvent::BuyEvent(e) => Swap {
                dex: "pump-swap",
                pool: e.pool,
                amount_in: e.quote_amount_in,
                amount_out: e.base_amount_out,
            },
            PumpSwapEvent::SellEvent(e) => Swap {
                dex: "pump-swap",
                pool: e.pool,
                amount_in: e.base_amount_in,
                amount_out: e.quote_amount_out,
            },
            _ => return Ok(()),
        };
        self.record(input.metadata.transaction_metadata.signature, swap);
        Ok(())
    }
}

impl Processor<InstructionProcessorInputType<'_, MeteoraDlmmInstruction>> for SwapIndexer {
    async fn process(
        &mut self,
        input: &InstructionProcessorInputType<'_, MeteoraDlmmInstruction>,
    ) -> CarbonResult<()> {
        let MeteoraDlmmInstruction::CpiEvent { data, .. } = input.decoded_instruction else {
            return Ok(());
        };
        // DLMM emits both `Swap` and `Swap2Evt` for each swap, with the same amounts: read one.
        let DlmmEvent::Swap(e) = data else {
            return Ok(());
        };
        let swap = Swap {
            dex: "meteora-dlmm",
            pool: e.lb_pair,
            amount_in: e.amount_in,
            amount_out: e.amount_out,
        };
        self.record(input.metadata.transaction_metadata.signature, swap);
        Ok(())
    }
}
```

### Step 3: Run it

```sh
cargo run
```

The first build takes one to three minutes. Then swaps start printing, newest first, from whichever crawler gets a response. On the public endpoint the first line can take a minute or more.

Output:

```text
raydium-cpmm pool=hzZaxBF4J6sKNhGtDt2TK1sgEDY31YUzfnkthWA5LpB in=8963567785194 out=196216404 tx=2hx82t435iDNNV6SzYCmh6Hn6MKSdpCKeM69Kxvv6zDm3Dg1Z3pCg6MscmyLmZpNQNB3iDPfZvLjxxmDRraB9GrR
pump-swap    pool=47MMNWgNHKUZFJGKqECZRCpzFKSLMq5YaFXxrieiJQv1 in=7049236 out=15289158360 tx=2Yand83ERTbY9qGjkYKUCrTJCxQnsJeJDkJX4u4LcjDQSPh8jfLBeHAPJig5j9fTYdFfvRraG11KxmfrT2yjL3q7
meteora-dlmm pool=GMgh4NtWrGaUf1RR2kcXD7LY1jou1qFAuSsQeKp5ow4a in=3404263 out=38912302 tx=5Tv7HWwmTxw2avMdHwJGapG932BuYB6ARiJsqxhBc4ejPooMaR7bGfJeNTXtt8wJYtZkwRgwgu6VLtFkCN5VN79W
pump-swap    pool=8zVwLfWw8KpbKoTGdufn6BwTg6Z12SFREHBuZM6UUrVs in=698486361 out=277606557301 tx=2rA17BYRFf4NLP3uGyuxb91bnchdW1rzZfgJLK6M2n5BaoZH1gJoaGjQuEBJ1GNYn1fNyhDQJewX4hFdmZeWZWqk
meteora-dlmm pool=8BD8x5Ms3f1unL9YiDGG4XQYxEcot1UqCkqkyTjniUNZ in=274040325 out=165191368923 tx=2rA17BYRFf4NLP3uGyuxb91bnchdW1rzZfgJLK6M2n5BaoZH1gJoaGjQuEBJ1GNYn1fNyhDQJewX4hFdmZeWZWqk
meteora-dlmm pool=6e7V9eegCHw997T72MxgwwJipZ6GJyZF8NvjkzT1rvpN in=465609675 out=721187169 tx=2rA17BYRFf4NLP3uGyuxb91bnchdW1rzZfgJLK6M2n5BaoZH1gJoaGjQuEBJ1GNYn1fNyhDQJewX4hFdmZeWZWqk
…
```

The last three lines are one transaction that routed through a PumpSwap pool and two Meteora DLMM pools. Each hop is its own swap, so a multi-hop route prints one line per pool. Amounts are raw token units. Press `Ctrl+C` to stop.

## Why each pipe has a deduplication filter

Each crawler fetches every transaction that mentions its program. A transaction that routes through two of the three DEXs is fetched by both crawlers, so without a filter its swaps print twice. [`DeduplicationFilter`](https://carbon.sevenlabs.org/reference/filters.md#deduplicationfilter) keys each instruction by transaction signature and position in the transaction, and drops the second copy.

With a single datasource that follows all three programs, such as a [Yellowstone gRPC](https://carbon.sevenlabs.org/guides/stream-with-yellowstone.md) subscription with the three program IDs in `account_include`, every transaction arrives once. The filter is then harmless, and it still protects you if you add a second datasource later.

## Meteora DLMM emits two events per swap

Recent DLMM swaps emit both `Swap` and `Swap2Evt`, with the same pool and amounts. Matching both prints every swap twice, so the indexer reads `Swap` only. `Swap2Evt` adds `amount_left` and a breakdown of the fees, including limit-order fees; read it instead if you need those.

## Add another DEX

1. Find the program on the [DEX and AMM decoders](https://carbon.sevenlabs.org/decoders/dex-and-amm.md) page, add its crate at 2.0.0, and add a crawler for its `PROGRAM_ID`.
2. Find its swap event in the decoder page's **Events** section. For example, Orca Whirlpools has [`Traded`](https://carbon.sevenlabs.org/decoders/orca-whirlpool.md#traded), with `whirlpool`, `input_amount` and `output_amount`.
3. Implement `Processor` for its instruction input on `SwapIndexer`, read the event as a `CpiEvent` or from the logs (see above), and build a `Swap`.
4. Register it with `.instruction_with_filters(Decoder, SwapIndexer, dedupe())`.

Some programs have no events at all: the [Raydium AMM v4 decoder](https://carbon.sevenlabs.org/decoders/raydium-amm-v4.md) lists none, and its `SwapBaseIn` carries only `amount_in` and `minimum_amount_out`. For those, compute the executed amounts from the transaction's token balance changes in `input.metadata.transaction_metadata.meta`.

## Turn raw amounts into token amounts

The events report raw units of each mint. To show token amounts, divide by 10 to the power of the mint's decimals. Look up each mint's decimals once and cache them: a mint's decimals never change. PumpSwap's quote token is usually wrapped SOL, which has 9 decimals.

## Common problems

### Nothing prints for a minute or more

The public endpoint rate-limits `getTransaction` per IP, and the three crawlers share that limit. This is why each crawler keeps one request in flight. Run with `RUST_LOG=info` to see the crawler counters and 429 retries, and set `RPC_URL` to your own endpoint for more than a test.

### The same swap prints twice

Either a pipe has no deduplication filter, or you matched both of DLMM's events. See [why each pipe has a deduplication filter](#why-each-pipe-has-a-deduplication-filter) and [Meteora DLMM emits two events per swap](#meteora-dlmm-emits-two-events-per-swap).

### Raydium CPMM swaps never print

You matched `RaydiumCpmmInstruction::CpiEvent`. Raydium CPMM logs its events instead, so read them from `SwapBaseInput` and `SwapBaseOutput` with `decode_log_events`, as in the example.

## Next steps

- [Track Pump.fun launches and trades](https://carbon.sevenlabs.org/guides/track-pumpfun-launches.md): Follow tokens from launch through their bonding curve, before they reach PumpSwap.
- [Store decoded data in Postgres](https://carbon.sevenlabs.org/guides/postgres.md): Write each swap to a table instead of printing it.
- [Index multiple programs](https://carbon.sevenlabs.org/guides/multiple-programs.md): Datasource filters for several programs, and one processor over a whole transaction.

---

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