
Jito Bundles
- 195 installs
- 257 repo stars
- Updated June 24, 2026
- agiprolabs/claude-trading-skills
jito-bundles is a Claude Code skill that builds and submits atomic Jito bundles on Solana for MEV protection and competitive execution.
About
jito-bundles builds and submits Jito bundles of up to five Solana transactions that execute atomically for MEV protection and competitive execution. A developer uses it when swaps, arbitrage, or liquidations need front-run-resistant, ordered execution within a single slot. It documents tip mechanics, block-engine endpoints, and the sendBundle/getBundleStatuses API.
- Submits atomic Jito bundles (up to 5 txs) for MEV protection on Solana
- Ships build_bundle.py and check_bundle_status.py plus tip-strategy and bundle-API references
- EXECUTION skill with an explicit real-SOL safety warning and --demo default
Jito Bundles by the numbers
- 195 all-time installs (skills.sh)
- Ranked #102 of 479 Web3 & Blockchain skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
jito-bundles capabilities & compatibility
- Capabilities
- jito bundles · transaction execution · mev analysis
- Use cases
- api development · trading
What jito-bundles says it does
Jito bundles allow you to submit up to 5 Solana transactions that execute **atomically** — either all land in the same slot or none do.
Submitting bundles spends real SOL on tips. Always test with `--demo` mode first.
npx skills add https://github.com/agiprolabs/claude-trading-skills --skill jito-bundlesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 195 |
|---|---|
| repo stars | ★ 257 |
| Last updated | June 24, 2026 |
| Repository | agiprolabs/claude-trading-skills ↗ |
What it does
Build and submit atomic Jito bundles for MEV-protected Solana transaction execution.
Who is it for?
Atomic, front-run-resistant execution of swaps, arbitrage, or liquidations on Solana.
Skip if: Simple SOL transfers or time-insensitive swaps where priority fees suffice.
When should I use this skill?
You need MEV-protected, ordered execution of up to five Solana transactions in one slot.
By the numbers
- up to 5 txs per bundle
- 8 Jito tip accounts
- 4 block-engine regions
Files
Jito Bundle Submission for Solana
Jito bundles allow you to submit up to 5 Solana transactions that execute atomically — either all land in the same slot or none do. This is the primary mechanism for MEV protection and competitive transaction execution on Solana. Approximately 85%+ of Solana validators run the Jito-modified client, making bundles the standard for reliable, front-run-resistant execution.
EXECUTION SKILL — SAFETY WARNING: Submitting bundles spends real SOL on tips. Always test with --demo mode first. Never submit bundles with real funds without explicit confirmation. Default to simulation/dry-run in all scripts and examples.When to Use Bundles
| Scenario | Use Bundle? | Why |
|---|---|---|
| Swap on illiquid token | Yes | Prevents sandwich attacks |
| Multi-step arbitrage | Yes | Atomic execution prevents partial fills |
| Liquidation | Yes | Competitive — tip determines priority |
| Simple SOL transfer | No | Priority fees are cheaper and sufficient |
| Time-insensitive swap | Maybe | Bundles cost tips; priority fees may suffice |
| NFT mint / competitive action | Yes | Guarantees ordering within the slot |
Core Concepts
Bundle Anatomy
A Jito bundle is a JSON-RPC request containing 1-5 base58-encoded signed transactions. The transactions execute sequentially and atomically within a single slot.
Bundle = [Tx1, Tx2, ..., TxN] (N <= 5)
- All transactions must be signed
- Transactions execute in order: Tx1 → Tx2 → ... → TxN
- If ANY transaction fails, the ENTIRE bundle is dropped
- The tip instruction goes in the LAST transaction (last instruction)
- Bundle has ~2 slots (~800ms) to land before expiryTip Mechanism
Tips are SOL transfers to one of Jito's 8 tip accounts. The tip incentivizes validators to include your bundle.
# Tip is a standard SOL transfer instruction
tip_instruction = transfer(
from_pubkey=your_wallet,
to_pubkey=tip_account, # One of 8 Jito tip accounts
lamports=tip_amount # Tip in lamports (1 SOL = 1e9 lamports)
)
# Add as the LAST instruction of the LAST transaction in the bundleTip accounts are fetched dynamically via getTipAccounts. Rotate through them to distribute load.
Block Engine Endpoints
Jito operates geographically distributed block engines. Choose the one closest to your infrastructure:
| Region | Endpoint |
|---|---|
| New York | https://mainnet.block-engine.jito.wtf |
| Amsterdam | https://amsterdam.block-engine.jito.wtf |
| Frankfurt | https://frankfurt.block-engine.jito.wtf |
| Tokyo | https://tokyo.block-engine.jito.wtf |
All endpoints accept JSON-RPC over HTTPS on port 443. The /api/v1/bundles path handles bundle operations.
API Methods
sendBundle
Submit a bundle of up to 5 transactions.
import httpx
BLOCK_ENGINE = "https://mainnet.block-engine.jito.wtf"
payload = {
"jsonrpc": "2.0",
"id": 1,
"method": "sendBundle",
"params": [
[tx1_base58, tx2_base58], # List of base58-encoded signed txs
]
}
resp = httpx.post(f"{BLOCK_ENGINE}/api/v1/bundles", json=payload)
data = resp.json()
bundle_id = data["result"] # UUID stringgetBundleStatuses
Check the landing status of submitted bundles (up to 5 bundle IDs per request).
payload = {
"jsonrpc": "2.0",
"id": 1,
"method": "getBundleStatuses",
"params": [[bundle_id]]
}
resp = httpx.post(f"{BLOCK_ENGINE}/api/v1/bundles", json=payload)
statuses = resp.json()["result"]["value"]
# Each status: {bundle_id, status, slot, transactions: [{signature, ...}]}
# status: "Invalid", "Pending", "Failed", "Landed"getTipAccounts
Fetch the current list of Jito tip accounts.
payload = {
"jsonrpc": "2.0",
"id": 1,
"method": "getTipAccounts",
"params": []
}
resp = httpx.post(f"{BLOCK_ENGINE}/api/v1/bundles", json=payload)
tip_accounts = resp.json()["result"] # List of 8 base58 pubkeysgetInflightBundleStatuses
Check status of bundles that haven't landed yet (in-flight).
payload = {
"jsonrpc": "2.0",
"id": 1,
"method": "getInflightBundleStatuses",
"params": [[bundle_id]]
}
resp = httpx.post(f"{BLOCK_ENGINE}/api/v1/bundles", json=payload)
# status: "Pending", "Failed", "Landed"Bundle Construction Pattern
A typical bundle for a protected swap:
from solders.transaction import VersionedTransaction
from solders.message import MessageV0
from solders.instruction import Instruction
from solders.system_program import transfer, TransferParams
from solders.pubkey import Pubkey
import random
def build_protected_swap_bundle(
swap_ix: Instruction,
payer: Pubkey,
tip_lamports: int,
tip_accounts: list[str],
recent_blockhash: str,
) -> list[VersionedTransaction]:
"""Build a 1-tx bundle: swap + tip in the same transaction.
For simple swaps, a single-transaction bundle is sufficient.
The tip instruction is appended as the last instruction.
"""
# Pick a random tip account
tip_account = Pubkey.from_string(random.choice(tip_accounts))
# Tip instruction
tip_ix = transfer(TransferParams(
from_pubkey=payer,
to_pubkey=tip_account,
lamports=tip_lamports,
))
# Build transaction with swap + tip
msg = MessageV0.try_compile(
payer=payer,
instructions=[swap_ix, tip_ix],
address_lookup_table_accounts=[],
recent_blockhash=recent_blockhash,
)
tx = VersionedTransaction(msg, [keypair])
return [tx]Tip Sizing Guide
| Scenario | Tip Range (lamports) | Tip Range (SOL) |
|---|---|---|
| Normal swap (low urgency) | 1,000 - 10,000 | 0.000001 - 0.00001 |
| Normal swap (standard) | 10,000 - 50,000 | 0.00001 - 0.00005 |
| Competitive action (arb, liquidation) | 50,000 - 500,000 | 0.00005 - 0.0005 |
| Highly competitive (NFT mint, MEV) | 500,000 - 5,000,000 | 0.0005 - 0.005 |
| Emergency (must land this slot) | 5,000,000+ | 0.005+ |
Dynamic tip calculation based on recent tip levels:
def calculate_dynamic_tip(
base_tip: int = 10_000,
urgency_multiplier: float = 1.0,
recent_tip_percentile_50: int = 15_000,
) -> int:
"""Calculate tip based on urgency and recent network tips.
Args:
base_tip: Minimum tip in lamports.
urgency_multiplier: 1.0 = normal, 2.0 = urgent, 5.0 = critical.
recent_tip_percentile_50: Median tip from recent bundles.
Returns:
Tip amount in lamports.
"""
dynamic_tip = max(base_tip, int(recent_tip_percentile_50 * urgency_multiplier))
# Cap at 0.01 SOL to prevent accidents
return min(dynamic_tip, 10_000_000)Common Errors and Fixes
| Error | Cause | Fix |
|---|---|---|
Bundle dropped (slot expired) | Bundle didn't land within 2 slots | Retry with fresh blockhash; consider higher tip |
Transaction simulation failed | A tx in the bundle would fail on-chain | Simulate each tx individually to find the failing one |
Bundle already processed | Duplicate bundle ID | Expected on retry; check status instead |
Rate limited | Too many requests to block engine | Back off; rotate between block engine endpoints |
Invalid transaction | Malformed or unsigned transaction | Verify all txs are signed and base58-encoded |
Blockhash not found | Stale blockhash | Use getLatestBlockhash with finalized commitment |
Landing Rate Optimization
Strategies to maximize bundle landing probability:
1. Multi-region submission: Send the same bundle to multiple block engines simultaneously. The first to reach the current leader wins.
2. Fresh blockhash: Use getLatestBlockhash with confirmed commitment immediately before building. Stale blockhashes are the #1 cause of dropped bundles.
3. Retry with backoff: If a bundle doesn't land within 2-3 seconds, rebuild with a fresh blockhash and resubmit. Do NOT resubmit with the same blockhash.
4. Adequate tipping: Under-tipped bundles are deprioritized. Monitor the network's tip distribution and tip at or above the 50th percentile for your urgency level.
5. Minimal bundle size: Fewer transactions = less simulation time = higher landing rate. Use single-transaction bundles when possible.
async def submit_with_retry(
bundle_txs: list[str],
endpoints: list[str],
max_retries: int = 3,
) -> str | None:
"""Submit bundle to multiple endpoints with retry logic.
Returns bundle_id if submitted, None if all retries exhausted.
"""
for attempt in range(max_retries):
# Submit to all endpoints in parallel
async with httpx.AsyncClient() as client:
tasks = [
client.post(
f"{ep}/api/v1/bundles",
json={
"jsonrpc": "2.0", "id": 1,
"method": "sendBundle",
"params": [bundle_txs],
},
timeout=5.0,
)
for ep in endpoints
]
# Process first successful response
for resp in asyncio.as_completed(tasks):
result = (await resp).json()
if "result" in result:
return result["result"]
# Wait before retry with fresh blockhash
await asyncio.sleep(0.5 * (attempt + 1))
return NoneSafety Checklist (Execution)
Before submitting any bundle with real funds:
- [ ] Simulated all transactions individually via
simulateTransaction - [ ] Verified tip amount is reasonable (not accidentally SOL instead of lamports)
- [ ] Confirmed blockhash is fresh (< 60 seconds old)
- [ ] Verified all transactions are properly signed
- [ ] Checked wallet balance covers all transaction costs + tip
- [ ] Tested with devnet or --demo mode first
- [ ] Set maximum tip cap to prevent accidental overpayment
Files
References
references/bundle_api.md— Complete JSON-RPC API reference with request/response schemas and error codesreferences/tip_strategies.md— Tip calculation strategies, dynamic tipping, cost optimizationreferences/best_practices.md— Bundle construction patterns, landing rate optimization, common pitfalls
Scripts
scripts/build_bundle.py— Bundle construction with tip instruction;--demomode builds but does not submitscripts/check_bundle_status.py— Bundle status checking and tip account fetching;--demomode uses mock responses
Jito Bundles — Best Practices
Bundle Construction Patterns
Single-Transaction Bundle (Most Common)
For a simple protected swap, put everything in one transaction:
Transaction 1:
Instruction 1: Compute budget (set compute units)
Instruction 2: Compute budget (set priority fee — optional alongside tip)
Instruction 3: Swap instruction (e.g., Jupiter route)
Instruction 4: Tip transfer (LAST instruction)Advantages: Simplest to build, fastest to simulate, highest landing rate.
Multi-Transaction Bundle
For operations requiring atomicity across multiple transactions:
Transaction 1: Setup (create accounts, approve delegations)
Transaction 2: Execute (the core operation)
Transaction 3: Cleanup + Tip (close accounts, collect rent, tip as last ix)Rules:
- Tip goes in the LAST instruction of the LAST transaction
- Each transaction must be independently signed
- All transactions share the same recent blockhash
- If ANY transaction fails simulation, the entire bundle is dropped
- Transactions execute in order: Tx1 then Tx2 then Tx3
Arbitrage Bundle Pattern
Transaction 1: Buy on DEX A
Transaction 2: Sell on DEX B + TipThe atomicity guarantees you never get stuck with inventory — if the sell fails, the buy is also reverted.
Blockhash Management
The blockhash is the single most critical factor for bundle landing. Stale blockhashes are the #1 cause of dropped bundles.
Best practices: 1. Fetch getLatestBlockhash with confirmed commitment immediately before building 2. A blockhash is valid for approximately 60-90 seconds (150 slots) 3. For retries, ALWAYS fetch a new blockhash — never resubmit with the old one 4. Do not cache blockhashes for more than a few seconds
import httpx
import time
class BlockhashManager:
"""Manage blockhash freshness for bundle construction."""
def __init__(self, rpc_url: str, max_age_seconds: float = 5.0):
self.rpc_url = rpc_url
self.max_age = max_age_seconds
self._blockhash: str | None = None
self._fetched_at: float = 0.0
def get_fresh_blockhash(self) -> str:
"""Get a blockhash, refreshing if stale."""
if (
self._blockhash is None
or (time.time() - self._fetched_at) > self.max_age
):
resp = httpx.post(self.rpc_url, json={
"jsonrpc": "2.0", "id": 1,
"method": "getLatestBlockhash",
"params": [{"commitment": "confirmed"}],
})
data = resp.json()
self._blockhash = data["result"]["value"]["blockhash"]
self._fetched_at = time.time()
return self._blockhashLanding Rate Optimization
Multi-Region Submission
Submit the same bundle to all block engine endpoints simultaneously. The bundle reaching the current leader first wins.
JITO_ENDPOINTS = [
"https://mainnet.block-engine.jito.wtf/api/v1/bundles",
"https://amsterdam.block-engine.jito.wtf/api/v1/bundles",
"https://frankfurt.block-engine.jito.wtf/api/v1/bundles",
"https://tokyo.block-engine.jito.wtf/api/v1/bundles",
]
async def multi_region_submit(
bundle_txs: list[str],
endpoints: list[str] = JITO_ENDPOINTS,
) -> list[dict]:
"""Submit bundle to all endpoints in parallel."""
import asyncio
payload = {
"jsonrpc": "2.0", "id": 1,
"method": "sendBundle",
"params": [bundle_txs],
}
async with httpx.AsyncClient(timeout=5.0) as client:
tasks = [client.post(ep, json=payload) for ep in endpoints]
results = await asyncio.gather(*tasks, return_exceptions=True)
return [
r.json() if not isinstance(r, Exception) else {"error": str(r)}
for r in results
]Retry Strategy
import asyncio
async def submit_with_landing_check(
build_bundle_fn, # Callable that builds fresh bundle
max_attempts: int = 3,
poll_interval: float = 0.5,
poll_timeout: float = 3.0,
) -> dict:
"""Submit bundle, poll for landing, retry if needed."""
for attempt in range(max_attempts):
# Build fresh bundle (fresh blockhash each time)
bundle_txs = build_bundle_fn()
bundle_id = await submit_to_endpoints(bundle_txs)
if bundle_id is None:
continue
# Poll for landing
landed = await poll_bundle_status(
bundle_id, poll_interval, poll_timeout
)
if landed:
return {"status": "landed", "bundle_id": bundle_id,
"attempt": attempt + 1}
return {"status": "failed", "attempts": max_attempts}Compute Budget Optimization
Set compute units accurately to reduce simulation time:
from solders.compute_budget import set_compute_unit_limit
# Profile your transaction's actual compute usage, then add 10-20% buffer
compute_ix = set_compute_unit_limit(200_000) # Adjust based on profilingOver-allocating compute units doesn't cost more in fees, but accurate limits help validators simulate bundles faster.
Common Pitfalls
1. Tip in Wrong Position
The tip MUST be the last instruction of the last transaction. Placing it elsewhere causes bundle rejection.
# WRONG — tip is not last
instructions = [tip_ix, swap_ix]
# CORRECT — tip is last
instructions = [swap_ix, tip_ix]2. Unsigned Transactions
Every transaction in the bundle must be fully signed before base58 encoding. Partially signed transactions cause immediate rejection.
3. Inconsistent Blockhash
All transactions in a multi-tx bundle should use the same recent blockhash. Mixing blockhashes from different fetches can cause intermittent failures.
4. Transaction Too Large
Solana transactions have a 1232-byte limit. Adding a tip instruction uses ~52 bytes. If your swap instruction is near the limit, the tip may push it over. Solutions:
- Use versioned transactions with address lookup tables
- Split into a multi-transaction bundle
5. Simulating the Bundle as Individual Transactions
When debugging, simulate each transaction individually. But remember that in a bundle, Tx2 sees the state changes from Tx1. If Tx2 depends on Tx1's output (e.g., Tx1 creates an account that Tx2 uses), simulating Tx2 alone will fail.
6. Not Handling Partial Information
getBundleStatuses may return null for a bundle ID if:
- The bundle hasn't been processed yet (check inflight status)
- The bundle expired (rebuild and retry)
- The bundle ID is wrong
Always check for null entries in the response.
When NOT to Use Bundles
1. Simple SOL transfers: Priority fees are cheaper and sufficient 2. Low-value swaps (< 0.1 SOL): MEV risk is negligible; tip costs more than protection saves 3. Devnet/testnet: Jito bundles only work on mainnet 4. When you need finality guarantees: Bundles land or drop — they don't provide faster finality than normal transactions
Debugging Bundles
Step-by-step debugging workflow:
1. Simulate each transaction individually via simulateTransaction on your RPC 2. Check for program errors in simulation logs 3. Verify all accounts are correct and have sufficient balances 4. Check blockhash freshness — if the blockhash is > 60s old, refresh 5. Verify tip account is in the current getTipAccounts list 6. Check base58 encoding — ensure transactions are properly serialized
Logging Best Practices
Log these fields for every bundle submission:
- Bundle ID (from sendBundle response)
- Timestamp of submission
- Tip amount (lamports)
- Tip account used
- Blockhash used
- Number of transactions
- Block engine endpoint(s) submitted to
- Landing status (poll after 2-3 seconds)
import logging
logger = logging.getLogger("jito-bundles")
def log_bundle_submission(
bundle_id: str,
tip_lamports: int,
tip_account: str,
blockhash: str,
num_txs: int,
endpoints: list[str],
) -> None:
logger.info(
"Bundle submitted | id=%s tip=%d tip_acct=%s blockhash=%s "
"txs=%d endpoints=%s",
bundle_id, tip_lamports, tip_account[:8] + "...",
blockhash[:8] + "...", num_txs, len(endpoints),
)Jito Bundle API Reference
Overview
The Jito Bundle API uses JSON-RPC 2.0 over HTTPS. All requests go to /api/v1/bundles on a block engine endpoint.
Base URLs:
https://mainnet.block-engine.jito.wtf/api/v1/bundleshttps://amsterdam.block-engine.jito.wtf/api/v1/bundleshttps://frankfurt.block-engine.jito.wtf/api/v1/bundleshttps://tokyo.block-engine.jito.wtf/api/v1/bundles
Headers:
Content-Type: application/jsonSome endpoints may require a UUID auth token (obtained from the Jito dashboard) passed as a query parameter or header. Public endpoints (sendBundle, getBundleStatuses, getTipAccounts) generally do not require auth.
---
sendBundle
Submit a bundle of up to 5 signed transactions for atomic execution.
Request:
{
"jsonrpc": "2.0",
"id": 1,
"method": "sendBundle",
"params": [
["<base58_tx_1>", "<base58_tx_2>"]
]
}Parameters:
params[0](array of strings, required): List of 1-5 base58-encoded signed transactions. Transactions execute in order.
Response (success):
{
"jsonrpc": "2.0",
"id": 1,
"result": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}result(string): Bundle UUID for status tracking.
Response (error):
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32602,
"message": "Invalid params: bundle must contain 1-5 transactions"
}
}Error codes:
| Code | Message | Cause |
|---|---|---|
| -32600 | Invalid Request | Malformed JSON-RPC |
| -32601 | Method not found | Typo in method name |
| -32602 | Invalid params | Wrong param format, >5 txs, or invalid base58 |
| -32603 | Internal error | Block engine internal failure |
| -32000 | Bundle simulation failed | A transaction in the bundle fails simulation |
| -32001 | Rate limited | Too many requests; back off |
curl example:
curl -X POST "https://mainnet.block-engine.jito.wtf/api/v1/bundles" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "sendBundle",
"params": [["<base58_tx>"]]
}'---
getBundleStatuses
Check landing status of previously submitted bundles.
Request:
{
"jsonrpc": "2.0",
"id": 1,
"method": "getBundleStatuses",
"params": [
["a1b2c3d4-e5f6-7890-abcd-ef1234567890"]
]
}Parameters:
params[0](array of strings, required): List of 1-5 bundle UUIDs to check.
Response:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"context": {
"slot": 280000000
},
"value": [
{
"bundle_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"transactions": [
{
"signature": "5rGz...",
"slot": 280000000,
"confirmation_status": "confirmed",
"err": null
}
],
"slot": 280000000,
"confirmation_status": "confirmed",
"err": {
"Ok": null
}
}
]
}
}Status values in `confirmation_status`:
| Status | Meaning |
|---|---|
processed | Transaction seen by the cluster |
confirmed | Transaction confirmed by supermajority |
finalized | Transaction finalized (max confirmations) |
If bundle not found, the corresponding entry in value will be null. This means the bundle either expired, was never submitted, or is still in-flight. Use getInflightBundleStatuses to check in-flight bundles.
---
getTipAccounts
Fetch the current list of Jito tip payment accounts.
Request:
{
"jsonrpc": "2.0",
"id": 1,
"method": "getTipAccounts",
"params": []
}Parameters: None.
Response:
{
"jsonrpc": "2.0",
"id": 1,
"result": [
"96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5",
"HFqU5x63VTqvQss8hp11i4bVqkfRtQ7NmXwkiAMXBiap",
"Cw8CFyM9FkoMi7K7Crf6HNQqf4uEMzpKw6QNghXLvLkY",
"ADaUMid9yfUytqMBgopwjb2o2J3mF9Cp4vFsMhBBe6Vy",
"DfXygSm4jCyNCybVYYK6DwvWqjKee8pbDmJGcLWNDXjh",
"ADuUkR4vqLUMWXxW9gh6D6L8pMSawimctcNZ5pGwDcEt",
"DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL",
"3AVi9Tg9Uo68tJfuvoKvqKNWKkC5wPdSSdeBnizKZ6jT"
]
}Returns an array of 8 base58-encoded public keys. Select one randomly or rotate through them to distribute load across validators.
---
getInflightBundleStatuses
Check status of bundles that are still being processed (not yet landed or expired).
Request:
{
"jsonrpc": "2.0",
"id": 1,
"method": "getInflightBundleStatuses",
"params": [
["a1b2c3d4-e5f6-7890-abcd-ef1234567890"]
]
}Parameters:
params[0](array of strings, required): List of 1-5 bundle UUIDs.
Response:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"context": {
"slot": 280000000
},
"value": [
{
"bundle_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "Pending",
"landed_slot": null
}
]
}
}Status values:
| Status | Meaning |
|---|---|
Pending | Bundle received, awaiting inclusion |
Landed | Bundle successfully included in a block |
Failed | Bundle failed simulation or expired |
Invalid | Bundle was malformed or contained invalid transactions |
---
Rate Limits
- Public endpoints: approximately 5-10 requests/second per IP
- Authenticated endpoints: higher limits based on plan
sendBundle: subject to per-IP and per-bundle rate limitinggetBundleStatuses/getInflightBundleStatuses: relatively generous limits
Rate limit response:
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32001,
"message": "Rate limited. Please slow down."
}
}Mitigation strategies: 1. Rotate between block engine endpoints (NY, Amsterdam, Frankfurt, Tokyo) 2. Implement exponential backoff on rate limit errors 3. Batch status checks (up to 5 bundle IDs per request) 4. Cache tip accounts (refresh every 60 seconds, not every request)
---
Request/Response Schema Summary
| Method | Params | Result Type | Notes |
|---|---|---|---|
sendBundle | [[tx1, tx2, ...]] | string (UUID) | Max 5 txs |
getBundleStatuses | [[id1, id2, ...]] | {context, value} | Max 5 IDs |
getTipAccounts | [] | string[] | 8 accounts |
getInflightBundleStatuses | [[id1, id2, ...]] | {context, value} | Max 5 IDs |
Jito Tip Strategies
Overview
The tip is a SOL transfer added as the last instruction of the last transaction in a bundle. Tips incentivize Jito-enabled validators to include your bundle. The tip amount directly affects landing probability — under-tipped bundles are deprioritized relative to higher-tipping competitors.
Tip Account Selection
Jito maintains 8 tip accounts. Fetch them via getTipAccounts and rotate through them:
import random
def select_tip_account(tip_accounts: list[str]) -> str:
"""Select a random tip account to distribute validator load."""
return random.choice(tip_accounts)Cache tip accounts for 60 seconds. They rarely change, and fetching them on every bundle wastes API calls.
Static Tip Strategies
Fixed Tip
Set a constant tip amount. Simple but doesn't adapt to network conditions.
TIP_LAMPORTS = 25_000 # 0.000025 SOL — reasonable for standard swapsWhen to use: Low-urgency transactions where you don't mind occasional drops.
Tiered by Urgency
Pre-define tip levels by transaction urgency:
TIP_TIERS = {
"low": 5_000, # 0.000005 SOL — background tasks
"normal": 25_000, # 0.000025 SOL — standard swaps
"high": 100_000, # 0.0001 SOL — time-sensitive trades
"critical": 500_000, # 0.0005 SOL — must land this slot
"extreme": 5_000_000, # 0.005 SOL — competitive MEV/liquidation
}
def get_tip(urgency: str = "normal") -> int:
return TIP_TIERS.get(urgency, TIP_TIERS["normal"])Dynamic Tip Strategies
Percentile-Based Tipping
Query recent tip distribution and tip at a target percentile:
def calculate_percentile_tip(
recent_tips: list[int],
target_percentile: float = 0.50,
minimum: int = 5_000,
) -> int:
"""Tip at a given percentile of recent bundle tips.
Args:
recent_tips: List of recent tip amounts in lamports.
target_percentile: 0.0-1.0, where 0.5 = median.
minimum: Floor tip amount.
Returns:
Tip in lamports.
"""
if not recent_tips:
return minimum
sorted_tips = sorted(recent_tips)
idx = int(len(sorted_tips) * target_percentile)
idx = min(idx, len(sorted_tips) - 1)
return max(sorted_tips[idx], minimum)Guideline percentiles:
- 25th percentile: Economy — may drop during congestion
- 50th percentile: Standard — lands most of the time
- 75th percentile: Priority — high landing probability
- 90th+ percentile: Competitive — for time-critical operations
Congestion-Adjusted Tipping
Scale tips based on network congestion signals:
def congestion_adjusted_tip(
base_tip: int,
recent_slot_time_ms: float,
avg_slot_time_ms: float = 400.0,
max_multiplier: float = 5.0,
) -> int:
"""Increase tip when slots are slower (congested).
When slots take longer than average, the network is congested
and competition for block space increases.
"""
if recent_slot_time_ms <= avg_slot_time_ms:
return base_tip
# Congestion ratio: 1.0 = normal, 2.0 = slots taking 2x longer
congestion_ratio = recent_slot_time_ms / avg_slot_time_ms
multiplier = min(congestion_ratio, max_multiplier)
return int(base_tip * multiplier)Escalating Retry Tips
Increase tip on each retry attempt:
def escalating_tip(
base_tip: int,
attempt: int,
escalation_factor: float = 1.5,
max_tip: int = 1_000_000,
) -> int:
"""Increase tip with each failed attempt.
Args:
base_tip: Starting tip in lamports.
attempt: Current attempt number (0-indexed).
escalation_factor: Multiplier per attempt.
max_tip: Maximum tip cap to prevent accidents.
Returns:
Tip in lamports, capped at max_tip.
"""
tip = int(base_tip * (escalation_factor ** attempt))
return min(tip, max_tip)
# attempt 0: 25,000
# attempt 1: 37,500
# attempt 2: 56,250
# attempt 3: 84,375 (capped if > max_tip)Cost Optimization
Tip Budgeting
Set a per-trade tip budget and track spending:
class TipBudget:
"""Track tip spending against a budget."""
def __init__(self, daily_budget_lamports: int = 5_000_000):
self.daily_budget = daily_budget_lamports
self.spent_today = 0
def can_afford(self, tip: int) -> bool:
return (self.spent_today + tip) <= self.daily_budget
def record_tip(self, tip: int) -> None:
self.spent_today += tip
def remaining(self) -> int:
return max(0, self.daily_budget - self.spent_today)
def reset_daily(self) -> None:
self.spent_today = 0Bundle vs Priority Fee Decision
Not every transaction needs a bundle. Compare costs:
def should_use_bundle(
trade_size_lamports: int,
estimated_mev_risk_bps: float,
bundle_tip: int,
priority_fee: int,
) -> bool:
"""Decide whether a bundle is worth the extra tip cost.
Args:
trade_size_lamports: Size of the trade in lamports.
estimated_mev_risk_bps: Estimated MEV loss in basis points.
bundle_tip: Cost of bundle tip in lamports.
priority_fee: Cost of priority fee in lamports.
Returns:
True if bundle protection saves more than it costs.
"""
mev_cost = int(trade_size_lamports * estimated_mev_risk_bps / 10_000)
bundle_extra_cost = bundle_tip - priority_fee
return mev_cost > bundle_extra_costRule of thumb: If MEV risk (in lamports) exceeds the bundle tip premium over a priority fee, use a bundle.
Tip Minimization
For non-competitive transactions (no one else is trying to do the same trade):
1. Start with the minimum viable tip (5,000-10,000 lamports) 2. If dropped, retry with 1.5x the tip 3. Track your personal landing rate at each tip level 4. Find the minimum tip that gives you an acceptable landing rate (>80%)
def find_minimum_viable_tip(
landing_rates: dict[int, float],
target_rate: float = 0.80,
) -> int:
"""Find the lowest tip that achieves the target landing rate.
Args:
landing_rates: {tip_amount: landing_rate} from historical data.
target_rate: Minimum acceptable landing rate (0.0-1.0).
Returns:
Minimum tip amount meeting the target.
"""
for tip, rate in sorted(landing_rates.items()):
if rate >= target_rate:
return tip
# If no tip meets the target, return the highest tested
return max(landing_rates.keys()) if landing_rates else 50_000Safety Guardrails
Always implement tip caps to prevent accidental overpayment:
MAX_TIP_LAMPORTS = 10_000_000 # 0.01 SOL hard cap
def safe_tip(tip: int) -> int:
"""Apply safety cap to tip amount."""
if tip > MAX_TIP_LAMPORTS:
print(f"WARNING: Tip {tip} exceeds cap {MAX_TIP_LAMPORTS}, capping")
return min(tip, MAX_TIP_LAMPORTS)Common tip mistakes:
- Using SOL instead of lamports (1 SOL = 1,000,000,000 lamports)
- Not capping dynamic tips (congestion spike = 100x tip)
- Tipping on every retry with the same blockhash (waste; bundle won't land anyway)
- Over-tipping non-competitive transactions (nobody is competing for your swap)
#!/usr/bin/env python3
"""Build a Jito bundle with tip instruction for MEV-protected execution.
Demonstrates bundle construction with a SOL transfer + tip. In --demo mode,
builds the bundle payload locally using mock data without submitting to the
block engine or requiring any API keys.
SAFETY WARNING: Without --demo, this script submits real bundles that spend
real SOL on tips. Always test with --demo first.
Usage:
python scripts/build_bundle.py --demo
python scripts/build_bundle.py --tip 25000 --endpoint mainnet
Dependencies:
uv pip install httpx
Environment Variables:
JITO_BLOCK_ENGINE: Block engine endpoint (default: mainnet)
SOLANA_RPC_URL: Solana RPC endpoint for blockhash fetching
"""
import argparse
import json
import os
import sys
import time
import random
import base64
import hashlib
from typing import Optional
# ── Configuration ───────────────────────────────────────────────────
BLOCK_ENGINE_URLS = {
"mainnet": "https://mainnet.block-engine.jito.wtf/api/v1/bundles",
"amsterdam": "https://amsterdam.block-engine.jito.wtf/api/v1/bundles",
"frankfurt": "https://frankfurt.block-engine.jito.wtf/api/v1/bundles",
"tokyo": "https://tokyo.block-engine.jito.wtf/api/v1/bundles",
}
DEFAULT_TIP_LAMPORTS = 25_000 # 0.000025 SOL
MAX_TIP_LAMPORTS = 10_000_000 # 0.01 SOL safety cap
# Known Jito tip accounts (fetched dynamically in production)
DEMO_TIP_ACCOUNTS = [
"96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5",
"HFqU5x63VTqvQss8hp11i4bVqkfRtQ7NmXwkiAMXBiap",
"Cw8CFyM9FkoMi7K7Crf6HNQqf4uEMzpKw6QNghXLvLkY",
"ADaUMid9yfUytqMBgopwjb2o2J3mF9Cp4vFsMhBBe6Vy",
"DfXygSm4jCyNCybVYYK6DwvWqjKee8pbDmJGcLWNDXjh",
"ADuUkR4vqLUMWXxW9gh6D6L8pMSawimctcNZ5pGwDcEt",
"DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL",
"3AVi9Tg9Uo68tJfuvoKvqKNWKkC5wPdSSdeBnizKZ6jT",
]
# ── Helper Functions ────────────────────────────────────────────────
def select_tip_account(tip_accounts: list[str]) -> str:
"""Select a random tip account for load distribution.
Args:
tip_accounts: List of Jito tip account public keys.
Returns:
A randomly selected tip account public key.
"""
return random.choice(tip_accounts)
def safe_tip(tip_lamports: int) -> int:
"""Apply safety cap to tip amount.
Args:
tip_lamports: Requested tip in lamports.
Returns:
Tip capped at MAX_TIP_LAMPORTS.
"""
if tip_lamports > MAX_TIP_LAMPORTS:
print(
f"WARNING: Tip {tip_lamports} lamports exceeds safety cap "
f"{MAX_TIP_LAMPORTS}. Capping to {MAX_TIP_LAMPORTS}."
)
return MAX_TIP_LAMPORTS
if tip_lamports < 0:
print("WARNING: Negative tip not allowed. Using 0.")
return 0
return tip_lamports
def lamports_to_sol(lamports: int) -> float:
"""Convert lamports to SOL.
Args:
lamports: Amount in lamports.
Returns:
Amount in SOL.
"""
return lamports / 1_000_000_000
def build_demo_bundle_payload(
tip_lamports: int,
tip_account: str,
num_transactions: int = 1,
) -> dict:
"""Build a mock bundle payload for demonstration.
This constructs the JSON-RPC payload structure that would be sent
to the Jito block engine. In demo mode, the transactions are
placeholder strings (not real signed transactions).
Args:
tip_lamports: Tip amount in lamports.
tip_account: Selected Jito tip account.
num_transactions: Number of transactions in the bundle (1-5).
Returns:
JSON-RPC request payload dict.
"""
if num_transactions < 1 or num_transactions > 5:
raise ValueError("Bundle must contain 1-5 transactions")
# Generate mock base58-encoded "transactions"
mock_txs = []
for i in range(num_transactions):
# Create a deterministic mock transaction for reproducibility
mock_data = f"demo_tx_{i}_{tip_lamports}_{tip_account[:8]}"
mock_hash = hashlib.sha256(mock_data.encode()).hexdigest()
mock_txs.append(f"DEMO_{mock_hash[:44]}")
payload = {
"jsonrpc": "2.0",
"id": 1,
"method": "sendBundle",
"params": [mock_txs],
}
return payload
def fetch_tip_accounts(block_engine_url: str) -> list[str]:
"""Fetch current Jito tip accounts from the block engine.
Args:
block_engine_url: Jito block engine API URL.
Returns:
List of tip account public keys.
Raises:
httpx.HTTPStatusError: On non-2xx response.
RuntimeError: If the response format is unexpected.
"""
import httpx
payload = {
"jsonrpc": "2.0",
"id": 1,
"method": "getTipAccounts",
"params": [],
}
resp = httpx.post(block_engine_url, json=payload, timeout=10.0)
resp.raise_for_status()
data = resp.json()
if "error" in data:
raise RuntimeError(f"getTipAccounts error: {data['error']}")
if "result" not in data or not isinstance(data["result"], list):
raise RuntimeError(f"Unexpected response format: {data}")
return data["result"]
def submit_bundle(
block_engine_url: str,
bundle_txs: list[str],
) -> str:
"""Submit a bundle to the Jito block engine.
Args:
block_engine_url: Jito block engine API URL.
bundle_txs: List of base58-encoded signed transactions.
Returns:
Bundle UUID string.
Raises:
httpx.HTTPStatusError: On non-2xx response.
RuntimeError: If the bundle submission returns an error.
"""
import httpx
payload = {
"jsonrpc": "2.0",
"id": 1,
"method": "sendBundle",
"params": [bundle_txs],
}
resp = httpx.post(block_engine_url, json=payload, timeout=10.0)
resp.raise_for_status()
data = resp.json()
if "error" in data:
raise RuntimeError(
f"sendBundle error: {data['error'].get('message', data['error'])}"
)
if "result" not in data:
raise RuntimeError(f"Unexpected response: {data}")
return data["result"]
# ── Demo Mode ───────────────────────────────────────────────────────
def run_demo(tip_lamports: int, num_transactions: int = 1) -> None:
"""Run bundle construction in demo mode with mock data.
Args:
tip_lamports: Tip amount in lamports.
num_transactions: Number of transactions in the bundle.
"""
print("=" * 60)
print("JITO BUNDLE BUILDER — DEMO MODE")
print("=" * 60)
print()
# Step 1: Select tip account
tip_account = select_tip_account(DEMO_TIP_ACCOUNTS)
print(f"[1/4] Selected tip account: {tip_account}")
print(f" (randomly chosen from {len(DEMO_TIP_ACCOUNTS)} accounts)")
print()
# Step 2: Calculate tip
final_tip = safe_tip(tip_lamports)
print(f"[2/4] Tip amount: {final_tip:,} lamports ({lamports_to_sol(final_tip):.9f} SOL)")
print(f" Safety cap: {MAX_TIP_LAMPORTS:,} lamports ({lamports_to_sol(MAX_TIP_LAMPORTS):.6f} SOL)")
print()
# Step 3: Build bundle payload
payload = build_demo_bundle_payload(
tip_lamports=final_tip,
tip_account=tip_account,
num_transactions=num_transactions,
)
print(f"[3/4] Built bundle with {num_transactions} transaction(s)")
print(f" Method: {payload['method']}")
print(f" Transactions in bundle: {len(payload['params'][0])}")
print()
# Step 4: Show what would be sent
print("[4/4] Bundle payload (would be sent to block engine):")
print("-" * 60)
print(json.dumps(payload, indent=2))
print("-" * 60)
print()
# Summary
print("BUNDLE CONSTRUCTION SUMMARY")
print(f" Transactions: {num_transactions}")
print(f" Tip account: {tip_account}")
print(f" Tip amount: {final_tip:,} lamports")
print(f" Tip (SOL): {lamports_to_sol(final_tip):.9f} SOL")
print(f" Tip position: Last instruction of transaction {num_transactions}")
print(f" Status: NOT SUBMITTED (demo mode)")
print()
print("To submit for real, run without --demo (requires signed transactions).")
# ── Live Mode ───────────────────────────────────────────────────────
def run_live(
tip_lamports: int,
endpoint_name: str,
) -> None:
"""Run bundle submission against the live block engine.
This function demonstrates the submission flow but requires real
signed transactions to actually work. It fetches tip accounts
from the block engine and shows the submission process.
Args:
tip_lamports: Tip amount in lamports.
endpoint_name: Block engine endpoint name.
"""
block_engine_url = BLOCK_ENGINE_URLS.get(endpoint_name)
if not block_engine_url:
print(f"Unknown endpoint: {endpoint_name}")
print(f"Available: {', '.join(BLOCK_ENGINE_URLS.keys())}")
sys.exit(1)
print("=" * 60)
print("JITO BUNDLE BUILDER — LIVE MODE")
print(f"Endpoint: {block_engine_url}")
print("=" * 60)
print()
print("WARNING: Live mode requires real signed transactions.")
print("This demonstration fetches tip accounts but does not submit")
print("because building real transactions requires wallet keys.")
print()
try:
print("[1/3] Fetching tip accounts from block engine...")
tip_accounts = fetch_tip_accounts(block_engine_url)
print(f" Retrieved {len(tip_accounts)} tip accounts:")
for i, acct in enumerate(tip_accounts):
print(f" [{i}] {acct}")
print()
tip_account = select_tip_account(tip_accounts)
final_tip = safe_tip(tip_lamports)
print(f"[2/3] Selected tip account: {tip_account}")
print(f" Tip: {final_tip:,} lamports ({lamports_to_sol(final_tip):.9f} SOL)")
print()
print("[3/3] To submit a real bundle, you would need to:")
print(" 1. Build transaction(s) with solders or solana-py")
print(" 2. Add tip transfer as last instruction of last tx")
print(" 3. Sign all transactions with your wallet keypair")
print(" 4. Base58-encode the signed transactions")
print(" 5. Call sendBundle with the encoded transactions")
print()
print("See SKILL.md for complete bundle construction examples.")
except Exception as e:
print(f"Error: {e}")
print("Check your network connection and endpoint availability.")
sys.exit(1)
# ── Main ────────────────────────────────────────────────────────────
def parse_args() -> argparse.Namespace:
"""Parse command line arguments.
Returns:
Parsed arguments namespace.
"""
parser = argparse.ArgumentParser(
description="Build a Jito bundle with tip instruction",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=(
"Examples:\n"
" python scripts/build_bundle.py --demo\n"
" python scripts/build_bundle.py --demo --tip 50000 --txs 3\n"
" python scripts/build_bundle.py --endpoint mainnet --tip 25000\n"
),
)
parser.add_argument(
"--demo",
action="store_true",
help="Run in demo mode with mock data (no API calls, no keys needed)",
)
parser.add_argument(
"--tip",
type=int,
default=DEFAULT_TIP_LAMPORTS,
help=f"Tip amount in lamports (default: {DEFAULT_TIP_LAMPORTS})",
)
parser.add_argument(
"--txs",
type=int,
default=1,
choices=range(1, 6),
help="Number of transactions in the bundle (1-5, default: 1)",
)
parser.add_argument(
"--endpoint",
type=str,
default="mainnet",
choices=list(BLOCK_ENGINE_URLS.keys()),
help="Block engine endpoint (default: mainnet)",
)
return parser.parse_args()
if __name__ == "__main__":
args = parse_args()
if args.demo:
run_demo(tip_lamports=args.tip, num_transactions=args.txs)
else:
run_live(tip_lamports=args.tip, endpoint_name=args.endpoint)
#!/usr/bin/env python3
"""Check Jito bundle status and fetch tip accounts.
In --demo mode, uses mock responses to demonstrate the status-checking
workflow without requiring network access or API keys. In live mode,
queries the Jito block engine for real bundle statuses.
Usage:
python scripts/check_bundle_status.py --demo
python scripts/check_bundle_status.py --demo --bundle-id abc123
python scripts/check_bundle_status.py --bundle-id <real-uuid> --endpoint mainnet
python scripts/check_bundle_status.py --tip-accounts --endpoint mainnet
Dependencies:
uv pip install httpx
Environment Variables:
JITO_BLOCK_ENGINE: Block engine endpoint name (default: mainnet)
"""
import argparse
import json
import os
import sys
import time
from typing import Optional
# ── Configuration ───────────────────────────────────────────────────
BLOCK_ENGINE_URLS = {
"mainnet": "https://mainnet.block-engine.jito.wtf/api/v1/bundles",
"amsterdam": "https://amsterdam.block-engine.jito.wtf/api/v1/bundles",
"frankfurt": "https://frankfurt.block-engine.jito.wtf/api/v1/bundles",
"tokyo": "https://tokyo.block-engine.jito.wtf/api/v1/bundles",
}
# Mock data for demo mode
DEMO_TIP_ACCOUNTS = [
"96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5",
"HFqU5x63VTqvQss8hp11i4bVqkfRtQ7NmXwkiAMXBiap",
"Cw8CFyM9FkoMi7K7Crf6HNQqf4uEMzpKw6QNghXLvLkY",
"ADaUMid9yfUytqMBgopwjb2o2J3mF9Cp4vFsMhBBe6Vy",
"DfXygSm4jCyNCybVYYK6DwvWqjKee8pbDmJGcLWNDXjh",
"ADuUkR4vqLUMWXxW9gh6D6L8pMSawimctcNZ5pGwDcEt",
"DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL",
"3AVi9Tg9Uo68tJfuvoKvqKNWKkC5wPdSSdeBnizKZ6jT",
]
DEMO_BUNDLE_STATUSES = {
"landed": {
"bundle_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"transactions": [
{
"signature": "5rGzK8mVpEr2nT9xQwYJk3Fp4BsVH7m1dC6yNqRfW8aL"
"p2sXjMv4uDhZ9bK1cA3nE7tG6wF5rHq8iJ0kL",
"slot": 280000000,
"confirmation_status": "confirmed",
"err": None,
}
],
"slot": 280000000,
"confirmation_status": "confirmed",
"err": {"Ok": None},
},
"pending": {
"bundle_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"status": "Pending",
"landed_slot": None,
},
"failed": {
"bundle_id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"status": "Failed",
"landed_slot": None,
},
}
# ── Core Functions ──────────────────────────────────────────────────
def fetch_tip_accounts_live(block_engine_url: str) -> list[str]:
"""Fetch tip accounts from the live Jito block engine.
Args:
block_engine_url: Full URL to the block engine bundles API.
Returns:
List of tip account public keys.
Raises:
httpx.HTTPStatusError: On non-2xx response.
RuntimeError: On unexpected response format.
"""
import httpx
payload = {
"jsonrpc": "2.0",
"id": 1,
"method": "getTipAccounts",
"params": [],
}
resp = httpx.post(block_engine_url, json=payload, timeout=10.0)
resp.raise_for_status()
data = resp.json()
if "error" in data:
raise RuntimeError(f"getTipAccounts error: {data['error']}")
return data.get("result", [])
def get_bundle_statuses_live(
block_engine_url: str,
bundle_ids: list[str],
) -> list[Optional[dict]]:
"""Check bundle statuses from the live Jito block engine.
Args:
block_engine_url: Full URL to the block engine bundles API.
bundle_ids: List of bundle UUIDs to check (max 5).
Returns:
List of status dicts (or None for unknown bundles).
Raises:
httpx.HTTPStatusError: On non-2xx response.
RuntimeError: On unexpected response format.
"""
import httpx
if len(bundle_ids) > 5:
raise ValueError("Maximum 5 bundle IDs per request")
payload = {
"jsonrpc": "2.0",
"id": 1,
"method": "getBundleStatuses",
"params": [bundle_ids],
}
resp = httpx.post(block_engine_url, json=payload, timeout=10.0)
resp.raise_for_status()
data = resp.json()
if "error" in data:
raise RuntimeError(f"getBundleStatuses error: {data['error']}")
result = data.get("result", {})
return result.get("value", [])
def get_inflight_statuses_live(
block_engine_url: str,
bundle_ids: list[str],
) -> list[Optional[dict]]:
"""Check in-flight bundle statuses from the live Jito block engine.
Args:
block_engine_url: Full URL to the block engine bundles API.
bundle_ids: List of bundle UUIDs to check (max 5).
Returns:
List of in-flight status dicts.
Raises:
httpx.HTTPStatusError: On non-2xx response.
RuntimeError: On unexpected response format.
"""
import httpx
if len(bundle_ids) > 5:
raise ValueError("Maximum 5 bundle IDs per request")
payload = {
"jsonrpc": "2.0",
"id": 1,
"method": "getInflightBundleStatuses",
"params": [bundle_ids],
}
resp = httpx.post(block_engine_url, json=payload, timeout=10.0)
resp.raise_for_status()
data = resp.json()
if "error" in data:
raise RuntimeError(
f"getInflightBundleStatuses error: {data['error']}"
)
result = data.get("result", {})
return result.get("value", [])
def format_status(status: Optional[dict]) -> str:
"""Format a bundle status dict into a human-readable string.
Args:
status: Bundle status dict from the API, or None.
Returns:
Formatted status string.
"""
if status is None:
return " Status: NOT FOUND (expired, invalid ID, or not yet processed)"
lines = []
bundle_id = status.get("bundle_id", "unknown")
lines.append(f" Bundle ID: {bundle_id}")
# getBundleStatuses format
if "confirmation_status" in status:
lines.append(f" Confirmation: {status['confirmation_status']}")
lines.append(f" Slot: {status.get('slot', 'unknown')}")
err = status.get("err", {})
if err and err.get("Ok") is None:
lines.append(" Error: None (success)")
elif err:
lines.append(f" Error: {err}")
txs = status.get("transactions", [])
if txs:
lines.append(f" Transactions ({len(txs)}):")
for tx in txs:
sig = tx.get("signature", "unknown")
lines.append(f" Sig: {sig[:20]}...")
lines.append(f" Slot: {tx.get('slot', 'unknown')}")
lines.append(
f" Status: {tx.get('confirmation_status', 'unknown')}"
)
# getInflightBundleStatuses format
elif "status" in status:
lines.append(f" Status: {status['status']}")
landed = status.get("landed_slot")
if landed:
lines.append(f" Landed Slot: {landed}")
return "\n".join(lines)
# ── Demo Mode ───────────────────────────────────────────────────────
def run_demo_tip_accounts() -> None:
"""Display mock tip accounts in demo mode."""
print("TIP ACCOUNTS (demo data):")
print("-" * 60)
for i, account in enumerate(DEMO_TIP_ACCOUNTS):
print(f" [{i}] {account}")
print(f"\nTotal: {len(DEMO_TIP_ACCOUNTS)} accounts")
print("Tip account selection: random rotation recommended")
def run_demo_status(bundle_id: Optional[str]) -> None:
"""Display mock bundle statuses in demo mode.
Args:
bundle_id: Optional bundle ID to check. If None, shows all demo statuses.
"""
print("BUNDLE STATUS CHECK (demo data):")
print("-" * 60)
if bundle_id:
# Show a single mock status
print(f"\nChecking bundle: {bundle_id}")
print()
# Use the "landed" mock for any provided ID
mock = DEMO_BUNDLE_STATUSES["landed"].copy()
mock["bundle_id"] = bundle_id
print(format_status(mock))
else:
# Show all demo scenarios
print("\nScenario 1 — LANDED bundle:")
print(format_status(DEMO_BUNDLE_STATUSES["landed"]))
print()
print("Scenario 2 — PENDING bundle (in-flight):")
print(format_status(DEMO_BUNDLE_STATUSES["pending"]))
print()
print("Scenario 3 — FAILED bundle:")
print(format_status(DEMO_BUNDLE_STATUSES["failed"]))
print()
print("Scenario 4 — NOT FOUND (expired or invalid):")
print(format_status(None))
def run_demo(bundle_id: Optional[str], show_tip_accounts: bool) -> None:
"""Run the full demo workflow.
Args:
bundle_id: Optional bundle ID to check status for.
show_tip_accounts: Whether to display tip accounts.
"""
print("=" * 60)
print("JITO BUNDLE STATUS CHECKER — DEMO MODE")
print("=" * 60)
print()
if show_tip_accounts:
run_demo_tip_accounts()
print()
run_demo_status(bundle_id)
print()
print("STATUS INTERPRETATION:")
print(" Landed — Bundle successfully included in a block")
print(" Pending — Bundle received, waiting for slot inclusion")
print(" Failed — Bundle failed simulation or expired")
print(" Not Found — Bundle expired, ID invalid, or not yet seen")
print()
print("RECOMMENDED WORKFLOW:")
print(" 1. Submit bundle via sendBundle → get bundle_id")
print(" 2. Wait 1-2 seconds")
print(" 3. Check getInflightBundleStatuses (for in-flight bundles)")
print(" 4. Check getBundleStatuses (for landed bundles)")
print(" 5. If not found after 3s, rebuild with fresh blockhash and retry")
# ── Live Mode ───────────────────────────────────────────────────────
def run_live(
bundle_id: Optional[str],
show_tip_accounts: bool,
endpoint_name: str,
) -> None:
"""Run against the live Jito block engine.
Args:
bundle_id: Bundle UUID to check status for.
show_tip_accounts: Whether to fetch and display tip accounts.
endpoint_name: Block engine endpoint name.
"""
block_engine_url = BLOCK_ENGINE_URLS.get(endpoint_name)
if not block_engine_url:
print(f"Unknown endpoint: {endpoint_name}")
print(f"Available: {', '.join(BLOCK_ENGINE_URLS.keys())}")
sys.exit(1)
print("=" * 60)
print("JITO BUNDLE STATUS CHECKER — LIVE MODE")
print(f"Endpoint: {block_engine_url}")
print("=" * 60)
print()
try:
if show_tip_accounts:
print("Fetching tip accounts...")
accounts = fetch_tip_accounts_live(block_engine_url)
print(f"Retrieved {len(accounts)} tip accounts:")
for i, acct in enumerate(accounts):
print(f" [{i}] {acct}")
print()
if bundle_id:
print(f"Checking bundle: {bundle_id}")
print()
# Try getBundleStatuses first
print("[getBundleStatuses]")
statuses = get_bundle_statuses_live(block_engine_url, [bundle_id])
if statuses:
for s in statuses:
print(format_status(s))
else:
print(" No results returned")
print()
# Also check in-flight
print("[getInflightBundleStatuses]")
inflight = get_inflight_statuses_live(
block_engine_url, [bundle_id]
)
if inflight:
for s in inflight:
print(format_status(s))
else:
print(" No in-flight results (bundle may have landed or expired)")
elif not show_tip_accounts:
print("No action specified. Use --bundle-id or --tip-accounts.")
print("Run with --help for usage information.")
except Exception as e:
print(f"Error: {e}")
print("Check your network connection and endpoint availability.")
sys.exit(1)
# ── Main ────────────────────────────────────────────────────────────
def parse_args() -> argparse.Namespace:
"""Parse command line arguments.
Returns:
Parsed arguments namespace.
"""
parser = argparse.ArgumentParser(
description="Check Jito bundle status and fetch tip accounts",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=(
"Examples:\n"
" python scripts/check_bundle_status.py --demo\n"
" python scripts/check_bundle_status.py --demo --bundle-id abc-123\n"
" python scripts/check_bundle_status.py --tip-accounts --endpoint mainnet\n"
" python scripts/check_bundle_status.py --bundle-id <uuid> --endpoint mainnet\n"
),
)
parser.add_argument(
"--demo",
action="store_true",
help="Run in demo mode with mock responses (no network calls)",
)
parser.add_argument(
"--bundle-id",
type=str,
default=None,
help="Bundle UUID to check status for",
)
parser.add_argument(
"--tip-accounts",
action="store_true",
help="Fetch and display current Jito tip accounts",
)
parser.add_argument(
"--endpoint",
type=str,
default=os.getenv("JITO_BLOCK_ENGINE", "mainnet"),
choices=list(BLOCK_ENGINE_URLS.keys()),
help="Block engine endpoint (default: mainnet or JITO_BLOCK_ENGINE env)",
)
return parser.parse_args()
if __name__ == "__main__":
args = parse_args()
if args.demo:
run_demo(
bundle_id=args.bundle_id,
show_tip_accounts=args.tip_accounts,
)
else:
run_live(
bundle_id=args.bundle_id,
show_tip_accounts=args.tip_accounts,
endpoint_name=args.endpoint,
)