1 entry is withheld from this page, shown in place below.
A neutral analytics firm that never publishes a correction is not being careful, it is not checking. Every material error in anything we send or publish gets an entry here: what was wrong, what it should have said, where it went, and what changed in the pipeline so it does not recur.
This file is quotable. If a counterparty finds an error we have not logged, that is a worse failure than the error.
Where it went: the revert row and reliability paragraph on the aggregator-vs-aggregator pages under https://cleartracedata.com/compare/, and the matching FAQPage structured data. Not in any newsletter, scorecard edition or outreach message: the leaderboard has published the user-only figure since methodology v6 landed, and this was the surface that did not.
Wrong. The pages printed the v5 headline rate — every routing transaction, including solver and MEV-bot traffic — in a row labelled "Revert Rate (bot-filtered)", under the sentence *"After a sender-level filter that excludes solver and MEV-bot spam … so it reflects genuine user flow rather than bot retries."* The number is not that: v5 removes only addresses classified as bots, and the residual is still bot-heavy. And the figure was an all-chain rollup published directly beneath a chain-scoped slippage verdict — the same defect as this day's other correction, one table row down. It put /compare and /leaderboard 19x apart on 1inch, 3.45% against 0.18%, with the ordering against KyberSwap reversed between them; both surfaces called it the genuine-user rate.
Correct. The published rate is now the user-only rate (v6) on the page's own chain, weighted on user_reverted_txs / user_txs. Both things changed at once, so this is what the pages published against what they publish now — the old figure is the v5 headline over all chains, the new one the v6 user-only rate on Ethereum, the chain each of these pages is scoped to:
| venue | published (v5, all chains) | now (v6 user-only, Ethereum) |
|---|---|---|
| 1inch | 4.01% | 0.18% |
| KyberSwap | 2.72% | 0.81% |
| Bitget Wallet | 2.26% | 0.48% |
| ParaSwap | 1.42% | 0.39% |
| CoWSwap | 0.78% | 0.21% |
Holding the scope fixed, the filter alone accounts for most of it: venue-wide, the headline overstates the user-only rate on seventeen of the eighteen venues we measure, by 1.2x to 12x (Bitget Wallet 2.26% against 0.61%, LI.FI 2.24% against 0.37%). The eighteenth, Tokenlon, reverts at 0.00% either way and is on no compare page. The outlier is 1inch, whose all-chain figure is dominated by Base — 22,961 of its 23,502 user reverts — and which reads 0.18% on the chain its pages compare it on.
No head-to-head changes direction on any pair that renders a revert row. It corrects the magnitude of a number, not anyone's standing.
It does not follow that it favours everyone, and an earlier draft of this entry said it did. Every absolute figure falls, but a head-to-head page is about the RATIO, and several widen against a venue: 1inch against ParaSwap goes from 2.8x to 9.6x, and KyberSwap against Bitget Wallet from near parity at 1.2x to 2.8x. KyberSwap's analyst will not read that as their favour.
What the filter actually is, stated because the first version of this fix got it wrong too. is_user in dune_queries/aggregator_revert_rates_template.sql is *personal revert rate below 10%, and at least one success*. That selects senders on the outcome being reported: the published rate is bounded under 10% by construction, and a trader whose two swaps included one failure is dropped from numerator and denominator alike. It is not a bot classifier — that is is_bot, which the v5 headline already applies, so the headline is not "unfiltered" either. The pages briefly described it as "senders classified as genuine users, not solvers or MEV bots": a description of the wrong predicate, and a flattering one. landing_page/methodology.html has always had it right ("senders whose personal revert rate is under 10%"), one click from the footnote. The copy now states the predicate and its bound. Registered separately as user-revert-rate-selects-on-the-outcome-2026-09-17, because the leaderboard and the Scorecard publish this figure too and the caveat belongs with it everywhere, not only here.
Why the switch was still right. A rate presented as user experience has to be the rate users experience. leaderboard.py says exactly this about the column it publishes — onchain_user_revert_rate_pct is "the rate users actually experience" — and instructs renderers to footnote the headline "instead of presenting it as user experience". revert_roster.py builds the population any "routers stayed under X" claim is about from venues carrying a user-only rate. The compare pages were the one published surface still presenting the headline as the filtered figure, and the prose on the page already described the number it was not printing.
The asterisk went with it. REVERT_KNOWN_RESIDUAL_CELLS dropped certain cells from the aggregate and footnoted them, because the headline on those cells is a documented spam residual. Publishing the user-only rate removes that premise — those cells now carry an honest figure and are counted like any other, which is what v6 was built for. No cell in the current seed was affected (the two registered cells are Odos, which wound down and has no rows), so this changes no published number today; it changes what happens the next time such a cell appears.
What changed in the pipeline. scripts/generate_compare_pages.py requires the v6 columns and renders no revert row at all without them, rather than falling back to the headline under a user-experience sentence. Row label, body copy and both FAQ answers now say what is actually counted. Guarded by tests/test_compare_chain_scope.py, which fails if any page publishes a venue's headline rate, or its all-chain rollup, where the scoped user-only rate differs.
Where it went: all 24 public pages under https://cleartracedata.com/compare/, each built to be found by someone choosing between the two venues named in its URL, plus the FAQPage structured data on each — the part a search engine quotes. Not in any newsletter, outreach body, scorecard edition or research page. Live from generation until 2026-09-17.
Wrong. Not one figure: every head-to-head verdict. The generator picked each venue's largest dune_execution_slippage row, which for a multi-chain venue is its all-chain rollup. Venues trade on different chains, so the pages set rollups computed over different chain sets against each other and published the difference as execution quality. Four examples, all against the 2026-09-16 seed:
| page | published | on a chain both venues are benchmarked on |
|---|---|---|
curve-vs-uniswap | "Curve shows tighter execution than Uniswap … a 3.3-point edge" | Ethereum: Curve +1.4, which the page's own thresholds call a dead heat |
velodrome-vs-uniswap | Velodrome +2.2 | Optimism: +10.9, five times understated |
camelot-vs-uniswap | "effectively tied" (1.9) | Arbitrum: Camelot +3.1, a different band |
aerodrome-vs-velodrome | Aerodrome +2.1 | no shared chain exists — Base-only against Optimism-only |
It also made two ClearTrace surfaces contradict each other on the same export: /compare/kyberswap-vs-uniswap said KyberSwap executes better than Uniswap (the rollups, 8.4 vs 12.6 bps) while /leaderboard said worse (Ethereum only, 12.8 vs 10.9). Both read the table correctly. Only one of them said which chain it meant.
Correct. Each page now compares the two on one chain both are benchmarked on, and says which in the same sentence as the verdict, or refuses to rank them.
Which chain, and what that costs. ClearTrace compares a pair on Ethereum, its publication chain (the scope the live leaderboard and every Scorecard edition already use) when both venues are benchmarked there, and otherwise on the largest chain they share. That is a stated convention, not a per-pair choice, and it has a real consequence: on 11 of the 18 pairs sharing more than one chain, the direction of the verdict depends on which chain is picked. KyberSwap is the clearest case — it leads Uniswap on Arbitrum (+7.7), Base (+5.5) and Optimism (+7.6) and trails on Ethereum, the chain the rule selects. The Ethereum figure is a true statement about Ethereum; published on its own it would let a scoped finding read as a settled one. Every page that states a verdict and whose ordering reverses on another shared chain now names those chains and their directions, so the disagreement is on the page rather than left for the venue to find. Seven pages carry it.
The qualifier is load-bearing and an earlier draft omitted it: four more pages have a reversal that is NOT disclosed, because they refuse to rank at all (the five 1inch pages and dodo-vs-uniswap publish no ordering for one to differ from), and 1inch-vs-kyberswap hides the largest reversal in the set, 17.6 points on Base. A page that withholds its verdict withholds the disagreement with it; that is the design, but it is not what "every page" said.
Of the eleven pairs whose verdict is chain-dependent, seven are true sign reversals and four are tie-band changes — a lead here that is within measurement noise there. The pages distinguish the two ("the ordering is not the same" versus "the gap is not the same"); an earlier draft of the generator called both a reversed ordering, which published a false sentence about KyberSwap and ParaSwap, who share no reversal at all. Seven verdicts change direction or kind, and they are the material ones — five of the seven move against the venue that was ahead:
ahead by 2.5. And ahead of Uniswap by 4.2; on Ethereum the two are within noise (87.2 vs 89.1).
8.2 (89.1 vs 80.9).
(90.5 vs 89.1).
(89.7 vs 89.1).
ranks them at all.
3.1 — a change in Camelot's favour.
ParaSwap against CoWSwap, 3.3 → 8.8. Both cross the same "meaningful but not decisive" → "a clear edge" boundary that puts Camelot in this list, and both go against the venue; an earlier draft counted Camelot's band change as material and demoted these two into the magnitude paragraph.
favour.
Eleven more pages keep their direction and move in magnitude, several of them materially: SushiSwap's gap behind Uniswap widens 9.3 → 19.6 and behind PancakeSwap 13.7 → 21.8 (its Ethereum cell is much weaker than its rollup), while its gap behind Camelot narrows 11.2 → 5.8. Fluid's edge over Uniswap falls 10.4 → 6.5, PancakeSwap's 4.4 → 2.2, Aerodrome's 4.3 → 3.0; Bitget Wallet's over KyberSwap rises 3.4 → 8.7 and CoWSwap's over ParaSwap 3.3 → 8.8. The six pages that already refused to rank (five 1inch pages and dodo-vs-uniswap) still refuse, for the same unchanged reason.
Why it happened, and why no number was mistyped. Every per-venue figure was a correct reading of the table. The defect was in the COMPARISON: a rollup is a fair summary of one venue and is not comparable to another venue's rollup over a different chain set, because chains differ in liquidity depth, fee tiers, block times and MEV conditions. app/slippage_rows.py had said so in writing since it was created — its closing paragraph names "a common-chain rule" as the missing piece and explains that dropping the rollup without one "would compare uniswap-on-arbitrum against kyberswap-on-base". The rule was described and not built, and the pages shipped in the state the docstring warned about.
Scope, checked rather than assumed. All 24 pages were regenerated and diffed against the live ones: every page's verdict is restated, and 23 of 24 carry at least one changed figure. Five pages keep a figure byte-identical, because a venue that trades on ONE chain has the same row either way — Aerodrome 91.7 (aerodrome-vs-uniswap, aerodrome-vs-velodrome), Camelot 89.3 (camelot-vs-uniswap, camelot-vs-sushiswap), Velodrome 89.6 (uniswap-vs-velodrome, aerodrome-vs-velodrome). On aerodrome-vs-velodrome both figures are unchanged and only the ranking is withdrawn. An earlier draft of this entry said "every published figure moved on 23 of them", which is not true; in a log whose preamble says an unlogged error is worse than the error, a loose claim inside a correction is the wrong place to be loose. scripts/check_published_figures.py passes against the new set. The same comparator is not used anywhere else: /leaderboard and every Scorecard edition were already scoped to one chain (leaderboard.PUBLICATION_CHAIN), which is how the contradiction was visible at all, and no newsletter, research page or outreach message has carried a compare-page verdict.
What changed in the pipeline. slippage_rows.compare_pair() scopes a pair to PUBLICATION_CHAIN when both venues qualify there, else to the shared chain with the largest smaller side, else returns no chain and the page refuses to rank through the existing _unrankable machinery. scripts/check_published_figures.py derives its expectations from the same function — the two are one spec in two places. Every surface that states a verdict (the bottom line, the analysis, three FAQ answers, the scorecard table and its caption) asks one predicate, no_ranking(), and tests/test_compare_chain_scope.py reads the generated HTML for unscoped ranking language rather than testing branches, so a surface added later fails without being named.
Two guards were added in the same change because scoping can go wrong in both directions. Scoping must narrow what a page claims and never widen it: the one-sidedness screen now reads the scoped row AND the venue's rollup, because DODO's rollup is 49.0% positive while its Ethereum cell is 50.2%, and scoping alone would have re-published the ranking withdrawn on 2026-09-09 on 0.2 points of noise. And a rollup is never labelled with a chain name, while a single-chain venue's row keeps its own: Aerodrome reads "Base" and Velodrome "Optimism" on the page that refuses to rank them.
Registered as compare-pages-ranked-rollups-over-different-chains-2026-09-17 in app/metric_findings.json.
Where it went: newsletters/2026-08-20.html, live and publicly reachable at /newsletter/2026-08-20 (HTTP 200, verified). It states *"Coverage on Ethereum holds at 95.8% of tracked DEX volume attributed to a named frontend or router."* It was the only issue carrying a coverage figure with no correction banner — all six issues from 2026-06-28 to 2026-07-30 have one.
Why it was missed, and this is the part worth keeping. The 2026-09-08 entry below states that the last issue carrying a numeric coverage figure is 2026-07-30 and that 2026-08-20 carries none. That was wrong, and it was wrong in this file, so the 09-08 sweep trusted the enumeration instead of grepping the issues. A corrections log that is itself the source of a missed correction is the failure this file exists to prevent. The bullet is annotated in place below rather than rewritten.
What the banner says, and what it deliberately does not. It does not restate 95.8%. That figure cannot be recomputed on the data the issue was built from — dune_attribution holds only the current window, and metric_history carries tracked_contracts / tracked_volume_usd (renamed from attributed_* on 2026-09-15, a label correction with no change to any value), not a coverage percentage. Asserting a corrected value would be inventing one. Instead it states what is verifiable: coverage was redefined on 2026-09-08 (a label ending in a truncated identifier now counts as unattributed), that change alone moved Ethereum 91.9% → 82.3% on a single seed measured both ways, and Ethereum reads 90.4% on the 2026-09-14 sync under the current definition. So 95.8% is not comparable to anything published since 09-08.
Distribution: the newsletter audience was ~1 test contact, so this reached effectively nobody by email. The page being publicly served is the exposure that mattered.
What changed so it does not recur: the enumeration is no longer trusted — newsletters are now swept by grepping every issue for a coverage figure and for a correction banner, which is how this was found. 2026-09-10 was checked in the same pass and needs no banner: its figures (Ethereum 82.5%, Base 82.1%, Arbitrum 94.3%, Optimism 94.2%) are already on the post-09-08 basis.
Where it went: The methodology page /research/how-execution-quality-works, in two places, for the 24 hours after the withdrawal landed elsewhere. Also, and this is the part that matters, three delivered outreach bodies cite the single-venue KyberSwap figure and link to these pages: M1X and Valinor (2026-08-31), Renaiss (2026-08-18).
What was withdrawn, and when. PR #746 (2026-09-08) removed the per-venue "off-chain RFQ routing (%)" axis from both interactive pages, following the Edition 2 errata. PR #775 the same day added the other half of the statement: the KyberSwap measurement itself survives; what does not survive is comparing it across venues. Neither PR touched the methodology page, so the page that exists to explain how the numbers are made was the last surface still publishing the retracted comparison.
Wrong. "KyberSwap routes 86% of its $1M flow through off-chain RFQ market makers", set beside Odos and OpenOcean as one of "three characters" — a per-venue axis presented as ranked, with no correction context anywhere on the page. And a row in the Claims-and-sources table reading "KyberSwap 86% off-chain RFQ share at $1M | pinned snapshot; matches /research/execution-quality". That row is the worse of the two: it offered the retracted framing as an independently checkable claim, and pointed a reviewer at a page that had already stopped carrying it.
Correct. 86% of KyberSwap's 448 sampled $1M routes carried at least one off-chain RFQ leg — 385 of 448 on the pinned snapshot research_drafts/snapshots/quote_samples_2026-07-20.csv, reproducible today. A share of sampled routes, not of volume, and one venue's number rather than a ranking. The claims row now says so and cites the row counts.
Why the comparison is an artifact, stated more plainly than before. At $1M in that snapshot, 6 of the 11 sampled venues have zero rows with any readable route string at all — lifi, odos, okx, oneinch, openocean, sushi. Their 0.0% is the detector finding nothing to parse. The clinching case is bebop: 283 of its 284 rows carry venue strings and it still reads 0.0%, and bebop is an RFQ-native venue. A detector that reads 0.0% on an RFQ-native venue is not measuring RFQ.
What did not change. The flag stays authoritative internally, and RFQ-routed rows stay excluded from "fails to realize" — a fork cannot adjudicate an off-chain commitment. That exclusion was always correct and is separate from publishing the share. app/rfq_detection.py is untouched; its RFQ_MARKERS docstring already records the 2026-08-28 measurement that rejected KyberSwap's pmm-* desk tokens as markers, which is a different question again.
What changed in the pipeline. Nothing automated, and that is the gap worth naming: scripts/check_published_figures.py carries four figures for this page and the RFQ share was never one of them, so no checker could have caught a withdrawn number sitting on it. The lesson is the same one #746 already paid for — a withdrawal has to sweep every surface that repeats the claim, and the methodology page repeats every claim by design.
Where it went: https://cleartracedata.com/compare/dodo-vs-uniswap, a public page built to be found by someone choosing between the two. Live from its generation until 2026-09-09. Not in any newsletter, outreach body, scorecard edition or research page; the claim existed only on that page and in its FAQPage structured data, which is the part a search engine quotes.
Wrong. "DODO shows tighter execution than Uniswap on ClearTrace's on-chain slippage benchmark, 99.2 vs 87.7 out of 100 (a 11.5-point edge)", and in the FAQ, "On slippage, DODO was tighter (~0.8 bps vs ~12.3 bps)."
Correct. ClearTrace does not rank those two on slippage. Both scores stay published, with both sample sizes; the ranking does not.
Why. exec_slippage_bps is the median absolute gap between the oracle value of what the taker received and of what they gave, so it cannot tell a cost from a gain of the same size. DODO's all rollup — the row the generator picks, being its largest — is 40.9% positive, meaning its median *signed* gap is negative: most of that flow settles in the taker's favour. A low number built that way is describing what the flow is, not what a user pays. cleartrace#832 characterised the arbitrum half of that rollup as largely a StakingVault rebalancing itself, with a one-sided tail beneath it. Uniswap's 12.3 bps, by contrast, is a genuine two-sided cost measured over 1.47M trades. The page set one against the other as though they measured the same thing.
Scope, checked rather than assumed. All 31 comparison pages were re-parsed for the same defect. Five rest on a leader scoring ≥95, but only DODO's row has a negative signed median; the other four (Bitget Wallet 4.6 bps, 1inch 4.8, Fluid 1.1) are ordinary positive-median cells. One page needed the correction.
What changed in the pipeline. scripts/generate_compare_pages.py now reads pct_positive and refuses to state a ranking when either side is under 50% — _unrankable(). The threshold is a sign change, not a tuned number (pct_positive < 50 is exactly median(signed) <= 0), and it is the same test scheduler.validate_slippage_onesidedness applies at sync time. The SELECT aborts if the column is absent, which is the existing posture in that file: a missing input is a reason not to generate, never a reason to generate unguarded.
The page itself was hand-edited to the wording the generator now produces, rather than regenerated — a rebuild reads the current DB and would silently rewrite 32 pages of published numbers. tests/test_compare_unrankable_guard.py pins the two together so they cannot drift, and verifies the FAQPage JSON-LD carries the same answer as the visible page.
Registered as exec-slippage-includes-integrator-fees-and-drops-sign-2026-09-09 in app/metric_findings.json.
This correction concerns a venue ClearTrace is currently unable to name in published material. The entry is held, not deleted, and appears here in full once that restriction lifts. It is logged in full internally and is available on request.
Where it went: Nowhere external. Every surface carrying 92.4% is internal. Logged anyway, for the reason the 2026-08-19 entry gives: the figure was wrong in our own memos for eight days, one of them the exit memo whose attribution-moat driver feeds the acquirer artifact planned for ~2026-09-23, and the audit establishing that nothing was sent is itself the useful record.
Checked and clear, so that this is an audit and not an assumption:
it is the ethereum execution leaderboard, and landing_page/data/scorecard-edition-3.json contains zero coverage keys. It is pinned to seed b507ba7 and is unaffected.
2026-07-30 (90.3%, itself a correction issue). 2026-08-06 mentions coverage only as a pointer to the dashboard; 2026-08-20 and 2026-09-03 carry none. > THIS BULLET WAS WRONG — corrected 2026-09-15, see the entry at the top of > this file. 2026-08-20 carries "Coverage on Ethereum holds at 95.8%", > and it is live and was un-bannered for a week because of this line. The rest > of the bullet holds: 2026-08-06 and 2026-09-03 do carry none.
landing_page/data/leaderboard.json, research pages.** No coverage percentage anywhere. The coverage meter lives in admin_api._attribution_coverage, behind require_admin.
newsletters/2026-07-09.html and automation/social/2026-07-09.md is a different, July-vintage figure, already corrected to 88.7% by the 2026-07-30 entry below. The 92.4% in docs/METRICS.md is Arbitrum ex-outliers on the 2026-08-19 sync, correctly labelled as-of. Neither is this number and neither is restated here.
Wrong. coverage_pct_v2 = 92.4, coverage_unknown_usd_v2 = $1,193.6M, carried by automation/strategy/2026-09-07.md (scorecard row), automation/strategy/exit/2026-09-02.md (driver D2) and three blocks of automation/state.json.
Correct. On the 2026-09-08T20:29:11Z seed, recomputed independently from the 8,808 rows of dune_attribution with database.UNKNOWN_NAME_SQL — agrees with the stored key to the decimal:
| Published | Correct | |
|---|---|---|
| overall | 92.4% | 87.9% |
| untraced | $1,193.6M | $1,362.9M |
| denominator | $15,613.1M | $11,279.0M |
| arbitrum | 94.7% | 94.3% |
| base | 91.7% | 82.1% |
| ethereum | 92.6% | 91.9% |
| optimism | 94.5% | 94.0% |
Why it was wrong. Both inputs behind the ratio were frozen behind a fresh last_synced_at: dune_router_entry (the coverage denominator) unwritten since 2026-08-24, dune_attribution (attribution and untraced volume) unwritten since 2026-08-31. Both refresh only on the Monday sync — scheduler._attr_run gates on weekday() == 0 — so every green weekday sync stamped a current freshness date over tables it had not touched. Two query failures kept them frozen once they were attempted: the affiliate matview namespace (#762, the ethereum attribution query joined dune.rantum rather than dune.cleartrace) and the frozen _extended stablecoin registry arm (#771). Registered as attribution-frozen-behind-a-fresh-sync-stamp-2026-09-08 (#763), with the mechanism written up in research_drafts/2026-09-08-attribution-frozen-eight-days-behind-a-fresh-stamp.md.
Why it moved 4.5pp, and where. Base is ~4.2pp of the 4.5pp. Base untraced rose $590.3M → $871.3M while its denominator fell $7,131.8M → $4,857.4M; had Base held its 91.7%, overall would read 92.1% rather than 87.9%. The other three chains moved 0.4pp or less. This is eight days of genuine market and labelling movement arriving at once, not a restatement of the same window: the denominator is a rolling window that advanced eight days, so this is not a like-for-like recomputation of the kind the 2026-07-30 entry reports.
Stability, and one caveat on it. Two clean syncs on refreshed data agree to 0.1pp — run 34261842612 (18:38:44Z) read 87.8% / $1,375.4M, run 34273038832 (20:29:11Z, 16/16 succeeded) read 87.9% / $1,362.9M — and app/table_freshness.py reports zero stale claim tables on both. They were not, however, identically configured: #771 merged at 19:49Z, after the 18:38Z run, so only the later run carries both fixes. 87.9% is the figure to quote.
What changed in the pipeline. Both query defects are fixed and merged, and the structural defect that let them hide is closed in the same PR as this entry. The previous guard covered *attempted and failed*, not *not attempted*, so the weekly-gate skip path could advance last_synced_at over an untouched claim table with nothing reported. Now table_freshness.overdue() asks whether a claim table's data is older than its refresh cadence allows — the one question answered the same way for a fetch that failed, one that was skipped, and one nobody wired up — and the sync uses it twice: the weekly gate opens itself and refetches when a weekly table is already overdue (escalating, because refusing to stamp would block every other table's fresh data until Monday without ever refreshing the stale one), and a backstop refuses to stamp before set_last_synced() if anything is still over its limit. Rows with no write stamp count as overdue. app/table_freshness.py — not last_synced_at — remains the instrument to read before quoting a coverage figure, and it now prints that verdict directly.
Where it went: Nowhere external. This is logged because the figures were wrong in our own repo, memos and methodology docs for two days, and because the audit that established nothing was sent is itself the useful record. Internal surfaces affected: automation/strategy/2026-08-17.md (the memo make plan prints), automation/state.json's weekly lever banner, outreach/targets.md, and docs/denominator-vs-defillama.md.
Wrong. The 2026-08-17 seed reported arbitrum 94.2% coverage on $929.1M tracked and base 92.1% on $5,122.0M, and the 08-17 memo read Arbitrum's $763.0M → $929.1M (+21.8%, coverage +5.7pp) as a genuine, if composition-driven, move. All of it was three transactions:
| Chain | Address | Volume | Trades | Legs | Dune's own name |
|---|---|---|---|---|---|
| arbitrum | 0x1b6e01139bceba444ff4ac04261d263d68af6888 | $500.0M | 1 | 256 | camax.eth |
| base | 0xbd06c326b6779f2f3e048ff3093196e30192e888 | $1,102.5M | 1 | 132 | FakeHook |
| base | 0x78ca169834f585b367703514d22a3dc59375a888 | $684.0M | 1 | 132 | FakeHook |
$2,286.5M of fabricated volume — 53.8% of the Arbitrum denominator and 34.9% of Base. Dune's own resolver names the Base pair FakeHook, and we were carrying their volume as ours. The Arbitrum transaction (0xf5872c19325262b25ef74dad347c1cbc81bc92e71d2a2bdbea9e22cc97f82b2c, block 494,929,768) settles what it is: 270 logs, 264 of them from a single unnamed contract cycling against two microcaps, on 164 bytes of input and 0.00001 ETH. USDC, the only asset in it with a trustworthy price, appears in exactly two logs. Dune reads that as 256 dex legs at ~$1.95M each. All three contracts share a fingerprint: one inbound transfer of exactly 0.00001 ETH, zero outbound, no balance, unverified code, address ending 888.
Correct. Arbitrum 87.5% on $429.0M tracked (untraced 5.8% → 12.5%) and Base 87.8% on $3,335.5M (untraced 7.9% → 12.2%). Ethereum (90.6%) and Optimism (95.1%) carry no phantom row and are unchanged.
And the direction of the error is the part worth stating plainly: Arbitrum did not improve, it fell. 87.5% is where the chain already sat on the 2026-07-29 seed (87.8%), so the apparent +5.7pp coverage gain and +21.8% volume growth were *entirely* this one transaction. The real move was a ~45% decline in tracked volume. The corrected numbers are also the believable ones — 87.5 / 87.8 / 90.6 is a tight band, and the two figures that stood out from it were exactly the two chains carrying fake rows.
Nothing was sent, and here is how that was established rather than assumed. Two independent checks, because "we probably didn't send it" is not a finding:
1. The contamination is confined to one seed. Every committed sync seed from 2026-08-03 through 2026-08-14 (eight of them) was checked out of git history and re-scanned, both for the three addresses directly and with the new app/outlier_flows.py filter. All eight are clean; the rows appear for the first time in the 2026-08-17 seed. So no figure quoted before 08-17 can carry this error. 2. No send between 08-17 and 08-19 carried a coverage or denominator figure. The three sends in that window were the Arbitrum OpCo closing note (deliberately statistics-free), the Uniswap/Sov courtesy reply to the decline (no figures), and the KyberSwap clarification to Victor Tran (execution-quality bps only — no coverage, denominator or untraced number).
This also resolves an open question left in automation/digests/capture-drift-2026-08-19.md, which flagged Base's 106.3% capture for the window ending 2026-08-03 as possibly carrying the same contamination: it does not. The 08-03 seed is clean under the filter.
The public research pages are unaffected. landing_page/research/base-unattributed.html publishes 91.4% Base coverage dated to *the 7 days to August 3, 2026*, off a clean seed.
What changed in the pipeline. Three things, so this class of error is caught by machinery rather than by someone happening to look at the top of a list:
app/outlier_flows.py (#479) —a statistical tripwire. A router is flagged when volume/trade ≥ $25M and its volume is ≥ 5% of its chain. Both conditions are required, so a dominant router (huge share, small per-trade) and a genuine block trade (large per-trade, small share) are not flagged. Validated against eight historical seeds: zero false positives, and exactly the three known rows on 08-17.
database.attribution_by_chain() (#478) —the single accessor allowed to answer "what is chain X's untraced volume". It returns the raw and ex-outlier readings together, so a consumer cannot obtain the flattering figure alone, and a repo-wide scan bans hand-rolled copies. This also retired the three-different-untraced-figures problem: $53.4M is the figure (all unattributed volume); $39.9M is the *material subset* (unknown contracts ≥ $1M, a regression tripwire, never a coverage number); and $60.1M was that subset frozen before the 2026-07-30 predicate bump, now deleted from the seed.
#493) detect these rows on the way in and alert on them, and #493 corrected the stored capture-drift history.
They do not remove them, and that is deliberate — worth stating plainly here because "caught at sync time" reads as "cleaned at sync time" and it is not. validate_attribution_rows declines to quarantine on purpose: *"Dropping data on a heuristic risks deleting a real market maker that genuinely is 40% of a chain. This warns loudly and leaves the call to a human."* The findings go to send_alert; the fetched rows are saved either way.
So the phantom rows are still in the committed seed, and re-running the sync does not clear them — attribution is a 7-day rolling window and all three transactions still fall inside it. They age out on their own at the Monday 2026-08-24 sync, when the window (08-17 09:23 → 08-24 09:23 UTC) rolls past them. Decision 2026-08-19: wait for that rather than hand-edit the seed, since the publishing paths are already blocked and the public research pages carry clean Aug-3-dated figures.
The three transaction timestamps are measured, not estimated (2026-08-19, from eth_getTransactionByHash + eth_getBlockByNumber on a public Arbitrum RPC, and alchemy_getAssetTransfers on Base):
| chain | contract | transaction | block | timestamp (UTC) | |---|---|---|---:|---| | base | 0x78ca1698… | 0x951ad0f7… | 49,979,273 | 2026-08-14 22:51:33 | | base | 0xbd06c326… | 0xa9e4d132… | 49,991,538 | 2026-08-15 05:40:23 | | arbitrum | 0x1b6e0113… | 0xf5872c19… | 494,929,768 | 2026-08-15 21:45:26 |
All three carry the identical fingerprint — 164 bytes of input and exactly 0.00001 ETH, sent straight to the contract, status success — which is what confirms the Base pair are the phantom swaps themselves rather than separate funding transactions. The Arbitrum one carries 270 logs, the Base pair 146 each.
The latest is 2026-08-15 21:45:26 UTC, so the set ages out of a 7-day window after 2026-08-22 21:45 UTC. Against the sync schedule (Mon 09:23, Wed 18:42, Fri 17:08 UTC) that makes Monday 2026-08-24 the first sync that clears them, confirming the original estimate.
One margin worth knowing: at the Friday 08-21 17:08 sync, the earliest of the three (0x78ca1698…, 08-14 22:51) sits only 5h43m inside the window edge. A Friday sync running ~6h later would drop that row alone and leave the other two, half-correcting Base — which would read as a partial fix rather than the clean before/after this entry describes. If Base looks inconsistent after a Friday sync, that is why.
Standing rule: quote database.attribution_by_chain() or scripts/coverage_reconcile.py. Never the raw 94.2% / 92.1%. Note also that $422.8M / ~87.4% appears in some 2026-08-18 notes: that was hand-computed from dune_router_entry, and $429.0M / 87.5% from dune_attribution supersedes it.
> ↻ Do not quote 87.5% / 87.8% as current either — they were correct for the > 2026-08-17 seed this entry describes, and that is all this entry claims. On the > 2026-08-19 sync they are 92.4% and 93.8% ex-outliers, on the same > unchanged $429.0M and $3,335.5M. The move is real labeling: 32 contracts worth > $412,001,012 went unknown → named, untraced fell $813.2M → $401.2M, and tracked > volume is byte-identical because dune_attribution holds a Monday-dated window. > The phantom rows are still in the seed, so ex-outliers stays mandatory on both > chains. Before/after table in docs/METRICS.md; this entry stays as written.
Where it went: Discord DM to Ken Tran | Kyber (kentrance), thread 1534273447591542906, relayed to Victor Tran (KyberSwap co-founder/CTO). Sent 2026-08-06, 9:56 AM PT: "On eth over the last 44 days, your quotes lose about 2.6 bps more than Uni's at $10k, and 3.1 at $100k, when I re-execute them." Victor replied he didn't understand the sentence; this correction was folded into the 2026-08-17 reply that answered him.
Wrong. "Lose" is a claim about execution price: what a taker actually receives. The 2.6/3.1 bps figures are median_gap_bps: a venue's own quoted output vs its fork-simulated re-execution, matched against Uniswap's version of the same metric. docs/METRICS.md's own rule, written after an earlier KyberSwap draft made this exact error on 2026-07-14: "A gap is not a price... Any claim about execution *price* must come from median_realized_vs_baseline_bps. Do not substitute one for the other." We substituted one for the other in a message sent to the venue it named.
Correct. On the same file (ethereum, June 23 – August 7), what KyberSwap's takers actually received vs a matched Uniswap fill: +0.02 bps at $10k (n=1,421), −0.16 at $100k (n=635). Parity. The 2.6/3.1 gap figures are unchanged and correctly sourced; they measure something real (how far Kyber's quotes diverge from their own simulated re-execution, which no other sampled venue exhibits to the same degree) but that something is quote accuracy, not what users paid.
What changed in the pipeline: nothing mechanical: claim_gate.py has no check for whether a sentence's wording matches the metric it cites, only whether the metric itself is sourced and matched correctly. That gap in the gate is unresolved. The practical fix applied here was manual: before the next outbound message citing median_gap_bps, run the paired --realized comparison and state explicitly which one licenses which word ("gap" vs "loses"/"lose").
Where it went: landing_page/research/base-unattributed.html, published 2026-07-31 and served publicly at /research/base-unattributed. The same window date appears in the page body and in its footer. It was not quoted in any outreach draft, newsletter issue or document sent to a counterparty, so this correction is confined to the page.
Wrong. The page said "All figures are tracked DEX volume over the 7 days ending July 31, 2026." Attribution volume does not refresh daily. dune_attribution is fetched on the Monday UTC sync only (scheduler.py's _attr_run gate, a deliberate ~3x Dune cost cut in place since 2026-06-22). The volume window behind the page therefore closed on Monday July 27, not July 31.
Verified rather than inferred: the attribution volume fingerprint is byte-identical across every committed seed from the 2026-07-27T13:10Z sync through 2026-07-31, while the execution-quality, revert and MEV tables all moved in that period.
Correct. The figures are unchanged. The window date is not:
| Published | Correct | |
|---|---|---|
| Volume window | 7 days ending Jul 31 | 7 days ending Jul 27 |
| Label date | not stated | Jul 31 |
| Base unattributed | $566M (9.3%) | unchanged |
| Base tracked volume | $6.09B | unchanged |
| Three largest unattributed | $216M | unchanged |
Why the numbers survive the date being wrong. The page is a hybrid, and that is legitimate as long as it says so. Volumes are the July 27 window. Labels kept resolving through the intervening syncs, so the unattributed slice fell from $573.4M when the window closed to $566.5M by July 31, against a fixed denominator. The page reported the July 31 label state, which was the most current identification available and the honest number to publish. It simply attached the label date to the volume window. The page now states both dates separately, and carries an inline note of what it previously said.
How this evaded the existing guard. The 2026-07-30 entry below installed table_written stamping precisely so a frozen table could not silently outlive its date. It cannot catch this one: every sync runs the label apply-back, which re-saves dune_attribution in full and stamps table_written with that day's date. A real write occurs, so the table looks fresh while its volumes are up to six days old. last_synced_at reads fresh for the same reason.
Pipeline fix. scripts/moat_numbers.py (2026-08-05) dates volumes from table_written:dune_router_entry, which is fetched inside the same Monday-gated block and is never touched by the label pass, and prints the volume window and the label date as two separate lines. Any figure taken from attribution now carries the Monday. The numeric-honesty gate's path filter has also been widened to research_drafts/ and landing_page/research/: research drafts and the pages built from them were outside it, so this page merged ungated.
Standing rule this establishes: an attribution figure is quoted with its volume window, not with the date of the file it was read from. Where labels are more current than volumes, both dates are stated.
Where it went: the five newsletter issues sent to subscribers via Resend (2026-06-28, 07-02, 07-09, 07-16, 07-23), which are also still served publicly at /newsletter; the coverage stat on outreach/cleartrace-one-pager.pdf attached to the 2026-07-14 Uniswap Foundation resubmission; and one line in the Arbitrum OpCo forum DM sent 2026-07-13.
Wrong. Attribution coverage counted contracts we had explicitly failed to identify as attributed volume. app/custom_labels.json holds 2,310 entries, of which 1,627 are the literal string "Manual Review Required", the labeling pipeline recording that a contract is NOT identified. Since 2026-06-19 the sync has stamped those over the honest Unknown Proxy (0x…) name in dune_attribution, and every coverage consumer classified a row as unknown only on resolved_contract_name LIKE 'Unknown%' or a bare 0x…. "Manual Review Required (0x278d85…f8d2)" matched neither, so $404M of self-declared non-identification was reported as attributed.
Correct. Recomputed from the committed seed at each issue's own sync, so these are like-for-like with what was sent:
| Sent | Published | Correct | Per chain (published -> correct) |
|---|---|---|---|
| 2026-06-28 | 90.6% | 87.8% | n/a (issue gave overall only) |
| 2026-07-02 | 92.3% | 89.5% | eth 95.2->92.1 · base 85.3->83.0 · arb 86.5->85.4 · op 83.8->82.6 |
| 2026-07-09 | 92.4% | 88.7% | eth 94.4->90.0 · base 88.2->85.8 · arb 88.4->86.5 · op 86.7->85.2 |
| 2026-07-16 | 93.1% | 89.3% | eth 94.4->89.9 · base 90.5->88.3 · arb 88.4->85.8 · op 83.8->82.8 |
| 2026-07-23 | 94.5% | 90.6% | eth 97.4->91.2 · base 92.7->90.5 · arb 91.1->87.7 · op 88.9->88.9 |
The one-pager attached to the Uniswap resubmission printed 90.8%; the correct figure for the seed it was generated from is 86.9%. It has been regenerated (2026-07-30) and now reads 90.3% off the current seed. Today's seed reads 90.3%, not the 93.9% the admin meter showed (ethereum 90.2 not 96.1, base 90.6 not 92.8, arbitrum 87.8 not 91.3, optimism 89.6 unchanged).
The Arbitrum OpCo DM (forum DM to @opco, 2026-07-13) said "11.7% of Arbitrum DEX volume, $172.7M this window, traces to no named origin." On that same seed the correct figures are 14.3%, $211.3M. This one erred against our own argument: the untraced slice we were offering to measure was larger than we told them. If that thread reopens, the correction goes first.
What was sent stands as sent. The newsletter issues now carry a correction banner where they are served at /newsletter, with the original text left unchanged beneath it. The unsent outreach drafts have had their figures restated against the current seed, with the superseded numbers struck rather than deleted (outreach/targets.md log, 2026-07-30).
Delivered to the Uniswap Foundation 2026-08-03: the correction went to Sov (grants manager) as a reply in the resubmission thread, correction before ask — 90.8% → 86.9% stated plainly, both stale-dated one-pager stats disclosed, and the one-pager regenerated from the 08-03 seed (91.6%) attached so the corrected document traveled with the correction. The Arbitrum OpCo correction (11.7%/$172.7M → 14.3%/$211.3M) was ALSO delivered 2026-08-03, leading the operator's follow-up on thread 31068 (sent text: outreach/drafts/2026-08-03-arbitrum-opco-correction-followup.md). With both delivered, no open thread carries an unsent correction from this entry.
Two claims survive intact and are worth stating, because the error was in the level, not the trend: coverage still rose materially over the period (87.8% -> 90.6% corrected), and every chain is still above the 85% internal bar on the current seed. The direction we reported was right; the number was ~3-4 points too generous throughout.
Found while regenerating the one-pager: two of its four headline stats come from tables the sync has stopped refreshing. dune_mev_sandwiches has not moved since the 7-day window ending 2026-06-17, and dune_router_entry has not moved since the week ending 2026-07-15 (byte-identical across the 07-15, 07-22 and 07-29 seeds). The sync deliberately preserves a table when its fetch returns no rows, which is right for the app and wrong for a document: the number keeps its old value while the PDF gets a new date. So the version sent on 2026-07-14 described a June-17 sandwich count as "tracked weekly" and a mid-July volume figure as "current window". Neither number was invented, both were presented as more current than they were.
Pipeline fix for that one: the generator now fingerprints those two tables and prints the window each actually describes ("7d window to Jun 17", "wk to Jul 15") instead of implying "now". If a fingerprint stops matching, the table has refreshed and the generator says so, so the dating cannot silently outlive the freeze.
Both freezes were then diagnosed and fixed (2026-07-30), and one of them changes a claim we were about to send.
dune_mev_sandwiches: Dune query 7665966 ended with a ; and someone appended a -- Last bumped ... comment below it on 2026-06-17. Trino rejects anything after the terminator, so every execution since failed in about three seconds and the sync kept the old table. The query is fixed, now lives in the repo at queries/mev_sandwiches.sql, and returns current data.
dune_router_entry: scheduler.py called save_dataframe(df, "dune_router_entry") with the arguments swapped, so from 2026-07-14 every Monday sync PAID for the Dune execution, received the rows, raised 'str' object has no attribute 'empty' inside the save, and threw them away.
The consequence. With the MEV query working, the trailing 7 days read ethereum 12,927 attacks / $34.36M, base 9,514 / $6.54M, arbitrum 300 / $0.18M, optimism 222 / $0.04M. The frozen mid-June table said Base led every chain with 60,084 attacks at 5x Ethereum. Today Ethereum leads Base on count (1.36x) and on value (5.3x). That hook was queued to go to the Base ecosystem team and has been rewritten: Base carries 74% of Ethereum's attack count on 19% of the value, average victim $688 against $2,658, which is a retail-exposure signal rather than a league-table claim. Nothing with the old framing reached a counterparty. The one-pager's "72,000+ sandwich attacks" was dated to the Jun 17 window; the honest current figure is ~23,000, and it corrects itself on the next sync.
So nobody has to notice this by hand again: save_dataframe now stamps sync_metadata on every real write, and a new ops autotask opens when a table has not been written inside its refresh cadence. A preserved table used to be a log line; it is now a task.
Pipeline fix. The unknown predicate now lives in exactly one place, database.UNKNOWN_NAME_SQL, and it treats a placeholder as unattributed. The admin meter, the growth goal, the ops breach logic, the sync's regression alert, the labeler's work queue and the one-pager generator all import it rather than restating it, and a parity test fails the build if any of them writes its own copy. auto_labeler no longer stamps a placeholder over an Unknown … name at all.
The pattern, again. This is the same error as the 2026-07-14 entry below, counting "we don't know" as "we know", caught there in the label count and missed in the coverage ratio, in the same file, on the same day: _is_a_label() in scripts/make_one_pager.py was fixed to exclude the placeholder while the coverage query in _live_stats() directly above it was not. Fixing an instance is not fixing the class, so the definition is now shared and the next consumer inherits it.
Where it went: the Uniswap Foundation Open Grants resubmission (HubSpot form, submitted 2026-07-14) with outreach/cleartrace-one-pager.pdf attached. A correction was sent to the grants manager the same day, unprompted.
Wrong. The generator summed contract_labels with every entry in app/custom_labels.json. Of those 2,299 custom entries, 1,627 are the literal string "Manual Review Required": rows recording that a contract is *not* identified. The headline counted "we don't know" as "we know".
Correct: 1,100+ contracts labeled. Also dropped "proprietary": of 493 labels in contract_labels, 280 are reconciled from public sources (Etherscan, Sourcify, Arkham, Blockscout) and 213 come from our own pipeline.
Pipeline fix: scripts/make_one_pager.py::_is_a_label() now requires a label to actually name something. Rebuild recomputes from the seed every time.
Wrong, and the worst of the three, because it was confident and falsifiable. It is true of *our table* (canonical $0.0M, two clones $1.3M) but our entire Optimism window is $140M at 83.5% attribution coverage. A zero for the canonical router is far more likely our own indexing gap than a fact about Optimism. Anyone could disprove it on Dune in five minutes.
Correct: the claim is withdrawn. We do not make claims about a chain's composition from a window we do not fully cover.
Pipeline fix: a warning note sits in the resubmission scaffold so the claim cannot be re-derived from the same query without tripping over it.
Wrong. Five distinct contracts. Eight was a row count. The $82.4M total is correct.
Correct: five non-canonical deployments on Base moving $82.4M, against the canonical router's $2.10B.
All three are the same error: a count of rows presented as a count of things in the world. Labels that were not labels, rows that were not contracts, a data gap that was not a market fact. This is precisely the failure ClearTrace exists to catch in other people's dashboards, which is why it is logged here in full rather than quietly patched.
Ran the diagnostics on Dune rather than continuing to speculate. Three results.
1. The canonical OP UniversalRouter genuinely is tiny. True router-entry volume (dex.trades, tx_to = UniversalRouter, 7d) is $143,750 across 1,631 trades. Our table said $2,525. So we undercount it 40x, *and* the router really is a rounding error on that chain. The withdrawn claim was directionally right and evidentially broken at the same time. Withdrawing it was still correct: you do not get to be accidentally right in a grant application.
2. It is Optimism-specific, not a systematic bias. Our canonical-router figure against true entry volume: Base 92%, Ethereum 86%, Arbitrum 112%, Optimism 1.8%.
Mechanism: a trade carrying an ERC20 interface fee is credited to the *fee recipient*, and an aggregator-routed trade to the *aggregator*, so only the residue lands on the router's row as proxy_volume. That leak is 8-14% on the big chains. On Optimism it consumes 98% of a router doing $144k.
Consequence for method: router-composition analysis must use entry volume (tx_to), never dune_attribution.total_volume_usd. The two measure different things. The Uniswap census (grant Milestone 1) is being built on the former.
3. The clone figures we published are accurate. Verified against the same entry-volume measure: Base clones $83.20M (we said $82.4M), Arbitrum clone $19.91M (we said $19.6M), OP clones $1.30M (we said $1.3M). All within 1%.
The same clone addresses are live on 14 chains, moving ~$144M/week: base $83.2M, arbitrum $19.9M, polygon $17.7M, bnb $13.3M, monad $3.6M, celo $2.4M, ink $1.4M, optimism $1.3M, unichain $0.6M, plus avalanche, xlayer, worldchain, linea, tempo, superseed. We index four of those chains, so every number we have published on this cluster is a partial view of it.
Where it went: the Uniswap Foundation grant resubmission and two emails to the grants manager. Withdrawn the same day, before he replied.
The application's central claim was a census of "non-canonical UniversalRouter deployments" that nobody separates from Uniswap volume. Three of the addresses we cited are official Uniswap deployments, listed in Uniswap's own public repo (Uniswap/universal-router/deploy-addresses/*.json):
| Cited as a clone | Actually |
|---|---|
0x4c82d1fb… (ETH, "out-trades the canonical router") | mainnet:UniversalRouterV2_1_1 |
0xfdf682f5… (Base, "$53.4M largest clone") | base:UniversalRouterV2_1_1 (+ monad, unichain, zora) |
0x8b844f88… (ARB, "27% of UniversalRouter flow") | arbitrum:UniversalRouterV2_1_1 (+ op, polygon, bsc, celo, linea) |
The headline "a non-canonical UniversalRouter out-trades the canonical one on Ethereum" is Uniswap's V2.1.1 out-trading its V2. Version adoption, not a finding. The "14-chain coordinated operator fleet" is Uniswap's deterministic CREATE2 rollout: the same address on every chain is the signature of an official deployment, and we read it as evidence of an anonymous operator.
Our labeler bytecode-matches a contract to UniversalRouter, and we then treated absence from Dune's decoded {chain}.contracts set as evidence of being unofficial. Uniswap's newer routers are not in Dune's decoded set. So the tooling manufactured a clone problem out of Uniswap's own rollout.
We never checked the deployer's published addresses. They are a public JSON file in the project's own GitHub repo.
Before any contract is described as non-canonical, unofficial, or a clone, it MUST be checked against the project's own published deployment addresses. Absence from a third-party label set is not evidence of anything. See the follow-up task: ingest Uniswap/universal-router/deploy-addresses/*.json (and equivalents) as ground truth.
Four Base/OP addresses are genuinely absent from Uniswap's deploy list: 0xcaf22ce3 ($25.5M Base + $1.07M OP), 0xc5b6786d ($2.5M), 0x6df1c914 ($0.65M), 0xc479b79e ($0.30M). About $29M, not $82.4M — and they need verifying against other Uniswap repos and forks before being called anything.
Not a single wrong claim: a class of them, all found by building the checks.
dex.trades has one row per swap leg. A multi-hop route produces several priced legs for one user trade, and we summed them. Odos/ethereum: 1,792 legs across 459 txs (3.90 legs/tx), naive sum $7.1M vs per-tx notional $3.3M.
Aggregators route multi-hop far more than plain DEXes, so leg-summing systematically flattered exactly the venues we make claims about. Our headline "DEX volume tracked" read $31.5B against DefiLlama's $14.85B for the same four chains (2.12x; ethereum alone 2.80x). Honest figure: $13.05B.
Fixed in queries/router_entry_volume.sql (one notional per tx per entrypoint). The one-pager now refuses to build if it has to fall back to the leg-summed source.
We credited Odos $85.1M on ethereum: ~$40M of entrypoint flow through their published routers, plus $44.8M on 0x365084b0…, whose volume is 100% erc20_fee_volume. That contract is a fee RECIPIENT, not an entrypoint. It is unverified on-chain, absent from Odos's published contracts, and its label came from Arkham at "high" confidence.
A third-party label is evidence, not proof. And summing entrypoint volume with fee-recipient volume produces a number that answers no question; a revert rate computed over the mixed set is not well defined. We were days from sending Odos a revert-rate critique built on it.
Much of their flow settles through Fusion resolvers, so the entrypoint is a third-party resolver contract, not a 1inch router. Their router does not appear in the top 20 entrypoints by volume on ethereum, base or arbitrum. We attribute 0% of their Base and Arbitrum volume. No address list fixes this: an entrypoint-framed pipeline cannot see intent-settled flow. They stay blocked, and the block is honest.
scripts/claim_gate.py now stands between all of this and a counterparty: freshness, provenance, reconciliation (under 10% = our attribution is failing, over 150% = we are over-counting), unproven attribution, and sample floors.
Cleared today: uniswap, kyberswap, cow. Blocked: odos (we see 7.6% of their ethereum flow through their published routers), 1inch (unmeasurable, above).
Found an error we have not logged? That is a worse failure than the error itself — tell us at andrew@cleartracedata.com.