Perspective On-Chain Data Engineering

The Check I Didn't Run

A week spent tuning the smallest term. Most bad numbers do not come from checks nobody built.

Andrew Maury
Andrew Maury
September 2026

I set out to build a detector for coordinated trading fleets on DEX data. I spent the week on a candidate filter, measuring its precision, hunting its false positives, and refusing three tempting fixes. At the end I ran one command I had never run:

OVERDUE — older than the refresh cadence allows. DO NOT QUOTE these figures:
  dune_router_entry   17.8d old (limit 7.5d)   entrypoint volume and the coverage denominator

The tool was already in the repo. It takes one command. It prints DO NOT QUOTE in capital letters. Every number I had produced that week came from the table it was shouting about.

That is the piece. Not a missing instrument. A present one, nobody ran.

The stack, largest term first

The result degrades in three places. I found them in exactly the wrong order, so here they are in the right one.

Two of the three cost recall. The third costs characterization and no recall at all, and calling all three "recall" would be the same error this piece is about: comparing two things that are not the same measurement.

TermSize
1. Stalenessthe candidate table was 17.8 days old against a 7.5-day limit; its figures reflect 39.1% of the one operator fleet I can measure completely
2. Thresholda $1M cut dropped a fleet member carrying $6.70M, because it read $0.61M inside the stale window
3. The filter
(characterization, not recall)
4.7% of candidate volume un-characterizable, zero membership lost

I spent the week on term 3.

Term 1: recall is bounded by a cron job

The candidate table is rebuilt weekly from a 7-day window, and the pipeline rebuilt it on schedule. The copy I was analyzing had not been. Production's copy was two days old. Mine was 17.8, and the freshness stamp sitting in it read the current date, honestly, because that stamp records the newest write of any table. The candidate table underneath it was more than two weeks behind.

Two failures had to line up: a working copy that diverged from the shipped one, and a freshness field that answers a different question from the one everybody asks it.

So the classifier could only ever see what happened before that write. Here is what it costs, measured against a target I happen to know completely. One operator deployed 13 contracts on a single day, all inside 84 minutes: four in an early batch that never traded, then nine that did. Testing, a pause, then production. Eight of the thirteen carry volume, $67.24M across their active span.

The stale snapshot carries $26.27M of it. 39.1%.

Nothing about the filter changes that number. A perfect classifier on a 17.8-day-old candidate set is a classifier that cannot see 61% of this one.

This is not a crypto problem. Anyone who has fit a model on a periodically rebuilt candidate set has this exposure, and the failure is silent in a specific way: the freshness stamp people check is usually an upper bound across many tables, so it reads current while the one table that matters has not moved.

Term 2: thresholds compound with staleness, and nobody owns both

A volume floor and a refresh cadence are set by different people, usually months apart, and neither is wrong on its own. Together they multiply: a stale window makes every floor stricter than the person who chose it intended, and nothing in either system reports the combined effect.

One fleet member traded $6.70M. Inside the stale window it reads $0.61M, so a $1M floor dropped it. Nobody tuned anything badly.

Term 3: the filter, which is where the week went

The filter is one predicate: a contract whose transaction count equals its swap leg count is doing one swap per transaction, so it is a single-purpose executor rather than a multi-hop router. It is free, it runs on a table that already exists, and on a labeled set it is decent.

It is also the smallest term in the stack, and it took the last measurement of the week to find out why.

I expected a large blind spot. Some executors never appear as a party to their own token transfers: they move a counterparty's inventory by transferFrom, so anything built on transfer records cannot see them. I assumed that hid them from the filter too, which would have meant the candidate set was quietly restricted to executors that touch assets directly.

It does not. The filter reads the transaction's to address, which never required transfer-party presence, and the clearest example of the pattern sits in the candidate table at $19.6M with legs and trades equal. It passes. Zero membership is lost to it. What the pattern does blind is the adjudication step, which reads asset transfers to find funders and counterparties: 4 of the 32 candidates, 4.7% of the set's volume, are never a transfer party, so those signals are structurally empty for them. That is the whole of term 3.

Three heuristics, three failures, one cause

Before the filter, I tried to separate market-maker infrastructure from anonymous fleets. Three plausible discriminators, each derived from entities I had already identified by hand:

  1. Fleet-shape features (shared funder, batch refuelling, uniform start times, single-venue concentration). They score positive on a professional market maker's execution fleet: persistent worker addresses drawing gas from a common treasury and settling exclusively against one inventory address. The features describe automation under single-operator control, and that is what market-making infrastructure is.
  2. Counterparty concentration. The intuition: a market maker settles exclusively against its own inventory address, a fleet trades against many public pools. Measured: the market maker scored 0.989. A known fleet scored 1.000, with exactly one counterparty, because it traded exclusively on Uniswap V4, whose singleton design puts the same PoolManager contract on the other side of every swap. The metric measures venue architecture, not intent.
  3. A standard-read probe, to exclude venue contracts. It would have deleted the best-documented fleet in my set, which answers owner() and returns its deployer.

Every one was derived from the known-entity set and failed against the known-entity set. Each failure was caught only because labeled entities existed to test against. Without them, all three would have shipped.

The one rule that survived measurement

Narrowing the third heuristic to venue-specific reads only (token0, token1, factory, name, symbol, dropping owner) gives a rule that can be measured rather than asserted. On held-out sets of 53 labeled executor contracts and 17 labeled pool contracts, neither consulted when the keys were chosen:

RuleFires on executors (false negatives)Catches pools
any standard read12/53 = 22.6%17/17
venue keys only1/53 = 1.9%15/17

Ten of the 53 were then re-derived through a separate JSON-RPC path with its own positive control, because the script that produced these numbers is the same class of script that had already produced two false ones. Ten of ten agreed.

The two "missed" pools are both position rebalancers that answer only owner, which is a rebalancer shape rather than a pool. Reclassifying them makes it 15 of 15. That reclassification was decided after seeing they were the misses, so both numbers stay in the record and the reader picks.

Three refusals

The single false negative answers name with eight zero bytes, not a valid ABI string. Requiring well-formed returns would likely take the rule to 0 of 53. I did not make that change, because it was identified by looking at which case it would rescue.

Same for the venue-singleton exclusion that would have saved heuristic 2, and the minimal-proxy test that would have caught one remaining false positive. Each would have been fitted to the case that motivated it, on a validation set of one or two.

A threshold moved to keep a case passing is a threshold that has stopped meaning anything.

Four errors, one shape

The work produced four wrong results of my own. They share a structure worth more than any of them individually: a missing or mismatched value silently became a value.

  1. I read a dictionary key that did not exist. .get() returned None, or {} made it an empty dict, and "no interface responded" was printed for eight contracts that were never asked.
  2. A guard applied to one branch and not the next. A legitimate empty return, meaning the bare hex prefix with no payload, failed the first check and passed the second. The decoder then concatenated a prefix onto that value's own last 40 characters, which for an empty return is the prefix itself, yielding a doubled prefix it treated as an address. Every contract without an owner was bucketed into that one phantom owner, producing an 8-contract, $116M cluster that was the exact inverse of the truth.
  3. I inferred that the candidate table was missing active routers by comparing a 30-day sum of swap legs against a 7-day sum of per-transaction maximum legs from a snapshot taken 17.8 days earlier. Three mismatches at once: window length, metric definition, as-of date. The gap did not exist; every absence was the window. That retraction is what led to term 1.
  4. I reported two recall figures side by side, 82.3% and 39.1%, which count different things. The first credits every identified contract its full activity. The second measures how much activity the figures actually reflect. Identification is not measurement.

A fifth produced the lede itself, and I only caught it after the draft was finished: I read a freshness stamp out of my local working copy and checked it against a refresh cadence defined for the production copy. Different lineages, same shape. The pipeline was healthy the whole time.

Two of the four above are fixed at the value level: probes now return an explicit tri-state, so an empty return and a transport error can never collapse into each other, and an address decoder raises rather than returning something address-shaped. Nine tests, each named for the bug it prevents.

The other two need the same move one level up: carry the metric definition, the window, and the as-of date on the value itself, so that comparing a 30-day leg sum to a 7-day per-transaction maximum from a 17.8-day-old snapshot raises instead of returning a number. That single change would have caught all four.

About the addresses

The filter surfaces 32 high-volume contracts that three public labelers cannot name. That set is an illustration here, not a finding, and it carries every caveat above: its figures reflect 39.1% of the one operator I can measure completely, most of its members are classified by the absence of a venue interface rather than positive identification, and the fleet that taught me the most entered the analysis only because three of its members were already in the queue. Anything that missed entirely is invisible to me and contributes nothing to any recall number in this piece. All of them are upper bounds.

I am not publishing a list of suspects. I am publishing the filter, the rule's measured false-negative rate, and the order of the stack.

So, to answer the question the first line of this piece opened: yes, there is a detector. It is one predicate and one probe, with a 1.9% false-negative rate on 53 held-out executors. It sits on a candidate set whose staleness costs more than the detector could ever win back.

What I would tell the version of me who started

Measure the candidate set before the classifier that sits on it.

And run the check. Most bad numbers do not come from checks nobody built. They come from checks nobody ran.

Building a measurement layer you need to trust?

Rantum is a senior data science & ML studio. We turn messy, fragmented, and adversarial data into models, APIs, and products that ship, and into numbers a board can rely on.

Work with us