DEV Community

Cover image for Who Got Paid: Identifying Anonymous DEX Frontends by Their Fee Recipient
Andrew Maury
Andrew Maury

Posted on Originally published at rantum.xyz

Who Got Paid: Identifying Anonymous DEX Frontends by Their Fee Recipient

Originally published at rantum.xyz, which is the canonical source.

Most swaps do not announce their origin. A meta-frontend, a wallet's in-app swap tab, and a Telegram trading bot can all route through the same public router, emit the same Swap event, and leave calldata that looks generic. Three of ClearTrace's four attribution vectors work on routing evidence: was the entrypoint a known aggregator, did the user call a pool directly or through a proxy, is there a fingerprint appended to the calldata. When all three come up empty, the transaction is anonymous by every structural measure.

But an anonymous frontend is usually still a business, and a business takes a cut. When that cut is taken inside the swap transaction itself, the way affiliate and referral skims are, it has to move on-chain where the receipt records it. That is the evidence this vector reads. A frontend that sweeps its revenue later, or monetizes off-chain entirely, leaves nothing here. And one dependency shapes everything downstream: finding who got paid is mechanical, but naming them requires a label somebody has already attached to that address.

The problem is that the cut does not identify itself. There is no fee standard on Ethereum. Individual projects emit their own fee events, LiFi's collector contract logs a FeesCollected event a decoder can subscribe to, but nothing generic marks one token movement as revenue and another as settlement across the long tail of routers this vector exists for. A frontend's revenue is an ordinary ERC-20 Transfer log sitting in a receipt beside the swap's own transfers, structurally identical to them. You cannot decode it, because there is nothing to decode. You can only work out which transfers are not part of the swap and see who is left.

Trap 1: identification by exclusion puts every error in the exclusion set

Turning "which transfer is the fee" into "which transfers are not the swap" is the move available here, and it relocates all of the risk into the exclusion list. Every category you forget to exclude becomes a false frontend. The user receiving their own output tokens is not a fee. The contract that was called is not a fee. A pool receiving the input side of the trade is not a fee. A token contract receiving its own token, which some fee-on-transfer designs do, is not a fee. A burn address is not a fee. Miss any one of those and the vector confidently reports a counterparty as the frontend that originated the trade.

Trap 2: a fee sink is a signal in one direction only

This one is not obvious until it breaks something. Once a candidate recipient is found, its identity comes from a resolved label, and the labels contain infrastructure names as well as product names. A contract labelled as a fee vault is strong evidence when it is receiving the skim: something paid it, and that something is the monetization path you are trying to name. The same label on the contract the user called means the opposite, or rather it means nothing, because a fee vault is not a user interface and treating it as one invents a frontend out of plumbing.

So the same marker has to be a positive signal in one position and no signal in the other. In the taxonomy this is a single boolean, as_fee_recipient, threaded through the name classifier to select which exclusion set applies. It is one flag standing in for a real asymmetry: evidence about who got paid does not transfer to evidence about who was called.

Trap 3: getting paid is not the same as being the frontend

Even a clean recipient with a clean label is indirect evidence. Referral programs pay addresses that belong to partners rather than to the interface. A single fee address can serve several products from the same team. An affiliate skim names whoever holds the affiliate relationship, which is often but not always the surface the user actually touched. The vector answers "who monetized this trade," and that is a strong hint about the frontend rather than a proof of it.

The implementation

# app/attribution_kernel.py: the fee-recipient vector.
# ERC20 transfers to an address that is not the user, not the called
# contract, not a pool, not a token contract, and not a burn sink —
# i.e. a party skimming a fee out of the swap.
excluded = {tx_from, tx_to} | BURN_ADDRESSES | pools
token_contracts = {t[0] for t in transfers}
fee_recipients = []
seen = set()
for token, recipient, raw_amount in transfers:
    if not recipient or recipient in excluded or recipient in token_contracts:
        continue
    if recipient in seen:
        continue
    seen.add(recipient)
    fee_recipients.append({
        "address": recipient,
        "name": resolve_name(recipient),
        "token_contract": token,
        "raw_amount": str(raw_amount),  # exact; no USD/decimals applied
    })
Enter fullscreen mode Exit fullscreen mode

Two decisions in that block are worth pulling out.

The amount is stored exactly and never converted. raw_amount is the integer from the transfer's data field, kept as a string, with no token decimals applied and no USD price attached. That looks like an omission and is a refusal. Converting it requires the token's decimals and a price at that block, and both are lookups that can fail quietly and produce a number that is wrong by orders of magnitude while still looking plausible in a table. Attribution does not need the fee's value.

Recipients are deduplicated within a transaction. A single address can receive several transfers in one swap, on a multi-hop route or when both sides of a trade pay the same collector. Reporting it repeatedly would imply several fee events where there was one relationship.

The confidence tier, and the bug that set it

A matched recipient is handed to the frontend taxonomy, which resolves the label into a kind (wallet, frontend, MEV bot, or nothing) and returns a bucket with an explicit confidence. A calldata-suffix match returns high. A fee-recipient match returns medium, always, by construction: indirect but specific, which is exactly what an affiliate payment is.

The tiering earns its keep, and the repository records what happened when the name matching underneath it was too loose. An earlier version treated 0x as a plain substring marker for the 0x protocol. But 0x is also the prefix of every hex address, and the labeler routinely embeds addresses in names, rendering them as Wintermute (0x51c7…8ac2) or emitting heuristic labels like Ethereum First Funder: 0xa4aF…. Every such label matched. The comment in the source states the damage plainly: any suffix-tagged transaction carrying any non-MEV, non-wallet label was booked as a named frontend at high confidence. One loose substring rule silently promoted a large class of unknowns into confident, named, wrong attributions.

The fix was a stricter entity pattern, but the durable lesson is about tiering. A system that only emits identities cannot be audited after a matching bug. A system that emits identities with the evidence class attached lets you find every attribution that rested on the broken rung and re-evaluate exactly those.

Under-claiming, in code

The taxonomy sorts every transaction into one of five buckets: named frontend, aggregator API direct, wallet embedded, bots and MEV, or unattributed. The interesting case is a transaction that carries a calldata fingerprint proving some frontend tagged it, where the identity cannot be resolved. The obvious move is to guess from the router. The implemented move is to return unattributed with a fingerprinted flag set, so the leaderboard can show "fingerprinted, unidentified" as its own quantity rather than folding it into either a named frontend or the raw unknown pile.

What one real run returns

Method pieces are cheap without a run, so here is one: the 50 most recent Ethereum swaps in ClearTrace's synced dex.trades sample, a seven-minute window from 2026-07-01, receipts refetched from an archive node on 2026-08-20 and pushed through the kernel in receipt-only mode with ClearTrace's own label store answering resolve_name. All 50 classified. On 39, at least one transfer survived the five exclusions. The calldata-suffix vector settled 23, the aggregator and bot rungs took four each, fourteen resolved nothing, and the fee vector decided the final attribution on five.

Then we inspected the five, and this is the part that earns the confidence tier. Every one rested on a label attached to routing infrastructure rather than to a monetizing interface. Two resolved through an adapter contract whose name contains a venue string, so the frontend marker matched plumbing. The three we dissected transfer by transfer were not fees at all. In a $4,165 stablecoin swap routed through TransitSwapRouterV5, the surviving recipients were a market-making counterparty that received 2.655 WETH and paid back 4,165 USDC, a routing contract that both received and forwarded funds, and an address carrying an aggregator label that received the full trade notional. The taxonomy walked the list, hit the aggregator label, and booked a named frontend at medium confidence. In a $172k swap, three surviving recipients received the identical amount, which is not three fees, it is the notional in flight.

So the run's honest summary: in receipt-only mode, the exclusion set cannot tell a fee from a hop. Everything in a multi-hop route that is not the user, the entry contract, a pool, a token contract, or a burn survives, wearing whatever label it has. The vector found the route, not the revenue. The structural fix is known and not yet built: exclude every address that also sends within the same receipt, or use trace data to remove the whole call path. Neither exists in the kernel today. This run is the argument for building both.

What the tier system did do is exactly its job. All five wrong names went out the door marked medium and fee_recipient, which means one query finds every attribution resting on this rung, and re-evaluating them after the exclusion fix touches nothing booked on suffix or aggregator evidence. An untiered system would have published the same five names as flat facts and left no seam to find them by.

And that is also the honest answer to where this rung lives in production until the fix lands. The receipt-only kernel serves single transactions on demand, with the method and confidence in every response, and feeds no leaderboard. The vector's SQL twin, which does feed the aggregate dashboards, already treats fee-recipient volume as an overlay rather than as truth: the query credits the full trade amount to every qualifying intermediate, reports the result as its own column, and never folds it into the conserved volume total, with a comment in the repo measuring the over-crediting it would otherwise cause at roughly 80% of an Ethereum total. The rung stays because a quarantined hint is still a hint; nothing it produces reaches a published total.

What this does not claim

It does not cluster frontends by fee rate. The obvious next step from "who got paid" is "how much, as a rate," on the theory that a consistent basis-point cut would group several anonymous routers under one operator. That is not implemented, and the reason is in the design above: the vector deliberately holds the amount as a raw integer, so a rate would require the decimals-and-price conversion it exists to avoid, applied to both the fee and the trade notional, before any two swaps could be compared. A fee rate is also weak identity evidence in a way an address is not: interface fees cluster around a handful of conventional sizes, so a rate is a habit shared across unrelated products, where an address belongs to exactly one operator. The address is the identifier.

It also inherits every limit of the labeling layer beneath it. The vector can isolate a recipient cleanly and still return nothing, because there is no label for that address, and the run above adds the sharper failure: when the store's labels are attached to infrastructure, or simply wrong, the vector inherits those errors at medium confidence. Whatever it names, it names because someone already labelled the collector, so on genuinely anonymous frontends it can only surface what the label store already holds, and on this sample it correctly named none. And the exclusion set is a fixed list of five categories, not a learned classifier: a fee routed through an intermediate contract that resembles a pool, or paid in a later transaction rather than inside the swap, is invisible to this pass by construction.

Nothing in a swap receipt is labelled as revenue. The fee is legible only as the transfer left over once everything structural has been ruled out, so the exclusions are the entire product, and every category they miss walks into the output wearing a name. The run above is that account for this vector, published rather than filed.

The full case study, including the taxonomy cascade and what this proves to a client, is at rantum.xyz. ClearTrace is at cleartracedata.com.

Top comments (0)