Here's the deal: Jito Bundles let you slam up to 5 Solana transactions together so they either all land in the same block-in perfect order-or none do. Atomic as hell. No half assed fails where one tx succeeds and you're left screwed. Sounds clutch, right? Especially if you're swapping tokens across DEXs like Raydium and Orca, arbitraging, or just need sequential moves without the chaos of regular tx spam. Why does this matter? Solana's fast, but solo txs can get front run or dropped. Bundles tip validators (Jito runs ~95% stake) to prioritize yours. Tips start tiny-like 1000 lamports (0.000001 SOL)-but bump to 0.001 SOL or more in congestion. I usually start simple: memo txs for testing. Then real stuff like swaps. Let's build this.
Grab a wallet with ~0.01 SOL. Devnet for practice, mainnet for real. Install Node.js, then these:
npm init -ynpm i @solana/web3.js @solana program/memo @solana program/system @solana/kit jito ts Or skip kit, use raw web3.js-your call.npm i -g @solana/cli for keygen and balance checks.Make a keypair: solana keygen new -o ~/.config/solana/id.json. Fund it. Devnet? solana airdrop 2. Check: solana balance.
The thing is, Jito's Block Engine is at https://mainnet.block engine.jito.wtf/api/v1/bundles. Devnet version too. You'll POST bundles there. Tips go to their accounts-fetch 'em dynamically.
Okay, picture this. You craft 5 signed txs. Last one gets a tip: transfer lamports to a Jito tip account. Serialize to base64. Bundle 'em as an array. Send via JSON RPC.
Block Engine checks: sanity (≤5 txs, valid), simulates (will it pass?), auctions (highest tip wins). Only Jito Solana leaders process-95% coverage means you're good most slots.
Fail states? Tip too low, bad sim, non Jito leader. Bundles expire fast-retry with fresh blockhash.
In my experience, sim first. Saves SOL on duds.
Hardcode? Nah. Call getTipAccounts endpoint. Pick random to spread load. Example response: 8 pubkeys like HFqU5x63VTqvQss8hp11i4wVV8bD44PvwucfZ2bU7gRe. Send 1000-50000 lamports (0.000001-0.00005 SOL). More = priority.
Let's code. New file bundle.ts. I'll walk exact steps. Run with ts node bundle.ts (npm i ts node).
Why TS? Autocomplete rocks. Here's the full script-copy paste ready. Tweaks yours after.
import { Connection, Keypair, Transaction, LAMPORTSPERSOL, PublicKey } from '@solana/web3.js';
import { createMemoInstruction } from '@solana program/memo';
import { createTransferInstruction } from '@solana program/system';
import * as bs58 from 'bs58';
import fetch from 'node fetch'; // npm i node fetch const RPC = 'https://api.mainnet beta.solana.com'; // Or devnet
const BUNDLE_RPC = 'https://mainnet.block engine.jito.wtf/api/v1/bundles';
const payer = Keypair.fromSecretKey(bs58.decode('YOURBASE58SECRET_HERE')); // Or Uint8Array from JSON async function main() { const connection = new Connection(RPC); console.log('Payer:', payer.publicKey.toBase58());
} Replace secret. Load from file? import fs from 'fs'; const secret = JSON.parse(fs.readFileSync('.json', 'utf8')); payer = Keypair.fromSecretKey(new Uint8Array(secret));
// Get tip accounts const tipRes = await fetch('https://mainnet.block engine.jito.wtf/api/v1/tip_accounts'); const tips = await tipRes.json(); const tipAccount = new PublicKey(tips); // Randomize: tips[Math.floor(Math.random()*tips.length)] console.log('Tip acct:', tipAccount.toBase58()); // Recent blockhash const { blockhash } = await connection.getLatestBlockhash();
const bundle: Transaction[] = []; for (let i = 1; i <= 5; i++) { const tx = new Transaction({ recentBlockhash: blockhash, feePayer: payer.publicKey }); if (i < 5) { // Memo txs tx.add(createMemoInstruction(Jito bundle demo tx #${i})); } else { // Last: memo + tip (say 10000 lamports) tx.add(createMemoInstruction('Jito bundle demo tx #5 with tip!')); tx.add(createTransferInstruction( payer.publicKey, tipAccount, 10000 // Bump this )); } tx.sign(payer); bundle.push(tx); }
Short? Yeah. Real world: swaps via Jupiter SDK or Raydium instructions. Add token swaps here-output of tx1 feeds tx2.
const encodedBundle = bundle.map(tx => Buffer.from(tx.serialize()).toString('base64'));
const body = JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'sendBundle', params: [encodedBundle] }); const res = await fetch(BUNDLE_RPC, { method: 'POST', headers: { 'Content Type': 'application/json' }, body }); const json = await res.json(); console.log('Bundle ID:', json.result);
// Poll getBundleStatuses let status; for (let i = 0; i < 10; i++) { const statRes = await fetch('https://mainnet.block engine.jito.wtf/api/v1/bundlestatuses', { method: 'POST', headers: { 'Content Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'getBundleStatuses', params: [[json.result]] }) }); status = await statRes.json(); if (status.result?.value?.confirmationstatus === 'processed') break; await new Promise(r => setTimeout(r, 1000)); } console.log('Status:', status); // Explorer: https://explorer.solana.com/bundle/{bundleId}?cluster=mainnet console.log(Check: https://explorer.solana.com/bundle/${json.result});
} main().catch(console.error);
Run it. Boom-bundle ID spits out. Wait 1min, hit Jito explorer or Solana Explorer. All 5 txs? Same block, sequential. Tip tx shows transfer. Failed? Check sim (add before send: connection.simulateTransaction).
Okay, memos are cute. But atomic arb? Gold. Say SOL → USDC on Orca, USDC → SOL on Raydium. Profit check inside.
Don't code full Jupiter aggregator here-too long. But pattern: tx1 approve/spend SOL for USDC. Tx2 swap USDC back, assert profit > 0.0001 SOL or revert (via compute budget or CPI fail).
Steps tweak:
Example flow in code: Import Jupiter SDK, compute routes, pack into txs. Last tx: conditional revert if final SOL < start + profit.
Why bundles over single tx? Single tx max bytes. Bundles chain dependent outputs across txs. Like multi hop: SOL→BONK→SOL, each tx reads prev balance delta.
Bundles flop? Common crap:
| Issue | Fix |
|---|---|
| Bundle rejected instantly | Invalid txs. Sim each solo first. Blockhash stale? Refresh every 10s. |
| "Too low tip" | Scan recent bundles on explorer-what're others tipping? 0.001+ SOL in wars. |
| Not landing | Non Jito leader slot. Retry loop till Jito leader (stake weighted RPCs help). |
| Partial success | Impossible-atomic. But sim mismatches state locks. |
| High fees | Tip is extra 0.000001-0.01 SOL. Base tx ~0.000005 SOL each. |
In my experience, loop sends every slot with escalating tips. Track via websocket on bundle statuses.
State locks kill bundles-tx2 can't write acct tx1 reads unless sequential. Jito's BundleStage handles ordering.
simulateBundle. Params same as sendBundle.Scale? Rust for speed. JS fine for starters. I usually monitor via Helius or QuickNode RPCs-better slots.
3-hop arb: Tx1 SOL→LAYER (Orca). Tx2 LAYER→BONK (Raydium). Tx3 BONK→SOL (Jupiter), check profit.
Trick: Tx2 instruction reads post tx1 LAYER balance. Atomic, so outputs chain. Fail profit? Whole bundle reverts-no dust left.
Potential issue: Slippage. Build in 1% guards. Gas? Solana compute units ~1.4M/tx-bundles ~7M total.
Honesty hour: Congestion kills. Jan 2026? Post ETF pumps mean 0.01 SOL tips. Track via Dune dashboards.
solana create nonce account.Prevents copycats. Genius for sniping.
Post send, hit getBundleStatuses with ID. Statuses: PendingProcess, Processed, Landed, Failed.
Explorer: solana.fm or explorer.solana.com/bundle/{id}. Drill txs-see memos, tips. Same slot? Success. Timestamps match? Atomic win.
Thing is, explorer lags 30s. Use RPC confirmTransaction on first tx sig.
What's next? MEV bot wrapping this. But start here. Pretty much plug and play.