
Mev Analysis
- 216 installs
- 257 repo stars
- Updated June 24, 2026
- agiprolabs/claude-trading-skills
mev-analysis is a Claude Code skill that assesses MEV exposure, detects sandwich attacks, and covers protection strategies for Solana DEX trading.
About
mev-analysis assesses MEV exposure, detects sandwich attacks, and covers protection strategies for Solana DEX trading. A developer uses it to estimate how much a swap is at risk of being sandwiched or arbitraged and how to defend against it. It explains the Solana MEV supply chain and how it differs from Ethereum.
- Assesses MEV exposure, detects sandwich attacks, and covers protection strategies for Solana DEX trading
- Ships mev_risk_estimator.py and sandwich_detector.py plus Solana MEV mechanics and protection references
- Contrasts Solana vs Ethereum MEV supply chains
Mev Analysis by the numbers
- 216 all-time installs (skills.sh)
- Ranked #85 of 479 Web3 & Blockchain skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
mev-analysis capabilities & compatibility
- Capabilities
- mev analysis · sandwich detection · risk management
- Use cases
- data analysis · research · security audit · trading
What mev-analysis says it does
Maximal Extractable Value (MEV) is the profit that validators and searchers can extract by reordering, inserting, or censoring transactions within a block.
On Solana DEXes, MEV primarily manifests as sandwich attacks against swaps, cross-DEX arbitrage, and liquidation extraction.
npx skills add https://github.com/agiprolabs/claude-trading-skills --skill mev-analysisAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 216 |
|---|---|
| repo stars | ★ 257 |
| Last updated | June 24, 2026 |
| Repository | agiprolabs/claude-trading-skills ↗ |
What it does
Assess MEV/sandwich risk on a Solana swap and choose protection strategies.
Who is it for?
Estimating sandwich/MEV exposure on a swap and picking protection strategies.
Skip if: Building or submitting the protective bundle itself (use jito-bundles).
When should I use this skill?
You need to gauge MEV/sandwich risk on a Solana swap or plan defenses.
By the numbers
- 5 MEV types enumerated
- Solana vs Ethereum comparison table
- liquidation bonus typically 5-10%
Files
MEV Analysis for Solana DEX Trading
Maximal Extractable Value (MEV) is the profit that validators and searchers can extract by reordering, inserting, or censoring transactions within a block. On Solana DEXes, MEV primarily manifests as sandwich attacks against swaps, cross-DEX arbitrage, and liquidation extraction. This skill covers detection, estimation, and protection strategies.
What Is MEV on Solana?
MEV occurs when someone with transaction ordering power profits at other traders' expense. On Solana, the MEV supply chain works as follows:
1. You submit a swap through an RPC endpoint 2. Searchers observe your transaction (via RPC forwarding, block engine access, or leader TPU sniffing) 3. Searcher constructs a profitable bundle (e.g., sandwich your swap) 4. Bundle submitted to Jito block engine with a tip to the validator 5. Validator includes the bundle in the block, earning the tip 6. You receive worse execution; the searcher profits the difference
How Solana MEV Differs from Ethereum
| Aspect | Ethereum | Solana |
|---|---|---|
| Block time | 12 seconds | ~400ms slots |
| Mempool | Public mempool | No mempool (but tx visible in transit) |
| Ordering | Proposer-builder separation (PBS) | Jito block engine (~85%+ validators) |
| Bundle system | Flashbots bundles | Jito bundles with tips |
| MEV cost | Gas priority fees | Jito tips (SOL) |
| Latency pressure | Moderate | Extreme (sub-100ms decisions) |
Key Solana-specific factors:
- No public mempool: Transactions flow RPC → TPU → Leader, but searchers tap into this flow via Jito's block engine and modified validators
- Known leader schedule: The leader (block producer) schedule is known ~2 epochs ahead, letting searchers target specific leaders
- Jito dominance: ~85%+ of validators run the Jito-modified client, making Jito bundles the primary MEV vector
- Speed: 400ms slots mean MEV bots must operate in microseconds, favoring co-located infrastructure
MEV Types on Solana
1. Sandwich Attacks
The most common MEV attack against retail traders.
Mechanics:
1. Attacker sees your pending swap: Buy 10 SOL worth of TOKEN_X
2. Front-run: Attacker buys TOKEN_X first → price rises
3. Your swap: You buy TOKEN_X at higher price → worse execution
4. Back-run: Attacker sells TOKEN_X → profits the differenceYour loss = price impact from front-run + attacker's profit margin Attacker profit = your_loss - jito_tip - transaction_fees
Risk factors:
- Trade size: Larger trades = more profitable to sandwich
- Token liquidity: Illiquid tokens = easier price manipulation
- Slippage setting: Wide slippage = more room for the attacker
- Pool type: CPMM pools more vulnerable than CLMM pools at concentrated ranges
2. Arbitrage (Cross-DEX)
Searchers capture price discrepancies between DEXes.
Pool A: TOKEN_X = 1.00 USDC
Pool B: TOKEN_X = 1.02 USDC
→ Buy on A, sell on B, profit 0.02 USDC per token (minus fees)This is generally beneficial to the market — it equalizes prices across venues. However, your trade may trigger the arbitrage opportunity that the searcher captures.
3. Liquidation Extraction
When DeFi positions (Solend, Marginfi, Kamino) become undercollateralized, searchers race to liquidate them and claim the liquidation bonus (typically 5-10%).
4. JIT (Just-In-Time) Liquidity
Searchers add concentrated liquidity to a CLMM pool just before a large swap and remove it immediately after, earning swap fees without sustained impermanent loss exposure. This is a sophisticated MEV form that can actually improve execution for the swapper.
5. Back-Running
Trading immediately after a large swap that moved the price, capturing the reversion. Less harmful than sandwiching because it does not worsen your execution — it profits from the market response to your trade.
Estimating MEV Exposure
Estimate your MEV risk before executing a trade:
import httpx
def estimate_mev_risk(
trade_size_sol: float,
pool_liquidity_usd: float,
slippage_bps: int,
token_daily_volume_usd: float,
) -> dict:
"""Estimate sandwich attack profitability for a given trade.
Returns risk assessment with estimated cost and recommendations.
"""
# Trade as percentage of pool liquidity
sol_price = 150.0 # approximate; fetch live price in production
trade_usd = trade_size_sol * sol_price
trade_pct_of_pool = (trade_usd / pool_liquidity_usd) * 100
# Estimated price impact from constant-product AMM
# price_impact ≈ trade_size / pool_liquidity (simplified)
price_impact_bps = int(trade_pct_of_pool * 100)
# Sandwich profitability: attacker captures portion of slippage headroom
# Rough model: sandwich_profit ≈ 0.5 * slippage_headroom * trade_size
slippage_headroom_bps = slippage_bps - price_impact_bps
if slippage_headroom_bps < 0:
slippage_headroom_bps = 0
sandwich_profit_usd = (slippage_headroom_bps / 10000) * trade_usd * 0.5
jito_tip_cost = 0.001 * sol_price # ~0.001 SOL typical tip
tx_fees = 0.000015 * sol_price * 2 # two transactions for sandwich
net_mev_profit = sandwich_profit_usd - jito_tip_cost - tx_fees
is_profitable_to_sandwich = net_mev_profit > 0.10 # $0.10 minimum
# Volume ratio indicates MEV bot attention level
volume_ratio = trade_usd / max(token_daily_volume_usd, 1)
risk_level = "LOW"
if is_profitable_to_sandwich and trade_pct_of_pool > 1.0:
risk_level = "HIGH"
elif is_profitable_to_sandwich or trade_pct_of_pool > 0.5:
risk_level = "MEDIUM"
return {
"risk_level": risk_level,
"trade_pct_of_pool": round(trade_pct_of_pool, 2),
"estimated_price_impact_bps": price_impact_bps,
"slippage_headroom_bps": slippage_headroom_bps,
"estimated_sandwich_cost_usd": round(max(net_mev_profit, 0), 2),
"is_profitable_to_sandwich": is_profitable_to_sandwich,
"recommendations": _get_recommendations(
risk_level, trade_size_sol, slippage_bps, trade_pct_of_pool
),
}
def _get_recommendations(
risk_level: str,
trade_size_sol: float,
slippage_bps: int,
trade_pct_of_pool: float,
) -> list[str]:
"""Generate protection recommendations based on risk assessment."""
recs = []
if risk_level == "HIGH":
recs.append("Use Jito bundle with 0.001-0.005 SOL tip")
recs.append("Use private/protected RPC endpoint")
if trade_pct_of_pool > 2.0:
n_splits = max(2, int(trade_pct_of_pool))
recs.append(f"Split into {n_splits} trades over 2-5 minutes")
if slippage_bps > 100:
recs.append(f"Reduce slippage from {slippage_bps}bps to 50-100bps")
if risk_level in ("MEDIUM", "HIGH"):
recs.append("Enable Jupiter dynamic slippage / MEV protection")
if not recs:
recs.append("Standard execution is likely safe for this trade size")
return recsMEV Protection Strategies
Strategy 1: Tight Slippage Settings
Set slippageBps as low as feasible. Sandwich profit is bounded by your slippage tolerance.
| Token Liquidity | Recommended Slippage |
|---|---|
| > $5M pool | 50 bps (0.5%) |
| $1M - $5M pool | 100 bps (1%) |
| $100K - $1M pool | 150-200 bps |
| < $100K pool | 200-500 bps (high risk) |
Trade-off: Too-tight slippage causes failed transactions, costing you fees with no execution.
Strategy 2: Jito Bundles
Submit your swap as a Jito bundle with a priority tip:
import httpx
JITO_BLOCK_ENGINE = "https://mainnet.block-engine.jito.wtf"
async def submit_jito_bundle(
signed_transactions: list[str],
tip_lamports: int = 1_000_000, # 0.001 SOL
) -> str:
"""Submit a transaction bundle to Jito block engine.
Args:
signed_transactions: Base64-encoded signed transactions.
tip_lamports: Tip amount in lamports (1 SOL = 1e9 lamports).
Returns:
Bundle ID for tracking.
"""
async with httpx.AsyncClient() as client:
resp = await client.post(
f"{JITO_BLOCK_ENGINE}/api/v1/bundles",
json={
"jsonrpc": "2.0",
"id": 1,
"method": "sendBundle",
"params": [signed_transactions],
},
timeout=10.0,
)
resp.raise_for_status()
result = resp.json()
return result.get("result", "")Tip guidelines:
- Normal priority: 0.0001 - 0.001 SOL
- High priority: 0.001 - 0.01 SOL
- Urgent (volatile market): 0.01 - 0.05 SOL
Strategy 3: Private/Protected RPCs
Send transactions through endpoints that do not expose them to searchers:
- Jito bundles (described above)
- Helius priority fee API with staked connections
- QuickNode private transaction submission
- Direct TPU forwarding (requires infrastructure)
Strategy 4: Trade Splitting
For large trades (> 1% of pool liquidity), split execution:
def compute_split_plan(
total_sol: float,
pool_liquidity_usd: float,
sol_price: float = 150.0,
max_pct_per_trade: float = 0.5,
) -> list[dict]:
"""Compute a trade splitting plan to minimize MEV exposure."""
total_usd = total_sol * sol_price
trade_pct = (total_usd / pool_liquidity_usd) * 100
if trade_pct <= max_pct_per_trade:
return [{"sol_amount": total_sol, "delay_seconds": 0}]
n_splits = max(2, int(trade_pct / max_pct_per_trade) + 1)
per_trade = total_sol / n_splits
delay = 30 # seconds between trades
return [
{"sol_amount": round(per_trade, 4), "delay_seconds": i * delay}
for i in range(n_splits)
]Strategy 5: Jupiter MEV Protection
Jupiter v6 includes built-in MEV protection features:
- Dynamic slippage: Automatically adjusts slippage to minimize sandwich window
- Priority fee estimation: Sets appropriate compute unit price
- Transaction landing optimization: Retry logic with increasing priority
Enable via Jupiter API:
params = {
"inputMint": "So11111111111111111111111111111111111111112",
"outputMint": token_mint,
"amount": str(amount_lamports),
"slippageBps": "50",
"dynamicSlippage": "true", # Auto-adjust slippage
"prioritizationFeeLamports": "auto", # Auto priority fee
}Detecting Sandwich Attacks
After a trade, check whether you were sandwiched:
1. Fetch your transaction and identify the slot 2. Fetch all transactions in that slot involving the same token 3. Look for the pattern:
- Transaction A: Buy TOKEN_X (before your tx in slot ordering)
- Your transaction: Buy TOKEN_X (worse price than expected)
- Transaction B: Sell TOKEN_X (after your tx, same signer as A)
4. Verify: Signer of A and B is the same wallet (the attacker) 5. Estimate cost: Difference between your expected and actual execution price
See scripts/sandwich_detector.py for a working implementation.
Known MEV indicators:
- Transaction signer has thousands of transactions per day
- Same-slot buy-then-sell of the same token around your swap
- Signer interacts with Jito tip program frequently
- Wallet has no token holdings (just in-and-out)
Integration with Other Skills
- `slippage-modeling`: Use slippage estimates to set protective limits
- `liquidity-analysis`: Pool liquidity determines MEV vulnerability
- `jupiter-api`: Jupiter's MEV protection features and swap execution
- `solana-onchain`: On-chain transaction analysis for sandwich detection
- `helius-api`: Transaction parsing and historical analysis
Files
References
references/solana_mev_mechanics.md— Solana block production, Jito block engine, MEV supply chain, and transaction flow pathsreferences/protection_strategies.md— Detailed protection strategies with implementation guidance, cost-benefit analysis, and decision matrix
Scripts
scripts/sandwich_detector.py— Detects sandwich attacks around a given transaction signature using on-chain datascripts/mev_risk_estimator.py— Estimates MEV exposure for a planned trade based on token liquidity, trade size, and slippage settings
MEV Protection Strategies
Strategy 1: Tight Slippage Settings
Rationale
Sandwich attack profit is bounded by your slippage tolerance. If you set slippage to 50bps, the attacker can extract at most ~50bps of your trade value (minus their costs). Tighter slippage makes sandwiching less profitable or unprofitable.
Implementation
# Jupiter API quote with tight slippage
params = {
"inputMint": "So11111111111111111111111111111111111111112",
"outputMint": token_mint,
"amount": str(amount_lamports),
"slippageBps": "50", # 0.5% — tight for liquid tokens
}Recommended Settings by Pool Liquidity
| Pool Liquidity (USD) | Slippage (bps) | Rationale |
|---|---|---|
| > $10M | 30-50 | Deep liquidity, minimal natural slippage |
| $1M - $10M | 50-100 | Moderate liquidity, need some buffer |
| $100K - $1M | 100-200 | Thinner books, more natural price impact |
| < $100K | 200-500 | Very thin, high natural slippage, high MEV risk |
Trade-offs
- Too tight: Transaction fails (you still pay base fee, get no execution)
- Too loose: Sandwich bots extract maximum value
- Sweet spot: Set slippage to ~1.5x your expected price impact
Dynamic Approach
Monitor recent price volatility and adjust:
def compute_safe_slippage(
expected_impact_bps: float,
recent_volatility_bps: float,
safety_margin: float = 1.5,
) -> int:
"""Compute slippage that covers natural movement but limits MEV."""
base = expected_impact_bps + recent_volatility_bps
return max(30, int(base * safety_margin))Strategy 2: Jito Bundles
Rationale
By submitting your transaction as a Jito bundle, you control the execution context. No searcher can insert transactions before or after yours within your bundle. You pay a tip for priority inclusion.
When to Use
- Trade size > 5 SOL
- Trading illiquid tokens (< $1M pool)
- During high-MEV periods (meme coin launches, volatile markets)
- When you need guaranteed execution ordering
Implementation
import httpx
import base64
JITO_BLOCK_ENGINE = "https://mainnet.block-engine.jito.wtf"
async def get_tip_accounts() -> list[str]:
"""Fetch current Jito tip accounts."""
async with httpx.AsyncClient() as client:
resp = await client.post(
f"{JITO_BLOCK_ENGINE}/api/v1/bundles",
json={
"jsonrpc": "2.0",
"id": 1,
"method": "getTipAccounts",
"params": [],
},
timeout=10.0,
)
resp.raise_for_status()
return resp.json().get("result", [])
async def send_bundle(
serialized_txs: list[bytes],
tip_lamports: int = 100_000,
) -> str:
"""Submit a bundle to Jito block engine.
Args:
serialized_txs: List of serialized, signed transactions.
tip_lamports: Tip amount (1 SOL = 1_000_000_000 lamports).
Returns:
Bundle ID string.
"""
encoded = [base64.b64encode(tx).decode() for tx in serialized_txs]
async with httpx.AsyncClient() as client:
resp = await client.post(
f"{JITO_BLOCK_ENGINE}/api/v1/bundles",
json={
"jsonrpc": "2.0",
"id": 1,
"method": "sendBundle",
"params": [encoded],
},
timeout=10.0,
)
resp.raise_for_status()
return resp.json().get("result", "")Tip Sizing Guide
| Scenario | Tip (SOL) | Tip (Lamports) |
|---|---|---|
| Low priority, small trade | 0.0001 | 100,000 |
| Normal priority | 0.001 | 1,000,000 |
| High priority, large trade | 0.005 | 5,000,000 |
| Urgent (volatile market) | 0.01-0.05 | 10,000,000-50,000,000 |
Rule of thumb: Tip should be < 0.1% of trade value. If tip exceeds MEV risk, skip the bundle.
Bundle Status Checking
async def check_bundle_status(bundle_id: str) -> dict:
"""Check if a bundle was included."""
async with httpx.AsyncClient() as client:
resp = await client.post(
f"{JITO_BLOCK_ENGINE}/api/v1/bundles",
json={
"jsonrpc": "2.0",
"id": 1,
"method": "getBundleStatuses",
"params": [[bundle_id]],
},
timeout=10.0,
)
resp.raise_for_status()
return resp.json().get("result", {})Strategy 3: Private / Protected RPCs
Rationale
Standard public RPCs forward your transaction to multiple validators, increasing the number of parties that can observe it. Private RPCs minimize this exposure.
Providers
| Provider | Feature | Endpoint |
|---|---|---|
| Jito | Bundle submission | mainnet.block-engine.jito.wtf |
| Helius | Staked connections, priority fees | mainnet.helius-rpc.com/?api-key=KEY |
| QuickNode | Private transaction submission | Via QuickNode endpoint |
| Triton | Dedicated RPC with staked connections | Custom endpoint |
How Staked Connections Help
Validators prioritize transactions from staked connections. With a staked RPC:
- Your transaction goes directly to the leader via a trusted channel
- Less forwarding through intermediary nodes
- Faster inclusion (competitive advantage over public RPC users)
Implementation
# Use Helius with staked connection for reduced MEV exposure
HELIUS_RPC = f"https://mainnet.helius-rpc.com/?api-key={os.getenv('HELIUS_API_KEY')}"
async def send_protected_transaction(signed_tx_base64: str) -> str:
"""Send transaction via Helius staked connection."""
async with httpx.AsyncClient() as client:
resp = await client.post(
HELIUS_RPC,
json={
"jsonrpc": "2.0",
"id": 1,
"method": "sendTransaction",
"params": [
signed_tx_base64,
{"skipPreflight": False, "maxRetries": 3},
],
},
timeout=30.0,
)
resp.raise_for_status()
return resp.json().get("result", "")Strategy 4: Trade Splitting
Rationale
MEV profitability scales with trade size. Splitting a large trade into smaller pieces makes each individual piece less attractive to sandwich.
When to Split
- Trade is > 1% of pool liquidity
- Estimated sandwich cost > $5
- Token is actively targeted by MEV bots (high volume micro-cap)
Implementation
import asyncio
async def execute_split_trades(
total_sol: float,
pool_liquidity_usd: float,
max_pct_per_trade: float = 0.5,
delay_seconds: int = 30,
sol_price: float = 150.0,
) -> list[dict]:
"""Execute a trade in smaller pieces to reduce MEV exposure.
Note: This is a framework. In production, each piece requires
building, signing, and submitting a real transaction.
"""
total_usd = total_sol * sol_price
trade_pct = (total_usd / pool_liquidity_usd) * 100
if trade_pct <= max_pct_per_trade:
return [{"piece": 1, "sol": total_sol, "status": "single_trade"}]
n = max(2, int(trade_pct / max_pct_per_trade) + 1)
per_trade = total_sol / n
results = []
for i in range(n):
if i > 0:
await asyncio.sleep(delay_seconds)
results.append({
"piece": i + 1,
"sol": round(per_trade, 4),
"status": "submitted",
})
return resultsTrade-offs
- Pro: Each piece has lower MEV risk
- Con: Total execution takes longer; price may move against you
- Con: More transaction fees (base fee per trade)
- Con: Potential information leakage if pattern is detected
Strategy 5: Jupiter MEV Protection
Dynamic Slippage
Jupiter's dynamic slippage mode adjusts the slippage parameter based on:
- Current market volatility
- Historical fill rates at different slippage levels
- Pool depth and expected price impact
params = {
"inputMint": input_mint,
"outputMint": output_mint,
"amount": str(amount_lamports),
"dynamicSlippage": "true",
"prioritizationFeeLamports": "auto",
}Transaction Landing Improvements
Jupiter v6 includes retry logic and priority fee optimization that reduces the chance of failed transactions when using tight slippage.
Decision Matrix
Use this matrix to select protection level:
| Trade Size | Liquid Token (>$5M pool) | Mid Liquidity ($500K-$5M) | Low Liquidity (<$500K) |
|---|---|---|---|
| < 1 SOL | No protection needed | Tight slippage (50-100bps) | Tight slippage (100-200bps) |
| 1-10 SOL | Tight slippage (50bps) | Jito bundle + tight slippage | Jito bundle + split |
| 10-50 SOL | Tight slippage + private RPC | Jito bundle + split (2-3x) | Split (5x+) + Jito bundles |
| > 50 SOL | Split (2-3x) + Jito | Split (5x+) + Jito + private RPC | Avoid or OTC |
Cost-Benefit Analysis
Only protect when MEV risk exceeds protection cost (tip + extra fees + delay).
Rule of thumb: Under 0.5 SOL in liquid pools: skip protection. 0.5-5 SOL: tight slippage. 5-50 SOL: Jito bundle + tight slippage. Over 50 SOL: full stack (split + Jito + private RPC).
Solana MEV Mechanics
Solana Block Production
Leader Schedule
Solana uses a rotating leader schedule. One validator is the "leader" for each slot (~400ms). The leader:
- Receives transactions from the network
- Orders them into a block
- Streams the block to the cluster via Turbine
The leader schedule is deterministic and known 2 epochs ahead (~4 days). This means searchers know exactly which validator will produce each block, enabling targeted strategies.
Slot Timing
- Slot duration: ~400ms (target)
- Actual slot time: 400-600ms depending on network load
- Transactions per slot: ~2,000-4,000 (theoretical max much higher)
- Finality: ~6.4 seconds (2/3 stake confirmation), but for MEV purposes, slot inclusion is what matters
Transaction Flow (Standard)
User Wallet → RPC Node → TPU (Transaction Processing Unit) → Leader Validator → Block
↓
(Forwarded to other validators for redundancy)The TPU accepts transactions via UDP (QUIC since v1.15). There is no public mempool — transactions are forwarded directly to the current and upcoming leaders.
However, transactions are observable:
- RPC nodes forward to multiple leaders for reliability
- Jito block engine receives transactions from RPC infrastructure
- Validators can inspect incoming transactions before ordering
Jito Block Engine
Overview
Jito Labs operates a modified Solana validator client that adds an auction mechanism for transaction ordering. As of early 2025, approximately 85-90% of Solana validators run the Jito-modified client.
How It Works
Searcher → Jito Block Engine → Jito-Modified Validator → Block
1. Searcher identifies MEV opportunity
2. Searcher constructs a "bundle" (ordered list of transactions)
3. Searcher submits bundle to Jito block engine with a SOL tip
4. Block engine validates the bundle (simulates execution)
5. Block engine forwards bundle to the current leader (if running Jito client)
6. Leader includes the bundle atomically in the block
7. Validator receives the tipBundle Properties
- Atomic execution: All transactions in a bundle execute or none do
- Ordered: Transactions execute in the specified order within the bundle
- Priority: Higher tips get priority placement in the block
- Size limit: Up to 5 transactions per bundle
- Simulation: Block engine simulates before forwarding; reverted bundles are discarded
Tip Mechanics
Tips are paid via a special Jito tip program. The tip transaction is included in the bundle:
Bundle = [
tx_1 (front-run),
tx_2 (victim's transaction), # or reference to it
tx_3 (back-run),
tx_4 (tip to Jito: sendSOL to tip account)
]Tip distribution:
- 100% of the tip goes to the validator producing the block
- Jito Labs charges no fee on tips (revenue from other services)
Typical tip amounts (as of 2025):
- Low priority: 1,000 - 10,000 lamports (0.000001 - 0.00001 SOL)
- Normal: 100,000 - 1,000,000 lamports (0.0001 - 0.001 SOL)
- High priority: 1,000,000 - 10,000,000 lamports (0.001 - 0.01 SOL)
- Competitive MEV: 10,000,000+ lamports (0.01+ SOL)
Jito Endpoints
- Mainnet block engine:
https://mainnet.block-engine.jito.wtf - Bundle submission:
POST /api/v1/bundleswithsendBundleJSON-RPC method - Bundle status:
POST /api/v1/bundleswithgetBundleStatusesmethod - Tip accounts: Rotate periodically; fetch via
getTipAccountsmethod
MEV Supply Chain on Solana
Participants
1. Users: Submit swaps, provide liquidity, borrow/lend 2. Searchers: Identify and capture MEV opportunities. Run co-located infrastructure with sub-millisecond latency 3. Block Engine (Jito): Auction platform connecting searchers to validators 4. Validators: Produce blocks, earn tips from MEV bundles 5. Protocols: DEXes, lending platforms — where MEV opportunities originate
Value Flow
MEV Opportunity (user's swap creates arbitrage or sandwich opportunity)
↓
Searcher extracts value:
gross_profit = extracted_value
net_profit = extracted_value - jito_tip - tx_fees - infrastructure_cost
↓
Jito tip to validator:
validator_revenue = jito_tip
↓
Cost to user:
user_cost = worse execution price (implicit cost, not a visible fee)MEV Extraction Economics
For a sandwich attack to be profitable:
sandwich_profit = victim_slippage_captured - (2 * tx_fee) - jito_tip - capital_cost
Where:
victim_slippage_captured ≈ victim_trade_size * (slippage_bps / 10000) * capture_rate
capture_rate ≈ 0.3 to 0.7 (depends on pool depth and attacker's capital)
tx_fee ≈ 0.000005 SOL per transaction (5000 lamports base fee)
jito_tip ≈ 0.0001 to 0.01 SOL (depends on competition)
capital_cost ≈ minimal for atomic bundles (no holding risk)Minimum viable sandwich (rough estimate):
- Trade size: > 0.5 SOL with > 100bps slippage in an illiquid pool
- Or: > 5 SOL with > 50bps slippage in a moderately liquid pool
- Below these thresholds, gas + tips exceed potential profit
Transaction Flow Paths and MEV Visibility
Path 1: Public RPC (Most Vulnerable)
User → Public RPC → Multiple Leaders (via QUIC)
→ Jito Block Engine (intercepted)Transactions are visible to the Jito block engine and any searcher connected to it. This is the most MEV-exposed path.
Path 2: Private/Staked RPC (Less Visible)
User → Private RPC (Helius/QuickNode) → Current Leader OnlyStaked connections can send directly to the leader. Fewer intermediaries see the transaction. Not immune to MEV but reduces the observation window.
Path 3: Jito Bundle (User-Controlled Ordering)
User → Jito Block Engine → Leader
(user IS the searcher, controlling tx order)By submitting your own bundle, you control the ordering. A searcher cannot insert transactions into your bundle. This is the strongest MEV protection available.
Path 4: Direct TPU (Requires Infrastructure)
User → Leader's TPU directly (UDP/QUIC)Requires knowing the leader's TPU address and having network proximity. Used by professional trading firms. Minimizes intermediary visibility.
Historical MEV on Solana
MEV on Solana has grown significantly:
- 2022-2023: Early MEV, mostly arbitrage. Limited sandwich attacks due to lower DEX volume
- 2024: Explosive growth with meme coin trading surge. Sandwich attacks became routine on Jupiter and Raydium swaps. Jito tips exceeded $1M/day during peak periods
- 2025: MEV infrastructure matured. Protection mechanisms improved (Jupiter dynamic slippage, private RPCs). MEV remains significant but users have more tools to mitigate it
Key metrics to track:
- Jito bundle volume (indicates MEV activity level)
- Average Jito tip (indicates MEV competition/profitability)
- Sandwich attack frequency (via on-chain analysis)
- Protected vs unprotected transaction ratio
#!/usr/bin/env python3
"""Estimate MEV risk for a planned Solana DEX trade.
Analyzes token liquidity, trade size, and slippage settings to estimate
the probability and cost of sandwich attacks. Provides actionable
protection recommendations.
Usage:
python scripts/mev_risk_estimator.py # demo mode
python scripts/mev_risk_estimator.py --demo # explicit demo
python scripts/mev_risk_estimator.py --mint <TOKEN_MINT> --size 10 --slippage 100
Dependencies:
uv pip install httpx
Environment Variables:
TOKEN_MINT: Token mint address (alternative to --mint flag)
TRADE_SIZE_SOL: Trade size in SOL (default: 1.0)
SLIPPAGE_BPS: Slippage tolerance in basis points (default: 100)
"""
import argparse
import json
import os
import sys
from dataclasses import dataclass, field
from typing import Optional
import httpx
# ── Configuration ───────────────────────────────────────────────────
TOKEN_MINT = os.getenv("TOKEN_MINT", "")
TRADE_SIZE_SOL = float(os.getenv("TRADE_SIZE_SOL", "1.0"))
SLIPPAGE_BPS = int(os.getenv("SLIPPAGE_BPS", "100"))
DEXSCREENER_BASE = "https://api.dexscreener.com/latest/dex"
COINGECKO_BASE = "https://api.coingecko.com/api/v3"
REQUEST_TIMEOUT = 15.0
# SOL mint for reference
SOL_MINT = "So11111111111111111111111111111111111111112"
# ── Data Classes ────────────────────────────────────────────────────
@dataclass
class TokenPoolData:
"""Liquidity and volume data for a token's best pool."""
pair_address: str
dex_name: str
base_token_symbol: str
quote_token_symbol: str
liquidity_usd: float
volume_24h_usd: float
price_usd: float
price_change_24h_pct: float
fdv: float
pool_created_at: Optional[str]
@dataclass
class MevRiskAssessment:
"""Complete MEV risk assessment for a planned trade."""
risk_level: str # "LOW", "MEDIUM", "HIGH", "CRITICAL"
trade_size_sol: float
trade_size_usd: float
slippage_bps: int
pool_liquidity_usd: float
volume_24h_usd: float
trade_pct_of_pool: float
estimated_price_impact_bps: float
slippage_headroom_bps: float
estimated_sandwich_cost_usd: float
is_profitable_to_sandwich: bool
recommended_slippage_bps: int
recommendations: list[str] = field(default_factory=list)
protection_plan: list[str] = field(default_factory=list)
# ── Data Fetching ───────────────────────────────────────────────────
def fetch_sol_price(client: httpx.Client) -> float:
"""Fetch current SOL price in USD from CoinGecko.
Args:
client: httpx Client instance.
Returns:
SOL price in USD. Falls back to 150.0 on error.
"""
try:
resp = client.get(
f"{COINGECKO_BASE}/simple/price",
params={"ids": "solana", "vs_currencies": "usd"},
timeout=REQUEST_TIMEOUT,
)
resp.raise_for_status()
data = resp.json()
return float(data.get("solana", {}).get("usd", 150.0))
except (httpx.HTTPError, KeyError, ValueError):
return 150.0
def fetch_token_pool_data(
mint: str, client: httpx.Client
) -> Optional[TokenPoolData]:
"""Fetch pool data for a token from DexScreener.
Finds the most liquid Solana pool for the given token mint.
Args:
mint: Token mint address.
client: httpx Client instance.
Returns:
TokenPoolData for the most liquid pool, or None.
"""
try:
resp = client.get(
f"{DEXSCREENER_BASE}/tokens/{mint}",
timeout=REQUEST_TIMEOUT,
)
resp.raise_for_status()
data = resp.json()
except httpx.HTTPError as e:
print(f" Warning: DexScreener API error: {e}")
return None
pairs = data.get("pairs", [])
if not pairs:
return None
# Filter to Solana pairs and sort by liquidity
solana_pairs = [p for p in pairs if p.get("chainId") == "solana"]
if not solana_pairs:
solana_pairs = pairs # Fall back to all chains
solana_pairs.sort(
key=lambda p: float(p.get("liquidity", {}).get("usd", 0) or 0),
reverse=True,
)
best = solana_pairs[0]
liquidity = best.get("liquidity", {})
volume = best.get("volume", {})
price_change = best.get("priceChange", {})
return TokenPoolData(
pair_address=best.get("pairAddress", ""),
dex_name=best.get("dexId", "unknown"),
base_token_symbol=best.get("baseToken", {}).get("symbol", "???"),
quote_token_symbol=best.get("quoteToken", {}).get("symbol", "???"),
liquidity_usd=float(liquidity.get("usd", 0) or 0),
volume_24h_usd=float(volume.get("h24", 0) or 0),
price_usd=float(best.get("priceUsd", 0) or 0),
price_change_24h_pct=float(price_change.get("h24", 0) or 0),
fdv=float(best.get("fdv", 0) or 0),
pool_created_at=best.get("pairCreatedAt"),
)
# ── Risk Estimation ────────────────────────────────────────────────
def estimate_mev_risk(
trade_size_sol: float,
slippage_bps: int,
pool: TokenPoolData,
sol_price: float,
) -> MevRiskAssessment:
"""Estimate MEV risk for a planned trade.
Uses a simplified constant-product AMM model to estimate price impact
and sandwich attack profitability.
Args:
trade_size_sol: Trade size in SOL.
slippage_bps: Slippage tolerance in basis points.
pool: Pool liquidity and volume data.
sol_price: Current SOL price in USD.
Returns:
Complete MevRiskAssessment.
"""
trade_usd = trade_size_sol * sol_price
pool_liq = max(pool.liquidity_usd, 1.0) # Avoid division by zero
# Trade as percentage of pool
trade_pct = (trade_usd / pool_liq) * 100
# Estimated price impact (constant-product model: impact ~ trade/liquidity)
# For a CPMM pool, price impact ≈ trade_size / (pool_size / 2)
# Since liquidity_usd represents total pool, one side is ~half
price_impact_bps = (trade_usd / (pool_liq / 2)) * 10000
# Slippage headroom: how much room the attacker has to work with
slippage_headroom = max(0, slippage_bps - price_impact_bps)
# Sandwich profitability model:
# The attacker front-runs, pushing price up, then your swap executes
# at the worse price. Attacker profit ≈ capture_rate * headroom * trade_size.
# capture_rate depends on pool depth and attacker capital (typically 0.3-0.6)
capture_rate = 0.4
sandwich_gross_usd = (slippage_headroom / 10000) * trade_usd * capture_rate
# Attacker costs
jito_tip_sol = _estimate_jito_tip(sandwich_gross_usd, sol_price)
jito_tip_usd = jito_tip_sol * sol_price
tx_fees_usd = 0.000015 * sol_price * 2 # Two transactions
attacker_cost = jito_tip_usd + tx_fees_usd
net_sandwich_profit = sandwich_gross_usd - attacker_cost
is_profitable = net_sandwich_profit > 0.05 # $0.05 minimum threshold
# Determine risk level
risk_level = _classify_risk(
is_profitable, trade_pct, slippage_headroom, pool.volume_24h_usd
)
# Recommended slippage
recommended_slippage = _recommend_slippage(
price_impact_bps, pool_liq, pool.volume_24h_usd
)
# Build recommendations
recommendations = _build_recommendations(
risk_level,
trade_size_sol,
slippage_bps,
recommended_slippage,
trade_pct,
is_profitable,
net_sandwich_profit,
)
# Build protection plan
protection_plan = _build_protection_plan(
risk_level, trade_size_sol, trade_pct, pool_liq
)
return MevRiskAssessment(
risk_level=risk_level,
trade_size_sol=trade_size_sol,
trade_size_usd=round(trade_usd, 2),
slippage_bps=slippage_bps,
pool_liquidity_usd=round(pool_liq, 2),
volume_24h_usd=round(pool.volume_24h_usd, 2),
trade_pct_of_pool=round(trade_pct, 4),
estimated_price_impact_bps=round(price_impact_bps, 1),
slippage_headroom_bps=round(slippage_headroom, 1),
estimated_sandwich_cost_usd=round(max(net_sandwich_profit, 0), 2),
is_profitable_to_sandwich=is_profitable,
recommended_slippage_bps=recommended_slippage,
recommendations=recommendations,
protection_plan=protection_plan,
)
def _estimate_jito_tip(
sandwich_profit_usd: float, sol_price: float
) -> float:
"""Estimate the Jito tip an attacker would pay.
Searchers typically tip 30-60% of expected profit. Minimum viable
tip is ~0.0001 SOL.
Args:
sandwich_profit_usd: Gross sandwich profit in USD.
sol_price: Current SOL price.
Returns:
Estimated tip in SOL.
"""
if sandwich_profit_usd <= 0:
return 0.0001
tip_usd = sandwich_profit_usd * 0.5 # 50% of profit to tip
tip_sol = tip_usd / sol_price
return max(0.0001, tip_sol)
def _classify_risk(
is_profitable: bool,
trade_pct: float,
headroom_bps: float,
volume_24h: float,
) -> str:
"""Classify overall MEV risk level.
Args:
is_profitable: Whether sandwiching is estimated profitable.
trade_pct: Trade as percentage of pool liquidity.
headroom_bps: Slippage headroom in basis points.
volume_24h: 24h trading volume in USD.
Returns:
Risk level string.
"""
score = 0
if is_profitable:
score += 2
if trade_pct > 5.0:
score += 3
elif trade_pct > 1.0:
score += 2
elif trade_pct > 0.5:
score += 1
if headroom_bps > 200:
score += 2
elif headroom_bps > 100:
score += 1
# High-volume tokens attract more MEV bots
if volume_24h > 1_000_000:
score += 1
elif volume_24h > 100_000:
score += 0 # Moderate volume, moderate attention
if score >= 6:
return "CRITICAL"
elif score >= 4:
return "HIGH"
elif score >= 2:
return "MEDIUM"
return "LOW"
def _recommend_slippage(
price_impact_bps: float,
pool_liquidity: float,
volume_24h: float,
) -> int:
"""Recommend slippage setting based on pool characteristics.
Args:
price_impact_bps: Estimated price impact.
pool_liquidity: Pool liquidity in USD.
volume_24h: 24h volume in USD.
Returns:
Recommended slippage in basis points.
"""
# Base: 1.5x expected price impact, minimum 30bps
base = max(30, int(price_impact_bps * 1.5))
# Adjust for pool characteristics
if pool_liquidity < 100_000:
base = max(base, 200) # Very thin pool needs buffer
elif pool_liquidity < 500_000:
base = max(base, 100)
# Cap at reasonable maximum
return min(base, 500)
def _build_recommendations(
risk_level: str,
trade_size_sol: float,
current_slippage: int,
recommended_slippage: int,
trade_pct: float,
is_profitable: bool,
estimated_cost: float,
) -> list[str]:
"""Build list of actionable recommendations.
Args:
risk_level: Assessed risk level.
trade_size_sol: Trade size in SOL.
current_slippage: Current slippage setting in bps.
recommended_slippage: Recommended slippage in bps.
trade_pct: Trade as percentage of pool.
is_profitable: Whether sandwich is profitable.
estimated_cost: Estimated sandwich cost in USD.
Returns:
List of recommendation strings.
"""
recs = []
if current_slippage > recommended_slippage:
recs.append(
f"Reduce slippage from {current_slippage}bps to "
f"{recommended_slippage}bps"
)
if risk_level in ("HIGH", "CRITICAL"):
recs.append("Use Jito bundle submission for MEV protection")
recs.append("Use a private/staked RPC endpoint (Helius, Triton)")
if trade_pct > 2.0:
n_splits = max(2, int(trade_pct / 0.5))
recs.append(
f"Split trade into {n_splits} pieces "
f"({trade_size_sol / n_splits:.2f} SOL each)"
)
if is_profitable and estimated_cost > 1.0:
recs.append(
f"Estimated sandwich cost: ${estimated_cost:.2f} — "
f"protection is strongly recommended"
)
if risk_level == "CRITICAL":
recs.append(
"Consider whether this trade size is appropriate "
"for the available liquidity"
)
if risk_level == "LOW" and not is_profitable:
recs.append(
"Standard execution is likely safe. "
"MEV risk is minimal for this trade."
)
recs.append("Enable Jupiter dynamic slippage for auto-adjustment")
return recs
def _build_protection_plan(
risk_level: str,
trade_size_sol: float,
trade_pct: float,
pool_liquidity: float,
) -> list[str]:
"""Build step-by-step protection plan.
Args:
risk_level: Assessed risk level.
trade_size_sol: Trade size in SOL.
trade_pct: Trade as percentage of pool.
pool_liquidity: Pool liquidity in USD.
Returns:
Ordered list of protection steps.
"""
if risk_level == "LOW":
return [
"1. Execute normally with tight slippage",
"2. Verify execution price matches quote",
]
if risk_level == "MEDIUM":
return [
"1. Set slippage to 50-100bps",
"2. Enable Jupiter dynamic slippage",
"3. Submit via private RPC if available",
"4. Verify execution price post-trade",
]
if risk_level == "HIGH":
steps = [
"1. Set slippage to minimum viable (30-50bps for liquid tokens)",
"2. Submit transaction as Jito bundle with 0.001 SOL tip",
"3. Use private/staked RPC endpoint",
]
if trade_pct > 2.0:
n = max(2, int(trade_pct))
steps.append(
f"4. Split into {n} trades, 30s apart"
)
steps.append("5. Monitor each execution for sandwich indicators")
else:
steps.append("4. Monitor execution for sandwich indicators")
return steps
# CRITICAL
n = max(3, int(trade_pct))
return [
f"1. Split trade into {n}+ pieces",
"2. Use Jito bundle for each piece (0.001-0.005 SOL tip)",
"3. Use private/staked RPC endpoint",
"4. Set 30-60 second delay between trades",
"5. Monitor pool liquidity between trades for manipulation",
"6. Consider OTC or limit order alternatives",
"7. Verify each execution against expected price",
]
# ── Output ──────────────────────────────────────────────────────────
def print_assessment(
assessment: MevRiskAssessment,
pool: Optional[TokenPoolData] = None,
) -> None:
"""Print formatted MEV risk assessment.
Args:
assessment: The risk assessment to display.
pool: Optional pool data for additional context.
"""
risk_markers = {
"LOW": "[OK]",
"MEDIUM": "[!!]",
"HIGH": "[!!!]",
"CRITICAL": "[XXXX]",
}
marker = risk_markers.get(assessment.risk_level, "[??]")
print("=" * 70)
print(f"MEV RISK ASSESSMENT {marker} {assessment.risk_level}")
print("=" * 70)
print()
# Trade details
print("TRADE DETAILS")
print(f" Size: {assessment.trade_size_sol} SOL "
f"(${assessment.trade_size_usd:.2f})")
print(f" Slippage setting: {assessment.slippage_bps} bps "
f"({assessment.slippage_bps / 100:.1f}%)")
print()
# Pool details
if pool:
print("POOL DATA")
print(f" DEX: {pool.dex_name}")
print(f" Pair: {pool.base_token_symbol}/"
f"{pool.quote_token_symbol}")
print(f" Liquidity: ${pool.liquidity_usd:,.0f}")
print(f" 24h Volume: ${pool.volume_24h_usd:,.0f}")
print(f" Token Price: ${pool.price_usd:.6f}")
print(f" 24h Change: {pool.price_change_24h_pct:+.1f}%")
print()
# Risk metrics
print("RISK METRICS")
print(f" Trade % of pool: {assessment.trade_pct_of_pool:.4f}%")
print(f" Est. price impact: {assessment.estimated_price_impact_bps:.1f} bps")
print(f" Slippage headroom: {assessment.slippage_headroom_bps:.1f} bps")
print(f" Sandwich viable: "
f"{'Yes' if assessment.is_profitable_to_sandwich else 'No'}")
if assessment.is_profitable_to_sandwich:
print(f" Est. sandwich cost: ${assessment.estimated_sandwich_cost_usd:.2f}")
print(f" Recommended slip: {assessment.recommended_slippage_bps} bps")
print()
# Recommendations
print("RECOMMENDATIONS")
for rec in assessment.recommendations:
print(f" - {rec}")
print()
# Protection plan
print("PROTECTION PLAN")
for step in assessment.protection_plan:
print(f" {step}")
print()
print("=" * 70)
print("NOTE: Estimates use simplified AMM models. Actual MEV risk depends")
print("on real-time searcher activity, pool type (CPMM vs CLMM), and")
print("current network conditions. This is informational analysis only.")
print("=" * 70)
# ── Demo Mode ───────────────────────────────────────────────────────
def run_demo() -> None:
"""Run demonstration with synthetic data showing different risk levels."""
print("=" * 70)
print("MEV RISK ESTIMATOR — DEMO MODE")
print("=" * 70)
print()
print("Demonstrating risk estimation across different scenarios...")
print()
sol_price = 150.0
scenarios = [
{
"name": "Small trade, liquid token",
"trade_sol": 1.0,
"slippage": 100,
"pool": TokenPoolData(
pair_address="DemoPool1",
dex_name="raydium",
base_token_symbol="BONK",
quote_token_symbol="SOL",
liquidity_usd=5_000_000,
volume_24h_usd=10_000_000,
price_usd=0.00002,
price_change_24h_pct=5.2,
fdv=1_200_000_000,
pool_created_at=None,
),
},
{
"name": "Medium trade, moderate liquidity",
"trade_sol": 10.0,
"slippage": 200,
"pool": TokenPoolData(
pair_address="DemoPool2",
dex_name="orca",
base_token_symbol="MEME",
quote_token_symbol="SOL",
liquidity_usd=500_000,
volume_24h_usd=1_000_000,
price_usd=0.005,
price_change_24h_pct=-12.3,
fdv=50_000_000,
pool_created_at=None,
),
},
{
"name": "Large trade, low liquidity (HIGH RISK)",
"trade_sol": 50.0,
"slippage": 300,
"pool": TokenPoolData(
pair_address="DemoPool3",
dex_name="raydium",
base_token_symbol="NEWCOIN",
quote_token_symbol="SOL",
liquidity_usd=80_000,
volume_24h_usd=200_000,
price_usd=0.0001,
price_change_24h_pct=45.0,
fdv=5_000_000,
pool_created_at=None,
),
},
]
for scenario in scenarios:
print(f"\n{'─' * 70}")
print(f"SCENARIO: {scenario['name']}")
print(f"{'─' * 70}")
assessment = estimate_mev_risk(
trade_size_sol=scenario["trade_sol"],
slippage_bps=scenario["slippage"],
pool=scenario["pool"],
sol_price=sol_price,
)
print_assessment(assessment, scenario["pool"])
print()
print()
print("Use --mint <TOKEN_MINT> --size <SOL> --slippage <BPS>")
print("to analyze a real token with live market data.")
# ── Live Analysis ───────────────────────────────────────────────────
def analyze_token(
mint: str, trade_size_sol: float, slippage_bps: int
) -> None:
"""Analyze MEV risk for a real token using live market data.
Args:
mint: Token mint address.
trade_size_sol: Planned trade size in SOL.
slippage_bps: Planned slippage setting in basis points.
"""
print(f"Analyzing MEV risk for token: {mint[:20]}...")
print(f"Trade size: {trade_size_sol} SOL | Slippage: {slippage_bps} bps")
print()
with httpx.Client() as client:
# Fetch SOL price
print("Fetching SOL price...")
sol_price = fetch_sol_price(client)
print(f"SOL price: ${sol_price:.2f}")
# Fetch pool data
print("Fetching pool data from DexScreener...")
pool = fetch_token_pool_data(mint, client)
if not pool:
print()
print("ERROR: No pool data found for this token.")
print("The token may not be listed on any Solana DEX,")
print("or the mint address may be incorrect.")
sys.exit(1)
if pool.liquidity_usd <= 0:
print()
print("WARNING: Pool has zero reported liquidity.")
print("MEV risk cannot be accurately assessed.")
print("Exercise extreme caution with this token.")
return
print(f"Found pool: {pool.base_token_symbol}/{pool.quote_token_symbol} "
f"on {pool.dex_name}")
print(f"Liquidity: ${pool.liquidity_usd:,.0f}")
print()
# Run assessment
assessment = estimate_mev_risk(
trade_size_sol=trade_size_sol,
slippage_bps=slippage_bps,
pool=pool,
sol_price=sol_price,
)
print_assessment(assessment, pool)
# ── Main ────────────────────────────────────────────────────────────
def main() -> None:
"""Entry point for MEV risk estimator."""
parser = argparse.ArgumentParser(
description="Estimate MEV risk for a planned Solana DEX trade"
)
parser.add_argument(
"--mint",
type=str,
default="",
help="Token mint address to analyze",
)
parser.add_argument(
"--size",
type=float,
default=0,
help="Trade size in SOL",
)
parser.add_argument(
"--slippage",
type=int,
default=0,
help="Slippage tolerance in basis points",
)
parser.add_argument(
"--demo",
action="store_true",
help="Run with synthetic demo data",
)
args = parser.parse_args()
mint = args.mint or TOKEN_MINT
trade_size = args.size if args.size > 0 else TRADE_SIZE_SOL
slippage = args.slippage if args.slippage > 0 else SLIPPAGE_BPS
if args.demo or not mint:
run_demo()
else:
analyze_token(mint, trade_size, slippage)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Detect sandwich attacks around a Solana transaction.
Analyzes a transaction and its surrounding slot transactions to identify
potential sandwich attack patterns: same-token buy before and sell after
the target transaction by the same wallet.
Usage:
python scripts/sandwich_detector.py # demo mode
python scripts/sandwich_detector.py --tx <SIGNATURE> # analyze real tx
python scripts/sandwich_detector.py --demo # explicit demo
Dependencies:
uv pip install httpx
Environment Variables:
SOLANA_RPC_URL: Solana RPC endpoint (default: public mainnet-beta)
HELIUS_API_KEY: Optional Helius API key for enhanced transaction parsing
TX_SIGNATURE: Transaction signature to analyze (alternative to --tx flag)
"""
import argparse
import json
import os
import sys
from dataclasses import dataclass, field
from typing import Optional
import httpx
# ── Configuration ───────────────────────────────────────────────────
SOLANA_RPC_URL = os.getenv(
"SOLANA_RPC_URL", "https://api.mainnet-beta.solana.com"
)
HELIUS_API_KEY = os.getenv("HELIUS_API_KEY", "")
TX_SIGNATURE = os.getenv("TX_SIGNATURE", "")
# If Helius key is available, use Helius RPC for better parsing
if HELIUS_API_KEY:
SOLANA_RPC_URL = f"https://mainnet.helius-rpc.com/?api-key={HELIUS_API_KEY}"
# Known MEV bot patterns (partial list — real detection uses heuristics)
KNOWN_MEV_PROGRAMS = [
"JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4", # Jupiter v6
]
# Jito tip program addresses
JITO_TIP_ACCOUNTS = [
"96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5",
"HFqU5x63VTqvQss8hp11i4bVqkfRtQ7NmXwkiYGganbN",
"Cw8CFyM9FkoMi7K7Crf6HNQqf4uEMzpKw6QNghXLvLkY",
"ADaUMid9yfUytqMBgopwjb2o2J5Yfc54gXokGe4vDbvL",
"DfXygSm4jCyNCybVYYK6DwvWqjKee8pbDmJGcLWNDXjh",
"ADuUkR4vqLUMWXxW9gh6D6L8pMSawimctcNZ5pGwDcEt",
"DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL",
"3AVi9Tg9Uo68tJfuvoKvqKNWKkC5wPdSSdeBnizKZ6jT",
]
REQUEST_TIMEOUT = 30.0
MAX_SLOT_TXS = 50 # Max transactions to fetch from a slot
# ── Data Classes ────────────────────────────────────────────────────
@dataclass
class TokenTransfer:
"""A token transfer within a transaction."""
mint: str
source: str
destination: str
amount: float
decimals: int
@dataclass
class ParsedTransaction:
"""Simplified parsed transaction for MEV analysis."""
signature: str
slot: int
block_time: Optional[int]
signer: str
success: bool
fee_lamports: int
token_transfers: list[TokenTransfer] = field(default_factory=list)
has_jito_tip: bool = False
instruction_count: int = 0
@dataclass
class SandwichResult:
"""Result of sandwich detection analysis."""
is_sandwiched: bool
confidence: str # "HIGH", "MEDIUM", "LOW", "NONE"
attacker_wallet: Optional[str]
front_run_tx: Optional[str]
back_run_tx: Optional[str]
token_mint: Optional[str]
estimated_cost_usd: float
details: list[str] = field(default_factory=list)
# ── RPC Helpers ─────────────────────────────────────────────────────
def rpc_request(method: str, params: list, client: httpx.Client) -> dict:
"""Send a JSON-RPC request to Solana RPC.
Args:
method: RPC method name.
params: Method parameters.
client: httpx Client instance.
Returns:
Parsed JSON response.
Raises:
httpx.HTTPStatusError: On non-2xx response.
RuntimeError: On RPC error response.
"""
resp = client.post(
SOLANA_RPC_URL,
json={
"jsonrpc": "2.0",
"id": 1,
"method": method,
"params": params,
},
timeout=REQUEST_TIMEOUT,
)
resp.raise_for_status()
data = resp.json()
if "error" in data:
raise RuntimeError(f"RPC error: {data['error']}")
return data.get("result", {})
def fetch_transaction(
signature: str, client: httpx.Client
) -> Optional[ParsedTransaction]:
"""Fetch and parse a transaction by signature.
Args:
signature: Transaction signature (base58).
client: httpx Client instance.
Returns:
ParsedTransaction or None if not found.
"""
try:
result = rpc_request(
"getTransaction",
[
signature,
{
"encoding": "jsonParsed",
"maxSupportedTransactionVersion": 0,
"commitment": "confirmed",
},
],
client,
)
except RuntimeError:
return None
if not result:
return None
tx_data = result.get("transaction", {})
meta = result.get("meta", {})
message = tx_data.get("message", {})
account_keys = message.get("accountKeys", [])
# Extract signer (first account key)
signer = ""
if account_keys:
if isinstance(account_keys[0], dict):
signer = account_keys[0].get("pubkey", "")
else:
signer = account_keys[0]
# Parse token transfers from inner instructions and post/pre token balances
transfers = _extract_token_transfers(meta)
# Check for Jito tip
has_jito_tip = _check_jito_tip(meta, message)
# Count instructions
instructions = message.get("instructions", [])
inner = meta.get("innerInstructions", [])
total_ix = len(instructions) + sum(
len(ix_set.get("instructions", [])) for ix_set in inner
)
return ParsedTransaction(
signature=signature,
slot=result.get("slot", 0),
block_time=result.get("blockTime"),
signer=signer,
success=meta.get("err") is None,
fee_lamports=meta.get("fee", 0),
token_transfers=transfers,
has_jito_tip=has_jito_tip,
instruction_count=total_ix,
)
def _extract_token_transfers(meta: dict) -> list[TokenTransfer]:
"""Extract token transfers from transaction metadata.
Args:
meta: Transaction metadata from RPC response.
Returns:
List of TokenTransfer objects.
"""
transfers = []
pre_balances = meta.get("preTokenBalances", [])
post_balances = meta.get("postTokenBalances", [])
# Build balance change map: (account_index, mint) -> change
pre_map: dict[tuple[int, str], float] = {}
for bal in pre_balances:
idx = bal.get("accountIndex", -1)
mint = bal.get("mint", "")
amount = float(bal.get("uiTokenAmount", {}).get("uiAmount") or 0)
pre_map[(idx, mint)] = amount
for bal in post_balances:
idx = bal.get("accountIndex", -1)
mint = bal.get("mint", "")
post_amount = float(
bal.get("uiTokenAmount", {}).get("uiAmount") or 0
)
pre_amount = pre_map.get((idx, mint), 0.0)
change = post_amount - pre_amount
if abs(change) > 0:
owner = bal.get("owner", "")
decimals = bal.get("uiTokenAmount", {}).get("decimals", 0)
transfers.append(
TokenTransfer(
mint=mint,
source=owner if change < 0 else "",
destination=owner if change > 0 else "",
amount=change,
decimals=decimals,
)
)
return transfers
def _check_jito_tip(meta: dict, message: dict) -> bool:
"""Check if the transaction includes a Jito tip.
Args:
meta: Transaction metadata.
message: Transaction message.
Returns:
True if a Jito tip transfer is detected.
"""
# Check if any account in the transaction is a Jito tip account
account_keys = message.get("accountKeys", [])
for key in account_keys:
pubkey = key.get("pubkey", "") if isinstance(key, dict) else key
if pubkey in JITO_TIP_ACCOUNTS:
return True
return False
def fetch_slot_transactions(
slot: int, client: httpx.Client
) -> list[str]:
"""Fetch transaction signatures from a specific slot.
Args:
slot: Slot number.
client: httpx Client instance.
Returns:
List of transaction signatures in the slot.
"""
try:
result = rpc_request(
"getBlock",
[
slot,
{
"encoding": "jsonParsed",
"maxSupportedTransactionVersion": 0,
"transactionDetails": "signatures",
"rewards": False,
},
],
client,
)
except RuntimeError:
return []
if not result:
return []
signatures = result.get("signatures", [])
return signatures[:MAX_SLOT_TXS]
# ── Sandwich Detection ─────────────────────────────────────────────
def detect_sandwich(
target_tx: ParsedTransaction,
slot_txs: list[ParsedTransaction],
) -> SandwichResult:
"""Analyze whether a transaction was sandwiched.
Looks for the pattern:
1. TX_A: Wallet W buys token T (front-run)
2. TARGET: Your swap of token T (victim)
3. TX_B: Wallet W sells token T (back-run)
Where TX_A appears before TARGET and TX_B after in slot ordering.
Args:
target_tx: The target transaction to analyze.
slot_txs: Other transactions in the same slot.
Returns:
SandwichResult with detection outcome.
"""
details: list[str] = []
# Get mints involved in target transaction
target_mints = set()
for t in target_tx.token_transfers:
if t.mint:
target_mints.add(t.mint)
if not target_mints:
return SandwichResult(
is_sandwiched=False,
confidence="NONE",
attacker_wallet=None,
front_run_tx=None,
back_run_tx=None,
token_mint=None,
estimated_cost_usd=0.0,
details=["No token transfers found in target transaction"],
)
details.append(
f"Target tx involves {len(target_mints)} token mint(s): "
f"{', '.join(list(target_mints)[:3])}"
)
# Find potential front-run/back-run pairs
# Group slot transactions by signer and mint
candidates: dict[str, dict[str, list[ParsedTransaction]]] = {}
for tx in slot_txs:
if tx.signature == target_tx.signature:
continue
if not tx.success:
continue
for transfer in tx.token_transfers:
if transfer.mint in target_mints:
signer = tx.signer
mint = transfer.mint
if signer not in candidates:
candidates[signer] = {}
if mint not in candidates[signer]:
candidates[signer][mint] = []
candidates[signer][mint].append(tx)
if not candidates:
return SandwichResult(
is_sandwiched=False,
confidence="NONE",
attacker_wallet=None,
front_run_tx=None,
back_run_tx=None,
token_mint=None,
estimated_cost_usd=0.0,
details=details
+ ["No other transactions in this slot touch the same tokens"],
)
# Look for buy-before + sell-after pattern
best_match: Optional[SandwichResult] = None
best_confidence_rank = 0
for signer, mint_txs in candidates.items():
for mint, txs in mint_txs.items():
if len(txs) < 2:
continue
# Check for opposite-direction transfers (buy and sell)
buys = [
tx
for tx in txs
if any(
t.amount > 0 and t.mint == mint
for t in tx.token_transfers
)
]
sells = [
tx
for tx in txs
if any(
t.amount < 0 and t.mint == mint
for t in tx.token_transfers
)
]
if buys and sells:
# Potential sandwich found
has_jito = any(tx.has_jito_tip for tx in txs)
high_ix_count = any(tx.instruction_count > 5 for tx in txs)
confidence_score = 0
indicators = []
if has_jito:
confidence_score += 3
indicators.append("Uses Jito bundles")
if high_ix_count:
confidence_score += 1
indicators.append("High instruction count")
if len(buys) == 1 and len(sells) == 1:
confidence_score += 2
indicators.append("Single buy + single sell pattern")
if signer != target_tx.signer:
confidence_score += 1
indicators.append("Different signer from target")
if confidence_score >= 4:
confidence = "HIGH"
elif confidence_score >= 2:
confidence = "MEDIUM"
else:
confidence = "LOW"
confidence_rank = confidence_score
if confidence_rank > best_confidence_rank:
best_confidence_rank = confidence_rank
best_match = SandwichResult(
is_sandwiched=confidence_score >= 2,
confidence=confidence,
attacker_wallet=signer,
front_run_tx=buys[0].signature,
back_run_tx=sells[0].signature,
token_mint=mint,
estimated_cost_usd=0.0, # Would need price data
details=details + indicators,
)
if best_match:
return best_match
return SandwichResult(
is_sandwiched=False,
confidence="NONE",
attacker_wallet=None,
front_run_tx=None,
back_run_tx=None,
token_mint=None,
estimated_cost_usd=0.0,
details=details
+ [
"No buy-sell pairs found from the same wallet "
"around the target transaction"
],
)
# ── MEV Wallet Heuristics ──────────────────────────────────────────
def is_likely_mev_wallet(
tx_count: int,
avg_hold_time_seconds: float,
jito_usage_pct: float,
) -> dict:
"""Heuristic check for MEV bot wallet characteristics.
Args:
tx_count: Number of transactions in last 24h.
avg_hold_time_seconds: Average time tokens are held.
jito_usage_pct: Percentage of transactions using Jito.
Returns:
Assessment dict with probability and indicators.
"""
score = 0
indicators = []
if tx_count > 500:
score += 3
indicators.append(f"Very high tx count ({tx_count}/day)")
elif tx_count > 100:
score += 2
indicators.append(f"High tx count ({tx_count}/day)")
if avg_hold_time_seconds < 5:
score += 3
indicators.append(
f"Near-zero hold time ({avg_hold_time_seconds:.1f}s)"
)
elif avg_hold_time_seconds < 60:
score += 2
indicators.append(f"Very short hold time ({avg_hold_time_seconds:.0f}s)")
if jito_usage_pct > 80:
score += 3
indicators.append(f"Heavy Jito usage ({jito_usage_pct:.0f}%)")
elif jito_usage_pct > 40:
score += 2
indicators.append(f"Moderate Jito usage ({jito_usage_pct:.0f}%)")
if score >= 7:
probability = "VERY HIGH"
elif score >= 5:
probability = "HIGH"
elif score >= 3:
probability = "MEDIUM"
else:
probability = "LOW"
return {
"probability": probability,
"score": score,
"max_score": 9,
"indicators": indicators,
}
# ── Demo Mode ───────────────────────────────────────────────────────
def run_demo() -> None:
"""Run a demonstration with synthetic sandwich attack data."""
print("=" * 70)
print("SANDWICH ATTACK DETECTOR — DEMO MODE")
print("=" * 70)
print()
print("Simulating analysis of a sandwiched SOL → TOKEN_X swap...")
print()
# Synthetic target transaction
target = ParsedTransaction(
signature="5xDemoTargetTx111111111111111111111111111111111111",
slot=280_000_000,
block_time=1710000000,
signer="UserWa11et1111111111111111111111111111111111",
success=True,
fee_lamports=5000,
token_transfers=[
TokenTransfer(
mint="DemoToken111111111111111111111111111111111111",
source="",
destination="UserWa11et1111111111111111111111111111111111",
amount=9850.0, # Got less than expected (10000)
decimals=6,
),
TokenTransfer(
mint="So11111111111111111111111111111111111111112",
source="UserWa11et1111111111111111111111111111111111",
destination="",
amount=-10.0, # Spent 10 SOL
decimals=9,
),
],
has_jito_tip=False,
instruction_count=4,
)
# Synthetic attacker front-run
front_run = ParsedTransaction(
signature="5xDemoFrontRun11111111111111111111111111111111111",
slot=280_000_000,
block_time=1710000000,
signer="MEVBot111111111111111111111111111111111111111",
success=True,
fee_lamports=5000,
token_transfers=[
TokenTransfer(
mint="DemoToken111111111111111111111111111111111111",
source="",
destination="MEVBot111111111111111111111111111111111111111",
amount=50000.0,
decimals=6,
),
],
has_jito_tip=True,
instruction_count=8,
)
# Synthetic attacker back-run
back_run = ParsedTransaction(
signature="5xDemoBackRun111111111111111111111111111111111111",
slot=280_000_000,
block_time=1710000000,
signer="MEVBot111111111111111111111111111111111111111",
success=True,
fee_lamports=5000,
token_transfers=[
TokenTransfer(
mint="DemoToken111111111111111111111111111111111111",
source="MEVBot111111111111111111111111111111111111111",
destination="",
amount=-50000.0,
decimals=6,
),
],
has_jito_tip=True,
instruction_count=8,
)
slot_txs = [front_run, target, back_run]
result = detect_sandwich(target, slot_txs)
_print_result(target, result)
# Also demo the MEV wallet heuristic
print()
print("-" * 70)
print("MEV WALLET HEURISTIC ANALYSIS")
print("-" * 70)
wallet_assessment = is_likely_mev_wallet(
tx_count=2400,
avg_hold_time_seconds=1.2,
jito_usage_pct=92.0,
)
print(f" Wallet: MEVBot111...111")
print(f" MEV probability: {wallet_assessment['probability']}")
print(f" Score: {wallet_assessment['score']}/{wallet_assessment['max_score']}")
print(f" Indicators:")
for ind in wallet_assessment["indicators"]:
print(f" - {ind}")
print()
print("=" * 70)
print("NOTE: This is synthetic demo data. Use --tx <SIGNATURE> to analyze")
print("a real transaction with live on-chain data.")
print("=" * 70)
def _print_result(
target: ParsedTransaction, result: SandwichResult
) -> None:
"""Print sandwich detection results.
Args:
target: The target transaction.
result: Detection result.
"""
print("-" * 70)
print("SANDWICH DETECTION RESULT")
print("-" * 70)
print(f" Target TX: {target.signature[:20]}...")
print(f" Slot: {target.slot}")
print(f" Signer: {target.signer[:20]}...")
print()
if result.is_sandwiched:
print(f" *** SANDWICH DETECTED (Confidence: {result.confidence}) ***")
print()
print(f" Attacker: {result.attacker_wallet or 'Unknown'}")
print(f" Front-run TX: {(result.front_run_tx or '')[:20]}...")
print(f" Back-run TX: {(result.back_run_tx or '')[:20]}...")
print(f" Token mint: {(result.token_mint or '')[:20]}...")
if result.estimated_cost_usd > 0:
print(f" Est. cost: ${result.estimated_cost_usd:.2f}")
else:
print(f" No sandwich detected (Confidence: {result.confidence})")
if result.details:
print()
print(" Analysis details:")
for detail in result.details:
print(f" - {detail}")
# ── Live Analysis ───────────────────────────────────────────────────
def analyze_transaction(signature: str) -> None:
"""Analyze a real transaction for sandwich attacks.
Args:
signature: Transaction signature to analyze.
"""
print(f"Fetching transaction: {signature[:20]}...")
print(f"RPC endpoint: {SOLANA_RPC_URL[:50]}...")
print()
with httpx.Client() as client:
# Fetch target transaction
target = fetch_transaction(signature, client)
if not target:
print("ERROR: Could not fetch transaction. Check the signature")
print("and ensure the RPC endpoint is accessible.")
sys.exit(1)
print(f"Transaction found in slot {target.slot}")
print(f"Signer: {target.signer}")
print(f"Success: {target.success}")
print(f"Token transfers: {len(target.token_transfers)}")
print()
if not target.token_transfers:
print("No token transfers found. This may not be a swap transaction.")
print("Sandwich detection requires a token swap to analyze.")
return
# Fetch slot transactions
print(f"Fetching transactions from slot {target.slot}...")
slot_sigs = fetch_slot_transactions(target.slot, client)
print(f"Found {len(slot_sigs)} transactions in slot")
print()
if len(slot_sigs) < 2:
print("Too few transactions in slot for sandwich analysis.")
return
# Parse surrounding transactions (limit to avoid rate limits)
print("Parsing surrounding transactions for sandwich patterns...")
slot_txs: list[ParsedTransaction] = []
analyzed = 0
for sig in slot_sigs:
if sig == signature:
continue
if analyzed >= MAX_SLOT_TXS:
break
tx = fetch_transaction(sig, client)
if tx and tx.success:
slot_txs.append(tx)
analyzed += 1
print(f"Parsed {len(slot_txs)} surrounding transactions")
print()
# Run detection
result = detect_sandwich(target, slot_txs)
_print_result(target, result)
# ── Main ────────────────────────────────────────────────────────────
def main() -> None:
"""Entry point for sandwich detector."""
parser = argparse.ArgumentParser(
description="Detect sandwich attacks around a Solana transaction"
)
parser.add_argument(
"--tx",
type=str,
default="",
help="Transaction signature to analyze",
)
parser.add_argument(
"--demo",
action="store_true",
help="Run with synthetic demo data",
)
args = parser.parse_args()
signature = args.tx or TX_SIGNATURE
if args.demo or not signature:
run_demo()
else:
analyze_transaction(signature)
if __name__ == "__main__":
main()