# Jito bundles

The Openfort Solana Paymaster can submit **Jito bundles** — a group of up to 5 transactions that execute **atomically and in order** (all land in the same slot, or none do). Openfort's fee payer covers the SOL base fees **and** the Jito tip, while the user reimburses Openfort in an SPL token (e.g. USDC) inside the bundle. The user never needs to hold SOL.

Use bundles when you need several transactions to land together with all-or-nothing semantics — for example a multi-step swap, a mint-then-transfer, or any sequence where a partial execution would be unsafe.

## Overview

A sponsored bundle works as follows:

1. Your application builds 1–5 transactions with **Openfort's fee payer** as the transaction fee payer, leaving that signature slot for Openfort to fill.
2. One transaction carries the **Jito tip** — a SOL transfer from Openfort's fee payer to a Jito tip account — plus a single **SPL payment** that reimburses Openfort for the aggregate fee **and** the tip.
3. Your application estimates the aggregate bundle fee with [`estimateBundleFee`](/docs/products/infrastructure/paymaster/solana/endpoints#estimatebundlefee), sizes the payment, and submits with [`signAndSendBundle`](/docs/products/infrastructure/paymaster/solana/endpoints#signandsendbundle).
4. Openfort validates the bundle against your fee sponsorship policy, co-signs each transaction as the fee payer, and forwards the bundle to Jito's block engine.

:::info[Requirements]
* **Mainnet only.** Jito bundles run on `mainnet-beta`; they are not available on `devnet`.
* **SPL fee model.** The user reimburses Openfort in an allowed SPL paid token — check [`getSupportedTokens`](/docs/products/infrastructure/paymaster/solana/endpoints#getsupportedtokens). Native-SOL fee payment is not used for bundles.
* **A `sponsorSolTransaction` policy** must be enabled on your project (project-scoped auto-discovery, same as single transactions). Create one in the [Dashboard](https://dashboard.openfort.io) or via the [Policy Engine](/docs/configuration/policies).
:::

## How the tip and payment work

Jito runs an auction: a bundle's **tip** is its bid, and a competitive tip is what gets the bundle included. Openfort's fee payer pays the tip in SOL, so from the user's perspective it is sponsored — but the user must reimburse it. Two rules follow:

* **Size the tip from the live tip floor.** Jito publishes recent landed-tip percentiles at [`https://bundles.jito.wtf/api/v1/bundles/tip_floor`](https://docs.jito.wtf/lowlatencytxnsend/). Bidding a percentile of recently-landed tips (rather than a fixed minimum) is what determines whether your bundle lands.
* **`estimateBundleFee` excludes the tip.** The estimate covers the Kora fee only. Because Openfort's payment validation requires the SPL reimbursement to cover **fee + tip**, convert the tip to token units using Kora's own rate (`fee_in_token / fee_in_lamports`), add it, and apply a small buffer so price movement between estimate and validation can't fail the bundle.

Each transaction in the bundle must be **byte-unique** (Jito rejects a bundle containing duplicate transactions) — add a per-transaction memo or vary the instructions so no two are identical.

## Full SDK example

This example builds a 3-transaction bundle, places the Jito tip and a single USDC reimbursement on the last transaction, prices it with `estimateBundleFee`, and submits it with `signAndSendBundle` using the [`@solana/kora`](/docs/products/infrastructure/paymaster/solana) client:

```typescript
import { KoraClient } from "@solana/kora";
import {
  address,
  createNoopSigner,
  getBase58Encoder,
  getBase64EncodedWireTransaction,
  createKeyPairSignerFromBytes,
  partiallySignTransactionMessageWithSigners,
  pipe,
  createTransactionMessage,
  setTransactionMessageFeePayerSigner,
  setTransactionMessageLifetimeUsingBlockhash,
  appendTransactionMessageInstruction,
  appendTransactionMessageInstructions,
  type Blockhash,
} from "@solana/kit";
import { getAddMemoInstruction } from "@solana-program/memo";
import { getTransferSolInstruction } from "@solana-program/system";
import {
  findAssociatedTokenPda,
  getTransferInstruction,
  TOKEN_PROGRAM_ADDRESS,
} from "@solana-program/token";

const CONFIG = {
  rpcUrl: "https://api.openfort.io/rpc/solana/mainnet-beta",
  bundleSize: 3,
  // Allowed SPL paid token the user reimburses Openfort in (USDC mainnet).
  paymentTokenMint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
  // Overpay the estimate slightly so price movement can't fail the bundle.
  paymentBufferPct: 15,
};

// One of Jito's tip accounts; the block engine accepts a tip to any of them.
const JITO_TIP_ACCOUNT = "96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5";

const client = new KoraClient({
  rpcUrl: CONFIG.rpcUrl,
  apiKey: "Bearer {{PUBLISHABLE_KEY}}",
});

// Size the Jito tip (lamports) from the live tip floor.
async function fetchTipLamports(): Promise<bigint> {
  try {
    const res = await fetch("https://bundles.jito.wtf/api/v1/bundles/tip_floor");
    const [floor] = await res.json();
    const sol = floor.landed_tips_75th_percentile as number;
    return BigInt(Math.max(1_000, Math.round(sol * 1_000_000_000))); // >= Jito's 1000-lamport minimum
  } catch {
    return 1_000n; // fallback minimum — may not win the auction
  }
}

export async function sendSponsoredBundle(userSecret: string) {
  const user = await createKeyPairSignerFromBytes(getBase58Encoder().encode(userSecret));

  // 1. Resolve Openfort's fee payer + payment address, a blockhash, and the tip.
  const { signer_address, payment_address } = await client.getPayerSigner();
  const { blockhash } = await client.getBlockhash();
  const tipLamports = await fetchTipLamports();
  const feePayer = createNoopSigner(address(signer_address));

  // Build one bundle transaction: a per-index memo (keeps each tx byte-unique)
  // plus any extra instructions. Openfort's fee payer is the (unsigned) fee payer.
  const buildTx = async (index: number, extras: Parameters<typeof appendTransactionMessageInstructions>[0]) => {
    const message = pipe(
      createTransactionMessage({ version: 0 }),
      (tx) => setTransactionMessageFeePayerSigner(feePayer, tx),
      (tx) => setTransactionMessageLifetimeUsingBlockhash(
        { blockhash: blockhash as Blockhash, lastValidBlockHeight: 0n }, tx),
      (tx) => appendTransactionMessageInstruction(
        getAddMemoInstruction({ memo: `bundle tx #${index + 1}`, signers: [user] }), tx),
      (tx) => appendTransactionMessageInstructions(extras, tx),
    );
    return getBase64EncodedWireTransaction(await partiallySignTransactionMessageWithSigners(message));
  };

  // 2. Build the bundle. The LAST transaction carries the Jito tip.
  const tipIx = getTransferSolInstruction({
    source: feePayer,
    destination: address(JITO_TIP_ACCOUNT),
    amount: tipLamports,
  });
  const transactions: string[] = [];
  for (let i = 0; i < CONFIG.bundleSize; i++) {
    transactions.push(await buildTx(i, i === CONFIG.bundleSize - 1 ? [tipIx] : []));
  }

  // 3. Estimate the aggregate fee (EXCLUDES the tip).
  const estimate = await client.estimateBundleFee({
    transactions,
    fee_token: CONFIG.paymentTokenMint,
    signer_key: signer_address,
  });

  // 4. Price fee + tip in token units, apply the buffer, and rebuild the last
  //    transaction with the tip AND the USDC reimbursement to Openfort.
  const tokenPerLamport = estimate.fee_in_token! / estimate.fee_in_lamports;
  const tipInToken = Math.ceil(Number(tipLamports) * tokenPerLamport);
  const paymentAmount = BigInt(
    Math.ceil((estimate.fee_in_token! + tipInToken) * (1 + CONFIG.paymentBufferPct / 100)),
  );

  const [userAta] = await findAssociatedTokenPda({
    owner: user.address, mint: address(CONFIG.paymentTokenMint), tokenProgram: TOKEN_PROGRAM_ADDRESS,
  });
  const [paymentAta] = await findAssociatedTokenPda({
    owner: address(payment_address), mint: address(CONFIG.paymentTokenMint), tokenProgram: TOKEN_PROGRAM_ADDRESS,
  });
  const paymentIx = getTransferInstruction({
    source: userAta, destination: paymentAta, authority: user, amount: paymentAmount,
  });
  transactions[CONFIG.bundleSize - 1] = await buildTx(CONFIG.bundleSize - 1, [tipIx, paymentIx]);

  // 5. Openfort co-signs every transaction as fee payer and submits to Jito.
  const { bundle_uuid, signed_transactions } = await client.signAndSendBundle({
    transactions,
    signer_key: signer_address,
  });

  return { bundle_uuid, childCount: signed_transactions.length };
}
```

### Key concepts

1. **Openfort as fee payer.** Set Openfort's `signer_address` (from [`getPayerSigner`](/docs/products/infrastructure/paymaster/solana/endpoints#getpayersigner)) as each transaction's fee payer with a no-op signer, so Openfort fills that signature slot when it co-signs.
2. **Unique transactions.** Give each transaction a distinct instruction (a per-index memo above) — Jito rejects bundles with duplicate transactions.
3. **Tip + reimbursement.** One transaction carries the Jito tip (SOL, from Openfort's fee payer) and one SPL payment covering `fee + tip + buffer` from the user to Openfort's payment ATA.
4. **Estimate excludes the tip.** Add the tip (converted to token units via Kora's rate) to the estimate before applying the buffer, or the bundle fails validation with insufficient payment.

## Confirmation and landing

`signAndSendBundle` returns a `bundle_uuid` as soon as Jito **accepts** the bundle into the auction — this is not the same as the bundle **landing** on-chain. Landing depends on the auction (your tip vs. current competition) and is outside Openfort's control.

Track the final outcome the same way you would an unconfirmed single transaction:

* The child signatures are recoverable from the returned `signed_transactions` (decode and read the fee-payer signature), and can be polled on a Solana RPC.
* Openfort records the bundle and emits the [`solana_transaction.successful` / `solana_transaction.failed` webhooks](/docs/configuration/webhooks) once the bundle's transactions land or fail.

A `bundle_uuid` means "accepted for the auction", not "landed". If a bundle isn't landing, raise your tip (bid a higher tip-floor percentile) — a min-tip bundle frequently loses under competition.

## Next steps

* [`estimateBundleFee`](/docs/products/infrastructure/paymaster/solana/endpoints#estimatebundlefee) and [`signAndSendBundle`](/docs/products/infrastructure/paymaster/solana/endpoints#signandsendbundle) endpoint reference
* [Solana Paymaster overview](/docs/products/infrastructure/paymaster/solana) — single-transaction fee sponsorship
* [Error reference](/docs/products/infrastructure/paymaster/solana/errors) — common bundle and sponsorship errors
