<?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: Edy Cu</title>
    <description>The latest articles on DEV Community by Edy Cu (@edycutjong).</description>
    <link>https://dev.to/edycutjong</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%2F3911089%2Fb24966e4-839d-4c19-8e2d-16c6a5c8838c.jpeg</url>
      <title>DEV Community: Edy Cu</title>
      <link>https://dev.to/edycutjong</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/edycutjong"/>
    <language>en</language>
    <item>
      <title>50,000 property tests passed while my app crowned an impostor</title>
      <dc:creator>Edy Cu</dc:creator>
      <pubDate>Fri, 25 Sep 2026 02:26:11 +0000</pubDate>
      <link>https://dev.to/edycutjong/50000-property-tests-passed-while-my-app-crowned-an-impostor-2bbo</link>
      <guid>https://dev.to/edycutjong/50000-property-tests-passed-while-my-app-crowned-an-impostor-2bbo</guid>
      <description>&lt;p&gt;You hear "buy PEPE", type PEPE into your wallet, and get fourteen tokens with the same name and the same frog. Which one do you buy?&lt;/p&gt;

&lt;p&gt;I built a tool to answer that. You type a ticker, and it asks &lt;a href="https://nansen.ai" rel="noopener noreferrer"&gt;Nansen&lt;/a&gt;'s API for every token with that name across chains. Then it checks who actually holds and trades each one: labelled Smart Money, whales, top-PnL wallets, exchange flow, tagged top holders. Exactly one card turns green, or the tool abstains.&lt;/p&gt;

&lt;p&gt;Last week it crowned a token that its own scorer had flagged as an impostor. My property test ran 50,000 generated cases on that exact decision, and they all passed. While writing this post I found out why, and the answer was worse than I expected.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Live: &lt;strong&gt;&lt;a href="https://whichone.edycu.dev" rel="noopener noreferrer"&gt;https://whichone.edycu.dev&lt;/a&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Code: &lt;strong&gt;&lt;a href="https://github.com/edycutjong/whichone" rel="noopener noreferrer"&gt;https://github.com/edycutjong/whichone&lt;/a&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Two rules in two files
&lt;/h2&gt;

&lt;p&gt;The engine judges each candidate twice.&lt;/p&gt;

&lt;p&gt;The impostor rule lives in &lt;code&gt;score.ts&lt;/code&gt;. A candidate is an impostor when nothing labelled has touched it, it has no meaningful exchange flow, and it is either brand new or barely held:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// packages/core/src/score.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;impostor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;labelledWallets&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;exchMag&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ageDays&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ageDays&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;14&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="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;totalHolders&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;totalHolders&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;impostor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;IMPOSTOR: nothing labelled has ever touched it&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The crown rule lives in &lt;code&gt;verdict.ts&lt;/code&gt;. It decides whether &lt;code&gt;ranked[0]&lt;/code&gt; gets the green card or the tool says "none of these looks real". This is how it read before the fix (abridged: I left out the guards for zero results, unscorable chains and failed lookups, which didn't change):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// packages/core/src/verdict.ts, before commit 3b5f16f (abridged)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;crown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ranked&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Scored&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;sameNameCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;winner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Scored&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;abstainReason&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;best&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ranked&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="cm"&gt;/* ... guards: 0 results, unscorable, every lookup failed, holders lookup failed ... */&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;best&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;ABSTAIN_THRESHOLD&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;best&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;labelledWallets&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;best&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recognisedHolders&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;MIN_RECOGNISED_TO_CROWN&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;winner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;abstainReason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;none of these looks real — nothing labelled has touched any of them&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;winner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;best&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;best.impostor&lt;/code&gt; isn't read anywhere in it. The score module had already answered the question, and the crown rule never asked.&lt;/p&gt;

&lt;h2&gt;
  
  
  A fix for one bug opened the door
&lt;/h2&gt;

&lt;p&gt;Three days earlier, live testing turned up a different failure. &lt;code&gt;SHIB2&lt;/code&gt; was a dead token, about $36K market cap, three years old, with zero labelled wallets, and the tool crowned it REAL. The cause was the holders tiebreak. Nansen's &lt;code&gt;tgm/holders&lt;/code&gt; returns an &lt;code&gt;address_label&lt;/code&gt; for each top holder, and SHIB2's top holders included a &lt;code&gt;UniswapV2&lt;/code&gt; pool and a &lt;code&gt;SHIB2 Token Deployer&lt;/code&gt;. Every token has a pool and a deployer, so those labels prove nothing.&lt;/p&gt;

&lt;p&gt;That fix excluded structural tags (pools, deployers, ENS/SNS names, burn addresses, the contract itself). It also raised the bar for a candidate with zero labelled wallets: it can be crowned only if at least 3 of its top 20 holders carry a wealth or activity tag, like "Token Millionaire" or "High Activity". That is &lt;code&gt;MIN_RECOGNISED_TO_CROWN = 3&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I added that clause on purpose as an escape hatch. Nansen's labels are uneven, and some real tokens have no labelled wallets in a 7-day window but plenty of tagged holders.&lt;/p&gt;

&lt;p&gt;On 2026-09-19, &lt;code&gt;PEPEGA&lt;/code&gt; got through that hatch. It had one candidate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;0 labelled wallets in 7 days&lt;/li&gt;
&lt;li&gt;no meaningful exchange flow&lt;/li&gt;
&lt;li&gt;283 holders&lt;/li&gt;
&lt;li&gt;7 of its top 20 holders wealth-tagged&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;283 holders is under 500, so &lt;code&gt;score()&lt;/code&gt; set &lt;code&gt;impostor: true&lt;/code&gt;. The score cleared the abstain threshold and 7 tagged holders beat the bar of 3, so &lt;code&gt;crown()&lt;/code&gt; crowned it anyway. The fix commit put it this way: "a green card and an IMPOSTOR badge at once."&lt;/p&gt;

&lt;p&gt;The fix takes two lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// packages/core/src/verdict.ts, after commit 3b5f16f&lt;/span&gt;
  &lt;span class="c1"&gt;// a card cannot be "this is the one" and IMPOSTOR at once: when the best candidate trips the impostor rule, abstain&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;best&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;impostor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;winner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;abstainReason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;none of these looks real — the best candidate trips the impostor rule&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is the CLI output now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PEPEGA — 1 same-name token on Nansen

✖ ethereum   0x9634…c879    PEPEGA   4.68   0 labelled wallets in 7 days · no meaningful exchange flow · 283 holders IMPOSTOR

no winner — none of these looks real — the best candidate trips the impostor rule
7 credits · 4 calls (0 cached) · 2.1s · verdict 47b4ac4fc907
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why 50,000 property cases missed it, part one
&lt;/h2&gt;

&lt;p&gt;Two days before PEPEGA, I had added property-based tests with fast-check. The crown-rule property generates random lists of scored candidates, ranks them, calls &lt;code&gt;crown()&lt;/code&gt;, and checks every winner. It runs 10,000 times for each of five properties, which is where the 50,000 comes from. Everything was green.&lt;/p&gt;

&lt;p&gt;This is what the property asserted about a winner before the fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// packages/core/test/property.test.ts, before commit 3b5f16f (the winner check only)&lt;/span&gt;
&lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;winner&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;ranked&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="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
  &lt;span class="nx"&gt;winner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scorable&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
  &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;winner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;unchecked&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
  &lt;span class="nx"&gt;winner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;ABSTAIN_THRESHOLD&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
  &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;winner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;labelledWallets&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;winner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recognisedHolders&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;MIN_RECOGNISED_TO_CROWN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
  &lt;span class="nx"&gt;abstainReason&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each line restates one of &lt;code&gt;crown()&lt;/code&gt;'s own &lt;code&gt;if&lt;/code&gt; statements, so the property only checked that the function did what the function said. The rule I had missed wasn't in &lt;code&gt;crown()&lt;/code&gt; at all. It was a rule about what the user sees: &lt;strong&gt;a card is never green and IMPOSTOR at once&lt;/strong&gt;. That rule crosses two modules, and my tests were organized by module.&lt;/p&gt;

&lt;p&gt;The fix commit added &lt;code&gt;!winner.impostor &amp;amp;&amp;amp;&lt;/code&gt; to that list. At the time I thought that closed it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part two: the generator never made an impostor
&lt;/h2&gt;

&lt;p&gt;While writing this post I wanted to say how fast the new assertion would have caught the bug. So I copied the test's generator into a scratch script, paired it with the pre-fix &lt;code&gt;crown()&lt;/code&gt;, and counted.&lt;/p&gt;

&lt;p&gt;The generator draws each wallet class separately, and the labelled count is their sum:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// packages/core/test/property.test.ts (abridged: 4 of the 17 fields)&lt;/span&gt;
&lt;span class="nx"&gt;smartTraderWallets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="nx"&gt;whaleWallets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="nx"&gt;topPnlWallets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="nx"&gt;publicFigureWallets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;facts&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;labelledWallets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;smartTraderWallets&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;whaleWallets&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;topPnlWallets&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;publicFigureWallets&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a candidate to have zero labelled wallets, all four independent draws have to land on 0. Across five seeded runs of 10,000 lists, about 44,000 candidates per run, the generator produced &lt;strong&gt;zero&lt;/strong&gt; candidates with zero labelled wallets. So it produced zero impostors, and zero cases where the pre-fix &lt;code&gt;crown()&lt;/code&gt; crowned one. Even with the right assertion, the old code passed.&lt;/p&gt;

&lt;p&gt;It is not just the impostor line. Every branch that matters for a zero-labelled token (the SHIB2 bar, the PEPEGA veto, the failed-holders abstain) was sitting in a corner of the input space the generator never visited.&lt;/p&gt;

&lt;p&gt;I changed one thing in the scratch copy, not in the repo. Each wallet count became &lt;code&gt;fc.oneof(fc.constant(0), fc.nat(500))&lt;/code&gt;, so zero comes up about half the time for each class. Then I ran it against the pre-fix &lt;code&gt;crown()&lt;/code&gt; with the &lt;code&gt;!winner.impostor&lt;/code&gt; property. fast-check found a counterexample on all five seeds, after &lt;strong&gt;47, 53, 71, 109 and 244&lt;/strong&gt; cases.&lt;/p&gt;

&lt;p&gt;Two things went wrong, and fixing either one alone would not have caught the bug:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The invariant was the implementation restated.&lt;/strong&gt; Write properties about what the output must never do, not about which branches the code takes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The generator had no weight where the bugs were.&lt;/strong&gt; A uniform draw almost never produces the edge case your domain revolves around. Here that case was "zero". If a value drives a branch, put it in the generator on purpose.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;As of this writing, the repo's generator still has that gap. The PEPEGA regression test pins the actual bug, so the crown rule is covered. The property test just doesn't cover as much of it as "50,000 cases" suggests.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rest of the system
&lt;/h2&gt;

&lt;p&gt;The score is plain arithmetic over four Nansen endpoints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;search/general&lt;/code&gt;: 0 credits&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tgm/flow-intelligence&lt;/code&gt; and &lt;code&gt;tgm/token-information&lt;/code&gt;: 1 credit each, for up to 8 candidates&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tgm/holders&lt;/code&gt;: 5 credits, for the top two finalists only&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Market cap, volume and search rank are left out of the score, because those are what an impostor can buy.&lt;/p&gt;

&lt;p&gt;The repo includes a benchmark: 12 queries × 2 cold runs against the live API, on 2026-09-16. Cold p50 was &lt;strong&gt;3.6 s&lt;/strong&gt;, p95 &lt;strong&gt;7.2 s&lt;/strong&gt;, warm p50 &lt;strong&gt;3 ms&lt;/strong&gt;. Verdicts cost &lt;strong&gt;18.6 credits&lt;/strong&gt; on average and 26 at most. &lt;code&gt;npm run verify&lt;/code&gt; replays twelve recorded verdicts offline with the same decision hash. It needs no API access and no network. The web page streams every Nansen call into a side rail as it fires, with the endpoint, credits, latency and a short hash of each response.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;search/general&lt;/code&gt; decides the candidate set. The tool can't warn about an impostor Nansen hasn't indexed.&lt;/li&gt;
&lt;li&gt;Label coverage is uneven across chains. A real token on a thinly labelled chain can lose to a bridged copy on a busier one, which is why there's a chain filter.&lt;/li&gt;
&lt;li&gt;Flow data covers a 7-day window, so a real but dormant token can look quiet.&lt;/li&gt;
&lt;li&gt;The zero-labelled escape hatch is still there. On 2026-09-16, &lt;code&gt;AI16Z&lt;/code&gt; and &lt;code&gt;PEPE UNCHAINED&lt;/code&gt; were crowned on wealth-tagged holders alone. The card says "0 labelled wallets" so you can see the weakness, but it's a weaker verdict than a green card backed by 90 labelled wallets.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DOGE&lt;/code&gt; crowns a Solana meme DOGE, because native DOGE has no contract to compare against.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;USDC&lt;/code&gt; is the slow outlier, about 15 s cold. Nansen times out on some of its lookups, and the call drawer shows each timeout.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Live: &lt;strong&gt;&lt;a href="https://whichone.edycu.dev" rel="noopener noreferrer"&gt;https://whichone.edycu.dev&lt;/a&gt;&lt;/strong&gt;. Try &lt;code&gt;PEPE&lt;/code&gt;, then &lt;code&gt;PEPEGA&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Code: &lt;strong&gt;&lt;a href="https://github.com/edycutjong/whichone" rel="noopener noreferrer"&gt;https://github.com/edycutjong/whichone&lt;/a&gt;&lt;/strong&gt;. &lt;code&gt;npm run whichone -- PEPE --explain&lt;/code&gt; prints every term of the score (you need your own Nansen key).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you write property tests, count how often your generator actually produces the values your branches depend on. I hadn't, until this post.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>typescript</category>
      <category>testing</category>
      <category>crypto</category>
    </item>
    <item>
      <title>Nansen called Binance 14 a 'Token Billionaire'. The name cost 1 credit</title>
      <dc:creator>Edy Cu</dc:creator>
      <pubDate>Fri, 25 Sep 2026 02:26:09 +0000</pubDate>
      <link>https://dev.to/edycutjong/nansen-called-binance-14-a-token-billionaire-the-name-cost-1-credit-4cm2</link>
      <guid>https://dev.to/edycutjong/nansen-called-binance-14-a-token-billionaire-the-name-cost-1-credit-4cm2</guid>
      <description>&lt;p&gt;It's 2am and you just sent 316 USDT to the wrong address. Maybe it was an old deposit address from an email, maybe a look-alike planted in your history, maybe a contract. You Google "can I get it back" and the first three results are recovery scams.&lt;/p&gt;

&lt;p&gt;What happens next depends on &lt;strong&gt;what that address actually is.&lt;/strong&gt; A Binance customer deposit address means a support ticket and a real chance of recovery. A burn address means it's gone, and anyone offering to recover it is lying.&lt;/p&gt;

&lt;p&gt;I built &lt;strong&gt;Sent Wrong&lt;/strong&gt; to answer that one question. You paste the address you sent to (and your own, if you like). It runs up to ten calls against &lt;a href="https://nansen.ai" rel="noopener noreferrer"&gt;Nansen&lt;/a&gt;'s API and gives you one of four routes, the evidence behind it, and a support ticket ready to copy.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Live: &lt;strong&gt;&lt;a href="https://sentwrong.edycu.dev" rel="noopener noreferrer"&gt;https://sentwrong.edycu.dev&lt;/a&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Code: &lt;strong&gt;&lt;a href="https://github.com/edycutjong/sentwrong" rel="noopener noreferrer"&gt;https://github.com/edycutjong/sentwrong&lt;/a&gt;&lt;/strong&gt; (MIT)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The whole thing depends on the address's &lt;em&gt;entity label&lt;/em&gt;. The obvious place to get that label turned out to be the wrong one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The field that should have worked
&lt;/h2&gt;

&lt;p&gt;Nansen has cheap profiler endpoints that return a label per address: &lt;code&gt;counterparties&lt;/code&gt; (5 credits), &lt;code&gt;first-funder&lt;/code&gt; and &lt;code&gt;related-wallets&lt;/code&gt; (1 credit each). So the first plan was to profile the address, read its label, and be done.&lt;/p&gt;

&lt;p&gt;On day one I pointed them at Binance 14, probably the best-known labelled hot wallet on Ethereum. The label that came back was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Token Billionaire"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Coinbase 10 came back as &lt;code&gt;["High Activity"]&lt;/code&gt;. Those are wealth and activity tags. They tell you the address is rich or busy. They don't tell you it belongs to an exchange, and they say nothing about a customer deposit address, which is the case that actually gets people their money back.&lt;/p&gt;

&lt;p&gt;The entity name does exist in &lt;code&gt;profiler/address/labels&lt;/code&gt;, which costs 100 credits per call. That's too much to spend by default on every panicked paste.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the name actually was
&lt;/h2&gt;

&lt;p&gt;It turned up, unexpectedly, in &lt;code&gt;transaction-with-token-transfer-lookup&lt;/code&gt;. It takes a transaction hash and returns its token transfers, each with a label for both sides. For 1 credit, on the hero address, the sending side read:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;🏦 Binance: Deposit [0xe46077]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That names the exact customer deposit address, not just the exchange. The receiving side read &lt;code&gt;🏦 Binance 14 [0x28c6c0]&lt;/code&gt;: the sweep into the exchange's own wallet. Nothing in the docs says this endpoint carries entity labels. The repo's decision-table doc says it plainly: &lt;em&gt;the whole engine hangs on that field.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That changed the design. I stopped asking "what is this address?" and started asking "which transactions should I look at to find out what this address is?"&lt;/p&gt;

&lt;h2&gt;
  
  
  Labels live on transactions, so pick transactions
&lt;/h2&gt;

&lt;p&gt;The engine gathers in stages: a free &lt;code&gt;search/general&lt;/code&gt; call (a token contract stops here), then 14 days of transactions plus the cheap identity lookups, full history for quiet addresses, and finally the choice of which hashes to look up. Abridged from &lt;code&gt;packages/core/src/lookups.ts&lt;/code&gt;, with the sender-matching branch removed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Which transaction hashes to look up: the newest 2 outbound (the sweeps), the newest inbound, the sender's transfer&lt;/span&gt;
&lt;span class="c1"&gt;// if we can see it, and the funding transaction. These carry the entity labels the profiler rows do not.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wanted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TxLookup&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;role&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;transactions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;outbound&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;inbound&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;splitDirection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;transactions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;outbound&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&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;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;wanted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transaction_hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;outbound&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="cm"&gt;/* … the sender's own transfer, if --from was given … */&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inbound&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="nx"&gt;wanted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;inbound&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="nx"&gt;transaction_hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;inbound&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;firstFunder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;firstFunder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&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="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;lc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;firstFunder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&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="nx"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;wanted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;firstFunder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&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="nx"&gt;transaction_hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;funding&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each hash does its own job:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Outbound transactions&lt;/strong&gt; show where the address sweeps to, and the address's own label.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The newest inbound transaction&lt;/strong&gt; carries the address's own label as a recipient, even if it has never sent anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The funding transaction&lt;/strong&gt; shows who paid the address's first gas. Exchanges fund their own deposit addresses, so &lt;code&gt;first-funder&lt;/code&gt; → funding tx → &lt;code&gt;🏦 Binance [0x943080]&lt;/code&gt; can name the exchange before the address has swept anything.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's up to four lookups at 1 credit each. The hero verdict costs 13 credits across 10 calls, against 100 for the labels endpoint, which is still available behind &lt;code&gt;--deep&lt;/code&gt; (cost printed first; the card says whether it agrees).&lt;/p&gt;

&lt;h2&gt;
  
  
  Parsing a label is its own small trap
&lt;/h2&gt;

&lt;p&gt;Labels are free text with emoji and an address stub: &lt;code&gt;🏦 Binance: Deposit [0xe46077]&lt;/code&gt;, &lt;code&gt;🤖 🏦 Coinbase [0xa9d1e0]&lt;/code&gt;, &lt;code&gt;Token Billionaire&lt;/code&gt;, &lt;code&gt;sofaking.eth*&lt;/code&gt;. The rule behind the most common good-news verdict is short. From &lt;code&gt;packages/core/src/labels.ts&lt;/code&gt;, verbatim:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/** "&amp;lt;Entity&amp;gt;: Deposit" — Nansen's label for a user-level exchange deposit address. */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isDepositLabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ParsedLabel&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;ParsedLabel&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="sr"&gt;/^Deposit$/i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Getting there took two regression tests:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;🏦 doesn't mean "exchange."&lt;/strong&gt; Nansen puts the same bank emoji on DEX routers: &lt;code&gt;🤖 🏦 Uniswap: V2 Router 2&lt;/code&gt;. A router's outflow looks exactly like a sweep, so any label that names a contract (Router, Pool, Proxy, …) is now kept out of the sweep rule.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Direction matters.&lt;/strong&gt; A &lt;code&gt;: Deposit&lt;/code&gt; label on a &lt;em&gt;destination&lt;/em&gt; means a person is depositing &lt;em&gt;into&lt;/em&gt; an exchange. That makes the sender an ordinary user wallet, not a deposit address. One of the recorded live runs hit exactly this, and it's now a named regression test: &lt;em&gt;"a user wallet sending to its own ': Deposit' address is NOT a deposit address."&lt;/em&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The refusal is the answer
&lt;/h2&gt;

&lt;p&gt;On burn addresses, &lt;code&gt;transactions&lt;/code&gt; and &lt;code&gt;counterparties&lt;/code&gt; return &lt;strong&gt;HTTP 422 &lt;code&gt;Burn address not allowed&lt;/code&gt;&lt;/strong&gt;. It reads like an error. It's actually the most certain answer the API gives, so it's rule 1 in the decision table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isBurnRejection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;LookupResult&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;422&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="sr"&gt;/burn address/i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Paste &lt;code&gt;0x…dEaD&lt;/code&gt; and you get a red &lt;strong&gt;CONTRACT OR BURN&lt;/strong&gt; card with the evidence line &lt;code&gt;profiler/address/transactions → HTTP 422 = Burn address not allowed&lt;/code&gt; and the sentence &lt;em&gt;"Do not pay anyone who promises recovery."&lt;/em&gt; This check runs before any transaction lookups, so a burn verdict costs 2 credits.&lt;/p&gt;

&lt;p&gt;The flip side: a 422 is an answer about the input, so it's cached and replayed; a timeout or 5xx is not, so a failed &lt;code&gt;transactions&lt;/code&gt; lookup yields a &lt;strong&gt;retry&lt;/strong&gt;, never a verdict. An early build turned a slow minute into "nothing on record", a made-up verdict. A property test now checks, over 10,000 generated response sets, that this can't happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it looks like end to end
&lt;/h2&gt;

&lt;p&gt;Real output from &lt;code&gt;--explain --no-cache&lt;/code&gt;, from the repo's &lt;code&gt;DEMO.md&lt;/code&gt; (abridged to the verdict and totals):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;EXCHANGE-DEPOSIT (high · direct-label)
This is a Binance deposit address. Recoverable through Binance support.
  ✔ Nansen has this exact address labelled as a Binance customer deposit address.
    transaction-with-token-transfer-lookup → token_transfer_array[].from_address_label = 🏦 Binance: Deposit [0xe46077]
  ✔ Everything it receives is swept into Binance's own wallet.
    transaction-with-token-transfer-lookup → token_transfer_array[].to_address_label = 🏦 Binance 14 [0x28c6c0]
  ✔ All outflow ($2,953 at today's prices) goes to one counterparty: the sweep pattern of a deposit address.
    profiler/address/counterparties → volume_out_usd = 100% to 0x28c6…1d60
  ✔ Binance paid this address's first gas — exchanges do that for their deposit addresses.
    profiler/address/first-funder → first_funder_address (looked up) = 🏦 Binance [0x943080]
…
13 credits · 10 calls (0 cached) · 3.9s · verdict 3ea6cfcd752b
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The numbers.&lt;/strong&gt; A benchmark of 13 real addresses × 3 runs with every call live gave a cold p50 of &lt;strong&gt;3,157 ms&lt;/strong&gt; and p95 of &lt;strong&gt;6,477 ms&lt;/strong&gt;, a mean of &lt;strong&gt;10.2 credits per verdict&lt;/strong&gt; (0 to 13), 309 live calls and 0 failures other than the expected 422s. Warm from the 24 h cache it's p50 2 ms at 0 credits, with the same decision hash every run. There are 234 vitest tests. 13 recorded fixtures replay offline with identical hashes (&lt;code&gt;npm run verify&lt;/code&gt;, no key, no network).&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Latency is Nansen's.&lt;/strong&gt; Per-call latency varies from 0.3 to 3 s depending on the minute, so a cold verdict takes 4–13 s. On Vercel the cache is in-memory per function instance, so a cold start is fully live.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Nothing on record" is ambiguous.&lt;/strong&gt; It can't tell a brand-new address from one Nansen doesn't index. vitalik.eth returns empty profiler pages and comes back as a low-confidence &lt;em&gt;fresh&lt;/em&gt; stranger. The card says so instead of promising that waiting will help.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ethereum is the deeply tested chain.&lt;/strong&gt; Seven other EVM chains go through the same endpoints, but only Base has been checked live.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Odds on the stranger route are words, not numbers.&lt;/strong&gt; Nansen can tell whether a wallet moves funds. Nobody can tell whether its owner is honest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's triage, not legal advice.&lt;/strong&gt; Every route's text tells you not to pay recovery services.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;If an API returns labels in more than one place, don't assume the cheapest field is a cheaper copy of the expensive one. Here they were different kinds of label: wealth tags on the profiler rows, identity on the transfer rows. Test the field on an address whose answer you already know. Binance 14 took one call to show the problem.&lt;/p&gt;

&lt;p&gt;Try it at &lt;strong&gt;&lt;a href="https://sentwrong.edycu.dev" rel="noopener noreferrer"&gt;https://sentwrong.edycu.dev&lt;/a&gt;&lt;/strong&gt;, or clone &lt;strong&gt;&lt;a href="https://github.com/edycutjong/sentwrong" rel="noopener noreferrer"&gt;https://github.com/edycutjong/sentwrong&lt;/a&gt;&lt;/strong&gt; and run &lt;code&gt;npm run verify&lt;/code&gt; without a key. The 30-second demo is &lt;a href="https://x.com/edycutjong/status/2103269945157308895" rel="noopener noreferrer"&gt;on X&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you've been sent here after a 2am mistake: paste the address before you reply to anyone offering help.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>typescript</category>
      <category>web3</category>
      <category>api</category>
    </item>
    <item>
      <title>I built a sell planner to dodge the pros. They were under 4% of buys</title>
      <dc:creator>Edy Cu</dc:creator>
      <pubDate>Fri, 25 Sep 2026 02:26:08 +0000</pubDate>
      <link>https://dev.to/edycutjong/i-built-a-sell-planner-to-dodge-the-pros-they-were-under-4-of-buys-4h82</link>
      <guid>https://dev.to/edycutjong/i-built-a-sell-planner-to-dodge-the-pros-they-were-under-4-of-buys-4h82</guid>
      <description>&lt;p&gt;Somebody hands you a token you never asked for, and now you have to turn it into dollars. Maybe a nonprofit got a $40K memecoin donation, or a freelancer got paid in a project's token. They have never used a DEX, and they don't want to be the seller who knocks the price down, or the one selling into a day when the funds are dumping too.&lt;/p&gt;

&lt;p&gt;I built &lt;strong&gt;Glidepath&lt;/strong&gt; for that person. You paste token, chain and amount held. It gives you a dated selling calendar: one tranche per day, each sized to the token's &lt;em&gt;organic&lt;/em&gt; demand, with a &lt;code&gt;.ics&lt;/code&gt; file so the plan lands in your calendar. It plans. It never trades.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Live: &lt;strong&gt;&lt;a href="https://glidepath.edycu.dev" rel="noopener noreferrer"&gt;https://glidepath.edycu.dev&lt;/a&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Code: &lt;strong&gt;&lt;a href="https://github.com/edycutjong/glidepath" rel="noopener noreferrer"&gt;https://github.com/edycutjong/glidepath&lt;/a&gt;&lt;/strong&gt; (MIT)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The whole design rested on one assumption. The data knocked it down in the first afternoon. This post covers what I assumed, what &lt;a href="https://nansen.ai" rel="noopener noreferrer"&gt;Nansen&lt;/a&gt;'s labels showed, and which part of the product turned out to do the real work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The assumption
&lt;/h2&gt;

&lt;p&gt;My premise was that a big chunk of a memecoin's DEX buying comes from pros: Smart Money wallets, funds, whales, exchanges, sniper-bot users. If you size your daily sell against headline volume, you are sizing it against money that can leave the same afternoon. So take the pros out, size against what's left, and call that organic demand.&lt;/p&gt;

&lt;p&gt;Nansen can do that split, because its wallets carry labels. The list of labels that make a buyer "not organic" lives in one constant:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/**
 * Labels that make a buyer "not organic": Nansen's Smart Money tiers, funds, whales, exchanges and the Telegram
 * sniper-bot user tags. From the OpenAPI `LabelType` enum; Public Figure / LP / miner labels are deliberately kept —
 * those are people, not the cohort a forced seller must avoid competing with.
 */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;EXCLUDED_LABELS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Smart Trader&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;30D Smart Trader&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;90D Smart Trader&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;180D Smart Trader&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Fund&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Whale&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Exchange&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Maestro Bot User&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Top Maestro Bot User&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;BananaGun Bot User&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Top BananaGun Bot User&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Surprise 1: you can't see the label you filtered on
&lt;/h2&gt;

&lt;p&gt;My first plan was to pull every buyer from &lt;code&gt;tgm/who-bought-sold&lt;/code&gt; and classify the rows myself. That doesn't work. The &lt;code&gt;address_label&lt;/code&gt; on a row is a display name, not the label the filter matched. On PEPE, the single wallet that &lt;code&gt;exclude_smart_money_labels&lt;/code&gt; removed shows up as &lt;code&gt;nftsindubai.eth&lt;/code&gt;. Elsewhere, excluded rows read &lt;code&gt;High Balance&lt;/code&gt; or &lt;code&gt;&amp;lt;X&amp;gt; Token Deployer&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So the organic/pro split only exists on Nansen's side, as a filter. On the 8 tokens I probed, &lt;code&gt;include_smart_money_labels&lt;/code&gt; and &lt;code&gt;exclude_smart_money_labels&lt;/code&gt; with the same list partitioned the buyer set exactly. That became the pager:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// packages/core/src/nansen.ts — abridged: signature and field list trimmed&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exclude&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exclude_smart_money_labels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exclude&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;include&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;include_smart_money_labels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;include&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WhoBoughtSoldRow&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;pages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;maxPages&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;WhoBoughtSoldResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tgm/who-bought-sold&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;token_address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;buy_or_sell&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;BUY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;iso&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;iso&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;pagination&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;per_page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nx"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;order_by&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;field&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bought_volume_usd&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;DESC&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="cm"&gt;/* , fields, opts */&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;pages&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(...(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[]));&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pagination&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;is_last_page&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;truncated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;truncated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Surprise 2: you can't page the organic side either
&lt;/h2&gt;

&lt;p&gt;The obvious next move was to page the organic buyers and add them up. Then I ran it on 5-to-7-day-old Solana launches. They had &lt;strong&gt;more than 20,000 buying addresses in 7 days&lt;/strong&gt;: 20 pages of 1,000, 110 seconds, 20 credits, and the list was still truncated.&lt;/p&gt;

&lt;p&gt;So the math is flipped. Glidepath never adds up the organic side. It takes total DEX buys from &lt;code&gt;tgm/token-information&lt;/code&gt; and subtracts the pro rows. Across every token I looked at, the pros were at most 67 wallets, so that list is one page, three at most:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// packages/core/src/plan.ts — computePlan, the organic-demand block (abridged: surrounding code omitted)&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;totalBuy7dUsd&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;proBuy7dUsd&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;organicBuy7dUsd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;totalBuy7dUsd&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;organicShare&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;warnings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pro-buyer split unavailable (who-bought-sold failed) — organic = all DEX buys&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;organicBuy7dUsd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&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="nx"&gt;totalBuy7dUsd&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;proBuy7dUsd&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;organicShare&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;totalBuy7dUsd&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;organicBuy7dUsd&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;totalBuy7dUsd&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// from the pager's own flag: a list that ends exactly on page 3 is complete, not truncated&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;facts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;proTruncated&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;warnings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`pro-buyer list truncated at &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;facts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;proPages&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; pages — organic volume is an upper bound`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;organicDailyUsd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;organicBuy7dUsd&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;organicBuy7dUsd&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last comment is a bug I fixed later. The warning used to fire on &lt;code&gt;proPages &amp;gt;= 3&lt;/code&gt;, so a pro list that happened to end on exactly page 3 was reported as truncated when it was complete.&lt;/p&gt;

&lt;h2&gt;
  
  
  Surprise 3: the premise was wrong
&lt;/h2&gt;

&lt;p&gt;Once the subtraction worked, I printed the split for real tokens. Here is PEPE, recorded live on 2026-09-16:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;organic buys $458,738/day = ($3,236,056 DEX buys 7d − $24,893 by Smart Money/Fund/Whale/Exchange/bot users [1 wallets]) / 7 · organic share 99.2% · 528 organic buyers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One labelled wallet. 99.2% organic. And PEPE wasn't unusual. &lt;strong&gt;On all 11 tokens I tried, pros were 0–3.6% of DEX buy volume.&lt;/strong&gt; Majors came out 98.5–100% organic. Fresh Solana launches came out 96.4–96.7%. The four sniper-bot labels matched zero rows, even on pump.fun launches with 20,000+ buyers. I kept them in the exclusion list, but I don't claim they do anything.&lt;/p&gt;

&lt;p&gt;So the label filter I built the product around moves the "organic" number by a few percent. That's worth knowing, but it isn't the headline. I had a choice: make the split look dramatic on screen, or show it at its real size. The app shows it at its real size, and the README lists it as a limitation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually changes the calendar
&lt;/h2&gt;

&lt;p&gt;The part of Nansen's data that visibly changes a plan is cohort &lt;em&gt;flows&lt;/em&gt;, not buyer labels. Glidepath marks a day red if Smart Money is net-selling, or if exchanges are receiving more tokens than organic buyers absorb in a day:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/** Red-day test: Smart Money net-selling past θ_sm, or net deposits to exchanges past θ_ex. */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;redDay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;smNetUsd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;exNetUsd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;th&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Theta&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;red&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fmt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;v&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;−&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;+&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;$&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toLocaleString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;en-US&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;smNetUsd&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;smNetUsd&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;th&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;smUsd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;red&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Smart Money net &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;smNetUsd&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;exNetUsd&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;exNetUsd&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;th&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exUsd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;red&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Exchange net deposits &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;exNetUsd&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;red&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;theta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;organicDailyUsd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Theta&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;organicDailyUsd&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;smUsd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;THETA_FLOOR_USD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;THETA_SM_SHARE&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;exUsd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;THETA_FLOOR_USD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;THETA_EX_SHARE&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"Today" comes from &lt;code&gt;tgm/flow-intelligence&lt;/code&gt;. The 13 complete days before it come from &lt;code&gt;tgm/flows&lt;/code&gt; daily cohort history. A red today halves the first tranche, and the red-day rate over the last 13 days stretches the expected finish date. On PEPE's 16 September run, 4 of the 13 days had been red, all from exchange deposits between +$499K and +$1.92M. That turned a 2-tranche plan into "expect ~3 days". When I recorded the demo on the 19th, today itself was red (+$736K net exchange deposits) and the first tranche was halved on camera.&lt;/p&gt;

&lt;p&gt;The exchange threshold is a full day of organic buying, and that's on purpose. For CEX-listed tokens, ±$1M of exchange flow is routine shuffling. The threshold is the point where you would really be competing with the pros.&lt;/p&gt;

&lt;p&gt;Tranche size is &lt;code&gt;min(k × organic/day, 1% of liquidity_usd)&lt;/code&gt;, where &lt;code&gt;k&lt;/code&gt; slides from 10% down to 3% as Nansen's peer-percentile risk indicators rise. For PEPE that came to 2 tranches, $30,598 then $9,372. Under a constant-product model the estimated cost is $147.90, against $230.36 for dumping the whole bag today.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing the planner
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;computePlan&lt;/code&gt; is a pure function of &lt;code&gt;(facts, input, now)&lt;/code&gt;, so it's cheap to test hard. There are 6 fast-check properties at 10,000 runs each, &lt;strong&gt;60,000 generated cases&lt;/strong&gt;. They check that tranches plus remainder equal the bag, that every tranche stays under both caps, that a red today halves tranche 1 and only tranche 1, and that the dates are consecutive. On its first run it found a real bug. At a dust price, the liquidity cap underflowed to a tranche of zero tokens, and the planner printed 90 empty rows. The fix is one guard:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// packages/core/src/plan.ts — sizeTranches (abridged: loop omitted)&lt;/span&gt;
&lt;span class="c1"&gt;// a tranche of zero tokens (liquidity cap underflowing at a dust price) would otherwise emit MAX_DAYS empty rows:&lt;/span&gt;
&lt;span class="c1"&gt;// nothing can be sold at this pace, so the calendar is empty and the whole bag is the unsold remainder&lt;/span&gt;
&lt;span class="k"&gt;if &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="nx"&gt;trancheTokens&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;tranches&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;days&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="na"&gt;truncated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;remainderTokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;remainderPct&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The suite has 258 vitest tests in total. 13 recorded live plans replay offline byte-for-byte with zero network calls. Cold plans run p50 3.5 s / p95 6.0 s against live Nansen, averaging 13 credits each (12 on EVM chains, 15 on solana/base, where three &lt;code&gt;trade/quote&lt;/code&gt; calls replace the cost model with a real route).&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The cost model is an approximation.&lt;/strong&gt; It treats &lt;code&gt;liquidity_usd&lt;/code&gt; as one pool with the token on one side. That's optimistic for tokens whose depth sits in a single thin pool, and it ignores MEV and gas. Only solana and base get a real routed quote.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Future days can't be known red or green.&lt;/strong&gt; Each calendar event carries the rule, so you re-check it on the morning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two definitions of "Smart Money."&lt;/strong&gt; Today's test uses flow-intelligence's &lt;code&gt;smart_trader&lt;/code&gt; cohort. History uses &lt;code&gt;tgm/flows&lt;/code&gt;' &lt;code&gt;smart_money&lt;/code&gt; cohort. For PEPE that's 21 wallets vs 18 holders.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A nonexistent-but-valid address still spends 12 credits&lt;/strong&gt;, because the calls fan out in parallel with no existence check first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stablecoins get no history strip.&lt;/strong&gt; &lt;code&gt;tgm/flows&lt;/code&gt; refuses them with a 422, and the plan says so.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is not financial advice. It is a pacing calculator with its reasoning shown.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Live: &lt;strong&gt;&lt;a href="https://glidepath.edycu.dev" rel="noopener noreferrer"&gt;https://glidepath.edycu.dev&lt;/a&gt;&lt;/strong&gt;. Click the PEPE or BONK chip, then open "Every Nansen call" to see what each number came from. Whether today is red depends on that day's flows.&lt;/li&gt;
&lt;li&gt;Code: &lt;strong&gt;&lt;a href="https://github.com/edycutjong/glidepath" rel="noopener noreferrer"&gt;https://github.com/edycutjong/glidepath&lt;/a&gt;&lt;/strong&gt;. &lt;code&gt;npm run verify&lt;/code&gt; replays all 13 plans with no key.&lt;/li&gt;
&lt;li&gt;Built for the Nansen Meridian Buildathon. The entry post is &lt;a href="https://x.com/edycutjong/status/2102964373044301841" rel="noopener noreferrer"&gt;here on X&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've measured the pro share on a token where it's actually large, I'd like to see the number.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>typescript</category>
      <category>api</category>
      <category>web3</category>
    </item>
    <item>
      <title>My AI trading tool would sign a wallet drain as a login challenge</title>
      <dc:creator>Edy Cu</dc:creator>
      <pubDate>Fri, 25 Sep 2026 01:44:23 +0000</pubDate>
      <link>https://dev.to/edycutjong/my-ai-trading-tool-would-sign-a-wallet-drain-as-a-login-challenge-7o7</link>
      <guid>https://dev.to/edycutjong/my-ai-trading-tool-would-sign-a-wallet-drain-as-a-login-challenge-7o7</guid>
      <description>&lt;p&gt;BagOS is an MCP server I maintain. It lets an AI agent read token data on Bags, a Solana launchpad,&lt;br&gt;
and, if you configure a wallet, trade and claim creator fees. I built it around one idea: a model&lt;br&gt;
should be able to &lt;em&gt;propose&lt;/em&gt; a spend but never complete one on its own. The first call to a write&lt;br&gt;
tool signs nothing. It returns a preview and a single-use token bound to the exact arguments. Every&lt;br&gt;
trade is capped. Every transaction is simulated before it's signed.&lt;/p&gt;

&lt;p&gt;This week an outside review showed that none of that mattered. There was a way to drain the wallet&lt;br&gt;
without touching a single write tool.&lt;/p&gt;
&lt;h2&gt;
  
  
  The attack
&lt;/h2&gt;

&lt;p&gt;It needed two flaws. Either one alone was harmless.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The server trusted the folder you had open.&lt;/strong&gt; MCP clients such as Claude Code start a local&lt;br&gt;
server in the current project folder. BagOS called &lt;code&gt;dotenv.config()&lt;/code&gt;, which reads &lt;code&gt;.env&lt;/code&gt; from the&lt;br&gt;
working directory. So any repository you opened could supply configuration you never set, including&lt;br&gt;
&lt;code&gt;BAGS_API_URL&lt;/code&gt;, the endpoint the login tool talks to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The login tool signed whatever it was given.&lt;/strong&gt; &lt;code&gt;bags_authenticate&lt;/code&gt; proves you own a wallet: it&lt;br&gt;
fetches a challenge from the auth endpoint, signs it, and trades the signature for an API key. It&lt;br&gt;
signed the challenge bytes without checking what they were.&lt;/p&gt;

&lt;p&gt;On Solana, a transaction signature is an ed25519 signature over the transaction's serialized&lt;br&gt;
message. So if the "challenge" is a transaction message, the signature the tool sends back is a&lt;br&gt;
valid signature for that transaction. Whoever receives it can broadcast it.&lt;/p&gt;

&lt;p&gt;Put them together. A repo ships a &lt;code&gt;.env&lt;/code&gt; that points &lt;code&gt;BAGS_API_URL&lt;/code&gt; at a server its author&lt;br&gt;
controls. You open the repo, and your agent calls &lt;code&gt;bags_authenticate&lt;/code&gt;, maybe because a README told&lt;br&gt;
it to. The fake endpoint returns a transfer of your balance as the challenge. The tool signs it and&lt;br&gt;
sends the signature to that server.&lt;/p&gt;

&lt;p&gt;The login tool wasn't a write tool, so none of the guardrails applied: no token gate, no cap, no&lt;br&gt;
preview, no confirmation. My docs even said "Signing a challenge is not signing a transaction."&lt;br&gt;
Before the fix, that wasn't true.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why 100% coverage didn't catch it
&lt;/h2&gt;

&lt;p&gt;The suite had 100% line, branch and function coverage, enforced in CI. Every line of the auth tool&lt;br&gt;
was tested. The tests checked that it fetched a challenge, signed it and exchanged it, and it did&lt;br&gt;
all of that correctly.&lt;/p&gt;

&lt;p&gt;Coverage measures which lines run. It says nothing about which inputs you assumed were safe. My&lt;br&gt;
tests used a well-behaved endpoint and a config I wrote myself, because I had never asked who else&lt;br&gt;
could write that config or what else could arrive as a challenge. The missing check wasn't&lt;br&gt;
untested; it had never been written.&lt;/p&gt;
&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;3.0.0 is a breaking release, because it changes how configuration loads.&lt;/p&gt;

&lt;p&gt;The server no longer reads &lt;code&gt;.env&lt;/code&gt; from the working directory. You name a file explicitly, and the&lt;br&gt;
path must be absolute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;explicit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;BAGS_ENV_FILE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;explicit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// A relative path resolves against the working directory, which is the&lt;/span&gt;
  &lt;span class="c1"&gt;// exact thing this function exists not to trust.&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;isAbsolute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;explicit&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* "refusing BAGS_ENV_FILE=...: it must be an absolute path" */&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refused-relative&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a &lt;code&gt;.env&lt;/code&gt; is sitting in the working directory, the server says it's ignoring it, on stderr.&lt;/p&gt;

&lt;p&gt;The login tool now signs only Bags' exact sign-in text, with the nonce from the same init response.&lt;br&gt;
It also refuses anything that decodes as a Solana transaction, and anything that isn't printable&lt;br&gt;
text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isTransactionMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;VersionedMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deserialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;serialize&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;equals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The auth endpoint is pinned to https on &lt;code&gt;bags.fm&lt;/code&gt; unless the operator sets&lt;br&gt;
&lt;code&gt;BAGS_ALLOW_CUSTOM_API_URL=true&lt;/code&gt;. The model can no longer choose the keypair path either.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shipping a security release
&lt;/h2&gt;

&lt;p&gt;I drafted a private GitHub security advisory, fixed the bug on a temporary private fork, and merged&lt;br&gt;
it from the advisory page. Then:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Released 3.0.0 through the normal pipeline, with npm provenance.&lt;/li&gt;
&lt;li&gt;Deprecated every older version on npm (1.0.0 through 2.6.0), with a message pointing to the
advisory.&lt;/li&gt;
&lt;li&gt;Published the advisory: GHSA-g679-3wq7-mh3m.&lt;/li&gt;
&lt;li&gt;Turned on private vulnerability reporting, so the next person has a private channel.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One trap for anyone doing this with release-please: merging from the advisory page squashes the&lt;br&gt;
private fork into one commit titled "Merge commit from fork." That isn't a conventional commit, so&lt;br&gt;
release-please computed a &lt;strong&gt;patch&lt;/strong&gt; version for a breaking change. I caught it by checking the&lt;br&gt;
release PR before merging, and fixed it by pushing an empty commit that restated the breaking&lt;br&gt;
change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then it happened again
&lt;/h2&gt;

&lt;p&gt;Hours later, I had the review check my launch plan against the code, and it turned up a second&lt;br&gt;
gap.&lt;/p&gt;

&lt;p&gt;The spend caps checked the amount the agent asked for. But the swap transaction that actually gets&lt;br&gt;
signed is built by the Bags API, and nothing compared the two. The cap bounded the request, not&lt;br&gt;
the signature.&lt;/p&gt;

&lt;p&gt;3.0.5 closes that. Before signing, BagOS reads the wallet's SOL balance, asks the simulation for&lt;br&gt;
the balance afterwards, and refuses if the difference is more than the approved amount plus 0.01&lt;br&gt;
SOL for fees and rent. A fee claim approves nothing, so it may cost fees only. If the simulation&lt;br&gt;
doesn't report the balance, the transaction isn't signed. The check is required on every path,&lt;br&gt;
so no transaction can skip it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's still open
&lt;/h2&gt;

&lt;p&gt;These are documented in SECURITY.md rather than hidden:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;HTTP mode has no auth.&lt;/strong&gt; &lt;code&gt;--http&lt;/code&gt; serves &lt;code&gt;/mcp&lt;/code&gt; on &lt;code&gt;0.0.0.0&lt;/code&gt;. Don't run it with a funded
wallet. The stdio default is unaffected.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The simulation check reads SOL, not tokens.&lt;/strong&gt; A transaction from the Bags API that also moved
SPL tokens wouldn't trip it. That's trust in the Bags API, whose endpoint is fixed in its SDK.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The confirmation token binds the arguments, not the quoted price.&lt;/strong&gt; Confirming re-runs the
quote, so the price can move inside the five-minute window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caps are in SOL.&lt;/strong&gt; A swap from another token can't be valued, so those swaps are refused
unless you opt in.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I'd tell anyone building an MCP server that holds a key
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Treat configuration as input.&lt;/strong&gt; An MCP server's working directory belongs to whatever project
is open. Only the operator should set config, in the client's own settings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never sign bytes you didn't construct or check.&lt;/strong&gt; "It's just a login" is how a signing tool
ends up outside every guardrail.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bound the effect, not the request.&lt;/strong&gt; Simulate the transaction and check what it actually
does to the wallet before you sign it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep an adversarial reviewer in the loop.&lt;/strong&gt; Both bugs were found by review, not by tests. The
tests were written with the same assumptions as the code, so they couldn't find them.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Links: &lt;a href="https://github.com/edycutjong/BagOS" rel="noopener noreferrer"&gt;repo&lt;/a&gt; ·&lt;br&gt;
&lt;a href="https://github.com/edycutjong/BagOS/security/advisories/GHSA-g679-3wq7-mh3m" rel="noopener noreferrer"&gt;advisory&lt;/a&gt; ·&lt;br&gt;
&lt;a href="https://github.com/edycutjong/BagOS/blob/main/.github/SECURITY.md" rel="noopener noreferrer"&gt;SECURITY.md&lt;/a&gt; ·&lt;br&gt;
&lt;a href="https://www.npmjs.com/package/bagos-mcp-server" rel="noopener noreferrer"&gt;npm&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Earlier I wrote about a different BagOS bug, where the write tools reported success without signing anything: &lt;a href="https://dev.to/edycutjong/i-shipped-an-mcp-server-that-reported-success-without-signing-anything-6oh"&gt;I shipped an MCP server that reported success without signing anything&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I build safety layers for AI agents that move money. I'm open to remote work.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>security</category>
      <category>mcp</category>
      <category>solana</category>
    </item>
    <item>
      <title>I went looking for sandwiches and found a DEX pool trading with itself</title>
      <dc:creator>Edy Cu</dc:creator>
      <pubDate>Wed, 23 Sep 2026 04:37:43 +0000</pubDate>
      <link>https://dev.to/edycutjong/i-went-looking-for-sandwiches-and-found-a-dex-pool-trading-with-itself-2kj8</link>
      <guid>https://dev.to/edycutjong/i-went-looking-for-sandwiches-and-found-a-dex-pool-trading-with-itself-2kj8</guid>
      <description>&lt;p&gt;A DEX trader sees a quote and a fill and cannot tell what stood between them. I set out to measure the obvious suspect, the sandwich: a bot that buys just before you and sells straight after, in the same block.&lt;/p&gt;

&lt;p&gt;I found almost none. What I found instead was a pool where two thirds of the volume was three wallets trading with themselves. The pool was ranked #1 on the same activity those trades were inflating.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Live: &lt;a href="https://middleman.edycu.dev" rel="noopener noreferrer"&gt;middleman.edycu.dev&lt;/a&gt; (the judge page is &lt;a href="https://middleman.edycu.dev/judge" rel="noopener noreferrer"&gt;/judge&lt;/a&gt;, every request behind it on &lt;a href="https://middleman.edycu.dev/evidence" rel="noopener noreferrer"&gt;/evidence&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/edycutjong/middleman" rel="noopener noreferrer"&gt;github.com/edycutjong/middleman&lt;/a&gt;. MIT, stdlib-only Python, no key.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The row that makes it possible
&lt;/h2&gt;

&lt;p&gt;CoinMarketCap's &lt;code&gt;/v1/dex/tokens/transactions&lt;/code&gt; returns a token's recent swaps, keyless, with a cursor. Each row carries the fields a sandwich detector needs: the maker's wallet (&lt;code&gt;ma&lt;/code&gt;), the block (&lt;code&gt;h&lt;/code&gt;), the log index inside the block (&lt;code&gt;lgid&lt;/code&gt;), the side (&lt;code&gt;tp&lt;/code&gt;) and both amounts (&lt;code&gt;a0&lt;/code&gt;, &lt;code&gt;a1&lt;/code&gt;). Here's one real row, trimmed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"h"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"26006339"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"lgid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"209"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sell"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ma"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0xc9160fdab187f2e55567b760d88a87ae7fe56d95"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"a0"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;842991.8537715519&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"a1"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.0430246602456774&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"en"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Uniswap v2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"t0s"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MOTO"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"t1s"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"WETH"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tx"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0x1345bed7cd96a35ae5543fdbfde710c1f7b41a3a365fc31b4c3c52e47512028b"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The maker address lets you count "the same wallet on both sides" instead of guessing. The log index makes "between" exact inside a block. You get no mempool and no MEV labels, and you don't need them: a middleman has to print.&lt;/p&gt;

&lt;h2&gt;
  
  
  First trap: the block number is a string
&lt;/h2&gt;

&lt;p&gt;Look at &lt;code&gt;h&lt;/code&gt; and &lt;code&gt;lgid&lt;/code&gt; again. They're quoted. The amounts on the same row are numbers, but the two fields that place a swap in the chain arrive as strings. Sort them as text and &lt;code&gt;"99"&lt;/code&gt; comes after &lt;code&gt;"1000"&lt;/code&gt;. Every "between" in the detector is then wrong, and nothing raises an error.&lt;/p&gt;

&lt;p&gt;So the first function in the engine does exactly one thing:&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sort_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;(block, log index) as integers, or None when the row cannot be placed.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lgid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;h&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="nf"&gt;_int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lgid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;lgid&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lgid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rows that can't be placed are dropped and counted, and every receipt states the count. A regression test pins the trap by name: &lt;code&gt;test_block_and_log_index_are_sorted_as_integers_not_as_the_strings_they_arrive_as&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The join
&lt;/h2&gt;

&lt;p&gt;With prints in chain order and grouped per pool (venue plus the two token contracts, because the rows carry no pool address), the rule is short. For each print by wallet A, find A's next print in the same block. If it's the other side and the size matches within 5 %, A stood on both sides of the block. The prints between the two legs decide the shape:&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_legs_match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tol&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Two rows that could be the two legs of one middleman: same wallet, same block,
    opposite sides, matched size.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ma&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
        &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ma&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ma&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;_int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;h&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
        &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;_int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;h&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nf"&gt;_int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;h&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;side&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;buy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sell&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;side&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;buy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sell&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;side&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nf"&gt;side&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;size_match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tol&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sandwich (A-B-A):&lt;/strong&gt; every print between the legs is another maker trading in leg 1's direction, there are 1 to 4 of them, and A came out ahead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Round-trip (A-A):&lt;/strong&gt; anything else. Usually nothing between, in one transaction: the same wallet buying back what it just sold.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Organic:&lt;/strong&gt; every print that isn't a leg. Victims stay organic. They're real fills, and what they paid is the question.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What the join actually found
&lt;/h2&gt;

&lt;p&gt;The day-1 spike ran this on 1,200 real prints from the busiest pair and found &lt;strong&gt;zero&lt;/strong&gt; same-block sandwiches. A ten-token census across Ethereum, BSC and Solana found &lt;strong&gt;2 in 8,000 prints&lt;/strong&gt;: the same wallet, front-running a seller on UNI and on LINK, for $0.95 and $2.19.&lt;/p&gt;

&lt;p&gt;That's a sandwich rate of 0.025 %. It's not a headline.&lt;/p&gt;

&lt;p&gt;The round-trips were a headline. On MOTO/WETH, CoinMarketCap's #1 Uniswap v2 pair on Ethereum by 24-hour transactions at capture time, the last 800 prints (3.6 hours) held &lt;strong&gt;29 round-trips by 3 wallets&lt;/strong&gt;, every one inside a single transaction. They made up &lt;strong&gt;66.2 % of the pool's volume&lt;/strong&gt;: $127,254 of $192,275. The first one, straight from the receipt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;leg 1  lgid 209  0xc9160fdab187f2e55567b760d88a87ae7fe56d95  sell  a1/a0 = 1.0430246602456774 / 842991.8537715519 = 1.237289133434893e-06
leg 2  lgid 218  0xc9160fdab187f2e55567b760d88a87ae7fe56d95  buy   a1/a0 = 1.0325944136432206 / 829640.6520318444 = 1.2446285161103536e-06
same wallet · same block 26006339 · same tx · |Δa0| / a0 = 0.0158 ≤ 0.05 → round-trip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So I changed the headline before building the page, not in a caveat after. The engine didn't change: same feed, same ordering, same maker join. The sandwich became one named case of a middleman, and the page leads with the number the join actually found.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it matters to the person placing the order
&lt;/h2&gt;

&lt;p&gt;Round-trip legs aren't trades anyone else can fill against, yet they sit on the tape. If you measure what a fill pays against the print before it across the raw tape, MOTO/WETH says &lt;strong&gt;55.3 bps&lt;/strong&gt; at the median. Take out the legs and measure only organic prints against the organic print before them, and the median is &lt;strong&gt;15.5 bps&lt;/strong&gt; (p90 60.9). The legs roughly triple the apparent cost of trading there.&lt;/p&gt;

&lt;p&gt;The measurement is deliberately plain. It's the adverse-signed move of each print against the previous one:&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tp&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;buy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;bps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;BPS&lt;/span&gt;
&lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;tp&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sell&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;bps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;BPS&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The price is always &lt;code&gt;a1 / a0&lt;/code&gt;. The feed's own &lt;code&gt;q&lt;/code&gt; field is rounded to two significant figures, and on Uniswap v4 rows it's sometimes zero. From the organic p90 the tool picks a route and a slippage cap. For MOTO: Uniswap v2 / WETH, cap 0.65 %.&lt;/p&gt;

&lt;h2&gt;
  
  
  The number moved, and that's the point
&lt;/h2&gt;

&lt;p&gt;I ran the same bare command 24 minutes later and it measured &lt;strong&gt;27.5 %&lt;/strong&gt;: one wallet, eight round-trips. The next morning the window was &lt;strong&gt;clean&lt;/strong&gt;, zero round-trips, and the tool printed zero on its own headline.&lt;/p&gt;

&lt;p&gt;Two days after that, the hero rule (it's a rule, not a hard-coded token) picked a different pair, wildebeest/WETH. This time the join found the other shape: &lt;strong&gt;17 sandwiches, 18 victim prints, 4 wallets&lt;/strong&gt; in a 4.5-hour window. The first wallet listed was the one the census had caught on UNI and LINK.&lt;/p&gt;

&lt;p&gt;So the census number stands as a census number: sandwiches are rare on average, not rare everywhere. All four transcripts are in &lt;a href="https://github.com/edycutjong/middleman/blob/main/DEMO.md" rel="noopener noreferrer"&gt;DEMO.md&lt;/a&gt; with their receipts, and &lt;code&gt;python3 scripts/verify_tape.py&lt;/code&gt; re-derives every published figure from the committed tapes offline.&lt;/p&gt;

&lt;p&gt;I also got the classification wrong once. The first cut of the join called four prints on a BSC pair sandwiches. They were two wallets washing around each other, each buying back for exactly what it received. A sandwich now requires the attacker to come out ahead, and a same-wallet return that extracted nothing is a round-trip. That's the one thing it's safe to call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A run measures the last 800 prints, not 24 hours.&lt;/strong&gt; That was 3.6 h on MOTO and 20 h on SHIB. The span is on every row.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Uniswap v3 fee tiers of one pair merge.&lt;/strong&gt; The feed carries no pool address, so the number is per pair, not per tier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quote-to-fill includes fees and is realised.&lt;/strong&gt; On a busy 0.30 % pool, consecutive opposite-side prints straddle the fee twice, so the p90 sits near 60 bps. Use it comparatively.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A round-trip is a shape, not a verdict.&lt;/strong&gt; The tool prints the rows and the definition and never labels a person.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The anonymous tier throttles per IP,&lt;/strong&gt; with no &lt;code&gt;Retry-After&lt;/code&gt;. The CLI backs off and says so.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Run it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/edycutjong/middleman.git &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;middleman
python3 scripts/middleman.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;pip install&lt;/code&gt;, no &lt;code&gt;.env&lt;/code&gt;, no key. It asks CoinMarketCap which Uniswap v2 pair is busiest right now, pulls 800 prints and prints the table, the route and the raw rows of the first middleman it finds. It takes about ten seconds and uses 0 credits.&lt;/p&gt;

&lt;p&gt;The live page is at &lt;a href="https://middleman.edycu.dev" rel="noopener noreferrer"&gt;middleman.edycu.dev&lt;/a&gt;, the code is at &lt;a href="https://github.com/edycutjong/middleman" rel="noopener noreferrer"&gt;github.com/edycutjong/middleman&lt;/a&gt;, and there's a 3-minute demo at &lt;a href="https://youtu.be/BwikjIf19hQ" rel="noopener noreferrer"&gt;youtu.be/BwikjIf19hQ&lt;/a&gt;. If you've ever wondered what your fill actually paid for, the rows are one keyless call away.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>python</category>
      <category>api</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Two in three 'liquidity pulled' alerts are the same wallet putting it back</title>
      <dc:creator>Edy Cu</dc:creator>
      <pubDate>Wed, 23 Sep 2026 04:37:13 +0000</pubDate>
      <link>https://dev.to/edycutjong/two-in-three-liquidity-pulled-alerts-are-the-same-wallet-putting-it-back-5f2f</link>
      <guid>https://dev.to/edycutjong/two-in-three-liquidity-pulled-alerts-are-the-same-wallet-putting-it-back-5f2f</guid>
      <description>&lt;p&gt;A treasury bot pings: &lt;strong&gt;LP removed, −$21,330,275, UNI/WBTC.&lt;/strong&gt; Somebody pulls up a chart, somebody asks in the group chat whether the market maker left, and for the next hour the answer is a guess.&lt;/p&gt;

&lt;p&gt;The answer was sitting in the same API, one query away. 132 seconds later the same wallet put 99.8% of that money — the same 1,962,475.5391248302 UNI, to the last decimal — into UNI/WETH, one pool over. The alert was true. The panic was not.&lt;/p&gt;

&lt;p&gt;So I stopped looking at one removal and followed 335 of them. &lt;strong&gt;Two in three "liquidity pulled" alerts were the same wallet putting it back within six hours.&lt;/strong&gt; This post is about that number, the one API parameter that produces it, and the two kinds of row that almost made it wrong.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Live: &lt;a href="https://forwarding.edycu.dev" rel="noopener noreferrer"&gt;forwarding.edycu.dev&lt;/a&gt; (the short path is &lt;a href="https://forwarding.edycu.dev/judge" rel="noopener noreferrer"&gt;/judge&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/edycutjong/forwarding" rel="noopener noreferrer"&gt;github.com/edycutjong/forwarding&lt;/a&gt; — MIT, stdlib-only Python, no API key&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The alert everyone fires, and the row nobody reads
&lt;/h2&gt;

&lt;p&gt;CoinMarketCap's DEX API has &lt;code&gt;/v1/dex/liquidity-change/list&lt;/code&gt;: every add and remove on a token's pools, with a USD value (&lt;code&gt;tu&lt;/code&gt;), a side (&lt;code&gt;tp&lt;/code&gt;), a timestamp, and — the part that matters — the wallet that did it, &lt;code&gt;m&lt;/code&gt;. Filter it with &lt;code&gt;minVolume=100000&lt;/code&gt;, sort by &lt;code&gt;tu&lt;/code&gt;, and you have an "LP removed" alert bot. That is the obvious product, and it stops at the first row.&lt;/p&gt;

&lt;p&gt;The endpoint also accepts &lt;code&gt;maker=&lt;/code&gt; as a &lt;strong&gt;server-side filter&lt;/strong&gt;. One keyless call returns everything one wallet did across every pool of the token. That is the whole trick — the second row was never hidden, it was just never joined:&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;follow_maker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maker&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;t0_ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;back_h&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;W_BACK_H&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fwd_h&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;W_FWD_H&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;FOLLOW_PAGES&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;THE JOIN. Every liquidity event by `maker` on `address`, inside t0 -+ the window.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;lo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t0_ms&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;back_h&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3600_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;meta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;walk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;platform&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;address&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;maker&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;maker&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;until&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;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;ts_ms&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;lo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;window_rows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t0_ms&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;t0_ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;back_h&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;back_h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fwd_h&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;fwd_h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;walk()&lt;/code&gt; with a cursor is there because &lt;code&gt;startTime&lt;/code&gt; is plan-gated on the keyless tier (HTTP 403, error 1013) while &lt;code&gt;endTime&lt;/code&gt; is not, so a ±6 h window has to be paged back to with &lt;code&gt;lastId&lt;/code&gt; and cut client-side on &lt;code&gt;ts&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here are the two rows from the live run, verbatim fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;remove  ts=1788321551000 tu=-21330274.564875204 m=0x4f0aa5900b8292273b2f9a178d5468f8048bb9a9 en=Ring Exchange (Ethereum) a0=-1962475.5391248302
add     ts=1788321683000 tu=21287254.934237212  m=0x4f0aa5900b8292273b2f9a178d5468f8048bb9a9 en=Ring Exchange (Ethereum) a0=1962475.5391248302
        21,287,254.93 ÷ 21,330,274.56 = 0.9980
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same maker, same token amount, a different pair. That run made 14 calls, all 200, 0 credits, in 50.6 s, and every response body is embedded in the receipt under the sha256 the trace prints.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning two rows into a verdict
&lt;/h2&gt;

&lt;p&gt;Once you have the wallet's rows inside the window, the classification is arithmetic on &lt;code&gt;tu&lt;/code&gt; — no model produces a number. The adds are split into &lt;em&gt;same pool&lt;/em&gt; and &lt;em&gt;other pools&lt;/em&gt;, and the shares decide:&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;same_share&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;FULL&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;same_share&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;other_share&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;REBALANCE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;other_share&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;FULL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MIGRATION&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;dest&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;dest&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pubAt_ms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;dest&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pubAt_ms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ts_ms&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;removal&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CONSOLIDATION&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;total_share&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;PARTIAL_MIN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PARTIAL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EXIT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INCOMPLETE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;FULL&lt;/code&gt; is 0.70 and &lt;code&gt;PARTIAL_MIN&lt;/code&gt; is 0.10. The last two lines carry the rule I care most about: an &lt;strong&gt;EXIT is only allowed when every follow completed&lt;/strong&gt;. If a page was throttled or the walk never reached the start of the window, the answer is &lt;code&gt;INCOMPLETE&lt;/code&gt;, never "they left". Calling a rebalance an exit is the exact false alarm this exists to kill, so the tool is not allowed to produce one from missing data.&lt;/p&gt;

&lt;p&gt;The same follow runs on the asset's other EVM chains too: &lt;code&gt;/v1/dex/search&lt;/code&gt; and &lt;code&gt;/v2/cryptocurrency/info&lt;/code&gt; resolve the same CoinMarketCap asset to its contract on each chain (13 for UNI), and a chain is followed only if the asset has a market there. &lt;code&gt;/v4/dex/pairs/quotes/latest&lt;/code&gt; then confirms the destination pool actually holds the money now.&lt;/p&gt;

&lt;h2&gt;
  
  
  The number
&lt;/h2&gt;

&lt;p&gt;Following one removal proves the mechanism. To measure how often the alert is the wrong headline, I ran the same-chain follow over every non-JIT removal ≥ $100k on an 11-token, 3-chain watchlist — 335 removals, 352 keyless calls, 0 credits, about 20 minutes of wall clock:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Outcome of a "≥ $100k removed" alert&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;th&gt;Share&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Rebalance — ≥ 70% back into the same pool&lt;/td&gt;
&lt;td&gt;194&lt;/td&gt;
&lt;td&gt;57.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Migration — ≥ 70% into a different pool&lt;/td&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;td&gt;5.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Partial — 10–70% came back&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;3.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No same-wallet re-add within ±6 h&lt;/td&gt;
&lt;td&gt;112&lt;/td&gt;
&lt;td&gt;33.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;223 of 335 — 66.6% — came back. The last row is deliberately &lt;em&gt;not&lt;/em&gt; labelled "exits": the base-rate sweep follows the same chain only, so 33.4% is an upper bound on real exits, not a count of them. The receipt is &lt;a href="https://github.com/edycutjong/forwarding/blob/main/docs/proof/base_rate.json" rel="noopener noreferrer"&gt;&lt;code&gt;docs/proof/base_rate.json&lt;/code&gt;&lt;/a&gt;, and every row in it can be re-derived.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two kinds of row that nearly broke it
&lt;/h2&gt;

&lt;p&gt;The number above only holds because two kinds of row are refused before anything is counted. Both came from running against the live API, not from the docs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A removal worth 10⁴² dollars.&lt;/strong&gt; On 2026-09-19, thirteen rows for the real CAKE contract on an unlabeled BSC venue carried &lt;code&gt;tu = -1e+42&lt;/code&gt; — and &lt;code&gt;a0 = -1e+42&lt;/code&gt; CAKE, trillions of times the supply. They pass &lt;code&gt;minVolume&lt;/code&gt;, and in an 11-token sweep they out-ranked every real removal. Sort by &lt;code&gt;tu&lt;/code&gt;, which is the obvious thing to do, and a scam pool's overflow is the top of every list. The guard is one constant and one function:&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;# A single liquidity event above this is not a removal, it is a price-feed artefact: live
# 2026-09-19, 13 CAKE rows on an unlabeled BSC venue carried tu = -1e42 (and a0 = -1e42 CAKE,
# trillions of times the supply) and out-ranked every real removal on the watchlist.
&lt;/span&gt;&lt;span class="n"&gt;SANE_USD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;10_000_000_000.0&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;plausible&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;False for a row whose USD value no market could have produced (see SANE_USD).&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;usd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;SANE_USD&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Removals that are really one swap.&lt;/strong&gt; Just-in-time liquidity — a searcher adds liquidity for a single swap and pulls it in the same transaction. 85 of 100 PEPE rows were JIT pairs; so were 28 AAVE, 27 DAI and 31 CAKE transactions in 100 rows. Every one of them is a "removal" to a naive consumer. Nothing in the row flags it, but both sides share a &lt;code&gt;txn&lt;/code&gt;:&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;jit_txns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Transactions that both add and remove — just-in-time liquidity, not an event.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;sides&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;sides&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setdefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;txn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&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;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;sides&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;add&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;remove&lt;/span&gt;&lt;span class="sh"&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="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Neither guard is clever. Both are the kind of thing you only write after the live data surprises you, which is the argument for building against the real API from the first hour instead of against fixtures.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pool identity is venue + pair, not pool address.&lt;/strong&gt; Rows never carry the pool contract, so three Uniswap v3 fee tiers of one pair collapse into one identity. The card says so.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The window is ±6 h.&lt;/strong&gt; A wallet that comes back a day later reads as "no re-add" for that window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A wallet is not an entity.&lt;/strong&gt; Liquidity moved through a second wallet is not followed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A wallet that cycles more than once in the window&lt;/strong&gt; can show a share over 100%; the adds are attributed to the whole window, and the output discloses the other removals.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The anonymous tier throttles per IP&lt;/strong&gt;, reported as 429/1022, 429/1011 or 500, with no &lt;code&gt;Retry-After&lt;/code&gt;. The client backs off 15/30/60 s and records the retry in the receipt; the hosted page leads with the dated receipt for that reason.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The whole thing is one stdlib Python file on the default path, an unattended &lt;code&gt;watch&lt;/code&gt; loop, and a three-tool MCP server (&lt;code&gt;claude mcp add forwarding -- python3 $PWD/scripts/mcp_server.py&lt;/code&gt;). 324 offline tests, 100% statement and branch coverage, gated in CI.&lt;/p&gt;

&lt;p&gt;Run it yourself — no key, no signup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/edycutjong/forwarding.git &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;forwarding
python3 scripts/forwarding.py investigate &lt;span class="nt"&gt;--platform&lt;/span&gt; ethereum &lt;span class="nt"&gt;--address&lt;/span&gt; 0x1f9840a85d5af5bf1d1762f925bdaddc4201f984
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The removal it picks comes from the market, so yours may differ from the one above. The live page is at &lt;a href="https://forwarding.edycu.dev" rel="noopener noreferrer"&gt;forwarding.edycu.dev&lt;/a&gt;, a 2:54 demo is on &lt;a href="https://youtu.be/ED5h27Xi2V0" rel="noopener noreferrer"&gt;YouTube&lt;/a&gt;, and the twelve dated things the API did that surprised me are in &lt;a href="https://github.com/edycutjong/forwarding/blob/main/FEEDBACK.md" rel="noopener noreferrer"&gt;FEEDBACK.md&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you run an alert on "liquidity removed", query the same endpoint once more with &lt;code&gt;maker=&lt;/code&gt; before you send it.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>python</category>
      <category>api</category>
      <category>web3</category>
    </item>
    <item>
      <title>My fact-checker said CONFIRMED about a group that doesn't exist</title>
      <dc:creator>Edy Cu</dc:creator>
      <pubDate>Wed, 23 Sep 2026 00:13:27 +0000</pubDate>
      <link>https://dev.to/edycutjong/my-fact-checker-said-confirmed-about-a-group-that-doesnt-exist-2eo8</link>
      <guid>https://dev.to/edycutjong/my-fact-checker-said-confirmed-about-a-group-that-doesnt-exist-2eo8</guid>
      <description>&lt;p&gt;I built a tool that fact-checks crypto claims. You paste a sentence like &lt;em&gt;"Whales are holding $XYZ"&lt;/em&gt; and it plans the on-chain calls that claim needs, runs them against &lt;a href="https://nansen.ai" rel="noopener noreferrer"&gt;Nansen&lt;/a&gt;'s API, and answers &lt;strong&gt;CONFIRMED / OVERSTATED / CONTRADICTED / UNVERIFIABLE&lt;/strong&gt; with the numbers that decided it.&lt;/p&gt;

&lt;p&gt;Last week it answered &lt;strong&gt;CONFIRMED&lt;/strong&gt; about a group of wallets that does not exist.&lt;/p&gt;

&lt;p&gt;Not "returned a weak signal." Not "was slightly off." It printed the word that means &lt;em&gt;this claim is true&lt;/em&gt;, next to a cohort the data source tags exactly zero members of. Here is how a tool whose entire purpose is refusing to overstate ended up overstating, and why the README had already warned me.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Live: &lt;strong&gt;&lt;a href="https://rebuttal.edycu.dev" rel="noopener noreferrer"&gt;https://rebuttal.edycu.dev&lt;/a&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Code: &lt;strong&gt;&lt;a href="https://github.com/edycutjong/rebuttal" rel="noopener noreferrer"&gt;https://github.com/edycutjong/rebuttal&lt;/a&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The promise I'd already written down
&lt;/h2&gt;

&lt;p&gt;Three limitations ship in the repo's &lt;code&gt;JUDGE.md&lt;/code&gt;. The second one says this, verbatim:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Nansen's Whale label is sparse.&lt;/strong&gt; A post's "whale" is often a big wallet Nansen does not tag; when no Whale-labelled wallet exists in the token the tool says UNVERIFIABLE rather than pretending a CONTRADICTED.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I wrote that line early, and I wrote it because it is &lt;em&gt;true of the data&lt;/em&gt;. My API notes from the build (&lt;code&gt;docs/DX-REPORT.md&lt;/code&gt;) record the measurement it came from:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;tgm/flow-intelligence&lt;/code&gt; 1d on WETH: &lt;code&gt;whale_wallet_count = 0&lt;/code&gt;. &lt;code&gt;tgm/holders label_type=whale&lt;/code&gt; on the same token: 38 holders.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Zero labelled whales on wrapped Ether. Not an edge case — the label is genuinely sparse, and the flow columns that report it are DEX-only. "No wallets of this class" is an ordinary, frequent answer from this API, not a rare one.&lt;/p&gt;

&lt;p&gt;So I knew. I documented it. And then I enforced it in exactly one of the two places it mattered.&lt;/p&gt;

&lt;h2&gt;
  
  
  The choke point that wasn't
&lt;/h2&gt;

&lt;p&gt;Claims come in three shapes: &lt;em&gt;buying&lt;/em&gt;, &lt;em&gt;selling&lt;/em&gt;, &lt;em&gt;holding&lt;/em&gt;. The first two go down one branch, holding goes down another. Both need the same question answered first — &lt;strong&gt;does this class of wallet exist in this token at all?&lt;/strong&gt; That question has a function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/** Does the subject class exist in this token at all (any window, any endpoint)? */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;presence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Evidence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;smart_trader&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;whale&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;flow1d&lt;/span&gt;&lt;span class="p"&gt;?.[&lt;/span&gt;&lt;span class="nx"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nx"&gt;wallets&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;flow7d&lt;/span&gt;&lt;span class="p"&gt;?.[&lt;/span&gt;&lt;span class="nx"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nx"&gt;wallets&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;holders&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cls&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;smart_trader&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;traders&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&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="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;named&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;named&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;buyRows&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;named&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sellRows&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The flow branch calls it, and does the right thing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;presence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;R&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;UNVERIFIABLE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;U-NOCLASS&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s2"&gt;`Nansen tags no wallet as &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;who&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; in this token (24 h, 7 d, holders) — `&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="s2"&gt;`the wallet in the post is not one Nansen labels`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;R&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CONTRADICTED&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;C-NOBODY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The holding branch never called it. Not once. Here is what it did instead — read it as a ladder, because that is how it fails:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;decideHolding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;claim&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;T7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;R&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;who&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;subjectName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;claim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// "Whales" | "Smart Money"&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;subjectClass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;claim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// "whale"  | "smart_trader"&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;holders&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;net7&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;flow7d&lt;/span&gt;&lt;span class="p"&gt;?.[&lt;/span&gt;&lt;span class="nx"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nx"&gt;net&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;net7&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;R&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;UNVERIFIABLE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;U-HOLD&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="cm"&gt;/* no data at all */&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="c1"&gt;// ↓ no presence() check here ↓&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;minHolders&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;R&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;OVERSTATED&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;O-HOLDERS&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;net7&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;net7&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;T7&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;delta7d&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;R&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CONTRADICTED&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;C-EXIT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;delta7d&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&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="nx"&gt;net7&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;net7&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;T7&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;R&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;OVERSTATED&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;O-TRIM&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;R&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CONFIRMED&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A-HOLD&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="cm"&gt;/* "balances are not shrinking" */&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Two ways to be wrong, and the second one is much worse
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Zero holders.&lt;/strong&gt; &lt;code&gt;tgm/holders&lt;/code&gt; answers with an empty page, so &lt;code&gt;h.count === 0&lt;/code&gt;. The first rung catches it — &lt;code&gt;0 &amp;lt; 5&lt;/code&gt; — and returns &lt;code&gt;OVERSTATED&lt;/code&gt;, whose UI copy means &lt;em&gt;partly true&lt;/em&gt;. Partly true about nobody. Bad, but at least it is hedging.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The holders call failed.&lt;/strong&gt; Now &lt;code&gt;h&lt;/code&gt; is &lt;code&gt;null&lt;/code&gt;, and the seven-day flow answered with &lt;code&gt;wallets: 0&lt;/code&gt;. Look at the ladder again: every remaining rung is guarded by &lt;code&gt;h &amp;amp;&amp;amp;&lt;/code&gt;. &lt;code&gt;O-HOLDERS&lt;/code&gt; needs &lt;code&gt;h&lt;/code&gt;. &lt;code&gt;C-EXIT&lt;/code&gt; needs &lt;code&gt;h&lt;/code&gt;. &lt;code&gt;O-TRIM&lt;/code&gt; needs &lt;code&gt;h&lt;/code&gt; or a net flow below the negative threshold — and the net flow is &lt;code&gt;0&lt;/code&gt;, which is not below anything. So the claim falls all the way through and lands on the last line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;label:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CONFIRMED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;rule:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A-HOLD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;presence:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;reasons:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"7 d Whales net flow $0 (threshold $5K)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
             &lt;/span&gt;&lt;span class="s2"&gt;"Whales balances are not shrinking"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"Whales balances are not shrinking." Technically unfalsifiable and completely true, in the way that &lt;em&gt;"all the unicorns in my garage are healthy"&lt;/em&gt; is true. There are no whales. Nothing is shrinking because there is nothing. And the tool rendered that as &lt;strong&gt;CONFIRMED&lt;/strong&gt;, the strongest word it owns, on the one screen a reader actually looks at.&lt;/p&gt;

&lt;p&gt;The path needs no exotic input. One failed HTTP call to a sparse endpoint, on a token whose class is empty — which, per my own notes, is WETH.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix is one line, which is the annoying part
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;presence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;R&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;UNVERIFIABLE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;U-NOCLASS&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="cm"&gt;/* same message the flow path uses */&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One line, plus a comment, plus 45 lines of tests. The whole commit is &lt;code&gt;+51&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I found the first case because I ran the source past an outside model, which spotted the zero-holders rung. Verifying its claim is what turned up the second: I wrote a throwaway test with &lt;code&gt;holders: null&lt;/code&gt; to see what the other branch did, and got &lt;code&gt;CONFIRMED&lt;/code&gt; back. The review found the hedge; checking the review found the lie.&lt;/p&gt;

&lt;p&gt;All 13 recorded fixtures still reproduce with byte-identical sha256 evidence hashes after the change, which is the thing that let me ship a one-line edit to the verdict engine four days before a deadline without flinching.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually take from this
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A limitation stated in prose is not a constraint.&lt;/strong&gt; I had the right belief, written in the right file, in public, in the artifact judges read. It did nothing. Prose cannot fail a build.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An invariant enforced in one branch is enforced in zero branches.&lt;/strong&gt; &lt;code&gt;presence()&lt;/code&gt; existed. It was correct. It was even &lt;em&gt;called&lt;/em&gt; — just not on every path that needed it. A shared helper that callers must remember to call is a convention, and conventions decay the moment someone adds a fourth claim type.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The dangerous bug was in the fallback, not the happy path.&lt;/strong&gt; Every test I had written covered tokens &lt;em&gt;with&lt;/em&gt; data, because that is what the demo uses and what the fixtures recorded. The failure mode lived where the data was absent — &lt;code&gt;h === null&lt;/code&gt;, &lt;code&gt;wallets: 0&lt;/code&gt;, a call that didn't answer — and absence is exactly what nobody writes fixtures for. If you have a decision function with a default return at the bottom, the question worth asking is not "is this the right default" but &lt;strong&gt;"what is the emptiest input that can reach this line?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sparse-by-design data is a first-class input.&lt;/strong&gt; I treated "no whales" as degenerate. It is not. It is Wednesday.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limits, since that is the whole point of the tool
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The fix makes the engine refuse more often, and refusing is the correct answer, but it is still a refusal — a user who pastes a claim about an unlabelled wallet gets &lt;code&gt;UNVERIFIABLE&lt;/code&gt;, not an investigation of who that wallet is.&lt;/li&gt;
&lt;li&gt;Two Nansen endpoints can report different numbers for the same 24 hours (&lt;code&gt;flow-intelligence&lt;/code&gt; vs &lt;code&gt;smart-money/netflow&lt;/code&gt;). I surface both rather than reconcile them; &lt;code&gt;docs/DX-REPORT.md&lt;/code&gt; has the measurement.&lt;/li&gt;
&lt;li&gt;Net flows are priced at current rates and drift. The evidence hash covers the integers that decided the label, so a fixture replay always matches and a live re-run usually does.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the numbers people ask for: 352 tests, 100% statement/branch/function/line coverage on the engine, 13 fixtures that replay offline with zero network calls, cold p50 4.0 s / p95 5.0 s, ~10 API credits per verdict, 0 of 154 live calls failed in the benchmark. Details in &lt;code&gt;docs/BENCH.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The tool is at &lt;strong&gt;&lt;a href="https://rebuttal.edycu.dev" rel="noopener noreferrer"&gt;https://rebuttal.edycu.dev&lt;/a&gt;&lt;/strong&gt; and the code is at &lt;strong&gt;&lt;a href="https://github.com/edycutjong/rebuttal" rel="noopener noreferrer"&gt;https://github.com/edycutjong/rebuttal&lt;/a&gt;&lt;/strong&gt; — the commit in this post is &lt;code&gt;fedb50b&lt;/code&gt;, and the test that pins both failure paths is &lt;code&gt;packages/core/test/review4.test.ts&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you have a verdict function with a &lt;code&gt;return&lt;/code&gt; at the bottom of a ladder, go and check what the emptiest possible input does to it. That is the whole article.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>typescript</category>
      <category>api</category>
      <category>testing</category>
    </item>
    <item>
      <title>has_tokens: true is a boolean. 476 of 791 have no market behind them.</title>
      <dc:creator>Edy Cu</dc:creator>
      <pubDate>Tue, 22 Sep 2026 08:00:43 +0000</pubDate>
      <link>https://dev.to/edycutjong/hastokens-true-is-a-boolean-476-of-791-have-no-market-behind-them-3666</link>
      <guid>https://dev.to/edycutjong/hastokens-true-is-a-boolean-476-of-791-have-no-market-behind-them-3666</guid>
      <description>&lt;p&gt;A desk that sizes a tokenised-equity position on &lt;code&gt;has_tokens: true&lt;/code&gt; finds out at the ticket that the one Morgan Stanley wrapper on CoinMarketCap has no price, no volume, and no market CoinMarketCap tracks.&lt;/p&gt;

&lt;p&gt;I built a small tool to count how often that happens. The first draft of the headline said the wrappers had &lt;strong&gt;never traded&lt;/strong&gt;. That was wrong, and the way it was wrong is the most useful thing I learned in the build — so this post is about the retraction as much as the number.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Live: &lt;a href="https://shelfware.edycu.dev" rel="noopener noreferrer"&gt;shelfware.edycu.dev&lt;/a&gt; (the judge page is &lt;a href="https://shelfware.edycu.dev/judge" rel="noopener noreferrer"&gt;/judge&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/edycutjong/shelfware" rel="noopener noreferrer"&gt;github.com/edycutjong/shelfware&lt;/a&gt; — MIT, stdlib-only Python&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Two rows, two endpoint families, one word
&lt;/h2&gt;

&lt;p&gt;CoinMarketCap's RWA API is new. &lt;code&gt;/v5/real-world-assets/map&lt;/code&gt; lists every underlying it knows about — 7,811 of them — and flags 791 as &lt;code&gt;has_tokens: true&lt;/code&gt;. &lt;code&gt;/v5/real-world-assets/quotes/latest&lt;/code&gt; returns, per underlying, a &lt;code&gt;tokens[]&lt;/code&gt; array of the wrappers minted against it, with the issuer that minted each one.&lt;/p&gt;

&lt;p&gt;Here is Morgan Stanley on that surface, trimmed to the fields that matter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"symbol"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Morgan Stanley"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"asset_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stock"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rwa_rank"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;43&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"has_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"crypto_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;41513&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"symbol"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"wMSx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"issuer_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Backed Assets"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"market_cap"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"volume_24h"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;price: null&lt;/code&gt;. Nothing on the RWA surface says why. The reason lives in a different endpoint family, &lt;code&gt;/v1/cryptocurrency/map&lt;/code&gt;, which is keyless and carries a &lt;code&gt;status&lt;/code&gt; per listing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;41513&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"symbol"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"wMSx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"untracked"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"platform"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"X Layer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"token_address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0x2874A11805783324C54562eDB1A641C5d1d077a5"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CoinMarketCap's documentation defines &lt;code&gt;untracked&lt;/code&gt; as &lt;em&gt;"registered cryptocurrency projects that are listed but do not yet meet methodology requirements to have tracked markets."&lt;/em&gt; So: tokenised, yes; a market CoinMarketCap tracks, no. The boolean and the listing state disagree about what "tokenised" means, and nobody joins them — every price-based RWA tool drops the &lt;code&gt;price: null&lt;/code&gt; rows before it starts.&lt;/p&gt;

&lt;p&gt;Shelfware is built on exactly those rows.&lt;/p&gt;

&lt;h2&gt;
  
  
  The join, and the three counting rules
&lt;/h2&gt;

&lt;p&gt;The engine is pure functions over the two ledgers. The rule that produces the headline:&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;zero_tracked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wrappers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;has_tokens_rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;strict&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="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;rwa_ids of underlyings with no wrapper that has a CMC-tracked market.

    strict (the headline): the underlying has &amp;gt;= 1 attached wrapper and EVERY one is untracked.
    loose: no wrapper is active — also counts unresolved-only and inactive-only underlyings.
    Strict is always &amp;lt;= loose; the headline never takes the larger number.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;groups&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;by_underlying&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wrappers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;has_tokens_rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;ws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rwa_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;strict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ws&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;is_shelf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;out&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="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rwa_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="ow"&gt;not&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;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;active&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;out&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="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rwa_id&lt;/span&gt;&lt;span class="sh"&gt;"&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;out&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;is_shelf&lt;/code&gt; is one line — &lt;code&gt;status == "untracked"&lt;/code&gt; — and that line is the whole retraction. The first draft tested &lt;code&gt;price is None&lt;/code&gt;. It gives the same number today, because on the committed census &lt;code&gt;price == null ⇔ status == "untracked"&lt;/code&gt; held with &lt;strong&gt;0 exceptions across 1,431 resolved wrappers&lt;/strong&gt;. But it is the wrong predicate: a null price is an observation, &lt;code&gt;untracked&lt;/code&gt; is CoinMarketCap's stated reason for it. The suite re-checks the equivalence on every run so the day CoinMarketCap prices an untracked wrapper, the join says so instead of quietly drifting.&lt;/p&gt;

&lt;p&gt;Live on 2026-09-18, 5 keyed credits, 54 calls, 40.9 seconds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;476 of the 791 underlyings flagged &lt;code&gt;has_tokens: true&lt;/code&gt; — 60.2% — have no wrapper with a CMC-tracked market.&lt;/strong&gt; Stocks alone: 437 of 689 (63%).&lt;/li&gt;
&lt;li&gt;673 of the 1,435 wrappers (46.9%) are &lt;code&gt;untracked&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;By issuer: Dinari 27 attached, 0 tracked (100% shelf). Backed 772 attached, 140 tracked (82%), $669M live in the rest. Robinhood 8 of 106 on the shelf. Ondo 4 of 214. bStocks 0 of 77.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Re-derive the headline from the committed rows with no code at all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;jq &lt;span class="s1"&gt;'.wrappers | group_by(.rwa_id) | map(select(all(.[]; .status=="untracked"))) | length'&lt;/span&gt; data/census.json   &lt;span class="c"&gt;# 476&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why "never traded" had to go
&lt;/h2&gt;

&lt;p&gt;The spike that settled it cost 2 credits and is committed as &lt;code&gt;docs/proof/spike.json&lt;/code&gt;. For &lt;code&gt;wMSx&lt;/code&gt;: map status &lt;code&gt;untracked&lt;/code&gt;, &lt;code&gt;quotes/latest&lt;/code&gt; price &lt;code&gt;null&lt;/code&gt;, &lt;code&gt;quotes/historical&lt;/code&gt; &lt;strong&gt;zero points&lt;/strong&gt; — and, on the same RWA row, a &lt;code&gt;tradfi_markets&lt;/code&gt; entry pointing at Binance's tokenised-stock venue for &lt;code&gt;MS&lt;/code&gt;. Dinari's dShares trade on Dinari's own permissioned venue. A pool CoinMarketCap does not index is still a pool.&lt;/p&gt;

&lt;p&gt;So "never traded" was a claim about the world that the data could not support. &lt;code&gt;untracked&lt;/code&gt; is a claim about CoinMarketCap's coverage of what CoinMarketCap calls tokenised — which is exactly why it can be re-derived from CoinMarketCap's own rows. Every surface was re-worded to &lt;em&gt;listed, no CMC-tracked market&lt;/em&gt;, and the readiness gate now fails the build if the stronger phrase comes back. The number did not change. What it means did, and the second meaning is the one an allocator can act on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Answering a ticker with no key
&lt;/h2&gt;

&lt;p&gt;The listing state is on the keyless &lt;code&gt;/public-api&lt;/code&gt; surface, so the ticker question runs from a fresh clone with nothing installed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;git clone https://github.com/edycutjong/shelfware.git &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;shelfware
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; shelfware MS
&lt;span class="go"&gt;
  MS  Morgan Stanley · stock · rwa_rank 43 · has_tokens: true
      ▒ wMSx  Wrapped Morgan Stanley Tokenized Stock (xStock)
          issuer   Backed Assets
          chain    X Layer  0x2874A11805783324C54562eDB1A641C5d1d077a5
          price    null · market_cap null · volume_24h null
          status   UNTRACKED      ← /public-api/v1/cryptocurrency/map, live, keyless, 0 credits
                   listed 2026-08-11 (date_added) · 38 days on the shelf

  0 of 1 wrapper(s) with a CMC-tracked market

receipt: cmc/map symbol=wMSx → HTTP 200 · keyless · 0 credits to any key · 491 ms · sha256 d7e1ecdef293e6c8
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The roster leg (&lt;code&gt;tokens[]&lt;/code&gt;) is keyed, so without a key it comes from the committed snapshot and says so on the row; the state leg is live regardless. Every answer ends in a receipt line — endpoint, HTTP status, credits, body hash — because a number with no receipt is an opinion.&lt;/p&gt;

&lt;p&gt;Numbers, briefly: the whole keyless question is p50 552 ms, p95 603 ms (n=9); the join over the 1,435 committed wrappers is 1.94 ms. 262 tests, 245 of them offline in about a second, engine coverage gated at 100%. A daily snapshot gives the shelf a time axis; day 5 recorded the first movement — &lt;code&gt;MXL&lt;/code&gt; flipped &lt;code&gt;active → untracked&lt;/code&gt; — and the page names it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it breaks
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;untracked&lt;/code&gt; is a listing state, not a claim about every venue. See above; it is the whole point.&lt;/li&gt;
&lt;li&gt;The RWA family is keyed. Keyless callers get a dated roster and a live state.&lt;/li&gt;
&lt;li&gt;CoinMarketCap's map rejects its own dotted symbols (&lt;code&gt;NVDA.D&lt;/code&gt;, &lt;code&gt;AI.FRx&lt;/code&gt; — 38 wrappers, HTTP 400 for the whole call). Those rows fall back to the coarser &lt;code&gt;/v2/cryptocurrency/info&lt;/code&gt; vocabulary, and each row names which source answered.&lt;/li&gt;
&lt;li&gt;4 wrapper ids in &lt;code&gt;tokens[]&lt;/code&gt; resolve on no public CoinMarketCap surface. Shown in their own bucket, never counted as shelf.&lt;/li&gt;
&lt;li&gt;The anonymous tier is per IP; a shared cloud egress can be refused outright. The CLI backs off, answers from the snapshot, and exits 75.&lt;/li&gt;
&lt;li&gt;It is a daily series, not a history. Untracked rows carry no dates; &lt;code&gt;date_added&lt;/code&gt; is a listing day, not a market day.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Twelve dated findings for the CoinMarketCap API team — the plan gates, the two vocabularies for one coin's state, the symbol filter — are in &lt;a href="https://github.com/edycutjong/shelfware/blob/main/FEEDBACK.md" rel="noopener noreferrer"&gt;&lt;code&gt;FEEDBACK.md&lt;/code&gt;&lt;/a&gt; at the repo root, each with the evidence that produced it.&lt;/p&gt;

&lt;p&gt;If you track tokenised assets, type a ticker at &lt;a href="https://shelfware.edycu.dev" rel="noopener noreferrer"&gt;shelfware.edycu.dev&lt;/a&gt; and open the evidence drawer — it shows you the raw rows, so you don't have to take my word for the count either.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>python</category>
      <category>api</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Twice the workers, 14% faster, three broken calls: what a 110-call map taught me about API latency</title>
      <dc:creator>Edy Cu</dc:creator>
      <pubDate>Tue, 22 Sep 2026 05:58:21 +0000</pubDate>
      <link>https://dev.to/edycutjong/twice-the-workers-14-faster-three-broken-calls-what-a-110-call-map-taught-me-about-api-latency-596g</link>
      <guid>https://dev.to/edycutjong/twice-the-workers-14-faster-three-broken-calls-what-a-110-call-map-taught-me-about-api-latency-596g</guid>
      <description>&lt;p&gt;A founder asks "so where are our holders — Korea or the US?" and the honest answer takes &lt;strong&gt;40 seconds&lt;/strong&gt; to compute. Not because the maths is hard. Because the answer is spread across ~110 API calls, and each one takes about two seconds.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://holderatlas.edycu.dev" rel="noopener noreferrer"&gt;Holder Atlas&lt;/a&gt; to draw that answer as one world map: type a token, and the countries its top holders reach exchanges from fill in bubble by bubble, read from Nansen's exchange entity labels and a curated exchange → country table. Code is at &lt;a href="https://github.com/edycutjong/holderatlas" rel="noopener noreferrer"&gt;github.com/edycutjong/holderatlas&lt;/a&gt;. This post is about the part that didn't work: the afternoon I doubled the concurrency to make it faster, got 14 %, and got three broken calls for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where 110 calls come from
&lt;/h2&gt;

&lt;p&gt;A map is one &lt;code&gt;atlas()&lt;/code&gt; function calling five Nansen endpoints. The first two are cheap and fixed — &lt;code&gt;search/general&lt;/code&gt; to resolve the contract (0 credits), &lt;code&gt;tgm/holders&lt;/code&gt; twice for the top-100 population and the exchange-custody subset (5 credits each). The cost is in the per-wallet part:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;per wallet   = tgm/transfers (CEX-only, newest, 1 cr)
             → transaction-with-token-transfer-lookup (1 cr)
             → "🏦 Upbit: Deposit"
country      = exchanges.json[entity]        # Upbit → KR · Coinbase → US · Binance → global (never placed)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Up to 52 wallets get examined (12 exchange-custody + 40 people), two calls each. Here is the tail of a real cold run for PEPE, pasted from the CLI's &lt;code&gt;--explain&lt;/code&gt; output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;117 credits · 110 calls (0 cached) · 57.3s · atlas 241f4d6ce145&lt;/span&gt;

&lt;span class="na"&gt;calls&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="s"&gt;search/general                             1 calls     0 cr  0 cached  580 ms avg live&lt;/span&gt;
  &lt;span class="s"&gt;tgm/holders                                2 calls    10 cr  0 cached  1025 ms avg live&lt;/span&gt;
  &lt;span class="s"&gt;tgm/transfers                             78 calls    78 cr  0 cached  1695 ms avg live&lt;/span&gt;
  &lt;span class="s"&gt;transaction-with-token-transfer-lookup    29 calls    29 cr  0 cached  2007 ms avg live&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;107 of the 110 calls average 1.7–2.0 s each. Serially that would be three and a half minutes; with the 4-wide pool the client ships with, under a 5-requests-per-second bucket, the benchmark across four tokens came out at &lt;strong&gt;cold p50 40.3 s, p95 59.0 s&lt;/strong&gt; (warm p50 7 ms — every call is cached for 24 h and a warm map costs 0 credits).&lt;/p&gt;

&lt;p&gt;Forty seconds is a long time to watch a map fill. So the obvious idea: widen the pool.&lt;/p&gt;

&lt;h2&gt;
  
  
  The experiment: 8-wide under 10 rps
&lt;/h2&gt;

&lt;p&gt;Same bench script (&lt;code&gt;npm run bench&lt;/code&gt;), same four tokens, one cold run each, &lt;code&gt;DEFAULT_CONCURRENCY&lt;/code&gt; 4 → 8 and the rate bucket 5 → 10 rps. Nansen's documented cap is 300 requests per minute, so 10 rps is fine for one map in isolation. Results, side by side with the defaults:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;4-wide / 5 rps (2026-09-18)&lt;/th&gt;
&lt;th&gt;8-wide / 10 rps (2026-09-22)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;cold p50&lt;/td&gt;
&lt;td&gt;40.3 s&lt;/td&gt;
&lt;td&gt;34.5 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cold p95&lt;/td&gt;
&lt;td&gt;59.0 s&lt;/td&gt;
&lt;td&gt;58.1 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;failed calls&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0 / 444&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3 / 441&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;warm hash = cold hash&lt;/td&gt;
&lt;td&gt;4 / 4 tokens&lt;/td&gt;
&lt;td&gt;3 / 4 tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;p50 improved by 14 %. p95 did not move — DEGEN on Base took 59.0 s and then 58.1 s. And the wider run produced three failed lookups where the narrow one had none, all on DEGEN, which is also why its warm hash no longer matched its cold hash: a failed lookup isn't cached, so the second run made different calls and got a different picture.&lt;/p&gt;

&lt;p&gt;I reverted it. The defaults stay 4-wide / 5 rps, and the comment in the client now says why:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Nansen's cap is 300/min. 5 rps keeps one ~110-call atlas inside it with headroom for a second visitor; the rolling&lt;/span&gt;
&lt;span class="c1"&gt;// 300/min window is the hard stop for a third. Measured 2026-09-22 (docs/BENCH.md): 8-wide under 10 rps only moved&lt;/span&gt;
&lt;span class="c1"&gt;// cold p50 40.3 → 34.5 s and introduced 3 failed calls in 441 — per-call latency, not the pool, is the ceiling.&lt;/span&gt;
&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;limiter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RateLimiter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rps&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rpm&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The mental model I had wrong
&lt;/h2&gt;

&lt;p&gt;I was treating the map's latency as a throughput problem: N calls, W workers, finish in N/W × per-call time. Double W, halve the time.&lt;/p&gt;

&lt;p&gt;That model has two holes when the per-call time is 2 s and the server is the one doing the work.&lt;/p&gt;

&lt;p&gt;First, the long pole isn't average-shaped. A map's wall-clock is dominated by its slowest handful of calls — &lt;code&gt;tgm/transfers&lt;/code&gt; with a &lt;code&gt;to_address&lt;/code&gt; filter over a year of a high-volume token is the slow one, and it stays slow no matter how many siblings run beside it. Widening the pool compresses the middle of the distribution and leaves the tail exactly where it was. That is what "p50 −14 %, p95 flat" means.&lt;/p&gt;

&lt;p&gt;Second, concurrency isn't free on the server side either. The client only reports a call as failed after an 8 s timeout and one retry (which also covers 429 and 5xx), so each of those three failures held a worker for up to ~17 s before giving up — and the same lookups had succeeded at 4-wide four days earlier. The bench doesn't record &lt;em&gt;which&lt;/em&gt; of timeout/429/5xx each one was, so I won't claim it. What it does record is that the extra width partly &lt;em&gt;paid for itself in failures&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The honest statement is in the DX report I wrote for Nansen alongside the repo: &lt;strong&gt;latency is the product's ceiling, and it lives in the per-call time, not the client.&lt;/strong&gt; The only thing that would make this a five-second experience is a batch endpoint — N transaction hashes in, N labelled transfers out. Nansen has &lt;code&gt;counterparties/batch&lt;/code&gt;; a &lt;code&gt;transfer-lookup/batch&lt;/code&gt; would do it. That is a wish, not a workaround.&lt;/p&gt;

&lt;p&gt;What did survive the experiment: the rolling 300-per-minute window. The old client only had a per-second bucket, which would happily let two concurrent visitors blow through Nansen's documented per-minute cap. The experiment introduced a second sliding window, and it's kept as a hard stop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/** Two sliding windows: at most `rps` requests per rolling second AND at most `rpm` per rolling minute. */&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RateLimiter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;timestamps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;rps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;rpm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;take&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(;;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timestamps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timestamps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lastSecond&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timestamps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lastSecond&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rps&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timestamps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rpm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timestamps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="c1"&gt;// wait for whichever window is full to open by one slot&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wait&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lastSecond&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rps&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;lastSecond&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="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timestamps&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="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;wait&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The other thing the same bench run caught: 🏦 is not "exchange"
&lt;/h2&gt;

&lt;p&gt;WLFI's attributable share went from 55.2 % to &lt;strong&gt;3.3 %&lt;/strong&gt; between the two runs. My first reaction was that the wider pool had broken something. It hadn't. The wallet holding 52 % of the analysed supply — Upbit's internal wallet — had a &lt;em&gt;newer&lt;/em&gt; exchange touch by the second run: "🏦 Blockchain.com: Deposit". Blockchain.com wasn't in my table yet, so the biggest wallet on the map fell into "entity not in the table" and the number collapsed. Live data moved; the atlas hash said so (&lt;code&gt;warm hash = cold: yes&lt;/code&gt; in both runs, but a different hash on each day).&lt;/p&gt;

&lt;p&gt;That was the second time the bank emoji bit me. Nansen puts 🏦 on exchange entities — "🏦 Binance 14", "🏦 Upbit: Deposit", "🤖 🏦 Coinbase" — which is exactly the field this whole product hangs on. But it also puts 🏦 on DEX pools, staking contracts, bridges and aggregators: "🤖 🏦 Uniswap: V3 USD1-WLFI … Liquidity Pool", "🤖 🏦 PancakeSwap: CAKE Staking Pool", "🤖 🏦 OKX: DEX Aggregator". And the &lt;code&gt;label_type: "exchange"&lt;/code&gt; filter on &lt;code&gt;tgm/holders&lt;/code&gt; returns those too. On CAKE, 5 of the 12 "exchange custody" holders were staking pools.&lt;/p&gt;

&lt;p&gt;If you take 🏦 at face value, a swap into a Uniswap pool reads as "this person used an exchange whose country I don't know" — grey, but the wrong kind of grey — and a staking contract gets counted as exchange custody in the denominator. The fix is to read the name, and to let a real exchange always win:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/**
 * Nansen puts 🏦 on DEX pools, staking contracts and bridges too ("🤖 🏦 Uniswap: PoolManager V4", "🤖 🏦 PancakeSwap: CAKE
 * Staking Pool"). A 🏦 label that names no exchange in the table AND reads as a pool/contract is not
 * an exchange trace at all; the caller treats such a transfer as "no exchange trace" instead of "entity not in the table".
 * A table exchange always wins ("🏦 Binance: Bridge" is still Binance).
 */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isDexOrContractEntity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;EXCHANGE_MARK&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;exchangeOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;entityKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;STRUCTURAL_TAG&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in the engine, a "custody" holder that turns out to be a pool leaves the denominator entirely rather than being placed as global:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;contractLabel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// the "exchange" holder is a pool / staking / bridge contract wearing Nansen's 🏦: structural, out of the denominator&lt;/span&gt;
  &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;structural&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;share&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;untraced&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reclass&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`lookup: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;contractLabel&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; is a pool/contract, not exchange custody`&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;onlyDexParties&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;transfers&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// the exchange-filtered transfer was a swap or a stake (Nansen marks DEX pools 🏦 too): no exchange trace, not a table gap&lt;/span&gt;
  &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;untraced&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every label spelling seen live — "Upbit", "Upbit : Link Wallet", "Upbit: Internal Wallet", "Coinbase Prime: Custody Wallet", the ones with invisible U+200B prefixes — is pinned in &lt;code&gt;labels.test.ts&lt;/code&gt;. 220 tests, 100 % coverage on the core package, plus 24,000 generated property cases that check things like "global is never attributed" and "structural never enters the denominator".&lt;/p&gt;

&lt;h2&gt;
  
  
  What it still doesn't do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Solana can't be named.&lt;/strong&gt; The transfer lookup has no Solana support, so Solana tokens show 0 % placed with a banner saying which field is missing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supply-weighted means whales decide.&lt;/strong&gt; 86 % of LINK's analysed supply is one 2017 team wallet with no exchange trace, so LINK reads 4 % placed; the by-wallet share (44 %) prints beside it for exactly this reason.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Countries are exchange jurisdictions, not people.&lt;/strong&gt; Coinbase → US, Revolut → GB by licence. Binance, OKX, Bybit are global and stay grey on purpose — 47 % of PEPE's analysed supply is that grey bar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's still 40 seconds cold.&lt;/strong&gt; That is the point of this post.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Try it on a token you care about — &lt;a href="https://holderatlas.edycu.dev" rel="noopener noreferrer"&gt;holderatlas.edycu.dev&lt;/a&gt; — and open the provenance drawer to see every one of the ~110 calls with its latency. The repo is &lt;a href="https://github.com/edycutjong/holderatlas" rel="noopener noreferrer"&gt;github.com/edycutjong/holderatlas&lt;/a&gt;; the benchmark that produced every number above is &lt;code&gt;docs/BENCH.md&lt;/code&gt;, and the DX report with all nine frictions is &lt;code&gt;docs/DX-REPORT.md&lt;/code&gt;. Built for Nansen's Meridian buildathon.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>typescript</category>
      <category>api</category>
      <category>performance</category>
    </item>
    <item>
      <title>I claimed my gas refunds were exact. An audit found the 21,000 I was crediting twice.</title>
      <dc:creator>Edy Cu</dc:creator>
      <pubDate>Mon, 21 Sep 2026 09:36:29 +0000</pubDate>
      <link>https://dev.to/edycutjong/i-claimed-my-gas-refunds-were-exact-an-audit-found-the-21000-i-was-crediting-twice-50fd</link>
      <guid>https://dev.to/edycutjong/i-claimed-my-gas-refunds-were-exact-an-audit-found-the-21000-i-was-crediting-twice-50fd</guid>
      <description>&lt;p&gt;My README said the gas refund was &lt;em&gt;exact&lt;/em&gt;. An audit round the next day showed it wasn't: a contract that batched K orders in one transaction was over-refunded 21,000 × (K − 1) gas. Nobody had exploited it, it was bounded by each order's reserve, and it was still not "exact". This post is the mechanism, the mistake, the fix, and the receipts that stayed in the repo.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Live: &lt;a href="https://legwork.edycu.dev/" rel="noopener noreferrer"&gt;https://legwork.edycu.dev/&lt;/a&gt; (reviewer path: &lt;code&gt;/#/judge&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Repo (MIT): &lt;a href="https://github.com/edycutjong/legwork" rel="noopener noreferrer"&gt;https://github.com/edycutjong/legwork&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;2:17 video of a real create → execute on Arc mainnet: &lt;a href="https://youtu.be/YPUbDlJc1lY" rel="noopener noreferrer"&gt;https://youtu.be/YPUbDlJc1lY&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why recurring payments need a keeper network (everywhere else)
&lt;/h2&gt;

&lt;p&gt;A standing order on-chain needs &lt;em&gt;someone&lt;/em&gt; to call it when it's due. Whoever does pays gas. To make that worth their while you have to repay them — and on any chain where gas is ETH and the app's money is an ERC-20, repaying gas &lt;em&gt;in the payment asset&lt;/em&gt; needs a price oracle, and finding executors needs a keeper network. That is the whole reason automation protocols exist.&lt;/p&gt;

&lt;p&gt;On Arc, gas &lt;strong&gt;is&lt;/strong&gt; USDC, with 18-decimal &lt;code&gt;msg.value&lt;/code&gt;. The payment and the fee are the same dollar. So the contract can meter the gas it used, multiply by &lt;code&gt;tx.gasprice&lt;/code&gt; (already USDC), and pay it back from the order's own deposit — in the same transaction, with no oracle and no keeper network. Anyone can run a due order and nets exactly the tip.&lt;/p&gt;

&lt;p&gt;Legwork is that contract, live on Arc mainnet at &lt;code&gt;0x8E2F8AFC29e9dc127103CD6AD5BCfBe661141ccb&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the meter works
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;execute(id)&lt;/code&gt; reads &lt;code&gt;gasleft()&lt;/code&gt; on its first statement, does the work, and reads it again at a fixed measurement point after which nothing of variable cost runs. Everything outside that window — intrinsic gas, calldata, the payout call, the event, one &lt;code&gt;tstore&lt;/code&gt; — is a constant, &lt;code&gt;OVERHEAD&lt;/code&gt;, calibrated on mainnet and baked in as an immutable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function execute(uint256 id) external {
    uint256 g0 = gasleft();                                                     // 1  first statement
    if (_lock != 0) revert Reentrant();                                         // 2  guard (transient)
    _lock = 1;
    Order storage o = orders[id];                                               // 3  load + checks
    if (o.payer == address(0)) revert NoOrder();
    if (o.paused) revert IsPaused();
    uint48 nextDue = o.nextDue;
    if (block.timestamp &amp;lt; nextDue) revert NotDue(nextDue);
    if (_txSeen == 0) { _txSeen = 1; g0 += INTRINSIC_GAS; }                     //    intrinsic credited to the first execute of a transaction only
    uint256 price = tx.gasprice;                                                // 4  price = min(gasprice, 2·basefee, maxGasPrice)
    if (price &amp;gt; block.basefee &amp;lt;&amp;lt; 1) price = block.basefee &amp;lt;&amp;lt; 1;
    if (price &amp;gt; o.maxGasPrice) price = o.maxGasPrice;
    // … pre-check, effects, pay the payee (a refused payment pauses the order instead of reverting) …
    uint256 metered = g0 - gasleft() + (OVERHEAD - INTRINSIC_GAS);              // 9  measurement point
    if (metered &amp;gt; REFUND_CEIL_GAS) metered = REFUND_CEIL_GAS;
    uint256 refund = metered * price;                                           // 10 bounded by the deposit
    if (refund &amp;gt; deposit) refund = deposit;
    o.deposit = uint128(deposit - refund);                                      // 11
    (bool ok,) = msg.sender.call{value: refund + tip, gas: EXECUTOR_GAS}("");   // 12 repay + tip, one call
    if (!ok) revert PayoutFailed();
    emit Executed(id, msg.sender, metered, price, refund, tip, nextDue, paid); // 13 fixed width on both branches
    _lock = 0;                                                                  // 14
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The price is capped at &lt;code&gt;min(tx.gasprice, 2 × basefee, maxGasPrice)&lt;/code&gt; so an executor cannot drain a deposit by choosing an absurd gas price, and the refund is capped at &lt;code&gt;REFUND_CEIL_GAS&lt;/code&gt; and at the deposit itself.&lt;/p&gt;

&lt;p&gt;Then you check it against reality. The transaction receipt has &lt;code&gt;gasUsed&lt;/code&gt; and &lt;code&gt;effectiveGasPrice&lt;/code&gt;, neither of which the contract can see. If &lt;code&gt;metered == gasUsed&lt;/code&gt; and &lt;code&gt;price == effectiveGasPrice&lt;/code&gt;, the refund equals the real fee to the wei. That difference — &lt;code&gt;gasUsed − metered&lt;/code&gt; — is what I call &lt;strong&gt;drift&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The line &lt;code&gt;if (_txSeen == 0) { … }&lt;/code&gt; is the fix. It wasn't there in v1.&lt;/p&gt;

&lt;h2&gt;
  
  
  What v1 got wrong
&lt;/h2&gt;

&lt;p&gt;The transaction's intrinsic 21,000 gas is paid once per &lt;em&gt;transaction&lt;/em&gt;. v1 credited it once per &lt;em&gt;call&lt;/em&gt;. For a wallet calling &lt;code&gt;execute&lt;/code&gt; directly that's the same thing, and every test and every mainnet run said drift 0. But a contract that batches several orders in one transaction pays the intrinsic once and would have been refunded it K times.&lt;/p&gt;

&lt;p&gt;Bounded? Yes — a refund can never exceed the order's reserve. Exploited? No — the only executors were my own two wallets. Exact? No. And "exact" was the word in the README's first paragraph.&lt;/p&gt;

&lt;p&gt;v2 tracks it in transient storage: the first &lt;code&gt;execute&lt;/code&gt; in a transaction claims the intrinsic, later ones in the same transaction don't. The regression test runs two orders through a batching contract and checks the second call's metered figure is the first's minus 21,000 (plus the one &lt;code&gt;tstore&lt;/code&gt; the first call paid):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function test_execute_batchedExecutorIsChargedTheIntrinsicOnce() public {
    // two orders to two existing, cold payees; a batcher runs both in one transaction
    address p1 = makeAddr("p1"); address p2 = makeAddr("p2");
    vm.deal(p1, 1); vm.deal(p2, 1);
    uint256 a = _create(p1, 0.01 ether, 60, 0.01 ether, 100 gwei, 0.05 ether);
    uint256 b = _create(p2, 0.01 ether, 60, 0.01 ether, 100 gwei, 0.05 ether);
    Batcher bt = new Batcher(lw);
    uint256[] memory ids = new uint256[](2); ids[0] = a; ids[1] = b;
    vm.recordLogs();
    bt.go(ids);
    Vm.Log[] memory logs = vm.getRecordedLogs();
    (uint256 m1,,,,,) = abi.decode(logs[0].data, (uint256, uint256, uint256, uint256, uint48, bool));
    (uint256 m2,,,,,) = abi.decode(logs[1].data, (uint256, uint256, uint256, uint256, uint48, bool));
    // the second call's metered figure is the first's minus the 21,000 intrinsic (and minus the one tstore the first paid)
    assertGe(m1 - m2, 21_000 + 100, "intrinsic credited once per transaction (plus the one tstore the first call paid)");
    assertLe(m1 - m2, 21_000 + 300, "nothing else differs between the two calls");
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The change is inside the measured window, so &lt;code&gt;OVERHEAD&lt;/code&gt; didn't move. v1 (&lt;code&gt;0x68a92aF2Be2e6A640a19508a0fe44cbc8B2C62E2&lt;/code&gt;) had its demo orders cancelled and holds nothing — but its 36 execute receipts stay in &lt;code&gt;proof/receipts/&lt;/code&gt; and are still re-checked by &lt;code&gt;npm run recheck&lt;/code&gt;, next to the 36 from v2. Deleting them would have made the repo look cleaner and the claim weaker.&lt;/p&gt;

&lt;p&gt;It was not the only correction that day. &lt;code&gt;OVERHEAD&lt;/code&gt; was estimated at 31,400 before the first deploy; three calibration runs on mainnet measured &lt;strong&gt;32,503&lt;/strong&gt; — under by exactly 1,103 on all three, spread 0. The wrong number stays visible in the deploy record. And the paused branch (payee refuses the payment) meters 6 gas &lt;em&gt;over&lt;/em&gt;, so on that path the executor is over-refunded by 6 × 20 Gwei — about a hundred-millionth of a dollar, and it's documented rather than hidden.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers, after the fix
&lt;/h2&gt;

&lt;p&gt;30 consecutive mainnet executes on the production contract (25 by the payer's wallet, 5 by the payee collecting its own payment):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;refund ÷ real fee = 1.000000 on all 30 rows&lt;/strong&gt; (the pre-stated invariant was 1.00 ± 0.02)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;drift = 0 gas on every row&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;gasUsed&lt;/code&gt; p50 = p95 = &lt;strong&gt;58,415&lt;/strong&gt; (the payee-as-executor rows are 55,915 — the payee is &lt;code&gt;tx.origin&lt;/code&gt; and already warm, and the meter caught that too: drift 0)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tx.gasprice == effectiveGasPrice&lt;/code&gt; on every row, 20 Gwei&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is no randomness to seed and no warm-up to discard: each run is its own transaction. The 107 committed receipts are recomputed from raw chain data by &lt;code&gt;npm run recheck&lt;/code&gt;, which exits non-zero on any drift outside the documented −6…0 window.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the retraction is the point
&lt;/h2&gt;

&lt;p&gt;The engineering interest here is small: one transient flag. The reason I'm writing it up is that "exact" is a claim you can only make after you've gone looking for the case where it isn't — and the case was a contract executor I hadn't built, running a batch I hadn't imagined, against a README I had already written. If your refund logic has never been called from a contract, it hasn't been tested for the intrinsic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Executors so far are my own two wallets. Nobody else has run an order yet.&lt;/li&gt;
&lt;li&gt;Every bench row sits at Arc's 20 Gwei base fee; the 2× cap is exercised by tests and one capped receipt, not by the bench.&lt;/li&gt;
&lt;li&gt;Explorer source verification wasn't possible (the API is behind a challenge page); the on-chain runtime bytecode is checked byte-for-byte against &lt;code&gt;forge build&lt;/code&gt; instead.&lt;/li&gt;
&lt;li&gt;This is a modifier for one contract's payouts, not an automation network. It doesn't schedule anything for you; it makes it worth someone's while to call you.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Proof
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Contract on Arc mainnet (chain 5042): &lt;code&gt;0x8E2F8AFC29e9dc127103CD6AD5BCfBe661141ccb&lt;/code&gt; · &lt;code&gt;OVERHEAD() = 32503&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The run in the video: &lt;a href="https://explorer.arc.io/tx/0x2b33e38f8404b12be1c07706c196991a05da1824ab08ea1b95f833c6cd6ad255" rel="noopener noreferrer"&gt;https://explorer.arc.io/tx/0x2b33e38f8404b12be1c07706c196991a05da1824ab08ea1b95f833c6cd6ad255&lt;/a&gt; — 58,415 gas × 20 Gwei refunded, drift 0&lt;/li&gt;
&lt;li&gt;42 Foundry tests (34 unit · 2 fuzz × 512 · 6 invariants), 36 vitest incl. 20,000 fast-check property cases, Playwright end-to-end with live mainnet reads&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Live page: &lt;a href="https://legwork.edycu.dev/" rel="noopener noreferrer"&gt;https://legwork.edycu.dev/&lt;/a&gt; · repo: &lt;a href="https://github.com/edycutjong/legwork" rel="noopener noreferrer"&gt;https://github.com/edycutjong/legwork&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you've shipped gas-refund logic on any EVM and metered it differently, I'd like to compare notes.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>solidity</category>
      <category>web3</category>
      <category>ethereum</category>
    </item>
    <item>
      <title>A USDC deposit address with no private key — and the 9,999-block RPC cap that almost froze it</title>
      <dc:creator>Edy Cu</dc:creator>
      <pubDate>Mon, 21 Sep 2026 09:34:41 +0000</pubDate>
      <link>https://dev.to/edycutjong/a-usdc-deposit-address-with-no-private-key-and-the-9999-block-rpc-cap-that-almost-froze-it-el6</link>
      <guid>https://dev.to/edycutjong/a-usdc-deposit-address-with-no-private-key-and-the-9999-block-rpc-cap-that-almost-froze-it-el6</guid>
      <description>&lt;p&gt;Every deposit address an exchange hands out is a private key someone has to generate, store, guard and eventually sign a sweep with. I built a deposit address that has no key at all — and then, the evening before submitting it, found out the live page would have frozen at "UNPAID" a few hours after deploy.&lt;/p&gt;

&lt;p&gt;This is the mechanism, the bug, and what pinned it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Live: &lt;a href="https://pigeonhole.edycu.dev/" rel="noopener noreferrer"&gt;https://pigeonhole.edycu.dev/&lt;/a&gt; (reviewer path: &lt;code&gt;/#/judge&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Repo (MIT): &lt;a href="https://github.com/edycutjong/pigeonhole" rel="noopener noreferrer"&gt;https://github.com/edycutjong/pigeonhole&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;2:30 video of a real cycle on Arc mainnet: &lt;a href="https://youtu.be/BGuzotTXQEA" rel="noopener noreferrer"&gt;https://youtu.be/BGuzotTXQEA&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A deposit address with no private key
&lt;/h2&gt;

&lt;p&gt;Pigeonhole runs on Arc, Circle's L1 where &lt;strong&gt;USDC is the native balance&lt;/strong&gt; — it pays for gas, and a plain &lt;code&gt;value&lt;/code&gt; transfer moves it. That one fact makes an old Ethereum trick work for a stablecoin.&lt;/p&gt;

&lt;p&gt;The factory holds an immutable &lt;code&gt;treasury&lt;/code&gt;. For any invoice, &lt;code&gt;salt = keccak256(invoiceId)&lt;/code&gt;, and the deposit address is the CREATE2 address of a 22-byte throwaway whose entire code is &lt;code&gt;PUSH20 treasury; SELFDESTRUCT&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/// @notice The sweeper init-code, derived from the immutable treasury.
function initCode() public view returns (bytes memory) {
    return abi.encodePacked(hex"73", treasury, hex"ff");
}

/// @notice The deterministic deposit address for `salt` (offline-reproducible).
function predict(bytes32 salt) public view returns (address) {
    return address(
        uint160(uint256(keccak256(abi.encodePacked(hex"ff", address(this), salt, keccak256(initCode())))))
    );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That address is handed out &lt;strong&gt;before anything is deployed&lt;/strong&gt;. It has no code, no nonce and no key. Because USDC is Arc's native balance, sending to it just works — &lt;code&gt;EXTCODESIZE == 0&lt;/code&gt; is fine for a native send, and Arc's system emitter (&lt;code&gt;0xffff…fffE&lt;/code&gt;) logs the movement as a &lt;code&gt;Transfer&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When anyone calls &lt;code&gt;sweep(salt)&lt;/code&gt;, the factory CREATE2-deploys the throwaway. Its constructor is the SELFDESTRUCT, so the whole balance moves to the treasury inside the deploy transaction, and EIP-6780 deletes the account again because it was created in the same transaction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/// @notice Sweep the pigeonhole for `salt` to the treasury. Permissionless; funds can only reach `treasury`.
function sweep(bytes32 salt) public returns (address deployed) {
    address expected = predict(salt);
    uint256 bal = expected.balance;
    bytes memory code = initCode();
    assembly {
        deployed := create2(0, add(code, 0x20), mload(code), salt)
    }
    if (deployed != expected) revert Create2Mismatch(expected, deployed);
    emit Swept(salt, expected, bal);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The address is empty and reusable afterwards. A balance-moving sweep on the production factory is &lt;strong&gt;64,162 gas at p50&lt;/strong&gt; over 25 bench rows, about $0.0013 at Arc's 20 Gwei floor.&lt;/p&gt;

&lt;p&gt;On every other EVM chain, USDC is an ERC-20. &lt;code&gt;SELFDESTRUCT&lt;/code&gt; cannot move an ERC-20 balance, and a native send to a codeless address leaves no log. So the pattern is either impossible or blind everywhere else. On Arc it is both possible and observable.&lt;/p&gt;

&lt;h2&gt;
  
  
  No backend: PAID and SWEPT are one log filter
&lt;/h2&gt;

&lt;p&gt;There is no database. &lt;code&gt;PAID&lt;/code&gt; is "the system emitter has a &lt;code&gt;Transfer&lt;/code&gt; with &lt;code&gt;to == pigeonhole&lt;/code&gt;", &lt;code&gt;SWEPT&lt;/code&gt; is "…and a &lt;code&gt;Transfer&lt;/code&gt; with &lt;code&gt;from == pigeonhole&lt;/code&gt; that took the balance to zero". The invoice &lt;em&gt;is&lt;/em&gt; its URL.&lt;/p&gt;

&lt;p&gt;Which means the entire product stands on &lt;code&gt;eth_getLogs&lt;/code&gt;. That is where it nearly fell over.&lt;/p&gt;

&lt;h2&gt;
  
  
  The wall: 9,999 blocks
&lt;/h2&gt;

&lt;p&gt;Day-2 code walked the full history in one call. It worked in every test and in every manual run, because the factory was hours old.&lt;/p&gt;

&lt;p&gt;The pre-submission audit asked a boring question: what does the public RPC actually allow? The answer, measured on 2026-09-17: a span of &lt;strong&gt;9,999 blocks is fine; 10,000 is rejected&lt;/strong&gt; with &lt;code&gt;-32012 requested range too large&lt;/code&gt;. Arc produces roughly two blocks a second. 9,999 blocks is &lt;strong&gt;about 85 minutes&lt;/strong&gt; of chain.&lt;/p&gt;

&lt;p&gt;So the page would have kept working for the rest of the afternoon, and then, for every invoice older than ~85 minutes, every refresh would have thrown, and the UI would have sat on "UNPAID" forever — for a paid invoice. The demo I had planned to record the next morning would have been the first thing to break.&lt;/p&gt;

&lt;p&gt;The fix is unglamorous: chunk every scan, poll incrementally, and carry the invoice's creation block in its URL (&lt;code&gt;?from=&lt;/code&gt;) so a scan never starts at the factory's genesis when it doesn't have to.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Chunked, incremental eth_getLogs over the EIP-7708 system emitter.&lt;/span&gt;
&lt;span class="c1"&gt;// The public Arc RPC rejects any eth_getLogs span of 10,000+ blocks with -32012 "requested range too large"&lt;/span&gt;
&lt;span class="c1"&gt;// (measured 2026-09-17: 9,999 ok, 10,000 rejected). At ~2 blocks/s that is ~85 minutes of chain, so every&lt;/span&gt;
&lt;span class="c1"&gt;// scan must be chunked and every poll must be incremental — a fresh full-history scan per refresh is not viable.&lt;/span&gt;

&lt;span class="cm"&gt;/** Largest span the RPC accepts, with headroom (9,000 &amp;lt; 10,000). Exported so the test can pin it. */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MAX_LOG_SPAN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="nx"&gt;_000n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="cm"&gt;/** Yields [from, to] inclusive spans, each ≤ MAX_LOG_SPAN blocks wide, covering from..to. */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;spans&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;max&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bigint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;MAX_LOG_SPAN&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Generator&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bigint&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="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;max&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;max&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The walk itself is sequential and paced, because the same RPC also rate-limits bursts, and it reports every completed chunk so the caller can checkpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fetchMovements&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;LogClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pigeonhole&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fromBlock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;toBlock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;onChunk&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;moves&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Movement&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;chunkEnd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Progress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Movement&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="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;all&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nf"&gt;spans&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fromBlock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;toBlock&lt;/span&gt;&lt;span class="p"&gt;)];&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;all&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;all&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;paced&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ins&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;withRetry&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getLogs&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ARC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;systemEmitter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;transferEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pigeonhole&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;fromBlock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;toBlock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;paced&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;outs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;withRetry&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getLogs&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ARC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;systemEmitter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;transferEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pigeonhole&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;fromBlock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;toBlock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
    &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;ins&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;outs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;onChunk&lt;/span&gt;&lt;span class="p"&gt;?.(&lt;/span&gt;&lt;span class="nf"&gt;toMovements&lt;/span&gt;&lt;span class="p"&gt;([...&lt;/span&gt;&lt;span class="nx"&gt;ins&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;outs&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;done&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;all&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;toBlock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;toMovements&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Checkpoints go to &lt;code&gt;localStorage&lt;/code&gt;, so a returning visitor never re-walks what they already read. The two seeded demo invoices ship a committed, receipt-verified checkpoint in the repo so they open in seconds.&lt;/p&gt;

&lt;p&gt;And it is pinned, because "fixed the same evening" is worth nothing without a test that fails if someone bumps the constant:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;no span ever reaches 10,000 blocks, and the spans tile the range exactly&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nf"&gt;spans&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="nx"&gt;_337_182n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="nx"&gt;_337_182n&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;250&lt;/span&gt;&lt;span class="nx"&gt;_000n&lt;/span&gt;&lt;span class="p"&gt;)];&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;every&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;MAX_LOG_SPAN&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="nx"&gt;_000n&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;out&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;0&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="nx"&gt;_337_182n&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="nx"&gt;_337_182n&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;250&lt;/span&gt;&lt;span class="nx"&gt;_000n&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&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="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;-&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;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two more tests cover a 250k-block history fetching without &lt;code&gt;-32012&lt;/code&gt;, and overlapping polls never double-counting a log (keyed by &lt;code&gt;tx:logIndex&lt;/code&gt; — the RPC is load-balanced, and two backends can disagree about the head).&lt;/p&gt;

&lt;h2&gt;
  
  
  What I took from it
&lt;/h2&gt;

&lt;p&gt;The mental model that was wrong: "a view call is free, so ask for everything." Log queries are not views. Every public RPC has a span cap and a rate, they are rarely in the docs, and they only bite once the chain has moved on without you — which is exactly the window between "demo recorded" and "reviewer opens the link".&lt;/p&gt;

&lt;p&gt;The audit question that found it was not clever. It was "what are the limits of the one external thing this depends on, measured, not assumed." I now ask it on day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The page needs an anonymous Arc RPC and reads history at the rate that RPC sustains (measured ≈ 0.5 &lt;code&gt;eth_getLogs&lt;/code&gt;/s). An invoice URL without &lt;code&gt;?from=&lt;/code&gt; scans from the factory's deploy block — about 38 calls per day of chain — so the first read of an old invoice takes minutes. Progress is shown and never repeated, but it is minutes.&lt;/li&gt;
&lt;li&gt;PAID latency is not benchmarked.&lt;/li&gt;
&lt;li&gt;The treasury is immutable: a single point of failure, by design. There is no beneficiary rotation; the remedy is a new factory.&lt;/li&gt;
&lt;li&gt;The factory's source is not verified on the explorer (its API sits behind a challenge page). The on-chain runtime code is byte-identical to &lt;code&gt;forge build&lt;/code&gt; output — keccak &lt;code&gt;0x8806de8d…&lt;/code&gt; — and that check is in the repo.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Proof, if you want to check
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Factory on Arc mainnet (chain 5042): &lt;code&gt;0x942b8c102e73aeea1a652ebC8F2d319fD08D9A40&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The pay in the video: &lt;a href="https://explorer.arc.io/tx/0x25658c68c396ff1c89ea8cb1f0d84e5263ab3694f39e11baf596db203614448a" rel="noopener noreferrer"&gt;https://explorer.arc.io/tx/0x25658c68c396ff1c89ea8cb1f0d84e5263ab3694f39e11baf596db203614448a&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The sweep in the video (64,162 gas): &lt;a href="https://explorer.arc.io/tx/0x423e71bb29e183f4875ad145db121ae1658bcf9205dc6c99548b90233ab941e6" rel="noopener noreferrer"&gt;https://explorer.arc.io/tx/0x423e71bb29e183f4875ad145db121ae1658bcf9205dc6c99548b90233ab941e6&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;12 Foundry tests, 25 vitest incl. 20,000 fast-check property cases, 34 Playwright checks read-only against mainnet; &lt;code&gt;npm run verify&lt;/code&gt; checks the offline CREATE2 arithmetic against the on-chain &lt;code&gt;predict()&lt;/code&gt; for 50 random ids.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Live page: &lt;a href="https://pigeonhole.edycu.dev/" rel="noopener noreferrer"&gt;https://pigeonhole.edycu.dev/&lt;/a&gt; · repo: &lt;a href="https://github.com/edycutjong/pigeonhole" rel="noopener noreferrer"&gt;https://github.com/edycutjong/pigeonhole&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you run deposit infrastructure and this pattern would or wouldn't work for you, I'd genuinely like to hear why.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>solidity</category>
      <category>web3</category>
      <category>ethereum</category>
    </item>
    <item>
      <title>Four numbers, five labels: how far can you read a crypto wallet without a model?</title>
      <dc:creator>Edy Cu</dc:creator>
      <pubDate>Sun, 20 Sep 2026 12:47:16 +0000</pubDate>
      <link>https://dev.to/edycutjong/four-numbers-five-labels-how-far-can-you-read-a-crypto-wallet-without-a-model-4mh5</link>
      <guid>https://dev.to/edycutjong/four-numbers-five-labels-how-far-can-you-read-a-crypto-wallet-without-a-model-4mh5</guid>
      <description>&lt;p&gt;You have read "Smart Money is buying" in forty tweets this month. Could you point at a Smart Money wallet if one were on your screen — no label, just the numbers?&lt;/p&gt;

&lt;p&gt;I couldn't. So I built a card game to find out how much of a wallet's label is actually visible in its numbers, and then I wrote the dumbest possible reader — one page of thresholds, no model, no LLM — to see how far "reading a wallet" goes before it stops being a skill and starts being a lookup.&lt;/p&gt;

&lt;p&gt;Play it: &lt;strong&gt;&lt;a href="https://labelme.edycu.dev" rel="noopener noreferrer"&gt;labelme.edycu.dev&lt;/a&gt;&lt;/strong&gt; · code: &lt;strong&gt;&lt;a href="https://github.com/edycutjong/labelme" rel="noopener noreferrer"&gt;github.com/edycutjong/labelme&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The game, in one card
&lt;/h2&gt;

&lt;p&gt;Ten real ethereum wallets. Each card shows four things Nansen computes for the address over the last 30 days — PnL, top trades, balance profile, counterparty mix — and five chips: &lt;strong&gt;Smart Money · Exchange · Whale · Contract / Pool · Regular&lt;/strong&gt;. You pick one. The reveal is Nansen's own label group for that wallet, plus a one-line &lt;em&gt;tell&lt;/em&gt; written from the card's numbers.&lt;/p&gt;

&lt;p&gt;Here is card 1 of the round I recorded, straight from the CLI (&lt;code&gt;npm run labelme -- play --seed meridian1933 --answers&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Wallet 1 of 10 · ethereum
PnL 30d    realised +$12K · win rate 67% · 472 trades · 6 tokens · top: GIVE 4%, GULD 3%, ETH 3%, WIN -10%, ELMT -13%
Trades     GIVE +$6.6K (113b/44s) · GULD +$5.4K (67b/113s) · ETH +$2 (2b/5s) · WIN −$68 (48b/9s) · ELMT −$87 (22b/48s)
Balance    10 tokens · $14K · top GULD 86% · stables 0%
Counterp.  9 in 30 d · 472 interactions · top outflow 51% · DEX 97% · wealth-tagged 0% · contracts 0% · unlabelled 3%
→ Smart Money
tell: 472 trades in 30 d · win rate 67% · realised $12K across 6 tokens · 97% of flow through DEX pools and routers — a trader Nansen tracks as Smart Money
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important design decision is that &lt;strong&gt;the answer key is not my opinion&lt;/strong&gt;. A card is Smart Money because the address came back from &lt;code&gt;tgm/holders&lt;/code&gt; with &lt;code&gt;label_type: "smart_money"&lt;/code&gt;, or from &lt;code&gt;smart-money/dex-trades&lt;/code&gt;. It is an exchange because it came back from the &lt;code&gt;exchange&lt;/code&gt; filter. It is &lt;em&gt;Regular&lt;/em&gt; because it came back from &lt;code&gt;tgm/who-bought-sold&lt;/code&gt; with all seventeen of Nansen's label groups excluded — an honest negative, "none of the groups", not a positive identification. Every clue on the face is a field from one of four profiler calls. If Nansen is wrong, the card is wrong, and the reveal says exactly which call decided it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The house rule: one page of thresholds
&lt;/h2&gt;

&lt;p&gt;Once the deck existed, the obvious question was: how much of this can a rule read? Not a classifier — a rule a player can read, argue with, and beat. This is the whole thing, from &lt;code&gt;packages/core/src/reader.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;READER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="cm"&gt;/** a pool: traffic without a trader, and few tokens (a two-sided pool holds two) — exchange hot wallets have traffic AND 20–100 tokens */&lt;/span&gt;
  &lt;span class="na"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;minInteractions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;interactionsPerCounterparty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;maxTokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="cm"&gt;/** an exchange: big, many tokens, no DEX trades; either its counterparties are wealth-tagged/exchange wallets or it simply holds 50+ tokens */&lt;/span&gt;
  &lt;span class="na"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;minUsd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="nx"&gt;_000_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;maxTrades&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="na"&gt;minTokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;minWealthMix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;manyTokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="cm"&gt;/** a whale: one position is most of a $1M+ balance and it barely trades */&lt;/span&gt;
  &lt;span class="na"&gt;whale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;minUsd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_000_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;minTopShare&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;maxTrades&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="cm"&gt;/** Smart Money (active): many trades across many tokens in 30 days — win rate and sign of PnL do NOT separate it from a regular buyer on this deck */&lt;/span&gt;
  &lt;span class="na"&gt;smartMoney&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;minTrades&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;minTokensTraded&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reader runs those in order — contract, exchange, whale, Smart Money, else Regular — and returns a guess with a sentence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;traffic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;interactions&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;R&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;minInteractions&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;countCapped&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;perCp&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;R&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;interactionsPerCounterparty&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;traffic&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;R&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxTokens&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;guess&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;contract&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;because&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;interactions&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; interactions from &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;countCapped&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;+&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; counterparties and only &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; tokens — traffic, not trading`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the 62-card deck it reads &lt;strong&gt;50/62 — 81%&lt;/strong&gt;. On ten fresh draws it had never seen (the benchmark, &lt;code&gt;docs/BENCH.md&lt;/code&gt;), it read &lt;strong&gt;7/10&lt;/strong&gt;. After every round the score screen shows the house rule's score next to yours. That's the bar.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the thresholds taught me
&lt;/h2&gt;

&lt;p&gt;Two things I believed going in were wrong, and the deck said so.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Win rate does not identify Smart Money.&lt;/strong&gt; I assumed the tell would be "wins more". It isn't. On this deck, a Smart Money wallet's win rate and the sign of its PnL do not separate it from a regular buyer who got lucky on one token. What separates it is &lt;em&gt;breadth and tempo&lt;/em&gt;: 25+ trades across 6+ tokens in 30 days. Smart Money, as Nansen labels it, looks like a job, not a streak.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exchanges are not "big wallets".&lt;/strong&gt; Whales are big wallets. An exchange hot wallet is big &lt;em&gt;and&lt;/em&gt; holds 15–100 tokens &lt;em&gt;and&lt;/em&gt; barely trades on DEXes &lt;em&gt;and&lt;/em&gt; — the clue that surprised me — its counterparties are themselves wealth-tagged or exchange-labelled wallets. Money moves in and out; nobody is trading. Card 2 of the recorded round is a $9.4B Binance wallet whose 30-day PnL is exactly $0.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the rule fails — and why that's the game
&lt;/h2&gt;

&lt;p&gt;The confusion matrix on the deck, rows are truth:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;truth \ guess&lt;/th&gt;
&lt;th&gt;smart-money&lt;/th&gt;
&lt;th&gt;exchange&lt;/th&gt;
&lt;th&gt;whale&lt;/th&gt;
&lt;th&gt;contract&lt;/th&gt;
&lt;th&gt;regular&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;smart-money&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;exchange&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;14&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;whale&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;11&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;contract&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;regular&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;12&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The rule is nearly perfect on Regular and good on Smart Money, exchanges and whales. It is bad at &lt;strong&gt;contracts&lt;/strong&gt;: 6 of 10 read as Regular. A dormant proxy or multisig — zero counterparties in 30 days, a handful of tokens, no trades — has &lt;em&gt;no&lt;/em&gt; numbers, and "no numbers" is what Regular looks like too. A Bybit wallet holding eight tokens reads as a whale. A "Token Millionaire" with 5,019 trades reads as a contract, because that much traffic from few counterparties is what a pool looks like.&lt;/p&gt;

&lt;p&gt;Those twelve cards are the hard ones, and a human can beat the rule on them — that's what makes it a game rather than a demo. The rule doesn't know that a $165K balance in one MOG position with zero activity is more likely a proxy than a person, because the rule has no priors. You do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building it so it can't break on camera
&lt;/h2&gt;

&lt;p&gt;The deck was recorded once, live, by &lt;code&gt;scripts/seed.ts&lt;/code&gt;: 163 candidate addresses from the sourcing calls, 101 dropped with a reason (100 of them for being in Nansen's Public Figure group — a person label is not a wallet behaviour, and the class turned out unreadable from clues), 62 dealt. Every raw response is committed. Each card carries a &lt;code&gt;sha256&lt;/code&gt; of &lt;code&gt;{address, chain, class, nansenLabel, entity, clues, tell}&lt;/code&gt;, so &lt;code&gt;npm run verify&lt;/code&gt; replays all 62 offline and fails if a clue, a tell or an answer drifts. The default round makes zero network calls and costs zero credits. Only "Draw fresh" is live — one sourcing page and four clue calls, about 12 credits, cold p50 2.1 s, streamed to the page as each call lands.&lt;/p&gt;

&lt;p&gt;134 tests, 100% statement/branch/function/line coverage on the engine, 12,000 property-based cases on the reader (it must be total over any clues, the tell must be one line, the hash must ignore time).&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Free-tier labels are wealth and structural tags (&lt;code&gt;Token Millionaire&lt;/code&gt;, &lt;code&gt;MultiSig&lt;/code&gt;), not entity names. An entity name (🏦 Binance) appears only where a 1-credit transfer lookup found one — 23 of 62 cards.&lt;/li&gt;
&lt;li&gt;"Regular" is a negative. It means Nansen filed the wallet in none of its groups, not that it's a retail trader.&lt;/li&gt;
&lt;li&gt;A dormant Smart Money wallet is unreadable from cheap clues; the deck keeps only active ones. A live draw can still deal one, and says so.&lt;/li&gt;
&lt;li&gt;Ethereum only. The 30-day window is a snapshot from 2026-09-18.&lt;/li&gt;
&lt;li&gt;The house rule's thresholds were set by looking at this deck. The 7/10 out-of-sample number is the honest one; the 81% is not.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The round in this post is shareable: &lt;a href="https://labelme.edycu.dev/r/meridian1933" rel="noopener noreferrer"&gt;labelme.edycu.dev/r/meridian1933&lt;/a&gt; — same ten cards for everyone. I got 7/10, which is exactly what the rule got. If you beat it, I'd genuinely like to know which of the hard cards you read right.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>typescript</category>
      <category>crypto</category>
      <category>web3</category>
    </item>
  </channel>
</rss>
