> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbitflare.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Sending Transactions with Apex

> Send Solana transactions to OrbitFlare Apex over JSON-RPC sendTransaction, the binary HTTP routes /send-bin and /send-batch, or persistent QUIC streams.

## Choose a Transport

|              | JSON-RPC                    | `POST /send`            | `POST /send-bin`        | `POST /send-batch`        | QUIC uni              | QUIC bidi                   |
| ------------ | --------------------------- | ----------------------- | ----------------------- | ------------------------- | --------------------- | --------------------------- |
| **Body**     | base64 in JSON              | base64 in JSON          | raw bytes               | up to 16 raw frames       | one packet per stream | one packet per stream       |
| **Reply**    | signature or JSON-RPC error | signature or JSON error | signature or JSON error | per-frame results         | none                  | admission code              |
| **Best for** | drop-in for existing code   | simple JSON clients     | lowest HTTP overhead    | many sends in one request | the fastest path      | the rejection reason inline |

All transports apply the same [tip rule](/apex/tips), the same validation, and the same routing. The transport only changes how the bytes reach the Apex endpoint.

Transaction size limits are the same everywhere: legacy and v0 transactions up to 1232 bytes, [transaction v1](/apex/transaction-v1) up to 4096 bytes.

## Build a Signed, Tipped Transaction

The samples on this page share one helper per language. It builds a memo transaction with a compute budget and a tip, signs it, and can confirm a signature. Run it directly to print a signed transaction as base64, which is what the cURL samples use.

Set the environment first:

```bash theme={null}
export APEX_API_KEY="YOUR_API_KEY"
export APEX_RPC="http://fra.apex.orbitflare.com"
export SOLANA_RPC_URL="https://api.mainnet-beta.solana.com"
export KEYPAIR_PATH="payer.json"
```

<CodeGroup>
  ```javascript apex-tx.mjs theme={null}
  // npm install @solana/web3.js
  // Print a signed, tipped transaction as base64: node apex-tx.mjs
  import {
    ComputeBudgetProgram, Connection, Keypair, PublicKey, SystemProgram,
    TransactionInstruction, TransactionMessage, VersionedTransaction,
  } from "@solana/web3.js";
  import { readFileSync } from "node:fs";
  import { fileURLToPath } from "node:url";

  export const APEX_RPC = process.env.APEX_RPC ?? "http://fra.apex.orbitflare.com";
  export const API_KEY = process.env.APEX_API_KEY ?? "";
  const SOLANA_RPC = process.env.SOLANA_RPC_URL ?? "https://api.mainnet-beta.solana.com";
  const TIP_LAMPORTS = Number(process.env.TIP_LAMPORTS ?? 1_000_000);
  const MEMO_PROGRAM = new PublicKey("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr");

  export const payer = Keypair.fromSecretKey(
    Uint8Array.from(JSON.parse(readFileSync(process.env.KEYPAIR_PATH ?? "payer.json", "utf8"))),
  );
  export const connection = new Connection(SOLANA_RPC, "confirmed");

  export async function apexRpc(method, params) {
    const res = await fetch(APEX_RPC, {
      method: "POST",
      headers: { "Content-Type": "application/json", "x-api-key": API_KEY },
      body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
    });
    const body = await res.json();
    if (body.error) throw new Error(`${method} failed ${body.error.code}: ${body.error.message}`);
    return body.result;
  }

  let tipAccounts;
  let counter = 0;

  // Returns a signed VersionedTransaction. Pass { tip: false } for a bundle member without a tip.
  export async function buildTx({ tip = true, label = "apex" } = {}) {
    tipAccounts ??= await apexRpc("getTipAccounts", []);
    const tipAccount = new PublicKey(tipAccounts[Math.floor(Math.random() * tipAccounts.length)]);
    const { blockhash } = await connection.getLatestBlockhash("confirmed");
    const instructions = [
      ComputeBudgetProgram.setComputeUnitLimit({ units: 100_000 }),
      ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 10_000 }),
      new TransactionInstruction({
        programId: MEMO_PROGRAM,
        keys: [{ pubkey: payer.publicKey, isSigner: true, isWritable: true }],
        data: Buffer.from(`${label} ${Date.now()} ${counter++}`),
      }),
    ];
    if (tip) {
      instructions.push(
        SystemProgram.transfer({ fromPubkey: payer.publicKey, toPubkey: tipAccount, lamports: TIP_LAMPORTS }),
      );
    }
    const message = new TransactionMessage({
      payerKey: payer.publicKey,
      recentBlockhash: blockhash,
      instructions,
    }).compileToV0Message();
    const tx = new VersionedTransaction(message);
    tx.sign([payer]);
    return tx;
  }

  // Poll getSignatureStatuses on the Solana RPC. Returns the slot, or null on timeout.
  export async function confirm(signature, timeoutMs = 30_000) {
    const deadline = Date.now() + timeoutMs;
    while (Date.now() < deadline) {
      const { value } = await connection.getSignatureStatuses([signature]);
      const status = value[0];
      if (status?.err) throw new Error(`failed on chain: ${JSON.stringify(status.err)}`);
      if (status?.confirmationStatus === "confirmed" || status?.confirmationStatus === "finalized") {
        return status.slot;
      }
      await new Promise((resolve) => setTimeout(resolve, 500));
    }
    return null;
  }

  if (process.argv[1] === fileURLToPath(import.meta.url)) {
    const tx = await buildTx();
    console.log(Buffer.from(tx.serialize()).toString("base64"));
  }
  ```

  ```python apex_tx.py theme={null}
  # pip install solders requests
  # Print a signed, tipped transaction as base64: python apex_tx.py
  import base64
  import itertools
  import json
  import os
  import random
  import time

  import requests
  from solders.compute_budget import set_compute_unit_limit, set_compute_unit_price
  from solders.hash import Hash
  from solders.instruction import AccountMeta, Instruction
  from solders.keypair import Keypair
  from solders.message import Message
  from solders.pubkey import Pubkey
  from solders.system_program import TransferParams, transfer
  from solders.transaction import Transaction

  APEX_RPC = os.environ.get("APEX_RPC", "http://fra.apex.orbitflare.com")
  API_KEY = os.environ.get("APEX_API_KEY", "")
  SOLANA_RPC = os.environ.get("SOLANA_RPC_URL", "https://api.mainnet-beta.solana.com")
  TIP_LAMPORTS = int(os.environ.get("TIP_LAMPORTS", "1000000"))
  MEMO_PROGRAM = Pubkey.from_string("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr")

  # One session keeps the HTTP connection to the Apex endpoint open between sends.
  session = requests.Session()
  session.headers.update({"x-api-key": API_KEY})

  with open(os.environ.get("KEYPAIR_PATH", "payer.json")) as f:
      payer = Keypair.from_bytes(bytes(json.load(f)))

  _tip_accounts = None
  _counter = itertools.count()


  def apex_rpc(method, params):
      r = session.post(APEX_RPC, json={"jsonrpc": "2.0", "id": 1, "method": method, "params": params}, timeout=5)
      body = r.json()
      if "error" in body:
          raise RuntimeError(f"{method} failed {body['error']['code']}: {body['error']['message']}")
      return body["result"]


  def solana_rpc(method, params):
      r = requests.post(SOLANA_RPC, json={"jsonrpc": "2.0", "id": 1, "method": method, "params": params}, timeout=10)
      return r.json()["result"]


  def build_tx(tip=True, label="apex"):
      """Returns a signed Transaction. Pass tip=False for a bundle member without a tip."""
      global _tip_accounts
      if _tip_accounts is None:
          _tip_accounts = apex_rpc("getTipAccounts", [])
      tip_account = Pubkey.from_string(random.choice(_tip_accounts))
      blockhash = Hash.from_string(
          solana_rpc("getLatestBlockhash", [{"commitment": "confirmed"}])["value"]["blockhash"])
      instructions = [
          set_compute_unit_limit(100_000),
          set_compute_unit_price(10_000),
          Instruction(MEMO_PROGRAM, f"{label} {time.time_ns()} {next(_counter)}".encode(),
                      [AccountMeta(payer.pubkey(), is_signer=True, is_writable=True)]),
      ]
      if tip:
          instructions.append(
              transfer(TransferParams(from_pubkey=payer.pubkey(), to_pubkey=tip_account, lamports=TIP_LAMPORTS)))
      return Transaction([payer], Message.new_with_blockhash(instructions, payer.pubkey(), blockhash), blockhash)


  def confirm(signature, timeout_s=30.0):
      """Poll getSignatureStatuses on the Solana RPC. Returns the slot, or None on timeout."""
      deadline = time.time() + timeout_s
      while time.time() < deadline:
          status = solana_rpc("getSignatureStatuses", [[signature]])["value"][0]
          if status and status.get("err"):
              raise RuntimeError(f"failed on chain: {status['err']}")
          if status and status.get("confirmationStatus") in ("confirmed", "finalized"):
              return status["slot"]
          time.sleep(0.5)
      return None


  if __name__ == "__main__":
      print(base64.b64encode(bytes(build_tx())).decode())
  ```
</CodeGroup>

For the cURL samples, sign a fresh transaction before each send:

```bash theme={null}
TX_BASE64=$(node apex-tx.mjs)      # or: TX_BASE64=$(python apex_tx.py)
```

A transaction is only valid while its blockhash is, roughly 60 to 90 seconds, so sign right before you send.

## JSON-RPC sendTransaction

`POST` to the root of the Apex endpoint. This is the standard Solana `sendTransaction` shape, so existing code moves over by swapping the URL and adding the API key.

| Param                  | Type              | Description                                                                                            |
| ---------------------- | ----------------- | ------------------------------------------------------------------------------------------------------ |
| `params[0]`            | string            | The signed transaction, base64 encoded                                                                 |
| `params[1].encoding`   | string            | `"base64"`                                                                                             |
| `params[1].maxRetries` | number, optional  | Retry budget for this transaction. Omit it to use the endpoint's default                               |
| `params[2]`            | boolean, optional | `mevProtect`. `true` skips leaders on the Shield blocklist. See [MEV protection](/apex/mev-protection) |

<CodeGroup>
  ```bash cURL theme={null}
  curl -s "$APEX_RPC" \
    -H "Content-Type: application/json" \
    -H "x-api-key: $APEX_API_KEY" \
    -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"sendTransaction\",\"params\":[\"$TX_BASE64\",{\"encoding\":\"base64\",\"maxRetries\":30},false]}"
  ```

  ```javascript JavaScript theme={null}
  import { apexRpc, buildTx, confirm } from "./apex-tx.mjs";

  const tx = await buildTx({ label: "apex json-rpc" });
  const signature = await apexRpc("sendTransaction", [
    Buffer.from(tx.serialize()).toString("base64"),
    { encoding: "base64", maxRetries: 30 },
    false, // mevProtect
  ]);
  console.log("accepted:", signature);
  console.log("landed in slot", await confirm(signature));
  ```

  ```python Python theme={null}
  import base64

  from apex_tx import apex_rpc, build_tx, confirm

  tx = build_tx(label="apex json-rpc")
  signature = apex_rpc("sendTransaction", [
      base64.b64encode(bytes(tx)).decode(),
      {"encoding": "base64", "maxRetries": 30},
      False,  # mevProtect
  ])
  print("accepted:", signature)
  print("landed in slot", confirm(signature))
  ```

  ```rust Rust theme={null}
  // `tx` is a signed, tipped VersionedTransaction, built as in the Quickstart.
  use apex_sender_client::rpc::RpcClient;
  use apex_sender_client::{serialize_transaction, Region};

  let rpc = RpcClient::new(Region::Frankfurt, &api_key);
  let wire = serialize_transaction(&tx)?;
  // mev_protect = false, max_retries = Some(30)
  let signature = rpc.send_transaction(&wire, false, Some(30)).await?;
  println!("accepted: {signature}");
  ```
</CodeGroup>

Response:

```json theme={null}
{ "jsonrpc": "2.0", "id": 1, "result": "<signature>" }
```

Apex does not simulate or run preflight checks. `skipPreflight` and `preflightCommitment` have no effect. If you want simulation, call `simulateTransaction` on your regular RPC before you send.

## Plain HTTP Routes

These routes sit beside JSON-RPC on the same host and port, and skip the JSON-RPC envelope.

| Route               | Request body                                                                    | Success reply                                      |
| ------------------- | ------------------------------------------------------------------------------- | -------------------------------------------------- |
| `POST /send`        | JSON: `{"transaction": "<base64>", "mevProtect"?: bool, "maxRetries"?: n}`      | `{"signature": "..."}`                             |
| `POST /send-bin`    | Raw transaction bytes, `Content-Type: application/octet-stream`                 | `{"signature": "..."}`                             |
| `POST /send-batch`  | Up to 16 frames, each a big-endian u16 length followed by the transaction bytes | `attempted`, `accepted`, `rejected`, and `results` |
| `POST /send-bundle` | 1 to 4 frames, same framing. See [Bundles](/apex/bundles)                       | `{"bundle_id": "...", "signatures": [...]}`        |
| `GET /ping`         | None, and no API key needed                                                     | `pong`                                             |

On the binary routes, options travel as query flags: `?mev_protect=1` and `&max_retries=N`.

Errors are JSON with an HTTP status: `{"error": "<label>", "message": "..."}`. See [Errors and rate limits](/apex/errors-and-rate-limits#plain-http-errors).

### POST /send

<CodeGroup>
  ```bash cURL theme={null}
  curl -s "$APEX_RPC/send" \
    -H "Content-Type: application/json" \
    -H "x-api-key: $APEX_API_KEY" \
    -d "{\"transaction\":\"$TX_BASE64\",\"mevProtect\":false,\"maxRetries\":30}"
  ```

  ```javascript JavaScript theme={null}
  import { APEX_RPC, API_KEY, buildTx, confirm } from "./apex-tx.mjs";

  const tx = await buildTx({ label: "apex send" });
  const res = await fetch(`${APEX_RPC}/send`, {
    method: "POST",
    headers: { "Content-Type": "application/json", "x-api-key": API_KEY },
    body: JSON.stringify({ transaction: Buffer.from(tx.serialize()).toString("base64"), mevProtect: false }),
  });
  const body = await res.json();
  if (!body.signature) throw new Error(`rejected ${res.status} ${body.error}: ${body.message}`);
  console.log("accepted:", body.signature);
  console.log("landed in slot", await confirm(body.signature));
  ```

  ```python Python theme={null}
  import base64

  from apex_tx import APEX_RPC, build_tx, confirm, session

  tx = build_tx(label="apex send")
  r = session.post(f"{APEX_RPC}/send",
                   json={"transaction": base64.b64encode(bytes(tx)).decode(), "mevProtect": False}, timeout=5)
  body = r.json()
  if "signature" not in body:
      raise RuntimeError(f"rejected {r.status_code} {body['error']}: {body['message']}")
  print("accepted:", body["signature"])
  print("landed in slot", confirm(body["signature"]))
  ```
</CodeGroup>

### POST /send-bin

The cheapest HTTP path: no base64 and no JSON on the way in. The body is the serialized transaction, byte for byte.

<CodeGroup>
  ```bash cURL theme={null}
  echo "$TX_BASE64" | base64 --decode | curl -s "$APEX_RPC/send-bin?mev_protect=0" \
    -H "Content-Type: application/octet-stream" \
    -H "x-api-key: $APEX_API_KEY" \
    --data-binary @-
  ```

  ```javascript JavaScript theme={null}
  import { APEX_RPC, API_KEY, buildTx, confirm } from "./apex-tx.mjs";

  const tx = await buildTx({ label: "apex send-bin" });
  const res = await fetch(`${APEX_RPC}/send-bin`, {
    method: "POST",
    headers: { "Content-Type": "application/octet-stream", "x-api-key": API_KEY },
    body: tx.serialize(),
  });
  const body = await res.json();
  if (!body.signature) throw new Error(`rejected ${res.status} ${body.error}: ${body.message}`);
  console.log("accepted:", body.signature);
  console.log("landed in slot", await confirm(body.signature));
  ```

  ```python Python theme={null}
  from apex_tx import APEX_RPC, build_tx, confirm, session

  tx = build_tx(label="apex send-bin")
  r = session.post(f"{APEX_RPC}/send-bin", headers={"Content-Type": "application/octet-stream"},
                   data=bytes(tx), timeout=5)
  body = r.json()
  if "signature" not in body:
      raise RuntimeError(f"rejected {r.status_code} {body['error']}: {body['message']}")
  print("accepted:", body["signature"])
  print("landed in slot", confirm(body["signature"]))
  ```

  ```rust Rust theme={null}
  // `tx` is a signed, tipped VersionedTransaction, built as in the Quickstart.
  use apex_sender_client::rpc::RpcClient;
  use apex_sender_client::{serialize_transaction, Region};

  let rpc = RpcClient::new(Region::Frankfurt, &api_key);
  let wire = serialize_transaction(&tx)?;
  let signature = rpc.send_transaction_binary(&wire, false, None).await?;
  println!("accepted: {signature}");
  ```
</CodeGroup>

### POST /send-batch

Send up to 16 independent transactions in one request. The body is a sequence of frames:

```text theme={null}
u16 big-endian   length of the transaction bytes
bytes            the serialized transaction
... repeated, up to 16 frames
```

Each transaction is admitted on its own. A batch is **not atomic**: some frames can be accepted while others are rejected. For all-or-nothing execution use [Bundles](/apex/bundles).

<CodeGroup>
  ```bash cURL theme={null}
  : > batch.bin
  for i in 1 2 3; do
    node apex-tx.mjs | base64 --decode > tx.bin
    printf '%04x' $(($(wc -c < tx.bin))) | xxd -r -p >> batch.bin   # u16 big-endian length
    cat tx.bin >> batch.bin
  done

  curl -s "$APEX_RPC/send-batch" \
    -H "Content-Type: application/octet-stream" \
    -H "x-api-key: $APEX_API_KEY" \
    --data-binary @batch.bin
  ```

  ```javascript JavaScript theme={null}
  import { APEX_RPC, API_KEY, buildTx, confirm } from "./apex-tx.mjs";

  const txs = [];
  for (let i = 0; i < 3; i++) txs.push(await buildTx({ label: `apex batch ${i}` }));

  // Each frame: u16 big-endian length, then the transaction bytes.
  const frames = txs.map((tx) => {
    const wire = Buffer.from(tx.serialize());
    const length = Buffer.alloc(2);
    length.writeUInt16BE(wire.length);
    return Buffer.concat([length, wire]);
  });

  const res = await fetch(`${APEX_RPC}/send-batch`, {
    method: "POST",
    headers: { "Content-Type": "application/octet-stream", "x-api-key": API_KEY },
    body: Buffer.concat(frames),
  });
  const body = await res.json();
  if (!body.results) throw new Error(`rejected ${res.status} ${body.error}: ${body.message}`);
  console.log(`accepted ${body.accepted} of ${body.attempted}`);
  for (const result of body.results) {
    if (result.signature) console.log(result.signature, "landed in slot", await confirm(result.signature));
    else console.log("rejected:", result.error, result.message);
  }
  ```

  ```python Python theme={null}
  import struct

  from apex_tx import APEX_RPC, build_tx, confirm, session

  txs = [bytes(build_tx(label=f"apex batch {i}")) for i in range(3)]
  # Each frame: u16 big-endian length, then the transaction bytes.
  payload = b"".join(struct.pack(">H", len(wire)) + wire for wire in txs)

  r = session.post(f"{APEX_RPC}/send-batch", headers={"Content-Type": "application/octet-stream"},
                   data=payload, timeout=5)
  body = r.json()
  if "results" not in body:
      raise RuntimeError(f"rejected {r.status_code} {body['error']}: {body['message']}")
  print(f"accepted {body['accepted']} of {body['attempted']}")
  for result in body["results"]:
      if "signature" in result:
          print(result["signature"], "landed in slot", confirm(result["signature"]))
      else:
          print("rejected:", result["error"], result["message"])
  ```

  ```rust Rust theme={null}
  // `txs` is a Vec of signed, tipped VersionedTransaction values.
  use apex_sender_client::rpc::{BatchItem, RpcClient};
  use apex_sender_client::{serialize_transaction, Region};

  let rpc = RpcClient::new(Region::Frankfurt, &api_key);
  let wires: Vec<Vec<u8>> = txs.iter().map(serialize_transaction).collect::<Result<_, _>>()?;
  let refs: Vec<&[u8]> = wires.iter().map(Vec::as_slice).collect();

  let batch = rpc.send_batch(&refs, false, None).await?;
  println!("accepted {} of {}", batch.accepted, batch.attempted);
  for item in &batch.results {
      match item {
          BatchItem::Accepted(signature) => println!("accepted: {signature}"),
          BatchItem::Rejected { error, message } => println!("rejected: {error}: {message}"),
      }
  }
  ```
</CodeGroup>

The reply is always HTTP 200 when the request itself is valid, with one result per frame, in frame order:

```json theme={null}
{
  "attempted": 3,
  "accepted": 2,
  "rejected": 1,
  "results": [
    { "signature": "<signature 1>" },
    { "signature": "<signature 2>" },
    { "error": "below_floor", "message": "..." }
  ]
}
```

### GET /ping

Returns `pong`. It needs no API key. Use it to open and warm an HTTP connection before you need it, and to keep an idle one alive. See [Best practices](/apex/best-practices#keep-connections-warm).

```bash theme={null}
curl -s "$APEX_RPC/ping"
```

### CORS

Every HTTP reply carries `Access-Control-Allow-Origin: *` and `OPTIONS` preflights are answered, so all of the routes above work from browser code. See the [browser notes](/apex/authentication#browsers) before you ship a key to a frontend.

## QUIC

QUIC is the fastest way in. You hold one persistent connection to the Apex endpoint, authenticated once by a [client certificate derived from your API key](/apex/authentication#quic-client-certificate), and open one stream per transaction. On a warm connection a send is one stream open and one write, a few microseconds of client time.

There are two kinds of stream:

| Stream             | What you get back                                                 | Use it for                                                    |
| ------------------ | ----------------------------------------------------------------- | ------------------------------------------------------------- |
| **Unidirectional** | Nothing. Fire and forget                                          | Production sends on the fastest path                          |
| **Bidirectional**  | One admission response: accepted, or a rejection code and message | Integration, debugging, and tools that want the reason inline |

### Rust

The [`apex-sender-client`](/apex/rust-client) crate implements the whole transport: the certificate, keep-alive, 0-RTT resumption, and reconnects. This program sends one transaction on each kind of stream. It uses the same `Cargo.toml` as the [Quickstart](/apex/quickstart#rust-dependencies).

```rust theme={null}
use std::str::FromStr;
use std::time::Duration;

use apex_sender_client::rpc::{RpcClient, SolanaRpc};
use apex_sender_client::{tip, tip_instruction, ApexSenderClient, Error, Region, MIN_TIP_LAMPORTS};
use solana_compute_budget_interface::ComputeBudgetInstruction;
use solana_instruction::{AccountMeta, Instruction};
use solana_keypair::{read_keypair_file, Keypair};
use solana_pubkey::Pubkey;
use solana_signer::Signer;
use solana_transaction::versioned::VersionedTransaction;
use solana_transaction::Transaction;

async fn tipped_memo(
    payer: &Keypair,
    tip_accounts: &[Pubkey],
    solana: &SolanaRpc,
    label: &str,
) -> Result<VersionedTransaction, Box<dyn std::error::Error>> {
    let nonce = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)?
        .as_nanos();
    let tip_account = tip::pick_tip_account(tip_accounts).ok_or("no tip accounts")?;
    let instructions = [
        ComputeBudgetInstruction::set_compute_unit_limit(100_000),
        ComputeBudgetInstruction::set_compute_unit_price(10_000),
        Instruction::new_with_bytes(
            Pubkey::from_str("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr")?,
            format!("{label} {nonce}").as_bytes(),
            vec![AccountMeta::new(payer.pubkey(), true)],
        ),
        tip_instruction(&payer.pubkey(), &tip_account, MIN_TIP_LAMPORTS),
    ];
    let blockhash = solana.latest_blockhash().await?;
    Ok(VersionedTransaction::from(Transaction::new_signed_with_payer(
        &instructions,
        Some(&payer.pubkey()),
        &[payer],
        blockhash,
    )))
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let api_key = std::env::var("APEX_API_KEY")?;
    let solana = SolanaRpc::new(
        std::env::var("SOLANA_RPC_URL").unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".into()),
    );
    let payer = read_keypair_file(std::env::var("KEYPAIR_PATH").unwrap_or_else(|_| "payer.json".into()))?;
    let region = Region::Frankfurt;

    let client = ApexSenderClient::connect(region, &api_key).await?;
    let tip_accounts = RpcClient::new(region, &api_key).get_tip_accounts().await?;

    // Unidirectional stream: fire and forget. Returns the signature, reads nothing back.
    let tx = tipped_memo(&payer, &tip_accounts, &solana, "apex quic uni").await?;
    let signature = client.send_transaction(&tx).await?;
    println!("sent: {signature}");
    println!("slot: {:?}", solana.confirm(&signature.to_string(), Duration::from_secs(30)).await?);

    // Bidirectional stream: the endpoint answers accepted or rejected before racing it.
    let tx = tipped_memo(&payer, &tip_accounts, &solana, "apex quic bidi").await?;
    match client.send_transaction_with_response(&tx).await {
        Ok(signature) => {
            println!("accepted: {signature}");
            println!("slot: {:?}", solana.confirm(&signature.to_string(), Duration::from_secs(30)).await?);
        }
        // Fix the transaction, not the client. The message names the floor when the tip is short.
        Err(Error::Rejected { code, message }) => println!("rejected: {code:?}: {message}"),
        Err(e) => return Err(e.into()),
    }
    Ok(())
}
```

### Admission Codes

A bidirectional stream answers with one of these:

| Code | Name             | Meaning                                                             |
| ---- | ---------------- | ------------------------------------------------------------------- |
| `0`  | ok               | Accepted. The transaction is tracked and being raced to the leaders |
| `1`  | unauthorized     | The connection's key is not valid                                   |
| `2`  | rate limited     | Your key is over its per-second limit                               |
| `3`  | invalid          | The transaction failed sanitization. The message says what failed   |
| `4`  | no tip           | No transfer to an Apex tip account was found                        |
| `5`  | below floor      | The tip is under your tier's floor. The message states the floor    |
| `6`  | busy             | The endpoint is shedding load. Retry shortly                        |
| `7`  | malformed packet | The packet framing is wrong                                         |

A rejection is final for that packet. A transport error before the response arrives is safe to retry, because the endpoint deduplicates by signature.

### QUIC from Other Languages

You can implement the transport in any language with a QUIC library that supports client certificates.

**Connection**

* QUIC (RFC 9000) with TLS 1.3 and ALPN `solana-tpu`.
* The server certificate is a self-signed placeholder. Do not verify it.
* The client must present the [certificate derived from your API key](/apex/authentication#quic-client-certificate).
* Keep one connection open per Apex endpoint. Send a QUIC PING regularly (the Rust client pings every second). The endpoint's idle timeout is 30 seconds. 0-RTT is enabled.

**Transaction packet**

Open a stream, write exactly one packet, and finish the stream:

```text theme={null}
u64 LE   length of the transaction bytes
bytes    the serialized transaction: legacy or v0 at most 1232 bytes, v1 at most 4096
u8       mev_protect: 1 to skip Shield-blocklisted leaders, else 0
u8       max_retries present: 0 or 1
u16 LE   max_retries, only if present
```

This is exactly bincode of `{ wire_transaction: Vec<u8>, mev_protect: bool, max_retry: Option<u16> }` with bincode's default fixed-int little-endian options. The whole packet must be at most 4160 bytes.

**Admission frame** (bidirectional streams only)

```text theme={null}
u8       status: one of the admission codes above
[64]     status 0 only: the transaction's first signature
u16 LE   status != 0: message length
bytes    status != 0: UTF-8 message
```

In Rust, `wire::encode_packet` and `wire::decode_admission` are the reference implementation of both frames.
