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.
What You Will Do
- Install the Lil' JIT marketplace add-on
- Write a script using TypeScript and Solana Kit
- 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
- A Quicknode account
- Experience with optimizing Solana transactions
- Experience with Solana Kit will be helpful
- Mainnet SOL (0.01 SOL should be enough for this demo)
- Solana CLI, latest
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
| Characteristic | Description |
|---|---|
| Sequential Execution | Transactions in a bundle are guaranteed to execute in the order they are listed |
| Atomic Execution | All transactions in a bundle execute within the same slot |
| All-or-Nothing Outcome | If any transaction in a bundle fails, none of the transactions are committed to the chain |
| Bundle Size | Maximum of 5 transactions per Bundle (enabling complex operations exceeding the 1.4M Compute Units per transaction limit) |
How Jito Bundles Work
- The user creates and signs transactions
- The user bundles transactions together with a tip instruction to the last transaction
- Users send bundles to Jito's Block Engine
- Block Engine forwards bundles to validators running Jito
- Jito-Solana features a specialized BundleStage for executing bundles
- 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
getTipAccountsRPC method (we will cover this in a bit) or via the Jito Docs
Block Engine Bundle Selection Process
- Basic Sanity Check: Validates transactions and ensures the Bundle contains five or fewer transactions
- Simulation Check: Verifies all transactions will succeed and compares payment to other bundle submissions
- 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 (updated April 2025), the Jito validator client represents about 95% 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/kit @solana-program/memo @solana-program/system
Note: You may need to install the
@solana-program/systemand@solana-program/memopackages using the--legacy-peer-depsflag.
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:
- Setup our script by importing our Solana keypair from the
secret.jsonfile and establishing a connection to our Lil' Jit-enabled Solana endpoint - Get a Jito Tip account to use for sending our tip to
- Fetch the recent blockhash
- 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}")
- Simulate our Bundle to ensure it will succeed
- Send our Bundle
- 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
} from "@solana/kit";
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/kit 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;
const DEFAULT_WAIT_BEFORE_POLL_MS = 5000;
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 Solana Kit 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: Base64EncodedWireTransaction[]): 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/kit 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
LilJitAddonas the type for yourapiobject - note that we are using a
responseTransformerto only return theresultproperty 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.