> ## 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 TypeScript Client

> Send transactions, batches and bundles through OrbitFlare Apex from TypeScript with the ApexClient in @orbitflare/sdk. HTTP JSON-RPC only, with region failover and tip helpers.

> [!NOTE]
>
> The TypeScript Apex client is part of [`@orbitflare/sdk`](/sdk/overview), not a separate package. It talks to Apex over **HTTP JSON-RPC only**. There is no QUIC transport for TypeScript yet, because Node has no QUIC implementation that is reliable enough to send on. For the lowest-latency path over QUIC, use the [Rust client](/apex/rust-client) or the [Go client](/apex/go-client).

## Install

```bash theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
npm install @orbitflare/sdk @solana/kit @solana-program/system @solana-program/compute-budget
```

`@solana/kit` and the two program packages build and sign the transaction. The Apex client itself only needs `@orbitflare/sdk`.

Set your Apex API key, a funded keypair, and a Solana RPC endpoint for the blockhash and confirmation. The example uses OrbitFlare RPC with your license key:

```bash theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
export ORBITFLARE_APEX_API_KEY=apex_xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
export ORBITFLARE_LICENSE_KEY=ORBIT-XXXXXX-NNNNNN-NNNNNN
export KEYPAIR_PATH=payer.json
export SOLANA_RPC_URL="https://fra.rpc.orbitflare.com?api_key=$ORBITFLARE_LICENSE_KEY"
```

## Quick Start

This program sends 1,000 lamports to an address of your choice (itself by default) with the minimum tip, then waits for it to land. Save it as `send.ts` and run it with `npx tsx send.ts`.

```ts theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
import { readFileSync } from 'node:fs';

import {
  address,
  appendTransactionMessageInstructions,
  createKeyPairSignerFromBytes,
  createTransactionMessage,
  getSignatureFromTransaction,
  getTransactionEncoder,
  lamports,
  pipe,
  setTransactionMessageFeePayerSigner,
  setTransactionMessageLifetimeUsingBlockhash,
  signTransactionMessageWithSigners,
  type Blockhash,
} from '@solana/kit';
import {
  getSetComputeUnitLimitInstruction,
  getSetComputeUnitPriceInstruction,
} from '@solana-program/compute-budget';
import { getTransferSolInstruction } from '@solana-program/system';

import {
  APEX_DEFAULT_COMPUTE_UNIT_PRICE_MICRO_LAMPORTS,
  APEX_MIN_TIP_LAMPORTS,
  ApexClientBuilder,
  RpcClientBuilder,
  pickApexTipAccount,
  waitForConfirmation,
} from '@orbitflare/sdk';

async function main() {
  // Reads ORBITFLARE_APEX_API_KEY; sends over HTTP to Frankfurt.
  const apex = new ApexClientBuilder().region('fra').build();
  const solana = new RpcClientBuilder()
    .url(process.env.SOLANA_RPC_URL ?? 'https://api.mainnet-beta.solana.com')
    .build();

  const keypair = JSON.parse(readFileSync(process.env.KEYPAIR_PATH ?? 'payer.json', 'utf8'));
  const payer = await createKeyPairSignerFromBytes(Uint8Array.from(keypair));
  const to = address(process.env.TO ?? payer.address);

  // A tip to any other account is rejected, so pick from the published list.
  const tipAccount = pickApexTipAccount(await apex.getTipAccounts());
  const { blockhash, lastValidBlockHeight } = await solana.getLatestBlockhash();

  const message = pipe(
    createTransactionMessage({ version: 'legacy' }),
    (m) => setTransactionMessageFeePayerSigner(payer, m),
    (m) =>
      setTransactionMessageLifetimeUsingBlockhash(
        { blockhash: blockhash as Blockhash, lastValidBlockHeight: BigInt(lastValidBlockHeight) },
        m,
      ),
    (m) =>
      appendTransactionMessageInstructions(
        [
          getSetComputeUnitLimitInstruction({ units: 50_000 }),
          getSetComputeUnitPriceInstruction({
            microLamports: APEX_DEFAULT_COMPUTE_UNIT_PRICE_MICRO_LAMPORTS,
          }),
          getTransferSolInstruction({ source: payer, destination: to, amount: lamports(1_000n) }),
          // Exactly one top-level tip of at least 0.001 SOL.
          getTransferSolInstruction({
            source: payer,
            destination: address(tipAccount),
            amount: lamports(BigInt(APEX_MIN_TIP_LAMPORTS)),
          }),
        ],
        m,
      ),
  );
  const tx = await signTransactionMessageWithSigners(message);
  const wire = getTransactionEncoder().encode(tx);

  const sentAt = Date.now();
  const signature = await apex.sendTransactionBinary(wire);
  console.log(`accepted in ${Date.now() - sentAt} ms: ${signature}`);
  console.log(`matches the signed transaction: ${signature === getSignatureFromTransaction(tx)}`);

  // Accepted is not landed: confirm on a Solana RPC.
  const slot = await waitForConfirmation(solana, signature);
  console.log(
    slot === null
      ? 'not confirmed within 30 s'
      : `landed in slot ${slot} after ${Date.now() - sentAt} ms`,
  );
}

void main();
```

The snippets below reuse `apex`, `solana` and the signed transaction bytes (`wire`) from this program.

## Sending

Two methods send a single transaction. Both return the signature as soon as the endpoint accepts it.

| Method | Route | Body |
| - | - | - |
| `sendTransactionBinary(bytes)` | `POST /send-bin` | Raw transaction bytes. The fastest HTTP path, with nothing to encode |
| `sendTransaction(bytesOrBase64)` | JSON-RPC `sendTransaction` | Base64 inside a JSON-RPC request. Accepts bytes or a base64 string |

```ts theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
const signature = await apex.sendTransaction(wire, { mevProtect: true, maxRetries: 10 });
```

Both take optional `mevProtect` (skip Shield-blocklisted leaders, see [MEV protection](/apex/mev-protection)) and `maxRetries` (cap how many times Apex resubmits), overriding the builder defaults for that call. A transaction must be between 1 and 4,096 bytes (`APEX_MAX_TRANSACTION_SIZE`).

## Batches

`sendBatch` sends up to 16 transactions (`APEX_MAX_BATCH`) in one request. Each transaction is checked and raced on its own, so one bad transaction does not stop the others.

```ts theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
const batch = await apex.sendBatch([wire1, wire2, wire3]);
console.log(`${batch.accepted} of ${batch.attempted} accepted`);
for (const item of batch.results) {
  console.log(item.accepted ? item.signature : `${item.error}: ${item.message}`);
}
```

## Bundles

`sendBundle` sends 1 to 4 transactions (`APEX_MAX_BUNDLE`) that land together or not at all, through Jito leaders only. Exactly one transaction in the bundle carries the tip.

```ts theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
const { bundleId, signatures } = await apex.sendBundle([wire1, wire2]);
const [status] = await apex.getBundleStatuses([bundleId]);
console.log(status?.state, status?.landedSlot);
```

`state` is `Pending`, `Landed`, `Failed` or `Invalid`. Bundle statuses can lag behind the chain, so confirm the bundle's `signatures` with `waitForConfirmation` rather than waiting on the status alone. See [Bundles](/apex/bundles) for the rules and rate limits.

## Tips and Confirmation

| Helper | What it does |
| - | - |
| `apex.getTipAccounts()` | The tip accounts the endpoint accepts |
| `pickApexTipAccount(accounts)` | One of them at random, to spread write locks |
| `APEX_MIN_TIP_LAMPORTS` | The standard tier's tip floor: 1,000,000 lamports (0.001 SOL) |
| `APEX_DEFAULT_COMPUTE_UNIT_PRICE_MICRO_LAMPORTS` | A sensible default priority fee: 10,000 micro-lamports per compute unit |
| `fetchApexVaults(rpc)` | Reads the tip program's vault accounts from chain. This is not the tip list: it includes unpublished vaults the endpoint rejects. Use `getTipAccounts()` for accounts to tip, and this only to check that a given account is a real vault |
| `waitForConfirmation(rpc, signature, { timeoutMs, pollMs })` | Polls until the transaction is confirmed. Returns the slot, `null` on timeout (30 s by default), and throws if it landed and failed |

The tip rules are on the [Tips](/apex/tips) page.

## Regions and Failover

```ts theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
const apex = new ApexClientBuilder()
  .region('fra')
  .fallbackRegion('ams')
  .fallbackRegion('lon')
  .build();
```

When a request to the primary fails, the client tries the fallbacks in order. Region codes are `fra`, `ams`, `dub`, `lon`, `nyc`, `slc`, `sgp`, `tyo`, `sqq` and `global` (the nearest healthy endpoint). With no region set, the client uses `ORBITFLARE_APEX_URL`, then `ORBITFLARE_APEX_REGION`, then Frankfurt. See [Endpoints](/apex/endpoints).

## Options

| Builder method | Default | What it does |
| - | - | - |
| `.apiKey(key)` | `ORBITFLARE_APEX_API_KEY` | Your Apex API key, sent as the `x-api-key` header |
| `.region(code)` / `.url(url)` | Frankfurt | The primary endpoint |
| `.fallbackRegion(code)` / `.fallbackUrl(url)` | None | Endpoints to try when the primary fails |
| `.mevProtect(enabled)` | `false` | Skip Shield-blocklisted leaders for every send. See [MEV protection](/apex/mev-protection) |
| `.maxRetries(n)` | Apex default | How many times Apex resubmits each transaction |
| `.retry(policy)` | 1 attempt per endpoint | Client-side retries of a failed request on the same endpoint, with backoff |
| `.timeoutMs(ms)` | `5000` | Per-request timeout |

By default the client makes one attempt per endpoint and moves straight on to the next fallback instead of retrying. That is deliberate: resending a transaction Apex already accepted wastes your rate limit, and Apex keeps resubmitting accepted transactions itself.

## Keep the Connection Warm

`apex.ping()` calls `GET /ping` and resolves when the endpoint answers `pong`. Calling it every few seconds keeps the HTTP connection open, so the next send skips the TCP handshake.

```ts theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
setInterval(() => apex.ping().catch(() => {}), 5_000);
```

## Errors

Every failure is an `OrbitflareError` with a `kind`:

| `kind` | Meaning |
| - | - |
| `config` | Missing API key, unknown region, or a transaction, batch or bundle outside the size limits. Nothing was sent |
| `rpc` | Apex answered a JSON-RPC call with an error, such as a short tip or an invalid transaction. The code and message are on the error |
| `apex` | One of the binary routes (`/send-bin`, `/send-batch`, `/send-bundle`) rejected the request. The HTTP status, label and message are on the error |
| `rateLimited` | Over your key's per-second limit (HTTP 429). `retryAfterMs` says how long to wait when Apex provides it |
| `timeout` | No answer within `timeoutMs` |
| `transport` | The request failed on every endpoint tried, or the endpoint returned a server error |

The Apex error codes and rate limits are listed in [Errors and rate limits](/apex/errors-and-rate-limits).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.