# MEV bundles over the BDN

> Submit up to five ordered, atomic Solana transactions as a Jito bundle, place the tip transfer inside the bundle, and trace a bundle id back to a landed slot.

A **bundle** is an ordered list of signed transactions that the block engine
either lands together, in the order you gave, or not at all. The single-submit
paths (BDN fan-out, direct TPU) treat each transaction independently: they can
land in any order, in different slots, or partially. A bundle removes that
uncertainty.

The console's **MEV bundles** screen submits through the FirstByte BDN. The
call path is `bdn.submitBundle` → `mod_bdn` → the Jito block engine. You sign
the transactions yourself; FirstByte forwards the already-signed payload
without modifying it or reordering it.

## When to use a bundle instead of a single submit

Use a bundle when the correctness of transaction *n* depends on transaction
*n-1* having executed in the same slot — the classic cases being a back-run, a
sandwich, a liquidation paired with the swap that funds it, or any sequence
where a partial fill is worse than no fill.

Use a single submit (BDN fan-out or direct TPU) when each transaction stands on
its own and you only care about inclusion latency. A bundle is the wrong tool
there: it adds the tip transaction and the all-or-nothing constraint for no
benefit.

## Building the bundle payload

The bundle builder takes **one base64-encoded signed transaction per line**.
Blank lines are ignored. Line order is the execution order — the first line
executes first.

```
AYd3k9…tx1base64==
AXm2p0…tx2base64==
AQr7s5…tipTransferBase64==
```

The console validates the textarea before it will submit, and refuses to send
until every rule holds:

| Rule | Message shown |
| ---- | ------------- |
| At least one non-blank line | `Paste at least one signed transaction` |
| No more than five non-blank lines | `A bundle can include at most 5 transactions` |
| Every non-blank line decodes as base64 | `Line N is not valid base64 — paste one base64 signed transaction per line` |

The base64 check runs per line and reports the **first** offending line by its
position in the textarea, counting blank lines, so the number matches what you
see in the editor. A line fails if it contains characters outside
`A–Z a–z 0–9 + / =`, or if it matches that character set but still does not
decode.

The live counter under the textarea shows `txs N / 5` and turns red once you
pass five lines.

Before submitting, the console joins the non-blank, trimmed lines back together
with newlines. Leading and trailing whitespace on each line is therefore
harmless; whitespace *inside* a base64 payload is not, because it breaks the
character check.

## Tipping

There is no separate tip field, and no tip parameter on the submit call.
**Include the Jito tip transfer as one of the transactions in the signed bundle
itself.** The BDN forwards the bundle atomically, so the tip transfer lands or
fails with the rest of the bundle.

Because the tip is just another transaction, it counts against the
five-transaction limit: a bundle with a tip has at most four transactions of
your own logic.

Where you place the tip line is your decision, and it is a real one — the tip
executes in the position you put it in. A tip on the last line is only paid if
the preceding transactions all succeed, which is the usual reason to put it
there.

The **Recent bundles** table reads the tip back out of each bundle and renders
it in SOL, converted from lamports.

## Submitting and reading the bundle id

Submission is a single mutation:

```ts
const submit = trpc.submitBundle.useMutation();

submit.mutate({
  orgId,                      // the active org in the console
  body: txLines.join("\n"),   // base64 signed txs, one per line
  cluster: "mainnet",
});
```

| Input | Meaning |
| ----- | ------- |
| `orgId` | The org the bundle is billed and scoped to. The console takes this from the active org and disables the submit button while it is empty. |
| `body` | The newline-joined base64 transactions, in execution order. |
| `cluster` | The target cluster. The console sends `mainnet`. |

The response is read as follows:

- Anything where `ok` is not `false` is treated as accepted. If the response
  carries a `bundleId`, the console shows it (truncated) next to a live
  `submitted` pill; if it does not, it shows `bundle accepted`.
- If `ok` is `false`, the console surfaces `err` as the failure reason, falling
  back to `bundle submit failed`.
- A transport-level failure surfaces the thrown error's `message`, falling back
  to `request failed`.

Keep the `bundleId`. It is the handle that links this submission to the row that
later appears in **Recent bundles**, and therefore to the slot the bundle landed
in.

While the mutation is pending the button reads **Landing…**, so treat the
in-flight window as "outcome not yet known" rather than "not submitted".

Signing in on the portal is what enables the submit button in the console. The
API itself accepts your key regardless of portal session state — see the
API keys screen for the direct call.

## Bundle states and the slot column

**Recent bundles** lists bundles submitted via the BDN for the org, with one row
per bundle:

| Column | Source |
| ------ | ------ |
| `Bundle` | The bundle id returned at submit time. |
| `Txs` | The number of transactions in the bundle. |
| `Tip ◎` | The bundle's tip, converted from lamports to SOL. |
| `State` | The bundle's current state, colour-coded by status. |
| `Leader` | The leader the bundle was forwarded to. |
| `Slot` | The slot the bundle landed in, or `—` when there is no slot yet. |

`landed` is the terminal success state, and the only one the table marks as
live. A row in any other state has no landed slot, so its `Slot` cell shows
`—`: the slot is populated precisely when the block engine has included the
bundle. That makes the pairing of `State` and `Slot` the thing to read — a
bundle id with a slot is on-chain, a bundle id without one is not.

A bundle that does not land keeps its all-or-nothing property: none of its
transactions executed, including the tip transfer. You are free to rebuild and
resubmit, but do so with fresh signed transactions rather than reusing the same
payload blindly, since blockhash validity and account state both move on.

Failures that happen *before* the block engine sees the bundle never produce a
row here at all — they come back inline on the submit call as `ok: false` with
an `err`, or as a request error.

## Limits and cluster scope

- **Five transactions per bundle, inclusive of the tip transfer.** The console
  blocks a sixth line client-side.
- **One base64 signed transaction per line.** FirstByte does not sign,
  re-encode, or reorder anything in the payload.
- **Atomic and ordered.** The bundle lands whole, in your line order, or not at
  all.
- **The console submits to `mainnet`.** The `cluster` field is what selects the
  target, so a different cluster is a different value on the same call rather
  than a different endpoint.

Bundle-level counters and per-bundle history are scoped to the org in `orgId`;
switching the active org switches the **Recent bundles** list with it.

## Related

- [Authentication](/concepts/authentication) — API keys for direct
  `submitBundle` calls from your backend
- [Telemetry](/platform/telemetry) — metrics and traces for submission paths
