<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Andrew Maury</title>
    <description>The latest articles on DEV Community by Andrew Maury (@andrewmaury).</description>
    <link>https://dev.to/andrewmaury</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3993629%2Fa72c3384-bc53-4d9a-9c4c-29fd4b073e94.jpg</url>
      <title>DEV Community: Andrew Maury</title>
      <link>https://dev.to/andrewmaury</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/andrewmaury"/>
    <language>en</language>
    <item>
      <title>Reconstructing Wallet Journeys Across DEX Frontends, One Chain at a Time</title>
      <dc:creator>Andrew Maury</dc:creator>
      <pubDate>Wed, 05 Aug 2026 16:04:07 +0000</pubDate>
      <link>https://dev.to/andrewmaury/reconstructing-wallet-journeys-across-dex-frontends-one-chain-at-a-time-33kn</link>
      <guid>https://dev.to/andrewmaury/reconstructing-wallet-journeys-across-dex-frontends-one-chain-at-a-time-33kn</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://rantum.xyz/case-studies/multichain-journey-attribution.html" rel="noopener noreferrer"&gt;rantum.xyz&lt;/a&gt;, which is the canonical source.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A blockchain records what happened, but not the story that connects it. Every DEX trade is a row in a wide, flat table: an address, a venue, a timestamp, an amount. There is no session, no user ID, no ordered clickstream. So when someone asks an ordinary product question, "after a user swaps on our frontend, where do they go next, and where did they come from?", the ordering that a journey is made of does not exist as a column. It has to be reconstructed.&lt;/p&gt;

&lt;p&gt;Reconstructing it with window functions is the easy part, and it is where most write-ups on this stop. The work that decides whether the output means anything happens before the first &lt;code&gt;LAG&lt;/code&gt;, in three places where the raw data will happily produce a confident, wrong answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 1: a leg is not a trade
&lt;/h2&gt;

&lt;p&gt;Dune's &lt;code&gt;dex.trades&lt;/code&gt; stores one row per swap leg. A single aggregator route through three pools writes three rows for one user action. Sequence those rows directly and you manufacture hops the user never made, several within the same second, separated by a zero-minute gap.&lt;/p&gt;

&lt;p&gt;ClearTrace measured this leg inflation at 2.15x on Odos/Ethereum while building its attribution engine, which is why every volume path in that engine collapses to one row per transaction before any arithmetic. A journey query needs the same collapse for the same reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 2: the busiest wallets are not users
&lt;/h2&gt;

&lt;p&gt;On an unfiltered transition count, MEV searchers and arbitrage bots take the entire top of the table. They hop venues thousands of times a day, which is exactly the behaviour the query is built to surface, and they are not people whose journey anyone wants to understand. Any transition map published without a bot guard is largely a map of arbitrage routing wearing the label "user behaviour."&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 3: the chains are separate timelines
&lt;/h2&gt;

&lt;p&gt;It is tempting to run one query over every chain at once and call the output a cross-chain journey. &lt;code&gt;dex.trades&lt;/code&gt; spans all chains, so partitioning by wallet without a chain filter interleaves that wallet's Ethereum, Base, Arbitrum, and Optimism swaps into one sequence, then reports the gaps between unrelated chains as dwell time.&lt;/p&gt;

&lt;p&gt;The wallet did not travel from a Base entrypoint to an Arbitrum one, because a bridge, a different key, and a different session sat in between. The honest unit of reconstruction is one wallet on one chain. Cross-chain behaviour is a separate problem, and answering it takes bridge-level evidence that a timestamp sort cannot supply.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the query does
&lt;/h2&gt;

&lt;p&gt;A per-chain reconstruction with the guards in front of the window function. It resolves the &lt;em&gt;entrypoint&lt;/em&gt; of each trade (the router or frontend contract the transaction was sent to) rather than the venue it settled on, collapses legs to one row per transaction, drops dust and implausible wallets, then lets each remaining swap read its own predecessor. The output is a ranked edge list: for every ordered pair of entrypoints, how many distinct wallets crossed, how many times, and the median wait in between.&lt;/p&gt;

&lt;p&gt;One detail worth naming: &lt;code&gt;project&lt;/code&gt; in &lt;code&gt;dex.trades&lt;/code&gt; is the DEX venue (Uniswap, Curve), not the interface a user touched. Building hops from it reports venue routing as user behaviour. Resolving &lt;code&gt;tx_to&lt;/code&gt; against the chain's decoded contracts, and leaving anything unresolved as &lt;code&gt;Unknown Proxy&lt;/code&gt;, keeps the map about interfaces and keeps the unknowns visible instead of silently absorbed into a named venue.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Dune SQL (Trino) query for the Multi-Hop Origin Trace (Vector #3)&lt;/span&gt;
&lt;span class="c1"&gt;-- Reconstructs each wallet's ordered path between ENTRYPOINTS (the router or&lt;/span&gt;
&lt;span class="c1"&gt;-- frontend contract a swap was sent to) and rolls the hops up into a ranked&lt;/span&gt;
&lt;span class="c1"&gt;-- transition map: which entrypoint feeds which, for how many distinct wallets,&lt;/span&gt;
&lt;span class="c1"&gt;-- and how long they wait before the next swap.&lt;/span&gt;
&lt;span class="c1"&gt;--&lt;/span&gt;
&lt;span class="c1"&gt;-- SCOPE IS ONE CHAIN PER RUN. dex.trades spans every chain, so an unfiltered&lt;/span&gt;
&lt;span class="c1"&gt;-- PARTITION BY tx_from interleaves a wallet's Ethereum, Base, Arbitrum and&lt;/span&gt;
&lt;span class="c1"&gt;-- Optimism swaps into a single timeline that never happened, and reports the&lt;/span&gt;
&lt;span class="c1"&gt;-- gaps between them as user dwell time. {CHAIN} is substituted per chain the&lt;/span&gt;
&lt;span class="c1"&gt;-- same way queries/master_attribution_query_template.sql is templated&lt;/span&gt;
&lt;span class="c1"&gt;-- (ethereum | base | arbitrum | optimism); {{Params}} are Dune runtime params.&lt;/span&gt;

&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;base_trades&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tx_hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tx_from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;block_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount_usd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c1"&gt;-- The entrypoint is the contract the user's transaction was sent TO.&lt;/span&gt;
        &lt;span class="c1"&gt;-- Resolve it to a name the same way queries/sankey_routing_edges.sql&lt;/span&gt;
        &lt;span class="c1"&gt;-- does, and leave it as 'Unknown Proxy' when nothing resolves it rather&lt;/span&gt;
        &lt;span class="c1"&gt;-- than substituting the venue it happened to settle on.&lt;/span&gt;
        &lt;span class="c1"&gt;--&lt;/span&gt;
        &lt;span class="c1"&gt;-- NOTE: t.project is the DEX VENUE (uniswap, curve), NOT the interface&lt;/span&gt;
        &lt;span class="c1"&gt;-- that originated the trade. Building hops out of t.project reports&lt;/span&gt;
        &lt;span class="c1"&gt;-- venue routing as if it were user behaviour: a single aggregator route&lt;/span&gt;
        &lt;span class="c1"&gt;-- that touches three pools looks like a user visiting three frontends.&lt;/span&gt;
        &lt;span class="k"&gt;REPLACE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Unknown Proxy'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;'{CHAIN}: '&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;entrypoint&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;dex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;trades&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
    &lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="k"&gt;CHAIN&lt;/span&gt;&lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="n"&gt;contracts&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tx_to&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;blockchain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'{CHAIN}'&lt;/span&gt;
      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;block_time&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'{{Days_Back}}'&lt;/span&gt; &lt;span class="k"&gt;DAY&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;per_tx&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="c1"&gt;-- Collapse legs to ONE row per transaction before any sequencing.&lt;/span&gt;
    &lt;span class="c1"&gt;-- dex.trades stores one row PER SWAP LEG; a multi-hop route prices several&lt;/span&gt;
    &lt;span class="c1"&gt;-- legs for ONE user trade (measured at 2.15x inflation on Odos/ethereum,&lt;/span&gt;
    &lt;span class="c1"&gt;-- see master_attribution_query_template.sql). Sequencing raw legs invents&lt;/span&gt;
    &lt;span class="c1"&gt;-- transitions the user never made, several per second, separated by a&lt;/span&gt;
    &lt;span class="c1"&gt;-- 0-minute gap. tx_to is the transaction's `to` field, so the entrypoint is&lt;/span&gt;
    &lt;span class="c1"&gt;-- constant per tx and one row survives the collapse.&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt;
        &lt;span class="n"&gt;tx_hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;tx_from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;entrypoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;block_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;block_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount_usd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;notional_usd&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;base_trades&lt;/span&gt;
    &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;eligible_wallets&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="c1"&gt;-- Bot and dust guard. An unfiltered transition count is dominated by MEV&lt;/span&gt;
    &lt;span class="c1"&gt;-- and arbitrage wallets that hop venues thousands of times a day. That is&lt;/span&gt;
    &lt;span class="c1"&gt;-- real on-chain activity, but it is not a user journey, and left in it sets&lt;/span&gt;
    &lt;span class="c1"&gt;-- the entire top of the table. Drop dust trades first, then drop any wallet&lt;/span&gt;
    &lt;span class="c1"&gt;-- whose swap count over the window is implausible for a human.&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;tx_from&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;per_tx&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;notional_usd&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="n"&gt;min_notional_usd&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
    &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;HAVING&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="n"&gt;max_swaps_per_wallet&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;ordered_swaps&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="c1"&gt;-- The reconstruction: partition by wallet, order by time, and let each swap&lt;/span&gt;
    &lt;span class="c1"&gt;-- read its own predecessor. tx_hash breaks ties so two swaps in the same&lt;/span&gt;
    &lt;span class="c1"&gt;-- block order deterministically instead of shuffling between runs.&lt;/span&gt;
    &lt;span class="c1"&gt;--&lt;/span&gt;
    &lt;span class="c1"&gt;-- LAG alone is enough for the transition map: every ordered pair in a&lt;/span&gt;
    &lt;span class="c1"&gt;-- wallet's path is emitted once as (previous -&amp;gt; current), so adding LEAD&lt;/span&gt;
    &lt;span class="c1"&gt;-- would restate the same edges in the opposite direction and double-count&lt;/span&gt;
    &lt;span class="c1"&gt;-- them. LEAD is the same primitive for a forward-looking per-wallet view.&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt;
        &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tx_from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;block_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entrypoint&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;current_entrypoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entrypoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tx_from&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;block_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tx_hash&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;previous_entrypoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;block_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tx_from&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;block_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tx_hash&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;previous_swap_time&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;per_tx&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;
    &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;eligible_wallets&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tx_from&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tx_from&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notional_usd&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="n"&gt;min_notional_usd&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;previous_entrypoint&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;from_entrypoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;current_entrypoint&lt;/span&gt;  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;to_entrypoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;-- Distinct wallets is the honest headline: COUNT(*) alone lets one wallet&lt;/span&gt;
    &lt;span class="c1"&gt;-- that crosses the same pair 500 times read as 500 users.&lt;/span&gt;
    &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;tx_from&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;wallets&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;transitions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;-- Median, not mean: one wallet returning three weeks later drags an average&lt;/span&gt;
    &lt;span class="c1"&gt;-- dwell time far past anything a typical user did.&lt;/span&gt;
    &lt;span class="n"&gt;APPROX_PERCENTILE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;DATE_DIFF&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'minute'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;previous_swap_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;block_time&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;median_gap_mins&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;ordered_swaps&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;previous_entrypoint&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
    &lt;span class="c1"&gt;-- A journey is movement between entrypoints; ignore consecutive self-swaps.&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;current_entrypoint&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;previous_entrypoint&lt;/span&gt;
    &lt;span class="c1"&gt;-- Minimum dwell. A wallet that lands on a second entrypoint inside the same&lt;/span&gt;
    &lt;span class="c1"&gt;-- minute did not decide anything; that is automation, and on ethereum/7d it&lt;/span&gt;
    &lt;span class="c1"&gt;-- is what a swap-count cap alone leaves behind -- the top of the unfiltered&lt;/span&gt;
    &lt;span class="c1"&gt;-- table came back as near-symmetric A-&amp;gt;B / B-&amp;gt;A pairs at a 0-minute median&lt;/span&gt;
    &lt;span class="c1"&gt;-- (pool-to-router and back), which is the shape of in-block bot activity&lt;/span&gt;
    &lt;span class="c1"&gt;-- rather than navigation. Costs the fastest legitimate users; worth it.&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;DATE_DIFF&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'minute'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;previous_swap_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;block_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="n"&gt;min_gap_mins&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="c1"&gt;-- Suppress edges thin enough to be one wallet's habit rather than a pattern.&lt;/span&gt;
&lt;span class="k"&gt;HAVING&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;tx_from&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="n"&gt;min_wallets&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;wallets&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why distinct wallets and a median
&lt;/h2&gt;

&lt;p&gt;Two small choices at the end carry most of the interpretive weight.&lt;/p&gt;

&lt;p&gt;Counting rows lets a single wallet that crosses the same pair five hundred times read as five hundred users, so the headline column counts distinct wallets and keeps the raw event count beside it as a separate, clearly labelled number. And dwell time is a long-tailed distribution: one wallet returning three weeks later drags a mean far past anything typical, so the query reports a median. Neither choice makes the chart look better, and both make it hold up when someone checks it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What one run returns
&lt;/h2&gt;

&lt;p&gt;Run against Ethereum on 5 August 2026 over a 7-day window, with a $100 minimum trade size, wallets capped at 50 swaps for the week, a one-minute minimum gap between hops, and edges suppressed below 20 wallets. The query returned 103 entrypoint-to-entrypoint edges covering 19,518 hops in about seven seconds. These are the ten largest by distinct wallets:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;From entrypoint&lt;/th&gt;
&lt;th&gt;To entrypoint&lt;/th&gt;
&lt;th&gt;Wallets&lt;/th&gt;
&lt;th&gt;Hops&lt;/th&gt;
&lt;th&gt;Median gap&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Unknown Proxy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;UniversalRouter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;935&lt;/td&gt;
&lt;td&gt;1,233&lt;/td&gt;
&lt;td&gt;2h 39m&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;UniversalRouter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Unknown Proxy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;893&lt;/td&gt;
&lt;td&gt;1,165&lt;/td&gt;
&lt;td&gt;3h 39m&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Unknown Proxy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Router02&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;765&lt;/td&gt;
&lt;td&gt;1,278&lt;/td&gt;
&lt;td&gt;2h 30m&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Unknown Proxy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;UniswapV2Factory&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;762&lt;/td&gt;
&lt;td&gt;1,216&lt;/td&gt;
&lt;td&gt;2h 14m&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;UniswapV2Factory&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Unknown Proxy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;720&lt;/td&gt;
&lt;td&gt;1,210&lt;/td&gt;
&lt;td&gt;3h 49m&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Router02&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Unknown Proxy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;716&lt;/td&gt;
&lt;td&gt;1,176&lt;/td&gt;
&lt;td&gt;3h 19m&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;UniswapV2Factory&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Router02&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;628&lt;/td&gt;
&lt;td&gt;1,112&lt;/td&gt;
&lt;td&gt;2h 14m&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Router02&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;UniswapV2Factory&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;619&lt;/td&gt;
&lt;td&gt;1,104&lt;/td&gt;
&lt;td&gt;2h 12m&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;FWA&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;UniversalRouter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;292&lt;/td&gt;
&lt;td&gt;506&lt;/td&gt;
&lt;td&gt;6h 50m&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Unknown Proxy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DexRouter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;285&lt;/td&gt;
&lt;td&gt;379&lt;/td&gt;
&lt;td&gt;3h 29m&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Names are Dune's decoded contract labels, verbatim. Several describe a contract rather than a product (&lt;code&gt;Router02&lt;/code&gt;, &lt;code&gt;UniswapV2Factory&lt;/code&gt;), a useful reminder that a decoded name is not a brand; mapping those to the interface a user actually touched is a separate entity-resolution problem, and it is why &lt;code&gt;Unknown Proxy&lt;/code&gt; is left standing here instead of being guessed at.&lt;/p&gt;

&lt;h3&gt;
  
  
  The bot guard earning its place
&lt;/h3&gt;

&lt;p&gt;An earlier pass of the same run used only a swap-count cap (100 for the week) and no minimum gap. It returned 115 edges and 34,674 hops, and the top of the table looked like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;UniswapV2Factory&lt;/code&gt; -&amp;gt; &lt;code&gt;Router02&lt;/code&gt;, 2,269 wallets, median gap &lt;strong&gt;0 minutes&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Router02&lt;/code&gt; -&amp;gt; &lt;code&gt;UniswapV2Factory&lt;/code&gt;, 2,265 wallets, median gap &lt;strong&gt;0 minutes&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A near-perfectly symmetric pair, in both directions, with no time passing between the hops. That is not a person moving between two interfaces; it is automated in-block activity, and a swap-count cap alone never caught it because each individual wallet stayed under the threshold. Across the whole first pass, edges with a zero-minute median carried 40% of all hops. Adding the one-minute floor removed them entirely, and that same Uniswap pair fell to 628 wallets at a 2h 14m median, which is a number that behaves like a person deciding something.&lt;/p&gt;

&lt;p&gt;The other thing the run says plainly: 58% of the surviving hops still touch an &lt;code&gt;Unknown Proxy&lt;/code&gt; on one side. Journey reconstruction inherits whatever the labeling layer knows, and on this window the largest single destination in DEX navigation is a contract nobody has named yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does not claim
&lt;/h2&gt;

&lt;p&gt;The table above is one query execution on one chain over one week, which is enough to demonstrate the method and not enough to characterise anybody's product. A single seven-day window on Ethereum says nothing about seasonality, nothing about the other three chains, and nothing about any named venue's quality.&lt;/p&gt;

&lt;p&gt;Two more limits worth stating. Entrypoint resolution inherits whatever the chain's decoded contract set knows, so a genuinely unresolved router stays &lt;code&gt;Unknown Proxy&lt;/code&gt; and appears in the map as an unknown node, which on this run is most of the map. And the bot guards are heuristic thresholds, not a classifier: a swap-count cap and a minimum dwell remove wallets whose behaviour is implausible for a person, and they will also drop the fastest legitimate power users along the way. Every threshold is a query parameter, so a reader can move them and watch the answer change, which is why they are exposed at all. The 40% figure above is exactly what moving one of them cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this generalises
&lt;/h2&gt;

&lt;p&gt;The technique is about ordering, not about blockchains. Wherever records exist but the sequence connecting them does not (wallets, users, devices, transactions), the same window-function pass reconstructs it, and the same three traps decide whether the resulting funnel describes customers or machines. Collapse the rows that represent one action, exclude the automated actors, and keep separate timelines separate. The reconstruction is the easy half.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Andrew Maury is the founder of &lt;a href="https://rantum.xyz" rel="noopener noreferrer"&gt;Rantum&lt;/a&gt;, a senior data science and ML studio that turns messy, fragmented, and adversarial data into models, APIs, and products that ship. Recent work: &lt;a href="https://cleartracedata.com" rel="noopener noreferrer"&gt;ClearTrace&lt;/a&gt; (neutral on-chain execution intelligence) and AddressIntel (predictive real-estate intelligence).&lt;/em&gt;&lt;/p&gt;

</description>
      <category>datascience</category>
      <category>sql</category>
      <category>blockchain</category>
      <category>analytics</category>
    </item>
    <item>
      <title>The Dashboard That Lied: Why DeFi Volume Isn't the Same as Adoption</title>
      <dc:creator>Andrew Maury</dc:creator>
      <pubDate>Tue, 28 Jul 2026 14:36:39 +0000</pubDate>
      <link>https://dev.to/andrewmaury/the-dashboard-that-lied-why-defi-volume-isnt-the-same-as-adoption-2l6c</link>
      <guid>https://dev.to/andrewmaury/the-dashboard-that-lied-why-defi-volume-isnt-the-same-as-adoption-2l6c</guid>
      <description>&lt;p&gt;&lt;em&gt;This is an adapted excerpt from the opening of my new book, &lt;a href="https://www.amazon.com/dp/B0H9GF88GR" rel="noopener noreferrer"&gt;The DeFi Data Quick-Start: Reading On-Chain Truth with Dune Analytics&lt;/a&gt;. If it makes you want to run the queries yourself, that is the whole idea.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The dashboard that lied
&lt;/h2&gt;

&lt;p&gt;Picture a scenario that plays out somewhere in DeFi every quarter.&lt;/p&gt;

&lt;p&gt;The foundation behind an L2 launches an incentive program: trade on our network, earn rewards proportional to your volume. Six weeks in, the growth dashboard looks spectacular. Daily DEX volume has tripled, unique trading wallets are up 40%, and someone drafts a blog post about "organic adoption."&lt;/p&gt;

&lt;p&gt;Then someone on the data team pulls the underlying trades. A third of the new volume traces back to a few dozen wallets trading the same two tokens back and forth with each other, dozens of times an hour, at sizes calibrated to just clear the rewards threshold. Another slice is bot flow whose activity scales with volume mechanically. Much of what remains routes through aggregators, so nobody can actually say which frontend, if any, deserves credit for bringing the user.&lt;/p&gt;

&lt;p&gt;The chart was not wrong. Every transaction on it really happened, and every one is publicly verifiable by anyone with a block explorer. The chart was still a lie, because "volume" was standing in for "adoption," and on-chain those are very different things.&lt;/p&gt;

&lt;h2&gt;
  
  
  The transparency paradox
&lt;/h2&gt;

&lt;p&gt;Blockchain data is strange: it is at once the most transparent dataset in finance and one of the easiest to misread.&lt;/p&gt;

&lt;p&gt;In traditional markets, data arrives pre-interpreted. An exchange reports volume through regulated channels, an auditor signs the books, a vendor cleans and labels the feed before you see it. You trust a chain of institutions, and in exchange the numbers come with meaning attached.&lt;/p&gt;

&lt;p&gt;On-chain, the trust model is inverted. You do not have to trust anyone, because every transaction is public, permanent, and independently verifiable. But nothing arrives labeled. The chain records what happened with perfect fidelity and says almost nothing about why it happened or who was really behind it. It is a flawless court stenographer that transcribes every word and identifies none of the speakers.&lt;/p&gt;

&lt;p&gt;The specific gap that matters: when a swap executes on a decentralized exchange, no standard field announces where the trade came from. Nothing says "this user came from the Uniswap web app" or "this was a wallet's built-in swap feature" or "this is a market-making bot talking directly to the contract." Everything except the raw movement of tokens has to be inferred.&lt;/p&gt;

&lt;p&gt;Skip that inference and just count, and three kinds of flow masquerade as adoption.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wash trading&lt;/strong&gt; is a trade with yourself: one entity controls both sides and swaps an asset back and forth to be counted trading, not to trade. Anywhere volume is rewarded, and round-tripping costs a fraction of a cent, washing becomes a yield strategy and people industrialize it. A volume counter cannot tell it from real demand, because it counts every leg the same.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Proxy routing&lt;/strong&gt; is subtler, because the trades are usually real. Most DEX flow passes through aggregators, routers, wallet swap buttons, and custom proxy contracts before it lands in a pool. A naive dashboard sees the router as the trader, credits the wrong layer, and can tally the same order two or three times as it moves through the stack. The interface that actually brought the user is often invisible on-chain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MEV-farm flow&lt;/strong&gt; is the perverse one. In a sandwich attack a bot buys right before a user's swap, lets the user execute at a worse price, and sells right after. Three transactions land where there was one real trade. A single instance of a user being exploited registers as a tripling of activity, so a venue with heavy sandwich activity shows inflated volume precisely because its users are getting worse execution.&lt;/p&gt;

&lt;h2&gt;
  
  
  What neutral measurement actually requires
&lt;/h2&gt;

&lt;p&gt;If dashboards can be this wrong on data this transparent, honest measurement takes four commitments. They sound obvious. Few published DeFi metrics survive all four.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Independence from the measured party.&lt;/strong&gt; The methodology is not controlled by anyone with a stake in the result. That is the difference between an audit and a press release.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stated, reproducible methodology.&lt;/strong&gt; Which trades counted, which were excluded and why, how origin was assigned, what the denominator is. On-chain, anyone can re-run the query and check your work, so faith is not required.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The same ruler for everyone.&lt;/strong&gt; One standard across every venue, even when the result is unflattering, measured against an external baseline rather than each venue's self-reported one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Honest denominators.&lt;/strong&gt; State the base rate and pick the denominator before seeing which one tells the better story.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the real sense in which on-chain data is different. Not that it is harder to misread than other data, but that for once you can check. The book exists to make you the person who checks.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the book does with it
&lt;/h2&gt;

&lt;p&gt;The DeFi Data Quick-Start is short and hands-on. In about a hundred pages it goes from your first query against live DEX trades to a dashboard you can defend: navigating the data stack from raw nodes to curated tables, attributing volume to the frontend that actually originated it, detecting sandwich attacks from in-block ordering, and measuring execution quality against a neutral price baseline. Every query in it was run against live on-chain data. The methodology draws on years building data infrastructure at 0x Labs and on ClearTrace, the execution-intelligence system we built to attribute DEX flow across Ethereum, Base, Arbitrum, and Optimism.&lt;/p&gt;

&lt;p&gt;If you want to stop taking on-chain numbers at face value, that is what it is for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The DeFi Data Quick-Start: Reading On-Chain Truth with Dune Analytics&lt;/strong&gt; is &lt;a href="https://www.amazon.com/dp/B0H9GF88GR" rel="noopener noreferrer"&gt;on Amazon&lt;/a&gt;, and more is on the &lt;a href="https://www.amazon.com/author/andrewmaury" rel="noopener noreferrer"&gt;author page&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Andrew Maury is the founder of &lt;a href="https://rantum.xyz" rel="noopener noreferrer"&gt;Rantum&lt;/a&gt;, a senior data science and ML studio that turns messy, fragmented, and adversarial data into models, APIs, and products that ship.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>ethereum</category>
      <category>datascience</category>
      <category>dune</category>
    </item>
    <item>
      <title>What DEX Volume Actually Measures — and Why Attribution Has to Come First</title>
      <dc:creator>Andrew Maury</dc:creator>
      <pubDate>Fri, 24 Jul 2026 12:00:39 +0000</pubDate>
      <link>https://dev.to/andrewmaury/what-dex-volume-actually-measures-and-why-attribution-has-to-come-first-1h3h</link>
      <guid>https://dev.to/andrewmaury/what-dex-volume-actually-measures-and-why-attribution-has-to-come-first-1h3h</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://rantum.xyz/research/cross-frontend-attribution.html" rel="noopener noreferrer"&gt;rantum.xyz&lt;/a&gt;, which is the canonical source. The full preprint is on Zenodo, &lt;a href="https://doi.org/10.5281/zenodo.21513262" rel="noopener noreferrer"&gt;DOI 10.5281/zenodo.21513262&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Decentralized exchange volume is one of the most-cited numbers in crypto. Liquidity-mining programs pay against it. Layer-2 foundations report it as adoption. Grant committees read it as evidence a product is being used. It is treated as a proxy for real people doing real trades.&lt;/p&gt;

&lt;p&gt;It is a weak proxy, for a structural reason: the trade-execution infrastructure everyone shares exposes no standard way for a frontend to announce who it is on-chain. A wallet-native swap widget, an aggregator, an institutional smart contract, and a meta-router can all produce the same kind of settled trade, and none is required to leave a signature saying which one it was. Once volume is not tied back to a true origin, it is trivially inflated — by wash trading, proxy routing, and MEV-adjacent flow — and any incentive program built on top of it rewards the wrong behavior.&lt;/p&gt;

&lt;p&gt;I wrote a preprint on the attribution methodology we built for &lt;a href="https://cleartracedata.com" rel="noopener noreferrer"&gt;ClearTrace&lt;/a&gt;, a neutral third-party execution-intelligence system covering Ethereum, Base, Arbitrum, and Optimism. This is the plain-language version; the full methodology, validation, and limitations are in the paper.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem stated plainly
&lt;/h2&gt;

&lt;p&gt;If you cannot say &lt;em&gt;which frontend&lt;/em&gt; produced a trade, you cannot say whether the volume is organic. And you usually cannot say which frontend produced a trade, because that information is not natively recorded anywhere. Attribution is the missing first step every downstream number quietly depends on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four independent heuristics
&lt;/h2&gt;

&lt;p&gt;The paper describes four ways to recover frontend origin from raw transaction and trace data. They are deliberately independent — each fails on different trades, so where several agree the signal is strong, and where they disagree you have found something worth a closer look.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Calldata-suffix trapping&lt;/strong&gt; — many frontends append an identifying tag to the end of the transaction calldata. Where it exists, it is the cleanest signal available.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proxy-router mismatch detection&lt;/strong&gt; — when the contract that appears to send a trade is not the one that actually originated it, the mismatch itself is evidence of routing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-hop origin tracing&lt;/strong&gt; — following a trade back through intermediary contracts to the interface a user actually touched, rather than stopping at the last hop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fee-recipient clustering&lt;/strong&gt; — grouping trades by where the fee ends up, which tends to cluster by frontend even when nothing else does.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No single heuristic is trusted on its own. The point is the ensemble.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attribution first, then execution quality — kept separate
&lt;/h2&gt;

&lt;p&gt;On top of the attribution layer sits an execution-quality layer that benchmarks the price a trade actually got against a volume-weighted average price (VWAP) oracle. That quote-accuracy score is kept deliberately separate from a sandwich/MEV-exposure score. They answer different questions — "did you get a fair price?" and "were you exposed to predatory ordering?" — and blending them into one number hides more than it reveals.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the paper reports
&lt;/h2&gt;

&lt;p&gt;Rather than assert the method works, the preprint reports what happened running it in production:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Validation from an in-production quote-accuracy sampler, with 112 of 168 tracked cells reaching statistical significance over a 9.6-day window.&lt;/li&gt;
&lt;li&gt;A worked hypothesis reversal: a major aggregator that looked strong until additional data revealed order-size-dependent quote degradation.&lt;/li&gt;
&lt;li&gt;A structural finding on Arbitrum: the near-zero sandwich-attack count there is genuine structural immunity, not a coverage gap, confirmed by reading detector active-days alongside the raw victim count and consistent with Arbitrum's first-come-first-served private sequencing. It is a distinction that is easy to get backwards.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also states the limitation plainly: this is single-team validation, and the paper argues for treating cross-frontend attribution as a first-class, auditable input to incentive design rather than an afterthought bolted onto a volume dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read the paper
&lt;/h2&gt;

&lt;p&gt;The full preprint — methodology, validation, and limitations — is on Zenodo under &lt;a href="https://doi.org/10.5281/zenodo.21513262" rel="noopener noreferrer"&gt;DOI 10.5281/zenodo.21513262&lt;/a&gt;, licensed CC-BY 4.0. The abstract and citation live at &lt;a href="https://rantum.xyz/research/cross-frontend-attribution.html" rel="noopener noreferrer"&gt;rantum.xyz/research&lt;/a&gt;, and the methodology is deployed in production at &lt;a href="https://cleartracedata.com" rel="noopener noreferrer"&gt;cleartracedata.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>ethereum</category>
      <category>datascience</category>
      <category>web3</category>
    </item>
    <item>
      <title>Naming the “Unknown Proxy”: On-Chain Entity Resolution With a Cheapest-First Ladder</title>
      <dc:creator>Andrew Maury</dc:creator>
      <pubDate>Wed, 22 Jul 2026 16:08:47 +0000</pubDate>
      <link>https://dev.to/andrewmaury/naming-the-unknown-proxy-on-chain-entity-resolution-with-a-cheapest-first-ladder-3cch</link>
      <guid>https://dev.to/andrewmaury/naming-the-unknown-proxy-on-chain-entity-resolution-with-a-cheapest-first-ladder-3cch</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fopr3fdc8m4fz9bcmcp17.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fopr3fdc8m4fz9bcmcp17.png" width="799" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On-chain attribution has a stubborn tail. You can trace DEX volume back to the contract that carried it, and still be left staring at the most important rows labeled as nothing more useful than Unknown Proxy (0x…). Those anonymous entries are the flow that matters most: institutional desks, custom enterprise frontends, and wallet-native swap extensions. This is how ClearTrace turns them into named entities, cheaply, without guessing, and without burning paid API credits on answers a free source already holds.&lt;/p&gt;

&lt;h4&gt;
  
  
  The challenge
&lt;/h4&gt;

&lt;p&gt;ClearTrace attributes DEX volume to the frontend that originated each trade, across Ethereum, Base, Arbitrum, and Optimism. But a single unnamed proxy can sit near the top of a chain’s volume table and quietly distort the entire picture of who is actually transacting.&lt;/p&gt;

&lt;p&gt;No single data source names them all. Etherscan misses verifications that Sourcify holds. A proxy hides the implementation that carries its real identity. The same router, redeployed byte-for-byte across four chains, looks like four unrelated unknowns. Paid entity APIs would name a chunk of it, but running them across every unknown, on every sync, burns credits fast.&lt;/p&gt;

&lt;p&gt;The hard part is resolving each address to the best available label without paying for answers a free source already holds.&lt;/p&gt;

&lt;h4&gt;
  
  
  What we built: a resolution ladder
&lt;/h4&gt;

&lt;p&gt;For each unknown contract, ClearTrace walks a fixed sequence of sources ordered cheapest-and-most-authoritative first, stopping at the first real name. Roughly a dozen rungs, from a free eth_call to paid entity APIs tried last. The order itself does most of the work: by the time an address reaches a paid source, the free rungs have already named the vast majority of volume.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ground truth first: the deployer’s own list.&lt;/strong&gt; The single highest authority is a protocol publishing its own deployment addresses. This rung was added after a hard lesson: a contract’s &lt;em&gt;absence&lt;/em&gt; from a third party’s decoded set is not evidence that it is unofficial. When the deployer publishes the addresses, that file is the answer, and it outranks every heuristic below it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On-chain registries.&lt;/strong&gt; Some protocols track their own deployments in on-chain registries. A single free eth_call (for example, an ownerOf against the 0x Settler deployer's ERC-721) returns the currently active contract for a feature. Authoritative, no third-party API.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Free local and public sources.&lt;/strong&gt; Intel tables already in the local database, then the Etherscan v2 multichain API, then the keyless public mirrors Sourcify and Blockscout, which frequently hold verifications Etherscan does not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Structural resolution.&lt;/strong&gt; When no directory has a name, the bytecode does. Identical normalized runtime bytecode to an already-named contract on &lt;em&gt;any&lt;/em&gt; chain inherits its label. Proxies are followed to their implementation via the EIP-1167 minimal-proxy target or the EIP-1967 implementation and beacon storage slots, and the proxy inherits that name.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deployer-graph attribution.&lt;/strong&gt; Still-unnamed contracts are attributed through their creator: a named deployer EOA, its ENS primary name, or the majority label of sibling contracts the same deployer already shipped.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Paid sources, last and gated.&lt;/strong&gt; Arkham (only when a session is configured) and Nansen (paid credits) sit at the bottom. Nansen halts for the rest of the run the moment it reports credit exhaustion, and a final pass batches every still-unnamed address into &lt;em&gt;one&lt;/em&gt; Dune labels.contracts execution, never one call per address.&lt;/p&gt;

&lt;h4&gt;
  
  
  Cross-chain identity
&lt;/h4&gt;

&lt;p&gt;Two facts collapse a lot of duplicate unknowns into single known entities.&lt;/p&gt;

&lt;p&gt;First, teams deploy byte-identical routers across chains, so ClearTrace fingerprints each contract as a SHA-256 of its runtime bytecode with the Solidity CBOR metadata trailer stripped. That trailer differs between otherwise-identical builds, so removing it lets one labeled instance name its twins everywhere.&lt;/p&gt;

&lt;p&gt;Second, the same key controls an address on every EVM chain, so a mainnet ENS primary name identifies a wallet cross-chain. The pipeline also recognizes EIP-7702 delegated EOAs by their 0xef0100 designator and labels them through their delegate, so a delegated wallet never masquerades as a high-volume "Unknown Proxy" whale.&lt;/p&gt;

&lt;h4&gt;
  
  
  Refusing to guess
&lt;/h4&gt;

&lt;p&gt;A wrong label is worse than an honest “Unknown.” Every rung is filtered against a blocklist of meaningless names: generic activity tags like “High Activity,” and proxy wrapper class names like TransparentUpgradeableProxy or ERC1967Proxy that describe the shape of a contract, not its owner. Masking those forces the ladder to fall through to a rung that resolves the actual implementation instead of propagating a name that says nothing. Each label also carries a confidence tier and is re-checked on a fixed interval, so a name from a weaker rung is replaced when a more authoritative source later resolves the same address.&lt;/p&gt;

&lt;h4&gt;
  
  
  Spending the effort where the volume is
&lt;/h4&gt;

&lt;p&gt;Unknown volume is heavily concentrated in a few large addresses, so the pipeline ranks unresolved contracts globally by attributed volume and resolves the top of that list each sync, rather than sweeping every address round-robin. It is the cheapest way to move the most attributed volume from “Unknown” to named per run. In a recent snapshot of the label store, 259 of the resolved contracts carried entity names, and about 89% of those (231 of 259) were named by the free and structural rungs before any live paid lookup to Arkham, Nansen, or the batched Dune call.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Rank still-unnamed contracts by the volume they carry, across all chains,&lt;/span&gt;
&lt;span class="c1"&gt;-- so the resolution ladder spends its effort (and any paid rungs) on the&lt;/span&gt;
&lt;span class="c1"&gt;-- addresses that actually distort attribution.&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;resolved_contract_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;blockchain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;total_volume_usd&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;dune_attribution&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resolved_contract_name&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'Unknown%'&lt;/span&gt;
       &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;resolved_contract_name&lt;/span&gt; &lt;span class="n"&gt;GLOB&lt;/span&gt; &lt;span class="s1"&gt;'0x[0-9a-fA-F]*'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;blockchain&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;total_volume_usd&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  What it proves
&lt;/h4&gt;

&lt;p&gt;Any one vendor names part of the on-chain world. The value is in the ladder that combines authoritative, free, and paid sources in the right order, resolves structurally when none of them answer, and knows when to stop. For a foundation, grant program, or protocol team, that is the difference between “a large unknown near the top of the table” and a named entity you can actually reason about. It runs today inside ClearTrace’s live dashboard and public API at &lt;a href="https://cleartracedata.com" rel="noopener noreferrer"&gt;cleartracedata.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Andrew Maury is the founder of Rantum, a senior data science &amp;amp; ML studio that turns messy, fragmented, and adversarial data into models, APIs, and products that ship. Recent work: ClearTrace (neutral on-chain execution intelligence) and AddressIntel (predictive real-estate intelligence). More at&lt;/em&gt; &lt;a href="https://andrewmaury.com" rel="noopener noreferrer"&gt;&lt;em&gt;andrewmaury.com&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>dataengineering</category>
      <category>ethereum</category>
      <category>blockchain</category>
      <category>defi</category>
    </item>
    <item>
      <title>Designing a Point-in-Time Backtest You Can Actually Trust</title>
      <dc:creator>Andrew Maury</dc:creator>
      <pubDate>Mon, 20 Jul 2026 22:06:24 +0000</pubDate>
      <link>https://dev.to/andrewmaury/designing-a-point-in-time-backtest-you-can-actually-trust-10ek</link>
      <guid>https://dev.to/andrewmaury/designing-a-point-in-time-backtest-you-can-actually-trust-10ek</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://rantum.xyz/case-studies/point-in-time-backtest.html" rel="noopener noreferrer"&gt;rantum.xyz&lt;/a&gt;, which is the canonical source.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Almost any predictive model can be made to look good on historical data. The hard part is a backtest that reports what was actually knowable at decision time, so the number survives contact with production. Here is the discipline we hold every model to at Rantum, and the worked example where it walked a flattering 5.3x result down to an honest 2x.&lt;/p&gt;

&lt;h2&gt;
  
  
  The challenge
&lt;/h2&gt;

&lt;p&gt;Every predictive product is sold on a backtest: "our model would have caught X% of these events in advance." The trouble is that a backtest is the easiest thing in machine learning to fake, usually by accident. Read a feature that was only knowable after the fact, shuffle your train and test sets at random, or report accuracy on an event that almost never happens, and you can produce an impressive number that means nothing. The model then ships, and the edge that looked real on paper quietly disappears in production.&lt;/p&gt;

&lt;p&gt;The gap between a flattering backtest and a defensible one is not a modeling trick. It is a set of disciplines about &lt;em&gt;time&lt;/em&gt;: what was knowable, when, and whether the test respects that. Below is the checklist we run on every predictive engagement, and a worked example, a teardown-probability model inside &lt;a href="https://addressintel.co" rel="noopener noreferrer"&gt;AddressIntel&lt;/a&gt;, our real-estate intelligence platform, where following it changed the answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we built
&lt;/h2&gt;

&lt;p&gt;A repeatable point-in-time backtesting discipline, applied here to predicting which aging houses on the San Francisco Peninsula get demolished and rebuilt. Teardowns are rare, well under 1% of parcels a year. The outcome is only visible in fragmented municipal permit records, and the features tempt you to cheat at every turn, which makes it a good stress test for a method meant to travel to any domain with rare events and messy history. Five rules do the work.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Score as-of T: features from data dated at or before T, nothing else
&lt;/h3&gt;

&lt;p&gt;Pick a set of historical scoring dates T, and at each one, build every feature using only information that existed on or before that date. This sounds obvious and is constantly violated, because the most predictive-looking features are exactly the ones that leak. A teardown &lt;em&gt;changes the structure&lt;/em&gt;: score a parcel as of 2018 but read its &lt;code&gt;year_built&lt;/code&gt;, square footage, or listing photos as they stand today, and every one of those attributes describes the &lt;em&gt;new&lt;/em&gt; house that replaced the old one. The feature would silently encode the answer. Only dated history, assessment rolls, sales, and permits, each stamped with the year it was true, reconstructs cleanly as-of T. Enforcing this disqualifies most of the obvious features before a single metric is computed.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Time-based folds, never random cross-validation
&lt;/h3&gt;

&lt;p&gt;Standard k-fold cross-validation shuffles rows at random into train and test sets. For anything with a time dimension, that is leakage by construction: the model trains on rows from 2020 and is tested on rows from 2018, learning from a future it would not have had. Pool observations across several as-of dates instead, and split on time: fit on T at or before 2018, test on T = 2020. Use enough distinct dates, we like six or more, that no single lucky window can carry the result, and report the metrics per fold so temporal instability shows up instead of hiding in an average.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Lead with the base rate; measure lift, not accuracy
&lt;/h3&gt;

&lt;p&gt;When positives are rare, accuracy is worse than useless: a model that predicts "no teardown, ever" is 99%+ accurate and completely worthless. The honest frame is the base rate first, then &lt;strong&gt;lift&lt;/strong&gt; (how much richer the top-ranked slice is than chance), alongside precision-at-k, capture (recall) at k, and the area under the precision-recall curve. Because positives are few, bootstrap a confidence interval on every figure so you don't over-read noise.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Rare positives (teardowns run &amp;lt;1% of parcels): accuracy is a trap —
# "never a teardown" scores 99%+. Lead with base rate, then lift.
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;observations&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;        &lt;span class="c1"&gt;# observations: (features, label), each built as-of T
&lt;/span&gt;    &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;observations&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;              &lt;span class="c1"&gt;# e.g. 0.0072
&lt;/span&gt;    &lt;span class="n"&gt;ranked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;observations&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt; &lt;span class="n"&gt;reverse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ranked&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;                                         &lt;span class="c1"&gt;# top decile
&lt;/span&gt;    &lt;span class="n"&gt;precision_at_k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ranked&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;precision_at_k&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;                           &lt;span class="c1"&gt;# lift is the headline
&lt;/span&gt;
&lt;span class="c1"&gt;# Pool across as-of dates T with TIME-based folds — fit on T &amp;lt;= 2018,
# test on T = 2020. Random k-fold shuffles future rows into training: leakage.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. The label must be independent of the feature it validates
&lt;/h3&gt;

&lt;p&gt;This is the rule that saved the model from shipping a lie. The first teardown label was a cheap proxy: California's Proposition 13 caps assessed-value growth except on new construction, so a teardown-rebuild shows up as a sharp jump in a parcel's &lt;em&gt;building&lt;/em&gt; value. Scored against that label, a single heuristic (sort parcels by their land-to-improvement value ratio) looked spectacular: a 5.3x lift at the top decile, capturing over half of all "teardowns," on 17,589 observations at a 0.72% base rate. It read as an undefeated baseline.&lt;/p&gt;

&lt;p&gt;It was an artifact. The proxy label &lt;em&gt;is&lt;/em&gt; a building-value spike, and the winning feature &lt;em&gt;is&lt;/em&gt; land divided by building value. Both are functions of the same number, so scoring by the ratio was, in effect, scoring against a shadow of the label it was supposed to predict. We caught it two ways. First, a direct cross-validation: of 140 proxy-flagged rebuilds, exactly one had a confirming demolition or new-build permit at the same parcel. Second, rebuilding the label from real, independent data (actual San Jose demolition and new-single-family permits, keyed to the parcel number across a 36,896-parcel universe). On that clean, non-circular test the ratio's edge as a standalone score evaporated to no signal at all, though the underlying lot-size economics still carried a smaller, defensible signal (about 2x lift), which is the number the shipped model stands on.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;5.3x&lt;/strong&gt; lift on the circular proxy label, then &lt;strong&gt;0x&lt;/strong&gt; for the same feature against an independent label.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The rule generalizes past this one case: whenever a label is derived from the same source as a feature (a churn label inferred from the activity you also feed the model, a fraud label built from the rules you're scoring against), the backtest measures arithmetic, not prediction. Build the label from a source the features can't see, or the number is a mirror.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Pre-register the go/no-go, and build a gate that lets you fail cheap
&lt;/h3&gt;

&lt;p&gt;Write down the bar for success before you look at any results, so you can't move the goalposts to fit the number you got. Ours here was concrete: on out-of-sample dates, a top-decile lift of at least 5x, &lt;em&gt;and&lt;/em&gt; a material margin over the best single heuristic (not within noise), &lt;em&gt;and&lt;/em&gt; a positive median lead time so the call is early enough to act on. Just as important is a cheap feasibility gate up front. Before running any metrics, we counted teardown positives in the candidate windows; the pre-set rule was that fewer than roughly 30 to 50 positives is dead on arrival. The first pass returned zero strict positives against a ceiling of 19, so the metrics were never run on noise, and the real work, assembling an independent permit label, was correctly identified as the project rather than a detour.&lt;/p&gt;

&lt;h3&gt;
  
  
  Backtest is not the same as the live model
&lt;/h3&gt;

&lt;p&gt;The final piece of the discipline is knowing what the backtest does and does not license. The leakage that disqualifies &lt;code&gt;year_built&lt;/code&gt;, square footage, and photos is a property of scoring a parcel that was &lt;em&gt;already rebuilt&lt;/em&gt; in the historical window. Score a house that is still standing today, and those same attributes describe the actual candidate structure: no leak, and they are strong features. So the backtest validates the economic and assessment layer under strict point-in-time rules, while the live product can honestly be richer than the backtest could ever prove. Conflating the two is how teams either cripple a live model with backtest-only caution or ship a backtest number the production features quietly violate.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it shows
&lt;/h2&gt;

&lt;p&gt;The unglamorous core of trustworthy predictive ML: reconstructing features as-of a decision date, splitting on time rather than at random, measuring rare events by lift instead of accuracy, keeping the label independent of what it validates, and pre-committing to a bar. None of it is exotic. All of it is routinely skipped, because skipping it produces better-looking numbers. It is the same through-line as the rest of Rantum's work (finding real signal in messy data) turned inward on the measurement itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it proves to a client
&lt;/h2&gt;

&lt;p&gt;That we will tell you whether a model works before you bet a roadmap on it. This case study is the proof: a 5.3x result that looked ready to ship as a "proprietary teardown score" got walked back to a defensible 2x lot-size signal once a clean test was possible, and we would rather deliver the smaller true number than the larger false one. Anyone can produce a flattering backtest by leaking the future into the features; the value is in the rigor that refuses to. For a business sitting on fragmented, underused data, that is the difference between a predictive product you can defend to a customer and a demo that falls apart in production. The method transfers directly to any domain with rare outcomes and messy history: insurance, lending, fraud, marketplaces, logistics, churn.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Andrew Maury is the founder of &lt;a href="https://rantum.xyz" rel="noopener noreferrer"&gt;Rantum&lt;/a&gt;, a senior data science and ML studio that turns messy, fragmented, and adversarial data into models, APIs, and products that ship. Read the full case study at &lt;a href="https://rantum.xyz/case-studies/point-in-time-backtest.html" rel="noopener noreferrer"&gt;rantum.xyz&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>datascience</category>
      <category>python</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>From Fragmented Permit Data to a Teardown-Probability Model</title>
      <dc:creator>Andrew Maury</dc:creator>
      <pubDate>Thu, 09 Jul 2026 03:20:49 +0000</pubDate>
      <link>https://dev.to/andrewmaury/from-fragmented-permit-data-to-a-teardown-probability-model-344b</link>
      <guid>https://dev.to/andrewmaury/from-fragmented-permit-data-to-a-teardown-probability-model-344b</guid>
      <description>&lt;p&gt;&lt;em&gt;A building permit is the earliest signal that a house is about to be torn down and rebuilt. It is also some of the messiest data in real estate. This is how AddressIntel turns permits scattered across four municipal systems into a teardown-probability model — and, just as importantly, how it backtests that model without lying to itself.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv8z01v4x0k3kcrubi91i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv8z01v4x0k3kcrubi91i.png" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A teardown is worth knowing about early. A developer who can spot, a year or two ahead, which aging house on an outsized lot is likely to be demolished and rebuilt has a real edge over the market. The signal that precedes almost every teardown is a building permit: a demolition filing, then a new single-family-residence filing on the same parcel.&lt;/p&gt;

&lt;p&gt;The trouble is that permits are the messiest data in real estate. On the San Francisco Peninsula alone they are filed across four different portal technologies — Accela ACA (Palo Alto, Cupertino, Santa Clara), Tyler EnerGov CSS (San Mateo, San Carlos), eTRAKiT (Hillsborough, Atherton, Woodside, Burlingame), and San Jose’s open-data CKAN API. Each has its own schema, its own free-text vocabulary for what a permit &lt;em&gt;is&lt;/em&gt;, its own history depth, and no shared identifier that ties a permit to a specific parcel or listing. The hard part is not the model. It is making the underlying data joinable and trustworthy at all.&lt;/p&gt;

&lt;h4&gt;
  
  
  What we built
&lt;/h4&gt;

&lt;p&gt;A permit pipeline and a teardown-probability model inside &lt;a href="https://addressintel.co" rel="noopener noreferrer"&gt;AddressIntel&lt;/a&gt;, our predictive real-estate intelligence platform. The pipeline normalizes those four sources into one schema, resolves every record to a parcel, and classifies free-text permit types into a fixed taxonomy. On top of that sits the part most predictive PropTech skips: a label and a backtest built so the numbers can be trusted.&lt;/p&gt;

&lt;h4&gt;
  
  
  One schema, joined on the parcel APN
&lt;/h4&gt;

&lt;p&gt;The parcel Assessor’s Parcel Number (APN) is the join key that makes everything else possible. San Jose’s open-data API carries the APN natively on every permit, along with clean fields for permit type, issue and final dates, valuation, and contractor — the highest-volume market, unlocked with a free JSON call rather than a brittle scrape. For the Selenium-scraped portals, the APN is recovered from the assessor record and used to stitch permits, sales, and listings onto a single parcel timeline. Permit type descriptions, which range from "NEW SINGLE FAMILY RESIDENCE" to "Single Family (New) - New" to a bare "Demolition", are classified into a stable taxonomy (demolition, new construction, addition, remodel, roof, ADU, and so on) so a query means the same thing in every city.&lt;/p&gt;

&lt;h4&gt;
  
  
  A label that doesn’t cheat
&lt;/h4&gt;

&lt;p&gt;The first version of the label was a proxy: California’s Proposition 13 caps assessed-value growth except on new construction, so a teardown-rebuild shows up as a sharp jump in a parcel’s &lt;em&gt;building&lt;/em&gt; value. It was cheap to compute and looked plausible. We cross-validated it against real permits anyway — and it failed. Of 140 proxy-flagged rebuilds, exactly one had a confirming demolition or new-build permit at the same APN. The proxy was catching ownership resets and additions, not teardowns.&lt;/p&gt;

&lt;p&gt;So the real label comes from the permits themselves: a parcel is a teardown if it has a demolition or new-single-family permit in the forward window. Because that label is built from permits rather than assessed value, it is &lt;em&gt;independent&lt;/em&gt; of the model’s strongest feature (the land-to-improvement value ratio) — which means the backtest measures prediction, not an artifact of the label sharing arithmetic with the feature.&lt;/p&gt;

&lt;h4&gt;
  
  
  Point-in-time, or it means nothing
&lt;/h4&gt;

&lt;p&gt;A teardown changes the structure, and that is the whole trap. Score a parcel as of 2018, but read its year_built, square footage, or listing photos today, and every one of those attributes describes the &lt;em&gt;new&lt;/em&gt; house that replaced it. The features would silently encode the answer. This is the point-in-time leakage principle, and it disqualifies most of the obvious features from the backtest: only dated history — assessment rolls, sales, and permits, each stamped with the year it was true — is clean.&lt;/p&gt;

&lt;p&gt;The label construction, reduced to its essentials, makes the discipline explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Point-in-time teardown label from real permits — no lookahead, no leakage
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2016&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2018&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2020&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="c1"&gt;# historical scoring dates
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;parcel&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;universe&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;roll&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;assessment_asof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parcel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# land/improvement split dated &amp;lt;= T
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;roll&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;

        &lt;span class="n"&gt;permit_years&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;teardown_permits&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;parcel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;apn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="c1"&gt;# DEMO or NEW-SFR at this APN
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;permit_years&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt; &lt;span class="c1"&gt;# already torn down by T — drop it
&lt;/span&gt;
        &lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;permit_years&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

        &lt;span class="c1"&gt;# Features use ONLY dated history (assessed values, sales, permits).
&lt;/span&gt;        &lt;span class="c1"&gt;# year_built, house sqft, and photos describe the *rebuilt* house — they leak.
&lt;/span&gt;        &lt;span class="n"&gt;observations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nf"&gt;point_in_time_features&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;roll&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  What the honest number turned out to be
&lt;/h4&gt;

&lt;p&gt;The first run, scored on the proxy label before it was cross-validated, looked like a strong result: 17,589 parcel-date observations across three scoring dates, a 0.72% base rate, and a single heuristic (sort parcels by land-to-improvement value ratio) capturing about half of all “teardowns” in the top decile, a 5.3x lift. A hand-blended model, land value alone, and a cheapest-structure heuristic all trailed it. It looked like an undefeated baseline.&lt;/p&gt;

&lt;p&gt;It wasn’t. Once the label was rebuilt from real permits, a well-powered, non-circular test on 36,896 San Jose parcels and 266 confirmed teardowns, the ratio’s edge evaporated: no signal, arguably inverted, on the low-power sample (11 positives) where it could be checked. The 5.3x had been an artifact of the proxy label sharing arithmetic with the ratio. A building-value spike triggers both, so scoring by the ratio was, in effect, scoring against a shadow of the label it was supposed to predict.&lt;/p&gt;

&lt;p&gt;(You can explore this reversal yourself in the interactive chart on the&lt;a href="https://rantum.xyz/case-studies/teardown-probability-model.html" rel="noopener noreferrer"&gt;live case study&lt;/a&gt;: the proxy-label and clean permit-label backtests side by side, toggle-able by scorer.)&lt;/p&gt;

&lt;p&gt;The one signal that held up on the clean test was plainer than any of the discarded ones: parcels with an outsized lot for the neighborhood carry a real, if modest, 2.0x lift, well-powered at 266 positives. Maintenance-permit history, hoped to be a clean negative signal (an owner investing in a structure they intend to keep), added nothing blended with lot size. Neighborhood-contagion, tenure, and assessment-growth features, tested earlier against the still-trusted proxy label, had already added nothing or diluted the ratio; none of them got a second look once the ratio itself stopped being trustworthy.&lt;/p&gt;

&lt;p&gt;That is a result, not a failure, and a sharper one than the model simply beating its baselines would have been. A backtest that would have shipped as a “proprietary 5x teardown score” turned out, on closer inspection, to be measuring its own label. The honest output is a much smaller claim (an outsized lot is worth about 2x) and an explicit rule against overselling it. It also clarified where a real edge would have to come from if one exists at all: explicit seller intent in listing language (“value in the land,” “as-is,” “contractor special”) remains untested and is the cheapest next thing to try. Just as important, it clarified the line between the backtest and the live product: physical features like lot coverage and structure age leak when backtesting a rebuilt parcel, but are clean and strong when scoring a house that is still standing today.&lt;/p&gt;

&lt;h4&gt;
  
  
  What it shows
&lt;/h4&gt;

&lt;p&gt;The hard, unglamorous discipline underneath predictive ML: unifying fragmented public records onto a single joinable spine, engineering a label that is independent of the feature it validates, and enforcing point-in-time hygiene so a backtest reports what was actually knowable at scoring time. It is the same through-line as ClearTrace — finding signal in messy, real-world data — applied to civic records instead of on-chain flow.&lt;/p&gt;

&lt;h4&gt;
  
  
  What it proves to a client
&lt;/h4&gt;

&lt;p&gt;That we will tell you whether a model works before you bet a roadmap on it. This piece is itself the proof: an early 5x number looked good enough to ship, and got walked back to a smaller, defensible 2x once a cleaner test was possible. Anyone can produce a flattering backtest by leaking the future into the features; the value is in the rigor that refuses to. For a business sitting on fragmented, underused data, that means the difference between a predictive product you can defend to a customer and a demo that quietly falls apart in production. The same capability transfers to any domain with messy records and a hard-to-measure outcome: insurance, lending, marketplaces, logistics.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Andrew Maury is the founder of Rantum, a senior data science &amp;amp; ML studio that turns messy, fragmented, and adversarial data into models, APIs, and products that ship. Recent work:&lt;/em&gt; &lt;a href="https://cleartracedata.com" rel="noopener noreferrer"&gt;&lt;em&gt;ClearTrace&lt;/em&gt;&lt;/a&gt; &lt;em&gt;(on-chain execution intelligence) and&lt;/em&gt; &lt;a href="https://addressintel.co" rel="noopener noreferrer"&gt;&lt;em&gt;AddressIntel&lt;/em&gt;&lt;/a&gt; &lt;em&gt;(predictive real-estate intelligence). More at&lt;/em&gt; &lt;a href="https://andrewmaury.com" rel="noopener noreferrer"&gt;&lt;em&gt;andrewmaury.com&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>proptech</category>
      <category>machinelearning</category>
      <category>dataengineering</category>
      <category>datascience</category>
    </item>
    <item>
      <title>AddressIntel: Turning Fragmented Permit Data into a Predictive Real-Estate Product</title>
      <dc:creator>Andrew Maury</dc:creator>
      <pubDate>Tue, 07 Jul 2026 16:26:01 +0000</pubDate>
      <link>https://dev.to/andrewmaury/addressintel-turning-fragmented-permit-data-into-a-predictive-real-estate-product-4h91</link>
      <guid>https://dev.to/andrewmaury/addressintel-turning-fragmented-permit-data-into-a-predictive-real-estate-product-4h91</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fikxyjc1nyl9edmz3p38h.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fikxyjc1nyl9edmz3p38h.jpeg" width="799" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Canonical version:&lt;/em&gt; &lt;a href="https://rantum.xyz/case-studies/address-intel.html" rel="noopener noreferrer"&gt;&lt;em&gt;rantum.xyz&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Predictive real-estate intelligence that fuses fragmented municipal permit records with multimodal AI to score teardown probability, flippability, and off-market opportunity.&lt;/p&gt;

&lt;h4&gt;
  
  
  The Challenge
&lt;/h4&gt;

&lt;p&gt;The most valuable real-estate signals are buried in the messiest data. Building permits, zoning changes, and code-enforcement records are filed across dozens of municipal systems — each with its own format, portal, and lag — and almost never connected to live listing data. By the time an off-market teardown or value-add opportunity shows up in a normal feed, the developers who watch permits already know.&lt;/p&gt;

&lt;p&gt;The hard part isn’t the model; it’s making the underlying data usable at all: fragmented, civic, unstructured, and constantly changing.&lt;/p&gt;

&lt;h4&gt;
  
  
  What We Built
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://addressintel.co" rel="noopener noreferrer"&gt;AddressIntel&lt;/a&gt; is a live real-estate intelligence platform covering the San Francisco Peninsula and Nantucket Island. A Python ingestion fleet, orchestrated on GitHub Actions, continuously pulls municipal permits and public listing data; the data is normalized, fused, and scored, then served through a B2B GraphQL API.&lt;/p&gt;

&lt;p&gt;It runs on a hybrid public-benefit model — a free public transparency layer over neighborhood development trends, funded by a monetized enterprise API for developers and institutional investors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;At a glance:&lt;/strong&gt; 10K+ sales records ingested, 3 predictive models in production, 2 live markets.&lt;/p&gt;

&lt;h4&gt;
  
  
  Predictive Scoring &amp;amp; Multimodal AI
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Teardown &amp;amp; flippability models&lt;/strong&gt;  — proprietary probability scores for teardown, flippability, and bidding-war likelihood, trained on fused permit + sales history. The permit data is the signal incumbents don’t have: a demolition permit filed six months before a property hits the market is invisible to anyone watching standard listing feeds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vision-based condition scoring&lt;/strong&gt;  — Google Gemini reads listing photos to score interior and exterior property condition automatically, turning unstructured images into a model feature. No manual tagging, no scraped review data — just the photos that already exist on every listing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Permit alpha&lt;/strong&gt;  — joining civic permit filings to listings surfaces off-market developer activity before it becomes visible in standard feeds. The latency between a permit filing and public awareness is weeks to months. That gap is the product.&lt;/p&gt;

&lt;h4&gt;
  
  
  Architecture
&lt;/h4&gt;

&lt;p&gt;The stack is built to be cheap to operate and easy to extend into new markets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pipeline:&lt;/strong&gt; Python ingestion fleet on GitHub Actions cron, normalizing multi-source civic and listing data, with Gemini handling unstructured vision and NLP tasks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend:&lt;/strong&gt; Firebase Data Connect (GraphQL over Cloud SQL Postgres) with Firebase Auth; Stripe-metered access for enterprise clients&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontend:&lt;/strong&gt; Next.js (App Router) + React on Firebase App Hosting, with Sentry edge error tracking&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;GitHub Actions as the orchestration layer keeps infra costs near-zero for a market-coverage product where freshness matters more than sub-second latency.&lt;/p&gt;

&lt;h4&gt;
  
  
  What It Shows
&lt;/h4&gt;

&lt;p&gt;Turning messy, fragmented public records into a predictive product — and crossing domains, from data science into real estate, with point-in-time ML discipline so the scores reflect what was knowable at the time, not hindsight.&lt;/p&gt;

&lt;p&gt;It’s the same through-line as &lt;a href="https://cleartracedata.com" rel="noopener noreferrer"&gt;ClearTrace&lt;/a&gt;: find the signal in data others write off as too messy to use, ship it as infrastructure. There it’s on-chain trace data. Here it’s municipal permit filings. The skill transfers.&lt;/p&gt;

&lt;h4&gt;
  
  
  What It Proves
&lt;/h4&gt;

&lt;p&gt;That you can find signal in a data-rich space incumbents underuse, and turn it into a monetizable product — pipeline, models, API, and billing included. The same capability transfers to any business sitting on fragmented or underused data: marketplaces, fintech, insurance, logistics.&lt;/p&gt;

&lt;p&gt;The question is never whether the data exists. It’s whether anyone has built the infrastructure to make it usable.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Andrew Maury is the founder of&lt;/em&gt; &lt;a href="https://rantum.xyz" rel="noopener noreferrer"&gt;&lt;em&gt;Rantum&lt;/em&gt;&lt;/a&gt;&lt;em&gt;, a senior data science &amp;amp; ML studio. He previously scaled data infrastructure at 0x Labs and contributed to Dune’s open-source Spellbook. AddressIntel is live at&lt;/em&gt; &lt;a href="https://addressintel.co" rel="noopener noreferrer"&gt;&lt;em&gt;addressintel.co&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>datascience</category>
      <category>realestate</category>
      <category>dataengineering</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Detecting MEV &amp; Sandwich Attacks On-Chain: A Practical Methodology</title>
      <dc:creator>Andrew Maury</dc:creator>
      <pubDate>Wed, 01 Jul 2026 14:41:42 +0000</pubDate>
      <link>https://dev.to/andrewmaury/detecting-mev-sandwich-attacks-on-chain-a-practical-methodology-2dbo</link>
      <guid>https://dev.to/andrewmaury/detecting-mev-sandwich-attacks-on-chain-a-practical-methodology-2dbo</guid>
      <description>&lt;p&gt;&lt;em&gt;Canonical version:&lt;/em&gt; &lt;a href="https://rantum.xyz/case-studies/mev-sandwich-detection.html" rel="noopener noreferrer"&gt;&lt;em&gt;rantum.xyz&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A practical methodology for separating adversarial extraction from normal execution cost — and why the naive approach gets it badly wrong.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fihz5djeorf4a90h38on8.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fihz5djeorf4a90h38on8.jpeg" width="800" height="462"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  The Challenge
&lt;/h4&gt;

&lt;p&gt;Sandwich attacks are the most common form of on-chain MEV against retail traders. A bot spots a pending swap in the mempool, inserts a frontrun buy just before it and a backrun sell just after — capturing the price impact the victim caused, at the victim’s expense. The victim’s trade executes at a worse price than expected, and the difference flows to the bot.&lt;/p&gt;

&lt;p&gt;The problem for anyone trying to measure this: sandwich attacks are invisible in standard analytics. They look like normal slippage. A $10,000 swap that gets sandwiched shows up in a dashboard as “executed at 42 bps of slippage” — indistinguishable from a large trade in a thin pool. Without a methodology that isolates the adversarial component, every slippage number is a mix of real execution cost and extraction, and you cannot act on either.&lt;/p&gt;

&lt;p&gt;This matters most to DEX aggregators, L2 foundations, and grant programs that are trying to evaluate which protocols actually protect their users — and which ones quietly route flow into sandwichable positions.&lt;/p&gt;

&lt;h4&gt;
  
  
  What We Built
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://cleartracedata.com" rel="noopener noreferrer"&gt;ClearTrace&lt;/a&gt;’s MEV detection layer covers Ethereum, Base, Arbitrum, and Optimism. It identifies victim swaps, quantifies the volume sandwiched over rolling 7-day windows, and reports MEV exposure as a distinct metric — kept separate from the execution quality score so they can be read independently.&lt;/p&gt;

&lt;h4&gt;
  
  
  The v1 Mistake: tx_hash Is Not Execution Order
&lt;/h4&gt;

&lt;p&gt;The first cut at sandwich detection tried to reconstruct in-block ordering directly from dex.trades, using tx_hash comparisons as a proxy for sequencer position — treating a lower hash as an earlier transaction. That logic is wrong: tx_hash is a cryptographic hash, not a sequence number. Sorting by hash produces an arbitrary permutation of transactions, not the order they were actually included in the block. A "sandwich" identified this way is noise.&lt;/p&gt;

&lt;p&gt;The correct source is Dune’s curated dex.sandwiched table, built from Dune's Spellbook detection macro — which uses actual evt_index (on-chain event position) to reconstruct frontrun → victim → backrun triples across every block. Switching to this source eliminated the false positives entirely.&lt;/p&gt;

&lt;h4&gt;
  
  
  The Detection Query
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;victims&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt;
        &lt;span class="n"&gt;blockchain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;block_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;token_pair&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;amount_usd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;tx_hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;tx_from&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;dex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sandwiched&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;block_time&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'7'&lt;/span&gt; &lt;span class="k"&gt;DAY&lt;/span&gt;
      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;blockchain&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ethereum'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'base'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'arbitrum'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'optimism'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;amount_usd&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="mi"&gt;100000000&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;enriched&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt;
        &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_attacks_7d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount_usd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_sandwiched_usd_7d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;blockchain&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;chain_attacks_7d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount_usd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;blockchain&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;chain_sandwiched_usd_7d&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;victims&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;enriched&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;block_time&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The window columns mean the 7-day KPIs are accurate across the full dataset — not just the 500-row page — so a per-chain breakdown shows the right totals without a second query.&lt;/p&gt;

&lt;h4&gt;
  
  
  Separating MEV from Slippage
&lt;/h4&gt;

&lt;p&gt;Slippage and MEV exposure are both costs, but they have different causes and different remedies. ClearTrace scores them separately. Execution quality compares each fill’s realized price against a 1-minute VWAP oracle — both legs valued explicitly against prices.usd, avoiding the circular-pricing trap of comparing against Dune's internally derived amount_usd.&lt;/p&gt;

&lt;p&gt;The benchmark across major aggregators:&lt;/p&gt;

&lt;p&gt;| Aggregator | Median slippage vs VWAP | MEV toxicity | Revert rate | | — -| — -| — -| — -| | CoW Swap | −0.1 bps | Zero | 0.0% | | 1inch | −0.4 bps | Low | 1.2% | | Odos | −0.6 bps | Medium | 2.1% |&lt;/p&gt;

&lt;p&gt;CoW Swap’s zero MEV toxicity is structural: its batch-auction settlement mechanism bypasses the public mempool entirely, making sandwich attacks impossible by design.&lt;/p&gt;

&lt;h4&gt;
  
  
  The L2 Finding: Structural Protection vs Coverage Gap
&lt;/h4&gt;

&lt;p&gt;Arbitrum and Optimism show near-zero sandwich counts compared to Ethereum. This is worth stating carefully — two very different things can produce near-zero counts: the detector runs and finds little (structural protection), or the detector has no data for the chain (a coverage gap that would be wrong to call protection).&lt;/p&gt;

&lt;p&gt;The verification checks active_days alongside sandwich_victims_30d. If the chain has ~30 active days but tiny victim counts, the detector is running and genuinely finding almost nothing — the Arbitrum case, consistent with its FCFS private-mempool sequencing that eliminates the pending mempool window bots need to frontrun.&lt;/p&gt;

&lt;h4&gt;
  
  
  What It Shows
&lt;/h4&gt;

&lt;p&gt;The ability to detect adversarial extraction in data that’s structured to hide it — and to distinguish a real signal from an absence of data. The v1 → v2 rewrite is the concrete example: identifying &lt;em&gt;why&lt;/em&gt; the wrong model was wrong (hash ≠ sequence) required understanding the underlying data structures deeply enough to know what to use instead.&lt;/p&gt;

&lt;h4&gt;
  
  
  What It Proves
&lt;/h4&gt;

&lt;p&gt;That you can build measurement infrastructure that holds up to scrutiny. ClearTrace’s MEV layer gives L2 foundations, DEX aggregators, and grant programs a neutral answer to a question their own analytics cannot answer honestly: how much of the slippage users pay is real execution cost, and how much is being extracted by bots?&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Andrew Maury is the founder of&lt;/em&gt; &lt;a href="https://rantum.xyz" rel="noopener noreferrer"&gt;&lt;em&gt;Rantum&lt;/em&gt;&lt;/a&gt;&lt;em&gt;, a senior data science &amp;amp; ML studio. He previously scaled data infrastructure at 0x Labs and contributed to Dune’s open-source Spellbook. ClearTrace is live at&lt;/em&gt; &lt;a href="https://cleartracedata.com" rel="noopener noreferrer"&gt;&lt;em&gt;cleartracedata.com&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ethereum</category>
      <category>mev</category>
      <category>web3</category>
      <category>defi</category>
    </item>
    <item>
      <title>ClearTrace: On-Chain Execution Intelligence</title>
      <dc:creator>Andrew Maury</dc:creator>
      <pubDate>Tue, 30 Jun 2026 15:15:47 +0000</pubDate>
      <link>https://dev.to/andrewmaury/cleartrace-on-chain-execution-intelligence-4p4o</link>
      <guid>https://dev.to/andrewmaury/cleartrace-on-chain-execution-intelligence-4p4o</guid>
      <description>&lt;p&gt;By &lt;a href="https://andrewmaury.com" rel="noopener noreferrer"&gt;Andrew Maury&lt;/a&gt;, Founder of Rantum&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4wgpkcy8wcg8s7y2dqv3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4wgpkcy8wcg8s7y2dqv3.png" alt="ClearTrace Data attribution" width="680" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The decentralized exchange ecosystem is flooded with fake volume. Wash trades, proxy routing, MEV-farm flow: they all look like “adoption” in standard dashboards.&lt;/p&gt;

&lt;p&gt;When L2 foundations and grant programs hand out incentives based on volume, they’re rewarding the wrong behavior because there’s no neutral way to attribute a trade back to the actual frontend that originated it. Was it Uniswap? A wallet-native swap? An aggregator? An institutional desk? On-chain, there’s no standard announcement.&lt;/p&gt;

&lt;p&gt;That gap is the problem &lt;a href="https://cleartracedata.com" rel="noopener noreferrer"&gt;ClearTrace&lt;/a&gt; solves.&lt;/p&gt;

&lt;h3&gt;
  
  
  What We Built
&lt;/h3&gt;

&lt;p&gt;ClearTrace is a live, neutral, third-party execution-intelligence engine built on Dune Analytics’ granular trace and call tables, building on our open-source contributions to Dune, including 67 contracts we submitted for decoding.&lt;/p&gt;

&lt;p&gt;It attributes trade origin, benchmarks execution quality, and measures MEV exposure across Ethereum, Base, Arbitrum, and Optimism using 172K+ attributed contracts, and serves it through a public dashboard and REST API at cleartracedata.com.&lt;/p&gt;

&lt;p&gt;The attribution uses four vectors:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Call-data suffix trapper: parses static hex suffixes appended to router calldata that standard decoders ignore, catching meta-frontends and wallet-native swap extensions.&lt;/li&gt;
&lt;li&gt;Proxy-router hunt: flags tx_to / pool-address mismatches to reveal custom enterprise frontends and institutional smart wallets.&lt;/li&gt;
&lt;li&gt;Multi-hop origin trace: uses window functions (LAG/LEAD) over per-wallet interaction sequences to map interface hops across chains.&lt;/li&gt;
&lt;li&gt;Fee-recipient vector: clusters anonymous frontends by the basis-point fees they route to on-chain identifiers during execution.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Execution Quality, Measured Neutrally
&lt;/h3&gt;

&lt;p&gt;ClearTrace scores execution by comparing each trade’s realized price against a 1-minute VWAP oracle: the &lt;em&gt;true&lt;/em&gt; cost of a trade, not the quoted price an aggregator advertises. Across the four chains covered, the best-performing aggregators land around ~7.5 bps median effective slippage.&lt;/p&gt;

&lt;p&gt;It separately detects sandwich attacks (frontrun → victim → backrun) by scanning in-block transaction ordering, reporting both the attack count and total value sandwiched as a distinct MEV-exposure metric.&lt;/p&gt;

&lt;p&gt;The full attribution and execution-quality methodology is documented in our whitepaper: &lt;a href="https://doi.org/10.5281/zenodo.21513263" rel="noopener noreferrer"&gt;ClearTrace: On-Chain Execution Intelligence (Zenodo, 2026), DOI 10.5281/zenodo.21513263&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What It Proves
&lt;/h3&gt;

&lt;p&gt;Foundations, grant programs, and protocol teams now have neutral proof of organic-versus-wash/proxy/bot volume for named recipients: the basis for spending incentives on real adoption. It runs today as a free dashboard and public API, with paid per-chain integrity reports and standing monthly monitoring engagements.&lt;/p&gt;

&lt;p&gt;Finding signal in deliberately adversarial data (data that is obfuscated, unlabeled, or actively trying not to be measured) and shipping it as live, defensible infrastructure is what Rantum does.&lt;/p&gt;

&lt;p&gt;If you have messy, fragmented data that needs to become a product, l&lt;a href="https://rantum.xyz" rel="noopener noreferrer"&gt;et’s build it together&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>ethereum</category>
      <category>datascience</category>
      <category>onchainanalysis</category>
    </item>
  </channel>
</rss>
