> 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/protect-transactions-with-shield.md).

# Protect transactions from MEV with Shield

Combine on-chain Yellowstone Shield policies with the Jet TPU client so your transactions only reach validators you trust.

Solana's leader schedule is deterministic, so you often know exactly which validator will process your transaction. If that leader is a bad actor, your transaction still routes to it by default, which leaves you exposed to sandwiching, frontrunning, censorship, and slow block production.

[Yellowstone Shield](/solana/sending-transactions/shield-mev-protection.md) addresses this with on-chain allowlists and blocklists for validators. Combined with the [Yellowstone Jet TPU client](/solana/quickstart/sending-transactions/yellowstone-jet-tpu-client.md), your application connects directly to validator TPUs, checks each leader against your Shield policies, and only sends to validators you allow. The result is RPC-independent transaction routing where you decide which validators are allowed to see your transactions.

* Shield program: [github.com/rpcpool/yellowstone-shield](https://github.com/rpcpool/yellowstone-shield)
* Policy store crate: [`yellowstone-shield-store`](https://crates.io/crates/yellowstone-shield-store)
* Policy explorer: [validators.app/yellowstone-shield](https://www.validators.app/yellowstone-shield)

## How validator-level MEV happens

A transaction has to reach the current block leader to be processed. When that leader misbehaves, default transaction sending gives you no say in the matter. Common behaviours worth routing around:

* **Sandwich attacks:** inserting transactions before and after yours to extract value.
* **Frontrunning:** copying your transaction and executing it first.
* **Censorship:** selectively dropping transactions.
* **Slow block production:** delaying your transactions.

Shield lets you decide, ahead of time, which validators are allowed to handle your transactions.

## How Shield works

Yellowstone Shield is an on-chain access-control program (`b1ockYL7X6sGtJzueDbxRVBEEPN4YeqoLW276R3MX8W`). You define policies, and each policy lives on-chain as a Program Derived Address (PDA), derived from the seeds `"shield"`, `"policy"`, and the policy's mint, that anyone with the address can use:

* **Allowlists** send only to trusted validators.
* **Blocklists** never send to known bad actors.

The TPU client reads these policies through a `PolicyStore`, which loads every policy over RPC when it starts and then follows changes over a gRPC subscription.

## Community policies

The community already maintains several Shield policies. Browse them at [validators.app/yellowstone-shield](https://www.validators.app/yellowstone-shield):

| Policy                                | Type  | Validators | Purpose                         |
| ------------------------------------- | ----- | ---------- | ------------------------------- |
| Top 25 Validators by Stake (TV25)     | Allow | 25         | Only send to top validators     |
| Sandwiched.me Sandwicher List (DROPS) | Deny  | 3          | Block known MEV sandwichers     |
| Jito Drop List (DROPJ)                | Deny  | 17         | Block Jito blocklist validators |
| Marinade Drop List (DROPM)            | Deny  | 73         | Block Marinade's blocklist      |
| Slow Block Producers (SLOWAF)         | Deny  | 21         | Avoid slow validators           |

Validator counts are as of October 2026; the lists change as their maintainers update them. You can use these as they are, or create your own.

## RPC Shield or the TPU client?

If you send through Triton RPC, you can apply Shield without writing any Rust by adding a policy to your `sendTransaction` call:

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "sendTransaction",
  "params": [
    "<base64_transaction>",
    {
      "encoding": "base64",
      "forwardingPolicies": ["<your_policy_pda>"]
    }
  ]
}
```

That route is the quickest to integrate, but it depends on the RPC's forwarding logic and gives you no direct control over routing. With the TPU client, you connect to validators yourself, so you decide everything. Pick based on your workload:

| Feature                    | RPC Shield (Triton RPC)     | TPU client Shield                           |
| -------------------------- | --------------------------- | ------------------------------------------- |
| Setup                      | Easy: add one parameter     | Moderate: requires Rust                     |
| RPC dependency             | Needs a Shield-enabled RPC  | None                                        |
| Validator connection       | Through the RPC             | Direct QUIC connections                     |
| Control over routing       | Limited to the RPC's logic  | Full                                        |
| Transaction prioritisation | RPC-controlled              | Yours                                       |
| Best for                   | Quick integration, web apps | High-throughput apps, MEV-sensitive routing |

The rest of this guide covers the TPU client route.

## Prerequisites

* Rust 1.97.1 or later, as for the [Yellowstone Jet TPU client](/solana/quickstart/sending-transactions/yellowstone-jet-tpu-client.md).
* A Triton RPC endpoint and a Yellowstone gRPC endpoint with an x-token. [Get an endpoint](https://customers.triton.one/onboarding).
* The [Yellowstone Jet TPU client](/solana/quickstart/sending-transactions/yellowstone-jet-tpu-client.md) set up, or follow the project setup below.

## Choose or create a policy

### Option A: use an existing policy

The quickest start is a community policy. For example, to block known MEV sandwichers with the Sandwiched.me list (DROPS):

{% code title="Policy address" %}

```rust
use solana_pubkey::Pubkey;
use std::str::FromStr;

// Sandwiched.me Sandwicher List (DROPS), a deny list of known MEV sandwichers.
let shield_policy = Pubkey::from_str("xMTozeQTEX2MR9KUon8vjg17U5Q9459RSASE3wy5eNB")
    .expect("valid pubkey");
```

{% endcode %}

Browse more at [validators.app/yellowstone-shield](https://www.validators.app/yellowstone-shield).

### Option B: create your own policy

For custom control, create a policy with the Yellowstone Shield CLI.

{% stepper %}
{% step %}

#### Build the CLI

```bash
git clone https://github.com/rpcpool/yellowstone-shield
cd yellowstone-shield
cargo build --release --bin yellowstone-shield-cli
```

The CLI uses the RPC URL and keypair from your Solana CLI config. If you pass `--rpc` or `--keypair`, it also saves those values into that config file.
{% endstep %}

{% step %}

#### Prepare policy metadata

Create a JSON file describing the policy, upload it to IPFS or Arweave, and keep the URL:

```json
{
  "name": "My MEV blocklist",
  "symbol": "MEVBLOCK",
  "description": "Blocks validators known for sandwich attacks",
  "image": "https://the-image-url.com/shield.png",
  "external_url": "https://your-website.com"
}
```

{% endstep %}

{% step %}

#### Create the policy

Save both addresses the command prints, Policy (the PDA) and Mint:

```bash
# A blocklist denies the listed validators.
./target/release/yellowstone-shield-cli policy create \
  --strategy Deny \
  --name "My MEV blocklist" \
  --symbol "MEVBLOCK" \
  --uri "https://the-metadata-url.json"

# The output includes both addresses:
#   Policy: <YOUR_POLICY_PDA>
#   Mint: <YOUR_POLICY_MINT>
```

{% endstep %}

{% step %}

#### Add validators

Put one validator pubkey per line in `validators-to-block.txt`, then add them. Lines that aren't valid addresses are skipped:

```bash
./target/release/yellowstone-shield-cli identities add \
  --mint <YOUR_POLICY_MINT> \
  --identities-path validators-to-block.txt
```

The policy is now live on-chain and ready to use.
{% endstep %}
{% endstepper %}

{% hint style="warning" %}
Use the Policy address (the PDA) wherever a policy is expected, such as `forwardingPolicies` or `shield_policy_addresses`, and the Mint only for the CLI's `--mint` flag. A Mint passed as a policy isn't found: Triton's RPC then skips every leader, so the transaction never lands even though you get a signature back, and the TPU client falls back to `default_return_value`.
{% endhint %}

## Set up the project

Create a binary crate with the TPU client's `shield` feature and the policy store. Both build on the upstream Solana crates from crates.io, so no `[patch.crates-io]` section is needed. `yellowstone-jet-tpu-client` 0.8 uses `yellowstone-grpc-client` 15.x, and `yellowstone-shield-store` 0.11 uses 13.x. Cargo builds both side by side, because the policy store does not expose its gRPC client. If you add `yellowstone-grpc-client` to your own crate, use 15.x to match the TPU client. 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.

{% code title="Cargo.toml" %}

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

[dependencies]
anyhow = "1"
bincode = "1.3.3"
tokio = { version = "1.48.0", features = ["macros", "rt-multi-thread", "sync", "time"] }
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
solana-client = "~4.3"
solana-commitment-config = "3"
solana-keypair = "3"
solana-message = "4"
solana-pubkey = "4"
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", "shield"] }
yellowstone-shield-store = "0.11.0"
```

{% endcode %}

Keep `solana-client` on 4.3.x until `yellowstone-shield-store` is built on `solana-account` 5. Version 0.11 uses `solana-account` 4, and `solana-client` 4.4 uses `solana-account` 5.

How the pieces fit together:

```
┌──────────────────────────────────────────────────────────┐
│                  Your application                        │
│  1. Create a Shield PolicyStore                          │
│  2. Create a TPU sender                                  │
│  3. Build and sign a transaction                         │
│  4. Call send_txn_with_shield_policies()                 │
└─────────────────┬────────────────────────────────────────┘
                  │
                  ▼
┌──────────────────────────────────────────────────────────┐
│           Yellowstone TPU client (Jet)                   │
│  • Tracks the current slot and the leader schedule       │
│  • Picks the leaders in range: the current leader,       │
│    plus the next one late in its 4-slot window           │
│  • Checks each leader against your policies              │
│    ├─ allowed → send the transaction over QUIC           │
│    └─ blocked → skip that leader                         │
│  • Every leader blocked → RemotePeerBlocked error        │
└──────────────────────────────────────────────────────────┘
```

## Send with Shield

This complete example sends a SOL transfer only to leaders that pass your Shield policy. Fill in your own endpoints and x-token.

{% code title="src/main.rs" lineNumbers="true" %}

```rust
use {
    solana_client::nonblocking::rpc_client::RpcClient,
    solana_commitment_config::CommitmentConfig,
    solana_keypair::Keypair,
    solana_message::{VersionedMessage, v0},
    solana_pubkey::Pubkey,
    solana_signer::Signer,
    solana_system_interface::instruction::transfer,
    solana_transaction::versioned::VersionedTransaction,
    std::{str::FromStr, time::Duration},
    url::Url,
    yellowstone_jet_tpu_client::{
        config::{TpuPortKind, TpuSenderConfig},
        core::{TpuSenderResponse, TpuSenderTxnInfo},
        identity::TpuIdentity,
        yellowstone_grpc::sender::{
            Endpoints, NewYellowstoneTpuSender, SendErrorKind, ShieldBlockList,
            YellowstoneTpuSenderConfig, create_yellowstone_tpu_sender_with_callback,
        },
    },
    yellowstone_shield_store::{
        PolicyStore, PolicyStoreConfig, PolicyStoreGrpcConfig, PolicyStoreRpcConfig,
    },
};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let rpc_endpoint = "<your-rpc-endpoint>".to_string();
    let grpc_endpoint = "<your-grpc-endpoint>".to_string();
    let grpc_x_token = Some("<your-token>".to_string());

    // In production, read this from a file.
    let identity = Keypair::new();
    println!("Using identity: {}", identity.pubkey());

    let recipient = Pubkey::new_unique();

    // Step 1: set up the Shield policy store.
    let config = PolicyStoreConfig {
        rpc: PolicyStoreRpcConfig {
            endpoint: rpc_endpoint.clone(),
        },
        grpc: PolicyStoreGrpcConfig {
            endpoint: grpc_endpoint.clone(),
            x_token: grpc_x_token.clone(),
            commitment: None,
            timeout: Duration::from_secs(60),
            connect_timeout: Duration::from_secs(10),
            tcp_nodelay: true,
            http2_adaptive_window: true,
            http2_keep_alive: false,
            http2_keep_alive_interval: None,
            http2_keep_alive_timeout: None,
            http2_keep_alive_while_idle: None,
            max_decoding_message_size: Some(16 * 1024 * 1024), // 16 MiB
            initial_connection_window_size: None,
            initial_stream_window_size: None,
        },
    };

    let policy_store = PolicyStore::build().config(config).run().await?;

    // Use the Sandwiched.me MEV blocklist (DROPS). This is the policy PDA, not its mint.
    let shield_policy_pubkey = Pubkey::from_str("xMTozeQTEX2MR9KUon8vjg17U5Q9459RSASE3wy5eNB")?;

    // Step 2: create the TPU sender.
    let sender_config = YellowstoneTpuSenderConfig {
        endpoints: Endpoints {
            rpc: Url::parse(&rpc_endpoint)?,
            grpc: Url::parse(&grpc_endpoint)?,
            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()
    };

    // The callback reports what happened to each transaction after it is queued.
    let (callback_tx, mut callback_rx) = tokio::sync::mpsc::unbounded_channel();

    let NewYellowstoneTpuSender {
        mut sender,
        related_objects_jh: _,
    } = create_yellowstone_tpu_sender_with_callback(
        sender_config,
        TpuIdentity::from_keypair(&identity),
        callback_tx,
    )
    .await?;

    // Step 3: create the RPC client.
    let rpc_client = RpcClient::new_with_commitment(rpc_endpoint, CommitmentConfig::confirmed());

    // Step 4: build the transaction.
    const LAMPORTS: u64 = 1000;
    let instructions = vec![transfer(&identity.pubkey(), &recipient, LAMPORTS)];

    let latest_blockhash = rpc_client.get_latest_blockhash().await?;

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

    let signature = transaction.signatures[0];
    let wire_txn = bincode::serialize(&transaction)?;

    // Step 5: send with Shield protection.
    let shield_blocklist = ShieldBlockList {
        policy_store: &policy_store,
        shield_policy_addresses: &[shield_policy_pubkey],
        default_return_value: true, // fail-open: send anyway if a policy address isn't found
    };

    match sender
        .send_txn_with_shield_policies(
            wire_txn,
            shield_blocklist,
            Some(TpuSenderTxnInfo::new(signature)),
        )
        .await
    {
        Ok(()) => {
            // Ok means the transaction was queued to the sender, not that it reached a leader.
            println!("Transaction queued. Signature: {}", signature);
            println!("At least one leader in range passed the Shield policy.");

            // Wait for the outcome before main returns: dropping `sender` shuts the driver down
            // and can discard a transaction that hasn't gone out yet.
            match callback_rx.recv().await {
                Some(TpuSenderResponse::TxSent(sent)) => {
                    println!("Sent to validator {}", sent.remote_peer_identity);
                }
                Some(TpuSenderResponse::TxFailed(failed)) => {
                    println!("Send to validator {} failed: {}", failed.remote_peer_identity, failed.failure_reason);
                }
                Some(TpuSenderResponse::TxDrop(dropped)) => {
                    println!("Dropped for validator {}: {}", dropped.remote_peer_identity, dropped.drop_reason);
                }
                None => println!("Sender shut down before reporting an outcome."),
            }
        }
        Err(e) if matches!(e.kind, SendErrorKind::RemotePeerBlocked) => {
            println!("Transaction not sent: every leader in range is blocked by the Shield policy.");
        }
        Err(e) => println!("Transaction not sent: {e}"),
    }

    Ok(())
}
```

{% endcode %}

What the key pieces do:

* **`PolicyStoreConfig`** points the store at your RPC and gRPC endpoints.
* **`PolicyStore`** caches Shield policies from on-chain data and keeps them current over gRPC. If that stream ends, the store logs a warning and stops receiving updates (it doesn't reconnect), so rebuild it to pick up later policy changes.
* **`ShieldBlockList`** links the policy store to the policy PDAs you want to enforce.
* **`send_txn_with_shield_policies()`** sends only to the leaders in range that your policies allow, and returns a `SendErrorKind::RemotePeerBlocked` error when none of them is allowed.
* **`default_return_value: true`** is fail-open: if a policy address isn't found in the policy store (a typo, the mint passed instead of the policy PDA, or a policy not yet synced), the transaction is sent anyway. Combined with a typo, fail-open gives no protection at all. Set it to `false` to fail closed.

## How it works under the hood

When you call `send_txn_with_shield_policies`, the client reads the current slot, picks the leaders in range from the schedule, and checks each one against your policies before deciding where to send:

```
1. send_txn_with_shield_policies(txn, shield, txn_info)
                  │
                  ▼
2. TPU client reads the current slot
                  │
                  ▼
3. Picks the leaders in range: the current leader, plus the
   next one once the current 4-slot window is half over
                  │
                  ▼
4. Shield check for each leader: is it allowed?
   ├─ PolicyStore → snapshot()
   └─ is_allowed([policies], &leader_pubkey)
                  │
        ┌─────────┴─────────┐
        ▼                   ▼
   YES (any leader)     NO (every leader)
   5a. Send over QUIC   5b. Return SendError { kind: SendErrorKind::RemotePeerBlocked, .. }
       to the allowed       (transaction not sent)
       leaders
```

The blocklist check, from the `yellowstone-jet-tpu-client` source:

{% code title="ShieldBlockList" %}

```rust
impl Blocklist for ShieldBlockList<'_> {
    fn is_blocked(&self, peer_address: &Pubkey) -> bool {
        use yellowstone_shield_store::PolicyStoreTrait;

        !self
            .policy_store
            .snapshot()
            .is_allowed(self.shield_policy_addresses, peer_address)
            .unwrap_or(self.default_return_value)
    }
}
```

{% endcode %}

This means:

* **Blocklists (Deny):** if the validator is on the list, `is_blocked()` is true and the transaction is not sent to it.
* **Allowlists (Allow):** if the validator is not on the list, `is_blocked()` is true and the transaction is not sent to it.

The `PolicyStore` syncs policies continuously, so `snapshot()` is a fast, lock-free read. It comes from the `PolicyStoreTrait` trait:

{% code title="PolicyStoreTrait" %}

```rust
use yellowstone_shield_store::PolicyStoreTrait; // provides snapshot()

let policy_store = PolicyStore::build().config(config).run().await?;

// Later: read the current snapshot and check a validator.
let snapshot = policy_store.snapshot();
match snapshot.is_allowed(&[policy_pda], &validator_pubkey) {
    Ok(true) => println!("Validator allowed"),
    Ok(false) => println!("Validator blocked"),
    Err(e) => println!("Policy check failed: {:?}", e),
}
```

{% endcode %}

## Handling blocked leaders

When every leader in range is blocked, the transaction is not sent, and nothing queues it for later. You have a few options:

**Retry when the next leader comes into range.** The error hands back the transaction bytes, so you can resend them unchanged:

{% code title="Retry" %}

```rust
use yellowstone_jet_tpu_client::yellowstone_grpc::sender::SendErrorKind;

let policies = [shield_policy_pubkey];
let shield = || ShieldBlockList {
    policy_store: &policy_store,
    shield_policy_addresses: &policies,
    default_return_value: true,
};
let txn_info = TpuSenderTxnInfo::new(signature);

if let Err(err) = sender
    .send_txn_with_shield_policies(wire_txn, shield(), Some(txn_info))
    .await
{
    if !matches!(err.kind, SendErrorKind::RemotePeerBlocked) {
        return Err(err.into());
    }
    // Every leader in range is blocked. A leader you haven't checked yet is only certain to be
    // in range after one full 4-slot leader window, so wait at least that long (four slots is
    // about 0.8 s at Solana's current 200 ms slot time; 1.6 s leaves margin), then resend.
    tokio::time::sleep(std::time::Duration::from_millis(1600)).await;
    sender
        .send_txn_with_shield_policies(err.txn, shield(), Some(txn_info))
        .await?;
}
```

{% endcode %}

**Fail open** by setting `default_return_value: true`, so the transaction is still sent if a policy address isn't found. With a mistyped address, that means no protection.

## Advanced usage

**Apply multiple policies.** Policies are checked in order, and the first one that lists the leader decides: a Deny policy blocks it and an Allow policy allows it. If none lists the leader, it's allowed unless one of the policies is an Allow policy:

{% code title="Multiple policies" %}

```rust
let policies = vec![
    Pubkey::from_str("xMTozeQTEX2MR9KUon8vjg17U5Q9459RSASE3wy5eNB")?, // DROPS
    Pubkey::from_str("7xTAiL3tTzgbDwsxMKi2rMXW7QKzcGrABsZc2tW6L6bj")?, // SLOWAF
];

let shield_blocklist = ShieldBlockList {
    policy_store: &policy_store,
    shield_policy_addresses: &policies,
    default_return_value: true,
};

sender
    .send_txn_with_shield_policies(
        wire_txn,
        shield_blocklist,
        Some(TpuSenderTxnInfo::new(signature)),
    )
    .await?;
```

{% endcode %}

**Broadcast to specific validators,** bypassing the leader schedule. No Shield check applies to these sends:

{% code title="Specific validators" %}

```rust
let target_validators = vec![
    Pubkey::from_str("ValidatorA...")?,
    Pubkey::from_str("ValidatorB...")?,
];

sender
    .send_txn_many_dest(
        wire_txn,
        &target_validators,
        Some(TpuSenderTxnInfo::new(signature)),
    )
    .await?;
```

{% endcode %}

## Resources

* Shield program: [github.com/rpcpool/yellowstone-shield](https://github.com/rpcpool/yellowstone-shield)
* Jet engine: [github.com/rpcpool/yellowstone-jet](https://github.com/rpcpool/yellowstone-jet)
* Policy store crate: [`yellowstone-shield-store`](https://crates.io/crates/yellowstone-shield-store)
* Policy explorer: [validators.app/yellowstone-shield](https://www.validators.app/yellowstone-shield)
* Shield announcement: [Introducing Yellowstone Shield](https://blog.triton.one/introducing-yellowstone-shield)

## What's next

Pair Shield with a sender you control: the [Yellowstone Jet TPU client](/solana/quickstart/sending-transactions/yellowstone-jet-tpu-client.md) combines on-chain Shield policies with your own routing and retry logic.

***

<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)
