<?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: Bryan MARTIN</title>
    <description>The latest articles on DEV Community by Bryan MARTIN (@mik3fly__).</description>
    <link>https://dev.to/mik3fly__</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3974308%2Fe5789af9-845a-421d-b21e-cfe936af473f.jpg</url>
      <title>DEV Community: Bryan MARTIN</title>
      <link>https://dev.to/mik3fly__</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mik3fly__"/>
    <language>en</language>
    <item>
      <title>We thought we caught every scam token. A dev.to post showed us a blind spot.</title>
      <dc:creator>Bryan MARTIN</dc:creator>
      <pubDate>Tue, 09 Jun 2026 16:30:53 +0000</pubDate>
      <link>https://dev.to/mik3fly__/we-thought-we-caught-every-scam-token-a-devto-post-showed-us-a-blind-spot-2c9g</link>
      <guid>https://dev.to/mik3fly__/we-thought-we-caught-every-scam-token-a-devto-post-showed-us-a-blind-spot-2c9g</guid>
      <description>&lt;p&gt;We run a real-time scam-token detector for Ethereum. It analyzes new ERC-20s, simulates buys and sells to catch honeypots, clusters deployers and funders, and scores everything live. After four months and ~93,000 analyzed contracts, we were fairly sure our funnel saw everything that mattered.&lt;/p&gt;

&lt;p&gt;Then &lt;a href="https://dev.to/"&gt;@sanjeevkkansal&lt;/a&gt; published &lt;a href="https://github.com/sanjeevkkansal/evm-deploy-watch" rel="noopener noreferrer"&gt;evm-deploy-watch&lt;/a&gt;, an MIT-licensed week-long study of every new contract on Ethereum. It is a genuinely good piece of work, and it did two things for us. First, it independently validated our core thesis. Second, it pointed a flashlight straight at a blind spot we did not know we had.&lt;/p&gt;

&lt;p&gt;This is the gap, measured, and how we closed it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thesis we already shared
&lt;/h2&gt;

&lt;p&gt;The deploy-watch author's central finding is that &lt;strong&gt;deploy-time heuristics beat lagging public blacklists&lt;/strong&gt;. Reading source code, deployer history, and bytecode the moment a contract is created catches scams days before they show up on a blocklist. The study reported essentially &lt;strong&gt;zero overlap&lt;/strong&gt; with ScamSniffer's feed - the contracts it flagged were not (yet) on anyone's list.&lt;/p&gt;

&lt;p&gt;That is exactly the bet our pipeline is built on, so it was great to see it confirmed by someone with a completely independent dataset and methodology. The article's "future next steps" section reads almost like our changelog: buy/sell simulation, CREATE2 / factory clustering, deployer-funder graphs, multi-signal scoring. We already run those in production. More on that at the end, because the honest part of this post is the thing we were &lt;em&gt;not&lt;/em&gt; doing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The blind spot: our funnel is DEX-triggered
&lt;/h2&gt;

&lt;p&gt;Here is the architectural assumption that bit us.&lt;/p&gt;

&lt;p&gt;Our ingestion is driven by &lt;strong&gt;liquidity&lt;/strong&gt;. A &lt;code&gt;factory-watcher&lt;/code&gt; service reacts to Uniswap V2/V3/V4 pool creation events. When a token opens a pool, it enters the funnel and gets the full analysis: source regex, bytecode clustering, a real buy-then-sell simulation, deployer graph, scoring. This is the right trigger for the scams we care most about, because a rug or a honeypot &lt;strong&gt;needs&lt;/strong&gt; a pool. You cannot trap a buyer who cannot buy.&lt;/p&gt;

&lt;p&gt;But there is an entire family of fraud that &lt;strong&gt;never creates a pool&lt;/strong&gt;, because it never intends for anyone to trade it.&lt;/p&gt;

&lt;h2&gt;
  
  
  FlashUSDT / fake-Tether: a scam with no on-chain victim
&lt;/h2&gt;

&lt;p&gt;The "FlashUSDT" or "proof-of-funds" family works entirely off-chain. The mechanics:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;An operator deploys an ERC-20 whose &lt;code&gt;name()&lt;/code&gt; returns &lt;code&gt;Tether USD&lt;/code&gt;, &lt;code&gt;symbol()&lt;/code&gt; returns &lt;code&gt;USDT&lt;/code&gt;, and &lt;code&gt;decimals()&lt;/code&gt; returns &lt;code&gt;6&lt;/code&gt;. Anyone can do this. Nothing on-chain stops you from naming your token whatever you want.&lt;/li&gt;
&lt;li&gt;They send a large balance of this token to a wallet, or show you a wallet that holds it. Your wallet UI and most explorers read the metadata and display it as &lt;strong&gt;250,000 USDT&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;They give you a reason the "funds" need to move: an OTC deal, an escrow proof, a job signing bonus, a "liquidity bot" demo.&lt;/li&gt;
&lt;li&gt;You send something genuinely valuable in return - real USDT, ETH, goods, a token approval.&lt;/li&gt;
&lt;li&gt;The fake balance was always worth zero. There is no pool, no price, no trade. The whole con is the spoofed metadata plus social engineering.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Because these tokens never touch a DEX, &lt;strong&gt;simulation-based detection never sees them&lt;/strong&gt;. There is nothing to simulate. They are invisible to anything that waits for a Uniswap pair - including, it turned out, us.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap, quantified
&lt;/h2&gt;

&lt;p&gt;Numbers, because "we have a blind spot" is a feeling and "we capture 0.5% of this family" is a bug report.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Our database held &lt;strong&gt;15&lt;/strong&gt; FlashUSDT-named tokens across &lt;strong&gt;four months&lt;/strong&gt; (2026-02-12 to 06-08), out of 93,000 analyzed.&lt;/li&gt;
&lt;li&gt;The deploy-watch study observed roughly &lt;strong&gt;~192 per week&lt;/strong&gt; of this family in its window (134 &lt;code&gt;FlashUSDT&lt;/code&gt; + 58 &lt;code&gt;FlashUSDTLiquidityBot&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;So we were capturing on the order of &lt;strong&gt;~0.5%&lt;/strong&gt; of it.&lt;/li&gt;
&lt;li&gt;The two contracts the article dissects in detail were simply &lt;strong&gt;not in our database at all&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is not a scoring miss. It is a coverage miss: the contracts never entered the funnel, so there was nothing to score. You cannot rank what you never ingest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing it: ingest every deploy, not every pool
&lt;/h2&gt;

&lt;p&gt;The fix is conceptually simple - watch contract &lt;strong&gt;creations&lt;/strong&gt;, not pool creations - and the engineering is mostly about keeping the volume bounded.&lt;/p&gt;

&lt;p&gt;We already run a &lt;code&gt;mempool-watcher&lt;/code&gt; that streams pending transactions. A contract creation is just a transaction with &lt;code&gt;to === null&lt;/code&gt;. So the cheapest place to hook this in is right there in the mempool loop:&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;// main-loop.ts - a contract creation is a tx with no recipient&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;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;to&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;adapters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;deployWatcher&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;adapters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;deployWatcher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onDeploy&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;tx&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;nonce&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nonce&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;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hash&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 deploy watcher predicts the contract address from the sender and nonce, waits for the deploy to mine, reads two metadata fields, and only then decides whether the contract is worth a full analysis:&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;import&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;JsonRpcProvider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;getCreateAddress&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ethers&lt;/span&gt;&lt;span class="dl"&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;ERC20_MIN_ABI&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;function name() view returns (string)&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;function symbol() view returns (string)&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="nf"&gt;onDeploy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DeployTx&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="na"&gt;address&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="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;address&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getCreateAddress&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;tx&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;nonce&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nonce&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&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="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// CREATE2 / malformed - out of scope for v1&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;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&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="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// dedupe; bounded Set, cleared at 50k&lt;/span&gt;
  &lt;span class="nx"&gt;seen&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="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &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;code&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;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCode&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="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;code&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0x&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="c1"&gt;// reverted or not mined yet&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;c&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;Contract&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;ERC20_MIN_ABI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;provider&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;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
      &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;symbol&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="o"&gt;=&amp;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;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isImpersonation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;symbol&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="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enqueue&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="c1"&gt;// -&amp;gt; analysis_queue -&amp;gt; contract-analyzer&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;delayMs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// ~45s: deploys are seen pending, read metadata after they mine&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few deliberate choices in there:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Metadata-only triage.&lt;/strong&gt; At this stage we make exactly two &lt;code&gt;eth_call&lt;/code&gt;s: &lt;code&gt;name()&lt;/code&gt; and &lt;code&gt;symbol()&lt;/code&gt;. No simulation, no source fetch, no graph. The study counted ~1,200 top-level deploys per day. Two reads each, against our own Nethermind node, is nothing. The expensive analysis only runs for the handful that actually spoof a major token.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Predict-then-confirm.&lt;/strong&gt; &lt;code&gt;getCreateAddress(from, nonce)&lt;/code&gt; gives us the deterministic address for a plain &lt;code&gt;CREATE&lt;/code&gt; deploy, so we can start watching it the moment the tx is pending and read its code once it mines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A &lt;code&gt;seen&lt;/code&gt; Set, bounded.&lt;/strong&gt; Deploys can appear multiple times across mempool snapshots; the set dedupes and self-clears at 50k entries so it never leaks memory. Re-checking a stale address is harmless.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CREATE2 is out of scope for v1.&lt;/strong&gt; Factory-deployed contracts whose address depends on a salt are not covered by &lt;code&gt;getCreateAddress&lt;/code&gt;. We flagged this as a known limitation rather than pretending otherwise - it is a real future item.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The check that defeats the whole family
&lt;/h2&gt;

&lt;p&gt;The actual detection is almost anticlimactic, and that is the point. A token is its &lt;strong&gt;address&lt;/strong&gt;, not its name. The real USDT lives at exactly one contract. Anything claiming to be USDT at any other address is, by definition, not USDT.&lt;/p&gt;

&lt;p&gt;So the signal is: name or symbol matches a major token, &lt;strong&gt;and&lt;/strong&gt; the address is not the canonical one.&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;CANONICAL_TOKENS&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="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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;name&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="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="na"&gt;USDT&lt;/span&gt;&lt;span class="p"&gt;:&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0xdac17f958d2ee523a2206206994597c13d831ec7&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tether 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;USDC&lt;/span&gt;&lt;span class="p"&gt;:&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;usd coin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;WETH&lt;/span&gt;&lt;span class="p"&gt;:&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wrapped ether&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;DAI&lt;/span&gt;&lt;span class="p"&gt;:&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0x6b175474e89094c44da98b954eedeac495271d0f&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dai stablecoin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;WBTC&lt;/span&gt;&lt;span class="p"&gt;:&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0x2260fac5e5542a773aa44fbcfedf7c193bc2c599&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wrapped btc&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// ... FRAX, LUSD, BUSD, stETH, wstETH&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;isImpersonation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&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="nx"&gt;symbol&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="nx"&gt;address&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="nx"&gt;boolean&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;addr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;address&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="nf"&gt;toLowerCase&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;sym&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;symbol&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="nf"&gt;toUpperCase&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&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="nf"&gt;toLowerCase&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;for &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;canonSym&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;info&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CANONICAL_TOKENS&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;matches&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sym&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;canonSym&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nm&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="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;nm&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&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;matches&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;addr&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;info&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="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&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="kc"&gt;false&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 pitfalls hide in that tiny function:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The allowlist is the safety rail, and it has to be exact.&lt;/strong&gt; If you get one canonical address wrong, you either miss a whole family or, worse, you flag the &lt;strong&gt;real&lt;/strong&gt; USDT as a scam. Every address is stored lowercased and compared lowercased so EIP-55 checksums never cause a false negative.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exact match, not fuzzy.&lt;/strong&gt; We compare &lt;code&gt;symbol === "USDT"&lt;/code&gt;, not &lt;code&gt;symbol.includes("USDT")&lt;/code&gt;. Fuzzy matching would light up every legitimate bridged or wrapped variant (&lt;code&gt;USDT.e&lt;/code&gt;, &lt;code&gt;axlUSDC&lt;/code&gt;, and friends) and bury the real signal in false positives. The trade-off is that a typo-squat like &lt;code&gt;USDTT&lt;/code&gt; slips this specific check - but those usually do open a pool and get caught by the rest of the pipeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duplicated, on purpose.&lt;/strong&gt; The same map and check live in both &lt;code&gt;mempool-watcher&lt;/code&gt; (the cheap pre-filter) and &lt;code&gt;contract-analyzer&lt;/code&gt; (the authoritative signal). The list is ten lines and changes maybe twice a year; a shared package would be more ceremony than it is worth.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When the pre-filter enqueues an address, &lt;code&gt;contract-analyzer&lt;/code&gt; runs the same check as a first-class signal and the scorer applies a hard floor:&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;// risk-assessment.ts - impersonation is high-severity regardless of other signals&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;allFlags&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;impersonates_major_token&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;score&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="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;70&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;A fake USDT with no pool and no trading history would otherwise score near zero on a pipeline built around liquidity and honeypot mechanics. The floor says: a contract pretending to be a major asset at a fake address is dangerous on its own, full stop.&lt;/p&gt;

&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;

&lt;p&gt;The deploy-watch path and the impersonation signal went fully live on 2026-06-08. The first two days:&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;Coverage of the impersonation family&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Before&lt;/strong&gt; (DEX-triggered)&lt;/td&gt;
&lt;td&gt;15 in 4 months - about 0.5% of the wild rate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;After&lt;/strong&gt; (deploy-time)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;77 in 2 days&lt;/strong&gt;: 30 fake USDT, 26 fake USDC, 21 fake DAI&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;77 in two days is roughly &lt;strong&gt;270 per week&lt;/strong&gt;, which puts us &lt;strong&gt;above&lt;/strong&gt; the ~192/week the study observed - and every one of them was flagged &lt;strong&gt;at deploy time, with no pool&lt;/strong&gt;, off two &lt;code&gt;eth_call&lt;/code&gt;s. The blind spot is now one of our better-covered families.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we were already doing on top
&lt;/h2&gt;

&lt;p&gt;Credit where it is due: the deploy-watch study is the reason we found this, and its author is clearly building toward the same place we are. The difference is mostly that we have had a head start on the items he lists as "next steps", so if you are building something similar, here is what is worth adding after deploy-time metadata:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Real buy/sell simulation.&lt;/strong&gt; We execute a buy then a sell against the live pair across multiple amounts. A contract that accepts buys and silently reverts sells is a honeypot, no matter how clean its source reads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bytecode clustering.&lt;/strong&gt; We hash deployed runtime bytecode and group identical contracts. One scam template gets reused thousands of times; clustering turns "a new scam" into "the 4,050th token off a known factory".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployer and funder graph.&lt;/strong&gt; We trace who deployed a contract and who funded that wallet, through mixers where possible. A throwaway EOA funded one hop from a known scam operator is a strong prior before you read a single opcode.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-signal weighted scoring with floors.&lt;/strong&gt; No single heuristic decides. Signals combine into a score, and high-severity ones (like impersonation) set floors so a dangerous contract cannot be averaged down to "safe".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live, not batch.&lt;/strong&gt; All of this runs as contracts appear, not in a nightly sweep.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that helped against FlashUSDT, because FlashUSDT never enters the part of the pipeline where any of it runs. That is the lesson worth keeping: a detector is only as good as its &lt;strong&gt;ingestion&lt;/strong&gt;. The cleverest scoring in the world is useless on a contract you never ingested. We had spent months tuning the scoring and almost none questioning the trigger.&lt;/p&gt;

&lt;p&gt;It took an outside writeup, with a different dataset and a different goal, to make the assumption visible. Thanks for that.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;You can paste any token address into the free detector at &lt;a href="https://rektradar.io" rel="noopener noreferrer"&gt;rektradar.io&lt;/a&gt; to check whether it is the real asset or an impersonator. If you want the user-facing version of how this scam plays out, we wrote that up &lt;a href="https://rektradar.io/blog/posts/fake-usdt-proof-of-funds-scam/" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ethereum</category>
      <category>security</category>
      <category>web3</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
