I have a confession: my mobile network almost made me quit this project.
Over the past week, I've been building a suite of DeFi monitoring tools that run entirely on my Android phone using Termux. The latest addition is a Price Alert Bot that monitors Centralized Exchange (CEX) prices against Decentralized Exchange (DEX) prices on Arbitrum, and watches for stablecoin depegs.
It runs 24/7 in the background, sends alerts to my Telegram, and caught a real 0.60% spread on AAVE just yesterday.
But getting here was a war. I dealt with Ethers v6 breaking changes, "ghost pools" showing fake 262% spreads, and a mobile ISP that actively blocks crypto APIs.
Here is the story of how I built it, and the exact fixes for the errors that almost broke me.
1. The Ethers v6 Trap (And the RPC Mix-up)
If you're migrating from Ethers v5 to v6, brace yourself. The first error I hit was:
TypeError: Cannot read properties of undefined (reading 'JsonRpcProvider')
In v6, the providers namespace was removed. Your syntax changes from new ethers.providers.JsonRpcProvider() to new ethers.JsonRpcProvider().
But the real facepalm moment? I was using an Ethereum Mainnet Alchemy RPC URL for a bot querying Arbitrum contracts. The provider was timing out trying to find contracts that didn't exist on the wrong chain.
Fix: Always create a dedicated Arbitrum app in your Alchemy dashboard, and hardcode the chain ID to prevent mobile timeouts:
const provider = new ethers.JsonRpcProvider(ARBITRUM_RPC, {
chainId: 42161,
name: 'arbitrum'
});
2. The Case of the 262% "Ghost" Spread
When I first built the price comparison logic, I used Uniswap V2 and SushiSwap. My bot immediately alerted me to a 262% arbitrage opportunity for WETH.
It was a trap. Uniswap V2 and SushiSwap are ghost towns on Arbitrum. The pools are practically empty, which completely distorts the math.
Fix: Before calculating a price, query the pool's liquidity directly. If it's below a certain threshold (e.g., $50k), skip it entirely.
const pairContract = new ethers.Contract(pairAddress, PAIR_ABI, provider);
const reserves = await pairContract.getReserves();
// Check if reserves are sufficient before proceeding
3. The Camelot V3 Conundrum
Once I switched to Uniswap V3 and Camelot V3, I hit a wall with Camelot. It's built on the Algebra protocol, meaning standard Uniswap V3 ABIs fail.
Uniswap V3 uses getPool(tokenA, tokenB, fee). Camelot uses poolByPair(tokenA, tokenB) with no fee parameter. If you don't account for this, your quotes will revert with execution reverted.
Fix: Use the correct factory ABI, and remember to compare BigInts correctly (0n vs 0).
4. The Mobile ISP Boss Fight
Here is the part that almost broke me. My bot was perfect, but it kept throwing these errors:
CEX Error: getaddrinfo ENOTFOUND api.bybit.com
JsonRpcProvider failed to detect network and cannot start up
My mobile carrier was actively blocking specific crypto exchange APIs and timing out the RPC connections.
Fix: Cloudflare WARP (1.1.1.1). I turned it on, and instantly, every ENOTFOUND error vanished. The bot was finally reaching Bybit and Uniswap without any issues.
5. The Final Architecture
The bot now does three things every 2 hours:
1. Price Targets: Alerts me if UNI or AAVE crosses a specific USD threshold.
2. CEX/DEX Spread: Compares Bybit/Coinbase prices against Uniswap V3 on Arbitrum. If the spread is >0.5%, it pings my Telegram.
3. Depeg Monitoring: Checks USDT and DAI against USDC on Arbitrum. If they deviate by >0.5%, I get an emergency alert.
Conclusion
Building DeFi tools on a mobile phone is chaotic, but it's the best way to learn about real-world infrastructure. You learn to handle rate limits, ISP blocks, and AMM math quirks that you'd never encounter in a sandboxed environment.
The entire codebase is open source. You can check it out on GitHub: github.com/superwavvy/price-alert-bot
Next up: I'm tackling the execution layer.
Have you ever run a bot on mobile? How do you handle ISP blocks? Let me know in the comments!
Also check my previous articles, thanks!
Click here

Top comments (3)
The ghost-pool example is a good reason to separate a displayed spread from an executable opportunity. I would attach a health record to every alert: source timestamp, chain ID, pool liquidity, quote size, expected price impact, RPC endpoint and whether both venues were reachable in the same observation window. A network failure should produce UNKNOWN rather than zero spread.
Thank you so much for this feedback. This is actually a valid point, I'd add a 'Health Record' to every alert now including pool liquidity, source timestamp, and RPC status. And concerning the fail-safe issue: I need to explicitly output UNKNOWN instead of 0 when the network fails, otherwise the alert is lying to the user.
Once again, thank you for this feedback.
Hi @alphai , I Implemented your suggestions. Every alert now carries a full health record: RPC latency, chain ID, timestamp, pool liquidity for both venues, quote size, and estimated price impact. Also switched to fail-safe: network failures now emit UNKNOWN instead of a false zero spread. One thing I ran into was the price impact estimation on V3 concentrated liquidity can blow up if the test trade hits the edge of the tick range. Added a sanity clamp so it reports UNKNOWN instead of a nonsense number. Thanks again for the detailed feedback. Code is on GitHub if you want to peek: github.com/superwavvy/arbitrage-bot