Sandwich bots extracted $289 million from Ethereum swaps in 2025 alone, accounting for over 51% of all MEV activity on the network (ScienceDirect, 2025). Every unprotected swap is a potential victim. If you are building a swap service, wallet, or trading bot, MEV protection is not optional.
This guide walks you through building a MEV-protected token swap service in TypeScript. You will learn what MEV is, how sandwich attacks work, and how to use pre-trade validation, slippage enforcement, and private transaction submission to protect every swap.
What You Will Need
- Node.js 18+ or Bun runtime
- TypeScript configured in your project
- A wallet library: viem or ethers.js v6
- Access to a swap API (we will use swapapi.dev -- free, no API key, 46 chains)
- A private RPC endpoint (Flashbots Protect, MEV Blocker, or similar)
Step 1: Understand MEV and Why Your Swaps Are Vulnerable
Maximal Extractable Value (MEV) is the profit that block producers and searchers can extract by reordering, inserting, or censoring transactions within a block. The most common MEV attack targeting swaps is the sandwich attack.
How a Sandwich Attack Works
- You submit a swap transaction to the public mempool (buy ETH with USDC).
- A bot sees your pending transaction and front-runs you -- it buys ETH first, pushing the price up.
- Your swap executes at the inflated price.
- The bot back-runs you -- it sells the ETH it just bought, pocketing the difference.
The result: you receive fewer tokens than you should have. The bot profits at your expense.
Sandwich attacks average more than one per block on Ethereum (arxiv.org, 2025). Across all chains, private routing adoption grew from 31.8% to over 50% of transactions between November 2024 and February 2025 as users sought protection (arxiv.org, 2024).
Step 2: Get a Protected Swap Quote with Price Impact Data
The first layer of MEV protection happens before you ever submit a transaction. You need to check the price impact and enforce slippage bounds on every quote.
swapapi.dev returns both priceImpact and minAmountOut in every response, giving you the data you need to reject bad trades programmatically.
interface SwapQuote {
success: boolean;
data: {
status: "Successful" | "Partial" | "NoRoute";
priceImpact: number;
expectedAmountOut: string;
minAmountOut: string;
amountIn: string;
tokenFrom: { symbol: string; decimals: number };
tokenTo: { symbol: string; decimals: number };
tx: {
from: string;
to: string;
data: string;
value: string;
gasPrice: number;
};
rpcUrls: string[];
};
timestamp: string;
}
async function getSwapQuote(
chainId: number,
tokenIn: string,
tokenOut: string,
amount: string,
sender: string,
maxSlippage: number = 0.005
): Promise<SwapQuote> {
const url = new URL(
`https://api.swapapi.dev/v1/swap/${chainId}`
);
url.searchParams.set("tokenIn", tokenIn);
url.searchParams.set("tokenOut", tokenOut);
url.searchParams.set("amount", amount);
url.searchParams.set("sender", sender);
url.searchParams.set("maxSlippage", maxSlippage.toString());
const response = await fetch(url.toString(), {
signal: AbortSignal.timeout(15_000),
});
return response.json() as Promise<SwapQuote>;
}
The maxSlippage parameter (0 to 1) controls the on-chain minimum. With 0.005 (0.5%), the transaction reverts if the output drops more than 0.5% below expectedAmountOut. This is your on-chain safety net against sandwich attacks.
Step 3: Validate Price Impact Before Execution
Price impact tells you how much your trade moves the market. A swap with -0.12% price impact is normal. A swap with -8% price impact is either hitting a low-liquidity pool or being set up for extraction.
Flashbots Protect has shielded $43 billion in DEX volume across 2.1 million unique accounts (Flashbots, 2025). But even with private submission, a high-impact trade on a thin pool still loses value. Pre-trade validation catches what private mempools cannot.
const MAX_ACCEPTABLE_IMPACT = -0.05; // -5%
const WARNING_IMPACT = -0.02; // -2%
function validatePriceImpact(
quote: SwapQuote
): { safe: boolean; warning: string | null } {
const impact = quote.data.priceImpact;
if (impact < MAX_ACCEPTABLE_IMPACT) {
return {
safe: false,
warning:
`Price impact ${(impact * 100).toFixed(2)}% exceeds ` +
`safety threshold. Trade rejected.`,
};
}
if (impact < WARNING_IMPACT) {
return {
safe: true,
warning:
`Price impact ${(impact * 100).toFixed(2)}% is elevated. ` +
`Consider reducing trade size.`,
};
}
return { safe: true, warning: null };
}
| Price Impact | Risk Level | Action |
|---|---|---|
| > -0.5% | Low | Execute normally |
| -0.5% to -2% | Medium | Proceed with monitoring |
| -2% to -5% | High | Warn user, suggest splitting |
| < -5% | Critical | Block execution |
Step 4: Detect Partial Fills and Liquidity Issues
The API returns a Partial status when only part of your requested amount can be filled. This is a liquidity signal. Large swaps hitting thin pools are prime sandwich targets because the price displacement is larger.
function checkPartialFill(
quote: SwapQuote,
requestedAmount: string
): { isPartial: boolean; fillPercentage: number } {
if (quote.data.status === "Partial") {
const requested = BigInt(requestedAmount);
const filled = BigInt(quote.data.amountIn);
const pct = Number((filled * 100n) / requested);
return { isPartial: true, fillPercentage: pct };
}
return { isPartial: false, fillPercentage: 100 };
}
When you detect a partial fill, you have three options: execute the partial amount as-is, reduce your trade to match available liquidity, or split the trade across multiple smaller swaps to reduce price impact per execution.
Step 5: Submit Transactions Through a Private RPC
The public mempool is where sandwich bots find their victims. The single most effective MEV protection is to bypass it entirely. Private transaction pools send your transaction directly to block builders, making it invisible to searchers.
Today, 80% of Ethereum transactions use private RPCs (arxiv.org, 2025). The major options are:
| Provider | Success Rate | Response Time | Cost |
|---|---|---|---|
| Flashbots Protect | 98.5% | 245ms | Free |
| MEV Blocker | 96.2% | 180ms | Free |
| Private builder endpoints | Varies | Varies | Paid |
Here is how to submit a swap transaction through Flashbots Protect using viem:
import {
createWalletClient,
http,
parseGwei,
} from "viem";
import { mainnet } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";
const FLASHBOTS_RPC = "https://rpc.flashbots.net";
const account = privateKeyToAccount(
process.env.PRIVATE_KEY as `0x${string}`
);
const walletClient = createWalletClient({
account,
chain: mainnet,
transport: http(FLASHBOTS_RPC),
});
async function submitProtectedSwap(
quote: SwapQuote
): Promise<string> {
const { tx } = quote.data;
const hash = await walletClient.sendTransaction({
to: tx.to as `0x${string}`,
data: tx.data as `0x${string}`,
value: BigInt(tx.value),
});
return hash;
}
By pointing your wallet client at https://rpc.flashbots.net instead of a public RPC, every transaction you send goes through Flashbots' private channel. No mempool exposure. No sandwich opportunity.
Step 6: Add Gas Estimation and Simulation
Before submitting, simulate the transaction with eth_call and estimate gas. This catches reverts before they cost you gas fees. The API does not include a gasLimit field -- you must estimate it yourself.
import {
createPublicClient,
http,
} from "viem";
import { mainnet } from "viem/chains";
const publicClient = createPublicClient({
chain: mainnet,
transport: http(FLASHBOTS_RPC),
});
async function simulateAndEstimate(
quote: SwapQuote
): Promise<{ gasLimit: bigint; simulationPassed: boolean }> {
const { tx } = quote.data;
const txParams = {
account: tx.from as `0x${string}`,
to: tx.to as `0x${string}`,
data: tx.data as `0x${string}`,
value: BigInt(tx.value),
};
try {
await publicClient.call(txParams);
} catch {
return { gasLimit: 0n, simulationPassed: false };
}
try {
const gasEstimate =
await publicClient.estimateGas(txParams);
const gasWithBuffer =
(gasEstimate * 120n) / 100n; // 20% buffer
return {
gasLimit: gasWithBuffer,
simulationPassed: true,
};
} catch {
return { gasLimit: 0n, simulationPassed: false };
}
}
The 20% gas buffer is important. Multi-hop swap routes can have variable gas consumption, and running out of gas means you pay fees but the swap reverts.
Step 7: Assemble the Full Protected Swap Pipeline
Now combine every layer into a single execution pipeline. This is the complete flow: quote, validate, simulate, submit.
const NATIVE_TOKEN =
"0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
const USDC_ETHEREUM =
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
async function executeProtectedSwap(
chainId: number,
tokenIn: string,
tokenOut: string,
amount: string,
sender: string
): Promise<{
success: boolean;
txHash?: string;
error?: string;
}> {
// 1. Get quote with tight slippage
const quote = await getSwapQuote(
chainId,
tokenIn,
tokenOut,
amount,
sender,
0.005
);
if (!quote.success) {
return {
success: false,
error: "Failed to get quote",
};
}
if (quote.data.status === "NoRoute") {
return {
success: false,
error: "No route found for this pair",
};
}
// 2. Check price impact
const impactCheck = validatePriceImpact(quote);
if (!impactCheck.safe) {
return {
success: false,
error: impactCheck.warning ?? "Price impact too high",
};
}
// 3. Check for partial fills
const fillCheck = checkPartialFill(quote, amount);
if (fillCheck.isPartial && fillCheck.fillPercentage < 80) {
return {
success: false,
error:
`Only ${fillCheck.fillPercentage}% of trade ` +
`can be filled. Insufficient liquidity.`,
};
}
// 4. Simulate and estimate gas
const sim = await simulateAndEstimate(quote);
if (!sim.simulationPassed) {
return {
success: false,
error: "Transaction simulation failed",
};
}
// 5. Submit through private RPC
const txHash = await submitProtectedSwap(quote);
return { success: true, txHash };
}
// Example: swap 1 ETH to USDC on Ethereum
executeProtectedSwap(
1,
NATIVE_TOKEN,
USDC_ETHEREUM,
"1000000000000000000",
"0xYourWalletAddress"
).then(console.log);
MEV Protection Strategy Comparison
Not all protection strategies are equal. Here is how they stack up:
| Strategy | Protects Against | Limitations | Complexity |
|---|---|---|---|
| Tight slippage tolerance | Sandwich attacks | May cause reverts in volatile markets | Low |
| Price impact checks | Large-trade extraction | Does not prevent frontrunning | Low |
| Private RPC (Flashbots) | Mempool-based MEV | Only works on supported chains | Medium |
| Trade splitting | Price impact manipulation | Increases gas costs and latency | Medium |
| Simulation (eth_call) | Failed transactions | Does not prevent MEV, only reverts | Low |
| All combined | Full coverage | Requires careful tuning | Medium |
The recommended approach is to layer all of these defenses. No single technique is sufficient. Tight slippage prevents the worst sandwich outcomes, price impact checks reject toxic trades, private RPCs hide transactions from searchers, and simulation prevents wasted gas.
Multi-Chain Considerations
MEV exists on every EVM chain, not just Ethereum. Arbitrum and Base have sequencer-ordered transactions that reduce but do not eliminate MEV. BSC and Polygon have active sandwich bots.
The swap API used in this guide supports 46 chains. The same quote-validate-simulate-submit pipeline works across all of them. Adjust the private RPC endpoint per chain:
-
Ethereum (chainId 1): Flashbots Protect (
https://rpc.flashbots.net) -
Arbitrum (chainId 42161): Sequencer provides some ordering guarantees. Use the API-provided
rpcUrlsas fallbacks. - BSC (chainId 56): bloXroute or 48 Club private RPCs available. Watch for the 18-decimal stablecoin gotcha -- USDC and USDT use 18 decimals on BSC, not 6.
- Polygon (chainId 137): Fastlane or Marlin private RPCs.
FAQ
What is MEV protection for swaps and why does it matter?
MEV protection prevents block producers and searchers from profiting at your expense during token swaps. Without it, sandwich bots can front-run and back-run your transactions, costing you 0.1% to 5%+ per trade. In 2025, sandwich attacks alone extracted $289 million on Ethereum.
How does slippage tolerance protect against sandwich attacks?
Slippage tolerance sets a minimum output amount for your swap. If a sandwich bot pushes the price far enough that your output drops below minAmountOut, the transaction reverts instead of executing at a bad price. Setting maxSlippage to 0.005 (0.5%) is a good default; tighter for stablecoins, looser for volatile pairs.
Can MEV attacks happen on Layer 2 chains like Arbitrum and Base?
Yes, though the attack surface is different. L2 sequencers control transaction ordering, which reduces public mempool exposure. However, sequencer operators or privileged parties can still extract MEV. Private RPCs are less critical on L2s, but price impact checks and slippage enforcement remain essential.
Is a private RPC enough to fully prevent MEV?
No. Research shows that even private routing channels are not immune -- 2,932 private sandwich attacks were confirmed in late 2024, producing $409,236 in losses (arxiv.org, 2024). You need layered protection: slippage bounds, price impact validation, simulation, and private submission together.
Get Started
swapapi.dev gives you the quote data you need to build MEV-protected swap services: priceImpact, expectedAmountOut, minAmountOut, and ready-to-execute tx calldata with built-in slippage enforcement.
- Free -- no API key, no registration, no rate limit surprises
- 46 chains -- Ethereum, Arbitrum, Base, BSC, Polygon, and 41 more
- OpenAPI spec -- generate typed clients in any language
- API docs -- interactive Swagger UI
Start building: https://api.swapapi.dev
Top comments (0)