DEV Community

Moon Soon
Moon Soon

Posted on • Originally published at swapapi.dev

How to Build a MEV-Protected Swap Service in TypeScript

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

  1. You submit a swap transaction to the public mempool (buy ETH with USDC).
  2. A bot sees your pending transaction and front-runs you -- it buys ETH first, pushing the price up.
  3. Your swap executes at the inflated price.
  4. 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>;
}
Enter fullscreen mode Exit fullscreen mode

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 };
}
Enter fullscreen mode Exit fullscreen mode
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 };
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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 };
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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 rpcUrls as 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)