Jito Tips and Bundle Submission: Priority Transactions on Solana
Jito bundles guarantee your transactions execute in sequence and with priority, essential for MEV-sensitive operations like arbitrage, liquidations, and NFT mints. By tipping Jito validators, you ensure your transaction bundle lands in the next block exactly as intended. This guide covers bundle creation, tip optimization, and production-ready implementation patterns.
TL;DR
- **Bundles execute atomically** - all transactions in a bundle succeed or fail together
- **Tips determine priority** - higher tips = faster inclusion, measured in lamports
- **Use bundles for MEV protection** - prevent sandwich attacks on your swaps
- **Two APIs available**: `sendBundle` for atomic execution, `sendTransaction` for single tx priority
- **Tip allocation matters** - 70% priority fee + 30% Jito tip for `sendTransaction`
Prerequisites
Before working with Jito bundles, ensure you have:
- Understanding of Solana transaction structure
- Experience with `@solana/web3.js`
- Familiarity with base58/base64 encoding
- Access to Jito Block Engine endpoints
Package versions used in this guide:
- `jito-ts`: 4.2.1
- `@solana/web3.js`: 1.98.4
- `bs58`: 5.0.0
What is Jito?
Jito is Solana's leading MEV infrastructure provider. Their block engine allows users to:
- **Submit transaction bundles** that execute atomically
- **Tip validators** for priority inclusion
- **Protect against MEV extraction** (sandwich attacks, frontrunning)
- **Guarantee transaction ordering** within bundles
Jito vs Standard Transactions
| Feature | Standard TX | Jito Bundle |
|---------|------------|-------------|
| Atomicity | Single TX | Multiple TXs |
| Ordering | Not guaranteed | Guaranteed |
| MEV Protection | None | Built-in |
| Priority | Priority fee | Priority fee + Tip |
| Latency | Variable | Consistent |
Jito Block Engine Endpoints
Mainnet Endpoints
const JITO_ENDPOINTS = {
// Primary block engine
mainnet: 'https://mainnet.block-engine.jito.wtf',
// Regional endpoints for lower latency
amsterdam: 'https://amsterdam.mainnet.block-engine.jito.wtf',
frankfurt: 'https://frankfurt.mainnet.block-engine.jito.wtf',
ny: 'https://ny.mainnet.block-engine.jito.wtf',
tokyo: 'https://tokyo.mainnet.block-engine.jito.wtf',
};
// API paths
const BUNDLE_API = '/api/v1/bundles';
const TRANSACTION_API = '/api/v1/transactions';Setting Up the Jito Client
Using jito-ts SDK
import { SearcherClient } from 'jito-ts/dist/sdk/block-engine/searcher';
import { Bundle } from 'jito-ts/dist/sdk/block-engine/types';
import {
Connection,
Keypair,
PublicKey,
Transaction,
SystemProgram,
LAMPORTS_PER_SOL,
TransactionMessage,
VersionedTransaction,
} from '@solana/web3.js';
import bs58 from 'bs58';
// Initialize connection
const connection = new Connection('https://api.mainnet-beta.solana.com');
// Your wallet keypair
const wallet = Keypair.fromSecretKey(
bs58.decode(process.env.PRIVATE_KEY!)
);
// Initialize Jito searcher client
const searcherClient = SearcherClient.connect(
'mainnet.block-engine.jito.wtf:443',
wallet
);
console.log('Connected to Jito Block Engine');Manual HTTP Client
import fetch from 'node-fetch';
class JitoClient {
private endpoint: string;
constructor(endpoint: string = 'https://mainnet.block-engine.jito.wtf') {
this.endpoint = endpoint;
}
async sendBundle(
transactions: string[],
encoding: 'base58' | 'base64' = 'base64'
): Promise<string> {
const response = await fetch(`${this.endpoint}/api/v1/bundles`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'sendBundle',
params: [transactions, { encoding }],
}),
});
const result = await response.json();
if (result.error) {
throw new Error(`Bundle submission failed: ${result.error.message}`);
}
return result.result;
}
async sendTransaction(
transaction: string,
encoding: 'base58' | 'base64' = 'base64'
): Promise<string> {
const response = await fetch(`${this.endpoint}/api/v1/transactions`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'sendTransaction',
params: [transaction, { encoding }],
}),
});
const result = await response.json();
if (result.error) {
throw new Error(`Transaction failed: ${result.error.message}`);
}
return result.result;
}
async getBundleStatuses(bundleIds: string[]): Promise<any> {
const response = await fetch(`${this.endpoint}/api/v1/bundles`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'getBundleStatuses',
params: [bundleIds],
}),
});
return (await response.json()).result;
}
async getTipAccounts(): Promise<PublicKey[]> {
const response = await fetch(`${this.endpoint}/api/v1/bundles`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'getTipAccounts',
params: [],
}),
});
const result = await response.json();
return result.result.map((addr: string) => new PublicKey(addr));
}
}Creating and Sending Bundles
Basic Bundle Structure
A Jito bundle contains 1-5 transactions that execute atomically:
import {
Connection,
Keypair,
PublicKey,
SystemProgram,
Transaction,
TransactionMessage,
VersionedTransaction,
LAMPORTS_PER_SOL,
} from '@solana/web3.js';
async function createBundle(
connection: Connection,
payer: Keypair,
tipAccount: PublicKey
): Promise<VersionedTransaction[]> {
const { blockhash } = await connection.getLatestBlockhash();
// Transaction 1: Your main operation
const mainTx = new Transaction().add(
SystemProgram.transfer({
fromPubkey: payer.publicKey,
toPubkey: new PublicKey('RECIPIENT_ADDRESS'),
lamports: 0.1 * LAMPORTS_PER_SOL,
})
);
// Transaction 2: Tip to Jito (REQUIRED - must be last in bundle)
const tipAmount = 10000; // 0.00001 SOL - adjust based on priority needed
const tipTx = new Transaction().add(
SystemProgram.transfer({
fromPubkey: payer.publicKey,
toPubkey: tipAccount,
lamports: tipAmount,
})
);
// Convert to versioned transactions
const transactions = [mainTx, tipTx].map(tx => {
const message = new TransactionMessage({
payerKey: payer.publicKey,
recentBlockhash: blockhash,
instructions: tx.instructions,
}).compileToV0Message();
const versionedTx = new VersionedTransaction(message);
versionedTx.sign([payer]);
return versionedTx;
});
return transactions;
}Sending the Bundle
async function sendJitoBundle(
transactions: VersionedTransaction[],
jitoClient: JitoClient
): Promise<string> {
// Serialize transactions to base64
const serializedTxs = transactions.map(tx =>
Buffer.from(tx.serialize()).toString('base64')
);
// Send bundle
const bundleId = await jitoClient.sendBundle(serializedTxs, 'base64');
console.log('Bundle submitted:', bundleId);
return bundleId;
}
// Usage
const jitoClient = new JitoClient();
const tipAccounts = await jitoClient.getTipAccounts();
const tipAccount = tipAccounts[Math.floor(Math.random() * tipAccounts.length)];
const bundle = await createBundle(connection, wallet, tipAccount);
const bundleId = await sendJitoBundle(bundle, jitoClient);
// Check status
const status = await jitoClient.getBundleStatuses([bundleId]);
console.log('Bundle status:', status);Tip Optimization Strategies
Dynamic Tip Calculation
interface TipStrategy {
calculateTip(params: {
urgency: 'low' | 'medium' | 'high' | 'critical';
expectedProfit?: number;
networkCongestion?: number;
}): number;
}
const tipStrategy: TipStrategy = {
calculateTip({ urgency, expectedProfit, networkCongestion = 1 }) {
const baseTips = {
low: 1000, // 0.000001 SOL
medium: 10000, // 0.00001 SOL
high: 100000, // 0.0001 SOL
critical: 1000000, // 0.001 SOL
};
let tip = baseTips[urgency];
// Adjust for network congestion (1-10 scale)
tip *= networkCongestion;
// For profitable operations, tip a percentage of expected profit
if (expectedProfit && expectedProfit > 0) {
const profitBasedTip = Math.floor(expectedProfit * 0.1); // 10% of profit
tip = Math.max(tip, profitBasedTip);
}
return Math.floor(tip);
},
};
// Usage
const tip = tipStrategy.calculateTip({
urgency: 'high',
expectedProfit: 1000000, // 0.001 SOL profit
networkCongestion: 5,
});
console.log('Calculated tip:', tip, 'lamports');Priority Fee + Tip Split for sendTransaction
When using sendTransaction (not bundles), split fees:
async function createPriorityTransaction(
connection: Connection,
payer: Keypair,
instructions: TransactionInstruction[],
tipAccount: PublicKey,
totalFeebudget: number // in lamports
): Promise<VersionedTransaction> {
const { blockhash } = await connection.getLatestBlockhash();
// Split: 70% priority fee, 30% Jito tip
const priorityFee = Math.floor(totalFeebudget * 0.7);
const jitoTip = Math.floor(totalFeebudget * 0.3);
// Add compute budget instructions
const computeBudgetIx = ComputeBudgetProgram.setComputeUnitPrice({
microLamports: Math.floor(priorityFee / 200000), // Assuming 200k CU
});
const computeLimitIx = ComputeBudgetProgram.setComputeUnitLimit({
units: 200000,
});
// Add Jito tip
const tipIx = SystemProgram.transfer({
fromPubkey: payer.publicKey,
toPubkey: tipAccount,
lamports: jitoTip,
});
const allInstructions = [
computeBudgetIx,
computeLimitIx,
...instructions,
tipIx,
];
const message = new TransactionMessage({
payerKey: payer.publicKey,
recentBlockhash: blockhash,
instructions: allInstructions,
}).compileToV0Message();
const tx = new VersionedTransaction(message);
tx.sign([payer]);
return tx;
}MEV Protection: Swap Bundle Example
Protect your DEX swaps from sandwich attacks:
import {
PublicKey,
TransactionInstruction,
VersionedTransaction,
} from '@solana/web3.js';
interface SwapParams {
inputMint: PublicKey;
outputMint: PublicKey;
amount: number;
slippageBps: number;
}
async function createProtectedSwapBundle(
connection: Connection,
payer: Keypair,
swapParams: SwapParams,
tipAccount: PublicKey,
tipAmount: number
): Promise<VersionedTransaction[]> {
const { blockhash } = await connection.getLatestBlockhash();
// 1. Create swap instruction (using Jupiter, Raydium, etc.)
const swapInstruction = await createSwapInstruction(swapParams);
// 2. Build swap transaction
const swapTx = new VersionedTransaction(
new TransactionMessage({
payerKey: payer.publicKey,
recentBlockhash: blockhash,
instructions: [
ComputeBudgetProgram.setComputeUnitLimit({ units: 400000 }),
swapInstruction,
],
}).compileToV0Message()
);
swapTx.sign([payer]);
// 3. Build tip transaction (must be last)
const tipTx = new VersionedTransaction(
new TransactionMessage({
payerKey: payer.publicKey,
recentBlockhash: blockhash,
instructions: [
SystemProgram.transfer({
fromPubkey: payer.publicKey,
toPubkey: tipAccount,
lamports: tipAmount,
}),
],
}).compileToV0Message()
);
tipTx.sign([payer]);
// Bundle: [swap, tip]
// This ensures atomic execution - no one can sandwich your swap
return [swapTx, tipTx];
}
// Placeholder - implement with your preferred DEX
async function createSwapInstruction(
params: SwapParams
): Promise<TransactionInstruction> {
// Use Jupiter, Raydium, or Orca SDK to create swap instruction
throw new Error('Implement with your DEX SDK');
}Arbitrage Bundle Pattern
Execute multi-step arbitrage atomically:
interface ArbitrageOpportunity {
buyDex: 'jupiter' | 'raydium' | 'orca';
sellDex: 'jupiter' | 'raydium' | 'orca';
tokenMint: PublicKey;
buyAmount: number;
expectedProfit: number;
}
async function executeArbitrage(
connection: Connection,
payer: Keypair,
opportunity: ArbitrageOpportunity,
jitoClient: JitoClient
): Promise<string> {
const { blockhash } = await connection.getLatestBlockhash();
const tipAccounts = await jitoClient.getTipAccounts();
const tipAccount = tipAccounts[0];
// Calculate tip based on expected profit (10%)
const tipAmount = Math.floor(opportunity.expectedProfit * 0.1);
// Transaction 1: Buy on cheaper DEX
const buyIx = await createBuyInstruction(opportunity);
const buyTx = new VersionedTransaction(
new TransactionMessage({
payerKey: payer.publicKey,
recentBlockhash: blockhash,
instructions: [
ComputeBudgetProgram.setComputeUnitLimit({ units: 300000 }),
buyIx,
],
}).compileToV0Message()
);
buyTx.sign([payer]);
// Transaction 2: Sell on more expensive DEX
const sellIx = await createSellInstruction(opportunity);
const sellTx = new VersionedTransaction(
new TransactionMessage({
payerKey: payer.publicKey,
recentBlockhash: blockhash,
instructions: [
ComputeBudgetProgram.setComputeUnitLimit({ units: 300000 }),
sellIx,
],
}).compileToV0Message()
);
sellTx.sign([payer]);
// Transaction 3: Tip (must be last)
const tipTx = new VersionedTransaction(
new TransactionMessage({
payerKey: payer.publicKey,
recentBlockhash: blockhash,
instructions: [
SystemProgram.transfer({
fromPubkey: payer.publicKey,
toPubkey: tipAccount,
lamports: tipAmount,
}),
],
}).compileToV0Message()
);
tipTx.sign([payer]);
// Create and send bundle
const bundle = [buyTx, sellTx, tipTx];
const serializedBundle = bundle.map(tx =>
Buffer.from(tx.serialize()).toString('base64')
);
const bundleId = await jitoClient.sendBundle(serializedBundle, 'base64');
console.log(`Arbitrage bundle submitted: ${bundleId}`);
console.log(`Expected profit: ${opportunity.expectedProfit} lamports`);
console.log(`Tip paid: ${tipAmount} lamports`);
return bundleId;
}Production Error Handling
enum BundleStatus {
Invalid = 'Invalid',
Pending = 'Pending',
Failed = 'Failed',
Landed = 'Landed',
}
async function sendBundleWithRetry(
jitoClient: JitoClient,
bundle: VersionedTransaction[],
maxRetries: number = 3
): Promise<{ bundleId: string; status: BundleStatus }> {
const serializedBundle = bundle.map(tx =>
Buffer.from(tx.serialize()).toString('base64')
);
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const bundleId = await jitoClient.sendBundle(serializedBundle, 'base64');
console.log(`Attempt ${attempt}: Bundle submitted - ${bundleId}`);
// Poll for status
for (let i = 0; i < 30; i++) {
await sleep(1000);
const statuses = await jitoClient.getBundleStatuses([bundleId]);
if (statuses?.value?.[0]) {
const status = statuses.value[0];
if (status.confirmation_status === 'confirmed') {
return { bundleId, status: BundleStatus.Landed };
}
if (status.err) {
console.log(`Bundle failed: ${JSON.stringify(status.err)}`);
break;
}
}
}
console.log(`Attempt ${attempt}: Bundle did not land, retrying...`);
} catch (error) {
console.error(`Attempt ${attempt} failed:`, error);
if (attempt === maxRetries) {
throw error;
}
}
}
return { bundleId: '', status: BundleStatus.Failed };
}
function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}Monitoring Bundle Status
async function monitorBundle(
jitoClient: JitoClient,
bundleId: string,
timeoutMs: number = 60000
): Promise<{
landed: boolean;
slot?: number;
error?: string;
}> {
const startTime = Date.now();
while (Date.now() - startTime < timeoutMs) {
try {
const statuses = await jitoClient.getBundleStatuses([bundleId]);
const bundleStatus = statuses?.value?.[0];
if (!bundleStatus) {
await sleep(2000);
continue;
}
console.log('Bundle status:', {
bundleId: bundleStatus.bundle_id,
status: bundleStatus.confirmation_status,
slot: bundleStatus.slot,
});
if (bundleStatus.confirmation_status === 'confirmed' ||
bundleStatus.confirmation_status === 'finalized') {
return {
landed: true,
slot: bundleStatus.slot,
};
}
if (bundleStatus.err) {
return {
landed: false,
error: JSON.stringify(bundleStatus.err),
};
}
await sleep(2000);
} catch (error) {
console.error('Error checking bundle status:', error);
await sleep(2000);
}
}
return {
landed: false,
error: 'Timeout waiting for bundle confirmation',
};
}Common Pitfalls
- **Tip transaction not last** - The tip must be the final transaction in the bundle
- **Insufficient tip** - During high congestion, low tips won't land
- **Stale blockhash** - Bundles expire quickly; get fresh blockhash
- **Bundle too large** - Maximum 5 transactions per bundle
- **Missing signatures** - All transactions must be signed before submission
- **Wrong endpoint** - Use regional endpoints for lower latency
Best Practices
- **Dynamic tip adjustment** - Monitor network and adjust tips accordingly
- **Regional endpoints** - Use the closest Jito endpoint for lower latency
- **Retry logic** - Implement exponential backoff for failed bundles
- **Simulation first** - Simulate transactions before bundling
- **Monitor performance** - Track landing rates and adjust strategy
Next Steps and Resources
Learn More:
- [Jito Documentation](https://docs.jito.wtf)
- [Jito Discord](https://discord.gg/jito)
- [Block Engine API Reference](https://docs.jito.wtf/lowlatencytxnsend)
Advanced Topics:
- Implement a MEV searcher bot
- Build a liquidation bot with Jito bundles
- Create a limit order system with guaranteed execution
Building MEV-aware applications on Solana? Builderz has deep experience with Jito integration and high-performance trading systems. Contact us to discuss your project.



