Why DEX Arbitrage Scanners Find Fake Edges (and How to Gate Them)
We built a cyclic-arbitrage detector on live DEX pool state. It found +956 bps of apparent free money — and every edge evaporated under atomic simulation. What we learned about quote fidelity, phantom liquidity, and why simulation gating matters.
In August we pointed a new instrument at our pool-state graph: a scanner that asks, every few minutes on every chain we serve, "starting from token X, can a single atomic transaction return to X with more than it started?" A closed loop — X → Y → Z → X — executed inside one transaction, funded entirely by a zero-fee flash loan, is the cleanest form of DEX arbitrage. No inventory, no leg risk, no timing exposure. If the quote is real, it's free money.
The scanner found them immediately. A WETH cycle on Base quoting +956 basis points at a 17knotional.AUSDCloopshowing∗∗+28bps,persistentacrosssweeps∗∗,worthroughly17k notional. A USDC loop showing **+28 bps, persistent across sweeps**, worth roughly 4,500 at the lender's maximum. Dozens of smaller flags per day, 6,000 probes taped overnight.
Every one of them was fake. And proving that, deterministically, before spending a wei of gas, is the system we actually built. This post is about why the edges were fake, how we proved it, and what it means for anyone trusting quote APIs.
What a "cyclic quote" actually is
Our routing graph holds live state for every pool the feed delivers — reserves, ticks, fees, protocol quirks — refreshed continuously over a websocket stream. A quote for X→X is a multi-hop simulation against that state: pick the path, apply each pool's get_amount_out, subtract gas.
The crucial word is simulation. The answer is only as good as the state's fidelity to the chain at this block — and block state is a moving target. A pool that traded ten seconds ago may have stale reserves in the stream. A component may have arrived with corrupted metadata. A price-impact curve may be interpolated from a liquidity range that no longer exists.
So a quoted edge has two possible explanations:
- A real dislocation — fleeting, usually arbed out in the same block by someone faster.
- A state artifact — the simulation believes something the chain doesn't.
You cannot distinguish them from the quote itself. That distinction is the entire engineering problem.
The pipeline we built around the question
The loop runs in four stages, each one cheap and independent:
Detect. Probe a curated token universe on every chain with X→X quotes. Record gross edge (pre-fee), gas, and an executable edge — gross minus gas, at zero partner fee, since we would submit through the router directly. Everything goes to a tape: Postgres plus SQLite, triple-replicated. Nothing is trusted; everything is measured.
Size. Flagged cycles get a size ladder — the same route re-quoted at escalating notional, up to a cap. Flash capital means the interesting question isn't "is there edge" but "how much USD of edge before slippage eats it." A +10bps quote at 1kislunchmoney;at1k is lunch money; at 64k it's a mortgage payment.
Finance. A candidate above our USD floor gets handed to a dedicated execution endpoint on the solver. It resolves the token (canonical registry first — we caught a fake WETH clone shadowing the real symbol this way), checks the lender's vault balance, and — this took one production iteration — clamps the borrow to available liquidity instead of rejecting. You re-quote at the feasible size; if the edge survives, you proceed.
Refuse. Before anything can broadcast, the transaction is simulated end-to-end at the RPC level: flash borrow → swap sequence → repay → skim profit, in one atomic call. eth_call/estimate_gas against real chain state. If any leg would revert, the attempt dies here, and the revert data goes to the tape.
Only after all of that — and an explicit dryRun:false plus a compile-time-out opt-in flag in the service environment — would a transaction ever leave the box.
What the tape actually recorded
After the first full day: 62 execution attempts, 59 refused at pre-flight, 3 refused by full-chain simulation, 0 broadcast, 0 gas spent.
The rejections broke down into honest categories:
- Stale edges. The scanner flags +28bps; by the time the endpoint re-quotes, it's +1bps — below the floor. The market moved; the tape had already caught up. These die quietly.
- Lender-capacity rejections. The ladder's best size was 64k;thevaultheld64k; the vault held 3.5k. (Now clamped — the endpoint re-quotes at the feasible amount.)
- Simulation reverts. The interesting ones. A USDC loop showed +28bps in the pool-state simulation, persistently, sweep after sweep. We forked the chain locally and replayed the exact calldata: the atomic transaction returns −9.5bps. The router's inner call reverts with
actual < required— the pool state's idea of the reserves diverges from the chain's by about 38bps, which is precisely the fake edge.
That last class is the finding. The opportunities our instrument reports are, so far, measurements of divergence in our own state feed — not prices the chain will honor. The scanner didn't find arbitrage; it found the gaps between simulation and reality, which is arguably a more valuable instrument. A pool that consistently reports phantom edge is a pool whose state we shouldn't trust for real routing either — so flagged suspects feed a blacklist that the pool stream itself enforces, closing a loop that improves quote quality for everyone, not just the arb bot.
What we took away
Quotes are hypotheses, not facts. Any system that treats a simulated quote as executable is one bad block away from funding someone else's sandwich. The only gate that matters is the last one: simulate the exact calldata atomically against chain state, or don't send.
Phantom liquidity is a spectrum. Some of it is obvious (a pool reporting a spot price 1000× off market). Some is subtle (correct spot, wrong depth curve — the +956bps looked sane on every per-hop check until the atomic run). Spot checks are necessary and nowhere near sufficient.
The lender's balance is part of the quote. "Profitable at 64k"ismeaninglessif64k" is meaningless if 3.5k is borrowable. Sizing isn't a post-processing step; it's where theoretical edge meets the physical world.
Persistence beats cleverness. Most of what we learned came from a boring Postgres tape: which tokens flag repeatedly, whether the flag persists across sweeps, what the ladder shape looks like. A transient spike and a corrupt pool look identical in one snapshot and completely different across a thousand.
Fail-closed is a product feature. Every layer assumes the previous one lied. The result is a system whose most common output is "no" — and whose "no" is backed by a transaction simulation, not a guess.
Where this goes
The divergence measurement itself is the asset now. When a pool's simulated output deviates from chain-truth by a stable margin, that deviation is signal — it localizes staleness in the feed, flags corrupt components for eviction, and prices the honesty of every route we quote for real users. We're extending the tape to record per-pool divergence over time; pools that reliably lie get excluded at the stream level, automatically.
The free money was fake. The instrument that proved it is real — and it's already paying for itself in the one currency that compounds: knowing exactly when not to trade.
The instrument described above feeds the routing engine behind Orkid swaps — the same quote-fidelity discipline that rejects phantom edges is what prices your route. Compare its output against 1inch.
Put it to work
The research above powers the product — try it or compare it: