> 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/use-cases/trading-and-market-making.md).

# Trading and market making

Build the full data and transaction stack for a Solana trading or market-making bot.

End-to-end stack for a market-making or trading bot on Solana: real-time DEX data, the earliest Dragon's Mouth signal, priority fees, reliable transaction landing.

## Overview

This guide walks you through setting up Triton's trading infrastructure stack for a market-making or trading bot on Solana. You'll connect to real-time DEX data, get transactions before execution with Deshred, estimate competitive priority fees, and land transactions reliably under congestion.

By the end you'll have a working setup that covers the full path from signal to fill.

## Products used in this guide

Five Triton products work together in this stack. Each card below opens the full product docs, useful when you want to tune defaults, see every option, or troubleshoot something specific to that product.

<table data-card-size="large" data-view="cards"><thead><tr><th></th><th></th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td><i class="fa-radio">:radio:</i> <strong>Dragon's Mouth</strong></td><td>Subscribe to DEX pool account updates and transactions in real time.</td><td><a href="/solana/real-time-streaming/dragon-s-mouth-grpc.md">Dragon's Mouth gRPC</a></td></tr><tr><td><i class="fa-fire">:fire:</i> <strong>Deshred</strong></td><td>Receive transactions from shreds before execution for the earliest signal.</td><td><a href="/solana/real-time-streaming/deshred-transactions.md">Deshred Transactions gRPC</a></td></tr><tr><td><i class="fa-arrow-trend-up">:arrow-trend-up:</i> <strong>Priority Fee API</strong></td><td>Get a percentile-based fee estimate before building your transaction.</td><td><a href="/solana/sending-transactions/priority-fees-api.md">Priority fees API</a></td></tr><tr><td><i class="fa-paper-plane">:paper-plane:</i> <strong>Jet</strong></td><td>Send transactions directly to the current leader with built-in SWQoS.</td><td><a href="/solana/sending-transactions.md">Sending transactions</a></td></tr><tr><td><i class="fa-shield">:shield:</i> <strong>Shield</strong></td><td>(Optional) Block specific validators from processing your transactions.</td><td><a href="/solana/sending-transactions/shield-mev-protection.md">Shield Protection</a></td></tr></tbody></table>

## Prerequisites

{% stepper %}
{% step %}

#### A Triton account with an active endpoint

You'll need your **endpoint URL** and **token** from the [customer dashboard](https://customers.triton.one).
{% endstep %}

{% step %}

#### Basic familiarity with Solana

Accounts, transactions, compute units.
{% endstep %}
{% endstepper %}

## Step 0: Install the dependencies

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

```bash
npm install @triton-one/yellowstone-grpc bs58 @solana/web3.js@1
```

{% hint style="info" %}
In an ES module project (`"type": "module"`) that TypeScript checks with `"module": "nodenext"`, `new Client(...)` from `@triton-one/yellowstone-grpc` fails type-checking with TS2351 ("This expression is not constructable"). The code still runs. To type-check it, use a CommonJS project or `"moduleResolution": "bundler"`.
{% endhint %}
{% endtab %}

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

```toml
[dependencies]
yellowstone-grpc-client = "17"
yellowstone-grpc-proto = "14"
tokio = { version = "1", features = ["full"] }
futures = "0.3"
bs58 = "0.5"
anyhow = "1"
reqwest = { version = "0.12", features = ["json"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
solana-client = "2"
solana-sdk = "2"
```

{% endcode %}
{% endtab %}

{% tab title="Python" %}

```bash
pip install grpcio grpcio-tools base58 solana httpx

# Generate yellowstone gRPC stubs. Save geyser.proto and solana-storage.proto into ./proto
# (geyser.proto imports solana-storage.proto, so compile both):
# python -m grpc_tools.protoc -I./proto --python_out=. --grpc_python_out=. geyser.proto solana-storage.proto
# Proto files: https://github.com/rpcpool/yellowstone-grpc/tree/master/yellowstone-grpc-proto/proto
```

{% endtab %}
{% endtabs %}

## Step 1: Stream real-time DEX data with Dragon's Mouth

Dragon's Mouth delivers account and transaction updates as the node processes them, at processed commitment. For a trading bot you'll subscribe to the pool accounts and transactions of the DEX programs you're trading on.

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

```typescript
import Client, {
  CommitmentLevel,
  SubscribeRequest,
} from "@triton-one/yellowstone-grpc";
import bs58 from "bs58";

const ENDPOINT = "https://<your-endpoint>.mainnet.rpcpool.com";
const TOKEN = "<your-token>";

// Raydium AMM v4 program, replace with the DEX program you're trading
const RAYDIUM_AMM = "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8";

async function streamDexData() {
  const client = new Client(ENDPOINT, TOKEN, {});
  await client.connect();
  const stream = await client.subscribe();

  const request: SubscribeRequest = {
    accounts: {
      pool_accounts: {
        account: [],
        owner: [RAYDIUM_AMM],
        filters: [],
      },
    },
    transactions: {
      dex_transactions: {
        vote: false,
        failed: false,
        accountInclude: [RAYDIUM_AMM],
        accountExclude: [],
        accountRequired: [],
      },
    },
    slots: {},
    transactionsStatus: {},
    entry: {},
    blocks: {},
    blocksMeta: {},
    blockFooter: {},
    commitment: CommitmentLevel.PROCESSED,
    accountsDataSlice: [],
    ping: undefined,
  };

  stream.on("data", (data) => {
    if (data.account) {
      const pubkey = bs58.encode(data.account.account!.pubkey);
      console.log("Pool state updated:", pubkey);
      // Decode and act on the account data here
    }

    if (data.transaction) {
      const sig = bs58.encode(data.transaction.transaction!.signature);
      console.log("Transaction processed:", sig);
      // Parse the transaction and look for arbitrage / copy-trade signals
    }
  });

  stream.on("error", (err) => {
    console.error("Stream error:", err);
  });

  await new Promise<void>((resolve, reject) => {
    stream.write(request, (err) => {
      if (err) reject(err);
      else resolve();
    });
  });

  // Keep stream alive with periodic pings
  setInterval(() => {
    stream.write({ ping: { id: 1 } } as SubscribeRequest, () => {});
  }, 30_000);
}

streamDexData().catch(console.error);
```

{% endtab %}

{% tab title="Rust" %}

```rust
use std::collections::HashMap;
use futures::StreamExt;
use yellowstone_grpc_client::{ClientTlsConfig, GeyserGrpcClient};
use yellowstone_grpc_proto::prelude::{
    subscribe_update::UpdateOneof, CommitmentLevel, SubscribeRequest,
    SubscribeRequestFilterAccounts, SubscribeRequestFilterTransactions,
};

const ENDPOINT: &str = "https://<your-endpoint>.mainnet.rpcpool.com";
const TOKEN: &str = "<your-token>";
const RAYDIUM_AMM: &str = "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8";

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let mut client = GeyserGrpcClient::build_from_shared(ENDPOINT.to_string())?
        .x_token(Some(TOKEN.to_string()))?
        .tls_config(ClientTlsConfig::new().with_native_roots())?
        .connect()
        .await?;

    let mut accounts = HashMap::new();
    accounts.insert(
        "pool_accounts".to_string(),
        SubscribeRequestFilterAccounts {
            owner: vec![RAYDIUM_AMM.to_string()],
            ..Default::default()
        },
    );

    let mut transactions = HashMap::new();
    transactions.insert(
        "dex_transactions".to_string(),
        SubscribeRequestFilterTransactions {
            vote: Some(false),
            failed: Some(false),
            account_include: vec![RAYDIUM_AMM.to_string()],
            ..Default::default()
        },
    );

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

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

    while let Some(Ok(msg)) = stream.next().await {
        match msg.update_oneof {
            Some(UpdateOneof::Account(acct)) => {
                let pubkey = bs58::encode(acct.account.unwrap().pubkey).into_string();
                println!("Pool state updated: {pubkey}");
            }
            Some(UpdateOneof::Transaction(tx)) => {
                let sig = bs58::encode(tx.transaction.unwrap().signature).into_string();
                println!("Transaction processed: {sig}");
            }
            _ => {}
        }
    }

    Ok(())
}
```

{% endtab %}

{% tab title="Python" %}

```python
import asyncio
import grpc
from grpc import aio
import base58

# Requires yellowstone gRPC Python stubs generated from the proto files.
# After running grpc_tools.protoc (see Step 0), the output includes geyser_pb2.py,
# geyser_pb2_grpc.py and solana_storage_pb2.py in whatever --python_out directory you
# specified. Adjust the imports if you placed them inside a package folder.
from geyser_pb2 import (
    SubscribeRequest,
    SubscribeRequestFilterAccounts,
    SubscribeRequestFilterTransactions,
    CommitmentLevel,
    SubscribeRequestPing,
)
from geyser_pb2_grpc import GeyserStub

ENDPOINT = "<your-endpoint>.mainnet.rpcpool.com:443"
TOKEN = "<your-token>"
RAYDIUM_AMM = "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8"

async def stream_dex_data():
    credentials = grpc.ssl_channel_credentials()
    async with aio.secure_channel(ENDPOINT, credentials) as channel:
        stub = GeyserStub(channel)
        metadata = (("x-token", TOKEN),)

        async def request_iterator():
            yield SubscribeRequest(
                accounts={
                    "pool_accounts": SubscribeRequestFilterAccounts(
                        owner=[RAYDIUM_AMM],
                    )
                },
                transactions={
                    "dex_transactions": SubscribeRequestFilterTransactions(
                        vote=False,
                        failed=False,
                        account_include=[RAYDIUM_AMM],
                    )
                },
                commitment=CommitmentLevel.PROCESSED,
            )
            while True:
                await asyncio.sleep(30)
                yield SubscribeRequest(ping=SubscribeRequestPing(id=1))

        async for update in stub.Subscribe(request_iterator(), metadata=metadata):
            if update.HasField("account"):
                pubkey = base58.b58encode(bytes(update.account.account.pubkey)).decode()
                print(f"Pool state updated: {pubkey}")
            elif update.HasField("transaction"):
                sig = base58.b58encode(bytes(update.transaction.transaction.signature)).decode()
                print(f"Transaction processed: {sig}")

asyncio.run(stream_dex_data())
```

{% endtab %}
{% endtabs %}

**Verify it works:** you should see pool state updates and transactions printing to the console within a few seconds of starting the script. If nothing appears, check your endpoint URL and token.

An account update prints something like:

{% code title="Output" %}

```
Pool state updated: 58oQChx4yWmvKdwLLZzBi4ChoCc2fqCUWBkwMihLYQo2
```

{% endcode %}

This is the pool's public key encoded in base58, the standard human-readable format for Solana addresses. The raw pool state (reserves, fees, prices) is in `data.account.account.data` as raw bytes. To use it in your trading logic, decode it against the program's account layout, for example, the Raydium SDK or the program's Anchor IDL.

A transaction update prints:

{% code title="Output" %}

```
Transaction processed: 5Vk5mYDpFq3vQtXm8ZkRj2PnCwBsHeLfY6gNuoA1T9KdEpRsuW3HqXjc...
```

{% endcode %}

The signature is base58-encoded (64 bytes, up to 88 characters). The full transaction is in `data.transaction` as raw protobuf bytes. To decode it into readable JSON for parsing instructions, accounts, and token balance changes:

```typescript
import { txEncode } from "@triton-one/yellowstone-grpc";

const decoded = txEncode.encode(
  data.transaction.transaction!,  // SubscribeUpdateTransactionInfo (not the outer wrapper)
  txEncode.encoding.JsonParsed,
  255,  // max transaction version
  true  // include rewards
);

// decoded.transaction.message.instructions contains the swap instructions
// decoded.meta.preTokenBalances / postTokenBalances shows what changed
console.log(JSON.stringify(decoded, null, 2));
```

## Step 2: Get the earliest signal with Deshred

Deshred reconstructs transactions from shreds before execution, so they usually arrive ahead of the Dragon's Mouth `processed` stream. The lead varies per transaction. It uses a separate RPC method, `SubscribeDeshred`, on the same endpoint and token as Dragon's Mouth.

{% hint style="warning" %}
**Deshred limitations**

* **No execution metadata**: you receive intent only. There are no logs, balance changes, status, or compute unit data.
* **No finality guarantee**: the transaction may fail, get dropped, or land on a dead fork. Always confirm with your Dragon's Mouth `processed` stream.
  {% endhint %}

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

```typescript
import Client, {
  SubscribeRequestFilterDeshredTransactions,
} from "@triton-one/yellowstone-grpc";
import bs58 from "bs58";

const ENDPOINT = "https://<your-endpoint>.mainnet.rpcpool.com";
const TOKEN = "<your-token>";
const RAYDIUM_AMM = "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8";

async function streamDeshred() {
  const client = new Client(ENDPOINT, TOKEN, {});
  await client.connect();
  const stream = await client.subscribeDeshred();

  const request = {
    deshredTransactions: {
      dex: {
        vote: false,
        accountInclude: [RAYDIUM_AMM],
        accountExclude: [],
        accountRequired: [],
      } as SubscribeRequestFilterDeshredTransactions,
    },
  };

  stream.on("data", (data) => {
    if (data.deshredTransaction) {
      const sig = bs58.encode(
        data.deshredTransaction.transaction!.signature
      );
      console.log("Deshred tx (pre-execution):", sig, "slot:", data.deshredTransaction.slot);
    }
  });

  stream.on("error", (err) => {
    console.error("Deshred stream error:", err);
  });

  await new Promise<void>((resolve, reject) => {
    stream.write(request, (err) => {
      if (err) reject(err);
      else resolve();
    });
  });
}

streamDeshred().catch(console.error);
```

{% endtab %}

{% tab title="Rust" %}

```rust
use std::collections::HashMap;
use futures::StreamExt;
use yellowstone_grpc_client::{ClientTlsConfig, GeyserGrpcClient};
use yellowstone_grpc_proto::prelude::{
    subscribe_update_deshred, SubscribeDeshredRequest, SubscribeRequestFilterDeshredTransactions,
};

const ENDPOINT: &str = "https://<your-endpoint>.mainnet.rpcpool.com";
const TOKEN: &str = "<your-token>";
const RAYDIUM_AMM: &str = "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8";

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let mut client = GeyserGrpcClient::build_from_shared(ENDPOINT.to_string())?
        .x_token(Some(TOKEN.to_string()))?
        .tls_config(ClientTlsConfig::new().with_native_roots())?
        .connect()
        .await?;

    let mut deshred_transactions = HashMap::new();
    deshred_transactions.insert(
        "dex".to_string(),
        SubscribeRequestFilterDeshredTransactions {
            vote: Some(false),
            account_include: vec![RAYDIUM_AMM.to_string()],
            ..Default::default()
        },
    );

    let request = SubscribeDeshredRequest {
        deshred_transactions,
        ..Default::default()
    };

    let (_sink, mut stream) = client.subscribe_deshred_with_request(Some(request)).await?;

    while let Some(Ok(msg)) = stream.next().await {
        if let Some(subscribe_update_deshred::UpdateOneof::DeshredTransaction(deshred_tx)) =
            msg.update_oneof
        {
            let sig = bs58::encode(deshred_tx.transaction.unwrap().signature).into_string();
            println!("Deshred tx (pre-execution): {} slot: {}", sig, deshred_tx.slot);
        }
    }

    Ok(())
}
```

{% endtab %}

{% tab title="Python" %}

```python
import asyncio
import grpc
from grpc import aio
import base58

from geyser_pb2 import (
    SubscribeDeshredRequest,
    SubscribeRequestFilterDeshredTransactions,
    SubscribeRequestPing,
)
from geyser_pb2_grpc import GeyserStub

ENDPOINT = "<your-endpoint>.mainnet.rpcpool.com:443"
TOKEN = "<your-token>"
RAYDIUM_AMM = "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8"

async def stream_deshred():
    credentials = grpc.ssl_channel_credentials()
    async with aio.secure_channel(ENDPOINT, credentials) as channel:
        stub = GeyserStub(channel)
        metadata = (("x-token", TOKEN),)

        async def request_iterator():
            yield SubscribeDeshredRequest(
                deshred_transactions={
                    "dex": SubscribeRequestFilterDeshredTransactions(
                        vote=False,
                        account_include=[RAYDIUM_AMM],
                    )
                }
            )
            while True:
                await asyncio.sleep(30)
                yield SubscribeDeshredRequest(ping=SubscribeRequestPing(id=1))

        async for update in stub.SubscribeDeshred(request_iterator(), metadata=metadata):
            if update.HasField("deshred_transaction"):
                sig = base58.b58encode(
                    bytes(update.deshred_transaction.transaction.signature)
                ).decode()
                print(f"Deshred tx (pre-execution): {sig} slot: {update.deshred_transaction.slot}")

asyncio.run(stream_deshred())
```

{% endtab %}
{% endtabs %}

**Verify it works:** you should see pre-execution transactions printing, usually before they appear in your Dragon's Mouth stream. To measure the lead, see [Deshred Transactions gRPC](/solana/real-time-streaming/deshred-transactions.md#how-it-works).

{% code title="Output" %}

```
Deshred tx (pre-execution): 5Vk5mYDpFq3vQtXm8ZkRj2PnCwBsHeLfY6gNuoA1T9Kd... slot: 307152834
```

{% endcode %}

The signature is base58-encoded. The transaction data in `data.deshredTransaction.transaction` contains the instructions and account keys, so you can see exactly which pools and wallets are involved, and in which direction. What you **cannot** see: whether it will succeed, what the balance changes will be, or any logs.

Use this signal to detect incoming order flow and react, for example, detecting a large swap into a pool you're quoting. Always cross-reference with your Dragon's Mouth stream to confirm the transaction actually executed.

## Step 3: Estimate a competitive priority fee

Before building your transaction, call Triton's Priority Fee API to get a fee at your target percentile. This avoids both underbidding (transaction doesn't land) and overbidding (unnecessary cost).

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

```typescript
async function getPriorityFee(
  endpoint: string,
  token: string,
  accounts: string[],
  percentile: number // 1-10000, where 5000 = median, 9000 = 90th percentile
): Promise<number> {
  const response = await fetch(`${endpoint}/${token}`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      jsonrpc: "2.0",
      id: 1,
      method: "getRecentPrioritizationFees",
      params: [accounts, { percentile }],
    }),
  });

  const { result } = await response.json();
  // result is an array of { slot, prioritizationFee }, take the most recent
  const fees: { slot: number; prioritizationFee: number }[] = result;
  const latest = fees.sort((a, b) => b.slot - a.slot)[0];
  return latest.prioritizationFee; // micro-lamports per compute unit
}

// Example: get the 90th percentile fee for a Raydium swap
const fee = await getPriorityFee(ENDPOINT, TOKEN, [RAYDIUM_AMM], 9000);
console.log("Priority fee (90th pct):", fee, "micro-lamports/CU");
```

{% endtab %}

{% tab title="Rust" %}

```rust
use reqwest::Client;
use serde::Deserialize;

#[derive(Deserialize)]
struct PrioritizationFee {
    slot: u64,
    #[serde(rename = "prioritizationFee")]
    prioritization_fee: u64,
}

#[derive(Deserialize)]
struct RpcResponse {
    result: Vec<PrioritizationFee>,
}

async fn get_priority_fee(
    endpoint: &str,
    token: &str,
    accounts: &[&str],
    percentile: u32, // 1-10000, where 5000 = median, 9000 = 90th percentile
) -> anyhow::Result<u64> {
    let client = Client::new();
    let url = format!("{}/{}", endpoint, token);

    let body = serde_json::json!({
        "jsonrpc": "2.0",
        "id": 1,
        "method": "getRecentPrioritizationFees",
        "params": [accounts, { "percentile": percentile }]
    });

    let mut response: RpcResponse = client
        .post(&url)
        .json(&body)
        .send()
        .await?
        .json()
        .await?;

    response.result.sort_by(|a, b| b.slot.cmp(&a.slot));
    Ok(response.result[0].prioritization_fee)
}
```

{% endtab %}

{% tab title="Python" %}

```python
import httpx

async def get_priority_fee(
    endpoint: str,
    token: str,
    accounts: list[str],
    percentile: int,  # 1-10000, where 5000 = median, 9000 = 90th percentile
) -> int:
    async with httpx.AsyncClient() as client:
        response = await client.post(
            f"{endpoint}/{token}",
            json={
                "jsonrpc": "2.0",
                "id": 1,
                "method": "getRecentPrioritizationFees",
                "params": [accounts, {"percentile": percentile}],
            },
        )
        fees = response.json()["result"]
        fees.sort(key=lambda x: x["slot"], reverse=True)
        return fees[0]["prioritizationFee"]
```

{% endtab %}
{% endtabs %}

**When to use which percentile** (1-10,000, where 5000 = median, 10000 = 100th percentile):

* **Normal conditions:** 5000-7500
* **Congested network or time-sensitive trade:** 9000-9500
* **Emergency / must-land:** 9900+

**Verify it works:** the call should return a non-zero number. During quiet periods fees are low; during congestion they spike significantly.

{% code title="Output" %}

```
Priority fee (90th pct): 45000 micro-lamports/CU
```

{% endcode %}

The fee is in micro-lamports per compute unit. A typical Raydium swap uses \~200,000 to 400,000 compute units, so 45,000 micro-lamports/CU × 300,000 CU ÷ 1,000,000 = 13,500 lamports (\~0.0000135 SOL) total priority fee, a fraction of a cent at current prices.

You can layer your own strategy on top of this (for example, adding a fixed buffer during high-volatility periods), or pass it directly to `setComputeUnitPrice` in Step 4:

```typescript
ComputeBudgetProgram.setComputeUnitPrice({ microLamports: fee })
```

## Step 4: Send your transaction via Jet

Jet is Triton's transaction relay engine. When you send through your Triton endpoint, Jet automatically routes to the current leader over QUIC, handles connection caching, leader schedule tracking, and leader fanout. SWQoS is included for mainnet sends, with no extra configuration.

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

```typescript
import {
  Connection,
  ComputeBudgetProgram,
  Transaction,
} from "@solana/web3.js";

async function sendWithJet(
  serializedTransaction: Buffer,
  endpoint: string,
  token: string
): Promise<string> {
  // Use your Triton endpoint, Jet routing is handled automatically
  const connection = new Connection(`${endpoint}/${token}`, "confirmed");

  const signature = await connection.sendRawTransaction(
    serializedTransaction,
    {
      skipPreflight: true, // simulate separately via simulateTransaction() if needed
      maxRetries: 0,       // Triton doesn't retry or rebroadcast server-side, so your client handles retries
    }
  );

  console.log("Sent:", signature);
  return signature;
}

// Add the priority fee from Step 3 to your transaction
function addPriorityFee(tx: Transaction, microLamportsPerCU: number): void {
  tx.add(
    ComputeBudgetProgram.setComputeUnitPrice({
      microLamports: microLamportsPerCU,
    })
  );
}
```

{% endtab %}

{% tab title="Rust" %}

```rust
use solana_client::rpc_client::RpcClient;
use solana_client::rpc_config::RpcSendTransactionConfig;
use solana_sdk::transaction::Transaction;

fn send_with_jet(
    transaction: &Transaction,
    endpoint: &str,
    token: &str,
) -> anyhow::Result<String> {
    let rpc_url = format!("{}/{}", endpoint, token);
    let client = RpcClient::new(rpc_url);

    let signature = client.send_transaction_with_config(
        transaction,
        RpcSendTransactionConfig {
            skip_preflight: true,
            max_retries: Some(0), // Triton doesn't retry or rebroadcast server-side, so your client handles retries
            ..Default::default()
        },
    )?;

    println!("Sent: {}", signature);
    Ok(signature.to_string())
}
```

{% endtab %}

{% tab title="Python" %}

```python
from solana.rpc.async_api import AsyncClient
from solana.rpc.models import TxOpts

async def send_with_jet(serialized_tx: bytes, endpoint: str, token: str) -> str:
    async with AsyncClient(f"{endpoint}/{token}") as client:
        response = await client.send_raw_transaction(
            serialized_tx,
            opts=TxOpts(skip_preflight=True, max_retries=0),
        )
    signature = str(response.value)
    print(f"Sent: {signature}")
    return signature
```

{% endtab %}
{% endtabs %}

{% hint style="warning" %}
Set `maxRetries: 0` (Triton doesn't retry on the server), then rebroadcast from your client: resend the **same signed transaction** every couple of seconds until it confirms or its blockhash expires. Triton sends go through Jet, which doesn't keep a server-side retry queue (unlike Agave's send-transaction-service, whose queue overflows under congestion), so client-managed rebroadcasts are what land your transaction, and they give your strategy direct control.

Don't re-sign with a new blockhash while the original can still land. A re-signed transaction is a different transaction, so if the original lands too, both execute.
{% endhint %}

**Verify it works:** the signature should appear on-chain within 1-3 slots under normal conditions. Check at solscan.io or via `getSignatureStatuses`.

{% code title="Output" %}

```
Sent: 5Vk5mYDpFq3vQtXm8ZkRj2PnCwBsHeLfY6gNuoA1T9KdEpRsuW3HqXjc...
```

{% endcode %}

This is the transaction signature, base58-encoded, up to 88 characters.

**Rebroadcast until it lands or expires.** Keep the `lastValidBlockHeight` from the `getLatestBlockhash` call whose blockhash you signed with, then resend the same bytes until the transaction confirms or the block height passes that value:

```typescript
import { Connection, TransactionSignature } from "@solana/web3.js";

// Resends the same signed bytes until the transaction confirms or its blockhash expires.
// Returns the signature once confirmed, or null if the blockhash expired first.
async function sendUntilConfirmedOrExpired(
  connection: Connection,
  serializedTransaction: Uint8Array,
  lastValidBlockHeight: number // from the getLatestBlockhash call you signed with
): Promise<TransactionSignature | null> {
  const options = { skipPreflight: true, maxRetries: 0 };
  const signature = await connection.sendRawTransaction(serializedTransaction, options);

  while (true) {
    await new Promise((resolve) => setTimeout(resolve, 2000));

    // Read the block height before the status, so an expiry is never reported
    // for a transaction that landed in between.
    const blockHeight = await connection.getBlockHeight("confirmed");
    const { value } = await connection.getSignatureStatuses([signature]);
    const status = value[0];

    if (status?.confirmationStatus === "confirmed" || status?.confirmationStatus === "finalized") {
      if (status.err) throw new Error(`Landed but failed: ${JSON.stringify(status.err)}`);
      return signature;
    }
    if (blockHeight > lastValidBlockHeight) {
      return null; // expired without landing: now it's safe to re-sign with a new blockhash
    }

    // Same bytes, same transaction: the network processes it at most once
    await connection.sendRawTransaction(serializedTransaction, options);
  }
}
```

Because you set `maxRetries: 0`, retry logic is entirely in your hands. A blockhash expires by block height, not time: the transaction can land until the block height passes `lastValidBlockHeight`, 150 blocks after the blockhash (well under a minute at current mainnet slot times). Only when the function returns `null` should you fetch a new blockhash from `confirmed` or `finalized` commitment, re-sign, and send again. A transaction that landed with an error has already executed: whether to send a new one is a trading decision, not a retry.

## Putting it together

A minimal trading loop combining Steps 1, 3 and 4:

{% code title="Trading loop" lineNumbers="true" %}

```typescript
import Client, { CommitmentLevel } from "@triton-one/yellowstone-grpc";
import {
  ComputeBudgetProgram,
  Connection,
  Keypair,
  Transaction,
  TransactionSignature,
} from "@solana/web3.js";
import bs58 from "bs58";

const ENDPOINT = "https://<your-endpoint>.mainnet.rpcpool.com";
const TOKEN = "<your-token>";
const RAYDIUM_AMM = "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8";
const payer = Keypair.fromSecretKey(bs58.decode(process.env.PAYER_SECRET_KEY!)); // your funded signer

async function getPriorityFee(
  endpoint: string,
  token: string,
  accounts: string[],
  percentile: number
): Promise<number> {
  const response = await fetch(`${endpoint}/${token}`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      jsonrpc: "2.0",
      id: 1,
      method: "getRecentPrioritizationFees",
      params: [accounts, { percentile }],
    }),
  });
  const { result } = await response.json();
  const fees: { slot: number; prioritizationFee: number }[] = result;
  return fees.sort((a, b) => b.slot - a.slot)[0].prioritizationFee;
}

// From Step 4: resends the same signed bytes until the transaction confirms or its blockhash expires.
async function sendUntilConfirmedOrExpired(
  connection: Connection,
  serializedTransaction: Uint8Array,
  lastValidBlockHeight: number
): Promise<TransactionSignature | null> {
  const options = { skipPreflight: true, maxRetries: 0 };
  const signature = await connection.sendRawTransaction(serializedTransaction, options);

  while (true) {
    await new Promise((resolve) => setTimeout(resolve, 2000));

    const blockHeight = await connection.getBlockHeight("confirmed");
    const { value } = await connection.getSignatureStatuses([signature]);
    const status = value[0];

    if (status?.confirmationStatus === "confirmed" || status?.confirmationStatus === "finalized") {
      if (status.err) throw new Error(`Landed but failed: ${JSON.stringify(status.err)}`);
      return signature;
    }
    if (blockHeight > lastValidBlockHeight) return null;

    await connection.sendRawTransaction(serializedTransaction, options);
  }
}

async function tradingLoop() {
  const connection = new Connection(`${ENDPOINT}/${TOKEN}`, "confirmed");
  const client = new Client(ENDPOINT, TOKEN, {});
  await client.connect();
  const stream = await client.subscribe();

  stream.on("data", async (data) => {
    if (!data.transaction) return;

    // 1. Signal detected, decide if you want to act
    const shouldTrade = analyseTransaction(data.transaction);
    if (!shouldTrade) return;

    try {
      // 2. Get competitive priority fee
      const fee = await getPriorityFee(ENDPOINT, TOKEN, [RAYDIUM_AMM], 9000);

      // 3. Build and sign your transaction
      const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash("confirmed");
      const tx = new Transaction({ feePayer: payer.publicKey, blockhash, lastValidBlockHeight });
      tx.add(ComputeBudgetProgram.setComputeUnitPrice({ microLamports: fee }));
      // ... add your swap instruction here
      tx.sign(payer);

      // 4. Send via Jet and rebroadcast until it lands or expires (Step 4)
      const sig = await sendUntilConfirmedOrExpired(connection, tx.serialize(), lastValidBlockHeight);
      console.log("Result:", sig ?? "expired");
    } catch (err) {
      console.error("Trade failed:", err);
    }
  });

  // Subscribe request (same as Step 1)
  await new Promise<void>((resolve, reject) => {
    stream.write(
      {
        accounts: {},
        transactions: {
          dex_transactions: {
            vote: false,
            failed: false,
            accountInclude: [RAYDIUM_AMM],
            accountExclude: [],
            accountRequired: [],
          },
        },
        slots: {},
        transactionsStatus: {},
        entry: {},
        blocks: {},
        blocksMeta: {},
        commitment: CommitmentLevel.PROCESSED,
        accountsDataSlice: [],
        ping: undefined,
      },
      (err) => {
        if (err) reject(err);
        else resolve();
      }
    );
  });
}

function analyseTransaction(tx: any): boolean {
  // Your signal logic here
  return false;
}

tradingLoop().catch(console.error);
```

{% endcode %}

## Prefer a faster path to market?

If you'd rather go live without building your own routing and quoting logic, Triton's Titan API covers real-time routing across DEXes and RFQ providers, with DART live on-chain re-optimisation and quote streaming over WebSockets, all out of the box.

Two paths for two types of teams:

* **This guide**: build your own signal and execution stack. Full control, lowest latency, more engineering work.
* **Titan: plug in** pre-built liquidity routing and quoting. Fastest time to market, less custom code.

<table data-card-size="large" data-view="cards"><thead><tr><th></th><th></th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td><i class="fa-route">:route:</i> <strong>Titan Swap API</strong></td><td>Live swap quotes and executable routes streamed over WebSocket, with DART on-chain re-optimisation on selected pairs.</td><td><a href="/solana/sending-transactions/3rd-party-apis/titan-swap-api.md">Titan Swap API</a></td></tr></tbody></table>

## FAQs

<details>

<summary>Do I need all five products?</summary>

Dragon's Mouth and Jet cover the minimum: real-time data in, transactions out. Add Deshred if your strategy depends on acting before the network processes a transaction. Priority Fee API prevents you from overbidding during quiet periods. Shield applies your validator allowlist or blocklist to each send that carries your policy address: `forwardingPolicies` on `sendTransaction`, or the `Solana-ForwardingPolicies` header on `/sendtx`. Sends without a policy go to the upcoming leaders unfiltered.

</details>

<details>

<summary>How do I pick the right percentile?</summary>

Start at 7500 (75th percentile) and monitor landing rate. Increase to 9000+ during congestion or when latency matters more than cost.

</details>

<details>

<summary>Should I configure retries when sending?</summary>

Set `maxRetries: 0` (Triton doesn't retry on the server), then rebroadcast from your application: resend the same signed transaction every couple of seconds until it confirms or its blockhash expires (`lastValidBlockHeight`, see Step 4). Re-sign with a fresh blockhash only after the block height has passed `lastValidBlockHeight` and `getSignatureStatuses` shows the original didn't land; re-signing earlier creates a second transaction that can also execute. Triton sends go through Jet, which doesn't keep a server-side retry queue (unlike Agave's send-transaction-service, whose queue overflows under congestion), so client-managed rebroadcasts are what land your transaction.

</details>

<details>

<summary>Can I use this with Orca or other DEXes?</summary>

Yes, replace the Raydium AMM program address with the program address of any DEX.

Orca Whirlpools: `whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc`

</details>

## What's next

<table data-card-size="large" data-view="cards"><thead><tr><th></th><th></th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td><i class="fa-fire">:fire:</i> <strong>Deshred Transactions gRPC</strong></td><td>Pre-execution transactions reconstructed from raw shreds. Earliest intent signal for traders.</td><td><a href="/solana/real-time-streaming/deshred-transactions.md">Deshred Transactions gRPC</a></td></tr><tr><td><i class="fa-shield">:shield:</i> <strong>Shield MEV protection</strong></td><td>On-chain allowlists or blocklists with local enforcement.</td><td><a href="/solana/sending-transactions/shield-mev-protection.md">Shield Protection</a></td></tr><tr><td><i class="fa-route">:route:</i> <strong>Titan Swap API</strong></td><td>Live swap quotes and executable routes streamed over WebSocket, with DART on-chain re-optimisation on selected pairs.</td><td><a href="/solana/sending-transactions/3rd-party-apis/titan-swap-api.md">Titan Swap API</a></td></tr></tbody></table>

***

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