<?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: Swapcore.Exchange</title>
    <description>The latest articles on DEV Community by Swapcore.Exchange (@swapcoreexchange).</description>
    <link>https://dev.to/swapcoreexchange</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%2F4118127%2Fbcdc6133-cb6c-4908-8a3e-3cbdbee4f891.jpg</url>
      <title>DEV Community: Swapcore.Exchange</title>
      <link>https://dev.to/swapcoreexchange</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/swapcoreexchange"/>
    <language>en</language>
    <item>
      <title>Checking whether a 2010 bitcoin output has moved, in about 30 lines of JavaScript</title>
      <dc:creator>Swapcore.Exchange</dc:creator>
      <pubDate>Sat, 14 Nov 2026 01:28:31 +0000</pubDate>
      <link>https://dev.to/swapcoreexchange/checking-whether-a-2010-bitcoin-output-has-moved-in-about-30-lines-of-javascript-3ope</link>
      <guid>https://dev.to/swapcoreexchange/checking-whether-a-2010-bitcoin-output-has-moved-in-about-30-lines-of-javascript-3ope</guid>
      <description>&lt;h2&gt;
  
  
  17.8 years
&lt;/h2&gt;

&lt;p&gt;That's how long the 50 BTC reward from Bitcoin's block 1 has been sitting unspent, according to the script below.&lt;/p&gt;

&lt;p&gt;It's a timely question. On November 13, 2026, two mining rewards from September 2010, 50 BTC each, moved for the first time in more than 16 years (Decrypt and The Block covered it; Whale Alert flagged the first move at 04:08 UTC). Each spend paid 1,110 satoshis in fees, and a later transaction combined coins from both wallets.&lt;/p&gt;

&lt;p&gt;If you try to check a wallet like that with an address search, you can get a confusing answer. Many 2009 and 2010 coinbase outputs paid a raw public key (P2PK), not an address. Explorers can derive an address from the key, but the cleanest question to ask is about the output itself: was &lt;code&gt;txid:vout&lt;/code&gt; spent, and when?&lt;/p&gt;

&lt;h2&gt;
  
  
  The script
&lt;/h2&gt;

&lt;p&gt;It uses the public Esplora API (Blockstream's by default, mempool.space works too) and Node 18+'s built-in &lt;code&gt;fetch&lt;/code&gt;. No key, no dependencies.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// outspend.mjs: was one output spent, and how long did it sit first?&lt;/span&gt;
&lt;span class="c1"&gt;// usage: node outspend.mjs &amp;lt;txid&amp;gt; &amp;lt;vout&amp;gt;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="no"&gt;API&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;ESPLORA&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;'https://blockstream.info/api'&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="n"&gt;txid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'0'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&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;2&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="n"&gt;get&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="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="n"&gt;r&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;API&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="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="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`${p}: HTTP ${r.status}`&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="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&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;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;day&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toISOString&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="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'T'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="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;16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s1"&gt;' UTC'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;tx&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;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`/tx/${txid}`&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="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vout&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vout&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="nb"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`${out.value / 1e8} BTC · ${out.scriptpubkey_type} · created ${day(tx.status.block_time)} (block ${tx.status.block_height})`&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="n"&gt;sp&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;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`/tx/${txid}/outspend/${vout}`&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="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;sp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;spent&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="n"&gt;yrs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&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="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;block_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;365.25&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;86400&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nb"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`unspent, still there after ${yrs.toFixed(1)} years`&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="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;s&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;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`/tx/${sp.txid}`&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="n"&gt;yrs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;block_time&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;block_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;365.25&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;86400&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nb"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`spent ${day(s.status.block_time)} after ${yrs.toFixed(1)} years, fee ${s.fee} sat`&lt;/span&gt;&lt;span class="p"&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;const&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`  -&amp;gt; ${(o.value / 1e8).toFixed(8)} BTC ${o.scriptpubkey_type}`&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;
  
  
  Two runs
&lt;/h2&gt;

&lt;p&gt;Block 1's coinbase:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ node outspend.mjs 0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098 0
50 BTC · p2pk · created 2009-01-09 02:54 UTC (block 1)
unspent, still there after 17.8 years
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And output 0 of the transaction in block 170, the 10 BTC Satoshi Nakamoto sent Hal Finney in the first person-to-person bitcoin transaction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ node outspend.mjs f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16 0
10 BTC · p2pk · created 2009-01-12 03:30 UTC (block 170)
spent 2010-11-16 20:39 UTC after 1.8 years, fee 0 sat
  -&amp;gt; 10.00000000 BTC p2pkh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the output types. Both started as &lt;code&gt;p2pk&lt;/code&gt;: a payment to a public key, with no address in the script. When the block 170 coins moved, they went to a &lt;code&gt;p2pkh&lt;/code&gt; output, the address format most wallets show.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do with it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Watch a list of old outputs: loop over &lt;code&gt;txid:vout&lt;/code&gt; pairs and alert when &lt;code&gt;spent&lt;/code&gt; flips to &lt;code&gt;true&lt;/code&gt;. That is roughly what whale-tracking bots do.&lt;/li&gt;
&lt;li&gt;Read the spend, not just the flag. Friday's two spends split 50 BTC into about 49.99 + 0.01 and 49 + 1. The split, the fee and any later consolidation are the only facts the chain gives you. Ownership and intent are not on it.&lt;/li&gt;
&lt;li&gt;Don't hammer a public API. Add a delay between requests, or run your own Esplora or Electrum server.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Decrypt, "Dormant Bitcoin Wallets Wake Up After 16 Years, Moving $6.9 Million" (Nov 13, 2026)&lt;/li&gt;
&lt;li&gt;The Block, "From $6 to $6.9 million: Two dormant Bitcoin addresses wake up to move 100 BTC after more than 16 years" (Nov 13, 2026)&lt;/li&gt;
&lt;li&gt;Esplora HTTP API docs: &lt;a href="https://github.com/Blockstream/esplora/blob/master/API.md" rel="noopener noreferrer"&gt;https://github.com/Blockstream/esplora/blob/master/API.md&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>bitcoin</category>
      <category>blockchain</category>
      <category>javascript</category>
      <category>node</category>
    </item>
    <item>
      <title>worse_query and wallet apps on iOS: what the sandbox escape reads, and what to check in your own app</title>
      <dc:creator>Swapcore.Exchange</dc:creator>
      <pubDate>Wed, 11 Nov 2026 01:12:15 +0000</pubDate>
      <link>https://dev.to/swapcoreexchange/worsequery-and-wallet-apps-on-ios-what-the-sandbox-escape-reads-and-what-to-check-in-your-own-app-49gp</link>
      <guid>https://dev.to/swapcoreexchange/worsequery-and-wallet-apps-on-ios-what-the-sandbox-escape-reads-and-what-to-check-in-your-own-app-49gp</guid>
      <description>&lt;h2&gt;
  
  
  The report
&lt;/h2&gt;

&lt;p&gt;On November 10, 2026, Binance posted a warning about worse_query, an iOS flaw that lets a malicious app read local files belonging to other apps. Binance lists iOS 26.7.1, 27.0.1 and 27.2 as affected and calls it "an iOS-level issue, not unique to Binance."&lt;/p&gt;

&lt;p&gt;The technique is public. A researcher who goes by forcequitOS published a proof of concept on GitHub; Decrypt reports the repository describes it as a sandbox escape for iOS 26.0 through 26.7.2 and iOS 27.2, and as the successor to an earlier exploit, bad_query. BleepingComputer covers a tool built on it, WQ Files, that can browse, export, edit and delete other apps' data without a jailbreak. The attacker's app has to be installed on the device, so sideloaded apps are the realistic delivery route.&lt;/p&gt;

&lt;p&gt;Apple shipped iOS 27.2.1 and 26.7.3 on November 9 with nine security fixes, including one for an App Intents issue that could expose sensitive user data. Apple's notes don't name worse_query, and none of the coverage confirms the two are the same bug. No crypto theft has been confirmed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the coverage says is exposed
&lt;/h2&gt;

&lt;p&gt;The reports describe reading files in other apps' storage. None of them says anything about Keychain items, so I'm not going to claim the Keychain is safe or unsafe here. What is clear is that anything your app writes to a file is in scope.&lt;/p&gt;

&lt;h2&gt;
  
  
  A checklist for your own app
&lt;/h2&gt;

&lt;p&gt;If your app holds a mnemonic, a private key, an API secret or a long-lived session token:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Search the container for secrets on disk. Look at &lt;code&gt;Documents/&lt;/code&gt;, &lt;code&gt;Library/Application Support/&lt;/code&gt;, &lt;code&gt;Library/Caches/&lt;/code&gt;, &lt;code&gt;tmp/&lt;/code&gt;, any App Group container, &lt;code&gt;UserDefaults&lt;/code&gt; plists, Core Data or SQLite stores, and crash or debug logs. A seed phrase in a JSON backup file or a logged request body counts.&lt;/li&gt;
&lt;li&gt;Keep secrets in the Keychain with the narrowest accessibility class the feature allows, and avoid classes that sync or survive a device migration unless you need that.&lt;/li&gt;
&lt;li&gt;If you must keep something in a file, encrypt it with a key that lives in the Keychain (or the Secure Enclave), so a copied file is useless on its own.&lt;/li&gt;
&lt;li&gt;Treat App Group containers as shared space. Anything you put there for an extension or widget should be data you'd accept being read.&lt;/li&gt;
&lt;li&gt;Make server sessions revocable, and give users a "sign out everywhere" action. That is the recovery step Binance tells people to take.&lt;/li&gt;
&lt;li&gt;For wallets: make "create a new wallet and move funds" a short, well-tested path. If a user's seed may have leaked, a new seed and an on-chain transfer is the only real fix, and that transfer is irreversible, so show the destination address and network clearly before signing.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Telling your users
&lt;/h2&gt;

&lt;p&gt;Binance's user advice is a good template: update iOS and all apps, delete apps from untrusted sources (sideloaded or manually signed), and if compromise is suspected, move funds to a new wallet created on a clean device and reset all sessions.&lt;/p&gt;

&lt;p&gt;Sources: Binance on X, BleepingComputer and Decrypt (November 10, 2026); 24/7 Wall St. (November 10, 2026).&lt;/p&gt;

</description>
      <category>ios</category>
      <category>security</category>
      <category>mobile</category>
      <category>web3</category>
    </item>
    <item>
      <title>Flagging "pool hops" like the ones in the Binance-hack laundering, in 60 lines of JavaScript</title>
      <dc:creator>Swapcore.Exchange</dc:creator>
      <pubDate>Tue, 10 Nov 2026 01:38:14 +0000</pubDate>
      <link>https://dev.to/swapcoreexchange/flagging-pool-hops-like-the-ones-in-the-binance-hack-laundering-in-60-lines-of-javascript-145p</link>
      <guid>https://dev.to/swapcoreexchange/flagging-pool-hops-like-the-ones-in-the-binance-hack-laundering-in-60-lines-of-javascript-145p</guid>
      <description>&lt;h2&gt;
  
  
  The pattern
&lt;/h2&gt;

&lt;p&gt;On November 9, Elliptic described how the launderers behind the Binance hack moved stolen NEAR through home-made meme coins. Between November 5 and 7 they created at least six tokens on NEAR, each costing about 25 NEAR to make, and paired each with NEAR in its own liquidity pool. One set of wallets bought the token with stolen NEAR. Another set sold it back to the pool. About 117,000 NEAR (roughly $430,000) went through before the tokens were abandoned; none has traded since November 8.&lt;/p&gt;

&lt;p&gt;No wallet pays another wallet directly, so a naive "who sent money to whom" graph has a gap where the pool is. The trail is still all on-chain. You just have to look at the trades instead of the transfers.&lt;/p&gt;

&lt;p&gt;From Elliptic's description, a pool hop has five signs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The token is brand new when the swaps start.&lt;/li&gt;
&lt;li&gt;Almost nobody else trades it.&lt;/li&gt;
&lt;li&gt;The wallets that buy are never the wallets that sell.&lt;/li&gt;
&lt;li&gt;What comes out is close to what went in.&lt;/li&gt;
&lt;li&gt;The buyers were funded from a wallet you already flag.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of these alone means much. New tokens with few traders appear by the thousand every day. All five together is a different story.&lt;/p&gt;

&lt;h2&gt;
  
  
  The script
&lt;/h2&gt;

&lt;p&gt;It takes a JSON file of swaps and token creation times, groups swaps by token and scores each one from 0 to 5.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// pool-hop.mjs&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;readFileSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s1"&gt;'node:fs'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&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="o"&gt;??&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="no"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'./sample-swaps.json'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;import&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;url&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="n"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;swaps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flagged&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'utf8'&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="kd"&gt;const&lt;/span&gt; &lt;span class="no"&gt;HOURS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;3600e3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="no"&gt;MAX_TOKEN_AGE_H&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="no"&gt;MAX_TRADERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="no"&gt;MATCH_TOLERANCE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;byToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&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="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="n"&gt;s&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="n"&gt;swaps&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="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;byToken&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="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;byToken&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="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;token&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;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;byToken&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;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&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="n"&gt;s&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="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;report&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="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="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="n"&gt;byToken&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="n"&gt;meta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="n"&gt;token&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="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;first&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;min&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;span class="n"&gt;list&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="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ts&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;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;ageH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first&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;parse&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;createdAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="no"&gt;HOURS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;buyers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;list&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="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;side&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;'buy'&lt;/span&gt;&lt;span class="p"&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="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wallet&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="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;sellers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;list&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="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;side&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;'sell'&lt;/span&gt;&lt;span class="p"&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="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wallet&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="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;traders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Set&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;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buyers&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;span class="n"&gt;sellers&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="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;overlap&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="p"&gt;.&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buyers&lt;/span&gt;&lt;span class="p"&gt;]&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="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;sellers&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="n"&gt;w&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="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;inAmt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;list&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="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;side&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;'buy'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;(&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;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&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="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;outAmt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;list&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="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;side&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;'sell'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;(&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;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&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="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;taintedBuyers&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="p"&gt;.&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buyers&lt;/span&gt;&lt;span class="p"&gt;]&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="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wallet&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;flagged&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="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;funder&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;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;reasons&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="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="n"&gt;ageH&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="no"&gt;MAX_TOKEN_AGE_H&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&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="sb"&gt;`token ${ageH.toFixed(1)} h old at first swap`&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="n"&gt;traders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="no"&gt;MAX_TRADERS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&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="sb"&gt;`${traders.size} wallets ever traded it`&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="n"&gt;overlap&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="n"&gt;buyers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;sellers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&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="s1"&gt;'buyers and sellers never overlap'&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="n"&gt;inAmt&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&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="n"&gt;outAmt&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;inAmt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;inAmt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="no"&gt;MATCH_TOLERANCE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&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="sb"&gt;`out ${outAmt} vs in ${inAmt} (within 5%)`&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="n"&gt;taintedBuyers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&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="sb"&gt;`${taintedBuyers.length} buyer(s) funded by a flagged wallet`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="n"&gt;report&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="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sellers&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;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sellers&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;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(&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="o"&gt;=&amp;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;score&lt;/span&gt; &lt;span class="o"&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;score&lt;/span&gt;&lt;span class="p"&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;const&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`${r.token.padEnd(8)} score ${r.score}/5`&lt;/span&gt;&lt;span class="p"&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;const&lt;/span&gt; &lt;span class="n"&gt;why&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`   - ${why}`&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="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`   =&amp;gt; treat sells to ${r.sellers.join(', ')} as coming from the flagged source`&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;
  
  
  Running it
&lt;/h2&gt;

&lt;p&gt;The sample file is made up, shaped like the pattern in the report: one throwaway token (&lt;code&gt;DUCK&lt;/code&gt;) bought by two wallets funded from a flagged address and sold by two fresh ones, and one older token (&lt;code&gt;REALDOG&lt;/code&gt;) traded by seven ordinary wallets. Wallet names are placeholders.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node pool-hop.mjs sample-swaps.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DUCK     score 5/5
   - token 0.3 h old at first swap
   - 4 wallets ever traded it
   - buyers and sellers never overlap
   - out 19700 vs in 20000 (within 5%)
   - 2 buyer(s) funded by a flagged wallet
   =&amp;gt; treat sells to collector-1, collector-2 as coming from the flagged source
REALDOG  score 0/5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What it won't catch
&lt;/h2&gt;

&lt;p&gt;The thresholds are guesses you should tune on real data. A launderer who adds a few decoy traders, lets the token age for a week or splits the exit across many tokens will slip under a fixed cutoff. Elliptic's own description of the operation is many cheap layers stacked together, so treat a score like this as a first filter that tells you which wallets to look at by hand.&lt;/p&gt;

&lt;p&gt;The point that holds up is Elliptic's advice to compliance teams: funds that arrive from a liquidity pool shouldn't be assumed clean, especially when a fresh token and a few wallets are involved. Follow the funding of the buyers and the gap closes.&lt;/p&gt;

&lt;p&gt;To use this on a real chain, you'd build the &lt;code&gt;swaps&lt;/code&gt; list from the pool contract's swap events and the &lt;code&gt;funder&lt;/code&gt; field from each buying wallet's incoming transfers. On NEAR that means reading receipts from an indexer; on EVM chains, &lt;code&gt;Swap&lt;/code&gt; logs plus the transfers before them.&lt;/p&gt;

&lt;p&gt;Sources: Elliptic, "North Korea is minting meme coins to launder the Binance hack" (Nov 9, 2026); Decrypt and Cointelegraph coverage (Nov 9, 2026).&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>security</category>
      <category>javascript</category>
      <category>web3</category>
    </item>
    <item>
      <title>Samsung Wallet sends USDC on Solana. Here's a 40-line script that tells which chain an address is for</title>
      <dc:creator>Swapcore.Exchange</dc:creator>
      <pubDate>Fri, 06 Nov 2026 14:06:44 +0000</pubDate>
      <link>https://dev.to/swapcoreexchange/samsung-wallet-sends-usdc-on-solana-heres-a-40-line-script-that-tells-which-chain-an-address-is-4e0g</link>
      <guid>https://dev.to/swapcoreexchange/samsung-wallet-sends-usdc-on-solana-heres-a-40-line-script-that-tells-which-chain-an-address-is-4e0g</guid>
      <description>&lt;h2&gt;
  
  
  The news, briefly
&lt;/h2&gt;

&lt;p&gt;On November 5, 2026, Samsung Wallet launched stablecoin accounts for eligible U.S. Galaxy users. The account holds USDC &lt;strong&gt;on Solana&lt;/strong&gt;. Users can send it to other Samsung Wallet users or to "compatible external crypto wallets", and Bastion and Coinbase handle custody (The Block, Decrypt).&lt;/p&gt;

&lt;p&gt;"Compatible" is doing a lot of work there. USDC is issued on many chains, and each issuance is a separate token. Send Solana USDC to an Ethereum address and, at best, the wallet blocks you. Sending on the wrong chain from a multi-network exchange is a common way to lose funds.&lt;/p&gt;

&lt;p&gt;So I wrote a small script that answers one question: &lt;strong&gt;what chain family is this address from, judging by its format alone?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The script
&lt;/h2&gt;

&lt;p&gt;No dependencies, Node 18+. It decodes base58 by hand and uses &lt;code&gt;node:crypto&lt;/code&gt; for the TRON checksum.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// which-network.mjs&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createHash&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="s1"&gt;node:crypto&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;ALPHABET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;base58Decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&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;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&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;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;ch&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;str&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;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ALPHABET&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ch&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;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="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;58&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nc"&gt;BigInt&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="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&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="mi"&gt;0&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;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unshift&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;/=&lt;/span&gt; &lt;span class="mi"&gt;256&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;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;ch&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;str&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;ch&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&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="nf"&gt;unshift&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="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Uint8Array&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sha256&lt;/span&gt; &lt;span class="o"&gt;=&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="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&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="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;addr&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="sr"&gt;/^0x&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;0-9a-fA-F&lt;/span&gt;&lt;span class="se"&gt;]{40}&lt;/span&gt;&lt;span class="sr"&gt;$/&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;addr&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;EVM (20 bytes). Same format on Ethereum, Arbitrum, Base, BNB Chain, Avalanche C-Chain... the address alone cannot tell you which one.&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="sr"&gt;/^0x&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;0-9a-fA-F&lt;/span&gt;&lt;span class="se"&gt;]{64}&lt;/span&gt;&lt;span class="sr"&gt;$/&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;addr&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;32-byte hex. Matches Sui or Aptos style, not EVM. An EVM wallet cannot receive here.&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;base58Decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;addr&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;raw&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;raw&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;25&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;raw&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;===&lt;/span&gt; &lt;span class="mh"&gt;0x41&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;ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subarray&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;21&lt;/span&gt;&lt;span class="p"&gt;))).&lt;/span&gt;&lt;span class="nf"&gt;subarray&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;4&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;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subarray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;21&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;ok&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;TRON (base58check, 0x41 prefix, checksum OK).&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="s1"&gt;Looks like TRON but the checksum FAILS. Typo?&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;raw&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;raw&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;32&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="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;32&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="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="mi"&gt;44&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Solana (base58, 32 bytes). No checksum in this format, so a typo can still decode to a valid-looking key.&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;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unknown format. Do not send until you know the network.&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;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;a&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argv&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;2&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;log&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;a&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n  -&amp;gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;classify&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="s2"&gt;\n`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Running it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node which-network.mjs &lt;span class="se"&gt;\&lt;/span&gt;
  EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v &lt;span class="se"&gt;\&lt;/span&gt;
  0xA6ca3bF1a63bCab37725bF4B1FA3F74fBbc9596a &lt;span class="se"&gt;\&lt;/span&gt;
  TCz734oe5wBtXLWYrJAcC5RrRECarmEEsJ &lt;span class="se"&gt;\&lt;/span&gt;
  TCz734oe5wBtXLWYrJAcC5RrRECarmEEsK
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v
  -&amp;gt; Solana (base58, 32 bytes). No checksum in this format, so a typo can still decode to a valid-looking key.

0xA6ca3bF1a63bCab37725bF4B1FA3F74fBbc9596a
  -&amp;gt; EVM (20 bytes). Same format on Ethereum, Arbitrum, Base, BNB Chain, Avalanche C-Chain... the address alone cannot tell you which one.

TCz734oe5wBtXLWYrJAcC5RrRECarmEEsJ
  -&amp;gt; TRON (base58check, 0x41 prefix, checksum OK).

TCz734oe5wBtXLWYrJAcC5RrRECarmEEsK
  -&amp;gt; Looks like TRON but the checksum FAILS. Typo?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first input is the USDC mint on Solana. The last one is the TRON address from line three with its final character changed, and the checksum catches it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the output teaches
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;TRON catches typos, Solana doesn't.&lt;/strong&gt; A TRON address is base58check: 21 bytes of payload plus 4 bytes of double-SHA256. Change one character and the checksum almost certainly fails. A Solana address is a raw 32-byte public key in base58, so a mistyped address usually still decodes to 32 bytes and passes every format check. If your UI accepts pasted Solana addresses, the format check is nowhere near enough. Show the address back to the user in chunks, or require a small test send.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;EVM addresses are ambiguous by design.&lt;/strong&gt; &lt;code&gt;0x&lt;/code&gt; plus 40 hex chars is valid on every EVM chain. The address is the same on all of them, but the USDC contract and the balance are different. Format checks can't tell you whether the user meant Ethereum or Arbitrum, so the network has to be an explicit choice in your UI and never inferred. (EIP-55 mixed-case checksums help with typos, not with the chain question.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;32-byte hex is not EVM.&lt;/strong&gt; Sui and Aptos use 32-byte hex addresses with the same &lt;code&gt;0x&lt;/code&gt; prefix. A regex that only checks &lt;code&gt;^0x[0-9a-f]+$&lt;/code&gt; will happily accept them for an EVM transfer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this fits
&lt;/h2&gt;

&lt;p&gt;Samsung hides most of this behind a custodial account, which is a reasonable product choice. But the moment users send funds out, someone's code has to answer "is this address for the right chain?" A format check like this one is cheap first-line validation. It doesn't replace an explicit network selector, and it doesn't replace a test transaction.&lt;/p&gt;

&lt;p&gt;The full script and its output are about 40 lines. Feel free to lift it.&lt;/p&gt;

&lt;p&gt;Sources: The Block, "Samsung Wallet launches USDC accounts on Solana for US Galaxy users" (Nov 5, 2026); Decrypt, "Stablecoins Go Mainstream as Samsung Wallet Adds USDC on Solana" (Nov 5, 2026).&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>blockchain</category>
      <category>solana</category>
      <category>security</category>
    </item>
    <item>
      <title>"USDT" Is at Least Three Different Tokens. Your Token Registry Should Know That.</title>
      <dc:creator>Swapcore.Exchange</dc:creator>
      <pubDate>Thu, 22 Oct 2026 20:47:40 +0000</pubDate>
      <link>https://dev.to/swapcoreexchange/usdt-is-at-least-three-different-tokens-your-token-registry-should-know-that-14h4</link>
      <guid>https://dev.to/swapcoreexchange/usdt-is-at-least-three-different-tokens-your-token-registry-should-know-that-14h4</guid>
      <description>&lt;p&gt;If your product handles USDT across networks, there's a good chance your token model treats it as one asset with several "networks." That model is wrong in ways that matter for accounting, risk and support.&lt;/p&gt;

&lt;p&gt;In practice, what wallets display as "USDT" includes at least three structurally different things:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Kind&lt;/th&gt;
&lt;th&gt;Example chains&lt;/th&gt;
&lt;th&gt;Issued by&lt;/th&gt;
&lt;th&gt;How it moves cross-chain&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Native USDT&lt;/td&gt;
&lt;td&gt;Ethereum, Tron, Solana, TON, Avalanche&lt;/td&gt;
&lt;td&gt;Tether&lt;/td&gt;
&lt;td&gt;It doesn't. Each chain has its own supply&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exchange-pegged USDT&lt;/td&gt;
&lt;td&gt;BNB Chain (Binance-Peg BSC-USD)&lt;/td&gt;
&lt;td&gt;Binance, backed by USDT&lt;/td&gt;
&lt;td&gt;Via the issuer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Omnichain USDT0&lt;/td&gt;
&lt;td&gt;Arbitrum, Optimism, Polygon, newer EVM chains&lt;/td&gt;
&lt;td&gt;Everdawn Labs, backed by USDT locked on Ethereum&lt;/td&gt;
&lt;td&gt;LayerZero burn-and-mint&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;They're all worth about a dollar. They don't share an issuer, a redemption path or a risk profile.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Model issuer and kind, not just chain
&lt;/h2&gt;

&lt;p&gt;A registry keyed only by &lt;code&gt;(chain, contract)&lt;/code&gt; is necessary but not sufficient. Add the fields that describe what the token actually is:&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;"USDT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"chain"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arbitrum"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"contract"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0x…"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"decimals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"omnichain"&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"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"everdawn-labs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"backing"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"native-usdt-locked-ethereum"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"redeemable_with_tether"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&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;Why it matters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Accounting.&lt;/strong&gt; Treating Binance-Peg USDT and USDT0 as interchangeable with native USDT in your books means your stablecoin exposure has counterparties you aren't tracking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Risk limits.&lt;/strong&gt; A depeg, an exploit in a bridge's lockbox or an issuer problem hits one kind without touching the others. Exposure limits should be per kind.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Support.&lt;/strong&gt; "I sent USDT and received USDT0" is a real ticket. If your system can't tell them apart, neither can your agents.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. Never resolve tokens by symbol
&lt;/h2&gt;

&lt;p&gt;On most EVM chains, anyone can deploy a contract named "USDT." Explorers on BNB Chain list many of them. Combined with the legitimate variety above, symbol-based resolution is guaranteed to misfire eventually.&lt;/p&gt;

&lt;p&gt;Keep an explicit allowlist of &lt;code&gt;(chain, contract)&lt;/code&gt; pairs and reject everything else, including in admin tools and indexers.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Tron needs its own transfer logic
&lt;/h2&gt;

&lt;p&gt;TRC20 isn't an EVM chain with a different address prefix. Its fee model is different:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bandwidth&lt;/strong&gt; covers transaction size. Each account gets a small free daily allowance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Energy&lt;/strong&gt; covers smart contract execution. A TRC20 transfer is a contract call, so it needs energy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the sending account hasn't staked TRX for energy, the network burns TRX to pay for it. If there isn't enough TRX, the transaction fails, and the TRX it consumed is still gone.&lt;/p&gt;

&lt;p&gt;Practical consequences for payouts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before a TRC20 payout, check the sender can actually pay for it.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resources&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;tronWeb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAccountResources&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hotWallet&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;energyAvailable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;EnergyLimit&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;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;EnergyUsed&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;energyAvailable&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;estimatedEnergy&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;trxNeeded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;estimateTrxBurn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;estimatedEnergy&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;energyAvailable&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;assertBalanceAtLeast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hotWallet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;trxNeeded&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;Also budget for &lt;strong&gt;first-transfer cost&lt;/strong&gt;. Sending USDT to a Tron address that has never held it costs roughly double, because the transfer has to create the token balance. If you sweep deposits to many fresh addresses, this shows up in your costs.&lt;/p&gt;

&lt;p&gt;Set &lt;code&gt;feeLimit&lt;/code&gt; on TRC20 transactions deliberately. Too low and they fail mid-execution; too high and a bug can burn more than you meant.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Cross-network conversion is a trade, not a transfer
&lt;/h2&gt;

&lt;p&gt;If your product lets users "move USDT from Tron to Ethereum," it's executing a swap between two different tokens, whatever the UI says. That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Quote it like a swap.&lt;/strong&gt; Show the amount received net of the destination network fee and any spread. It won't be exactly 1:1.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Track it like a swap.&lt;/strong&gt; Two legs, two chains, two transaction IDs, and a failure state for each.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Label the output precisely.&lt;/strong&gt; If the destination is USDT0, say USDT0. Don't let a user find out from their wallet.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. Gas tokens on both sides
&lt;/h2&gt;

&lt;p&gt;A user receiving USDT on a chain where they hold no native token can't move it. On Ethereum, that's ETH; on Tron, TRX; on BNB Chain, BNB.&lt;/p&gt;

&lt;p&gt;If your product delivers USDT to a user's wallet on a network that's new for them, say so before they confirm. Better still, point them to how to get the gas token.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Registry fields for &lt;code&gt;kind&lt;/code&gt;, &lt;code&gt;issuer&lt;/code&gt;, &lt;code&gt;backing&lt;/code&gt; and redeemability&lt;/li&gt;
&lt;li&gt;Allowlist by &lt;code&gt;(chain, contract)&lt;/code&gt;; never resolve by symbol&lt;/li&gt;
&lt;li&gt;Exposure limits per token kind, not per symbol&lt;/li&gt;
&lt;li&gt;Tron payouts check energy and TRX before sending; &lt;code&gt;feeLimit&lt;/code&gt; set deliberately&lt;/li&gt;
&lt;li&gt;First-transfer costs to fresh Tron addresses budgeted&lt;/li&gt;
&lt;li&gt;Cross-network moves modeled as two-leg swaps, labeled with the actual output token&lt;/li&gt;
&lt;li&gt;Users warned when they'll receive on a chain without gas&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Disclosure: I work on SwapCore (swapcore.exchange), a non-custodial instant exchange. Cross-network USDT conversions are one of the most common things people use us for, and most of the edge cases above come from them.&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>crypto</category>
      <category>cryptocurrency</category>
    </item>
    <item>
      <title>Your Ethereum Gas Assumptions Are Probably From 2021</title>
      <dc:creator>Swapcore.Exchange</dc:creator>
      <pubDate>Wed, 14 Oct 2026 01:09:15 +0000</pubDate>
      <link>https://dev.to/swapcoreexchange/your-ethereum-gas-assumptions-are-probably-from-2021-d64</link>
      <guid>https://dev.to/swapcoreexchange/your-ethereum-gas-assumptions-are-probably-from-2021-d64</guid>
      <description>&lt;p&gt;A lot of Ethereum deposit and payout code still carries assumptions from when gas was the dominant cost: hardcoded fee constants, minimums sized for twenty-dollar transfers, defaults that steer users away from mainnet. Through 2026, standard gas has mostly sat well under one gwei. An ERC-20 transfer costs cents.&lt;/p&gt;

&lt;p&gt;That doesn't just make old code expensive. It makes some of it wrong. Here's what to revisit if you accept ETH or ERC-20 deposits.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Stop hardcoding gas prices and minimums
&lt;/h2&gt;

&lt;p&gt;If a constant like this is still in your codebase, it's overcharging by two orders of magnitude:&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;# Written for a different fee era
&lt;/span&gt;&lt;span class="n"&gt;PAYOUT_GAS_PRICE_GWEI&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;
&lt;span class="n"&gt;MIN_DEPOSIT_USDT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since the London upgrade, fees are EIP-1559: a protocol-set base fee plus a priority tip. Estimate both from live data at the moment of payout:&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;estimate_fees&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;priority_percentile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;hist&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;w3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;eth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fee_history&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;latest&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="n"&gt;priority_percentile&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;base_fee&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hist&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;baseFeePerGas&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;# next block's base fee
&lt;/span&gt;    &lt;span class="n"&gt;tips&lt;/span&gt; &lt;span class="o"&gt;=&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="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="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;hist&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reward&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
    &lt;span class="n"&gt;tip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tips&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tips&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="c1"&gt;# Allow the base fee to rise for a few blocks before the tx lands
&lt;/span&gt;    &lt;span class="n"&gt;max_fee&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base_fee&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;tip&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;maxFeePerGas&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;max_fee&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;maxPriorityFeePerGas&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tip&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then derive minimums from the live estimate instead of a fixed value. A minimum sized for 40 gwei turns away users your economics could serve profitably today, and a minimum sized for 0.2 gwei will be wrong the next time the network gets busy. Recompute per quote.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Detect internal transactions, not just top-level transfers
&lt;/h2&gt;

&lt;p&gt;The simplest deposit watcher checks each block for transactions whose &lt;code&gt;to&lt;/code&gt; is a deposit address and whose &lt;code&gt;value&lt;/code&gt; is non-zero. That misses ETH sent from smart contract wallets.&lt;/p&gt;

&lt;p&gt;A Safe multisig, for example, executes a transfer through a contract call. On-chain, the ETH movement is an internal call, not a top-level transaction to your address. A naive watcher never sees it.&lt;/p&gt;

&lt;p&gt;You have two reliable options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Trace calls.&lt;/strong&gt; Use &lt;code&gt;debug_traceTransaction&lt;/code&gt; or &lt;code&gt;trace_block&lt;/code&gt; (provider-dependent) and look for value transfers to your addresses at any call depth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch balances.&lt;/strong&gt; Poll &lt;code&gt;eth_getBalance&lt;/code&gt; for deposit addresses at each block, and reconcile any increase against known transactions. Cruder, but it catches everything.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;EIP-7702, introduced in the Pectra upgrade, lets ordinary accounts delegate to contract code. As adoption grows, the share of ETH deposits arriving through contract execution rather than plain transfers will grow with it. Plan for internal transfers to be normal.&lt;/p&gt;

&lt;p&gt;ERC-20 deposits don't have this problem in the same way, because they emit a &lt;code&gt;Transfer&lt;/code&gt; event regardless of who calls the token contract. Watch logs for those.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Use finality, not just confirmations
&lt;/h2&gt;

&lt;p&gt;Ethereum produces a block every 12 seconds. Proof-of-stake adds a second signal: finality. A block becomes finalized after two epochs, roughly 13 minutes, at which point reverting it would require a large share of staked ETH to be destroyed.&lt;/p&gt;

&lt;p&gt;Many services still use a confirmation count carried over from proof-of-work habits. You can do better with the block tags the JSON-RPC API exposes:&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="n"&gt;latest&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;w3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;eth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_block&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;latest&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;safe&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;w3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;eth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_block&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;safe&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="c1"&gt;# justified, very unlikely to reorg
&lt;/span&gt;&lt;span class="n"&gt;finalized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;w3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;eth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_block&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;finalized&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# economically final
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;is_credited&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deposit_block&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount_usd&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;threshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;safe&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;amount_usd&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;10_000&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;finalized&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;deposit_block&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;number&lt;/span&gt;&lt;span class="sh"&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;safe&lt;/code&gt; usually trails the head by a few minutes; &lt;code&gt;finalized&lt;/code&gt; by around 13. Tiering by amount gives most users a fast credit and keeps large deposits on economic finality.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Treat L2 ETH as a different asset
&lt;/h2&gt;

&lt;p&gt;ETH on mainnet, Arbitrum, Base and Optimism all use identical 0x addresses. A deposit address you generated for mainnet is, byte for byte, a valid address on every one of them.&lt;/p&gt;

&lt;p&gt;Users will send L2 ETH to a mainnet deposit address. Your mainnet watcher won't see it. Two things help:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Model assets as &lt;code&gt;(chain_id, asset)&lt;/code&gt; pairs everywhere&lt;/strong&gt;, so "ETH on Base" is never conflated with "ETH on mainnet" in quotes, orders or UI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optionally watch your deposit addresses on the major L2s too.&lt;/strong&gt; You won't credit those deposits automatically, but you can flag them for support instead of having a user report "lost" funds days later.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. Warn about the empty-gas-tank problem
&lt;/h2&gt;

&lt;p&gt;A user swapping their entire ETH balance into USDT on Ethereum ends up with tokens they can't move, because they have no ETH for gas.&lt;/p&gt;

&lt;p&gt;If your interface offers a "max" button for ETH, consider a soft default that leaves a small reserve, with an explicit override:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RESERVE_WEI&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;estimateReserve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;liveFees&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="c1"&gt;// ~5 future transfers&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;maxSendable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;gasForThisTx&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;RESERVE_WEI&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At current fee levels the reserve is tiny. The point is that it isn't zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Never ask for approvals on a deposit flow
&lt;/h2&gt;

&lt;p&gt;A deposit-address model is a plain transfer. It needs no &lt;code&gt;approve&lt;/code&gt;, no &lt;code&gt;permit&lt;/code&gt;, no signed message.&lt;/p&gt;

&lt;p&gt;If your product uses deposit addresses, make that explicit in your UI copy. Users have learned that legitimate swaps sometimes ask for approvals (router-based DEX swaps do). A clear statement that yours never will gives them a way to spot clones impersonating you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Fees estimated live with EIP-1559 fields; no hardcoded gwei&lt;/li&gt;
&lt;li&gt;Minimums derived per quote from live fees&lt;/li&gt;
&lt;li&gt;Deposit detection covers internal transactions&lt;/li&gt;
&lt;li&gt;Crediting uses &lt;code&gt;safe&lt;/code&gt; / &lt;code&gt;finalized&lt;/code&gt; tags, tiered by amount&lt;/li&gt;
&lt;li&gt;Assets keyed by chain ID; L2 deposits to mainnet addresses flagged&lt;/li&gt;
&lt;li&gt;"Max" leaves a gas reserve by default&lt;/li&gt;
&lt;li&gt;Deposit flow never requests approvals or signatures&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Disclosure: I work on SwapCore (swapcore.exchange), a non-custodial instant exchange. Several of these came from revisiting our own assumptions after Ethereum fees fell.&lt;/p&gt;

</description>
      <category>crypto</category>
      <category>ethereum</category>
      <category>web3</category>
    </item>
    <item>
      <title>Accepting Bitcoin Deposits Correctly: Confirmations, Replacement and Address Types</title>
      <dc:creator>Swapcore.Exchange</dc:creator>
      <pubDate>Tue, 06 Oct 2026 20:02:41 +0000</pubDate>
      <link>https://dev.to/swapcoreexchange/accepting-bitcoin-deposits-correctly-confirmations-replacement-and-address-types-39ko</link>
      <guid>https://dev.to/swapcoreexchange/accepting-bitcoin-deposits-correctly-confirmations-replacement-and-address-types-39ko</guid>
      <description>&lt;p&gt;If your product accepts BTC deposits, for a swap, a checkout or a top-up, the patterns that work on account-based chains will get you into trouble. Bitcoin's UTXO model, probabilistic finality and fee market each need specific handling.&lt;/p&gt;

&lt;p&gt;Here's what to get right.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Never credit on zero confirmations
&lt;/h2&gt;

&lt;p&gt;An unconfirmed Bitcoin transaction is a proposal, not a payment. Until it's in a block, the sender can replace it with a conflicting transaction that pays the same coins elsewhere.&lt;/p&gt;

&lt;p&gt;Most nodes on the network now accept fee-bumping replacements, so treating unconfirmed transactions as safe is not a viable risk model.&lt;/p&gt;

&lt;p&gt;Make the confirmation threshold a config value, and consider scaling it with amount:&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;required_confirmations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount_btc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Illustrative policy. Tune to your own risk tolerance.
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;amount_btc&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;amount_btc&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expose the threshold and the current count to the user. "2 of 3 confirmations" ends a support ticket before it starts.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Track deposits by outpoint, not by txid alone
&lt;/h2&gt;

&lt;p&gt;A replaced transaction gets a new txid. If your deposit detection keys on the original txid, a fee bump turns into a "missing" deposit from your system's point of view.&lt;/p&gt;

&lt;p&gt;Watch the address, not the transaction. When a new transaction pays the same deposit address, check whether it conflicts with one you've already seen (spends the same inputs). If it does, treat it as a replacement: update the record and keep the order alive.&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;on_transaction_seen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;deposit_address&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;outputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;outputs&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;deposit_address&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;outputs&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;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;find_pending_by_inputs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inputs&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;existing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# RBF: same inputs, new txid
&lt;/span&gt;    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;create_pending_deposit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same logic handles the reverse case: a deposit that disappears from the mempool because it was replaced by a transaction paying somewhere else. Mark it failed explicitly instead of leaving it pending forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Your quote window and Bitcoin block time don't agree
&lt;/h2&gt;

&lt;p&gt;Average block time is ten minutes, but it follows an exponential distribution. Waits of thirty minutes or more between blocks happen regularly.&lt;/p&gt;

&lt;p&gt;If you offer locked-rate quotes, a ten-minute window on a BTC deposit will expire often. Options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Measure the window from first sighting in the mempool, not from confirmation.&lt;/strong&gt; The user did their part when they broadcast. This is the fairest approach, but you carry price risk until confirmation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extend windows for BTC specifically&lt;/strong&gt;, and price the extra risk into the spread.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Default BTC deposits to floating rates&lt;/strong&gt;, and make locked rates opt-in with a clear warning.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whichever you choose, model &lt;code&gt;quote_expired&lt;/code&gt; as a normal state with a one-click re-quote.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Validate address types, all four of them
&lt;/h2&gt;

&lt;p&gt;Bitcoin has four mainnet address formats:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Prefix&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Encoding&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;1…&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;P2PKH (legacy)&lt;/td&gt;
&lt;td&gt;Base58Check&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;3…&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;P2SH (often nested SegWit)&lt;/td&gt;
&lt;td&gt;Base58Check&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bc1q…&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;P2WPKH / P2WSH (native SegWit v0)&lt;/td&gt;
&lt;td&gt;Bech32&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bc1p…&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;P2TR (Taproot, SegWit v1)&lt;/td&gt;
&lt;td&gt;Bech32m&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The common bug is a validator written before Taproot that accepts Bech32 but rejects Bech32m, so valid &lt;code&gt;bc1p&lt;/code&gt; addresses get refused. The less common but worse bug is a validator that accepts Bech32m checksums for v0 addresses, or the reverse. BIP-350 defines which checksum applies to which witness version. Use a maintained library instead of a regex.&lt;/p&gt;

&lt;p&gt;Also reject testnet prefixes (&lt;code&gt;tb1&lt;/code&gt;, &lt;code&gt;m&lt;/code&gt;, &lt;code&gt;n&lt;/code&gt;, &lt;code&gt;2&lt;/code&gt;) in production, and the reverse in staging.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Handle amounts in satoshis, and expect wallets to disagree about fees
&lt;/h2&gt;

&lt;p&gt;Store amounts as integers in satoshis. Floating-point BTC values cause rounding errors at the eighth decimal place.&lt;/p&gt;

&lt;p&gt;Expect underpayments. Some wallets subtract the network fee from the amount the user typed; others add it on top. A user who "sent 0.05 BTC" may have delivered 0.04998. Decide upfront how you treat a shortfall: tolerance band, execute on received amount, or refund. Surface the rule before the user sends.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Refunds need a refund address
&lt;/h2&gt;

&lt;p&gt;Refunding to the sender address is wrong for Bitcoin in two ways. A UTXO transaction can have many inputs from many addresses, so "the sender" may not be one address. And if the deposit came from an exchange withdrawal, every input belongs to the exchange's shared wallet.&lt;/p&gt;

&lt;p&gt;Collect a refund address at order creation. Validate it with the same rules as above.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Estimate payout fees from live data
&lt;/h2&gt;

&lt;p&gt;If you send BTC back out, for refunds or payouts, fee estimation can't be a constant. Pull from your node's &lt;code&gt;estimatesmartfee&lt;/code&gt; or a mempool API, choose a confirmation target, and recompute minimums from the result. A fixed fee will overpay in quiet periods and get stuck in busy ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Credit on confirmations, never zero-conf&lt;/li&gt;
&lt;li&gt;Track deposits by address and inputs so RBF doesn't lose them&lt;/li&gt;
&lt;li&gt;Align quote windows with Bitcoin's real block time distribution&lt;/li&gt;
&lt;li&gt;Validate all four address types with a library that implements BIP-350&lt;/li&gt;
&lt;li&gt;Store satoshis as integers; define underpayment handling upfront&lt;/li&gt;
&lt;li&gt;Collect a refund address&lt;/li&gt;
&lt;li&gt;Estimate payout fees live&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Disclosure: I work on SwapCore (swapcore.exchange), a non-custodial instant exchange. BTC deposits are where most of the edge cases above come from, for us and for anyone else handling them.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>bitcoin</category>
      <category>blockchain</category>
      <category>crypto</category>
    </item>
    <item>
      <title>Integrating a Swap Provider? Here's the Threat Model You Inherit</title>
      <dc:creator>Swapcore.Exchange</dc:creator>
      <pubDate>Tue, 29 Sep 2026 09:21:08 +0000</pubDate>
      <link>https://dev.to/swapcoreexchange/integrating-a-swap-provider-heres-the-threat-model-you-inherit-38c8</link>
      <guid>https://dev.to/swapcoreexchange/integrating-a-swap-provider-heres-the-threat-model-you-inherit-38c8</guid>
      <description>&lt;p&gt;"Non-custodial" in a swap provider's docs usually gets read as "not my security problem." That's half right. Integrating a non-custodial instant exchange removes the biggest risk class, which is holding user balances. But it hands your users a different, smaller one, and parts of that risk run straight through your frontend.&lt;/p&gt;

&lt;p&gt;Here's the threat model, split by who owns each piece.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, get the custody model right
&lt;/h2&gt;

&lt;p&gt;A non-custodial instant exchange isn't zero-custody. The flow looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dot"&gt;&lt;code&gt;&lt;span class="nv"&gt;user&lt;/span&gt; &lt;span class="nv"&gt;wallet&lt;/span&gt; &lt;span class="err"&gt;──&lt;/span&gt;&lt;span class="nv"&gt;deposit&lt;/span&gt;&lt;span class="err"&gt;──▶&lt;/span&gt; &lt;span class="nv"&gt;provider&lt;/span&gt; &lt;span class="nv"&gt;deposit&lt;/span&gt; &lt;span class="nv"&gt;address&lt;/span&gt; &lt;span class="err"&gt;──&lt;/span&gt;&lt;span class="nv"&gt;swap&lt;/span&gt;&lt;span class="err"&gt;──▶&lt;/span&gt; &lt;span class="nv"&gt;user&lt;/span&gt; &lt;span class="nv"&gt;wallet&lt;/span&gt;
   &lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;user&lt;/span&gt; &lt;span class="nv"&gt;keys&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;            &lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;PROVIDER&lt;/span&gt; &lt;span class="nv"&gt;KEYS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;transit&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;           &lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;user&lt;/span&gt; &lt;span class="nv"&gt;keys&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From deposit confirmation until payout, the provider controls the funds. The window is short, usually minutes, bounded by confirmation time on the input chain. But it exists, and any honest threat model starts from it.&lt;/p&gt;

&lt;p&gt;What your app never holds: keys, balances, deposits. That's the real win. You're not a custodian, you don't carry a hot wallet, and your breach doesn't drain anyone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Threats the provider owns
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Transit-window compromise.&lt;/strong&gt; If the provider's operational wallets are breached, in-flight funds are exposed. This has happened in this category: FixedFloat lost around $26M from its own wallets in February 2024. You can't mitigate it in code. You mitigate it in vendor selection (incident history, how they handled it, whether in-flight users were made whole) and by keeping per-swap exposure small.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AML holds.&lt;/strong&gt; Deposits get screened, and flagged ones pause. That's the provider's process, but surfacing it is your job. Give it a distinct &lt;code&gt;held_for_review&lt;/code&gt; state and don't fold it into &lt;code&gt;pending&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Availability.&lt;/strong&gt; Rate feeds stall, pairs get disabled, minimums spike with congestion. Treat every quote as perishable and every pair as possibly unavailable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Threats you own
&lt;/h2&gt;

&lt;p&gt;This is the part integrators underestimate. Your frontend is the only place several of these can be caught.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Address substitution.&lt;/strong&gt; Clipboard malware swaps a copied address for the attacker's. Your UI can't detect malware, but it can make substitution visible:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Render the destination with its first and last 6 chars emphasized,&lt;/span&gt;
&lt;span class="c1"&gt;// and restate it on the confirmation step. Visual diffing catches&lt;/span&gt;
&lt;span class="c1"&gt;// most substitutions, which change the whole string.&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;renderAddress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;addr&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="na"&gt;head&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;addr&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;6&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;middle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;addr&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;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;tail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;addr&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="o"&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Chain ambiguity.&lt;/strong&gt; EVM addresses carry no chain identity. Never infer the chain from an address. Make it a required selection with no default, and restate it in words at confirmation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conditional memos.&lt;/strong&gt; For XRP, XLM, ATOM, HBAR, EOS and TON, a memo is required when the destination is a shared (exchange) address and meaningless when it's self-custody. Ask which one it is, and make the field blocking only when it applies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Refund routing.&lt;/strong&gt; Refunds go to the sender address by default. If the user funded from an exchange withdrawal, that's a pooled hot wallet and the refund is effectively lost. Collect a refund address at order creation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;order&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;createOrder&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;pair&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="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;refundAddress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;connectedWallet&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;userInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;refundAddress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// never null silently&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Clone-site exposure.&lt;/strong&gt; If you embed a widget, load it from the provider's canonical origin and pin it in your CSP. A compromised or typo-squatted widget source is a deposit-address swap at scale.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Content-Security-Policy: frame-src https://widget.&amp;lt;provider-domain&amp;gt;;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Quote integrity.&lt;/strong&gt; If quotes pass through your backend, sign or verify them end-to-end. A deposit address that can be altered anywhere between provider and user is the single highest-value target in your stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  Threats nobody in the stack owns
&lt;/h2&gt;

&lt;p&gt;Worth stating in your docs so users aren't surprised:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Key loss&lt;/strong&gt;: self-custody moves it entirely to the user&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Malicious token approvals&lt;/strong&gt; signed elsewhere, which live in the user's wallet&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Issuer freezes&lt;/strong&gt;: Tether can freeze USDT at the contract level on any chain&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Shrinking the transit window by design
&lt;/h2&gt;

&lt;p&gt;The one risk the model adds is time in transit. Design to minimise it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prefer fast input chains&lt;/strong&gt; in your defaults when the user holds the asset on several&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't create the order until the user is ready to send.&lt;/strong&gt; Generate the deposit address at the last step, not the first, so abandoned sessions don't leave stale addresses around.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offer split execution above a threshold.&lt;/strong&gt; Several moderate swaps cap peak exposure below one large one.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Threat&lt;/th&gt;
&lt;th&gt;Owner&lt;/th&gt;
&lt;th&gt;Mitigation lives in&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Transit-window breach&lt;/td&gt;
&lt;td&gt;Provider&lt;/td&gt;
&lt;td&gt;Vendor selection, size limits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AML hold&lt;/td&gt;
&lt;td&gt;Provider&lt;/td&gt;
&lt;td&gt;Your state machine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Address substitution&lt;/td&gt;
&lt;td&gt;You&lt;/td&gt;
&lt;td&gt;Confirmation UI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wrong chain&lt;/td&gt;
&lt;td&gt;You&lt;/td&gt;
&lt;td&gt;Required selection, restatement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Missing memo&lt;/td&gt;
&lt;td&gt;You&lt;/td&gt;
&lt;td&gt;Conditional blocking field&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lost refunds&lt;/td&gt;
&lt;td&gt;You&lt;/td&gt;
&lt;td&gt;Refund address at creation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Widget/clone tampering&lt;/td&gt;
&lt;td&gt;You&lt;/td&gt;
&lt;td&gt;Canonical origin, CSP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Key loss, bad approvals, issuer freeze&lt;/td&gt;
&lt;td&gt;User&lt;/td&gt;
&lt;td&gt;Documentation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Non-custodial removes the catastrophic risk: holding everyone's money. What's left is smaller, but most of it gets caught or missed in your frontend.&lt;/p&gt;




&lt;p&gt;Disclosure: I work on SwapCore (swapcore.exchange), a non-custodial instant exchange. We have a transit window like every provider in this category, and the mitigations above are the ones I'd expect any integrator to ask us about.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>cybersecurity</category>
      <category>security</category>
    </item>
    <item>
      <title>Show Users the Real Cost: Building Honest Pricing Into a Crypto Swap UI</title>
      <dc:creator>Swapcore.Exchange</dc:creator>
      <pubDate>Sun, 27 Sep 2026 23:17:17 +0000</pubDate>
      <link>https://dev.to/swapcoreexchange/show-users-the-real-cost-building-honest-pricing-into-a-crypto-swap-ui-4j9m</link>
      <guid>https://dev.to/swapcoreexchange/show-users-the-real-cost-building-honest-pricing-into-a-crypto-swap-ui-4j9m</guid>
      <description>&lt;p&gt;If your product quotes crypto conversions, your UI is making a claim about cost whether you intended to or not. Most make it badly — and the fix is a handful of fields you probably already have access to.&lt;/p&gt;

&lt;p&gt;Here's what an honest quote object looks like and how to compute it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why &lt;code&gt;fee: 0.005&lt;/code&gt; is not a price
&lt;/h2&gt;

&lt;p&gt;A swap has four cost components. Most quote objects surface one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. input network fee    — paid by the user to their own chain (never yours)
2. output network fee   — deducted from the payout
3. service fee          — your explicit charge
4. spread               — quoted rate vs. market rate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Display only component 3 and your interface is technically accurate and practically misleading. Users discover the difference by comparing the balance change in their wallet against what your UI promised, and they discover it after the transaction is irreversible.&lt;/p&gt;

&lt;p&gt;The number a user cares about is a single figure: &lt;strong&gt;what fraction of market value did this operation consume.&lt;/strong&gt; Everything else is internal accounting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Computing the effective rate
&lt;/h2&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;effective_cost_bps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount_in&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount_out_received&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mid_market_rate&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Total cost in basis points, relative to mid-market.
    Captures spread + service fee + output network fee together.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;amount_in&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;mid_market_rate&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;expected&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;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mid_market_rate and amount_in must be positive&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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;expected&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;amount_out_received&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;10_000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two implementation notes that matter more than the formula.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your reference rate needs a timestamp and a source.&lt;/strong&gt; A cost figure computed against a rate from thirty seconds ago is noise on a volatile pair. Pin the rate you quoted against, store it with the quote, and surface both. &lt;code&gt;cost_bps: 180&lt;/code&gt; with no reference rate is an unfalsifiable claim.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decide gross or net and label it.&lt;/strong&gt; If your &lt;code&gt;amount_out&lt;/code&gt; is before the output network fee is deducted, say so in the field name — &lt;code&gt;amount_out_gross&lt;/code&gt; — because a user comparing your quote against their wallet balance is comparing net. This single ambiguity generates more "you charged me more than you said" tickets than actual overcharging does.&lt;/p&gt;

&lt;h2&gt;
  
  
  A quote object that doesn't lie
&lt;/h2&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;"pair"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ETH_USDT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"amount_in"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rate_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;"floating"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"reference_rate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"3000.00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reference_source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"coingecko"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reference_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-09-28T01:40:12Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"amount_out_gross"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2955.00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"output_network_fee"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"15.00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"amount_out_net"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2940.00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"service_fee_bps"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"spread_bps"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"total_cost_bps"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"quote_expires_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-09-28T01:50:12Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"minimum_in"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.012"&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;Points worth arguing about:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;spread_bps&lt;/code&gt; should be derived, not asserted.&lt;/strong&gt; Compute it as &lt;code&gt;total_cost_bps − service_fee_bps − (output_network_fee / expected × 10000)&lt;/code&gt;. If you assert it independently, the three numbers will drift out of agreement and someone will notice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;total_cost_bps&lt;/code&gt; is the headline field.&lt;/strong&gt; It's the only one that maps to what the user experiences. If your UI shows a percentage anywhere, it should be this one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;quote_expires_at&lt;/code&gt;, not &lt;code&gt;valid_for_seconds&lt;/code&gt;.&lt;/strong&gt; A duration requires the client to know when the quote was issued, and clock skew makes that unreliable. An absolute timestamp doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Minimums are dynamic and most codebases hardcode them
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;minimum_in&lt;/code&gt; exists because of component 2: if the output network fee exceeds the output value, the operation is uneconomic. Network fees move with congestion, so the minimum moves with them.&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;minimum_input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output_network_fee&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_fee_fraction&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Smallest input where the output fee stays under max_fee_fraction
    of the payout. Recompute per quote — never cache across sessions.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;min_output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;output_network_fee&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;max_fee_fraction&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;min_output&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A hardcoded minimum fails in both directions — rejecting valid amounts when fees fall, accepting uneconomic ones when they spike — and it fails silently. Take it from the live quote every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixed vs floating: same schema, different honesty requirement
&lt;/h2&gt;

&lt;p&gt;A fixed rate legitimately costs more, because the platform absorbs price movement for the quote window. Your UI should say that in one line rather than presenting the two as if one is simply worse value.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Floating — 1.2% est. cost. Final amount set when your deposit confirms.
Fixed    — 2.1% cost. Exact amount locked for 9:47.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things this framing gets right that a bare toggle doesn't: the cost difference is visible, and the reason for it is attributed to something the user chose.&lt;/p&gt;

&lt;p&gt;One rule worth encoding: &lt;strong&gt;do not offer a fixed rate as the default when the source chain is slow.&lt;/strong&gt; A locked window funded by a Bitcoin transaction will expire. Either shorten the choice to floating, or extend the window and price it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparing against other providers
&lt;/h2&gt;

&lt;p&gt;If you aggregate or display competing quotes, three requirements to be honest about it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Same timestamp.&lt;/strong&gt; Quotes fetched more than a few seconds apart on a volatile pair aren't comparable. Fetch in parallel, record the fetch time, and show it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Same net basis.&lt;/strong&gt; If one provider quotes gross and another net, normalise before comparing or the comparison is meaningless.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Same rate type.&lt;/strong&gt; Ranking a fixed quote against floating quotes makes the fixed provider look expensive for offering more.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Skip any of these and you have a marketing table, not a price comparison.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test worth writing
&lt;/h2&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;test_cost_fields_reconcile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount_in&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reference_rate&lt;/span&gt;
    &lt;span class="n"&gt;fee_bps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output_network_fee&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;10_000&lt;/span&gt;
    &lt;span class="n"&gt;derived&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;service_fee_bps&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;spread_bps&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;fee_bps&lt;/span&gt;

    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;derived&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_cost_bps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;  &lt;span class="c1"&gt;# bps tolerance
&lt;/span&gt;    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount_out_net&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount_out_gross&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output_network_fee&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that test can't pass, your pricing fields don't describe the same transaction, and the discrepancy will surface as a support ticket rather than a failing build.&lt;/p&gt;




&lt;p&gt;Disclosure: I work on SwapCore (swapcore.exchange), a non-custodial instant exchange. The schema above is what we'd defend; the reconciliation test is the part I'd argue every provider in this category should be able to pass.&lt;/p&gt;

</description>
      <category>crypto</category>
      <category>frontend</category>
      <category>product</category>
      <category>web3</category>
    </item>
    <item>
      <title>Adding Crypto Swaps to Your App Without Taking Custody</title>
      <dc:creator>Swapcore.Exchange</dc:creator>
      <pubDate>Sat, 12 Sep 2026 21:36:07 +0000</pubDate>
      <link>https://dev.to/swapcoreexchange/adding-crypto-swaps-to-your-app-without-taking-custody-1358</link>
      <guid>https://dev.to/swapcoreexchange/adding-crypto-swaps-to-your-app-without-taking-custody-1358</guid>
      <description>&lt;p&gt;If your product touches crypto — a wallet, a portfolio tracker, a game with an in-app token — you will eventually get asked for swaps. Users hold asset A and want asset B, and every time you send them to an external site to do it, you lose them for the rest of the session.&lt;/p&gt;

&lt;p&gt;The obvious implementation is the one you should not build: take deposits, hold balances, execute conversions internally. That turns your app into a custodian, which brings licensing exposure, an attack surface holding other people's money, and an operational burden that has nothing to do with your product.&lt;/p&gt;

&lt;p&gt;The alternative is integrating a non-custodial instant exchange. Funds never touch your infrastructure. Here's how that model actually works and what you need to handle in your code.&lt;/p&gt;

&lt;p&gt;The execution model&lt;/p&gt;

&lt;p&gt;An instant exchange is not an order book. There's no matching, no bids and asks, no depth. The flow is:&lt;/p&gt;

&lt;p&gt;Request a quote for a pair and amount&lt;br&gt;
Create an order, submitting the user's destination address&lt;br&gt;
Receive a one-time deposit address for that specific order&lt;br&gt;
The user sends funds to that address from their own wallet&lt;br&gt;
On deposit confirmation, the swap executes and output goes directly to the destination address&lt;/p&gt;

&lt;p&gt;Your application is a coordinator. It never holds a key, never holds a balance, and never becomes a party to custody. The user's wallet sends, the exchange routes, the user's wallet receives.&lt;/p&gt;

&lt;p&gt;The design decisions that matter&lt;br&gt;
Rate type is a risk-allocation choice, not a UX preference&lt;/p&gt;

&lt;p&gt;Most exchange APIs expose fixed and floating rates. This is not a cosmetic toggle and you should not pick a default without understanding it.&lt;/p&gt;

&lt;p&gt;Floating calculates the rate at deposit confirmation. The market moves between order creation and confirmation, so the output amount is an estimate. Spread is thinner because the exchange carries no price risk.&lt;/p&gt;

&lt;p&gt;Fixed locks the output at order creation for a bounded window. Your user sees a guaranteed amount. The exchange absorbs the price movement and charges for it via a wider spread.&lt;/p&gt;

&lt;p&gt;If you're building a checkout flow where a precise amount must arrive, you need fixed — a floating rate that lands two percent short breaks your business logic downstream. If you're building a general-purpose swap UI, expose both and explain the difference in one line. Silently defaulting to floating and showing an estimate as though it's a promise is how you generate support tickets.&lt;/p&gt;

&lt;p&gt;Order state is asynchronous and long-lived&lt;/p&gt;

&lt;p&gt;A swap is not a request/response operation. It spans a user action in another application and one or more blockchain confirmations. Elapsed time is dominated by network confirmation, which varies by chain and congestion — a Bitcoin deposit and a Tron deposit are not comparable operations.&lt;/p&gt;

&lt;p&gt;Design for this:&lt;/p&gt;

&lt;p&gt;Persist the order ID on creation, before the user leaves to send funds. If you lose it, you cannot reconcile.&lt;br&gt;
Prefer webhooks over polling for status transitions, with polling as a fallback. Confirmation timing is unpredictable enough that a fixed polling interval is either wasteful or slow.&lt;br&gt;
Handle the terminal states explicitly: completed, expired (fixed-rate window elapsed before deposit), underpaid/overpaid, and held-for-review. That last one is real — deposits are screened against blockchain analytics and flagged ones pause pending manual review. If your UI only knows "pending" and "done", a held order looks like a hang.&lt;br&gt;
Treat expiry as a normal path. Users open a swap, get distracted, and come back after the rate window closed. Build the retry.&lt;br&gt;
Address validation belongs in your UI&lt;/p&gt;

&lt;p&gt;The most common failure in this entire flow is a user-supplied destination address that's wrong for the target network — a BEP20 address for an ERC20 payout, or a missing memo/tag on a chain that requires one.&lt;/p&gt;

&lt;p&gt;This is unrecoverable by design. No custodial party is holding the funds, so no custodial party can reverse it. Validate format and network client-side before you let the order be created, and surface the memo requirement as a blocking field rather than a hint, on the chains where it applies.&lt;/p&gt;

&lt;p&gt;Minimums are per-asset, and they move&lt;/p&gt;

&lt;p&gt;Minimum swap amounts are driven by network fees, so they shift with congestion. Don't hardcode them. Fetch the current minimum with the quote and validate against the live value, or you will ship a form that rejects valid amounts and accepts invalid ones.&lt;/p&gt;

&lt;p&gt;Two integration surfaces&lt;/p&gt;

&lt;p&gt;Most providers offer both:&lt;/p&gt;

&lt;p&gt;An embeddable widget. An iframe with your styling. Fast — a day of work — but you don't control the UX and you get limited state visibility.&lt;/p&gt;

&lt;p&gt;A REST API. You build the interface, you own the flow, you handle the state machine described above. More work, but it's the only option if swaps are part of a larger transaction in your product rather than a standalone feature.&lt;/p&gt;

&lt;p&gt;Start with the widget to validate that users actually want this. Move to the API once you know they do.&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
