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

> Send atomic Solana bundles of 1 to 4 transactions through OrbitFlare Apex with sendBundle or POST /send-bundle, and track them with getInflightBundleStatuses.

## What a Bundle Is

A bundle is a group of **1 to 4 transactions** that execute **in order** and **all or nothing**. Either every transaction in the bundle lands in the same block, in the order you gave, or none of them do.

Use a bundle when a later transaction only makes sense if an earlier one succeeded, for example a setup transaction followed by a swap.

<Warning>
  **Bundles only land on Jito-enabled leaders.** A group of transactions cannot be kept atomic on the stake-weighted or direct TPU paths, so bundles go to the block engine path only. When the current leader does not run Jito, the bundle waits for the next one that does. For a single transaction, a regular [send](/apex/sending-transactions) uses all three paths and lands sooner.
</Warning>

## Rules

| Rule            | Detail                                                                                                                                                  |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Size**        | 1 to 4 transactions                                                                                                                                     |
| **Order**       | Executed in the order given                                                                                                                             |
| **Atomicity**   | All or nothing                                                                                                                                          |
| **Tip**         | **Exactly one** transaction in the bundle carries the Apex tip, at or above your tier's floor                                                           |
| **Member size** | Each transaction is limited to 1232 bytes. [Transaction v1](/apex/transaction-v1) sizes do not apply inside a bundle                                    |
| **Rate**        | 2 bundles per second per API key, with a short burst of 4, on top of your transaction limit. Over it you get HTTP `429` or `-32029`                     |
| **Status**      | `getInflightBundleStatuses` needs the API key that sent the bundle and takes up to 100 ids. A bundle sent with another key reads as `Invalid`           |
| **Retries**     | Apex resubmits the bundle, under the same bundle id, until it lands, the **first** transaction's blockhash expires, or the resubmission budget runs out |

The tip follows the same [tip rule](/apex/tips) as a single send: one top-level SystemProgram transfer to a published tip account, funded by a signer, with the tip account in the static keys. That tip minus the 5,000 lamport base fee becomes the Jito bid for the whole bundle. Because the bundle is atomic, the tip is only paid if the entire bundle lands.

## JSON-RPC sendBundle

| Param                | Type      | Description                                                                             |
| -------------------- | --------- | --------------------------------------------------------------------------------------- |
| `params[0]`          | string\[] | 1 to 4 signed transactions in execution order, all in the encoding named by `params[1]` |
| `params[1].encoding` | string    | `"base64"` or `"base58"`                                                                |

The result is a **bundle id**. Keep it to query the bundle's status. `sendBundle` also accepts `{"encoding": "base58"}` for base58 encoded transactions. The examples below use base64.

<CodeGroup>
  ```bash cURL theme={null}
  # Two signed transactions from the helper in Sending transactions.
  # Exactly one of them may carry the tip, so build the first one without it.
  TX1=$(node -e 'import("./apex-tx.mjs").then(async (m) => console.log(Buffer.from((await m.buildTx({ tip: false, label: "apex bundle 1" })).serialize()).toString("base64")))')
  TX2=$(node -e 'import("./apex-tx.mjs").then(async (m) => console.log(Buffer.from((await m.buildTx({ tip: true, label: "apex bundle 2" })).serialize()).toString("base64")))')

  BUNDLE_ID=$(curl -s "$APEX_RPC" \
    -H "Content-Type: application/json" \
    -H "x-api-key: $APEX_API_KEY" \
    -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"sendBundle\",\"params\":[[\"$TX1\",\"$TX2\"],{\"encoding\":\"base64\"}]}" \
    | jq -r '.result // .error')
  echo "bundle id: $BUNDLE_ID"

  curl -s "$APEX_RPC" \
    -H "Content-Type: application/json" \
    -H "x-api-key: $APEX_API_KEY" \
    -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"getInflightBundleStatuses\",\"params\":[[\"$BUNDLE_ID\"]]}"
  ```

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

  // Exactly one transaction carries the tip.
  const txs = [
    await buildTx({ tip: false, label: "apex bundle 1" }),
    await buildTx({ tip: true, label: "apex bundle 2" }),
  ];

  const bundleId = await apexRpc("sendBundle", [
    txs.map((tx) => Buffer.from(tx.serialize()).toString("base64")),
    { encoding: "base64" },
  ]);
  console.log("bundle id:", bundleId);

  // Poll until the bundle settles.
  for (;;) {
    const { value } = await apexRpc("getInflightBundleStatuses", [[bundleId]]);
    const { status, landed_slot } = value[0];
    if (status === "Landed") { console.log("landed in slot", landed_slot); break; }
    if (status === "Failed" || status === "Invalid") { console.log("bundle", status); break; }
    await new Promise((resolve) => setTimeout(resolve, 1000));
  }
  ```

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

  from apex_tx import apex_rpc, build_tx

  # Exactly one transaction carries the tip.
  txs = [
      build_tx(tip=False, label="apex bundle 1"),
      build_tx(tip=True, label="apex bundle 2"),
  ]

  bundle_id = apex_rpc("sendBundle", [
      [base64.b64encode(bytes(tx)).decode() for tx in txs],
      {"encoding": "base64"},
  ])
  print("bundle id:", bundle_id)

  # Poll until the bundle settles.
  while True:
      entry = apex_rpc("getInflightBundleStatuses", [[bundle_id]])["value"][0]
      if entry["status"] == "Landed":
          print("landed in slot", entry["landed_slot"])
          break
      if entry["status"] in ("Failed", "Invalid"):
          print("bundle", entry["status"])
          break
      time.sleep(1)
  ```
</CodeGroup>

The JavaScript and Python samples import the helper from [Sending transactions](/apex/sending-transactions#build-a-signed-tipped-transaction).

Response:

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

## POST /send-bundle

The binary route takes the same framing as [`/send-batch`](/apex/sending-transactions#post-/send-batch): for each transaction, a big-endian u16 length followed by the raw transaction bytes, 1 to 4 frames in execution order.

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

  const txs = [
    await buildTx({ tip: false, label: "apex bundle 1" }),
    await buildTx({ tip: true, label: "apex bundle 2" }),
  ];

  // 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-bundle`, {
    method: "POST",
    headers: { "Content-Type": "application/octet-stream", "x-api-key": API_KEY },
    body: Buffer.concat(frames),
  });
  const body = await res.json();
  if (!body.bundle_id) throw new Error(`rejected ${res.status} ${body.error}: ${body.message}`);
  console.log("bundle id:", body.bundle_id);
  console.log("signatures:", body.signatures);
  ```

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

  from apex_tx import APEX_RPC, build_tx, session

  txs = [
      bytes(build_tx(tip=False, label="apex bundle 1")),
      bytes(build_tx(tip=True, label="apex bundle 2")),
  ]
  # 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-bundle", headers={"Content-Type": "application/octet-stream"},
                   data=payload, timeout=5)
  body = r.json()
  if "bundle_id" not in body:
      raise RuntimeError(f"rejected {r.status_code} {body['error']}: {body['message']}")
  print("bundle id:", body["bundle_id"])
  print("signatures:", body["signatures"])
  ```
</CodeGroup>

Response:

```json theme={null}
{
  "bundle_id": "<bundle id>",
  "signatures": ["<signature of transaction 1>", "<signature of transaction 2>"]
}
```

Errors use the same format and status codes as the other [plain HTTP routes](/apex/errors-and-rate-limits#plain-http-errors). A bundle refused as a whole comes back with the `bundle` label.

## Track a Bundle

Call `getInflightBundleStatuses` with an array of bundle ids:

```json theme={null}
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "getInflightBundleStatuses",
  "params": [["<bundle id>"]]
}
```

The result carries one entry per bundle id, in the order you asked:

```json theme={null}
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "context": { "slot": 367112345 },
    "value": [
      { "bundle_id": "<bundle id>", "status": "Landed", "landed_slot": 367112344 }
    ]
  }
}
```

`landed_slot` is the slot the bundle landed in, or `null` when it has not landed. `status` is one of four states:

| Status    | Meaning                                                                                                      |
| --------- | ------------------------------------------------------------------------------------------------------------ |
| `Pending` | Apex holds the bundle and is still submitting it                                                             |
| `Landed`  | The bundle landed. `landed_slot` holds the slot                                                              |
| `Failed`  | The bundle did not land before the first transaction's blockhash expired, or the resubmission budget ran out |
| `Invalid` | The bundle id is unknown to this Apex endpoint, or is older than five minutes                                |

The bundle id stays the same while Apex resubmits, so keep polling the id you got when you sent the bundle.

Ask the same Apex endpoint you sent the bundle to. Bundle ids are not shared between endpoints.

Since a bundle is atomic, you can also confirm it like any transaction: once any of its signatures shows up as confirmed in `getSignatureStatuses` on a Solana RPC, the whole bundle landed.

## Bundle or Batch?

|                       | Bundle                   | [Batch](/apex/sending-transactions#post-/send-batch) |
| --------------------- | ------------------------ | ---------------------------------------------------- |
| **Transactions**      | 1 to 4                   | Up to 16                                             |
| **Atomic**            | Yes                      | No, each is independent                              |
| **Ordered**           | Yes                      | No                                                   |
| **Tip**               | One for the whole bundle | One per transaction                                  |
| **Paths**             | Jito block engine only   | All three                                            |
| **Member size limit** | 1232 bytes               | 1232 bytes, or 4096 for v1                           |
