Documentation menu

Private TPU Send

Private TPU Send delivers your transactions directly to the TPU of the current and upcoming slot leaders over QUIC connections prioritized according to stake (stake-weighted QoS). Your transactions are not routed through public RPC nodes or shared queues, and they are excluded from the Private Orderflow stream. Private TPU Send also accepts atomic bundles, see Bundles.

Submitting transactions

PropertyValue
InterfaceHTTPS, JSON-RPC 2.0, method sendTransaction (compatible with the Solana RPC method)
EndpointSee Regions & endpoints
AuthenticationAPI key in the x-api-key header, see Authentication
Transaction encodingbase64
Responsethe transaction signature (base58)
Preflightnone: transactions are forwarded without simulation
ThroughputAs agreed in your offer, see Limits

Because the method follows the Solana RPC sendTransaction format, most existing senders need only a different URL and an extra header.

Request and response

Request:

http
POST / HTTP/1.1
Host: your-endpoint-host
Content-Type: application/json
x-api-key: mev_live_3f9a2c7e1b8d4f60a5c9e2b7d1f4a8c3

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "sendTransaction",
  "params": [
    "AV8x...base64-encoded signed transaction...AAA=",
    { "encoding": "base64" }
  ]
}

Success: the transaction was accepted and forwarded to the leaders. The result is its signature.

json
{ "jsonrpc": "2.0", "id": 1, "result": "4uQeVj5tqViQh7yWWGStvkEG1Zmhx6uasJtWCJziofM6...signature" }

Error: the transaction was not forwarded. See Errors for all codes.

json
{ "jsonrpc": "2.0", "id": 1, "error": { "code": -32602, "message": "invalid transaction: failed to deserialize" } }

A successful response means that the transaction was accepted and forwarded. It does not mean that it landed in a block. Always confirm the outcome through your RPC, as described below.

Options that only make sense on a full RPC node, such as skipPreflight, preflightCommitment and maxRetries, are accepted and ignored.

Bundles

Private TPU Send also accepts atomic bundles. A bundle is a list of signed transactions that the validator executes as a whole and in order, or not at all.

  • Bundles are delivered to the TPU of validators in our network and land in their leader slots.
  • A typical use is a backrun: the target transaction from the Private Orderflow stream first and your transaction right after it. See Placing your backrun.
  • The method and format for submitting bundles come with your access details from our support.

Preparing a transaction

Send fully signed transactions in the standard Solana wire format.

  1. Fetch a recent blockhash shortly before signing. A blockhash is valid for about 150 slots, roughly one minute.
  2. Set a compute unit limit and a priority fee with the Compute Budget program.
  3. Simulate the transaction through your RPC to catch errors before sending. Private TPU Send does not simulate it for you.
  4. Sign and serialize.
ts
import {
  ComputeBudgetProgram,
  Connection,
  Keypair,
  TransactionInstruction,
  TransactionMessage,
  VersionedTransaction,
} from "@solana/web3.js";

async function buildTransaction(
  rpc: Connection,
  payer: Keypair,
  instructions: TransactionInstruction[],
): Promise<{ base64: string; lastValidBlockHeight: number }> {
  const { blockhash, lastValidBlockHeight } = await rpc.getLatestBlockhash("confirmed");

  const message = new TransactionMessage({
    payerKey: payer.publicKey,
    recentBlockhash: blockhash,
    instructions: [
      ComputeBudgetProgram.setComputeUnitLimit({ units: 200_000 }),
      ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 50_000 }),
      ...instructions,
    ],
  }).compileToV0Message();

  const tx = new VersionedTransaction(message);
  tx.sign([payer]);

  const simulation = await rpc.simulateTransaction(tx);
  if (simulation.value.err) throw new Error(JSON.stringify(simulation.value.err));

  // Keep lastValidBlockHeight: after that block height the transaction can no longer land.
  return { base64: Buffer.from(tx.serialize()).toString("base64"), lastValidBlockHeight };
}

Tune the compute unit limit to what your transaction actually uses (the simulation reports unitsConsumed) and set the priority fee according to current network conditions.

Sending (TypeScript)

ts
const SEND_URL = process.env.MEV_SEND_URL!;

export async function sendTransaction(base64Tx: string): Promise<string> {
  const res = await fetch(SEND_URL, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "x-api-key": process.env.MEV_ALPHA_KEY!,
    },
    body: JSON.stringify({
      jsonrpc: "2.0",
      id: 1,
      method: "sendTransaction",
      params: [base64Tx, { encoding: "base64" }],
    }),
  });

  const body = await res.json();
  if (body.error) throw new Error(`HTTP ${res.status}, ${body.error.code}: ${body.error.message}`);
  return body.result; // signature
}

Node.js 18 and later reuse connections automatically with the built-in fetch, so the TLS handshake is paid only once.

Sending (Rust)

rust
use base64::{engine::general_purpose::STANDARD, Engine};
use serde_json::{json, Value};
use solana_sdk::transaction::VersionedTransaction;

/// Reuse one reqwest::Client for the lifetime of the process to keep the connection warm.
/// `url` is your Private TPU Send endpoint, for example read once from MEV_SEND_URL.
pub async fn send_transaction(
    http: &reqwest::Client,
    url: &str,
    api_key: &str,
    tx: &VersionedTransaction,
) -> anyhow::Result<String> {
    let wire = bincode::serialize(tx)?;
    let body = json!({
        "jsonrpc": "2.0",
        "id": 1,
        "method": "sendTransaction",
        "params": [STANDARD.encode(wire), { "encoding": "base64" }],
    });

    let resp: Value = http
        .post(url)
        .header("x-api-key", api_key)
        .json(&body)
        .send()
        .await?
        .json()
        .await?;

    if let Some(err) = resp.get("error") {
        anyhow::bail!("send failed: {err}");
    }
    Ok(resp["result"].as_str().unwrap_or_default().to_owned())
}

Dependencies: reqwest (with the json feature), serde_json, base64, bincode, solana-sdk, anyhow and tokio.

Confirming delivery

  • Track the signature through your RPC with getSignatureStatuses or a signatureSubscribe WebSocket subscription.
  • Resending the same signed transaction until it is confirmed or its blockhash expires is safe. The network never executes the same signature twice. Once per slot, about every 400 milliseconds, is enough.
  • If the blockhash expires, build and sign a new transaction.
ts
import { Connection } from "@solana/web3.js";

// lastValidBlockHeight comes from buildTransaction and belongs to the blockhash you signed with.
export async function waitForConfirmation(
  rpc: Connection,
  signature: string,
  base64Tx: string,
  lastValidBlockHeight: number,
) {
  while ((await rpc.getBlockHeight("confirmed")) <= lastValidBlockHeight) {
    const { value } = await rpc.getSignatureStatuses([signature]);
    const status = value[0];
    if (status?.err) throw new Error(`transaction failed: ${JSON.stringify(status.err)}`);
    if (status?.confirmationStatus === "confirmed" || status?.confirmationStatus === "finalized") return;

    await sendTransaction(base64Tx).catch(() => {}); // safe rebroadcast of the same signature
    await new Promise((r) => setTimeout(r, 400));
  }
  throw new Error("blockhash expired before confirmation");
}

Pass the lastValidBlockHeight returned by buildTransaction. It belongs to the blockhash the transaction was signed with, so the loop stops as soon as the transaction can no longer land.

Best practices

  • Submit from the region closest to your endpoint.
  • Keep one long-lived HTTP connection. Opening a new TLS connection for every transaction adds milliseconds.
  • Keep signing and serialization off the critical path where possible, for example by preparing transactions in advance.
  • Set a priority fee that reflects current conditions. Stake-weighted delivery gets your transaction to the leader; the fee still affects how the leader orders it.
  • Avoid writing to heavily contested accounts when you do not need to. Lock contention delays inclusion.
  • Rate-limit on your side slightly below your agreed TPS. See Limits.

Need help or access? Contact our support on Telegram.

Contact