DEV Community

rim dinov
rim dinov

Posted on

Building an MEV-Protected WebSocket Arbitrage Scanner on Base: A Technical Deep Dive

A comprehensive guide to building a real-time arbitrage scanner with Flashbots Protect integration for sub-millisecond execution on Base L2.

Introduction

Decentralized exchanges (DEXs) on Layer 2 networks like Base offer incredible opportunities for arbitrage traders. However, the same transparency that makes DeFi powerful also exposes traders to MEV (Maximal Extractable Value) attacks — where sophisticated bots front-run or sandwich your transactions.

In this article, I'll walk you through the architecture of atomic-arbitrage-executor, an open-source WebSocket-based scanner that detects arbitrage opportunities in real-time and executes them safely using Flashbots Protect.

Key features:

  • Real-time Swap event monitoring via WebSocket
  • MEV protection through Flashbots Protect RPC
  • Atomic execution via flash loan smart contracts
  • Zero risk of fund loss (transactions revert if unprofitable)

Architecture Overview

The system consists of three main components:

┌─────────────────┐ WebSocket ┌──────────────────┐
│ Base Network │ ─────────────────▶ │ Scanner │
│ (Swap Events) │ │ (TypeScript) │
└─────────────────┘ ────────┬─────────┘
│
│ HTTP (Flashbots RPC)
▼
┌──────────────────┐
│ ArbitrageExecutor│
│ Smart Contract │
└──────────────────┘

  1. WebSocket Listener: Subscribes to Swap events from Uniswap V3 and Aerodrome pools
  2. Spread Calculator: Compares prices across DEXs and identifies profitable opportunities
  3. Flashbots Executor: Sends transactions through private RPC to avoid MEV attacks

WebSocket Real-Time Event Listening

The foundation of any arbitrage bot is speed. Instead of polling REST APIs (which introduce latency), we use WebSocket subscriptions to receive Swap events in real-time.

Configuration

import { createPublicClient, webSocket } from 'viem';
import { base } from 'viem/chains';

const publicClient = createPublicClient({
chain: base,
transport: webSocket(config.wsRpcUrl) // e.g., wss://base-mainnet.g.alchemy.com/v2/YOUR_KEY
});

Event Subscription
const SWAP_EVENT_ABI = {
name: 'Swap',
type: 'event',
inputs: [
{ name: 'sender', type: 'address', indexed: true },
{ name: 'recipient', type: 'address', indexed: true },
{ name: 'amount0', type: 'int256', indexed: false },
{ name: 'amount1', type: 'int256', indexed: false },
{ name: 'sqrtPriceX96', type: 'uint160', indexed: false },
{ name: 'liquidity', type: 'uint128', indexed: false },
{ name: 'tick', type: 'int24', indexed: false }
]
};

publicClient.watchEvent({
address: config.uniswapPool,
event: SWAP_EVENT_ABI,
onLogs: async (logs) => {
for (const log of logs) {
await handleSwap(log.args);
}
}
});

Why WebSocket?
Sub-millisecond latency vs 100-500ms for REST polling
No rate limiting issues
Push-based architecture (events arrive instantly)
Flashbots Protect Integration
The biggest mistake arbitrage bots make is broadcasting transactions to the public mempool. This exposes them to sandwich attacks, where MEV bots front-run your trade and extract profit.
The Solution: Flashbots Protect RPC
Instead of using the public Base RPC, we send transactions through Flashbots Protect:

import { createWalletClient, http } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';

const account = privateKeyToAccount(config.privateKey);

const walletClient = createWalletClient({
chain: base,
transport: http('https://rpc.flashbots.net'), // Private RPC!
account
});

How Flashbots Protect Works
Your transaction is sent to Flashbots builders (not the public mempool)
Builders include it in a block without exposing it to other bots
If the transaction is unprofitable, it reverts (you only lose gas)
If profitable, it executes atomically
Benefits:
✅ No sandwich attacks
✅ No front-running
✅ Transaction privacy
✅ Failed transactions don't lose funds (only gas)
Atomic Execution via Smart Contracts
The ArbitrageExecutor smart contract uses flash loans to execute arbitrage without requiring upfront capital.
Key Contract Logic

function executeArbitrage(
address tokenBorrow,
uint256 amount,
address tokenIn,
address tokenOut,
uint24 fee1,
uint24 fee2
) external onlyOwner {
// Request flash loan
IFlashLoanPool(pool).flashLoan(
address(this),
tokenBorrow,
amount,
""
);
}

function onFlashLoan(
address initiator,
address token,
uint256 amount,
uint256 fee,
bytes calldata data
) external override returns (bytes32) {
// 1. Swap on Uniswap (buy cheaper)
ISwapRouter(uniswapRouter).exactInputSingle(
ISwapRouter.ExactInputSingleParams({
tokenIn: token,
tokenOut: tokenOut,
fee: fee1,
recipient: address(this),
deadline: block.timestamp,
amountIn: amount,
amountOutMinimum: 0,
sqrtPriceLimitX96: 0
})
);

// 2. Swap on Aerodrome (sell higher)
ISwapRouter(aerodromeRouter).exactInputSingle(
    ISwapRouter.ExactInputSingleParams({
        tokenIn: tokenOut,
        tokenOut: token,
        fee: fee2,
        recipient: address(this),
        deadline: block.timestamp,
        amountIn: receivedAmount,
        amountOutMinimum: amount + fee,
        sqrtPriceLimitX96: 0
    })
);

// 3. Verify profit
uint256 profit = IERC20(token).balanceOf(address(this)) - (amount + fee);
require(profit >= minProfit, "Insufficient profit");

// 4. Repay flash loan
IERC20(token).safeTransfer(flashLoanPool, amount + fee);

return keccak256("ERC3156FlashBorrower.onFlashLoan");
Enter fullscreen mode Exit fullscreen mode

}

Atomic Guarantee: If any step fails (insufficient profit, slippage, etc.), the entire transaction reverts. You lose only gas, never the principal.
Spread Calculation Logic
The scanner continuously monitors price differences between Uniswap V3 and Aerodrome:

async function calculateSpread(
uniswapPrice: bigint,
aerodromePrice: bigint,
amount: bigint,
fee1: number,
fee2: number
): Promise<{ profitable: boolean; profit: bigint }> {
// Calculate fees
const uniswapFee = (amount * BigInt(fee1)) / 1_000_000n;
const aerodromeFee = (amount * BigInt(fee2)) / 1_000_000n;

// Calculate profit
const profit = aerodromePrice - uniswapPrice - uniswapFee - aerodromeFee;

return {
profitable: profit > config.minProfit,
profit
};
}

Threshold: Only execute if profit > MIN_PROFIT (configurable, e.g., 0.001 ETH).
Community Testing Guide
Due to network restrictions in some environments, I'm inviting the community to help test this scanner!
Prerequisites
Node.js 18+
Base Mainnet RPC URL (Alchemy/QuickNode)
Test wallet with small ETH balance (0.01 ETH)
Deployed ArbitrageExecutor contract
Setup

git clone https://github.com/rdin777/atomic-arbitrage-executor.git
cd atomic-arbitrage-executor
npm install

cp .env.example .env

Edit .env with your configuration

Configuration
env

WS_RPC_URL=wss://base-mainnet.g.alchemy.com/v2/YOUR_KEY
PRIVATE_RPC_URL=https://rpc.flashbots.net
PRIVATE_KEY=0xYourTestWalletKey
CONTRACT_ADDRESS=0xYourContractAddress
UNISWAP_POOL=0x...
AERODROME_POOL=0x...
TOKEN_A=0x... # e.g., WETH
TOKEN_B=0x... # e.g., USDC
MIN_PROFIT=1000000000000000 # 0.001 ETH in wei

Run

npm run dev

The bot will connect via WebSocket and start monitoring Swap events. When a profitable spread is detected, it will attempt execution via Flashbots Protect.
Security Considerations

  1. Never Use Your Main Wallet Always use a dedicated test wallet with minimal funds (0.01 ETH). Never expose your primary wallet's private key.
  2. Protect Your .env File The .gitignore is configured to prevent committing .env, but always double-check before pushing.
  3. Smart Contract Audits Before deploying to mainnet with significant funds, get your contract audited by a professional security firm.
  4. Flashbots RPC Reliability Flashbots Protect is reliable, but always have a fallback strategy. Monitor transaction success rates. Future Improvements Multi-pool monitoring (more than 2 DEXs) Dynamic fee calculation based on gas prices Machine learning for spread prediction Integration with Base Cobalt Validity Transactions Telegram/Discord notifications for opportunities Links & Resources

GitHub Repository: github.com/rdin777/atomic-arbitrage-executor

Smart Contract: github.com/rdin777/base-arbitrage-project

Flashbots Docs: docs.flashbots.net
Base Docs: docs.base.org
Contributing
Found a bug? Have a better pool pair? Want to improve the spread calculation?
Open an Issue or submit a PR! All constructive feedback is welcome.
Happy building! If you found this useful, follow me for more Web3 development content.

web3 #typescript #blockchain #defi

Top comments (0)