Press ESC to close

MEV Bot Profitability 2026: Costs, Arbitrage & Gas Strategy

The raw economics of MEV come down to brutal block space competition where a millisecond of network latency wipes out your entire potential profit. Spinning up your own bot on Ethereum or Layer 2 EVM networks hasn't been an easy-money play in a long time. The market is thoroughly dominated by specialized searchers renting co-located servers in Frankfurt and Tokyo right next to validator nodes.

Real Numbers and Profitability

A solo bot running from a home server or standard cloud VPS is burning money 95% of the time due to gas auctions. Profitability entirely depends on your strategy:

  • Atomic Arbitrage: Trade margins have compressed to fractional percentages. A successful bot pulls in anywhere from $100 to $3,000 in net daily profit during high-volatility windows. Capital loss risk is effectively zero since the entire transaction is wrapped in a single atomic smart contract: if conditions don't align, the whole call stack reverts.
  • Sandwich Attacks: Theoretical yields are higher, ranging from $500 to $10,000 per day on low-liquidity pairs across Uniswap v2 or similar AMMs. Reality is much harsher. Competitive bots queue up through private transaction pools, aggressively outbidding rivals with higher validator bribes.
  • Liquidations: The spot market across lending protocols like Aave offers massive payouts, but requires millions in liquidity to cover debt and instantly settle positions. Solos need not apply.

Gas Cost Breakdown and Working with Builders

Your biggest expense when running an MEV bot isn't successful transactions—it's failed attempts and gas priority auctions (GPAs).

  • Public Mempool: Bypassing private relays when submitting transactions guarantees a loss. A basic bot spots a victim's tx, fires off its own with a higher gas bid, but rival bots intercept it first via private RPCs. End result: your transaction executes at a terrible price or outright reverts, burning real gas on-chain.
  • Block Builders and Flashbots: Modern arbitrage runs on eth_sendBundle. You bundle multiple transactions together and pipe them straight to block builders (Beaverbuild, Titan, Flashbots).
  • Failure Fee Structure: The upside of private bundles is that when an arbitrage play fails, the transaction simply doesn't make it into the block. You don't pay network fees for misses. The downside is that validators pocket the lion's share of profits as a direct tip (coinbase.transfer), leaving the developer with roughly 5% to 15% of the gross profit.

MEV Strategy Metrics

StrategyStarting CapitalAvg. Daily Net ProfitSuccess RatePrimary Loss Driver
Atomic Arbitrage$5k+$50 - $50015-25%Network latency outcompeting rivals
Sandwich Attacks$20k+$200 - $3,0005-10%Overbidding gas in validator auctions
Liquidations$100k+$1,000 - $10,000+< 2%Capital shortage for large debt payoffs

Python Private Bundle Submission Example

Here is a working Python skeleton using web3.py to submit a bundle via a Flashbots-compatible provider, stripped of unnecessary fluff.

import os
from eth_account import Account
from eth_account.signers.local import LocalAccount
from web3 import Web3
from flashbots import flashbot
# Key for signing relay requests (Flashbots reputation).
# DO NOT use a key holding actual funds.
signature_account: LocalAccount = Account.from_key(os.environ["FLASHBOTS_SIGNER_KEY"])
# Transaction sender private key
sender: LocalAccount = Account.from_key(os.environ["SENDER_PRIVATE_KEY"])
w3 = Web3(Web3.HTTPProvider(os.environ["ETH_NODE_URL"]))
flashbot(w3, signature_account, "https://relay-sepolia.flashbots.net")
def build_signed_tx(nonce: int) -> bytes:
    base_fee = w3.eth.get_block("latest")["baseFeePerGas"]
    tx = {
        "to": sender.address,          # example: self-transfer
        "value": 0,
        "gas": 21_000,
        "maxFeePerGas": base_fee * 2 + Web3.to_wei(2, "gwei"),
        "maxPriorityFeePerGas": Web3.to_wei(2, "gwei"),
        "nonce": nonce,
        "chainId": w3.eth.chain_id,
        "type": 2,
    }
    return sender.sign_transaction(tx).rawTransaction  # in newer versions: .raw_transaction
def send_private_bundle(target_block: int):
    nonce = w3.eth.get_transaction_count(sender.address)
    bundle = [{"signed_transaction": build_signed_tx(nonce)}]
    # Simulate before submitting
    w3.flashbots.simulate(bundle, target_block)
    result = w3.flashbots.send_bundle(bundle, target_block_number=target_block)
    result.wait()  # wait until target_block is processed
    try:
        return result.receipts()
    except Exception:
        return None  # bundle not included in block: retry on next block
if __name__ == "__main__":
    target = w3.eth.block_number + 1
    receipts = send_private_bundle(target)
    print("Included in block" if receipts else "Bundle not included, retry on the next block")

Bundles are only valid for a single target block, so in practice you need to loop submission across several consecutive blocks. Missing inclusion isn't an error—it just means rivals out-tipped you on the builder bribe.

Bot profitability ultimately boils down to infrastructure costs. Renting a dedicated rack server right next to a validator in a datacenter runs upwards of $500 a month. Without that hardware advantage, your bot physically cannot compute pool state fast enough to beat competitors. Whoever has the shortest cable to the validator and the leanest spread-detection algorithm wins.

Flash loans make it possible to run large-scale arbitrage operations without needing deep pockets of your own, using unsecured borrowing inside a single atomic transaction with a guaranteed rollback if things go south.

Flash Loans and Smart Contract Architecture

Having hundreds of thousands in startup capital isn't a hard requirement anymore. Integration contracts with Aave v3 or Balancer pools let you pull unsecured flash loans within a single transaction boundary. Your bot borrows the asset, routes it across DEX pairs, pays back the principal plus the 0.09% fee, and sweeps the leftover profit straight to the owner's balance. If the math doesn't clear after all the hops, the whole transaction reverts cleanly, leaving you out only the gas fees.

Writing an arbitrage contract in Solidity demands aggressive EVM bytecode optimization. Relying on standard ERC-20 interfaces via SafeERC20 brings in redundant checks that bleed precious gas units on every call limit check. Seasoned devs write raw low-level calls using call, keeping SLOAD and SSTORE persistent memory reads and writes down to the bare minimum.

Solidity Template Example

Here is an Aave V3 flash loan template built for Uniswap V2 style pools, completely free of bulky third-party libraries:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
interface IERC20 {
    function balanceOf(address account) external view returns (uint256);
    function transfer(address to, uint256 amount) external returns (bool);
    function approve(address spender, uint256 amount) external returns (bool);
}
/// Aave V3: single asset flash loan
interface IPool {
    function flashLoanSimple(
        address receiverAddress,
        address asset,
        uint256 amount,
        bytes calldata params,
        uint16 referralCode
    ) external;
}
interface IUniswapV2Pair {
    function token0() external view returns (address);
    function getReserves() external view returns (uint112, uint112, uint32);
    function swap(uint256 amount0Out, uint256 amount1Out, address to, bytes calldata data) external;
}
/// Aave V3 Flash Loan Arbitrage: asset -> tokenB (pair1) -> asset (pair2).
/// Example for Uniswap V2 type pools. Unaudited code.
contract FlashArbitrage {
    error Unauthorized();
    error InvalidCaller();
    error InvalidInitiator();
    error TransferFailed();
    error ApproveFailed();
    error InsufficientProfit(uint256 balanceAfter, uint256 required);
    address private immutable OWNER;
    IPool private immutable POOL;
    constructor(address aavePool) {
        OWNER = msg.sender;
        POOL = IPool(aavePool); // grab the Pool address from the target network's PoolAddressesProvider
    }
    modifier onlyOwner() {
        if (msg.sender != OWNER) revert Unauthorized();
        _;
    }
    /// Entry point. Borrows `amount` of `asset` and triggers the executeOperation callback loop.
    function executeArbitrage(
        address asset,
        uint256 amount,
        address tokenB,
        address pair1,
        address pair2,
        uint256 minProfit
    ) external onlyOwner {
        bytes memory params = abi.encode(tokenB, pair1, pair2, minProfit);
        POOL.flashLoanSimple(address(this), asset, amount, params, 0);
    }
    /// Aave V3 callback. Invoked by the pool right after issuing the loan.
    function executeOperation(
        address asset,
        uint256 amount,
        uint256 premium,
        address initiator,
        bytes calldata params
    ) external returns (bool) {
        // 1. Only the Aave pool can trigger this callback...
        if (msg.sender != address(POOL)) revert InvalidCaller();
        // 2. ...and only when triggered by this exact contract
        if (initiator != address(this)) revert InvalidInitiator();
        (address tokenB, address pair1, address pair2, uint256 minProfit) =
            abi.decode(params, (address, address, address, uint256));
        // Balance already includes the borrowed funds
        uint256 balanceBefore = IERC20(asset).balanceOf(address(this));
        uint256 amountB = _swap(pair1, asset, amount);
        _swap(pair2, tokenB, amountB);
        // Profit = balanceAfter - balanceBefore - premium (loan fee)
        uint256 balanceAfter = IERC20(asset).balanceOf(address(this));
        uint256 required = balanceBefore + premium + minProfit;
        if (balanceAfter < required) revert InsufficientProfit(balanceAfter, required);
        // Aave pulls the repayment via transferFrom, so an approve is required
        _safeApprove(asset, address(POOL), amount + premium);
        return true;
    }
    function withdraw(address token, uint256 amount) external onlyOwner {
        _safeTransfer(token, OWNER, amount);
    }
    function _swap(address pair, address tokenIn, uint256 amountIn) private returns (uint256 amountOut) {
        (uint256 r0, uint256 r1, ) = IUniswapV2Pair(pair).getReserves();
        bool zeroForOne = tokenIn == IUniswapV2Pair(pair).token0();
        (uint256 reserveIn, uint256 reserveOut) = zeroForOne ? (r0, r1) : (r1, r0);
        // Uniswap V2 formula (0.3% fee)
        uint256 amountInWithFee = amountIn * 997;
        amountOut = (amountInWithFee * reserveOut) / (reserveIn * 1000 + amountInWithFee);
        _safeTransfer(tokenIn, pair, amountIn);
        IUniswapV2Pair(pair).swap(
            zeroForOne ? 0 : amountOut,
            zeroForOne ? amountOut : 0,
            address(this),
            "" // empty data: standard swap, not a flash swap
        );
    }
    function _safeTransfer(address token, address to, uint256 amount) private {
        if (token.code.length == 0) revert TransferFailed();
        (bool ok, bytes memory data) = token.call(abi.encodeCall(IERC20.transfer, (to, amount)));
        if (!ok || (data.length != 0 && !abi.decode(data, (bool)))) revert TransferFailed();
    }
    function _safeApprove(address token, address spender, uint256 amount) private {
        if (token.code.length == 0) revert ApproveFailed();
        (bool ok, bytes memory data) = token.call(abi.encodeCall(IERC20.approve, (spender, amount)));
        if (!ok || (data.length != 0 && !abi.decode(data, (bool)))) revert ApproveFailed();
    }
}

The AWS Infrastructure Dead End

Deploying searcher bots on cloud VPS instances like Amazon Web Services or Google Cloud is a guaranteed way to burn your budget. Virtualization layers add latency jitter to the networking stack, and shared backbone routes suffer from packet drops during peak mempool congestion.

A competitive setup requires a physical bare-metal server with high single-core CPU clock speeds, fast NVMe drives running a local Geth or Reth execution client, and a direct gigabit uplink into Frankfurt data centers. Running your own lightweight node is non-negotiable since public RPC providers like Infura or Alchemy artificially throttle new block delivery speeds and delay pending transaction notifications by a fatal 100 to 300 milliseconds.

In the MEV game, profits don't go to the engineer with the most complex math model—they go to whoever is physically closest to the validator and optimizes gas down to every last assembly instruction in the bytecode.

Summarize this blog post with:

FAQ

Net daily earnings for single-operator MEV bots typically range between $50 and $500 for atomic cross-DEX arbitrage, while high-capital sandwich attack setups can net between $200 and $3,000 during periods of extreme market volatility. Professional searcher clusters running private node infrastructure in co-located data centers extract higher yields, but average standalone bots frequently operate at a loss after accounting for validator priority bids and failed transaction gas.

MEV gas expenses consist of base network execution fees and direct validator priority tips sent through builder private relays via Flashbots eth_sendBundle. While private relay submission prevents gas burn on reverted or lost arbitrage attempts, successful execution requires searchers to transfer 85% to 95% of the total trade profit directly to the block builder via coinbase.transfer as a competitive bid.

Independent sandwich attack bots are largely unprofitable on major EVM chains due to latency disadvantages, high capital requirements, and widespread private mempool adoption. Competing against institutional searchers with sub-millisecond node setups forces solo developers to overpay priority fees during builder block auctions, destroying net margins on standard Uniswap swap triplets.
Astra EXMON

Astra is the official voice of EXMON and the editorial collective dedicated to bringing you the most timely and accurate information from the crypto market. Astra represents the combined expertise of our internal analysts, product managers, and blockchain engineers.

...

Leave a comment

Your email address will not be published. Required fields are marked *