Skip to main content

What are Jito Bundles and How to Use Them?

Created on
Updated on
Dec 5, 2024

22 min read

Overview

JM! As demand for Solana block space increases, so does demand to ensure your transactions are included in a block. Additionally, many applications require the ability to atomically and sequentially execute multiple transactions. Jito Labs, a builder of high-performance MEV infrastructure for Solana, operates a Solana validator client that enables a unique feature called Bundles, which makes this possible. In this guide, we will discuss what Jito Bundles are and how to use the Lil' JIT marketplace add-on to bundle transactions and send them to the Jito Validator client.

Let's jump in!

Prefer a video walkthrough? Follow along with Sahil and learn about Jito bundles and how to send one.
Subscribe to our YouTube channel for more videos!

What You Will Do

  • Install the Lil' JIT marketplace add-on
  • Write a script using TyepScript and Solana Web3.js 2.0
  • Create a bundle of 5 transactions
  • Send the Bundle to the Jito Validator client
  • Verify the transactions are included in a block and processed in the correct order

What You Will Need

What are Jito Bundles?

Jito Bundles are a feature provided by Jito Labs that enables sequential and atomic execution of multiple transactions on the Solana blockchain. This feature is particularly useful for complex operations, efficient MEV (Miner Extractable Value) capture, and certain DeFi applications. To understand their significance, let's first review some key concepts:

  • Individual Solana transactions are atomic: all instructions within a transaction either execute successfully or fail together.
  • However, multiple transactions sent separately are not atomic: some may succeed while others fail. This can be problematic for many applications; for example, you may only want a transaction to land only if another transaction that impacts a user's position also succeeds.
  • Some complex transactions are not executable on Solana because they include multiple compute-intensive instructions that exceed the run-time's compute budget (currently 1.4M Compute Units per transaction)
  • Occasionally, network congestion can cause transactions to fail and not land in a given block (ref: Guide: How to Optimize Solana Transactions).

Jito Bundles address all of these issues by allowing multiple transactions to be bundled together and ensuring that all transactions in the Bundle either succeed or fail as a unit. A Jito Bundle is a group of up to five Solana transactions that are executed sequentially and atomically within the same block by Jito validators, ensuring that either all transactions in the Bundle succeed or none are processed. Bundles are prioritized based on a user-defined tip amount, incentivizing validators to process bundles ahead of other transactions.

Key Characteristics of Jito Bundles

CharacteristicDescription
Sequential ExecutionTransactions in a bundle are guaranteed to execute in the order they are listed
Atomic ExecutionAll transactions in a bundle execute within the same slot
All-or-Nothing OutcomeIf any transaction in a bundle fails, none of the transactions are committed to the chain
Bundle SizeMaximum of 5 transactions per Bundle (enabling complex operations exceeding the 1.4M Compute Units per transaction limit)

How Jito Bundles Work

  1. The user creates and signs transacitons
  2. The user bundles transactions together with a tip instruction to the last transaction
  3. Users send bundles to Jito's Block Engine
  4. Block Engine forwards bundles to validators running Jito
  5. Jito-Solana features a specialized BundleStage for executing bundles
  6. Bundles are processed only when a Jito-Solana leader is producing blocks

Use Cases for Jito Bundles

  • MEV Arbitrage: Execute atomic arbitrage by bundling user transactions with arbitrage transactions
  • Liquidations: Bundle oracle update transactions with liquidation transactions
  • Batching Complex Operations: Overcome transaction size limits or compute budget constraints by batching operations across multiple transactions

Important Notes on Tipping

Bundles are prioritized based on a user-defined tip amount, which incentivizes validators to process bundles ahead of other transactions. Some key considerations when using tips:

  • Tips incentivize validators to process bundles ahead of other transactions
  • Tips are simply a SOL transfer instruction to one of several on-chain tip addresses
  • The tip instruction must be in the last transaction of your Bundle
  • Minimum tip required: 1,000 lamports
  • Tip amounts vary based on demand and contested accounts
  • Tip accounts should not be included in Address Lookup Tables
  • Tip Payment Program:T1pyyaTNZsKv2WcRAB8oVnk93mLJw2XzjtVYqCsaHqt
  • Tip Accounts can be accessed via the getTipAccounts RPC method (we will cover this in a bit) or via the Jito Docs

Block Engine Bundle Selection Process

  1. Basic Sanity Check: Validates transactions and ensures the Bundle contains five or fewer transactions
  2. Simulation Check: Verifies all transactions will succeed and compares payment to other bundle submissions
  3. Auction Check: Groups bundles based on state locks, simulates them, and selects top N highest-paying bundles

Limitations

  • Jito Bundles are only processed when a Jito-Solana leader is producing blocks (the Anza validator client does not process them). At the time of this writing (October 2024), the Jito validator client represents about 85% of the stake in the Solana blockchain (source: Jito Labs).
  • Maximum of 5 transactions per Bundle

By leveraging Jito Bundles, developers can ensure complex, multi-transaction operations execute atomically and sequentially, enabling new possibilities for DeFi applications, trading strategies, and other advanced use cases on Solana.

For more detailed information, refer to Jito's official documentation.

Let's test it out!

Create a New Project

To get started, open a code editor of your choice and create a new project folder:

mkdir lil-jit-demo && cd lil-jit-demo

Then, initialize a new project using the following command:

npm init -y

Next, install the following dependencies:

npm install --save @solana/web3.js@2 @solana-program/memo @solana-program/system 

Note: You may need to install the @solana-program/system and @solana-program/memo packages using the --legacy-peer-deps flag.

Make sure you have Node types installed:

npm i --save-dev @types/node

And initialize a new TypeScript configuration file that supports JSON modules:

tsc --init --resolveJsonModule

And create a new file called index.ts in the root directory of your project:

echo > index.ts

You will need a paper wallet with ~0.01 SOL to test out this demo (and cover the cost of a bundle tip). If you don't have one, you can create one using the following command:

solana-keygen new -o secret.json -s --no-bip39-passphrase

You can get the new wallet's address by running the following command:

solana address -k secret.json

Make sure to send that wallet ~0.01 SOL before proceeding. You can verify your mainnet balance by running the following command:

solana balance -um -k secret.json

Great. Let's write our script!

Write the Script

Before we get to coding, let's just outline the steps we want our script to perform:

  1. Setup our script by importing our Solana keypair from the secret.json file and establishing a connection to our Lil' Jit-enabled Solana endpoint
  2. Get a Jito Tip account to use for sending our tip to
  3. Fetch the recent blockhash
  4. Create a set of transactions to bundle. For this example, we will create 5 Memo transactions that each include a unique, sequential message (e.g., "lil jit demo transaction # ${index}")
  5. Simulate our Bundle to ensure it will succeed
  6. Send our Bundle
  7. Verify it is included in a block

Let's build it!

Import Dependencies

First, open up your index.ts file and add the following imports:

import {
Rpc,
createDefaultRpcTransport,
createRpc,
createJsonRpcApi,
Address,
mainnet,
Base58EncodedBytes,
createSolanaRpc,
createKeyPairSignerFromBytes,
createTransactionMessage,
setTransactionMessageFeePayerSigner,
pipe,
setTransactionMessageLifetimeUsingBlockhash,
appendTransactionMessageInstruction,
TransactionPartialSigner,
signTransactionMessageWithSigners,
getBase64EncodedWireTransaction,
Base64EncodedWireTransaction,
getTransactionEncoder,
getBase58Decoder,
} from "@solana/web3.js";
import { getAddMemoInstruction } from "@solana-program/memo";
import { getTransferSolInstruction } from "@solana-program/system";
import secret from "./secret.json";

We will use several interfaces and functionality from the @solana/web3.js library. We will also use the @solana-program/memo and @solana-program/system libraries to create our transactions.

Define Constants

Let's define a few constants that we'll use throughout our script. Add the following constants below your imports:

const MINIMUM_JITO_TIP = 1_000; // lamports
const NUMBER_TRANSACTIONS = 5;
const SIMULATE_ONLY = true;
const ENDPOINT = 'https://example.solana-mainnet.quiknode.pro/123456/'; // 👈 replace with your endpoint
const POLL_INTERVAL_MS = 3000;
const POLL_TIMEOUT_MS = 30000;

Make sure you replace the ENDPOINT constant with your own Lil' JIT-enabled endpoint. If you do not already have an endpoint, you can create one at QuickNode.com. If you already have an endpoint and need to add the Lil' JIT Marketplace Add-on, you do so by going to your endpoint page and click Add-on and follow the steps to add the add-on.

Define Lil JIT Type

To best use the newest Solana Web3.js library, we will define a custom type for our Lil' JIT endpoint. Below your constants, add the following type definition:

type JitoBundleSimulationResponse = {
context: {
apiVersion: string;
slot: number;
};
value: {
summary: 'succeeded' | {
failed: {
error: {
TransactionFailure: [number[], string];
};
tx_signature: string;
};
};
transactionResults: Array<{
err: null | unknown;
logs: string[];
postExecutionAccounts: null | unknown;
preExecutionAccounts: null | unknown;
returnData: null | unknown;
unitsConsumed: number;
}>;
};
};

type LilJitAddon = {
getRegions(): string[];
getTipAccounts(): Address[];
getBundleStatuses(bundleIds: string[]): {
context: { slot: number };
value: {
bundleId: string;
transactions: Base58EncodedBytes[];
slot: number;
confirmationStatus: string;
err: any;
}[]
};
getInflightBundleStatuses(bundleIds: string[]): {
context: { slot: number };
value: {
bundle_id: string;
status: "Invalid" | "Pending" | "Landed" | "Failed";
landed_slot: number | null;
}[];
};
sendTransaction(transactions: Base64EncodedWireTransaction[]): string;
simulateBundle(transactions: [Base64EncodedWireTransaction[]]): JitoBundleSimulationResponse;
sendBundle(transactions: Base58EncodedBytes[]): string;
}

Here we are defining the LilJitAddon type, which specifies the methods, params, and return types for interacting with the Lil' JIT Marketplace Add-on. We will use this type to interact with our endpoint. Due to its size, we have also broken out the JitoBundleSimulationResponse type into its own definition.

Create Helper Functions

Let's create a few helper functions to make our script more readable.

Create a Jito Bundles RPC Client

First, let's create a createJitoBundlesRpc function that creates a new RPC client for interacting with our Lil' JIT endpoint. Add the following function below your imports:

function createJitoBundlesRpc({ endpoint }: { endpoint: string }): Rpc<LilJitAddon> {
const api = createJsonRpcApi<LilJitAddon>({
responseTransformer: (response: any) => response.result,
});
const transport = createDefaultRpcTransport({
url: mainnet(endpoint),
});
return createRpc({ api, transport });
}

We simply use the createRpc function from the @solana/web3.js library to create a new RPC client for our endpoint. We are also using the createJsonRpcApi and createDefaultRpcTransport functions to create the necessary API and transport objects for our RPC client. A couple of notes:

  • make sure you are specifying LilJitAddon as the type for your api object
  • note that we are using a responseTransformer to only return the result property of the response

Create a Simulation Checker

Next, let's create a validateSimulation function that checks the simulation results to ensure the Bundle was successful. Add the following function below your imports:

function isFailedSummary(summary: JitoBundleSimulationResponse['value']['summary']): summary is { failed: any } {
return typeof summary === 'object' && summary !== null && 'failed' in summary;
}

function validateSimulation(simulation: JitoBundleSimulationResponse) {
if (simulation.value.summary !== 'succeeded' && isFailedSummary(simulation.value.summary)) {
throw new Error(`Simulation Failed: ${simulation.value.summary.failed.error.TransactionFailure[1]}`);
}
}

Here, we are adding two functions to help us check the response of a received simulation. The isFailedSummary function checks if the simulation summary is an object with a failed property. The validateSimulation function checks if the simulation summary has succeeded and throws an error if it does not.

Create a Transaction Generator

We are going to need to create five distinct transactions for the purposes of our demo. Let's make a createTransaction function that generates a new transaction for each of the five transactions. We'll want to be able to specify an index that we can pass into a memo message to ensure each transaction is unique, and we'll want to allow the option to include a tip instruction in our transaction (this will be the last transaction in the Bundle). Add the following function below your imports:

async function createTransaction(
index: number,
latestBlockhash: Parameters<
typeof setTransactionMessageLifetimeUsingBlockhash
>[0],
payerSigner: TransactionPartialSigner,
includeTip?: Address
) {
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayerSigner(payerSigner, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
(tx) =>
appendTransactionMessageInstruction(
getAddMemoInstruction({
memo: `lil jit demo transaction # ${index}`,
}),
tx
),
(tx) =>
includeTip
? appendTransactionMessageInstruction(
getTransferSolInstruction({
source: payerSigner,
destination: includeTip,
amount: MINIMUM_JITO_TIP,
}),
tx
)
: tx
);
return await signTransactionMessageWithSigners(transactionMessage);
}

Here we are using @solana/web3.js pipe function to create a new transaction message:

  • we set the fee payer using setTransactionMessageFeePayerSigner,
  • we set the lifetime of the transaction using the setTransactionMessageLifetimeUsingBlockhash function
  • we are also using the appendTransactionMessageInstruction function to add a memo instruction to the transaction (each memo will have a unique message based on the index parameter--we will use this to verify the transaction sequence executed correctly)
  • we are also using the appendTransactionMessageInstruction function to add a tip instruction to the transaction if includeTip is passed in as a parameter

Get a Tip Account

The Lil' JIT add-on includes a method for retrieving a list of tip accounts. We will use this method to get all the tip accounts and then select one from the list randomly to include in our transaction. Add the following function below your imports:

async function getTipAccount(rpc: Rpc<LilJitAddon>): Promise<Address> {
try {
const tipAccounts = await rpc.getTipAccounts().send();
const jitoTipAddress = tipAccounts[Math.floor(Math.random() * tipAccounts.length)];
if (!jitoTipAddress) {
throw new Error("No JITO tip accounts found");
}
return jitoTipAddress;
} catch {
throw new Error("Failed to get Tip Account");
}
}

Here, we simply use the getTipAccounts method to retrieve a list of tip accounts. We then select a random tip account from the list and return it.

Poll for Bundle Status

We will use the getInflightBundleStatuses method to poll for the status of our Bundle until it has landed, failed, or timed out. Add the following function below your imports:

async function pollBundleStatus(
rpc: Rpc<LilJitAddon>,
bundleId: string,
timeoutMs = 30000,
pollIntervalMs = 3000
) {
const startTime = Date.now();
let lastStatus = '';
while (Date.now() - startTime < timeoutMs) {
try {
const bundleStatus = await rpc.getInflightBundleStatuses([bundleId]).send();
const status = bundleStatus.value[0]?.status ?? 'Unknown';

if (status !== lastStatus) {
lastStatus = status;
}

if (status === 'Landed') {
return true;
}

if (status === 'Failed') {
console.log(`Bundle ${status.toLowerCase()}. Exiting...`);
throw new Error(`Bundle failed with status: ${status}`);
}

await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
} catch {
console.error('❌ - Error polling bundle status.');
}
}
throw new Error("Polling timeout reached without confirmation");
}

There's not much going on here; we are simply defining a polling frequency and timeout period and calling getInflightBundleStatuses to check the status of our Bundle. If the Bundle has landed, we return true, breaking the loop. If the Bundle fails or the polling times out, we log an error and throw an error.

Create a Main Function

Let's start by framing out our main function based on the abovementioned steps. Add the following function below the rest of your code:

async function main() {
// Step 1 - Setup


// Step 2 - Get a Jitotip account


// Step 3 - Get Recent Blockhash


// Step 4 - Create Transactions


// Step 5 - Simulate Bundle


// Step 6 - Send Bundle


// Step 7 - Verify Bundle Landed

}

main().catch((error) => {
console.error(`❌ - Error: ${error}`);
process.exit(1);
});

Great. Now let's fill in the steps.

Step 1 - Setup

First, let's import our keypair and define both our Solana RPC and our Lil' JIT RPC. Add the following to your main function:

    // Step 1 - Setup
const signer = await createKeyPairSignerFromBytes(new Uint8Array(secret));
console.log(`Initializing Jito Bundles demo. Sending ${NUMBER_TRANSACTIONS} transactions from ${signer.address}.`);

const solanaRpc = createSolanaRpc(ENDPOINT);
const lilJitRpc = createJitoBundlesRpc({ endpoint: ENDPOINT });
console.log(`✅ - Established connection to QuickNode.`);

Now we can use both our native Solana RPC methods (using solanaRpc) and our Jito Bundles RPC methods (using lilJitRpc) to interact with the Jito validator client.

Step 2 - Get a Jitotip account

Since we have already defined our getTipAccount function, all we need to do is call it and store its response to a variable, jitoTipAddress. Add the following to your main function:

    // Step 2 - Get a Jitotip account
const jitoTipAddress = await getTipAccount(lilJitRpc);
console.log(`✅ - Using the following Jito Tip account: ${jitoTipAddress}`);

Step 3 - Get Recent Blockhash

Just like any transaction on Solana, we must pass a recent blockhash to ensure our transaction is processed timely. Add the following to your main function:

    // Step 3 - Get Recent Blockhash
const { value: latestBlockhash } = await solanaRpc
.getLatestBlockhash({ commitment: "confirmed" })
.send();
console.log(`✅ - Latest blockhash: ${latestBlockhash.blockhash}`);

Step 4 - Create Transactions

Now, we will create five memo transactions, each with a unique, sequential message. Add the following to your main function:

    // Step 4 - Create Transactions
const signedTransactions = await Promise.all(
Array.from({ length: NUMBER_TRANSACTIONS }, (_, i) => {
const isLastTransaction = i === NUMBER_TRANSACTIONS - 1;
return createTransaction(
i + 1,
latestBlockhash,
signer,
isLastTransaction ? jitoTipAddress : undefined
);
})
);

const base64EncodedTransactions = signedTransactions.map((transaction) => {
const base64EncodedTransaction = getBase64EncodedWireTransaction(transaction);
return base64EncodedTransaction;
}) as Base64EncodedWireTransaction[];

const transactionEncoder = getTransactionEncoder();
const base58Decoder = getBase58Decoder();

const base58EncodedTransactions = signedTransactions.map((transaction) => {
const transactionBytes = transactionEncoder.encode(transaction);
return base58Decoder.decode(transactionBytes) as Base58EncodedBytes;
});
console.log(`✅ - Transactions assembled and encoded.`);

Let's walk through what we've done here:

  • We use the createTransaction function to create 5 Memo transactions.
  • For the last transaction in the array, we include the jitoTipAddress as the tip account so the tip instruction is included in the transaction.
  • We then encode the transactions using base64 and base58 encoding (required for simulating and sending bundles, respectively).

Step 5 - Simulate Bundle

Next, we will use the Lil' JIT simulateBundle method to test our Bundle. We can use the validateSimulation function we wrote earlier to ensure the simulation succeeds. Finally, we create a conditional that will end the script if our configuration is set to Simulation Only mode. Add the following to your main function:

    // Step 5 - Simulate Bundle
const simulation = await lilJitRpc
.simulateBundle([base64EncodedTransactions])
.send();

validateSimulation(simulation);
console.log(`✅ - Simulation Succeeded.`);

if (SIMULATE_ONLY) {
console.log("🏁 - Simulation Only Mode - Exiting script.");
return;
}

Step 6 - Send Bundle

Now, we will use the sendBundle method to send our Bundle to the Jito validator client. Add the following to your main function:

    // Step 6 - Send Bundle
let bundleId: string;
try {
bundleId = await lilJitRpc
.sendBundle(base58EncodedTransactions)
.send();
console.log(`✅ - Bundle sent: ${bundleId}`);
} catch (error) {
console.error("❌ - Error sending bundle:", error);
throw error;
}

Step 7 - Verify Bundle Landed

Finally, let's call our pollBundleStatus function to verify the Bundle landed. Add the following to your main function:

    // Step 7 - Verify Bundle Landed
await pollBundleStatus(lilJitRpc, bundleId, POLL_TIMEOUT_MS, POLL_INTERVAL_MS);
console.log(`✅ - Bundle landed: ${bundleId}`);
console.log(` https://explorer.jito.wtf/bundle/${bundleId}`);
console.log(` (Note: This URL may take a few moments to become available.)`);

Great job. We should now have a working script that sends 5 Memo transactions to the Jito validator client and verifies that they landed. Let's run it!

Run Your Code

Before you run your code, check your SIMULATE_ONLY and MINIMUM_JITO_TIP variables. If you set SIMULATE_ONLY to true, the script will only simulate the Bundle and exit. Otherwise, it will send the Bundle to the validator client--if the transaction lands on mainnet, your transactions will be included in a block, meaning your tip will be paid. This is irreversible.

When you are ready, in your terminal, run the following command:

ts-node index.ts

If you set SIMULATE_ONLY to false, you should see something similar to the following output:

Initializing Jito Bundles demo. Sending 5 transactions from FUA7zS5PXVtW2VCmFGtfv5q6AdusbpVGy8HTjVjBAAzR.
✅ - Using the following Jito Tip account: HFqU5x63VTqvQss8hp11i4wVV8bD44PvwucfZ2bU7gRe
✅ - Latest blockhash: CghMwVqLLdKrdgdCKDbTtRgdRhj9BfCBZxL8n87QNB9C
✅ - Transactions assembled and encoded.
✅ - Simulation Succeeded.
✅ - Bundle sent: c4fb0940406d3fead71e29b9f87d2273ab6a743f3da1bf4aeeb73db3521685b0
✅ - Bundle landed: c4fb0940406d3fead71e29b9f87d2273ab6a743f3da1bf4aeeb73db3521685b0
https://explorer.jito.wtf/bundle/c4fb0940406d3fead71e29b9f87d2273ab6a743f3da1bf4aeeb73db3521685b0
(Note: This URL may take a few moments to become available.)

LFJitoooo! Great job! You've landed your first Bundle on Solana. As you should note in our terminal output, the Jito bundle explorer might take a minute to update with your Bundle, so wait a minute or so and then click your link. You should see something like this:

Confirmed Jito Bundle - Jito Explorer

The Bundle should show all five transactions in order. The fifth transaction should include the tip instruction with a SOL balance transfer. You can see this in the example above. You can also open each transaction individually by clicking the ⎘ icon in the top right of a transaction (or browse your wallet address in the Solana Explorer). You should see all five transactions with the same timestamp and block:

All Bundle&#39;s Transactions - Solana Explorer

You should be able to navigate to the 1st of the five transactions and see "lil jit demo transaction # 1" in the transaction details:

1st Transaction - Solana Explorer

And you should be able to navigate to the 5th transaction and see "lil jit demo transaction # 5" in the transaction details with our tip transfer as well:

5th Transaction - Solana Explorer

This shows:

  • All transactions are included in the same block
  • All transactions are executed in the same order that they were sent
  • All transactions are successful
  • The final transaction includes a tip transfer to the tip account

Pretty cool, right? Nice job! 🎉

That's a Bundle!

Jito Bundles are a powerful tool to level up your Solana development experience. By utilizing the Lil' JIT marketplace add-on, you can bundle transactions and ensure they are executed in the same block and in the specified order. This is a great way to optimize your transactions and maximize the performance of your applications.

We look forward to seeing what you build with the Lil' JIT add-on. Let us know what you are up to or if you have any questions! You can reach out to us on Discord or Twitter.

We ❤️ Feedback!

Let us know if you have any feedback or requests for new topics. We'd love to hear from you.

Resources

Share this guide