How to Implement Gasless Transaction Bundles with Jito and Kora
This guide teaches you how to implement gasless Jito Bundles using Kora.
Last Updated: 2025-01-09
What You'll Build
In the Full Transaction Flow Guide, you learned how to create gasless transactions using Kora. There are many scenarios, however, where a single transaction is inadequate OR there is insufficient space in a single transaction to include a Kora payment instruction. In this guide, we will build a demo that demonstrates how to use Kora to sign and send a bundle of transactions to Jito's block engine for atomic execution on Solana Mainnet. The Kora server will pay the Jito tip and all transaction fees.
The final result will be a working Jito bundle system:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
KORA JITO BUNDLE DEMO
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[1/5] Initializing clients
→ Kora RPC: http://localhost:8080/
→ Solana RPC: https://api.mainnet-beta.solana.com
[2/5] Setting up keypairs
→ Sender: BYJVBqQ2xV9GECc84FeoPQy2DpgoonZQFQu97MMWTbBc
→ Kora signer address: 3Z1Ef7YaxK8oUMoi6exf7wYZjZKWJJsrzJXSt1c3qrDE
[3/5] Creating bundle transactions
→ Blockhash: 7HZUaMqV...
→ Tip account: 96gYZGLn...
→ Transaction 1: Kora Memo "Bundle tx #1"
→ Transaction 2: Kora Memo "Bundle tx #2"
→ Transaction 3: Kora Memo "Bundle tx #3"
→ Transaction 4: Kora Memo "Bundle tx #4" + Jito tip
✓ 4 transactions created for bundle
[4/5] Signing bundle with Kora
✓ All transactions signed by user
✓ Bundle co-signed by Kora
→ 4 transactions signed
[5/5] Submitting bundle to Jito
✓ Bundle submitted to Jito block engine
→ Bundle UUID: 8f4a3b2c-1d5e-6f7a-8b9c-0d1e2f3a4b5c
⏳ Polling bundle status...
✓ Bundle landed (simulated for demo)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
SUCCESS: Bundle confirmed on Solana
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Bundle UUID:
8f4a3b2c-1d5e-6f7a-8b9c-0d1e2f3a4b5cHere's how we build it.
Prerequisites
Before starting this tutorial, ensure you have:
- Completed the Kora Full Transaction Flow Guide - we build on those concepts
- Node.js (LTS or later)
- Familiarity with Solana transactions
- Familiarity with Jito Bundles
Kora 2.2 Release Candidate
Important: This guide is built on the Kora 2.2 Release Candidate branch. You can find the branch here. This branch is unaudited and may contain bugs. Please use at your own risk.
You will need to install the Kora 2.2 Release Candidate:
cargo install kora-cli@2.2.0-beta.2Jito Bundles Basics
On Solana, every instruction in a transaction is atomic—if one instruction fails, the entire transaction fails. Bundles are a tool that enable you to execute up to 5 transactions atomically and sequentially. Bundles are incentivized by a tip, the higher the tip, the higher the priority.
This guide assumes you have some basic understanding of and experience with Jito Bundles.
Project Structure
The sample code for this demo can be found in the Kora 2.2 Release Candidate Branch here:
jito-bundles/
├── client/
│ ├── src/
│ │ └── index.ts # Bundle demo implementation
│ └── package.json
├── server/
│ ├── kora.toml # Kora configuration with bundles enabled
│ └── signers.toml # Signer configuration
└── scripts/
└── start-kora.sh # Server startup scriptClone the kora repository and navigate to the jito-bundles directory:
git clone https://github.com/solana-foundation/kora.git
cd kora/examples/jito-bundlesKora Server Configuration
kora.toml
The key configuration for bundle support:
[kora]
rate_limit = 100
[kora.auth]
api_key = "kora_facilitator_api_key_example"
[kora.enabled_methods]
sign_bundle = true
sign_and_send_bundle = true
estimate_bundle_fee = true
get_blockhash = true
get_config = true
get_payer_signer = true
[validation]
max_allowed_lamports = 1000000
max_signatures = 10
price_source = "Mock"
allowed_programs = [
"11111111111111111111111111111111", # System Program
"MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr", # Memo Program
]
[validation.fee_payer_policy.system]
allow_transfer = true # Required for Jito tip transfers
[validation.price]
type = "free" # No payment required for this demo
[kora.bundle]
enabled = true
[kora.bundle.jito]
block_engine_url = "https://mainnet.block-engine.jito.wtf"Important settings for bundle support:
- sign_bundle / sign_and_send_bundle - Enable the bundle RPC methods
- allow_transfer = true - Kora's signer pays the Jito tip, so it needs transfer permission
- bundle.enabled = true - Master switch for bundle functionality
- We are using the public mainnet block engine URL for this demo. In production, you would use the private block engine url.
signers.toml
[signer_pool]
strategy = "round_robin"
[[signers]]
name = "main_signer"
type = "memory"
private_key_env = "KORA_PRIVATE_KEY"Make sure to rename .env.example to .env and set the KORA_PRIVATE_KEY environment variable to your mainnet private key. The signer wallet needs SOL on mainnet to pay:
- Transaction fees for all bundle transactions
- The Jito tip (minimum 1,000 lamports)
Important: This guide demonstrates using Jito tips on Solana Mainnet. Tips are non-refundable.
Starting the Server
From the server/ directory:
kora --config kora.toml --rpc-url https://api.mainnet-beta.solana.com rpc start --signers-config signers.tomlOr use the provided script. From the server/ directory:
../scripts/start-kora.shGreat! You are running a Kora server with bundle support enabled on Solana Mainnet.
Client Implementation
We'll walk through the client implementation step by step, starting with imports.
Imports and Configuration
import { KoraClient } from "@solana/kora";
import {
createNoopSigner,
address,
getBase64EncodedWireTransaction,
partiallySignTransactionMessageWithSigners,
Blockhash,
KeyPairSigner,
pipe,
createTransactionMessage,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
appendTransactionMessageInstruction,
generateKeyPairSigner,
} from "@solana/kit";
import { getAddMemoInstruction } from "@solana-program/memo";
import { getTransferSolInstruction } from "@solana-program/system";
const MINIMUM_JITO_TIP = 1_000n; // lamports
const CONFIG = {
solanaRpcUrl: "https://api.mainnet-beta.solana.com",
koraRpcUrl: "http://localhost:8080/",
jitoTipLamports: MINIMUM_JITO_TIP,
bundleSize: 4, // We'll create 4 transactions for this demo
pollIntervalMs: 6000,
pollTimeoutMs: 60000,
};We're setting up:
- Solana Kit imports for building transactions
- Memo program for our demo transactions (you'd replace with real operations)
- System program for the Jito tip transfer
- Configuration for RPC endpoints and bundle parameters
Jito Tip Accounts
Jito has 8 tip accounts that you can send SOL to. We select one at random for this demo.
// Jito tip accounts - one is randomly selected by the block engine
const JITO_TIP_ACCOUNTS = [
"96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5",
"HFqU5x63VTqvQss8hp11i4wVV8bD44PvwucfZ2bU7gRe",
"Cw8CFyM9FkoMi7K7Crf6HNQqf4uEMzpKw6QNghXLvLkY",
"ADaUMid9yfUytqMBgopwjb2DTLSokTSzL1zt6iGPaS49",
"DfXygSm4jCyNCybVYYK6DwvWqjKee8pbDmJGcLWNDXjh",
"ADuUkR4vqLUMWXxW9gh6D6L8pMSawimctcNZ5pGwDcEt",
"DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL",
"3AVi9Tg9Uo68tJfuvoKvqKNWKkC5wPdSSdeBnizKZ6jT",
];
function getRandomTipAccount(): string {
return JITO_TIP_ACCOUNTS[
Math.floor(Math.random() * JITO_TIP_ACCOUNTS.length)
];
}The tip accounts are Jito-operated addresses. Sending SOL to any of them signals your tip amount to validators.
Step 1: Initialize Clients
We initialize the Kora client with our API key, which matches what's configured in kora.toml. In production, you'd load this from an environment variable.
async function initializeClients() {
console.log("\n[1/4] Initializing clients");
console.log(" → Kora RPC:", CONFIG.koraRpcUrl);
console.log(" → Solana RPC:", CONFIG.solanaRpcUrl);
const client = new KoraClient({
rpcUrl: CONFIG.koraRpcUrl,
apiKey: 'kora_facilitator_api_key_example', // Make sure this matches what's configured in kora.toml
});
return { client };
}Step 2: Setup Keys
async function setupKeys(client: KoraClient) {
console.log("\n[2/4] Setting up keypairs");
const senderKeypair = await generateKeyPairSigner();
console.log(" → Sender:", senderKeypair.address);
const { signer_address } = await client.getPayerSigner();
console.log(" → Kora signer address:", signer_address);
return { senderKeypair, signer_address };
}We use generateKeyPairSigner() to create a fresh keypair for the demo. Because the keypair is only signing the memo instructions and does not have to pay any Kora fees (per our configuration), no SOL or other tokens are needed. Kora's signer (fetched via getPayerSigner) pays all fees and the Jito tip.
Step 3: Create Bundle Transactions
Now let's create a bundle of transactions. We create multiple transactions, each with its own unique instructions. We use unique memo instructions here to easily verify our transactions after they land on Solana Mainnet.
async function createBundleTransactions(
client: KoraClient,
senderKeypair: KeyPairSigner,
signer_address: string
) {
console.log("\n[3/4] Creating bundle transactions");
const noopSigner = createNoopSigner(address(signer_address));
const latestBlockhash = await client.getBlockhash();
const tipAccount = getRandomTipAccount();
console.log(" → Blockhash:", latestBlockhash.blockhash.slice(0, 8) + "...");
console.log(" → Tip account:", tipAccount.slice(0, 8) + "...");
const transactions: string[] = [];
for (let i = 0; i < CONFIG.bundleSize; i++) {
const isLastTransaction = i === CONFIG.bundleSize - 1;
console.log(
` → Transaction ${i + 1}: Kora Memo "Bundle tx #${i + 1}"${
isLastTransaction ? " + Jito tip" : ""
}`
);
// Build transaction with memo
let transactionMessage = pipe(
createTransactionMessage({
version: 0,
}),
(tx) => setTransactionMessageFeePayerSigner(noopSigner, tx),
(tx) =>
setTransactionMessageLifetimeUsingBlockhash(
{
blockhash: latestBlockhash.blockhash as Blockhash,
lastValidBlockHeight: 0n,
},
tx
),
(tx) =>
appendTransactionMessageInstruction(
getAddMemoInstruction({
memo: `Kora Bundle tx #${i + 1} of ${CONFIG.bundleSize}`,
signers: [senderKeypair],
}),
tx
),
// Add Jito tip to the LAST transaction only
(tx) =>
isLastTransaction
? appendTransactionMessageInstruction(
getTransferSolInstruction({
source: noopSigner,
destination: address(tipAccount),
amount: CONFIG.jitoTipLamports,
}),
tx
)
: tx
);
// Sign with sender keypair (required for memo instruction)
const signedTransaction = await partiallySignTransactionMessageWithSigners(
transactionMessage
);
const base64Transaction =
getBase64EncodedWireTransaction(signedTransaction);
transactions.push(base64Transaction);
}
console.log(` ✓ ${transactions.length} transactions created for bundle`);
return transactions;
}Important: Tip paid by Kora signer: Since we want the Kora node to pay our Jito tip, we use a "no-op" signer (noopSigner), where Kora's address is the source of the tip transfer. Kora will sign this when processing the bundle.
Step 4: Sign and Submit Bundle
Now we can pull it together and send the bundle to Kora for signing and submission to Jito's block engine.
async function main() {
console.log("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
console.log("KORA JITO BUNDLE DEMO");
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
try {
// Step 1: Initialize clients
const { client } = await initializeClients();
// Step 2: Setup keys
const { senderKeypair, signer_address } = await setupKeys(client);
// Step 3: Create bundle transactions
const transactions = await createBundleTransactions(
client,
senderKeypair,
signer_address
);
// Step 4: Sign and send bundle
console.log("\n[4/4] Signing and sending bundle");
const { bundle_uuid } = await client.signAndSendBundle({
transactions,
signer_key: signer_address,
});
console.log("\nBundle UUID:");
console.log(bundle_uuid);
} catch (error) {
console.error("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
console.error("ERROR: Demo failed");
console.error("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
console.error("\nDetails:", error);
process.exit(1);
}
}
main().catch((e) => console.error("Error:", e));Running the Demo
1. Start the Kora Server
cd examples/jito-bundles/server
# Start Kora
kora --config kora.toml --rpc-url https://api.mainnet-beta.solana.com rpc start --signers-config signers.toml2. Run the Client
In a new terminal, navigate to the client/ directory and run the demo:
cd examples/jito-bundles/client
# Install dependencies
pnpm install
# Run the demo
pnpm startExpected Output
You should see the step-by-step execution with a successful bundle at the end. The bundle will:
- Create 4 memo transactions
- Add a Jito tip (1,000 lamports) to the last transaction
- Have all transactions signed by Kora as fee payer
- Submit atomically to Jito's block engine
Note:
- Jito's default router can hit rate limits. If you get a 429 error, you can try again later or request higher limits. Check out Jito's rate limiting documentation for more information.
- Because our demo is using a very small tip, the bundle may not land on Solana Mainnet. If you don't see the bundle on Jito's bundle explorer, you can try again later with a higher tip.
Understanding What Happened
Here's what happened differently from single transactions:
- Multiple Transactions - Instead of one transaction, we created 4 that must execute together
- Jito Tip - We added a tip transfer (paid by Kora's signer) to incentivize validators
- Bundle Validation - Kora validated all transactions meet requirements specified in
kora.toml - Atomic Submission - All transactions submitted as a single unit to Jito by our Kora server with all fees and tips paid by Kora's signer
The result: either all 4 transactions execute in sequence, or none do. No partial states.
Wrap Up
You've now implemented gasless transaction bundles with Kora and Jito. With Jito Bundles combined with Kora's gasless transaction support, you can provide users with seamless experiences for even the most complex operations.
Additional Resources
- Need help? Ask questions on Solana Stack Exchange with a
Koratag - Jito Documentation - Official Jito MEV documentation
- API Reference - signBundle and signAndSendBundle methods
- GitHub Repository - Source code and examples