Introduction

Running a Solana workload means operating in an extreme throughput environment. Every epoch generates gigabytes of account updates and transactions, and your systems are expected to keep up with that in real time.

Triton's Yellowstone gRPC (Dragon's Mouth) gives you an ultra-low latency, strongly typed stream that solves the speed problem (real-time data), the cost problem (no overhead from repeatedly asking for unchanged data), and the transport problem (protobuf messages are extremely light).

But receiving the data is only the first step. Parsing is the engineering challenge that turns raw bytes into data you can stream, records you can query, and alerts you can trigger.

This guide walks you through 3 approaches to parsing so you can pick the one that fits your workload based on latency, engineering effort, and resilience.

The table below is a quick overview of each, so you can skim before diving in:

Feature Vixen (Triton One) Carbon (Seven Labs) Manual parsing
Parsing model Pipeline framework (source → router → parser → handler) Vertical pipeline (source → decoder → processor) Manual loop (ingest → slice → route parse → handle)
Primary USP Middleware: gRPC server, Prometheus, cost-efficiency Speed: CLI generation for rapid indexing Control: zero abstraction overhead
Resilience High: native Fumarole (beta) support, auto-reconnect High: auto-reconnect Manual: you must code backoff/retry
Testing Vixen Mock (fixtures & replay) Live only Manual: custom implementation required
Output DB, Log, or gRPC stream Processor, defined by trait Manual, whatever you code
Integration Medium: implement handler traits Low: generated boilerplate High: manual wiring

Yellowstone Vixen parsing engine

Vixen is a modular pipeline framework for Solana, designed to ingest events, transform them into structured data, and route them efficiently. As systems scale, maintaining a monolithic parser to handle multiple programs becomes error-prone, and Vixen solves this by providing a clean, composable alternative.

Its primary architectural advantage is operational simplicity. Vixen is lightweight and dependency-free, designed to run on your own infrastructure (Docker/Kubernetes). It allows you to share a single stream among multiple pipelines, drastically reducing bandwidth costs compared to running separate connections for every program you index.

Key features

  • Easily transform raw Solana account/instruction data into structured models with logic hooks.
  • Out of the box integration with Dragon's Mouth gRPC and Fumarole (beta) reliable streams
  • Re-stream parsed data to downstream clients via a custom gRPC interface
  • Strict boundary between data decoding and storage logic (handlers)
  • Integration with the Codama toolchain for parser generation
  • Support for replayable inputs to facilitate offline testing and local debugging without node access
  • Native support for scraping lag, throughput, and error rates via a standardised /metrics endpoint
  • Built-in support for a wide and growing set of Solana programs, including Jupiter, Pump.fun, Boop.fun, Kamino, Raydium, Meteora, Whirlpools, and Virtuals

Implementation walkthrough

This walkthrough mirrors the filtered-pipeline example in the upstream repo. A working Cargo.toml is available there.

1. Implement a handler that describes what you want to do with each parsed event. A handler is any type that implements vixen::Handler<V, InstructionUpdate>V is whatever structured value the parser produced.

use yellowstone_vixen::{self as vixen, vixen_core::instruction::InstructionUpdate};

struct Logger;

impl<V: std::fmt::Debug + Sync> vixen::Handler<V, InstructionUpdate> for Logger {
    async fn handle(&self, value: &V, input: &InstructionUpdate) -> vixen::HandlerResult<()> {
        println!("ix {:?} - {value:?}", input.path);
        Ok(())
    }
}

2. Wire the runtime: plug a source (Dragon's Mouth), a parser crate (we'll be using the SPL Token instruction parser), and the handler above into a FilterPipeline. The Prefilter drops irrelevant transactions server-side so your handler only sees what matters.

use std::str::FromStr;

use yellowstone_vixen::{
    filter_pipeline::FilterPipeline,
    vixen_core::{KeyBytes, Prefilter},
};
use yellowstone_vixen_spl_token_parser::InstructionParser;
use yellowstone_vixen_yellowstone_grpc_source::YellowstoneGrpcSource;

fn main() {
    rustls::crypto::aws_lc_rs::default_provider()
        .install_default()
        .expect("install rustls provider");

    let config = std::fs::read_to_string("Vixen.toml").expect("read Vixen.toml");
    let config = toml::from_str(&config).expect("parse Vixen.toml");

    vixen::Runtime::<YellowstoneGrpcSource>::builder()
        .instruction(FilterPipeline::new(
            InstructionParser,
            [Logger],
            Prefilter::builder().transaction_accounts_include([
                KeyBytes::<32>::from_str("So11111111111111111111111111111111111111112").unwrap(),
            ]),
        ))
        .build(config)
        .run();
}

3. Point Vixen at your Dragon's Mouth endpoint with a Vixen.toml alongside the binary:

[source]
endpoint = "https://your-endpoint.rpcpool.com"
x-token = "<YOUR-X-TOKEN>"
timeout = 60

Run with cargo run. Swap InstructionParser for any of Vixen's other parser crates (or your own), and add more .instruction(...) / .account(...) pipelines to the builder to route multiple program streams off the same connection.

Best-fit checklist

  • You are building a multi-protocol platform aggregating many sources
  • You have customers who need custom program-specific parsed gRPC streams
  • You want to test your parser locally or on Devnet
  • You want to optimise bandwidth and multiplex a single gRPC connection

Manual parsing of raw gRPC streams

This approach is relevant when framework overhead, however minimal, is unacceptable, or when specific requirements around memory management and thread scheduling dictate full ownership of the event loop.

Going full manual means using the yellowstone-grpc-client crate directly, and taking full responsibility for the connection lifecycle, message routing, error handling, and state management.

You must implement

  • Connection establishment, health checks, and exponential backoff strategies
  • Managing subscription updates and filters
  • Routing message types (account vs transaction)
  • Manually checking discriminators and deserialising bytes
  • Monitoring lag and handling shutdowns safely

This method offers the highest possible control and performance ceiling but comes with the highest maintenance burden. A common failure mode is silent data corruption or crashes following an upstream program update, as there is no framework layer to abstract schema changes.

Implementation walkthrough

This walkthrough distils the canonical pattern from the yellowstone-grpc/examples/rust client: build + connect, subscribe with a filter, loop on the stream, and wrap the entire flow in exponential-backoff retry.

1. Build the client and connect. GeyserGrpcClient::build_from_shared returns a builder you chain with TLS config and an x-token, then .connect().await gives you a live client.

use backoff::{future::retry, ExponentialBackoff};
use futures::stream::StreamExt;
use std::collections::HashMap;
use tonic::transport::ClientTlsConfig;
use yellowstone_grpc_client::GeyserGrpcClient;
use yellowstone_grpc_proto::prelude::{
    subscribe_update::UpdateOneof, CommitmentLevel, SubscribeRequest,
    SubscribeRequestFilterTransactions,
};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let endpoint = std::env::var("GRPC_ENDPOINT")?;
    let x_token = std::env::var("X_TOKEN").ok();

    // Exponential backoff: 500ms, 750ms, 1.1s, 1.7s, 2.5s, ... reconnects
    // survive transient disconnects without hammering the server.
    retry(ExponentialBackoff::default(), || async {
        run(&endpoint, x_token.clone())
            .await
            .map_err(backoff::Error::transient)
    })
    .await
}

2. Subscribe with a filter so the server only sends the transactions you care about — here, anything touching the Jupiter Swap program at Confirmed commitment.

async fn run(endpoint: &str, x_token: Option<String>) -> anyhow::Result<()> {
    let mut client = GeyserGrpcClient::build_from_shared(endpoint.to_owned())?
        .x_token(x_token)?
        .tls_config(ClientTlsConfig::new().with_native_roots())?
        .connect()
        .await?;

    let mut transactions = HashMap::new();
    transactions.insert(
        "jupiter".to_string(),
        SubscribeRequestFilterTransactions {
            vote: Some(false),
            failed: Some(false),
            account_include: vec![],
            account_exclude: vec![],
            account_required: vec![
                "JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4".to_string(),
            ],
            signature: None,
        },
    );

    let request = SubscribeRequest {
        transactions,
        commitment: Some(CommitmentLevel::Confirmed as i32),
        ..Default::default()
    };

    let (mut _subscribe_tx, mut stream) = client.subscribe_with_request(Some(request)).await?;

3. Loop on the stream and dispatch your own decode logic. This is the part a framework would do for you: match on the update type, pull the raw bytes, check discriminators, and route to the right deserialiser.

    while let Some(message) = stream.next().await {
        match message?.update_oneof {
            Some(UpdateOneof::Transaction(tx)) => {
                // Your parsing logic: match discriminators, borsh-decode,
                // route to per-program handlers. You own the whole loop.
                println!("tx slot={}", tx.slot);
            }
            Some(UpdateOneof::Ping(_)) => { /* keepalive */ }
            _ => {}
        }
    }
    Ok(())
}

The tradeoff: this is roughly 60 lines of plumbing before you write a single line of parsing — and every new program you route doubles the match arms and discriminator checks. No framework layer means you also inherit every protocol update as a silent correctness risk.

Best-fit checklist

  • You are building a high-frequency trading engine.
  • You require non-standard memory management.
  • You have the engineering resources to maintain low-level plumbing.

Carbon application framework

Carbon is designed for scenarios where the primary goal is to index a specific protocol and ship a working product quickly. It is well-suited for vertical applications, such as a dashboard for a single DeFi protocol.

Carbon abstracts the event loop into a pipeline pattern: Source → Decoder → Processor. It uses a CLI tool to generate the necessary decoder code from an Anchor or Codama IDL, allowing the developer to focus almost entirely on the processor logic (what to do with the data).

Key features

  • CLI-based code generation for decoders driven by IDLs
  • Pipeline builder pattern to abstract connection management
  • Processor traits for defining business logic
  • Support for exposing metrics (e.g. Prometheus) depending on configuration

With the framework dictating the architecture, Carbon offers less granular control.

Implementation walkthrough

This walkthrough mirrors Carbon's jupiter-swap-alerts example, which consumes a Triton Dragon's Mouth stream via the carbon-yellowstone-grpc-datasource and decodes Jupiter Swap instructions.

1. Build the Yellowstone gRPC datasource with your filter. The filter syntax is the same SubscribeRequestFilterTransactions as the manual walkthrough, but you hand it to Carbon's datasource instead of driving the stream yourself.

use carbon_core::{
    error::CarbonResult,
    instruction::{DecodedInstruction, InstructionMetadata, NestedInstructions},
    metrics::MetricsCollection,
    processor::Processor,
};
use carbon_jupiter_swap_decoder::{
    instructions::JupiterSwapInstruction, JupiterSwapDecoder,
    PROGRAM_ID as JUPITER_SWAP_PROGRAM_ID,
};
use carbon_log_metrics::LogMetrics;
use carbon_yellowstone_grpc_datasource::{
    YellowstoneGrpcClientConfig, YellowstoneGrpcGeyserClient,
};
use std::{
    collections::{HashMap, HashSet},
    env, sync::Arc, time::Duration,
};
use tokio::sync::RwLock;
use yellowstone_grpc_proto::geyser::{CommitmentLevel, SubscribeRequestFilterTransactions};

#[tokio::main]
async fn main() -> CarbonResult<()> {
    dotenv::dotenv().ok();
    env_logger::init();
    rustls::crypto::aws_lc_rs::default_provider()
        .install_default()
        .expect("install rustls provider");

    let mut transaction_filters = HashMap::new();
    transaction_filters.insert(
        "jupiter_swap".to_string(),
        SubscribeRequestFilterTransactions {
            vote: Some(false),
            failed: Some(false),
            account_include: vec![],
            account_exclude: vec![],
            account_required: vec![JUPITER_SWAP_PROGRAM_ID.to_string()],
            signature: None,
        },
    );

    let geyser_config = YellowstoneGrpcClientConfig::new(
        None,
        Some(Duration::from_secs(15)),
        Some(Duration::from_secs(15)),
        None, None, None,
    );

    let yellowstone_grpc = YellowstoneGrpcGeyserClient::new(
        env::var("GEYSER_URL").unwrap_or_default(),
        env::var("X_TOKEN").ok(),
        Some(CommitmentLevel::Confirmed),
        HashMap::default(),
        transaction_filters,
        Default::default(),
        Arc::new(RwLock::new(HashSet::new())),
        geyser_config,
        None,
        None,
    );

2. Wire the pipeline with the datasource, a generated decoder, and your processor. Carbon handles the stream loop and reconnects; you only define what happens to each decoded instruction.

    carbon_core::pipeline::Pipeline::builder()
        .datasource(yellowstone_grpc)
        .metrics(Arc::new(LogMetrics::new()))
        .metrics_flush_interval(3)
        .instruction(JupiterSwapDecoder, JupiterSwapInstructionProcessor)
        .shutdown_strategy(carbon_core::pipeline::ShutdownStrategy::Immediate)
        .build()?
        .run()
        .await?;
    Ok(())
}

3. Implement the processor — the one piece of application code you actually own. Everything above (stream, reconnect, deserialisation, filter plumbing) is framework-provided.

use async_trait::async_trait;

pub struct JupiterSwapInstructionProcessor;

#[async_trait]
impl Processor for JupiterSwapInstructionProcessor {
    type InputType = (
        InstructionMetadata,
        DecodedInstruction<JupiterSwapInstruction>,
        NestedInstructions,
        solana_instruction::Instruction,
    );

    async fn process(
        &mut self,
        (metadata, instruction, _nested, _raw): Self::InputType,
        _metrics: Arc<MetricsCollection>,
    ) -> CarbonResult<()> {
        let sig = metadata.transaction_metadata.signature;
        if let JupiterSwapInstruction::Route(route) = instruction.data {
            log::info!("Jupiter route sig={sig} route={route:?}");
        }
        Ok(())
    }
}

To index a different program, generate a decoder from its IDL with Carbon's CLI (carbon-cli parse --idl my_program.json --out-dir ./src/decoders) and swap the (JupiterSwapDecoder, ...) line for your new decoder — the rest of the pipeline stays the same.

Best-fit checklist

  • You need to index a specific protocol or program
  • Speed to production is the primary metric
  • You prefer a monolithic all-in-one framework
  • You want to avoid writing manual deserialisation code

What to watch in production

Regardless of the chosen architecture, there are specific operational signals that indicate the health of your parsing infrastructure. Here's a checklist of what to look at when evaluating final implementation:

  • Lag: The time difference or slot difference between the chain tip and your processed state.
  • Reconnect frequency: Frequent drops may indicate network instability or unhandled backpressure.
  • Decode errors: A spike in "unknown discriminator" logs usually means the on-chain program has updated.
  • Channel saturation: If internal queues are full, your consumer is too slow for the ingestion rate.
  • Schema version mismatch: Ensure your pinned IDL version matches the on-chain deployment.

Other Triton products worth knowing

Parsing is one piece of a larger stack. Depending on your workload, these are the other Triton products worth pairing with it:

Streaming

  • Dragon's Mouth — the foundation this entire guide is built on: Triton's low-latency Yellowstone gRPC stream.
  • Fumarole — persistent, replayable Dragon's Mouth stream with multi-hour retention and consumer groups for horizontal scaling. Use when you cannot tolerate missing events during consumer restarts or deploys.
  • Deshred transactions (beta) — transactions streamed after shred reconstruction but before validator execution, with single-digit-millisecond median latency. Use for transaction-intent workloads that need the earliest possible signal.

Indexes

  • Steamboat — custom on-chain indexes for dedicated nodes that accelerate getProgramAccounts from seconds to milliseconds.

History

  • Hydrant — Triton's historical data service for Solana. A Solana-compatible JSON-RPC, backed by ClickHouse, so you can query old blocks and transactions without running your own archive. Successor to Old Faithful for history workloads.

Transactions

  • Yellowstone Jet — standalone Rust TPU client for sending transactions directly to leaders with SWQoS support. Pairs naturally with parsers that trigger on-chain actions.
  • Yellowstone Shield — on-chain validator allow/blocklist policies, enforced per leader at send time.

Final words

The future of blockchain infrastructure must be open. Whether you use the raw client or a framework like Vixen, you are building on the same battle-tested, open-source Yellowstone foundation that powers the entire ecosystem.

At Triton, we build these tools and sponsor independent innovations with 3 clear goals:

  • Solve hard engineering problems so you don't have to.
  • Push the ecosystem forward by giving builders reliable primitives to build upon.
  • Inspire the industry to follow our footprint in open-sourcing critical infrastructure.

We celebrate every team, like Seven Labs (Carbon), that chooses to open source their software, and we invite the next generation of builders to join us in keeping this foundation open.

FAQs

How to parse data in TypeScript?

Use the TypeScript WASM-based Solana transaction parser; it's a lightweight JavaScript utility for client-side deserialisation of general raw binary transactions into formats like JSON Parsed (Learn more).

How do I add support for a new Solana program?

With Vixen, you write or depend on a parser crate that implements Vixen's parser trait and reference it in your Runtime::builder() alongside the existing ones — each parser becomes another pipeline on the shared connection.

With Carbon, you run carbon-cli parse --idl my_program.json --out-dir ./src/decoders to generate a decoder from the program's Anchor or Codama IDL, then wire it into Pipeline::builder() as an additional .instruction(MyDecoder, MyProcessor) call.

With manual parsing, you hand-write the discriminator matches and borsh deserialisation yourself.

What happens when a program upgrades its IDL on-chain?

Your parser keeps decoding the old layout until you regenerate it against the new IDL. The failure mode is almost never a crash — it is silent decode drift or a spike in "unknown discriminator" errors. This is why the production checklist above flags decode errors and schema version mismatch as first-class signals.

Can I consume Dragon's Mouth from Python, TypeScript, or Go instead of Rust?

Yes. The yellowstone-grpc repo ships client examples for Rust, Python, TypeScript, and Go under examples/. Vixen and Carbon are Rust-first, so non-Rust consumers typically pair a direct gRPC client with their own decode logic, or generate language bindings from the proto definitions.

Can I replay past events for offline testing?

Yes, with Vixen. The yellowstone-vixen-mock crate lets you load real Solana devnet accounts and transactions as JSON fixtures and replay them through your parsers without connecting to a live node — useful for unit tests and local debugging.

Carbon doesn't ship a dedicated replay or fixture crate, though its StreamMessageDatasource gives you a channel you can feed recorded updates into.

With the manual approach, you build your own recorder and replayer or run against Devnet.