← ClearTrace

4 entries are withheld from this page, shown in place below.

ClearTrace corrections log

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.


2026-10-06 — entry withheld

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.


2026-10-02 — the public API served the headline slippage figure under the name avg_slippage_bps; it is not an average

Where it went: every surface that named the field. The /api/v1/protocols response and OpenAPI schema, from the endpoint's first publication to 2026-10-02. /llms.txt, which exists so AI agents can learn the API surface, listing the endpoint "with avg_slippage_bps" and nothing else. The methodology page (research/how-execution-quality-works.html), which labelled it "(public API)" and titled its definitional section "What avg_slippage_bps is actually measuring". The homepage sample response, the open-dataset README and the CSV header in both the published dataset and landing_page/data/. The Dune column itself. Still served under the old name, deliberately, until 2027-03-31.

Wrong. The field is APPROX_PERCENTILE(ABS(signed_bps), 0.5) — the median of the absolute per-fill gap against the minute reference price. dune_queries/execution_slippage.sql computes it internally as median_slippage_bps and then aliases it to avg_slippage_bps at the final SELECT, which the query header has described for months as kept "for schema compatibility with downstream consumers". So this was a known, deliberate alias whose cost had not been counted. It is not a mean of anything, and because the absolute value is taken per fill it is not a signed execution cost either.

No published figure was ever wrong. The schema description has said "Median ABSOLUTE gap" since 2026-09-09 and every value was computed exactly as documented. The error was the name, and the name is what gets read without opening a schema.

Nothing incorrect was sent. This is the part worth stating plainly, because the near-miss is the evidence for the entry. The first drafts of the 2026-09-25 Kaiko and BestX first touches were built on a spread across this field presented as an execution cost. They were caught before sending by the send-block memo (afm/send-block/drafts/2026-09-25-amberdata-bestx.md), which rejected the figure in the repo's own words and rebuilt both bodies on signed_median_bps, the signed central tendency measured since execution_slippage.sql v3.0. The bodies sent through Kaiko's and BestX's web forms on 2026-09-25 quote "a median taker cost between 2.5 and 8.9 basis points against a minute-level off-chain reference price", which is pancakeswap +2.54, aerodrome +6.33 and uniswap +8.89 — correct, correctly named, and correctly described. The Amberdata body was staged and never submitted. No counterparty received a figure derived from this field.

How much it would have mattered. On those same three venues the absolute median overstates the signed median by 3.2x, 1.5x and 1.5x (8.10 vs 2.54, 9.30 vs 6.33, 13.64 vs 8.89), so a spread quoted off the absolute column would have overstated taker cost to three prospective data buyers by up to a factor of three. Separately, an independent reviewer reading the same endpoint on 2026-09-25 concluded the field was a mean and that the board scored on a median — the exact inverse of the truth. Two readers, two opposite wrong answers, from the name alone, while the schema sat one click away carrying the warning "THIS ORDER IS NOT A RANKING OF EXECUTION COST and must not be presented as one". A field name that fights its own documentation wins.

What it should have said, and now does: /api/v1/protocols serves median_abs_gap_bps, the same value under a name that describes it, as of API version 1.4.0. avg_slippage_bps remains as a deprecated alias, marked deprecated: true in the schema, with a description that leads with "DEPRECATED, use median_abs_gap_bps instead" and "THIS IS NOT AN AVERAGE", and a removal date of 2027-03-31. Both names are produced from one rounding of one value, so they cannot drift. /llms.txt, the methodology page, the homepage sample and the dataset README now name the new field; the Dune column and the CSV header keep the old name, because renaming the column needs a Dune redeploy plus a sync and the CSV header is a parser contract.

Why an alias and not a rename. The consumer count was measured rather than read off the growth goal, which is itself a registered overstatement. On the api_usage window ending 2026-09-08, /api/v1/protocols showed 50 distinct hashed IPs, 30 of them classified bots. Of the remaining 20, the one curl caller with 11 active days is our own deploy box, six are the known rotating-egress single actor on python-httpx, and ten are one-day browser hits on the Swagger page. The honest distinct count is 3, with 9 of 10 qualifying IPs carrying no ASN at all. That is no evidence of a durable external integration, which is not the same claim as there being none — so on a public endpoint with no channel to reach a consumer we cannot see, a hard rename would have broken silently.

Pipeline fix. tests/test_protocols_field_name_deprecation.py pins that the two names cannot disagree on any rounding, that the deprecated flag and the removal date cannot leave the schema, and that the correction must stay in the first 200 characters of the description rather than sliding down the paragraph. That last test fired during this work and caught a change that had pushed "THIS IS NOT AN AVERAGE" past the lead.

Registered as exec-slippage-field-named-avg-is-a-median-of-absolutes-2026-09-25 in app/metric_findings.json, effect description, and it stays open: the misleading name is still on the wire by design. It closes when avg_slippage_bps is removed on or after 2027-03-31.


2026-10-01 — the user-only revert rate was described as a genuine-user rate with bots and searchers classified out; the filter classifies nobody

Where it went: four newsletter issues, two Scorecard editions, every live page that explains the column, and one message to a venue. The 2026-07-16 issue ("the revert rate over genuine users"), the 2026-08-20 issue (headline "A router's headline revert rate is mostly bots"; "count only the senders that behave like genuine users"; table caption "user-only strips senders classified as bots or searchers"; footer "senders classified as genuine users, with bot and searcher senders removed"), and the 2026-09-03 and 2026-09-17 issues ("these strip senders classified as bots and searchers", repeated in each footer). Scorecard Editions 2 and 3, in their findings ("senders our v6 classifier counts as genuine users", "Genuine-user revert rates span …") and, for Edition 2, the Errata section ("the rate among senders who behave like real users"); both frozen snapshots' note fields ("senders classified as genuine users", "the rate users actually experience"). The leaderboard column tooltip ("senders our sender heuristics class as genuine users … what a person using the venue experiences") and residual footnote, the methodology page ("the measured experience of genuine users"), the developers page, the research page, the /api/v1/revert-rates field descriptions and docstring ("senders classified as genuine users … the measured user experience"), the Streamlit revert page, both open-dataset READMEs and the Kaggle dataset-metadata.json, and the scorecard generator that would have printed the same words in Edition 4. And the 2026-09-23 Telegram DM to Octave Mesnard at LI.FI, which said the gap between LI.FI's 2.16% headline and 0.33% user-only rate "is bot and searcher traffic, which the page now separates out". From methodology v6 (2026-07-07) to 2026-10-01; the leaderboard's own note was corrected in #977, and the rest was not swept.

Wrong. is_user in dune_queries/aggregator_revert_rates_template.sql is *personal revert rate below 10%, and at least one success*. It performs no classification of who a sender is. It is a threshold on each sender's own outcomes, applied after the separate v5 is_bot filter that the headline already carries. Two things follow mechanically: the published figure is bounded under 10% by construction, and a sender whose few transactions included one failure is removed from numerator and denominator alike. The senders it drops carry a disproportionate share of the reverts (on the 2026-09-16 seed about 5% of transactions and roughly half of all reverts); some of them are searchers, but the filter does not identify them, so the gap between the two columns is not a measurement of bot traffic and "mostly bots" is not a thing the data says. Registered as user-revert-rate-selects-on-the-outcome-2026-09-17 on 2026-09-17, which is also when the compare pages were corrected; the registry entry named the leaderboard and the Scorecard as carrying the same gloss, and nobody swept the archive or the explanatory pages until a follow-up to that DM was being drafted.

Correct. The user-only rate is *the revert rate over senders whose own transactions to these routers succeeded at least 90% of the time over the window*: the failure rate among consistently-succeeding senders, not the rate people experience and not the share of all attempts that fail. That is the wording the leaderboard note, the 2026-09-24 and 2026-10-01 issues, and the compare pages already use.

How much. No figure moves. The SQL is unchanged, every published number was computed exactly as described above, and no ranking changes. The error is in the caption, and the caption is the claim a venue would repeat.

What changed. The four archive issues carry a dated correction banner in the style of the 2026-07-30 and 2026-09-15 banners, placed at the foot of each issue; their text is unchanged. Editions 2 and 3 carry a dated post-publication note; snapshots and hashes unchanged, and the hashed note fields are left as they are because they are inside the hash. Every live surface named above now states the predicate and its bound. scripts/build_scorecard_edition.py prints the predicate wherever it prints the rate, and Edition 4's ERRATA carries the correction forward. tests/test_leaderboard.py now fails if the leaderboard calls the column a genuine-user rate; tests/test_user_revert_rate_wording.py sweeps the live surfaces for the gloss; scripts/newsletter_gate.py CHECK E refuses it in any issue dated from 2026-10-01. The held X post drafted 2026-09-23 for LI.FI (automation/social/2026-09-24-lifi-revert-rank.md) repeats the gloss and is marked do-not-post pending a rewrite. The LI.FI DM is corrected in the follow-up being drafted; the 2026-06-28 issue's own 2026-07-05 correction note and Edition 1's 2026-07-05 note each say "genuine users" once and are left as dated records.


2026-09-25 — every description of the slippage baseline called it a "1-minute VWAP"; it is an interpolated 5-minute exchange-aggregate price

Where it went: everywhere the on-chain slippage metric is explained. The methodology page ("VWAP Slippage: … 1-minute volume-weighted average"), the homepage (a feature card titled "VWAP Slippage Tracking", a methodology step titled "VWAP Oracle Comparison", and the FAQ answer, both visible and as JSON-LD served to search engines), the /api/v1/protocols docstring, /llms.txt, the MCP tool description, all 24 compare pages (14 of them three times: body, visible FAQ answer, FAQ JSON-LD), Scorecard editions 1, 2 and 3 in their Methodology & scope paragraph, the one-pager PDF, writing.html (three times, two of them spelled "volume-weighted"), research.html, and the README. From 2026-06-08, the commit that added the landing page, to 2026-09-25, still live as this is written.

Wrong. dune_queries/execution_slippage.sql computes no VWAP. It values both legs of each fill against Dune's prices.usd at DATE_TRUNC('minute', block_time) and takes the gap. Nothing in the query, or in any deploy variant of it, volume-weights anything. Dune documents prices.usd (Prices overview, read 2026-09-25) as a legacy view of prices_external.minute, its Coinpaprika table: external market data aggregated from exchanges for roughly 2,000 major tokens, at 5-minute intervals, interpolated to minute level. The DEX-derived, hourly, volume-weighted long-tail prices Dune also publishes live in other tables and are not what this query reads. The join is at minute grain, but the feed behind it moves every 5 minutes and is interpolated between, so "1-minute" overstates its resolution; nothing in it is volume-weighted; and it is not an on-chain price at all.

How much. No figure moves. Every number was computed exactly as the query says, and the query did not change. The error is in the caption, and the caption is the thing a counterparty quotes. A transaction-cost-analysis reader who hears "VWAP" assumes a trade-weighted price built from the market's own fills, which carries information about liquidity at the moment of execution. An interpolated 5-minute exchange-aggregate carries none of that. By construction it cannot carry a move inside a 5-minute interval, and it embeds whatever basis sits between the exchanges it aggregates and the pool the fill hit. Those are also the mechanism behind the noise-floor limit already disclosed on the metric (a venue whose true cost is below the oracle's own dispersion publishes that dispersion). Naming the oracle correctly is what makes that disclosure legible.

What it should have said, and now does: "a minute-level off-chain reference price (Dune prices.usd, which Dune documents as a view of its Coinpaprika feed of exchange-aggregate market data at 5-minute intervals, interpolated to the minute, and not a VWAP)". We also stopped writing "price oracle" on its own, because to a crypto or TCA reader that means an on-chain oracle. Fills in tokens that feed does not price are excluded, not estimated.

Editions 1 to 3 carry a dated post-publication note beside the sentence rather than a silent edit, per the scorecard integrity model. The compare-page and scorecard generators are corrected so the old wording cannot regenerate. The SQL header now states the baseline precisely.

What we did not verify. What the prices.usd view actually returns on the query engine. The wording follows Dune's published documentation and says so. If a live probe shows DEX-derived rows in that view, this entry gets a follow-up.

Registered as exec-slippage-baseline-is-not-a-vwap-2026-09-25 in app/metric_findings.json, under a new effect, description, because the registry's existing vocabulary all describes a wrong figure and this figure is right.


2026-09-24 — every published quote gap was computed over all four chains, on boards labelled with one

Where it went: the public leaderboard (/leaderboard, its landing_page/data/leaderboard.json, and the dataset/leaderboard.json mirror), in the Quote gap column, and Scorecard editions 2 and 3, which carry quote_median_gap_bps as a frozen field. From 2026-08-04, the day L2 quote sampling began, until 2026-09-24 21:19 UTC, when the corrected board was republished. Before 08-04 the corpus was Ethereum only, so pooling it changed nothing and no earlier surface is affected. The two frozen editions keep their published figures and carry dated notes; see below.

Wrong. build_aggregator_leaderboard scoped the on-chain half of the board to one chain, passing chain into join_aggregator_metrics, and scoped the quote half to nothing at all. build_leaderboard never selected the chain column, and aggregate_quote_samples keyed its cells on (source, pair_label, size_cohort). So every quote column on a board that named a chain was computed from samples taken on all four.

Nothing about the output looked wrong, and that is the point. The four core pairs carry the same label on every chain: WETH/USDC, WETH/USDT, USDC/USDT and WBTC/USDC appear on three or four chains each. A pooled cell and a scoped cell are identical in shape, in sample count and in confidence band. The only way to see it was to read the SELECT.

How much. On the 2026-09-24 corpus the Ethereum board's quote columns were built from 144,956 samples, of which 80,968 were Ethereum. About 44% of the corpus behind an Ethereum-labelled number was Arbitrum, Base or Optimism. Pooling moved every affected venue in the same direction, because L2 fills quote tighter, so the published figure understated each venue's Ethereum gap:

venuepublishedEthereum only
LI.FI0.24 bps1.09 bps
OKX2.53 bps3.53 bps
KyberSwap0.21 bps0.63 bps
OpenOcean10.23 bps11.06 bps (stale either way)

Bebop, 1inch, ParaSwap, Sushi and Uniswap do not move. No venue's confidence band changed and every rated cell stayed rated, so nothing was published as rated that should not have been.

Edition 3 is restated; Edition 2 is flagged and not restated. Both carry a dated post-publication note, per this scorecard's integrity model: the snapshot, the findings text and the SHA-256 hash are unchanged and a correction ships as a note, never as a silent edit.

Edition 3 (published 2026-09-08, 39.3% of its 110,676 samples not Ethereum). Rebuilt from its own corpus at its own freeze and scoped to Ethereum, the affected cells read OpenOcean 11.06 (published 10.23), OKX 3.67 (published 2.50), LI.FI 1.33 (published 0.71) and KyberSwap 0.78 (published 0.26). 1inch, Bebop, Uniswap, Sushi, ParaSwap and Odos are unchanged. No verdict in that edition changes. OpenOcean is the widest gap on either basis, and no venue crosses the 2.0 bps threshold that adds or removes a quote-accuracy finding: OKX was already outside it at 2.50 and is further outside at 3.67, and LI.FI stays inside at 1.33. All four figures also appear in the edition's prose, and the note says the prose values are superseded.

The restatement rests on a reproduction control, not on trust. Before applying the chain filter, the rebuild reproduces Edition 3's PUBLISHED pooled figures exactly, venue for venue. The only cells it disagrees with are OpenOcean and Odos, which it calls stale where the edition printed rated — and that is the separately logged 2026-09-22 defect, so the rebuild is right and the edition was wrong for a reason already on this page.

Edition 2 (published 2026-08-07, 9.7% not Ethereum) is NOT restated, because the same control fails. L2 sampling had begun three days earlier, so its contamination is real but small. Rebuilt against today's code it returns KyberSwap 1.14 where 1.18 was published, OKX 2.96 against 3.04, OpenOcean 10.88 against 10.96 and LI.FI 0.83 against 0.82. Those differences are not the chain defect; they are the corpus and the cell code having moved in the seven weeks since. A restated figure that cannot first reproduce the published one is a different measurement wearing a correction's clothes. The direction is known and stated on the edition; the magnitude is not published.

This also corrects an earlier entry on this page. The 2026-09-17 compare-page entry below says "/leaderboard and every Scorecard edition were already scoped to one chain (leaderboard.PUBLICATION_CHAIN)". That was true of the revert and execution-cost columns and false of the quote column, on every one of those surfaces. The sentence was written to explain why the compare pages contradicted the leaderboard, and that reasoning still holds, because the contradiction it described was about execution cost. But the claim as written is wider than the truth and is corrected here.

Corrected and LIVE 2026-09-24 21:19 UTC. The board serves a precomputed JSON, so merging the fix changed nothing a reader sees; the correction landed only when the quote-sampler rebuild republished that file (run 36059223434). Verified on the live board immediately afterwards: LI.FI 1.09 bps, OKX 3.53, KyberSwap 0.63, OpenOcean 11.06 — every venue moved the direction and the distance this entry predicted. The registry entry quote-columns-pooled-every-chain-2026-09-24 was held OPEN through the merge for exactly that reason and was closed on the republish, not before.

chain is now part of the cell key in aggregate_quote_samples, so no caller can pool silently. build_leaderboard takes a chain argument that scopes the corpus and stamps it on the payload, so a scoped board is distinguishable from a pooled one instead of being inferred from the caller. build_aggregator_leaderboard passes its own chain to both halves, which is the actual defect: the two halves took their scope from different places. A corpus with no chain column now raises on a scoped call rather than widening the scope it cannot honour. Guarded by tests/test_quote_leaderboard.py.

Found while scoping a Base leaderboard, not by a check. A Base board on the old path would have carried a quote column roughly 85% other chains, which is what made the pooling visible. matched_gap_vs_baseline has filtered on chain since it was written, so the module read as sound and only the published path was not. That is the same shape as the 2026-09-17 compare-page entry below: the metric was right and a published surface compared it wrongly.


2026-09-23 — /api/v1/protocols served a signed median that was interpolated, and for one venue the sign was backwards

Where it went: /api/v1/protocols, every chain, from the 2026-09-22 deploy (#1005) until the v3.0 fix on 2026-09-23. Three new fields were affected: signed_median_bps, slippage_direction and dispersion_ratio. Nothing else published carried them, and avg_slippage_bps and slippage_score are untouched throughout.

Wrong. Those fields were added to show, per venue, whether the published figure is an execution cost or mostly the price oracle's dispersion. The underlying Dune query emitted the signed quartiles but no signed median, so the endpoint estimated it as (p25 + p75) / 2. That interpolation assumes the signed distribution is symmetric between its quartiles, and these are skewed. Measured against the true percentile over 103 live cells on 2026-09-23, it was off by a median of 2.44 bps (p90 11.9, max 30.6) on a metric whose published values are usually under 13.

And on one venue it inverted the sign. ethereum/1inch has quartiles -9.20 / +24.40, so the interpolation returned +7.60 and the endpoint reported slippage_direction: "cost". Its true signed median is -0.68: on balance its takers receive MORE than oracle value, so the honest label is "gain". ekubo was wrong in a different way, reading a dispersion_ratio of about 1.1, which says "the published number IS the cost", against a true 4.3.

The field was labelled an estimate and that was not enough. The schema said "Estimated by linear interpolation from the signed quartiles", which is accurate about the METHOD and silent about the ERROR. A reader had no way to know the estimate could be large or backwards. Disclosing a method is not disclosing a defect.

Corrected 2026-09-23. execution_slippage.sql v3.0 adds APPROX_PERCENTILE(signed_bps, 0.5), deployed to the live query 7665968 after validating the rendered SQL on a separate query first. The endpoint now prefers the measured column, keeps the interpolation only as a fallback for rows written before the deploy, and publishes signed_median_measured so a consumer can tell which one it received. No published figure moves: avg_slippage_bps is still the median of the absolute gap, computed by a byte-identical expression.

Not corrected, because it is not wrong: the 2026-09-23 X post said "every venue now shows the signed median beside it". That was true when posted and is more true now. The post makes no claim about any venue's value.


2026-09-22 — we told Talos we sample 10 DEX aggregators; the live figure was 7

Where it went: the Talos provider-network first touch, emailed to provider@talos.com on 2026-09-08 (outreach/drafts/2026-09-01-talos-provider-network.md). It is the only place this figure was sent. Talos is in the acquirer set, and the message offered "the methodology and some exports" as evidence of our measurement quality, which is what makes a miscounted venue list worth correcting rather than quietly fixing.

Wrong: "We sample quotes from 10 DEX aggregators, check each against what actually filled, benchmarked to Uniswap".

What it should have said: 7, benchmarked against the Uniswap baseline. The 10 was the count of leaderboard rows carrying quote_sampled: true, and that flag means "we have EVER collected quote data for this source", not "this source is answering now". Three of the ten do not belong in a present-tense sentence:

countedwhy it should not have been
Uniswapit is the BASELINE, and the same sentence already names it as the benchmark. Counting it among the aggregators it benchmarks is double-billing.
Odoswound down 2026-07-30; last successful sample 2026-07-27, six weeks before the send.
OpenOceanbehind a bot challenge since 2026-08-21; last successful sample 2026-08-21, 18 days before the send.

The error is larger than our own finding said. The metric finding quote-sampled-counts-wound-down-odos-2026-09-17 named odos alone and put the true figure at 9. It missed OpenOcean, and it did not notice the baseline was inside the count. It also said the sent body carried the claim "twice": the sentence appears twice in the FILE, once in the draft body and once in the record of what went out, and the message itself said it once.

Nothing else in that message is wrong, checked rather than assumed. Against the seed commit the draft cites (2026-09-08T11:32:33Z), "110,676 quotes" reproduces exactly and "77.5 days without a gap" reproduces exactly (2026-06-23 to 2026-09-08). The rated-cell count is not touched by this error.

What changed in the pipeline. scripts/dump_signals.py now reports component_b.quote_sampled as the count of sources with a LIVE feed, derived from quote_confidence != "stale" so it tracks the recency rule rather than a hand-kept list of dead venues, and publishes quote_sampled_ever and quote_sampled_not_live (each excluded source with its reason) beside it. The recency rule itself was extended to the matched comparisons in the same PR (see the entry above), which is what makes "stale" reliable enough to count on. A source that comes back is counted again with no edit.

Still to fix in copy, not code: any future message must not reuse the leaderboard row count as a present-tense venue count, and must not count the baseline among the venues it benchmarks.


2026-09-22 — entry withheld

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.


2026-09-21 — entry withheld

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.


2026-09-17 (also) — the compare pages published the bot-contaminated revert rate under a sentence describing the filtered one

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:

venuepublished (v5, all chains)now (v6 user-only, Ethereum)
1inch4.01%0.18%
KyberSwap2.72%0.81%
Bitget Wallet2.26%0.48%
ParaSwap1.42%0.39%
CoWSwap0.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.


2026-09-17 — every compare page ranked two venues on different chains

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:

pagepublishedon 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-uniswapVelodrome +2.2Optimism: +10.9, five times understated
camelot-vs-uniswap"effectively tied" (1.9)Arbitrum: Camelot +3.1, a different band
aerodrome-vs-velodromeAerodrome +2.1no 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.


2026-09-15 — a live newsletter carried an un-bannered coverage figure, because this log's own enumeration was wrong

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.

2026-09-09 — the withdrawn cross-venue RFQ axis was still live on the methodology page

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.


2026-09-09 — a compare page ranked a venue on a number that is not an execution cost

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.


2026-09-08 — entry withheld

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.


2026-09-08 — attribution coverage stood at 92.4%; the correct figure is 87.9% (nothing external carried it)

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.

pages.** No coverage percentage anywhere. The coverage meter lives in admin_api._attribution_coverage, behind require_admin.

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:

PublishedCorrect
overall92.4%87.9%
untraced$1,193.6M$1,362.9M
denominator$15,613.1M$11,279.0M
arbitrum94.7%94.3%
base91.7%82.1%
ethereum92.6%91.9%
optimism94.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.


2026-08-19 — $2.29B of phantom volume inflated our own Arbitrum and Base coverage figures (caught before any of it was sent)

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:

ChainAddressVolumeTradesLegsDune's own name
arbitrum0x1b6e01139bceba444ff4ac04261d263d68af6888$500.0M1256camax.eth
base0xbd06c326b6779f2f3e048ff3093196e30192e888$1,102.5M1132FakeHook
base0x78ca169834f585b367703514d22a3dc59375a888$684.0M1132FakeHook

$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:

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.

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.


2026-08-17 — told KyberSwap their quotes "lose" bps, when the metric measures quote honesty, not fill price

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").

2026-08-05 — the Base unattributed-volume research page was dated four days forward

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:

PublishedCorrect
Volume window7 days ending Jul 317 days ending Jul 27
Label datenot statedJul 31
Base unattributed$566M (9.3%)unchanged
Base tracked volume$6.09Bunchanged
Three largest unattributed$216Munchanged

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.


2026-07-30 — every published attribution-coverage figure since 2026-06-22

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:

SentPublishedCorrectPer chain (published -> correct)
2026-06-2890.6%87.8%n/a (issue gave overall only)
2026-07-0292.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-0992.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-1693.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-2394.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.

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.

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.


2026-07-14 — Uniswap Foundation grant resubmission and the product one-pager

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.

1. "2,600+ proprietary labels" (one-pager headline)

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.

2. "On Optimism the canonical UniversalRouter shows no volume; all UniversalRouter flow runs through clones"

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.

3. "Eight non-canonical UniversalRouter deployments on Base"

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.

The pattern

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.


2026-07-14 (later) — root cause of the Optimism claim, and what it exonerates

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 finding that fell out of the check

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.


2026-07-14 (worst) — we called Uniswap's own routers "non-canonical clones"

Where it went: the Uniswap Foundation grant resubmission and two emails to the grants manager. Withdrawn the same day, before he replied.

What was wrong

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 cloneActually
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.

Root cause (the one that matters)

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.

Pipeline fix

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.

What actually survives

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.


2026-07-14 (systemic) — every headline number was measuring the wrong thing

Not a single wrong claim: a class of them, all found by building the checks.

Volume was inflated ~2x, including the headline

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.

"Venue volume" was two different measures glued together

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.

1inch cannot currently be measured at all

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.

The gate

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.