Finding profitable DEX arbitrage opportunities between Uniswap V3 and Aerodrome using TypeScript and WebSocket subscriptions.
The Challenge
DeFi arbitrage is often seen as a game for well-funded teams with co-located servers and millions in capital. But what if you could build a lightweight detector that finds real opportunities on a budget server?
I set out to build exactly that: a real-time arbitrage detector monitoring price differences between Uniswap V3 and Aerodrome on Base L2, running on minimal resources.
The result? It found a 0.574% spread worth $1.62 profit on a $1,000 trade within the first hour of testing.
What is DEX Arbitrage?
In simple terms: the same token pair (like WETH/USDC) can have slightly different prices on different decentralized exchanges.
Uniswap V3 uses concentrated liquidity with complex tick-based pricing
Aerodrome uses a simpler constant-product formula (like Uniswap V2)
When these prices diverge, you can buy on the cheaper DEX and sell on the more expensive one, pocketing the difference.
The catch? You need to account for:
Pool fees (0.05% on Uniswap, 0.3% on Aerodrome)
Gas costs (~$0.10 on Base)
Slippage (price movement during execution)
MEV bots that might sandwich your transaction
Most "arbitrage opportunities" disappear once you factor in these costs. That's why I built a detector that calculates net profit, not just raw spread.
Architecture
The system is surprisingly simple:
WebSocket RPC (Alchemy)
│
┌────┴────┐
│ │
Uniswap V3 Aerodrome
Pool Pool
│ │
└────┬────┘
│
Price Calculator
(sqrtPriceX96 → USD)
│
Profit Engine
(fees + gas + slippage)
│
Alert System
Key design decisions:
WebSocket subscriptions instead of HTTP polling → sub-second latency
BigInt arithmetic for Uniswap V3 price calculation → avoid JavaScript number overflow
In-memory state (just two price variables) → minimal RAM usage
Threshold-based alerts → only notify when profit > $0.50
Technical Deep Dive
- Listening to Pool Events We subscribe to the Swap event on both pools simultaneously: typescript client.watchContractEvent({ address: UNISWAP_POOL, abi: UNISWAP_V3_POOL_ABI, eventName: 'Swap', onLogs: (logs) => { for (const log of logs) { const { sqrtPriceX96 } = log.args; lastUniswapPrice = sqrtPriceX96ToPrice(sqrtPriceX96, 18, 6); checkSpread(); } } });
- Converting sqrtPriceX96 to Human-Readable Price Uniswap V3 stores price as a Q64.96 fixed-point number. Converting it requires BigInt math to avoid overflow: function sqrtPriceX96ToPrice( sqrtPriceX96: bigint, decimals0: number, decimals1: number ): number { const Q96 = 2n ** 96n; const Q192 = Q96 * Q96; const numerator = sqrtPriceX96 * sqrtPriceX96;
if (decimals0 >= decimals1) {
const rawPriceScaled = (numerator * 10n ** BigInt(decimals0 - decimals1)) / Q192;
return Number(rawPriceScaled);
} else {
const rawPriceScaled = numerator / (Q192 * 10n ** BigInt(decimals1 - decimals0));
return Number(rawPriceScaled);
}
}
- Calculating Real Profit Here's where most detectors fail. They show "0.5% spread!" but forget that fees eat 0.35% of that. Our profit engine simulates the full trade: // Buy on Uniswap (cheaper), sell on Aerodrome (expensive) const afterFeeUni = TRADE_SIZE_USD * (1 - FEE_UNI); const tokensBought = afterFeeUni / lastUniswapPrice; const grossOut = tokensBought * lastAerodromePrice; const afterFeeAero = grossOut * (1 - FEE_AERO); const afterSlippage = afterFeeAero * (1 - SLIPPAGE); const netProfit = afterSlippage - TRADE_SIZE_USD - GAS_COST_USD; Alert only triggers if netProfit >= $0.50 — no false positives. Metric Value Spread 0.574% Route Uniswap → Aerodrome Investment $1,000 Fees ~$3.60 Net Profit $1.62 ✅ The detector correctly identified that despite the 0.574% spread, after fees and gas, the trade was still profitable. Why Base? Base L2 (by Coinbase) is perfect for this experiment: Low gas: ~$0.05-0.10 per transaction (vs $5-50 on Ethereum mainnet) Fast blocks: 2-second block time Growing liquidity: Uniswap V3 and Aerodrome have deep pools EVM-compatible: Standard Solidity contracts work out of the box Limitations & Risks This is an experimental tool, not a money printer: MEV Risk: If you send transactions to the public mempool, MEV bots can sandwich you. Use private RPCs like Flashbots Protect. Execution Risk: Prices change between detection and execution. This detector does NOT auto-execute trades. Liquidity Risk: Large trades move the price. The calculator assumes small trade sizes (~$1,000). Gas Spikes: Base gas is usually cheap, but can spike during network congestion. Always test with small amounts first. This is not financial advice. Running It Yourself The project is open-source and requires minimal resources: git clone https://github.com/rdin777/base_bot.git cd base_bot npm install cp .env.example .env # Edit .env with your Alchemy WebSocket URL npx tsx bot.ts Requirements: Node.js v18+ ~50MB RAM (yes, really) WebSocket RPC URL (Alchemy free tier works) What's Next? This is just the beginning. The roadmap includes: Transaction simulation via eth_call before execution More pairs: BRETT/WETH, DEGEN/USDC, cbBTC/WETH Smart contract for atomic arbitrage with flash loans MEV protection via private transaction submission Telegram alerts for mobile notifications Lessons Learned BigInt is your friend — JavaScript numbers overflow when dealing with 18-decimal tokens WebSocket > HTTP — polling is too slow for arbitrage detection Net profit > raw spread — always account for fees, gas, and slippage Start simple — you don't need a $10k server to experiment with DeFi Resources GitHub Repo: github.com/rdin777/base_bot Base Network: base.org Uniswap V3 Docs: docs.uniswap.org Aerodrome Finance: aerodrome.finance Built with TypeScript, Viem, and curiosity. Happy arbitraging! 🚀

Top comments (0)