> For the complete documentation index, see [llms.txt](https://docs.triton.one/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.triton.one/solana/quickstart/sending-transactions/yellowstone-jet-tpu-client.md).

# Yellowstone Jet TPU client

Send Solana transactions straight to validator TPUs over QUIC with the standalone Yellowstone Jet TPU client in Rust.

The Yellowstone Jet TPU client is a standalone Rust crate that sends transactions directly to validator TPUs over QUIC. It is the QUIC sending core of [Yellowstone Jet](https://github.com/rpcpool/yellowstone-jet), the open-source engine that delivers transactions sent through Triton endpoints, pulled out into a library you can drop into your own stack.

Writing a TPU client from scratch means handling every Agave release, managing QUIC connections, tracking the leader schedule, and caching connections without lock contention. This crate does all of that, and exposes the hooks a default client leaves out.

* Crate: [`yellowstone-jet-tpu-client`](https://crates.io/crates/yellowstone-jet-tpu-client)
* API docs: [docs.rs (0.8.1)](https://docs.rs/yellowstone-jet-tpu-client/0.8.1/yellowstone_jet_tpu_client/)
* Source: [github.com/rpcpool/yellowstone-jet](https://github.com/rpcpool/yellowstone-jet)

## When to use it

Anza's `tpu-client-next` shares much of its design and solves the performance problems of the legacy `ConnectionCache`. Reach for the Jet TPU client instead when you run arbitrage bots, liquidation engines, or custom routing, and need control the default client does not expose:

* **Transaction tracking.** Register a callback that fires when a transaction frame is written, fails, or is dropped by the internal event loop. Use it for custom retry logic, logging, and debugging.
* **Custom TPU contact info.** Bypass the gossip-provided contact info and supply your own TPU addresses.
* **Arbitrary remote peers.** Bypass the leader schedule entirely to implement your own offloading or routing logic.
* **Multi-step identity updates.** Rotate the sending identity through a controlled, multi-step process while keeping every sub-module in sync.
* **Allowlist and blocklist support.** Pass a custom blocklist when you call `send_txn_with_blocklist`. To gate sending on on-chain validator policies, see [Protect transactions from MEV with Shield](/solana/quickstart/sending-transactions/protect-transactions-with-shield.md).

## Prerequisites

* Rust 1.97.1 or later. The crate itself needs Rust 1.91 (edition 2024); the 4.3 Solana client crates it builds on need 1.97.1.
* A Triton RPC endpoint and a Yellowstone gRPC endpoint with an x-token. [Get an endpoint](https://customers.triton.one/onboarding).
* An identity keypair for the sender. Validators prioritise QUIC connections by the stake of this identity, so an unstaked identity gets no stake-weighted QoS. The example below falls back to a random keypair if you do not supply one. It also uses this keypair as the fee payer and the source of the transfer, so it must hold SOL. With the random fallback the transaction is sent but cannot land.

## Set up the project

Create a new binary crate and add the dependencies. Version 0.8 of the client is built on the 4.x `solana-client`, `solana-message`, `solana-pubkey`, and `solana-transaction` crates and the 3.x `solana-keypair`, `solana-signature`, and `solana-signer` crates. Pin the same major versions so the types match. The examples target 0.8.x. Cargo treats each 0.x minor version as a breaking release, so keep the version pinned in `Cargo.toml` and don't run `cargo add` without a version.

```toml
[package]
name = "example-jet-tpu-client"
version = "0.1.0"
edition = "2024"

[dependencies]
bincode = "1.3.3"
clap = { version = "4.5.51", features = ["derive"] }
dotenvy = "0.15.0"
tokio = { version = "1.48.0", features = ["macros", "rt-multi-thread", "sync"] }
url = "2"

# Solana crates, at the major versions yellowstone-jet-tpu-client 0.8 is built against
# solana-client 4.4 pulls in solana-account 5, which yellowstone-shield-store 0.11 rejects;
# stay on 4.3 so the Shield guide builds on this project
solana-client = "~4.3"
solana-commitment-config = "3"
solana-hash = "4"
solana-keypair = "3"
solana-message = "4"
solana-pubkey = "4"
solana-signature = "3"
solana-signer = "3"
solana-system-interface = { version = "3", features = ["bincode"] }
solana-transaction = { version = "4", features = ["serde", "wincode"] }

yellowstone-jet-tpu-client = { version = "0.8.1", features = ["yellowstone-grpc"] }
```

## Imports and CLI arguments

The example is a small CLI that sends lamports to any Solana address, reading its endpoints from arguments or a `.env` file.

In `src/main.rs`, start with the imports:

{% tabs %}
{% tab title="Rust" %}

```rust
use {
    clap::Parser,
    solana_client::nonblocking::rpc_client::RpcClient,
    solana_commitment_config::CommitmentConfig,
    solana_hash::Hash,
    solana_keypair::Keypair,
    solana_message::{VersionedMessage, v0},
    solana_pubkey::Pubkey,
    solana_signature::Signature,
    solana_signer::Signer,
    solana_system_interface::instruction::transfer,
    solana_transaction::versioned::VersionedTransaction,
    std::{env, path::PathBuf},
    url::Url,
    yellowstone_jet_tpu_client::{
        config::{TpuPortKind, TpuSenderConfig},
        core::{TpuSenderResponse, TpuSenderTxnInfo},
        identity::TpuIdentity,
        yellowstone_grpc::sender::{
            Endpoints, NewYellowstoneTpuSender, YellowstoneTpuSender, YellowstoneTpuSenderConfig,
            create_yellowstone_tpu_sender_with_callback,
        },
    },
};
```

{% endtab %}
{% endtabs %}

Define the CLI arguments:

{% tabs %}
{% tab title="Rust" %}

```rust
#[derive(clap::Parser, Debug)]
struct Args {
    /// Path to .env file to load
    #[clap(long, short)]
    dotenv: Option<PathBuf>,

    /// Endpoint to the RPC service
    #[clap(long, short)]
    rpc: Option<String>,

    /// Endpoint to the Yellowstone gRPC service
    #[clap(long, short)]
    grpc: Option<String>,

    /// X-Token for the Yellowstone gRPC service
    #[clap(long)]
    x_token: Option<String>,

    /// Path to the identity keypair file
    #[clap(long)]
    identity: Option<PathBuf>,

    /// Recipient pubkey
    #[clap(long)]
    recipient: Option<String>,
}
```

{% endtab %}
{% endtabs %}

## Write the send function

Build the transfer, sign it, serialize it to wire format with bincode, and hand it to the sender. Attach the signature as `TpuSenderTxnInfo` metadata: the callback returns it with each outcome, so you can match outcomes to transactions. `send_txn` sends to the current leader, and also to the next leader once the current leader's 4-slot window is half over:

{% tabs %}
{% tab title="Rust" %}

```rust
async fn send_lamports(
    tpu_sender: &mut YellowstoneTpuSender,
    identity: &Keypair,
    recipient: &Pubkey,
    lamports: u64,
    latest_blockhash: Hash,
) -> Signature {
    let instructions = vec![transfer(&identity.pubkey(), &recipient, lamports)];

    let transaction = VersionedTransaction::try_new(
        VersionedMessage::V0(
            v0::Message::try_compile(
                &identity.pubkey(),
                &instructions,
                &[],
                latest_blockhash,
            )
            .expect("try_compile"),
        ),
        &[&identity],
    )
    .expect("try_new");

    let signature = transaction.signatures[0];

    // The TPU sender expects the transaction in its wire format (bincode).
    let wire_txn: Vec<u8> = bincode::serialize(&transaction).expect("bincode::serialize");

    // Attach the signature as metadata: the callback hands it back with each outcome.
    let txn_info = TpuSenderTxnInfo::new(signature);

    // Sends to the current leader, and also to the next leader once the
    // current leader's 4-slot window is half over.
    tpu_sender
        .send_txn(wire_txn, Some(txn_info))
        .await
        .expect("send_txn");

    signature
}
```

{% endtab %}
{% endtabs %}

## Wire up main

Parse the arguments, load the optional `.env` file, resolve the endpoints and identity, then build the sender. The endpoints go in the sender's config, the identity goes in as a `TpuIdentity` built from your keypair, and passing a callback channel to `create_yellowstone_tpu_sender_with_callback` lets you track each transaction's outcome:

{% tabs %}
{% tab title="Rust" %}

```rust
#[tokio::main]
async fn main() {
    use std::io::Write;

    let mut out = std::io::stdout();
    let args = Args::parse();

    // Supports a .env file with RPC_ENDPOINT, GRPC_ENDPOINT, GRPC_X_TOKEN, and IDENTITY.
    if let Ok(env_path) = args.dotenv.unwrap_or("./.env".into()).canonicalize() {
        if dotenvy::from_path(env_path).is_err() {
            eprintln!("Warning: failed to load .env file");
        }
    } else {
        eprintln!("Warning: failed to canonicalize .env file path");
    }

    let recipient_pubkey: Pubkey = match args.recipient {
        Some(recipient) => recipient.parse().expect("Failed to parse recipient pubkey"),
        None => Pubkey::new_unique(),
    };

    let rpc_endpoint = match args.rpc {
        Some(endpoint) => endpoint,
        None => env::var("RPC_ENDPOINT")
            .expect("RPC_ENDPOINT must be set in the .env file or environment"),
    };

    let grpc_endpoint = match args.grpc {
        Some(endpoint) => endpoint,
        None => env::var("GRPC_ENDPOINT")
            .expect("GRPC_ENDPOINT must be set in the .env file or environment"),
    };

    let grpc_x_token = match args.x_token {
        Some(x_token) => Some(x_token),
        None => match env::var("GRPC_X_TOKEN") {
            Ok(token) => Some(token),
            Err(_) => {
                eprintln!("Warning: GRPC_X_TOKEN not set in the .env file or environment");
                None
            }
        },
    };

    let identity = match args.identity {
        Some(path) => solana_keypair::read_keypair_file(path)
            .expect("Failed to read identity keypair file"),
        None => {
            if let Ok(identity_path) = env::var("IDENTITY") {
                solana_keypair::read_keypair_file(identity_path)
                    .expect("Failed to read identity keypair file from env")
            } else {
                eprintln!("IDENTITY not set, using a new random identity");
                Keypair::new()
            }
        }
    };

    let rpc_client =
        RpcClient::new_with_commitment(rpc_endpoint.clone(), CommitmentConfig::confirmed());

    writeln!(out, "Using identity: {}", identity.pubkey()).expect("writeln");

    // The sender connects to the RPC and gRPC endpoints set in its config.
    let config = YellowstoneTpuSenderConfig {
        endpoints: Endpoints {
            rpc: Url::parse(&rpc_endpoint).expect("Failed to parse RPC endpoint URL"),
            grpc: Url::parse(&grpc_endpoint).expect("Failed to parse gRPC endpoint URL"),
            grpc_x_token,
        },
        // Send to each leader's normal TPU port. Agave 4.3 can drop transactions sent
        // to the forwards port, the crate's default.
        tpu: TpuSenderConfig {
            tpu_port: TpuPortKind::Normal,
            ..Default::default()
        },
        ..Default::default()
    };

    // Pass a callback so you can track whether each transaction was sent, failed, or dropped.
    let (callback_tx, mut callback_rx) = tokio::sync::mpsc::unbounded_channel();

    // Keep `sender` alive: dropping the last sender shuts down the TPU driver and
    // discards transactions still waiting for a connection.
    let NewYellowstoneTpuSender {
        mut sender,
        related_objects_jh: _,
    } = create_yellowstone_tpu_sender_with_callback(
        config,
        TpuIdentity::from_keypair(&identity),
        callback_tx,
    )
    .await
    .expect("tpu-sender");

    const LAMPORTS: u64 = 1000;

    let latest_blockhash = rpc_client
        .get_latest_blockhash()
        .await
        .expect("get_latest_blockhash");

    let signature = send_lamports(
        &mut sender,
        &identity,
        &recipient_pubkey,
        LAMPORTS,
        latest_blockhash,
    )
    .await;

    // Read the signature back from the metadata attached to the response.
    let signature_of = |info: Option<TpuSenderTxnInfo>| {
        info.as_ref()
            .and_then(|info| info.downcast_ref::<Signature>())
            .copied()
    };

    match callback_rx
        .recv()
        .await
        .expect("receive tpu sender response")
    {
        TpuSenderResponse::TxSent(sent) => {
            assert_eq!(signature_of(sent.info), Some(signature), "unexpected tx signature");
            writeln!(
                out,
                "sent transaction with signature `{}` to validator `{}`",
                signature, sent.remote_peer_identity
            )
            .expect("writeln");
        }
        TpuSenderResponse::TxFailed(failed) => {
            eprintln!(
                "sending to validator `{}` failed: {}",
                failed.remote_peer_identity, failed.failure_reason
            );
        }
        TpuSenderResponse::TxDrop(dropped) => {
            eprintln!(
                "transaction dropped for validator `{}`: {}",
                dropped.remote_peer_identity, dropped.drop_reason
            );
        }
    }
}
```

{% endtab %}
{% endtabs %}

## Run it

Pass the endpoints as arguments, or put them in a `.env` file:

{% code title=".env" %}

```bash
RPC_ENDPOINT=https://<your-endpoint>.mainnet.rpcpool.com/<your-token>
GRPC_ENDPOINT=https://<your-endpoint>.mainnet.rpcpool.com:443
GRPC_X_TOKEN=<your-token>
IDENTITY=/path/to/identity.json
```

{% endcode %}

Then run:

```bash
cargo run -- --recipient <recipient-pubkey>
```

The callback reports one of three outcomes: `TxSent` when the transaction was written to a validator's QUIC stream (with that validator's identity and the metadata you attached), `TxFailed` when writing to the validator's connection still failed after the client's own send retries (3 attempts by default, set by `max_send_attempt`), or `TxDrop` when the transaction was dropped before it was sent (for example, the validator stayed unreachable after the client's connection retries, or its queue was full). `TxSent` means the transaction was written to the connection, not that it landed, so confirm it with `getSignatureStatuses`.

## What's next

This client gives you the primitives to build your own routing logic without reinventing QUIC handling or leader tracking. To send only to validators you trust and skip known bad actors, combine it with on-chain Shield policies: [Protect transactions from MEV with Shield](/solana/quickstart/sending-transactions/protect-transactions-with-shield.md).

***

<i class="fa-life-ring">:life-ring:</i> Contact support by clicking the chat icon in your [customer dashboard](https://customers.triton.one)\ <i class="fa-briefcase">:briefcase:</i> Sales questions? [Contact us](https://triton.one/contact)\ <i class="fa-sparkles">:sparkles:</i> AI agent? Read [llms.txt](https://docs.triton.one/llms.txt)\ <i class="fa-rss">:rss:</i> Follow updates: [Blog](https://blog.triton.one) · [X](https://x.com/triton_one) · [YouTube](https://www.youtube.com/@triton_one_ltd) · [Telegram](https://t.me/tritonone) · [GitHub](https://github.com/rpcpool)
