A swap's calldata often carries a few bytes past the last ABI word boundary. Decoders throw them away. They are some of the only on-chain evidence that an interface tagged the trade, and most of the work went into not saying which one.
The problem
Shared execution infrastructure gives a DEX frontend no standard way to sign its own trades. A swap placed in a wallet's built-in swap tab, in a portfolio app, or on a white-label interface reaches the same routers and the same pools as a swap placed on the router's own website, and lands on-chain looking identical. That is the attribution problem ClearTrace exists to work on.
Some interfaces do leave a mark, in a place nothing reads. A well-formed ABI call is a four-byte selector followed by some number of thirty-two-byte words. Stick a tag on the end and the call still works, because the decoder reads the arguments it expects and ignores whatever trails them. Etherscan renders the call cleanly. A decoded-trades table carries the swap with no sign of the tag at all.
So the tag sits there unread, in the one part of the transaction everybody throws out. Finding it is arithmetic on a length:
def has_calldata_suffix(input_hex: str) -> bool:
"""
A well-formed ABI call is a 4-byte selector (8 hex chars) followed by some
number of 32-byte words (64 hex chars each). A trailing remainder that
doesn't fill a word is an appended fingerprint: the signature of modular
frontends and wallet-native swap extensions.
"""
if not input_hex:
return False
h = drop_hex_prefix(input_hex)
n = len(h)
return n > 8 and (n - 8) % 64 != 0
That is the whole detector. (The prefix strip is inline in the original; I gave it a name here so the arithmetic is the only thing on the page.) Notice what it does not do. It reads none of the bytes. It tells you something is there and nothing at all about what, which turns out to shape everything downstream of it.
What the bytes actually look like
The test is one of the vectors in ClearTrace's attribution kernel, next to direct-versus-proxy routing, fee-recipient detection and aggregator-router matching. The kernel does no I/O: hand it a transaction, a receipt and a function that resolves addresses to labels, and it hands back a classification. That means every decision in it can be tested against a static fixture. A second module maps the classification onto the published taxonomy, one of named frontend, aggregator-API-direct, wallet-embedded, bots and MEV, or unattributed, and tags it with a confidence tier that describes the method rather than how well-known the name is.
Both run per transaction on the live public API, so I could just point it at real trades. Everything below came out of it on 2026-09-30. The raw calldata I pulled separately, from a public node.
I took fifty consecutive Ethereum DEX trades above $1,000: a 3-minute-48-second window on 2026-09-29, 16:10:23 to 16:14:11 UTC, against Uniswap, Curve and Fluid pools, ranging from $1,043 to $1,999,358 with a median of $3,452. (Those sizes are the feed's leg-summed notional, a larger basis than the conserved figure in the aggregate table further down. I am flagging it because the last section of this piece complains about exactly that kind of double-count.) Eight of the fifty carried an unaligned tail on the top-level call. Here is what was actually in them:
-
1inch AggregationRouterV6, selector
0x07ed2379, 4 unaligned bytes, tail2a6f45f2 -
An unlabelled contract (
0x278d...f8d2), twice, selector0x70521ae9, 3 unaligned bytes, tail000012 -
MetaRouterGateway, twice, selector
0xa11b1198, 29 unaligned bytes, beginningbc_nlhvsi52in ASCII -
An unlabelled contract (
0x881d...300c), selector0x5f575529, 1 unaligned byte, tail1f -
A 7702 delegated EOA (
0x0000...089b), twice, leading bytes0x80000000, 23 and 30 unaligned bytes, packed payload
The MetaRouterGateway pair is the one I would hang something on: a byte-identical twenty-nine-byte tail on two separate transactions, starting with the ASCII string bc_nlhvsi52. Somebody's client identifier, in plain text, in calldata, where no standard decoder looks. The four bytes on the 1inch swap call are almost certainly a referral or partner tag, sitting after every declared argument has been filled, though I cannot prove that from the bytes alone. The three-byte 000012 tail does repeat across two transactions to the same contract, which is the pattern this vector is named for, but three low-entropy bytes are about as likely to be a packed trailing field as a tag.
Then there is the last row, which is where the whole thing gets its limit. Its leading four bytes are 0x80000000 and the rest is not ABI-encoded at all, just a packed argument block. There is no last word boundary for a tag to sit past, so the unaligned remainder the test reports is the payload. When I looked up the contract, its code turned out to be twenty-three bytes starting 0xef0100, an EIP-7702 delegation designator rather than a program, so the account is a delegated EOA and there probably is a wallet-side execution surface behind it. That came out of the contract code rather than the detector, though. What the detector caught here was an encoding choice.
That ceiling is structural and there is no clever way around it. Length arithmetic cannot tell an appended tag from an encoding that was never word-aligned to begin with, and two of my eight hits are the second kind. Some unknown share of every hit is noise like that. You can still use a detector in that state, but only if nothing downstream is ever allowed to read a hit as an identity.
What the eight hits were allowed to conclude
So how many of the eight produced a named frontend? None. All eight came back unattributed, at low confidence, with a separate fingerprinted flag set true. Two rules did that work, and a third sits in front of both.
- A suffix on an aggregator's own router names no frontend. One of the eight was a 1inch AggregationRouterV6 call. A tag on an aggregator router could belong to the aggregator or to somebody's interface layered on top of it, and there is no way to tell, so neither gets the credit. There is one hole in this, and the code does not hide it: if the router's label matches a wallet, it is taken as an in-wallet swap at high confidence no matter what the aggregator flag says.
- A suffix whose label matches nothing on the marker lists names nothing either. The other seven were on contracts with no label, or with a label that appears on neither the frontend nor the wallet list: three unlabelled, the two delegated accounts, the two MetaRouterGateway calls. I should be honest about that last pair. MetaRouterGateway is a routing interface. It goes unnamed because the marker list has not caught up, which is a coverage gap dressed up as a principled refusal.
-
MEV and bots get checked before the suffix at all. Two of the fifty went to
bots_mevon their originator label alone. Neither carried a suffix, so nothing actually collided here. It matters anyway: the biggest contract by suffix-overlay volume on Ethereum is an atomic-arbitrage searcher. Check the suffix first and you strip that searcher of its bot classification and file it as fingerprinted-unattributed instead. Worse, anything labelled as both a bot and a wallet would go out as an in-wallet swap at high confidence.
The flag is doing more work than it looks like. unattributed with fingerprinted: false means I found no evidence. unattributed with fingerprinted: true means an interface definitely tagged this trade and I could not work out which one. Those are two different admissions, and squashing them together is how a coverage number quietly starts flattering you.
The two layers also rank the suffix differently, which confused me until I read both. The kernel's primary_vector puts it fourth, behind proxy, aggregator and fee-recipient, because that field answers how the trade was routed. The taxonomy puts it second, behind only the MEV check, because that one answers what I know about the interface. Same transaction, two questions, two orderings.
One guard in the naming path failed, and it failed the way guards do. To attach a name, the resolved label has to look like an interface, which in practice is a substring match against a marker list. One protocol on that list has a two-character name that is also the prefix every hex address starts with. The labeller, meanwhile, puts addresses inside names all the time: Wintermute (0x51c7…8ac2), Ethereum First Funder: 0xa4aF…. Every label of that shape read as a frontend. So a suffix-tagged transaction carrying one of them went out as a named frontend at high confidence, unless an infrastructure exclusion happened to catch it first. The fix strips address-shaped tokens out of the label before matching and then requires the marker to stand alone as its own word. Of all the places to put a matching bug, the high-confidence tier is the worst one.
The feed I drew these from is a rolling one-day sample, so it has already rotated and you cannot reproduce my fifty. Here are the eight hashes instead, so you can put them through the endpoint yourself:
-
0xbd1df7e5bfa30e7b28acae7343def9e6f2137e047072d267d4f4833892ae0ed2(1inch AggregationRouterV6) -
0x562b40b68022f1ed6d9c97a1ceec06d3f37215331509a2f28d1ccaf5ce33f75d(unlabelled,0x278d...f8d2) -
0xb008c636327b9f43e7c928c94fcd6fe22ca5d5001a028a941d2b2d8c624d5345(unlabelled,0x278d...f8d2) -
0x9bb462fced2c7b5d7aabe31332d32ff000a4cfcea41a44a65127c18c8da01935(MetaRouterGateway) -
0x198342c58a22d5264dcfb1e5ed49a4d76faf72353d351bbb1047092935a2fdb7(MetaRouterGateway) -
0x7f0deb69de0f2e9d54ad7a85615ec20997d240b8f9f6577fe4bfe6cdf6173e6a(unlabelled,0x881d...300c) -
0x39f7f878402227b87b2d1197e68692791d626ee728d1bc333522161612310aea(7702 delegated EOA) -
0x685667cee982f6aecc8ad596a832076804cc59846b960d207cdf144ad48acdb9(7702 delegated EOA)
More evidence, less attribution
The endpoint has a deep mode that fetches internal call traces, widens the aggregator match to the whole call path, and runs the suffix test on every sub-call. More information, so better answers, or that was my assumption. One caveat first, because the code is plain about it: the live path counts a suffix on any traced call frame, reverted and static ones included, while the Dune version of the same vector requires a successful call. So some of the jump below is just a looser test. I did not measure how much. Here are the twenty newest transactions of the sample, run both ways:
- Carrying a calldata suffix: 3 top-level only, 7 with traces
- Attributed to any class: 4 top-level only, 3 with traces
- Fingerprinted and unidentified: 3 top-level only, 7 with traces
Before anyone lifts those numbers: they are per-transaction counts on fifty consecutive trades above $1,000, and they are not coverage. ClearTrace publishes coverage volume-weighted over a seven-day window, currently 89.5% on Ethereum. A fifty-trade slice of mostly retail-sized swaps does not compare to that figure in either direction.
Traces more than doubled the fingerprints and left the sample less attributed, which is not what I expected to find. Five records changed state. Four of them just picked up the fingerprinted flag. One picked up an identity: an originator labelled only Market Maker turned out to reach KyberSwap's MetaAggregationRouterV2 further down the call path, and moved from unattributed to aggregator_api_direct. Two went the other way, down from aggregator_api_direct at medium confidence to fingerprinted-unattributed at low, because a suffix showed up on a sub-call and the aggregator rule says a tagged router is not the interface. The fifth one is my favourite: an ERC-4337 EntryPoint call whose traces also reached MetaAggregationRouterV2, and the suffix rule threw that identity away too.
One name gained, two lost. I am fairly relaxed about that trade, and one of the two was never a published name anyway, since its venue is withheld from output under a separate policy. Tune this system for coverage and it keeps both.
Why the big number is an overlay and not volume
The same test also runs at scale, in the Dune query behind the aggregate attribution table, over every successful internal call instead of just the top-level one. At that grain it goes off on most of the market. From the live API on 2026-09-30, against a trailing seven-day window whose attribution table was written 2026-09-29 16:20 UTC:
- Ethereum: 4,077 contracts, $6.16B conserved volume, $5.13B suffix overlay (83.3%), and 2,180 rows whose overlay exceeds that row's own volume
- Base: 3,716 contracts, $5.71B, $4.10B (71.9%), 1,392 rows
- Arbitrum: 852 contracts, $1.07B, $527M (49.2%), 352 rows
- Optimism: 231 contracts, $143M, $117M (82.2%), 112 rows
On 2,180 of Ethereum's 4,077 contract rows, the suffix figure comes out larger than that row's entire conserved volume. The two are counted at different grains: conserved volume takes each transaction's notional once, the overlay sums the legs. The top row on Ethereum is that atomic-arb searcher again, at a $982.1M overlay against $945.0M of its own routed volume.
You cannot add a column to a total it can exceed. So it rides along as a reported overlay, out of the headline sum, with the reasoning written into the query instead of left for whoever reads the output next. The query keeps a note about what happens if you fold the sibling fee overlay back in: the Ethereum total jumps sharply and overshoots the independent third-party estimate of the same market. Double-counting announces itself as a number you like.
Which settles what this vector is actually for. At the trace grain it is nearly everywhere, so it cannot tell frontends apart. At the transaction grain it is rare and specific and, on a live sample, names nobody. Either way its job is to mark the edge of what I know.
What this proves
Finding a signal nobody else reads took an afternoon. The rest of the work was the layer that decides what the signal is allowed to conclude, and then checking what that layer really does against live trades rather than trusting what its docstring says about it. On this sample it turned eight fingerprints into zero names and one honest flag, and dropped two medium-confidence attributions as soon as better evidence said they were shaky.
Any vendor can report coverage. The hard part is drawing a line you can defend between what a dataset knows, what it suspects and what it only inferred, and then putting that line on every record so a buyer can act on it. That is the part that survives contact with an adversarial domain, and it is the part Rantum builds.
Originally published at rantum.xyz, which is the canonical source.
Andrew Maury is the founder of Rantum, a senior data science and ML studio that turns messy, fragmented, and adversarial data into models, APIs, and products that ship. He spent roughly four years on data infrastructure at 0x Labs, advised the Uniswap Grants Program, and has done client work for Art Blocks.
Top comments (1)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.