> ## 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.

# Apex Rust Client

> apex-sender-client is the Rust crate for OrbitFlare Apex: a persistent QUIC connection with 0-RTT reconnects, plus HTTP helpers for JSON-RPC and batches.

`apex-sender-client` is the reference client for Apex. It keeps one persistent QUIC connection per Apex endpoint, authenticates with a client certificate derived from your API key, sends one serialized transaction per stream, and reconnects with 0-RTT when the connection drops.

## Install

The crate is **coming to crates.io** as `apex-sender-client`. Until it is published, install it from the OrbitFlare GitHub repository:

```toml theme={null}
[dependencies]
apex-sender-client = { git = "https://github.com/orbitflare/apex-sender-client", features = ["rpc"] }
```

Once it is on crates.io:

```toml theme={null}
[dependencies]
apex-sender-client = { version = "0.1", features = ["rpc"] }
```

The `rpc` feature adds the HTTP helpers: `getTipAccounts`, JSON-RPC `sendTransaction`, the binary routes, and a blockhash and confirmation helper for any Solana RPC. The QUIC path alone has no HTTP dependency, so leave the feature off if you only need QUIC.

The crate requires Rust 1.92 or newer.

## Quick Start

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

use apex_sender_client::rpc::{RpcClient, SolanaRpc};
use apex_sender_client::{tip, tip_instruction, ApexSenderClient, Region, MIN_TIP_LAMPORTS};
use solana_signer::Signer;
use solana_transaction::versioned::VersionedTransaction;
use solana_transaction::Transaction;

let client = ApexSenderClient::connect(Region::Frankfurt, &api_key).await?;
let tip_accounts = RpcClient::new(Region::Frankfurt, &api_key).get_tip_accounts().await?;
let tip_account = tip::pick_tip_account(&tip_accounts).ok_or("no tip accounts")?;

// Your instructions, plus the tip as a top-level instruction.
let instructions = [
    your_instruction,
    tip_instruction(&payer.pubkey(), &tip_account, MIN_TIP_LAMPORTS),
];
let solana = SolanaRpc::new(solana_rpc_url);
let blockhash = solana.latest_blockhash().await?;
let tx = VersionedTransaction::from(Transaction::new_signed_with_payer(
    &instructions,
    Some(&payer.pubkey()),
    &[&payer],
    blockhash,
));

let signature = client.send_transaction(&tx).await?; // microseconds, no acknowledgement
let slot = solana.confirm(&signature.to_string(), Duration::from_secs(30)).await?;
```

For a complete program with a `Cargo.toml`, see the Rust tab of the [Quickstart](/apex/quickstart).

## Regions

`Region` names every Apex endpoint. `Region::parse("fra")` and `Region::code()` map to and from the short codes, and `Region::ALL` lists them.

| Variant                        | Code     |
| ------------------------------ | -------- |
| `Region::Frankfurt`            | `fra`    |
| `Region::Amsterdam`            | `ams`    |
| `Region::London`               | `lon`    |
| `Region::NewYork`              | `nyc`    |
| `Region::SaltLakeCity`         | `slc`    |
| `Region::Singapore`            | `sgp`    |
| `Region::Tokyo`                | `tyo`    |
| `Region::Siauliai`             | `sqq`    |
| `Region::Global` (rolling out) | `global` |

`region.quic_endpoint()` returns `<code>.apex.orbitflare.com:7001` and `region.rpc_url()` returns `http://<code>.apex.orbitflare.com`. See [Endpoints and regions](/apex/endpoints).

## Transports

|                   | QUIC unidirectional                          | QUIC bidirectional                                        | HTTP binary                                             | JSON-RPC                            |
| ----------------- | -------------------------------------------- | --------------------------------------------------------- | ------------------------------------------------------- | ----------------------------------- |
| **Call**          | `send_transaction`, `send_transaction_bytes` | `send_transaction_with_response`, `send_with_response`    | `rpc::RpcClient::send_transaction_binary`, `send_batch` | `rpc::RpcClient::send_transaction`  |
| **Returns**       | The signature. Nothing is read back          | Accepted, or a rejection code and message                 | The signature, or an error with a label                 | The signature, or a JSON-RPC error  |
| **Cost per send** | One stream on a warm connection              | One stream plus one round trip                            | An HTTP request with raw bytes                          | An HTTP request with base64 in JSON |
| **Best for**      | Bots on a persistent connection              | Integration, debugging, tools that want the reason inline | Batches                                                 | Drop-in for `sendTransaction` code  |

The transport does not change priority or routing. The tip does.

## API

| Item                                                                              | Description                                                                                                                     |
| --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `ApexSenderClient::connect(region, api_key)`                                      | Connect with an ephemeral local port and the default options                                                                    |
| `ApexSenderClient::connect_with_options(opts, api_key)`                           | Connect with [`ClientOptions`](#options)                                                                                        |
| `send_transaction(&tx)`                                                           | Serialize (legacy, v0, or v1) and send on a unidirectional stream. Returns the first signature                                  |
| `send_transaction_bytes(bytes)`                                                   | The same with bytes you already hold. Nothing re-encodes them                                                                   |
| `send_transaction_with_response(&tx)`                                             | Bidirectional stream. `Err(Error::Rejected { code, message })` on rejection                                                     |
| `send_with_response(bytes)`                                                       | The same with bytes. Returns the raw `Admission`                                                                                |
| `health()`, `reconnects_total()`, `zero_rtt_resumptions_total()`, `remote_addr()` | Connection state, for your metrics                                                                                              |
| `reconnect()`, `close()`                                                          | Lifecycle                                                                                                                       |
| `tip_instruction(payer, tip_account, lamports)`                                   | Builds the tip transfer                                                                                                         |
| `tip::pick_tip_account(&accounts)`                                                | Picks one of the tip accounts at random                                                                                         |
| `MIN_TIP_LAMPORTS`                                                                | The standard tier floor: 1,000,000 lamports                                                                                     |
| `rpc::RpcClient`                                                                  | The Apex endpoint over HTTP: `get_tip_accounts`, `send_transaction` (JSON-RPC), `send_transaction_binary`, `send_batch`, `ping` |
| `rpc::SolanaRpc`                                                                  | Any Solana RPC: `latest_blockhash`, `confirm(signature, timeout)`                                                               |
| `rpc::fetch_vaults(solana_rpc_url)`                                               | Reads the tip accounts straight from the on-chain tip program (`rpc::TIP_PROGRAM_ID`), without calling an Apex endpoint         |
| `client_pubkey(api_key)`                                                          | The certificate key your API key derives to, as shown on your dashboard                                                         |
| `serialize_transaction(&tx)`                                                      | The canonical wire bytes. The same as bincode for legacy and v0, and correct for [v1](/apex/transaction-v1)                     |
| `wire::encode_packet`, `wire::decode_admission`                                   | The QUIC wire format, as a reference for other languages                                                                        |

The client is cheap to `Clone`, and clones share one connection. Create it once and hand clones to your tasks.

<Note>
  The crate does not have a bundle helper yet. Send [bundles](/apex/bundles) with `sendBundle` or `POST /send-bundle` over HTTP.
</Note>

## Options

`ClientOptions` implements `Default`, so set only what you need:

| Field                 | Default                   | Description                                                                                        |
| --------------------- | ------------------------- | -------------------------------------------------------------------------------------------------- |
| `endpoint`            | The region's QUIC address | `host:port` of the QUIC address. Overrides the region                                              |
| `mev_protect`         | `false`                   | Skip Shield-blocklisted leaders. See [MEV protection](/apex/mev-protection)                        |
| `max_retries`         | `None`                    | Retry budget per transaction. `None` uses the endpoint's default                                   |
| `bind_addr`           | Ephemeral port            | Local UDP bind address, for firewall allowlists                                                    |
| `connect_timeout`     | 3 s                       | Handshake timeout                                                                                  |
| `send_timeout`        | 2 s                       | Timeout for one send                                                                               |
| `keep_alive`          | 1 s                       | QUIC PING interval. The endpoint's idle timeout is 30 s                                            |
| `auto_reconnect`      | `true`                    | When a send fails because the connection is gone, reconnect and send once more                     |
| `proactive_reconnect` | `true`                    | A background task re-handshakes as soon as a drop is noticed, so the next send does not pay for it |

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

use apex_sender_client::{ApexSenderClient, ClientOptions, Region};

let client = ApexSenderClient::connect_with_options(
    ClientOptions {
        endpoint: Some(Region::NewYork.quic_endpoint()),
        bind_addr: Some("0.0.0.0:47001".parse()?),
        mev_protect: true,
        max_retries: Some(30),
        send_timeout: Duration::from_secs(1),
        ..Default::default()
    },
    &api_key,
)
.await?;
```

## Keep-Alive and Reconnects

The connection stays open with a QUIC PING every second, against an idle timeout of 30 seconds on the Apex endpoint. On a warm connection a send is one stream open and one write.

If the connection drops:

* A background task notices and re-handshakes right away (`proactive_reconnect`), so the next send usually finds a live connection.
* A reconnect uses a cached session ticket and sends the waiting transaction in the handshake's first flight (**0-RTT**). If the endpoint declines the early data, the client resends it after the handshake.
* A send that fails because the connection is gone reconnects and retries once (`auto_reconnect`). Set it to `false` to handle that yourself.

`health()`, `reconnects_total()`, and `zero_rtt_resumptions_total()` show what happened. Export them to your metrics.

## Errors

| `Error` variant                                                    | Meaning                                                                                                                                   |
| ------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `Rejected { code, message }`                                       | The endpoint rejected the transaction on a bidirectional stream. See [admission codes](/apex/errors-and-rate-limits#quic-admission-codes) |
| `TooLarge(bytes)`                                                  | Over 4096 bytes. Caught locally before anything is sent                                                                                   |
| `Timeout`                                                          | The connect or send timeout passed                                                                                                        |
| `Resolve`, `Bind`, `Tls`, `Connect`, `Connection`, `Write`, `Read` | Transport failures                                                                                                                        |
| `Serialize`, `BadAdmission`                                        | Encoding problems                                                                                                                         |
| `Closed`                                                           | The client was closed                                                                                                                     |

The HTTP helpers return `rpc::RpcError`: `Http` for transport failures, `Rpc { code, message }` for a JSON-RPC or plain HTTP rejection, and `BadResponse`.

## Examples in the Repository

All examples read `APEX_API_KEY`, `KEYPAIR_PATH` (default `payer.json`), `SOLANA_RPC_URL`, and optionally `APEX_REGION`, `APEX_QUIC`, `APEX_RPC`, `TIP_LAMPORTS`, and `APEX_TX_VERSION` (`legacy` or `v1`). Each sends a tipped memo and reports the slot it landed in.

| Example                   | Shows                                                                             |
| ------------------------- | --------------------------------------------------------------------------------- |
| `quic_send`               | Unidirectional stream: the fastest path, then confirmation from a Solana RPC      |
| `quic_send_with_response` | Bidirectional stream: the accepted or rejected answer and how to read a rejection |
| `rpc_send`                | JSON-RPC `sendTransaction` over HTTP with the same tip rule                       |
| `raw_bytes`               | Pre-serialized bytes on the wire, and the exact packet layout for other languages |
| `throughput`              | One warm connection, N concurrent sends, per-send p50 and p99, landing count      |
| `client_pubkey`           | The certificate key an API key derives to, to compare with your dashboard         |
| `typescript/send_rpc.ts`  | The JSON-RPC path from `@solana/web3.js`, with no client library                  |
| `python/send.py`          | The binary route and JSON-RPC from Python with `solders`                          |

```bash theme={null}
APEX_API_KEY=... KEYPAIR_PATH=payer.json SOLANA_RPC_URL=https://... \
  cargo run --release --example quic_send --features rpc
```

Typical output on mainnet, from a client next to the Apex endpoint:

```text theme={null}
sent in 16 us
landed in slot 447831391 after 745 ms: 5gwAmfVM...
```

## What the Client Does Not Do

* Build or sign transactions, or choose your priority fee.
* Simulate or run preflight checks. Nothing between you and the leader does.
