<?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: Azalea</title>
    <description>The latest articles on DEV Community by Azalea (@azaleakuts).</description>
    <link>https://dev.to/azaleakuts</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%2F3979286%2Fa8f84aca-1b3a-4612-ae34-63b30587e774.png</url>
      <title>DEV Community: Azalea</title>
      <link>https://dev.to/azaleakuts</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/azaleakuts"/>
    <language>en</language>
    <item>
      <title>Your App Will Meet a Freeze. Design for It</title>
      <dc:creator>Azalea</dc:creator>
      <pubDate>Thu, 05 Nov 2026 15:40:20 +0000</pubDate>
      <link>https://dev.to/azaleakuts/your-app-will-meet-a-freeze-design-for-it-5gk6</link>
      <guid>https://dev.to/azaleakuts/your-app-will-meet-a-freeze-design-for-it-5gk6</guid>
      <description>&lt;p&gt;Last week Binance lost about $6.3 billion in roughly 20 minutes. Within hours, stablecoin issuers froze around $3 billion of it, Binance paused withdrawals for everyone, and analytics firms labeled the attacker's Bitcoin wallets in public.&lt;/p&gt;

&lt;p&gt;Most of us will never run an exchange that size. But almost every crypto app touches at least one of the mechanisms that showed up that week: a hot wallet, a stablecoin with a blacklist, a deposit from a flagged address, a "withdrawals paused" banner. Each of them is a state your product needs to handle on purpose.&lt;/p&gt;

&lt;p&gt;Here's what I'd take from it as a builder.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Watch your hot wallets for EIP-7702 delegations
&lt;/h2&gt;

&lt;p&gt;Binance co-CEO Richard Teng said the attacker used an EIP-7702 delegation to gain execution authority over a hot wallet and bypass normal approvals, &lt;a href="https://cointelegraph.com/news/live-updates-binance-63b-hack-linked-to-north-koreas-tradertraitor" rel="noopener noreferrer"&gt;Cointelegraph reported&lt;/a&gt;. Binance says no private keys leaked.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://eips.ethereum.org/EIPS/eip-7702" rel="noopener noreferrer"&gt;EIP-7702&lt;/a&gt; lets an externally owned account point to contract code. Once that happens, the address behaves partly like a smart contract. The spec marks a delegated account by setting its code to a fixed prefix, &lt;code&gt;0xef0100&lt;/code&gt;, followed by the 20-byte address of the delegate.&lt;/p&gt;

&lt;p&gt;That makes it cheap to monitor. If an address you control suddenly has code, something changed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createPublicClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;viem&lt;/span&gt;&lt;span class="dl"&gt;"&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="nx"&gt;mainnet&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;viem/chains&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;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createPublicClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;mainnet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;http&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;checkDelegation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`0x&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;address&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0x&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;delegated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0xef0100&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;delegated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;delegate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`0x&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;code&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;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;48&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="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;delegated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;delegate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unknown contract code&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run that on every hot wallet on a schedule, and alert on any change. It won't stop an attack, but it can shorten the gap between "something happened" and "someone noticed".&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Check the issuer's blacklist before you pay out
&lt;/h2&gt;

&lt;p&gt;Tether froze more than $1.5 billion in USDT during the hack and Circle more than $800 million in USDC. Both tokens have blacklist logic in their contracts. If your app sends stablecoins, an address can become unpayable at any moment. A blacklisted USDC address can't send or receive, so a transfer to it reverts.&lt;/p&gt;

&lt;p&gt;Check before you build the transaction, not after it fails. Both contracts expose a read method, but the names differ by one letter: USDC uses &lt;code&gt;isBlacklisted&lt;/code&gt;, USDT uses &lt;code&gt;isBlackListed&lt;/code&gt;. Easy to get wrong, so keep the name per token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BLACKLIST_FN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;isBlacklisted&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;isBlackListed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;USDC&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;isBlacklisted&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;USDT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;isBlackListed&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;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;canReceive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;USDC&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;USDT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`0x&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`0x&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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="nx"&gt;fn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;BLACKLIST_FN&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;abi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;function&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;stateMutability&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;view&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;account&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;address&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="na"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bool&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;as const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blocked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readContract&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;abi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;functionName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;blocked&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// can't read it: fall back to simulating the transfer&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;null&lt;/code&gt; branch still matters. Other stablecoins use other names or none at all, so when the check isn't available, simulate the transfer with &lt;code&gt;eth_call&lt;/code&gt; and treat a revert as "not payable right now".&lt;/p&gt;

&lt;p&gt;And tell the user plainly. "This address can't receive USDT" is better than a generic failure, and much better than a stuck payout.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Treat tainted deposits as a normal state
&lt;/h2&gt;

&lt;p&gt;Bitcoin can't be frozen, so the 18,270 BTC from the hack can move whenever the attacker wants. What stops them is everyone else. Exchanges and swap services screen deposits and can refuse coins linked to flagged addresses.&lt;/p&gt;

&lt;p&gt;If you accept deposits, some will eventually come from addresses that become flagged after the fact. Model that up front:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;DepositStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;confirming&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;confirmations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;credited&lt;/span&gt;&lt;span class="dl"&gt;"&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="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;under_review&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;risk_screening&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;since&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;returned&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;txHash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;under_review&lt;/code&gt; needs its own screen with a clear explanation and an expected timeline. Users who see "pending" for two days with no reason assume the worst, and they're usually right to.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Withdrawals reopen per asset, not all at once
&lt;/h2&gt;

&lt;p&gt;Binance paused withdrawals on October 29 and reopened them on November 1 in phases, starting with about 15 major assets and networks, &lt;a href="https://www.coindesk.com/markets/2026/11/01/binance-opens-crypto-withdrawals-in-phases-starting-with-bitcoin-following-usd6-3-billion-hack" rel="noopener noreferrer"&gt;CoinDesk reported&lt;/a&gt;. Hundreds of smaller tokens stayed locked.&lt;/p&gt;

&lt;p&gt;That's the realistic recovery pattern after any incident: some chains come back first, others later. A single global "withdrawals enabled" flag can't express that. Store status per asset and per network, and show it next to each balance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;WithdrawalStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;
  &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// e.g. "USDT:tron"&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;updatedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the first nine and a half hours after the reopening, users pulled about $4.65 billion out of Binance, according to &lt;a href="https://news.bitcoin.com/exchanges/binance-sheds-15-7b-proof-of-reserves-tuesday/" rel="noopener noreferrer"&gt;Bitcoin.com News&lt;/a&gt;. Plan your queue and rate limits for the day the gate opens, not for a normal Tuesday.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Know where privacy coins still fit
&lt;/h2&gt;

&lt;p&gt;During the hack week, Monero rose about 5.7%, and a reply that kept showing up under news posts was "XMR has no freeze switch". That's true, and it's also why regulated services keep dropping it. Kraken ended Monero trading for EEA clients in 2024. From July 10, 2027, &lt;a href="https://eur-lex.europa.eu/eli/reg/2024/1624/oj" rel="noopener noreferrer"&gt;Article 79 of the EU's Anti-Money Laundering Regulation&lt;/a&gt; bars regulated crypto providers from keeping accounts that allow anonymous transactions, including through coins like Monero.&lt;/p&gt;

&lt;p&gt;If your product touches XMR, check where your users are before you ship a feature around it. This overview of &lt;a href="https://blockspot.io/is-it-safe-legal-to-exchange-monero-for-ethereum/" rel="noopener noreferrer"&gt;whether it's safe and legal to exchange Monero for Ethereum&lt;/a&gt; has a country-by-country summary that's a reasonable starting point for that conversation with your legal team. It also flags the UX trap: after a swap to ETH, the user is on a public chain, and any address linked to their name is visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  A short checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Monitor hot wallets for unexpected code, including EIP-7702 delegations.&lt;/li&gt;
&lt;li&gt;Check stablecoin blacklists before building a payout, and simulate when you can't.&lt;/li&gt;
&lt;li&gt;Give flagged deposits their own state and their own screen.&lt;/li&gt;
&lt;li&gt;Keep withdrawal status per asset and per network.&lt;/li&gt;
&lt;li&gt;Size your withdrawal queue for the reopening, not the average day.&lt;/li&gt;
&lt;li&gt;Map which assets you can legally support in each market before you add them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Freezes, pauses and flags aren't edge cases anymore. Last week all of them happened at once, to the biggest exchange in the world. Your app should already know what to show when one of them happens to a user.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Sources: &lt;a href="https://cointelegraph.com/news/live-updates-binance-63b-hack-linked-to-north-koreas-tradertraitor" rel="noopener noreferrer"&gt;Cointelegraph live coverage&lt;/a&gt;, &lt;a href="https://www.coindesk.com/markets/2026/11/01/binance-opens-crypto-withdrawals-in-phases-starting-with-bitcoin-following-usd6-3-billion-hack" rel="noopener noreferrer"&gt;CoinDesk&lt;/a&gt;, &lt;a href="https://news.bitcoin.com/exchanges/binance-sheds-15-7b-proof-of-reserves-tuesday/" rel="noopener noreferrer"&gt;Bitcoin.com News&lt;/a&gt;, &lt;a href="https://eips.ethereum.org/EIPS/eip-7702" rel="noopener noreferrer"&gt;EIP-7702&lt;/a&gt;, &lt;a href="https://eur-lex.europa.eu/eli/reg/2024/1624/oj" rel="noopener noreferrer"&gt;EUR-Lex: Regulation (EU) 2024/1624&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>fintech</category>
      <category>security</category>
      <category>blockchain</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The Chain You Pay Out On Changed This Month</title>
      <dc:creator>Azalea</dc:creator>
      <pubDate>Fri, 30 Oct 2026 15:01:09 +0000</pubDate>
      <link>https://dev.to/azaleakuts/the-chain-you-pay-out-on-changed-this-month-18oe</link>
      <guid>https://dev.to/azaleakuts/the-chain-you-pay-out-on-changed-this-month-18oe</guid>
      <description>&lt;p&gt;If your app sends stablecoins, you probably made a network decision once and stopped thinking about it. Most teams did. TRON for USDT, maybe Solana or Base for USDC, and a dropdown so users can pick.&lt;/p&gt;

&lt;p&gt;October 2026 is a good month to reopen that decision. Four things changed at once, and each one affects a different part of a payout flow: finality, gas assumptions, which tokens you can offer in Europe, and how those tokens are supervised.&lt;/p&gt;

&lt;p&gt;I wrote before about &lt;a href="https://dev.to/azaleakuts/stablecoin-payments-are-becoming-a-developer-problem-4bah"&gt;why stablecoin payments are becoming a developer problem&lt;/a&gt;. This is the practical follow-up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the boring numbers
&lt;/h2&gt;

&lt;p&gt;A recent comparison of the &lt;a href="https://www.digitalconnectmag.com/best-blockchains-for-crypto-payments/" rel="noopener noreferrer"&gt;best blockchains for everyday crypto payments&lt;/a&gt; is a useful baseline, because it looks at the costs a regular user pays, not the costs an exchange with staked TRX pays. A few of its findings matter for anyone building payouts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;TRON holds the most USDT, but a single send costs roughly $2 to $4.50 for a wallet with no staked or rented Energy.&lt;/li&gt;
&lt;li&gt;An ERC-20 USDT transfer on Ethereum mainnet was about $0.22 in early October. Cheaper than TRON for that same wallet.&lt;/li&gt;
&lt;li&gt;Solana charges a fraction of a cent, but the first transfer to a new address needs a token account, about 0.002 SOL in rent.&lt;/li&gt;
&lt;li&gt;TON got sub-second finality in April and renamed its token to Gram in June, which scammers are already using.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first point surprises people. If your users self-custody and don't hold TRX, "send it on TRON, it's cheap" is no longer good default advice. I went into the TRON side of this in &lt;a href="https://dev.to/azaleakuts/trons-usdt-growth-is-changing-what-developers-build-around-4afl"&gt;an earlier post&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Solana's finality just changed, and your timeouts may be wrong
&lt;/h2&gt;

&lt;p&gt;On October 21, Solana activated &lt;a href="https://solana.com/upgrades/alpenglow" rel="noopener noreferrer"&gt;Alpenglow&lt;/a&gt;, replacing the consensus system it had run since 2020. The target is finality in about 150 milliseconds instead of roughly 12.8 seconds. On launch day the Foundation's dashboard showed about 6.6 seconds, so treat 150 ms as a goal for now.&lt;/p&gt;

&lt;p&gt;Two things to check in your code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you poll for &lt;code&gt;confirmed&lt;/code&gt; and wait a fixed number of seconds before crediting, revisit that. The Foundation says &lt;code&gt;confirmed&lt;/code&gt; and &lt;code&gt;finalized&lt;/code&gt; become the same state under Alpenglow, and &lt;code&gt;confirmed&lt;/code&gt; will be removed in a later release. Move new code to &lt;code&gt;finalized&lt;/code&gt;, but only now that Alpenglow is live. Before activation, &lt;code&gt;finalized&lt;/code&gt; meant a 12.8-second wait.&lt;/li&gt;
&lt;li&gt;If your dashboards count transactions, expect a sharp drop. Validator votes no longer appear as transactions. That's an accounting change, not a fall in usage.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. Ethereum's gas assumptions are on hold
&lt;/h2&gt;

&lt;p&gt;Glamsterdam went live on the Sepolia testnet on October 6. Its Hoodi testnet activation, planned for October 26, was &lt;a href="https://crypto.news/ethereum-glamsterdam-hoodi-upgrade-postponed/" rel="noopener noreferrer"&gt;postponed&lt;/a&gt; while developers check how EIP-8037 interacts with existing contracts. EIP-8037 raises and separately meters the gas cost of creating new state: new accounts, new storage slots, new contract code.&lt;/p&gt;

&lt;p&gt;For payouts, that means the cost of sending to a brand-new address could change once the upgrade ships. If you estimate gas with hardcoded values, stop. Use &lt;code&gt;eth_estimateGas&lt;/code&gt; per transaction and keep a margin. The fee itself is still gas used times base fee plus priority fee, and the base fee can move up to 12.5% per block (&lt;a href="https://ethereum.org/developers/docs/gas/" rel="noopener noreferrer"&gt;ethereum.org&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Your EU users may lose USDT
&lt;/h2&gt;

&lt;p&gt;Tether never applied for authorization under MiCA, so licensed platforms in the EU are phasing out USDT. ESMA told them to finish by January 8, 2027. Coinbase moved faster: today, October 30, is its deadline for customers in the European Economic Area to withdraw USDT, DAI and several others before leftovers are converted into USDC.&lt;/p&gt;

&lt;p&gt;If you serve EU users through a licensed partner, your available token list may shrink without a code change on your side. Model support as a state, not a boolean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;TokenSupport&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;"&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="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;deprecating&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;EEA&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;until&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;convertsTo&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unavailable&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;EEA&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then let the UI warn users before the partner emails them.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. USDC gets a supervisor with a date
&lt;/h2&gt;

&lt;p&gt;On October 14, the European Banking Authority &lt;a href="https://eba.europa.eu/publications-and-media/press-releases/eba-classifies-usdc-first-significant-e-money-token-european-union" rel="noopener noreferrer"&gt;classified USDC&lt;/a&gt; as the EU's first "significant" e-money token. From November 12, the EBA directly supervises its reserves and liquidity. Nothing changes in the contract or the peg, but it's another reason USDC is becoming the default for EU-facing flows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it together
&lt;/h2&gt;

&lt;p&gt;Here's roughly how I'd describe networks in a payout service now. The point is to keep the facts that change in data, not in branching logic.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;PayoutNetwork&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tron&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ethereum&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;arbitrum&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;solana&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stellar&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;TokenSupport&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;gasToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;needsGasForRecipient&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// e.g. TRX Energy, SOL token account rent&lt;/span&gt;
  &lt;span class="nl"&gt;finality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;probabilistic&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;economic&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fast&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;estimateFee&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;bigint&lt;/span&gt;&lt;span class="o"&gt;&amp;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;And a few rules I'd put in front of any default:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Recommend the network the recipient already uses, not the cheapest one in theory.&lt;/li&gt;
&lt;li&gt;Show the full cost, including first-transfer extras like Solana token accounts.&lt;/li&gt;
&lt;li&gt;Never hardcode gas or finality timings. Both changed this month.&lt;/li&gt;
&lt;li&gt;Keep per-region token support in config, so a regulator's deadline is a data update, not a deploy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cheapest chain on paper changes every few months. The habit of checking is what actually saves users money.&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>fintech</category>
      <category>webdev</category>
      <category>cryptocurrency</category>
    </item>
    <item>
      <title>Your "Licensed" Badge Is Doing More Work Than It Should</title>
      <dc:creator>Azalea</dc:creator>
      <pubDate>Wed, 28 Oct 2026 16:03:27 +0000</pubDate>
      <link>https://dev.to/azaleakuts/your-licensed-badge-is-doing-more-work-than-it-should-271o</link>
      <guid>https://dev.to/azaleakuts/your-licensed-badge-is-doing-more-work-than-it-should-271o</guid>
      <description>&lt;p&gt;A lot of crypto apps in Europe now show some version of the same line near the buy button: "Licensed under MiCA." It's true, it's reassuring, and a surprising number of users read it as "this is safe to buy."&lt;/p&gt;

&lt;p&gt;That's not what it means. And if you build these products, the gap between those two readings is partly a design problem.&lt;/p&gt;

&lt;p&gt;I wrote earlier about &lt;a href="https://dev.to/azaleakuts/mica-after-july-2026-what-actually-changes-for-crypto-users-and-developers-3ljj"&gt;what MiCA changed after July 2026&lt;/a&gt;. This post is narrower. It's about how a compliance fact turns into a trust signal on screen, and how to keep it honest.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the badge actually certifies
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://eur-lex.europa.eu/eli/reg/2023/1114/oj" rel="noopener noreferrer"&gt;MiCA&lt;/a&gt; regulates the operator, not the asset. A crypto-asset service provider gets authorized after meeting rules on capital, governance, custody of client assets, complaints handling and disclosure. Since July 1, 2026, no transition period is left anywhere in the EU, so a company without that authorization shouldn't be serving EU customers at all.&lt;/p&gt;

&lt;p&gt;So the badge says: this company is supervised and accountable.&lt;/p&gt;

&lt;p&gt;It doesn't say:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the asset is protected (there's no deposit guarantee for crypto);&lt;/li&gt;
&lt;li&gt;the user was checked for whether they understand the product;&lt;/li&gt;
&lt;li&gt;the user controls the coins.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Users don't read regulation text. They read your UI. If the license badge sits right next to the "Buy BTC" button with nothing else around it, you've made a claim you didn't intend.&lt;/p&gt;

&lt;h2&gt;
  
  
  Users are being told to check things your product should make easy
&lt;/h2&gt;

&lt;p&gt;Consumer guides have started telling people exactly what to verify before using a platform. A Spanish explainer on &lt;a href="https://elperiodicodeyecla.com/criptomonedas-y-regulacion-que-significa-mica-para-quien-usa-cripto-en-2026/" rel="noopener noreferrer"&gt;what MiCA means for people who use crypto in 2026&lt;/a&gt; is a good example. Its advice comes down to a short list: confirm the platform is authorized, understand which stablecoin you hold, check the destination address and network before sending, keep records for taxes, and remember that the regulation doesn't make crypto a safe investment.&lt;/p&gt;

&lt;p&gt;Read that list as a product spec. Every item is something your app can support or quietly make harder.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Make the license verifiable, not decorative
&lt;/h2&gt;

&lt;p&gt;A badge that just says "Licensed" is an image. A useful one names the regulator and the license number, and links to the public record.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;LicenseInfo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;regime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MiCA&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;authority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// e.g. "AMF (France)"&lt;/span&gt;
  &lt;span class="nl"&gt;licenseId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;registerUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;// national register or ESMA's list&lt;/span&gt;
  &lt;span class="nl"&gt;homeState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// where the passport comes from&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;homeState&lt;/code&gt; field matters more than it looks. A MiCA license works across the EU, so a user in Spain may be using an app supervised in Malta or France. Say so. ESMA keeps a &lt;a href="https://www.esma.europa.eu/esmas-activities/digital-finance-and-innovation/markets-crypto-assets-regulation-mica" rel="noopener noreferrer"&gt;central MiCA register&lt;/a&gt; you can point to.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Keep the badge and the risk warning apart
&lt;/h2&gt;

&lt;p&gt;Put the license and the price-risk warning in separate places, with separate visual weight. When they share a card, users tend to read the badge as cancelling the warning.&lt;/p&gt;

&lt;p&gt;A plain line works better than legal copy: "Our license covers how we hold and handle your assets. It doesn't protect you from price drops."&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Say who holds the keys, in the product, not the terms
&lt;/h2&gt;

&lt;p&gt;"Custodied by us" and "in your wallet" are different products. Some licensed services in Europe don't give users keys and only allow withdrawals to addresses at approved providers. That's a legitimate setup, but users should learn it before buying, not when they try to leave.&lt;/p&gt;

&lt;p&gt;Show withdrawal rules on the asset page:&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;"custody"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"platform"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"withdrawals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"selfCustody"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"allowedDestinations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"approved_providers_only"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"networks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"bitcoin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ethereum"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I covered the pre-send side of this in &lt;a href="https://dev.to/azaleakuts/what-a-crypto-app-should-do-before-you-press-send-3cp5"&gt;What a Crypto App Should Do Before You Press "Send"&lt;/a&gt;. The same idea applies one step earlier, at purchase.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Treat stablecoin support as something that can change
&lt;/h2&gt;

&lt;p&gt;Tether never applied for MiCA authorization, so licensed platforms have been phasing out USDT for EU users. ESMA told them to finish by January 8, 2027, and some exchanges moved faster, converting leftover balances into USDC after a deadline.&lt;/p&gt;

&lt;p&gt;Most users found out from an email. A better pattern is a status field per asset that the UI can surface early:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;AssetSupport&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;supported&lt;/span&gt;&lt;span class="dl"&gt;"&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="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;deprecating&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;until&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;convertsTo&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;withdraw_only&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;until&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Meanwhile USDC moves the other way. The European Banking Authority &lt;a href="https://eba.europa.eu/publications-and-media/press-releases/eba-classifies-usdc-first-significant-e-money-token-european-union" rel="noopener noreferrer"&gt;classified it&lt;/a&gt; as the EU's first "significant" e-money token, with direct EBA supervision of its reserves from November 12, 2026. That's worth reflecting in your asset info too.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Build tax records in, because the tax office already has them
&lt;/h2&gt;

&lt;p&gt;Under &lt;a href="https://eur-lex.europa.eu/eli/dir/2023/2226/oj" rel="noopener noreferrer"&gt;DAC8&lt;/a&gt;, platforms collect users' tax residence and report their transactions from January 1, 2026, with the first reports sent in 2027. Your users' tax authorities will see their trades. Many users still won't have their own records.&lt;/p&gt;

&lt;p&gt;A one-click export (date, pair, amount, price, fee, network, tx hash) costs little to build and saves people a lot of pain in spring. It's also one of the items on every consumer checklist, so it reads as care rather than a feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;A license tells users the company is accountable. Your interface decides whether they think it means more than that. Name the regulator, separate the badge from the risk, show custody and withdrawal rules up front, warn before an asset loses support, and give people their records.&lt;/p&gt;

&lt;p&gt;None of this is required by MiCA in exactly this form. It's just what users are now being told to look for.&lt;/p&gt;

</description>
      <category>fintech</category>
      <category>cryptocurrency</category>
      <category>webdev</category>
      <category>ux</category>
    </item>
    <item>
      <title>What Wrapped Bitcoin Actually Asks You to Trust</title>
      <dc:creator>Azalea</dc:creator>
      <pubDate>Thu, 22 Oct 2026 18:28:36 +0000</pubDate>
      <link>https://dev.to/azaleakuts/what-wrapped-bitcoin-actually-asks-you-to-trust-paj</link>
      <guid>https://dev.to/azaleakuts/what-wrapped-bitcoin-actually-asks-you-to-trust-paj</guid>
      <description>&lt;p&gt;Bitcoin can't run on Ethereum. So when a lending market or a DEX pool on Ethereum says it accepts "BTC", it means something else: a token that promises a real bitcoin is sitting somewhere, waiting to be redeemed.&lt;/p&gt;

&lt;p&gt;That promise has a structure. Someone holds the BTC, someone decides who can mint and burn, and someone charges for each step. In my last post on proof of reserves I looked at what an exchange's reserve report can and can't tell you. Wrapped bitcoin raises the same question, just with a token contract in the middle.&lt;/p&gt;

&lt;h2&gt;
  
  
  The token is a claim on a vault
&lt;/h2&gt;

&lt;p&gt;WBTC, the oldest and best-known wrapper, works through two kinds of participants: merchants and a custodian.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Mint:  User → Merchant → Custodian receives BTC → WBTC minted on Ethereum
Burn:  User → Merchant → WBTC burned → Custodian releases BTC
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The user never talks to the custodian directly. A merchant handles the request, usually after KYC, and the custodian mints or releases. Official minting waits for 6 Bitcoin confirmations, and burning waits for 25 Ethereum blocks.&lt;/p&gt;

&lt;p&gt;Once WBTC exists, moving it between users costs only network fees. The trust question sits entirely at the edges: when the token is created and when it's turned back into bitcoin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three wrappers, three different promises
&lt;/h2&gt;

&lt;p&gt;WBTC isn't the only way to hold BTC on Ethereum anymore. In a wallet, these tokens look almost identical. Underneath, they ask you to trust different parties:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;WBTC&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;backing&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;BTC held by a custodian&lt;/span&gt;
  &lt;span class="na"&gt;mint_and_redeem&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;through approved merchants, usually with KYC&lt;/span&gt;
  &lt;span class="na"&gt;fees&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;custodian fee + merchant fee, rates not published&lt;/span&gt;

&lt;span class="na"&gt;tBTC&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;backing&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;BTC deposits secured by Threshold network operators&lt;/span&gt;
  &lt;span class="na"&gt;mint_and_redeem&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0.2% each way, minimum deposit 0.01 BTC&lt;/span&gt;

&lt;span class="na"&gt;cbBTC&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;backing&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;BTC held by Coinbase&lt;/span&gt;
  &lt;span class="na"&gt;mint_and_redeem&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;through a Coinbase account, no wrap or unwrap fee&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They aren't interchangeable. A pool that accepts only WBTC won't take tBTC, so a cheaper mint on one wrapper can mean an extra swap, and another fee, later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Custody can change while the token stays the same
&lt;/h2&gt;

&lt;p&gt;This is the part that's easy to miss if you only look at the contract address.&lt;/p&gt;

&lt;p&gt;In 2024, BitGo announced it would move WBTC custody into a new structure with a partner, BiT Global. The token kept its name, its contract and its 1:1 claim. What changed was who stood behind it. Coinbase later delisted WBTC, saying it no longer met its listing standards, and promoted its own cbBTC instead.&lt;/p&gt;

&lt;p&gt;Nothing broke on-chain. Holders could still transfer and use their tokens. But the trust model they had originally accepted was different from the one they ended up with, and the change happened off-chain, in agreements most users never read.&lt;/p&gt;

&lt;p&gt;For anyone building on top of wrapped BTC, that's the real risk surface. Your code checks a contract. The guarantee lives somewhere else.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fee nobody publishes
&lt;/h2&gt;

&lt;p&gt;Cost is where wrapping gets murky. A recent comparison of &lt;a href="https://icoholder.com/blog/cheapest-way-to-wrap-btc-into-wbtc/" rel="noopener noreferrer"&gt;the cheapest ways to convert BTC to WBTC&lt;/a&gt; lines up four routes: official merchants, centralized exchanges, instant swap services and the tBTC bridge. One finding stood out to me. The custodian's fee for official WBTC minting isn't public. It's set in an off-chain agreement between merchant and custodian, and the third-party estimates the authors found disagreed with each other by a wide margin.&lt;/p&gt;

&lt;p&gt;So the most "official" route is also the one you can't price in advance.&lt;/p&gt;

&lt;p&gt;The routes with published numbers behave very differently depending on size. A percentage fee scales with the amount. The Bitcoin network fee doesn't. With a simple SegWit payment of about 140 vbytes:&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;function&lt;/span&gt; &lt;span class="nf"&gt;wrapCostShare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;amountSats&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;percentFee&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;satPerVb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;vbytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;140&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;serviceFee&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;amountSats&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;percentFee&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;networkFee&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;vbytes&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;satPerVb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;serviceFee&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;networkFee&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;amountSats&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;wrapCostShare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_000_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.002&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// 0.01 BTC → 0.0048   (~0.48%)&lt;/span&gt;
&lt;span class="nf"&gt;wrapCostShare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="nx"&gt;_000_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.002&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 1 BTC    → 0.002028 (~0.20%)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At 0.01 BTC on a busy day, the Bitcoin fee (about 2,800 sats) is bigger than a 0.2% bridge fee (2,000 sats). At 1 BTC it barely registers. Ethereum gas, meanwhile, was the smallest line of all in mid-October 2026: about $0.04 for a swap, according to Etherscan's &lt;a href="https://etherscan.io/gastracker" rel="noopener noreferrer"&gt;gas tracker&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The cheapest route for a $200 conversion and the cheapest route for a $200,000 one are often different routes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means if you're building
&lt;/h2&gt;

&lt;p&gt;If your app accepts BTC on Ethereum, the wrapper choice is a risk decision, and it deserves the same treatment as any other dependency:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Treat each wrapped asset as a separate asset with its own backing, custodian and redemption path. A shared ticker prefix doesn't make them equivalent.&lt;/li&gt;
&lt;li&gt;Keep an explicit allowlist and review it. Custody arrangements can change without a contract upgrade.&lt;/li&gt;
&lt;li&gt;Show users which wrapper they hold or receive. "BTC" in the UI hides the one detail that matters most.&lt;/li&gt;
&lt;li&gt;Don't promise instant redemption. Burning WBTC goes through a merchant, and that's an off-chain process with its own timing.&lt;/li&gt;
&lt;li&gt;When you quote a conversion, quote the amount that arrives after every fee. A good-looking rate with a separate fee can lose to a plain one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Wrapped bitcoin made BTC usable across Ethereum, and that's been genuinely useful for years. The token, though, is only as reliable as the arrangement behind it. Read that arrangement before you build on it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Drafted with AI assistance. Facts were checked against the linked sources, and the text was edited by me.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>ethereum</category>
      <category>bitcoin</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Private In, Public Out: What an XMR to ETH Swap Looks Like From the Inside</title>
      <dc:creator>Azalea</dc:creator>
      <pubDate>Tue, 20 Oct 2026 22:46:07 +0000</pubDate>
      <link>https://dev.to/azaleakuts/private-in-public-out-what-an-xmr-to-eth-swap-looks-like-from-the-inside-2fei</link>
      <guid>https://dev.to/azaleakuts/private-in-public-out-what-an-xmr-to-eth-swap-looks-like-from-the-inside-2fei</guid>
      <description>&lt;p&gt;Last week a privacy coin had a bad few days. On October 14, Pirate Chain found an exploit that let an attacker mint about 39.8 million ARRR out of thin air, roughly a fifth of the supply. The new coins went straight into the shielded pool, where the chain's mandatory privacy makes them impossible to fully trace. The team &lt;a href="https://www.cryptotimes.io/2026/10/16/pirate-chain-exploit-mints-39-8m-arrr-rollback-plan-dropped/" rel="noopener noreferrer"&gt;dropped its rollback plan&lt;/a&gt; and asked exchanges and pools to pause.&lt;/p&gt;

&lt;p&gt;That's the dramatic version of a trade-off every privacy chain lives with. Privacy hides the user, and it can also hide the problem.&lt;/p&gt;

&lt;p&gt;Most people will never touch an exploit like that. What they do touch is a much more boring moment: moving value out of a private chain and into a public one. Swapping Monero for Ether is the most common example. If you build wallets, payment flows or anything with a "Send" button, that boundary has its own timing, its own costs and a couple of states that most interfaces never show.&lt;/p&gt;

&lt;p&gt;Here's what happens inside it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two ledgers with opposite defaults
&lt;/h2&gt;

&lt;p&gt;Monero hides the sender, the receiver and the amount by default. Ethereum shows all three to anyone with a block explorer.&lt;/p&gt;

&lt;p&gt;So an XMR to ETH swap is a one-way door. Whatever happens on the Monero side stays fairly private. Whatever happens after the ETH lands is public forever: the receiving address, every transfer it makes afterward, and every contract it touches.&lt;/p&gt;

&lt;p&gt;I wrote earlier about &lt;a href="https://dev.to/azaleakuts/what-a-public-crypto-wallet-can-reveal-4kjm"&gt;what a public crypto wallet can reveal&lt;/a&gt;. The short version applies here too. If the ETH address is already linked to your name through an exchange account or an ENS name, the swapped ETH is linked to you as well. Privacy on the way in doesn't carry over.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the time goes
&lt;/h2&gt;

&lt;p&gt;People expect the swap itself to be the slow part. It isn't. Most of the waiting happens on the Monero side, before any exchange runs.&lt;/p&gt;

&lt;p&gt;Three things set the clock:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Your own balance. Monero locks newly received coins for 10 blocks before they can be spent. If you got the XMR twenty minutes ago, you may not be able to send it yet.&lt;/li&gt;
&lt;li&gt;Block pace. Monero produces a block &lt;a href="https://www.getmonero.org/resources/moneropedia/block.html" rel="noopener noreferrer"&gt;about every 2 minutes&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;The service's threshold. Each swap service decides how many confirmations it waits for before accepting a deposit. Five confirmations is about ten minutes. Twenty is forty minutes or more. Large or unusual orders often get a higher threshold.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The Ethereum side rarely slows anything down. Ethereum runs on &lt;a href="https://ethereum.org/developers/docs/consensus-mechanisms/pos" rel="noopener noreferrer"&gt;12-second slots&lt;/a&gt;, so once the payout is sent, it shows up quickly.&lt;/p&gt;

&lt;p&gt;For builders, this has a simple consequence: a spinner is the wrong UI here. Users wait more calmly when they see "3 of 5 confirmations" than when they stare at a loading animation for twelve minutes with no idea whether anything is happening.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the money goes
&lt;/h2&gt;

&lt;p&gt;There's no single fee in an XMR to ETH swap. There are four:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the Monero network fee on your deposit, which you pay from your own wallet;&lt;/li&gt;
&lt;li&gt;the provider's trading costs with its liquidity partners;&lt;/li&gt;
&lt;li&gt;the spread, the gap between the market price and the rate you're offered;&lt;/li&gt;
&lt;li&gt;the Ethereum network fee on the payout.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On most instant swap services, only the first one is visible to you as a separate charge. The rest is folded into the quote. This &lt;a href="https://coinmarketcal.com/news/xmr-to-eth-exchange-fees-compared-what-to-expect-in-2026" rel="noopener noreferrer"&gt;breakdown of XMR to ETH exchange fees&lt;/a&gt; puts it well: the number worth comparing is the ETH you actually receive, not the headline rate.&lt;/p&gt;

&lt;p&gt;A few details from the protocol docs are worth knowing.&lt;/p&gt;

&lt;p&gt;Monero wallets offer &lt;a href="https://www.getmonero.org/resources/user-guides/make-payment.html" rel="noopener noreferrer"&gt;four fee priority levels&lt;/a&gt;. The fee depends on congestion and transaction size, not on how much XMR you send.&lt;/p&gt;

&lt;p&gt;On Ethereum, the fee is gas used times the base fee plus a priority fee. A plain ETH transfer uses 21,000 gas, and the base fee can move by up to 12.5% per block as demand changes (&lt;a href="https://ethereum.org/developers/docs/gas/" rel="noopener noreferrer"&gt;ethereum.org&lt;/a&gt;). That's why the same swap can cost more to pay out on a busy afternoon.&lt;/p&gt;

&lt;p&gt;Then there's the minimum amount. Network costs are roughly fixed per transaction, so on a tiny swap they eat a large share. Near the minimum, the ETH you receive can look noticeably worse than the market rate.&lt;/p&gt;

&lt;p&gt;The choice between fixed and floating rates is a trade-off too. A fixed rate locks the amount for a short window, and the provider usually builds a small buffer into it for price swings. A floating rate follows the market until execution and can land a bit above or below the estimate. On a volatile day, the certainty may be worth the buffer. On a calm day, often it isn't.&lt;/p&gt;

&lt;p&gt;For builders: show the final received amount as the main number. Show the rate second. If your quote is an estimate, say so in plain words, right next to the figure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The state nobody designs for
&lt;/h2&gt;

&lt;p&gt;Every swap flow has the happy path: waiting for deposit, confirming, exchanging, sending, done. Most also have a state that rarely shows up in mockups: on hold for a compliance review.&lt;/p&gt;

&lt;p&gt;"No account" and "no checks" are not the same thing. Account-free services still run risk-based reviews. Reports of stolen funds, unusual transactions or local law can trigger identity verification, and in some countries verification is required before any order at all.&lt;/p&gt;

&lt;p&gt;The wider rules are getting tighter, and they differ a lot by country. Dubai banned privacy coin activity in February 2023. Kraken &lt;a href="https://cointelegraph.com/news/kraken-ends-monero-support-european-economic-area" rel="noopener noreferrer"&gt;ended Monero trading and deposits&lt;/a&gt; for clients in the European Economic Area in October 2024. And from July 10, 2027, &lt;a href="https://eur-lex.europa.eu/eli/reg/2024/1624/oj" rel="noopener noreferrer"&gt;Article 79 of the EU's Anti-Money Laundering Regulation&lt;/a&gt; bars regulated crypto service providers from keeping accounts that allow anonymised transactions, including through anonymity-enhancing coins.&lt;/p&gt;

&lt;p&gt;Read that last one carefully. It applies to providers. It doesn't stop individuals from holding XMR in their own wallets. If you want the country-by-country picture, this overview of &lt;a href="https://blockspot.io/is-it-safe-legal-to-exchange-monero-for-ethereum/" rel="noopener noreferrer"&gt;whether it's safe and legal to exchange Monero for Ethereum&lt;/a&gt; collects the main rules in one table.&lt;/p&gt;

&lt;p&gt;For builders, the lesson is to treat "verification requested" as a first-class state. Say what triggered it, what the user needs to do, and how long the request stays open. A held order with a clear explanation is annoying. A held order with no explanation feels like a scam, and users will treat it like one.&lt;/p&gt;

&lt;h2&gt;
  
  
  A short checklist if you're building this flow
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Show confirmation progress as a count, not a spinner.&lt;/li&gt;
&lt;li&gt;Lead with the amount the user will receive. Put the rate second.&lt;/li&gt;
&lt;li&gt;Label estimates as estimates, and say what's already included.&lt;/li&gt;
&lt;li&gt;Warn when the amount is close to the minimum, where fees weigh most.&lt;/li&gt;
&lt;li&gt;Make the payout network impossible to miss. Mainnet, Base and Arbitrum addresses look identical.&lt;/li&gt;
&lt;li&gt;Design the compliance-hold screen before you need it.&lt;/li&gt;
&lt;li&gt;Give the user an order ID they can save, and a status page that works with nothing else.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And if you're on the user side: send already-unlocked XMR, compare the full ETH address character by character, and assume the receiving address will be public for good. Because it will be.&lt;/p&gt;

</description>
      <category>cryptocurrency</category>
      <category>privacy</category>
      <category>ethereum</category>
      <category>fintech</category>
    </item>
    <item>
      <title>Why Sending USDT Still Requires a Second Coin</title>
      <dc:creator>Azalea</dc:creator>
      <pubDate>Fri, 16 Oct 2026 21:33:40 +0000</pubDate>
      <link>https://dev.to/azaleakuts/why-sending-usdt-still-requires-a-second-coin-gim</link>
      <guid>https://dev.to/azaleakuts/why-sending-usdt-still-requires-a-second-coin-gim</guid>
      <description>&lt;p&gt;If you've worked on a stablecoin wallet, you've probably seen this support ticket. A user has 500 USDT on TRON, presses Send, and gets an error. The balance is right there on the screen. What's missing is TRX, a coin they never bought and didn't know they needed.&lt;/p&gt;

&lt;p&gt;Every chain that carries stablecoins charges its fees in its own native token. So "send 50 USDT" is quietly a two-asset operation, and the second asset is the one users forget. My last post looked at why a swap screen shows the wrong number. This one is about the coin that never shows up on the send screen at all, until it blocks the transfer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On TRON, Solana, Stellar, TON and Ethereum, a USDT or USDC transfer costs something in the chain's native token, and each chain charges it differently.&lt;/li&gt;
&lt;li&gt;The cost often depends on the recipient. Sending USDT on TRON to an address that has never held USDT needs about twice the Energy. On Solana, a missing token account adds rent.&lt;/li&gt;
&lt;li&gt;Fee sponsorship exists on every one of these chains: TRON Energy delegation, Solana fee payers, Stellar fee-bump transactions, TON gasless transfers, Ethereum paymasters.&lt;/li&gt;
&lt;li&gt;Gasless doesn't mean free. Someone still pays the network, and the cost usually comes out of the stablecoin.&lt;/li&gt;
&lt;li&gt;A wallet should check the native balance before the user types an amount, not after they press Send.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Five chains, five billing models
&lt;/h2&gt;

&lt;p&gt;A comparison of the &lt;a href="https://www.digitalconnectmag.com/best-blockchains-for-crypto-payments/" rel="noopener noreferrer"&gt;best blockchains for crypto payments&lt;/a&gt; published this week scores TRON, Solana, TON, Stellar and Ethereum on cost per transfer, finality, stablecoin liquidity, real payment use and access. Its definition of access includes how hard it is to get the gas token. I think that criterion deserves more attention than it usually gets, because it's where first-time users get stuck.&lt;/p&gt;

&lt;p&gt;Here is what each chain actually charges for a stablecoin transfer, checked against the networks' own documentation:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Chain&lt;/th&gt;
&lt;th&gt;Native token&lt;/th&gt;
&lt;th&gt;What a stablecoin transfer costs&lt;/th&gt;
&lt;th&gt;What depends on the recipient&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TRON&lt;/td&gt;
&lt;td&gt;TRX&lt;/td&gt;
&lt;td&gt;About 65,000 Energy, or 6.5 TRX if burned at 100 sun per Energy&lt;/td&gt;
&lt;td&gt;About 131,000 Energy if the recipient has never held USDT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Solana&lt;/td&gt;
&lt;td&gt;SOL&lt;/td&gt;
&lt;td&gt;5,000 lamports per signature, plus an optional priority fee&lt;/td&gt;
&lt;td&gt;About 0.00204 SOL rent if the recipient has no token account&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stellar&lt;/td&gt;
&lt;td&gt;XLM&lt;/td&gt;
&lt;td&gt;100 stroops per operation (0.00001 XLM)&lt;/td&gt;
&lt;td&gt;The recipient needs a trustline, which locks 0.5 XLM of their reserve&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TON&lt;/td&gt;
&lt;td&gt;Gram (formerly Toncoin)&lt;/td&gt;
&lt;td&gt;A small fee in Gram, or in USDT through gasless transfers&lt;/td&gt;
&lt;td&gt;Depends on the wallet, see below&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ethereum and L2s&lt;/td&gt;
&lt;td&gt;ETH&lt;/td&gt;
&lt;td&gt;Gas units times gas price, paid in ETH on mainnet, Base and Arbitrum alike&lt;/td&gt;
&lt;td&gt;More gas if the recipient holds none of the token yet, since a new storage slot is written&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The rows don't line up, so a wallet that supports all five needs five different answers to "can this user send right now?"&lt;/p&gt;

&lt;h2&gt;
  
  
  TRON: Energy, Bandwidth and the first-time recipient
&lt;/h2&gt;

&lt;p&gt;TRON doesn't charge a gas price in the usual sense. A USDT transfer consumes two resources. Energy pays for running the token contract, and Bandwidth pays for the bytes of the transaction (&lt;a href="https://docs.getblock.io/tron-energy/tron-fee-model" rel="noopener noreferrer"&gt;GetBlock docs&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;A transfer to an address that already holds USDT takes about 65,000 Energy, because it updates an existing storage slot. A transfer to an address that has never held USDT takes about 131,000, because the contract has to create one. If the sender has no Energy, the network burns TRX instead, at 100 sun per Energy. That works out to about 6.5 TRX for the first case and 13.1 TRX for the second.&lt;/p&gt;

&lt;p&gt;Bandwidth is easier on the user. Every account gets 600 free Bandwidth points a day, and a USDT transfer uses about 345, so one transfer a day is covered.&lt;/p&gt;

&lt;p&gt;There are cheaper routes, but none of them helps a user who has zero TRX:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Staking TRX gives Energy in proportion to the stake. Unstaking takes 14 days.&lt;/li&gt;
&lt;li&gt;Energy rental markets delegate Energy from large stakers, typically at 30 to 50 sun per unit.&lt;/li&gt;
&lt;li&gt;In February 2025 Justin Sun announced a gas-free option for sending USDT on TRON without holding TRX. Whether you can use it depends on the wallet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The doubled cost for new recipients catches people out. The amount the wallet quoted yesterday for sending to a friend can be half of what it charges today for paying someone new.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solana: cheap fees, plus rent
&lt;/h2&gt;

&lt;p&gt;Solana's base fee is 5,000 lamports per signature, or 0.000005 SOL. A priority fee on top is optional (&lt;a href="https://solana.com/docs/core/fees" rel="noopener noreferrer"&gt;Solana docs&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The surprise is rent. USDC and USDT on Solana live in token accounts, one per owner per token. If the recipient doesn't have one yet, the sender's transaction usually creates it, and every account must hold a minimum balance proportional to its size. The formula in Solana's docs is &lt;code&gt;(account_size + 128) * 3,480 lamports * 2 years&lt;/code&gt;. A token account is 165 bytes, so that comes to 2,039,280 lamports, about 0.00204 SOL (&lt;a href="https://solana.com/docs/core/accounts" rel="noopener noreferrer"&gt;Solana docs&lt;/a&gt;). The balance is returned when the account is closed.&lt;/p&gt;

&lt;p&gt;That is a small number, but it is about 400 times the base fee. A wallet that shows "fee: 0.000005 SOL" and then takes 0.002 more has a trust problem, even if the amount is tiny.&lt;/p&gt;

&lt;p&gt;Solana also lets a different account pay the fee for a transaction, which is how many apps sponsor their users' transfers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stellar: reserves and trustlines
&lt;/h2&gt;

&lt;p&gt;Stellar's fees are close to nothing: the network minimum is 100 stroops per operation, which is 0.00001 XLM, and it only rises when the network is congested (&lt;a href="https://developers.stellar.org/docs/learn/fundamentals/fees-resource-limits-metering" rel="noopener noreferrer"&gt;Stellar docs&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The catch is the minimum balance. Every account must keep two base reserves, currently 1 XLM, plus 0.5 XLM for each subentry (&lt;a href="https://developers.stellar.org/docs/learn/fundamentals/lumens" rel="noopener noreferrer"&gt;Stellar docs&lt;/a&gt;). To hold USDC, an account needs a trustline to the issuer, and that trustline is a subentry. So a wallet holding only USDC still has to keep 1.5 XLM locked.&lt;/p&gt;

&lt;p&gt;The recipient side matters even more. An account without a USDC trustline can't receive USDC at all. On TRON or Ethereum a payment to a fresh address just works. On Stellar it fails until the recipient opts in.&lt;/p&gt;

&lt;p&gt;Stellar has two built-in ways around this. Fee-bump transactions let another account pay the fee, and sponsored reserves let an app cover a user's reserves.&lt;/p&gt;

&lt;h2&gt;
  
  
  TON: gasless, with a fee in USDT
&lt;/h2&gt;

&lt;p&gt;On TON, fees are paid in the native token, which was renamed from Toncoin to Gram in June 2026. Tonkeeper's gasless mode lets a W5 wallet send USDT with no Gram in it. The user signs a message, a relayer wraps it in a transaction and pays the network, and the fee is taken from the USDT being sent (&lt;a href="https://keeperwallet.com/en/article/how-tonkeeper-brings-gasless" rel="noopener noreferrer"&gt;Tonkeeper&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Tonkeeper's own explainer is honest about this: gas doesn't disappear, it just gets paid in a different token. Its example, at a price of $7 per token, puts a gasless USDT transfer at about $0.14, against about $0.10 through its prepaid Battery. Users get convenience and pay a small premium for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ethereum and L2s: paymasters
&lt;/h2&gt;

&lt;p&gt;On Ethereum mainnet, Base and Arbitrum, gas is paid in ETH. L2s made transfers cheap, but they didn't remove the need to hold ETH on that specific network. Someone with USDC on Base and ETH only on mainnet still can't send.&lt;/p&gt;

&lt;p&gt;Two standards changed this. ERC-4337 introduced paymasters, contracts that pay gas on a user's behalf. EIP-7702, activated with the Pectra upgrade in May 2025, lets regular wallets delegate to smart contract code, so they can use paymasters too. Circle's Paymaster, for example, checks that the sender has enough USDC and pays the gas, charging the user in USDC instead of ETH (&lt;a href="https://www.circle.com/blog/how-the-pectra-upgrade-is-unlocking-gasless-usdc-transactions-with-eip-7702" rel="noopener noreferrer"&gt;Circle&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;A different approach is a chain built around one stablecoin. Plasma launched in September 2025 with zero-fee USDT transfers during its initial rollout. That removes the problem for USDT on that chain, but users still have to get there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check before the user types an amount
&lt;/h2&gt;

&lt;p&gt;Every chain above has a fix, but the user only benefits if the wallet checks first. Here is a preflight check that returns how much of the native token a stablecoin transfer needs. It ignores some edge cases on purpose, such as TRON Bandwidth beyond the free daily allowance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;SendCheck&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;chain&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&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;recipientHasUsdt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;availableEnergy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bigint&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;solana&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;recipientHasTokenAccount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;priorityFeeLamports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bigint&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stellar&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;recipientHasTrustline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;senderSubentries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;evm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;gasLimit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;maxFeePerGasWei&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bigint&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SUN_PER_ENERGY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TOKEN_ACCOUNT_RENT_LAMPORTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="nx"&gt;_039_280n&lt;/span&gt; &lt;span class="c1"&gt;// 165-byte token account&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BASE_RESERVE_STROOPS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="nx"&gt;_000_000n&lt;/span&gt; &lt;span class="c1"&gt;// 0.5 XLM&lt;/span&gt;

&lt;span class="c1"&gt;// Native amount the sender must hold, in the chain's smallest unit&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;nativeNeeded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SendCheck&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;bigint&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tron&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;energy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recipientHasUsdt&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;65&lt;/span&gt;&lt;span class="nx"&gt;_000n&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;131&lt;/span&gt;&lt;span class="nx"&gt;_000n&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;missing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;energy&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;availableEnergy&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;energy&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;availableEnergy&lt;/span&gt; &lt;span class="p"&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;missing&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;SUN_PER_ENERGY&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;solana&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;rent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recipientHasTokenAccount&lt;/span&gt; &lt;span class="p"&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="nx"&gt;TOKEN_ACCOUNT_RENT_LAMPORTS&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="nx"&gt;_000n&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;priorityFeeLamports&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;rent&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stellar&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recipientHasTrustline&lt;/span&gt;&lt;span class="p"&gt;)&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="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Recipient has no trustline for this asset&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;minBalance&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="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;senderSubentries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;BASE_RESERVE_STROOPS&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;minBalance&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;100&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;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;evm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gasLimit&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxFeePerGasWei&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few outputs to sanity-check against the table: a TRON transfer to a new recipient with no Energy returns 13,100,000 sun, or 13.1 TRX. A Solana transfer that creates a token account returns 2,044,280 lamports. A Stellar sender with one USDC trustline needs 15,000,100 stroops, about 1.5 XLM, most of which stays locked as reserve.&lt;/p&gt;

&lt;p&gt;The real work is in the inputs. &lt;code&gt;recipientHasUsdt&lt;/code&gt; and &lt;code&gt;recipientHasTokenAccount&lt;/code&gt; need a lookup on the recipient before the quote, which is easy to skip and expensive to get wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  A checklist for the send screen
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Show the native balance next to the stablecoin balance, with the network name.&lt;/li&gt;
&lt;li&gt;[ ] Run the preflight check as soon as the recipient is known, before the amount.&lt;/li&gt;
&lt;li&gt;[ ] Look up the recipient: USDT history on TRON, a token account on Solana, a trustline on Stellar.&lt;/li&gt;
&lt;li&gt;[ ] Show the total fee, including rent or reserves, in both the native token and dollars.&lt;/li&gt;
&lt;li&gt;[ ] If the user has no native token, offer the fix in place: sponsored fee, gasless mode or a small top-up.&lt;/li&gt;
&lt;li&gt;[ ] When the fee is paid in the stablecoin, say so before signing and show the amount the recipient will get.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Why can't I send USDT if I have enough USDT?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Because the network fee is charged in the chain's native token, not in USDT. On TRON you need TRX or Energy, on Solana SOL, on Ethereum and its L2s ETH, on Stellar XLM and on TON Gram. Your USDT balance can cover the amount but not the fee. Some wallets now offer gasless sending, where a relayer or paymaster pays the network and takes the fee from your USDT, so check whether yours supports it. Otherwise, buying a small amount of the native token on the same network solves the problem for many future transfers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why did my USDT transfer on TRON cost twice as much this time?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most likely the recipient had never held USDT before. Sending to such an address takes about 131,000 Energy instead of about 65,000, because the token contract has to create a new storage entry for the recipient. If you have no staked or rented Energy, the network burns TRX to cover it, so the cost roughly doubles. Later transfers to the same address go back to the normal amount. Staking TRX or renting Energy lowers the cost for both cases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is a gasless transfer free?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. The network still charges a fee, and someone pays it. In gasless setups a relayer or paymaster pays the network in the native token and recovers the cost from you, usually in the stablecoin you're sending, sometimes with a small markup. Some apps cover the fee themselves as a feature and make the money back elsewhere. Gasless removes the need to hold a second coin. It doesn't remove the cost, so check the final amount the recipient gets before signing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing thoughts
&lt;/h2&gt;

&lt;p&gt;Sending a stablecoin still means managing two balances, and most of the confusion comes from wallets that show only one of them. All the tools for hiding the second coin already exist on these five chains. What's left is wiring the check into the moment the user picks a recipient.&lt;/p&gt;

&lt;p&gt;If you've shipped gasless sending or fee sponsorship, which part took the most work: the relayer, the pricing, or explaining it to users? I'd like to compare notes in the comments.&lt;/p&gt;

</description>
      <category>cryptocurrency</category>
      <category>fintech</category>
      <category>blockchain</category>
      <category>ux</category>
    </item>
    <item>
      <title>Your Swap Screen Is Showing the Wrong Number</title>
      <dc:creator>Azalea</dc:creator>
      <pubDate>Wed, 14 Oct 2026 23:10:30 +0000</pubDate>
      <link>https://dev.to/azaleakuts/your-swap-screen-is-showing-the-wrong-number-jd9</link>
      <guid>https://dev.to/azaleakuts/your-swap-screen-is-showing-the-wrong-number-jd9</guid>
      <description>&lt;p&gt;A month ago I wrote about &lt;a href="https://dev.to/azaleakuts/what-a-crypto-app-should-do-before-you-press-send-3cp5"&gt;what a crypto app should do before you press "Send"&lt;/a&gt;. There's one more thing that happens before that button, and most swap interfaces still get it wrong: the number they put in the biggest font.&lt;/p&gt;

&lt;p&gt;Usually it's the exchange rate. 1 XMR = 0.12 ETH. Clean, comparable, easy to sort.&lt;/p&gt;

&lt;p&gt;It's also the least useful figure on the screen.&lt;/p&gt;

&lt;p&gt;What lands in the user's wallet is an amount. Between the rate and that amount sit several costs the rate doesn't show.&lt;/p&gt;

&lt;h2&gt;
  
  
  One quote, four costs
&lt;/h2&gt;

&lt;p&gt;Cross-chain swaps make this easy to see, because each leg has its own costs. Take Monero to Ethereum. CoinMarketCal recently published &lt;a href="https://coinmarketcal.com/news/xmr-to-eth-exchange-fees-compared-what-to-expect-in-2026" rel="noopener noreferrer"&gt;a breakdown of XMR to ETH exchange fees&lt;/a&gt; and counted four separate costs behind a single quote:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;XMR network fee   → paid by the user, from their own wallet
Provider fees     → trading fees charged by liquidity sources
Spread            → gap between the market price and the offered rate
ETH payout fee    → gas to send ETH to the user's address
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only the first one is under the user's control. The Monero fee depends on network load and transaction size, not on the amount sent, and wallets let you choose from &lt;a href="https://www.getmonero.org/resources/user-guides/make-payment.html" rel="noopener noreferrer"&gt;four priority levels&lt;/a&gt;. Everything else is decided by the provider.&lt;/p&gt;

&lt;p&gt;The Ethereum side has its own formula. According to &lt;a href="https://ethereum.org/developers/docs/gas/" rel="noopener noreferrer"&gt;ethereum.org&lt;/a&gt;, a transaction costs gas units multiplied by the base fee plus a priority fee. A plain ETH transfer uses 21,000 gas. The base fee can move by up to 12.5% per block with demand, so the same payout can cost noticeably more at a busy moment.&lt;/p&gt;

&lt;p&gt;None of this shows up in "1 XMR = 0.12 ETH".&lt;/p&gt;

&lt;h2&gt;
  
  
  Better rate, worse result
&lt;/h2&gt;

&lt;p&gt;Here's a simple case. Two quotes for a 2 XMR swap (the numbers are illustrative):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Quote A shows a rate of 0.12000 ETH, charges a 0.1% trading fee, and takes a 0.002 ETH withdrawal fee afterwards.&lt;/li&gt;
&lt;li&gt;Quote B shows 0.11925 ETH, with every cost after the deposit already included.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sort by rate and A wins. Now compute what actually arrives:&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;function&lt;/span&gt; &lt;span class="nf"&gt;rankByReceived&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;quotes&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="nx"&gt;quotes&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;q&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;received&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amountIn&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rate&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tradingFee&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fixedFees&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;}))&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="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;received&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;received&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;rankByReceived&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;amountIn&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="na"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;tradingFee&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fixedFees&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.002&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;B&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;amountIn&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="na"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.11925&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;tradingFee&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fixedFees&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="c1"&gt;// [ { name: "B", received: 0.2385 }, { name: "A", received: 0.23776 } ]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;B pays out more. The gap is small here, but a UI that ranks by rate picks the wrong option every time this pattern appears. And it appears a lot, because "no fees" usually means the fees moved into the spread or into a step that comes later.&lt;/p&gt;

&lt;p&gt;The rate wasn't lying. It answered a question the user didn't ask.&lt;/p&gt;

&lt;h2&gt;
  
  
  Small swaps hurt the most
&lt;/h2&gt;

&lt;p&gt;Network fees are roughly fixed per transaction. They don't scale with the amount, so their share grows as the swap gets smaller.&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;feeShare&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;receivedEth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fixedFeesEth&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;fixedFeesEth&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;receivedEth&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;feeShare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.002&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// ≈ 0.0083 → 0.8%&lt;/span&gt;
&lt;span class="nf"&gt;feeShare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.03&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.002&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// ≈ 0.0667 → 6.7%&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same fees, eight times the impact. This is also why providers set minimum amounts: below a certain point, fees eat most of the order. A screen that shows only the rate hides this completely. A screen that shows the received amount makes it visible the moment someone types a small number.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the quote object should carry
&lt;/h2&gt;

&lt;p&gt;If I were designing the API behind a swap screen, the response would put the received amount first and say what's inside it:&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;"amountIn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.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;"asset"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"XMR"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"amountOut"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&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.2385"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"asset"&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"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rateMode"&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;"includedInQuote"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"spread"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"provider fees"&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 payout fee"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"paidSeparately"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"XMR network fee"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expiresAt"&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-10-15T12:00:00Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;amountOut&lt;/code&gt; is the headline. Anyone who wants the rate can derive it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;includedInQuote&lt;/code&gt; and &lt;code&gt;paidSeparately&lt;/code&gt; matter more than they look. Two quotes are only comparable if they cover the same costs. Without these fields, the frontend has no way to tell whether it's comparing like with like.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;rateMode&lt;/code&gt; tells the user what kind of promise they're getting. A floating quote follows the market until execution and can land slightly above or below the estimate. A fixed quote locks the amount for a limited window, usually with a small buffer priced in for volatility. Both are fine. Hiding which one it is isn't.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;expiresAt&lt;/code&gt; is there for fixed quotes. A locked amount with no visible deadline is a support ticket waiting to happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thin liquidity widens the gap
&lt;/h2&gt;

&lt;p&gt;For pairs with deep markets, the difference between the rate and the received amount stays small. For thinner pairs it grows, and Monero is a good example. Kraken &lt;a href="https://cointelegraph.com/news/kraken-ends-monero-support-european-economic-area" rel="noopener noreferrer"&gt;stopped XMR trading and deposits for clients in the European Economic Area&lt;/a&gt; from October 31, 2024. Fewer venues usually means wider spreads, and the spread is exactly the cost a rate-first screen never shows.&lt;/p&gt;

&lt;p&gt;So the less liquid the asset, the more misleading the headline rate gets. That's the opposite of what users need.&lt;/p&gt;

&lt;h2&gt;
  
  
  A short checklist for swap screens
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Lead with the amount the user will receive.&lt;/li&gt;
&lt;li&gt;Say whether that number is an estimate or a guaranteed amount, and for how long.&lt;/li&gt;
&lt;li&gt;List what's already included and what the user still pays.&lt;/li&gt;
&lt;li&gt;Flag amounts close to the minimum, where fixed fees dominate.&lt;/li&gt;
&lt;li&gt;Compare quotes by received amount, never by rate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this needs new infrastructure. The provider already calculates every one of these numbers to build the quote. The only question is whether the interface passes them on.&lt;/p&gt;

&lt;p&gt;People spend what arrives in their wallet. The screen should start there.&lt;/p&gt;

</description>
      <category>cryptocurrency</category>
      <category>fintech</category>
      <category>ux</category>
    </item>
    <item>
      <title>What an Exchange’s Proof of Reserves Actually Proves</title>
      <dc:creator>Azalea</dc:creator>
      <pubDate>Fri, 09 Oct 2026 15:55:49 +0000</pubDate>
      <link>https://dev.to/azaleakuts/what-an-exchanges-proof-of-reserves-actually-proves-1mn3</link>
      <guid>https://dev.to/azaleakuts/what-an-exchanges-proof-of-reserves-actually-proves-1mn3</guid>
      <description>&lt;p&gt;On September 24, 2026, attackers drained about $387.5 million from Bitget. Five days later, the snapshot for its 47th proof of reserves report put the overall reserve ratio at 131%. Withdrawals were still coming back one asset at a time.&lt;/p&gt;

&lt;p&gt;I found that pair of numbers more instructive than any explainer. The exchange had lost a large sum and was still holding more than it owed, at least at 09:00 UTC on one Tuesday. What the ratio couldn't say was how the attackers got in, or what the balance sheet would look like the following week.&lt;/p&gt;

&lt;p&gt;My last two posts dealt with a single transfer, before and after you press Send. This one is about the platform that holds the balance in the first place. If you build on exchange APIs, or simply keep coins on an exchange, you should know what a proof of reserves report covers and where it stops.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Proof of reserves has two halves: proof of assets (the exchange controls these wallets) and proof of liabilities (this is what it owes customers). A ratio needs both.&lt;/li&gt;
&lt;li&gt;A Merkle sum tree lets each user check that their own balance was counted, without the exchange publishing everyone's balances.&lt;/li&gt;
&lt;li&gt;The classic weak spot is a fake negative balance hidden in the tree. Zero-knowledge proofs close it, which is why OKX moved to zk-STARK proofs.&lt;/li&gt;
&lt;li&gt;A report is a snapshot. It doesn't cover borrowed assets moved in for the day, fiat, off-chain liabilities or operational security.&lt;/li&gt;
&lt;li&gt;Bitget's 131% after a $387.5 million theft is a good case for seeing what a ratio shows and what it leaves out.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Two halves of one number
&lt;/h2&gt;

&lt;p&gt;A reserve ratio is assets divided by liabilities, calculated per asset, and each side has to be proven separately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Proof of assets&lt;/strong&gt; shows that the exchange controls specific on-chain addresses. Usually it publishes the addresses and signs a message with each key. OKX, for example, provides open-source tools to verify signed "I am an OKX address" messages and to compare balances against a block-height snapshot (&lt;a href="https://www.okx.com/proof-of-reserves" rel="noopener noreferrer"&gt;OKX&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Proof of liabilities&lt;/strong&gt; shows what the exchange owes its users, and it's the harder half. Publishing every customer balance would wreck privacy, so the usual approach is a Merkle sum tree, which Vitalik Buterin walked through in his 2022 post on &lt;a href="https://vitalik.eth.limo/general/2022/11/19/proof_of_solvency.html" rel="noopener noreferrer"&gt;proof of solvency&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;OKX's 47th report lists 109% for BTC, 101% for ETH, 105% for USDT and 100% for USDC. You can only read those figures as reserve ratios because assets and liabilities were proven for the same snapshot.&lt;/p&gt;

&lt;h2&gt;
  
  
  How a Merkle sum tree proof works
&lt;/h2&gt;

&lt;p&gt;Each user becomes a leaf: a hash of a salted user identifier plus that user's balance. Every parent node stores a hash of its children and the sum of their balances, so the root ends up committing to total liabilities.&lt;/p&gt;

&lt;p&gt;The exchange publishes only the root. You get a proof made of the sibling nodes along the path from your leaf to the root. A verification tool recomputes that path, and if it lands on the published root, your balance was counted.&lt;/p&gt;

&lt;p&gt;Here is the check in TypeScript. Real exchanges use their own leaf formats, so read this as the shape of the verification rather than a drop-in tool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;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="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Node&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bigint&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Step&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;sibling&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Node&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;siblingOnLeft&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;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;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="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;s&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;leaf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userIdHash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Node&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`leaf|&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userIdHash&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;balance&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="na"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;right&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Node&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Node&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;left&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sum&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="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;right&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sum&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="nx"&gt;n&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="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Negative sum in tree&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;hash&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="s2"&gt;`node|&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;|&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sum&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;right&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;|&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;right&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sum&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="na"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;right&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sum&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;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;verifyInclusion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Step&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Node&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;sibling&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;siblingOnLeft&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;node&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;siblingOnLeft&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sibling&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sibling&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="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sum&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Balances are &lt;code&gt;bigint&lt;/code&gt; in the smallest unit of the asset, since floating point has no business near money. The sum also goes into every hash, so changing a subtotal anywhere breaks every path that runs through it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The negative balance trick
&lt;/h2&gt;

&lt;p&gt;Suppose an exchange holds 890 ETH but owes customers 1,390 ETH. It can add a fake account with a balance of -500 ETH, and the root will show liabilities of 890 ETH, matched exactly by reserves.&lt;/p&gt;

&lt;p&gt;In a plain Merkle sum tree, the defense is the check you see in &lt;code&gt;parent&lt;/code&gt;: a negative sum throws. A user whose path passes near the fake account sees the negative value and rejects the proof. But that only helps if someone in that branch actually runs the check, and an exchange can bet that nobody will. Buterin points out that quietly leaving those users out of the tree would work just as well for a dishonest exchange.&lt;/p&gt;

&lt;p&gt;Zero-knowledge proofs take that bet off the table. A zk proof can show that every balance in the tree is non-negative and that they add up to the claimed total, without revealing any individual balance. OKX moved its liabilities proof to zk-STARKs. For reports since September 2024, users download an inclusion proof file and run it through an open-source validator, and a separate file proves the total and the non-negative constraint (&lt;a href="https://www.okx.com/proof-of-reserves" rel="noopener noreferrer"&gt;OKX&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  What a reserve ratio can't tell you
&lt;/h2&gt;

&lt;p&gt;Proof of reserves became the standard reassurance after FTX collapsed in November 2022. A month later the accounting firm Mazars paused this work for all its crypto clients, citing concerns about "the way these reports are understood by the public." Its reports followed agreed-upon procedures, were not audits, and described a single past point in time (&lt;a href="https://www.cnbc.com/2022/12/16/mazars-suspends-all-work-with-crypto-clients-including-binance-cryptocom.html" rel="noopener noreferrer"&gt;CNBC&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Four years on, the gaps are the same:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Any day other than the snapshot. Assets can be borrowed for the snapshot and returned afterwards. Buterin's suggested fix is frequent or coordinated proofs, so two exchanges can't count the same funds.&lt;/li&gt;
&lt;li&gt;Fiat. Bank balances can't be proven cryptographically. At best they rest on bank confirmations and an auditor's attestation.&lt;/li&gt;
&lt;li&gt;Liabilities outside the tree. Loans, obligations to affiliates or market makers, and anything else that isn't a customer balance won't show up in a customer liabilities tree.&lt;/li&gt;
&lt;li&gt;Key security. A wallet can be fully reserved and badly protected. Bybit lost about $1.5 billion in ETH in February 2025, in an attack the FBI attributed to North Korea.&lt;/li&gt;
&lt;li&gt;Your own inclusion. If you never check that your balance is in the tree, the proof did nothing for you.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Bitget as a worked example
&lt;/h2&gt;

&lt;p&gt;After the September 24 theft, Bitget covered the losses from its protection fund, which had held 5,500 BTC beforehand. On September 30 it said the fund was back at $309 million. Withdrawals resumed for BTC on September 28, ETH on September 29 and USDT on September 30, with other tokens, fiat and P2P following on October 2 (&lt;a href="https://www.cryptotimes.io/2026/09/30/bitget-protection-fund-back-above-300m-after-387-5m-hack-p2p-withdrawals-return-october-2/" rel="noopener noreferrer"&gt;The Crypto Times&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The 47th report, snapshot at 09:00 UTC on September 29, gave an overall ratio of 131% across 19 assets, down from 135% in the previous report. BTC stood at 142%, ETH at 110%, USDT at 107% and USDC at 154%. In the 24 hours before the snapshot, net outflows came to about $463 million.&lt;/p&gt;

&lt;p&gt;So Bitget stayed solvent through a theft of that size and a run of withdrawals, and the report shows it. For how the attackers got in, how fast the fund gets rebuilt, or what the ratio looked like on September 23, you need other sources.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reserves are one column in the table
&lt;/h2&gt;

&lt;p&gt;A recent &lt;a href="https://www.thailand-business-news.com/thailand-prnews/top-crypto-exchanges-overall-in-2026-what-works-for-users-in-asia" rel="noopener noreferrer"&gt;comparison of top crypto exchanges for users in Asia&lt;/a&gt; scores six platforms on five public checks: trading volume share, regulatory access in Asian markets, proof of reserves and protection funds, base spot fees, and incident record since 2025. I like that reserves and incidents sit side by side there. Bitget's 131% reads differently once you know the exchange had just paid out $387.5 million from its own fund.&lt;/p&gt;

&lt;p&gt;If you build for Asia, the regulatory column deserves the same attention, because access changes from one country to the next. The comparison tracks platforms that are licensed in one market, blocked in another and winding down in a third.&lt;/p&gt;

&lt;p&gt;I also noticed how the table handles a non-custodial swap service: its reserves column is marked not applicable. That's the right call. A service that doesn't hold user balances has no liabilities tree to publish, and the risk sits in the transfer itself, which is where my last two posts went.&lt;/p&gt;

&lt;h2&gt;
  
  
  A checklist before you trust a ratio
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;If you keep a balance on an exchange&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Find the latest report and note the snapshot time as well as the publication date.&lt;/li&gt;
&lt;li&gt;[ ] Check your own inclusion with the exchange's proof file or validator.&lt;/li&gt;
&lt;li&gt;[ ] Look at the ratio for each asset you actually hold, not only the headline figure.&lt;/li&gt;
&lt;li&gt;[ ] Compare the last few reports. A falling trend tells you more than a single number.&lt;/li&gt;
&lt;li&gt;[ ] Read the incident history and the size of any protection fund next to the ratio.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;If you build on exchange APIs&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Show users when reserves were last proven, with the snapshot date.&lt;/li&gt;
&lt;li&gt;[ ] Treat withdrawal pauses as a normal state in your app, with clear messaging.&lt;/li&gt;
&lt;li&gt;[ ] Track where each venue is licensed or restricted, per user region.&lt;/li&gt;
&lt;li&gt;[ ] Don't present a reserve ratio as a safety rating. It is one input.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Does a proof of reserves report mean the exchange was audited?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. Most reports are attestations or self-published proofs, not financial audits. An audit looks at the whole company: all liabilities, internal controls, related parties and accounting policies. A proof of reserves covers a defined set of customer balances and wallets at one moment. When Mazars paused this work in December 2022, it stressed that its reports followed agreed-upon procedures and did not give an audit opinion. A report backs one specific claim: at the snapshot time, customer balances in scope were matched by on-chain assets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why would a reserve ratio be above 100% after a hack?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The ratio compares what the exchange holds with what it owes customers, and both sides move after an incident. If the exchange covers stolen funds from its own capital or a protection fund, customer balances stay whole while the company's own money shrinks. Many users also withdraw after a hack, which lowers liabilities and assets together. Bitget's 131% on September 29 reflected all of that. The incident still cost the company hundreds of millions; the ratio just shows customers were covered at the snapshot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How can I verify my own balance was included?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Log in to the exchange and open its proof of reserves page, which usually links a per-user proof file for each report. Download the file for the latest report and run it through the verification tool the exchange provides, ideally an open-source one you can inspect. For Merkle tree reports, the tool rebuilds the path from your leaf to the published root. For zk-based reports, it checks the proof against the published commitment. If your balance isn't there, contact support and keep a screenshot of the report date.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing thoughts
&lt;/h2&gt;

&lt;p&gt;Most of what an exchange tells you about itself has to be taken on trust. A reserve report is one of the rare exceptions, because you can run the check yourself. I'd still read it next to the snapshot date, your own inclusion proof and the incident history, since the ratio alone covers one moment and one kind of risk.&lt;/p&gt;

&lt;p&gt;Do you check your inclusion proof when a new report comes out, or has anyone built that check into an app? I'd like to hear how others handle it.&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>cryptocurrency</category>
      <category>fintech</category>
      <category>security</category>
    </item>
    <item>
      <title>What Happens After You Press Send on a Bitcoin-to-Monero Swap</title>
      <dc:creator>Azalea</dc:creator>
      <pubDate>Tue, 06 Oct 2026 13:40:50 +0000</pubDate>
      <link>https://dev.to/azaleakuts/what-happens-after-you-press-send-on-a-bitcoin-to-monero-swap-4f62</link>
      <guid>https://dev.to/azaleakuts/what-happens-after-you-press-send-on-a-bitcoin-to-monero-swap-4f62</guid>
      <description>&lt;p&gt;My last post covered what a transfer flow should do before the user signs. A reader replied with the obvious follow-up: make the settlement state visible before the user thinks the transfer is done. Fair point. Most crypto apps put real effort into the confirmation screen and then hand the user a spinner labeled "Pending".&lt;/p&gt;

&lt;p&gt;To show what belongs in place of that spinner, I picked the least convenient everyday case I could find: swapping Bitcoin for Monero. It has two chains with different block times, one leg anyone can inspect and one nobody can, a waiting period after the funds arrive, and a protocol upgrade that is in public testing this week. A status model that copes with this pair has an easy time with a stablecoin transfer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Seen", "confirmed", "spendable" and "final" are separate states. On a BTC to XMR swap the gap between two of them can run from minutes to hours.&lt;/li&gt;
&lt;li&gt;A Bitcoin deposit sitting in the mempool is replaceable by default since Bitcoin Core 28.0, so treat it as information and wait for a block.&lt;/li&gt;
&lt;li&gt;A Monero payout can't be verified in a block explorer. You need the recipient's wallet, or the transaction ID together with the transaction key and the address.&lt;/li&gt;
&lt;li&gt;Ten Monero confirmations make funds spendable, which is a weaker promise than final. In September 2025 an 18-block reorg invalidated 115 transactions.&lt;/li&gt;
&lt;li&gt;FCMP++ is still not on Monero mainnet as of October 6, 2026.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why this pair is a good stress test
&lt;/h2&gt;

&lt;p&gt;You can't send BTC to a Monero address. Bitcoin keys live on the secp256k1 curve and Monero keys on ed25519, the address formats have nothing in common, and neither chain knows the other exists. A &lt;a href="https://docs.getmonero.org/public-address/standard-address/" rel="noopener noreferrer"&gt;standard Monero address&lt;/a&gt; packs two public keys and a checksum into 95 characters. Moving value between the two networks always means two separate transactions with something in between, usually a swap service.&lt;/p&gt;

&lt;p&gt;TechBullion recently published a step-by-step guide on &lt;a href="https://techbullion.com/how-to-transfer-bitcoin-to-monero/" rel="noopener noreferrer"&gt;how to transfer Bitcoin to Monero&lt;/a&gt; that describes the flow from the user's chair: create a fresh Monero subaddress, open an order, send BTC to the deposit address, save the order ID, wait. I read it as a list of promises made to the user. The guide quotes 15 to 60 minutes for the swap on the service it uses as an example and about 20 more minutes before the XMR can be spent. It also names four usual reasons for a slow transfer: a low Bitcoin fee, a congested network, a deposit that doesn't match the order amount, or a compliance hold.&lt;/p&gt;

&lt;p&gt;Every one of those sentences is a state the backend has to track and the interface has to show. The two legs behave very differently:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Bitcoin leg (deposit)&lt;/th&gt;
&lt;th&gt;Monero leg (payout)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Average block interval&lt;/td&gt;
&lt;td&gt;About 10 minutes&lt;/td&gt;
&lt;td&gt;About 2 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What an outsider can read&lt;/td&gt;
&lt;td&gt;Sender, recipient, amount&lt;/td&gt;
&lt;td&gt;That the transaction exists and how many confirmations it has&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How a payment is verified&lt;/td&gt;
&lt;td&gt;Transaction ID in any explorer or node&lt;/td&gt;
&lt;td&gt;Recipient's wallet, or transaction ID + transaction key + address&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Before the first confirmation&lt;/td&gt;
&lt;td&gt;Replaceable by default (full RBF)&lt;/td&gt;
&lt;td&gt;In the pool; the wallet reports a &lt;code&gt;double_spend_seen&lt;/code&gt; flag&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;When received funds can be spent&lt;/td&gt;
&lt;td&gt;No protocol waiting period&lt;/td&gt;
&lt;td&gt;Once the output is 10 blocks old&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Model the swap as a state machine
&lt;/h2&gt;

&lt;p&gt;A single "pending" status sends every user to support with the same question. An explicit state machine answers it on the order page and gives your own team a shared vocabulary for incidents.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;SwapState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;awaiting_deposit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;deposit_seen&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;        &lt;span class="c1"&gt;// BTC tx in the mempool, still replaceable&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;deposit_confirming&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;  &lt;span class="c1"&gt;// in a block, below your confirmation threshold&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;exchanging&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;payout_sent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;         &lt;span class="c1"&gt;// XMR tx broadcast, txid known&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;payout_confirming&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;   &lt;span class="c1"&gt;// in a block, outputs still locked&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;spendable&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;           &lt;span class="c1"&gt;// the 10-block lock has passed&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;on_hold&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;             &lt;span class="c1"&gt;// compliance review, user action required&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;refunding&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;refunded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;expired&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;transitions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SwapState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;SwapState&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;awaiting_deposit&lt;/span&gt;&lt;span class="p"&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;deposit_seen&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;deposit_confirming&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;expired&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;deposit_seen&lt;/span&gt;&lt;span class="p"&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;deposit_confirming&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;awaiting_deposit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="c1"&gt;// replaced or dropped&lt;/span&gt;
  &lt;span class="na"&gt;deposit_confirming&lt;/span&gt;&lt;span class="p"&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;exchanging&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;on_hold&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;refunding&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;deposit_seen&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;exchanging&lt;/span&gt;&lt;span class="p"&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;payout_sent&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;on_hold&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;refunding&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;payout_sent&lt;/span&gt;&lt;span class="p"&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;payout_confirming&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;exchanging&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;        &lt;span class="c1"&gt;// never mined, send again&lt;/span&gt;
  &lt;span class="na"&gt;payout_confirming&lt;/span&gt;&lt;span class="p"&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;spendable&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;payout_sent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;               &lt;span class="c1"&gt;// reorg&lt;/span&gt;
  &lt;span class="na"&gt;spendable&lt;/span&gt;&lt;span class="p"&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;payout_confirming&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;                      &lt;span class="c1"&gt;// deep reorg&lt;/span&gt;
  &lt;span class="na"&gt;on_hold&lt;/span&gt;&lt;span class="p"&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;exchanging&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;refunding&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;refunding&lt;/span&gt;&lt;span class="p"&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;refunded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;refunded&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;           &lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="na"&gt;expired&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;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;canMove&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;SwapState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SwapState&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;transitions&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="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at the arrows that point backwards. Each one stands for a real event: a replaced deposit, a payout that never got mined, a chain reorganization. If your model has no backward transitions, it will report "completed" for money that has just disappeared from the recipient's wallet.&lt;/p&gt;

&lt;h2&gt;
  
  
  On the Bitcoin leg, seen is not confirmed
&lt;/h2&gt;

&lt;p&gt;The deposit is the public half, so tracking it is mostly a matter of discipline about wording.&lt;/p&gt;

&lt;p&gt;A transaction in the mempool tells you the user did something, and nothing is settled yet. Since version 28.0, released in October 2024, Bitcoin Core ships with &lt;code&gt;mempoolfullrbf=1&lt;/code&gt; as the default (&lt;a href="https://bitcoincore.org/en/releases/28.0/" rel="noopener noreferrer"&gt;release notes&lt;/a&gt;). Any unconfirmed transaction can be replaced by one that pays a higher fee, whether or not it signaled replaceability. Show "deposit seen" to calm the user down, and start the swap only after the confirmations you require.&lt;/p&gt;

&lt;p&gt;With an Esplora-compatible API the check takes two requests. &lt;code&gt;GET /tx/:txid&lt;/code&gt; returns the transaction with a &lt;code&gt;status&lt;/code&gt; object, and &lt;code&gt;GET /blocks/tip/height&lt;/code&gt; returns the current height (&lt;a href="https://github.com/Blockstream/esplora/blob/master/API.md" rel="noopener noreferrer"&gt;Esplora API&lt;/a&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ESPLORA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://blockstream.info/api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;BtcDeposit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;not_found&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;// never broadcast, dropped or replaced&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mempool&lt;/span&gt;&lt;span class="dl"&gt;'&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="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;confirmed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;confirmations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;btcDepositStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;txid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;BtcDeposit&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&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;fetch&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;ESPLORA&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/tx/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;txid&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;404&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;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;not_found&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Esplora responded with &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&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="c1"&gt;// Also check here that a vout pays your deposit address the expected value&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&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="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;confirmed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;block_height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;confirmed&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;block_height&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mempool&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;tipRes&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;fetch&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;ESPLORA&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/blocks/tip/height`&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;tip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tipRes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&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;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;confirmed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;confirmations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tip&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;block_height&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details matter in the interface. Show progress as "1 of 2 confirmations" so the user can see the order moving. When a transaction you had already seen comes back as &lt;code&gt;not_found&lt;/code&gt;, say that the deposit was replaced or dropped and tell the user what to do next. The Bitcoin side tends to be the slowest part of a BTC to XMR order, and a low fee can leave the deposit waiting for several blocks. The deposit screen is the right place to say so, before anything is sent.&lt;/p&gt;

&lt;h2&gt;
  
  
  On the Monero leg, someone has to tell you
&lt;/h2&gt;

&lt;p&gt;On the payout side the public data runs out. A Monero explorer can confirm that a transaction exists and count its confirmations. The amount and the recipient are hidden by design, so "paste the hash into an explorer" is not an answer you can give a user.&lt;/p&gt;

&lt;p&gt;There are two ways to verify a payout.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;From the recipient's wallet.&lt;/strong&gt; The wallet scans the chain with its own keys and lists what arrived. In &lt;code&gt;monero-wallet-rpc&lt;/code&gt;, &lt;code&gt;get_transfers&lt;/code&gt; with &lt;code&gt;in&lt;/code&gt; and &lt;code&gt;pool&lt;/code&gt; set to true returns each incoming transfer with &lt;code&gt;confirmations&lt;/code&gt;, &lt;code&gt;locked&lt;/code&gt;, &lt;code&gt;double_spend_seen&lt;/code&gt; and &lt;code&gt;suggested_confirmations_threshold&lt;/code&gt;. Amounts come in atomic units, and 1 XMR equals 1e12 of them (&lt;a href="https://docs.getmonero.org/rpc-library/wallet-rpc/" rel="noopener noreferrer"&gt;wallet RPC docs&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;From a payment proof.&lt;/strong&gt; The sender shares three things: the transaction ID, the destination address and the one-time transaction key. Anyone with a synced wallet can then check how much that transaction paid to that address (&lt;a href="https://www.getmonero.org/resources/user-guides/prove-payment.html" rel="noopener noreferrer"&gt;Monero user guide&lt;/a&gt;). Over RPC the method is &lt;code&gt;check_tx_key&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;walletRpc&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Add digest auth if monero-wallet-rpc runs with --rpc-login&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://127.0.0.1:18088/json_rpc&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="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&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;Content-Type&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;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;jsonrpc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;params&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;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&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="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;error&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="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="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Empty RPC response&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="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;result&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;XMR_LOCK_BLOCKS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;checkXmrPayout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;txid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;txKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;walletRpc&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;received&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;in_pool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;confirmations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;check_tx_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="nx"&gt;txid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;tx_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;txKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;address&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Atomic units. Parse as BigInt in production: values above 2^53 lose precision&lt;/span&gt;
    &lt;span class="na"&gt;receivedXmr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;received&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;e12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;inPool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;in_pool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;confirmations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;confirmations&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;pastDefaultLock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;in_pool&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;confirmations&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;XMR_LOCK_BLOCKS&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;If you run the sending side, three habits save a lot of support time:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Request the key when you send (&lt;code&gt;get_tx_key: true&lt;/code&gt; in &lt;code&gt;transfer&lt;/code&gt;) and store it with the order. The docs warn that &lt;code&gt;rescan_blockchain&lt;/code&gt; throws away anything that can't be rebuilt from the chain, and transaction keys are on that list.&lt;/li&gt;
&lt;li&gt;Show the XMR transaction ID on the order page and hand out the key on request, so the user can run the check in their own wallet.&lt;/li&gt;
&lt;li&gt;Ask users for a fresh subaddress per order and create one per deposit on your own side (&lt;code&gt;create_address&lt;/code&gt;). Reconciliation then becomes a lookup by &lt;code&gt;subaddr_index&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One caveat from the same documentation: a valid proof shows that funds were sent to an address. It does not guarantee that they are spendable. The recipient's wallet has the last word, and &lt;code&gt;get_balance&lt;/code&gt; reports &lt;code&gt;blocks_to_unlock&lt;/code&gt; and &lt;code&gt;time_to_unlock&lt;/code&gt; for exactly that purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seen, confirmed, spendable, final
&lt;/h2&gt;

&lt;p&gt;Status pages tend to use these four words as synonyms. On Monero the last two are the ones that get mixed up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spendable&lt;/strong&gt; is a consensus rule. A Monero transaction can only spend an output that was included at least 10 blocks earlier, which works out to about 20 minutes at one block every two minutes. An honest order page says "Arrived, spendable in about 14 minutes" and counts down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Final&lt;/strong&gt; is a judgment call. On September 14, 2025, the Qubic mining pool pushed an 18-block reorganization onto Monero's main chain. Researcher Rucknium's analysis counts 115 invalidated transactions, while 456 others from the orphaned blocks were mined again on the new chain (&lt;a href="https://rucknium.me/posts/monero-18-block-reorg/" rel="noopener noreferrer"&gt;Rucknium&lt;/a&gt;). The coins in the invalidated transactions went back to the senders' wallets. The 10-block lock only protects against reorgs up to 9 blocks deep, so in that hour "10 confirmations" stopped meaning "done". A month earlier Kraken had already reopened XMR deposits with a requirement of 720 confirmations, roughly a day of blocks (&lt;a href="https://www.fxstreet.com/cryptocurrencies/news/monero-price-forecast-xmr-eyes-wedge-breakout-rally-amid-qubic-hashrate-dominance-202508180526" rel="noopener noreferrer"&gt;FXStreet&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;For a developer this leads to three decisions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep the confirmation threshold in configuration, per asset and per amount. The wallet RPC even suggests a number through &lt;code&gt;suggested_confirmations_threshold&lt;/code&gt;, which scales with the amount received.&lt;/li&gt;
&lt;li&gt;Keep polling after &lt;code&gt;spendable&lt;/code&gt;. If a payout disappears, first look for the same transaction ID on the new chain, because most transactions survive a reorg. Create a new payout only once you know the original is gone, otherwise you pay twice.&lt;/li&gt;
&lt;li&gt;Check &lt;code&gt;locked&lt;/code&gt; and &lt;code&gt;unlock_time&lt;/code&gt; on incoming transfers. According to the &lt;a href="https://www.getmonero.org/2026/05/10/deprecating-unlock-time.html" rel="noopener noreferrer"&gt;Monero blog&lt;/a&gt;, merchant software has lost money by assuming that received XMR was spendable when a custom unlock time said otherwise.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Check where FCMP++ stands before you plan around it
&lt;/h2&gt;

&lt;p&gt;Several explainers currently online state that Monero's FCMP++ upgrade went live in January 2026. The project's own channels say otherwise.&lt;/p&gt;

&lt;p&gt;Here is the status as of October 6, 2026. A beta stressnet release tagged &lt;code&gt;v0.19.0.0-beta.3.0&lt;/code&gt; came out on September 25, with that test network scheduled to fork from testnet on October 5 at block 3,102,800. It is separate from mainnet (&lt;a href="https://www.tokenpost.com/news/technology/25090" rel="noopener noreferrer"&gt;TokenPost&lt;/a&gt;). The newest mainnet software is 0.18.5.3, published on October 6 as a bug-fix release in the current series (&lt;a href="https://www.getmonero.org/2026/10/06/monero-0.18.5.3-released.html" rel="noopener noreferrer"&gt;getmonero.org&lt;/a&gt;). The last official word on timing, from May 10, was that no FCMP++ fork date had been set (same Monero blog post as above).&lt;/p&gt;

&lt;p&gt;FCMP++ replaces ring signatures, where the real input hides among 16 outputs, with a membership proof over the whole set of more than 150 million outputs. CARROT, the addressing upgrade shipping with it, is designed to stay compatible with existing addresses. For anyone maintaining swap or payment code, the practical points are these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Custom transaction unlock times are being removed at consensus with the fork. A relay rule already rejects new transactions that set one.&lt;/li&gt;
&lt;li&gt;The 10-block lock keeps its role. Rucknium's analysis describes a trade-off: the privacy leak that follows a reorg today goes away, but every transaction caught in a reorg deeper than the lock would be invalidated, where at present only some are.&lt;/li&gt;
&lt;li&gt;The current stressnet build ships without transaction proofs, multisig and hardware wallet support. If your order page relies on &lt;code&gt;check_tx_key&lt;/code&gt;, put that flow on the retest list for the day a mainnet release candidate appears.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Holds and availability are states too
&lt;/h2&gt;

&lt;p&gt;The TechBullion guide makes a point that product teams often leave out of their diagrams: a swap without an account can still involve identity checks. Verification on such services is risk-based. A flagged transfer is put on hold, and the user may be asked for documents before the order continues.&lt;/p&gt;

&lt;p&gt;From the user's side an unexplained hold looks exactly like a lost payment. That is why &lt;code&gt;on_hold&lt;/code&gt; is a separate state in the model above, with a reason, a next action and a deadline attached. It should never be rendered as a longer "exchanging".&lt;/p&gt;

&lt;p&gt;Availability deserves the same treatment. From July 10, 2027, Article 79 of the EU Anti-Money Laundering Regulation prohibits crypto-asset service providers from keeping accounts that allow the anonymisation of transactions, including through anonymity-enhancing coins (&lt;a href="https://eur-lex.europa.eu/eli/reg/2024/1624/oj" rel="noopener noreferrer"&gt;EUR-Lex&lt;/a&gt;). The text names no specific coins and addresses providers, not people holding XMR in their own wallets. In code, pair availability becomes configuration by jurisdiction with an effective date, and the "not available in your region" answer has to come before a deposit address is shown.&lt;/p&gt;

&lt;h2&gt;
  
  
  A note on atomic swaps
&lt;/h2&gt;

&lt;p&gt;Swap services are one way to connect the two chains. Atomic swaps are another. Monero has no scripting, so the hashed timelock contracts used elsewhere don't apply. Joël Gugger's 2020 protocol and the COMIT team's follow-up work solve this with adaptor signatures on the Bitcoin side (&lt;a href="https://eprint.iacr.org/2020/1126" rel="noopener noreferrer"&gt;ePrint 2020/1126&lt;/a&gt;, &lt;a href="https://arxiv.org/abs/2101.12332" rel="noopener noreferrer"&gt;arXiv 2101.12332&lt;/a&gt;). The state machine gets longer, with lock, redeem, cancel and refund steps on the Bitcoin side. The observability rules from this post stay the same: the Bitcoin half is public, and the Monero half has to be checked from a wallet.&lt;/p&gt;

&lt;h2&gt;
  
  
  A checklist for a swap status page
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Every order has an explicit state, and "pending" is not one of them.&lt;/li&gt;
&lt;li&gt;[ ] Backward transitions exist for a replaced deposit, an unmined payout and a reorg.&lt;/li&gt;
&lt;li&gt;[ ] A mempool sighting is shown as "seen" and never triggers the payout.&lt;/li&gt;
&lt;li&gt;[ ] Bitcoin progress is displayed as "n of N confirmations".&lt;/li&gt;
&lt;li&gt;[ ] The XMR transaction ID is shown, and the transaction key is stored and available on request.&lt;/li&gt;
&lt;li&gt;[ ] "Arrived" and "spendable" are separate, with a countdown for the 10-block lock.&lt;/li&gt;
&lt;li&gt;[ ] Confirmation thresholds live in configuration and depend on the amount.&lt;/li&gt;
&lt;li&gt;[ ] Polling continues after "spendable", and a vanished payout is looked up again before it is re-sent.&lt;/li&gt;
&lt;li&gt;[ ] A hold has its own state with a reason and a next step.&lt;/li&gt;
&lt;li&gt;[ ] Pair availability is checked per jurisdiction before the deposit address appears.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Why can't I look up my XMR payout in a block explorer?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Monero hides the sender, the recipient and the amount of every transaction. An explorer can tell you that a transaction ID exists and how many confirmations it has, and nothing more. To confirm that a payout reached you, open your own synced wallet, which recognizes incoming outputs with your keys. If nothing shows up, ask the sender for the transaction ID and the transaction key. With those two values and your address, the Prove/Check tool in the Monero GUI, or &lt;code&gt;check_tx_key&lt;/code&gt; over RPC, shows how much that transaction paid to you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Are 10 confirmations final on Monero?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ten blocks is the point where received outputs become spendable under the consensus rules. Finality is a separate question that each service answers for itself. The 18-block reorganization of September 14, 2025 went deeper than the lock and invalidated 115 transactions that already looked settled. Services reacted by raising their own thresholds, in Kraken's case to 720 confirmations for deposits. If you accept XMR, pick a threshold that fits the amount at risk, keep it configurable and be ready to move an order back a state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is FCMP++ live on Monero mainnet?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. As of October 6, 2026, FCMP++ and CARROT run only on a beta stressnet, whose fork from testnet was scheduled for October 5. Mainnet runs the 0.18 series, and the latest release, 0.18.5.3, is a bug-fix update. The Monero blog stated in May 2026 that no fork date had been set, and I found no later announcement of one. Before changing wallet or payment code for the upgrade, check the blog on getmonero.org and the release notes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing thoughts
&lt;/h2&gt;

&lt;p&gt;A Bitcoin to Monero swap takes away the shortcut of pointing people to an explorer, so the order page has to say what the service knows at each step. I think that discipline is worth keeping for transfers where an explorer link does exist, because most users never open it.&lt;/p&gt;

&lt;p&gt;Which states does your product show between "sent" and "done"? I'd like to compare notes in the comments.&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>bitcoin</category>
      <category>fintech</category>
      <category>web3</category>
    </item>
    <item>
      <title>Irreversible by Design: Building Safer Crypto Transfer Flows</title>
      <dc:creator>Azalea</dc:creator>
      <pubDate>Thu, 01 Oct 2026 16:33:31 +0000</pubDate>
      <link>https://dev.to/azaleakuts/irreversible-by-design-building-safer-crypto-transfer-flows-3fg6</link>
      <guid>https://dev.to/azaleakuts/irreversible-by-design-building-safer-crypto-transfer-flows-3fg6</guid>
      <description>&lt;p&gt;On December 20, 2025, a trader withdrew USDT from Binance and did everything the security guides recommend. They sent a 50 USDT test transfer to their own wallet first. About 26 minutes later they sent the remaining 49,999,950 USDT, and it landed in a scammer's wallet.&lt;/p&gt;

&lt;p&gt;The test transfer was the trigger. An automated script spotted it, generated an address with the same first five and last four characters, and pushed a tiny transaction into the victim's history. When the trader came back to send the full amount, they copied the address from that history. The two addresses differed only in the middle characters, the part most wallet UIs hide behind an ellipsis. Within 30 minutes the funds were swapped to DAI, then ETH, then sent through Tornado Cash (&lt;a href="https://www.theblock.co/news/ecosystems/2025-12-20-crypto-trader-loses-50-million-in-address-poisoning-attack-offers-1-million-bounty-for-return-383423" rel="noopener noreferrer"&gt;The Block&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;No bank would let a $50M transfer go through on a lookalike account number without a second check. On a blockchain there is no chargeback and no operator who can roll a confirmed transaction back. The only place to catch the mistake is the interface where the user pastes an address and taps Send, and that interface is something we, as developers, build.&lt;/p&gt;

&lt;p&gt;Users notice the difference. A recent overview of &lt;a href="https://www.davestravelcorner.com/journals/destination-other/from-branches-to-blockchains-where-digital-money-habits-are-heading-in-2026/" rel="noopener noreferrer"&gt;where digital money habits are heading in 2026&lt;/a&gt; points out that people now choose a crypto exchange or swap service the way they choose a banking app: they compare fees, KYC requirements and supported assets. The same piece makes a point that matters for anyone shipping a send screen. People look for the platform where a wrong-network or wrong-address transfer is harder to make, and the lowest fee is only part of the decision.&lt;/p&gt;

&lt;p&gt;This post walks through four layers of protection you can build into a crypto transfer flow, with code you can adapt:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Validate addresses properly.&lt;/li&gt;
&lt;li&gt;Bind every address to a network, memo and tag included.&lt;/li&gt;
&lt;li&gt;Defend against address poisoning and clipboard swaps.&lt;/li&gt;
&lt;li&gt;Simulate and preview the transaction before the user signs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A checksum catches typos, but a poisoned lookalike address passes every checksum.&lt;/li&gt;
&lt;li&gt;Most recoverable mistakes are the ones where the user or an exchange still holds the destination key; transfers to a stranger's address are almost always lost.&lt;/li&gt;
&lt;li&gt;Wallets like MetaMask and explorers like Etherscan already ship lookalike-address warnings and hide zero-value transfers, so users will expect the same from your app.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;eth_simulateV1&lt;/code&gt; lets you show what a transaction will do before it is signed, on Ethereum and a growing list of other chains.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A taxonomy of transfer mistakes
&lt;/h2&gt;

&lt;p&gt;Whether a mistaken transfer can be recovered depends on one question: who controls the private key of the destination address? If it is the user or an exchange, there is often a path back. If it is a stranger, the funds are almost always gone. The table below maps the common mistakes to that question and to the place in your code where each one can be stopped.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mistake&lt;/th&gt;
&lt;th&gt;What actually happens&lt;/th&gt;
&lt;th&gt;Recovery odds&lt;/th&gt;
&lt;th&gt;Where you stop it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Typo in the address&lt;/td&gt;
&lt;td&gt;Funds go to an address nobody controls, or to a stranger&lt;/td&gt;
&lt;td&gt;Close to zero&lt;/td&gt;
&lt;td&gt;Checksum validation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Address poisoning&lt;/td&gt;
&lt;td&gt;User copies a lookalike address from their own history&lt;/td&gt;
&lt;td&gt;Close to zero&lt;/td&gt;
&lt;td&gt;Full address display, lookalike detection, hiding dust&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clipboard hijacking&lt;/td&gt;
&lt;td&gt;Malware swaps the copied address for the attacker's&lt;/td&gt;
&lt;td&gt;Close to zero&lt;/td&gt;
&lt;td&gt;Showing the pasted address in full, hardware wallet confirmation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wrong network, own address (EVM to EVM)&lt;/td&gt;
&lt;td&gt;Funds sit on the same 0x address on another chain&lt;/td&gt;
&lt;td&gt;Usually high, the user adds the network to their wallet&lt;/td&gt;
&lt;td&gt;Explicit network selection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wrong network, exchange address&lt;/td&gt;
&lt;td&gt;The exchange received the funds but did not credit them&lt;/td&gt;
&lt;td&gt;Depends on the exchange, often a fee and days or weeks of waiting&lt;/td&gt;
&lt;td&gt;Deposit addresses bound to asset plus network&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Missing memo or destination tag&lt;/td&gt;
&lt;td&gt;Funds land in the exchange's shared omnibus account with no owner attached&lt;/td&gt;
&lt;td&gt;Usually possible through support&lt;/td&gt;
&lt;td&gt;Mandatory memo field, protocol-level flags&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tokens sent to a token contract&lt;/td&gt;
&lt;td&gt;Tokens get stuck in a contract that cannot send them out&lt;/td&gt;
&lt;td&gt;Close to zero&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;eth_getCode&lt;/code&gt; check and a warning&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The memo row shows how expensive a single missing input can be. When a deposit arrives without the required memo, Blockchain.com may ask the user to make another minimum deposit of 1 XLM or XRP from the same address, this time with the correct memo, to prove ownership before any funds are returned (&lt;a href="https://support.blockchain.com/hc/en-us/articles/8219099905436-Crypto-Recovery-for-Unsupported-or-Incorrect-Transfers" rel="noopener noreferrer"&gt;Blockchain.com Support&lt;/a&gt;). A required field on the send screen would have avoided the whole support ticket.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 1. Validate addresses properly
&lt;/h2&gt;

&lt;p&gt;Format validation is the minimum, and it only protects against typos. A checksum tells you the string was not mistyped. It says nothing about whether the address belongs to the person the user meant to pay, because a poisoned lookalike address is a perfectly valid address with a perfectly valid checksum.&lt;/p&gt;

&lt;p&gt;Each chain family handles this differently:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Chain / format&lt;/th&gt;
&lt;th&gt;How the checksum works&lt;/th&gt;
&lt;th&gt;What it guarantees&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ethereum and EVM chains (EIP-55)&lt;/td&gt;
&lt;td&gt;Mixed-case hex: a letter is uppercase if the matching bit of the keccak256 hash is 1&lt;/td&gt;
&lt;td&gt;About 15 check bits; a random typo slips through 0.0247% of the time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bitcoin legacy and P2SH (Base58Check)&lt;/td&gt;
&lt;td&gt;Base58 plus 4 bytes of double SHA-256&lt;/td&gt;
&lt;td&gt;Typos are almost always caught&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bitcoin SegWit (Bech32) and Taproot (Bech32m)&lt;/td&gt;
&lt;td&gt;Lowercase only, no lookalike characters like 1, b, i, o&lt;/td&gt;
&lt;td&gt;Detects any error affecting up to 4 characters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tron&lt;/td&gt;
&lt;td&gt;Base58Check with a 0x41 version byte, addresses start with T&lt;/td&gt;
&lt;td&gt;Checksum included&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Solana&lt;/td&gt;
&lt;td&gt;Base58-encoded 32-byte ed25519 public key&lt;/td&gt;
&lt;td&gt;No checksum at all&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The EIP-55 figures come from the &lt;a href="https://eips.ethereum.org/EIPS/eip-55" rel="noopener noreferrer"&gt;ERC-55 specification&lt;/a&gt;. The Solana row deserves a warning in your UI: a typo there can produce another valid address, so format validation tells the user nothing about correctness.&lt;/p&gt;

&lt;p&gt;A few rules that save real money:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A regex like &lt;code&gt;^0x[a-fA-F0-9]{40}$&lt;/code&gt; is not validation. It accepts addresses with a broken checksum.&lt;/li&gt;
&lt;li&gt;A mixed-case address with an invalid checksum is an error. Do not silently lowercase it and continue.&lt;/li&gt;
&lt;li&gt;Validate the address against the selected network. A Bitcoin address in a BSC field should fail even though the string is valid somewhere.&lt;/li&gt;
&lt;li&gt;Check whether the recipient is a contract (&lt;code&gt;eth_getCode&lt;/code&gt; returns something other than &lt;code&gt;0x&lt;/code&gt;) and warn before sending tokens to a token contract.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With viem, the EVM part takes a few lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;isAddress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;getAddress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;createPublicClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;http&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;viem&lt;/span&gt;&lt;span class="dl"&gt;'&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;mainnet&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;viem/chains&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createPublicClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;mainnet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;http&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;validateEvmRecipient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;isAddress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;strict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt; &lt;span class="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;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid address or checksum&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;address&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getAddress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// checksummed form to show back to the user&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;address&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isContract&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0x&lt;/span&gt;&lt;span class="dl"&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;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isContract&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;For other chains, use the chain's own library: bitcoinjs-lib (&lt;code&gt;address.toOutputScript(addr, network)&lt;/code&gt;), TronWeb (&lt;code&gt;isAddress&lt;/code&gt;), &lt;code&gt;@solana/web3.js&lt;/code&gt; (&lt;code&gt;new PublicKey()&lt;/code&gt;) and ripple-address-codec for XRP. Generic multi-coin validators are handy for a first pass, but they check format only and know nothing about the network the user picked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 2. Bind every address to a network
&lt;/h2&gt;

&lt;p&gt;Hundreds of L2s use the same address format as Ethereum mainnet, so a &lt;code&gt;0x&lt;/code&gt; address on its own no longer tells you which chain it belongs to. Your send flow has to make the network an explicit, visible part of the destination, and for some assets the memo or tag too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In the interface&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Treat the network as a required choice. For large amounts, avoid a preselected default.&lt;/li&gt;
&lt;li&gt;Show the network name and icon on the confirmation screen right next to the address, not tucked away in a header.&lt;/li&gt;
&lt;li&gt;Ask for the receiving side's network first. The recipient knows which network their platform supports; the sender usually picks whatever is cheapest.&lt;/li&gt;
&lt;li&gt;On exchanges and swap services, bind each deposit address to an asset plus network pair.&lt;/li&gt;
&lt;li&gt;Use the address format to narrow the choice. &lt;code&gt;T…&lt;/code&gt; means Tron, &lt;code&gt;bc1…&lt;/code&gt; means Bitcoin, &lt;code&gt;0x…&lt;/code&gt; means some EVM chain, and in that last case you still ask which one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Memo and destination tags&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;XRP, XLM, TON, ATOM and EOS deposits to exchanges usually need a memo or tag, because the exchange routes everyone's deposits through one shared address. Make the field mandatory when the recipient is an exchange, and require an explicit "this recipient does not need a memo" confirmation to skip it. Validate the format as well: an XRP destination tag is an unsigned 32-bit integer (0 to 4,294,967,295), and a Stellar text memo is limited to 28 bytes.&lt;/p&gt;

&lt;p&gt;Both ledgers also give you protocol-level help:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;XRP Ledger.&lt;/strong&gt; An account can set the &lt;code&gt;RequireDest&lt;/code&gt; flag, and the network then rejects payments without a destination tag. Query &lt;code&gt;account_info&lt;/code&gt; before sending and enforce the tag in your UI when the flag is set.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stellar SEP-29.&lt;/strong&gt; An account can publish a &lt;code&gt;config.memo_required&lt;/code&gt; data entry, and wallets are expected to check it before submitting a payment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;XRP X-addresses.&lt;/strong&gt; They pack the classic address and the tag into one string, so the user cannot forget the tag.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Where the standards are heading&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Two Ethereum proposals tackle the network problem at the address level. &lt;a href="https://eips.ethereum.org/EIPS/eip-7930" rel="noopener noreferrer"&gt;ERC-7930&lt;/a&gt; defines a binary format that carries the chain type, chain reference and address together, and it covers non-EVM chains too. &lt;a href="https://eips.ethereum.org/EIPS/eip-7828" rel="noopener noreferrer"&gt;ERC-7828&lt;/a&gt; adds a human-readable form, &lt;code&gt;&amp;lt;address&amp;gt;@&amp;lt;chain&amp;gt;#&amp;lt;checksum&amp;gt;&lt;/code&gt;, so something like &lt;code&gt;alice.eth@base&lt;/code&gt; names both the account and the chain. Its optional checksum is meant to help against homoglyph and spoofing attacks. Both are still drafts, so treat them as a direction to watch and keep your own network checks in place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 3. Defend against address poisoning and clipboard swaps
&lt;/h2&gt;

&lt;p&gt;Address poisoning exploits truncated addresses: users compare the first and last few characters, and the attacker matches exactly those. Nobody can stop a stranger from sending dust to a public address, so the defense lives entirely in the wallet and app UI.&lt;/p&gt;

&lt;p&gt;The scale is real. Researchers counted 270 million poisoning attempts and 50 million lookalike addresses on Ethereum and BNB Smart Chain, with about $83.8M in confirmed losses across more than 6,600 cases (&lt;a href="https://arxiv.org/html/2501.16681v1" rel="noopener noreferrer"&gt;arXiv 2501.16681&lt;/a&gt;). On Bitcoin, Casa's Jameson Lopp identified 48,000 suspected attacks since 2023 (&lt;a href="https://www.theblock.co/news/ecosystems/2025-12-20-crypto-trader-loses-50-million-in-address-poisoning-attack-offers-1-million-bounty-for-return-383423" rel="noopener noreferrer"&gt;The Block&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Attackers use three techniques (&lt;a href="https://arxiv.org/pdf/2607.24172" rel="noopener noreferrer"&gt;arXiv 2607.24172&lt;/a&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero-value transfers.&lt;/strong&gt; A &lt;code&gt;transferFrom&lt;/code&gt; call for 0 tokens emits a real-looking Transfer event without moving anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dust transfers.&lt;/strong&gt; A negligible amount of ETH or tokens makes the entry look more legitimate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fake-token transfers.&lt;/strong&gt; An attacker-deployed ERC-20 mimics the amount of the victim's real transaction.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Large players already ship countermeasures, which sets user expectations for everyone else. MetaMask shows a blocking warning when the destination closely resembles an address from the user's history, for example when the first four and last four characters match and the middle differs, plus a separate warning for first-time recipients (&lt;a href="https://support.metamask.io/stay-safe/protect-yourself/wallet-and-hardware/address-poisoning-scams/" rel="noopener noreferrer"&gt;MetaMask Support&lt;/a&gt;). In September 2026 it expanded those warnings to mobile and the extension and added a feature that reverts transactions that do not match their preview (&lt;a href="https://decrypt.co/378117/metamask-adds-wallet-protections" rel="noopener noreferrer"&gt;Decrypt&lt;/a&gt;). Etherscan hides zero-value transfers and low-reputation tokens by default, highlights similar addresses and shows more characters of each address (&lt;a href="https://info.etherscan.com/what-is-address-poisoning/" rel="noopener noreferrer"&gt;Etherscan&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;What to build into your own flow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Never truncate the address on the confirmation screen. In lists, show at least six characters on each side.&lt;/li&gt;
&lt;li&gt;Hide or flag zero-value transfers, dust and transfers of unknown tokens in the transaction history.&lt;/li&gt;
&lt;li&gt;Do not offer a one-tap "copy address" on incoming transactions from unknown senders. Point users to the address book instead.&lt;/li&gt;
&lt;li&gt;Compare the recipient against history and contacts, and block the send when the edges match but the full address does not.&lt;/li&gt;
&lt;li&gt;Warn on the first transfer to any new address, and more loudly for large amounts.&lt;/li&gt;
&lt;li&gt;Against clipboard malware, show the pasted address in full and ask the user to check it against the source. For large amounts, confirm on a hardware wallet screen.&lt;/li&gt;
&lt;li&gt;After a test transfer, take the address for the main transfer from the address book, never from history. The $50M case shows why.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A minimal lookalike check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Flags a recipient that shares its first and last characters&lt;/span&gt;
&lt;span class="c1"&gt;// with a known address but is not the same address&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;findLookalike&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;recipient&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;known&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="nx"&gt;edge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&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;norm&lt;/span&gt; &lt;span class="o"&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^0x/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;recipient&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;known&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="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return &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;r&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
      &lt;span class="nx"&gt;n&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="nx"&gt;edge&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="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="nx"&gt;edge&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
      &lt;span class="nx"&gt;n&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="nx"&gt;edge&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="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="nx"&gt;edge&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="c1"&gt;// Usage: block the send and show both addresses side by side&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;findLookalike&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;addressBook&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;recentRecipients&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;match&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;showBlockingWarning&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;entered&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;similarTo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;match&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;In production you would also compare a few more characters (attackers generate vanity addresses with longer matching edges as they get cheaper to compute) and highlight the differing middle part in the warning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 4. Simulate and preview before signing
&lt;/h2&gt;

&lt;p&gt;The confirmation screen is the last moment a mistake can be caught, so it should describe what will happen on-chain rather than echo what the user typed. Transaction simulation gives you that description.&lt;/p&gt;

&lt;p&gt;Two RPC methods do most of the work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;eth_call&lt;/code&gt;&lt;/strong&gt; answers "will this succeed?". If the call reverts, the real transaction would revert with the same reason.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;eth_simulateV1&lt;/code&gt;&lt;/strong&gt; runs one transaction or a chain of them (approve plus swap, for example) on top of a block's state, supports state overrides and transfer tracing, and returns logs you can decode into a human-readable diff of balance changes (&lt;a href="https://docs.chainstack.com/docs/ethereum-simulate-a-uniswap-swap" rel="noopener noreferrer"&gt;Chainstack Docs&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Support goes beyond Ethereum mainnet. RPC providers expose &lt;code&gt;eth_simulateV1&lt;/code&gt; on chains like Arbitrum and Monad, and java-tron has a pull request implementing a base version specifically so wallets can show users token movements before they sign (&lt;a href="https://github.com/tronprotocol/java-tron/pull/6785" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;JsonRpcProvider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Interface&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;parseUnits&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;ethers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;JsonRpcProvider&lt;/span&gt;&lt;span class="p"&gt;(&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;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RPC_URL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;erc20&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Interface&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;function transfer(address to, uint256 amount)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;previewTokenTransfer&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;decimals&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;erc20&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encodeFunctionData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;transfer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;parseUnits&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;decimals&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;eth_simulateV1&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="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;blockStateCalls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;calls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
      &lt;span class="na"&gt;traceTransfers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;validation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;latest&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;call&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;calls&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;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;willSucceed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0x1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;gasUsed&lt;/span&gt;&lt;span class="p"&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;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gasUsed&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;logs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;logs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// decode Transfer events into a "you send / they receive" summary&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;Note the &lt;code&gt;decimals&lt;/code&gt; parameter. USDT and USDC on EVM chains usually use 6 decimals while most ERC-20 tokens use 18, so read &lt;code&gt;decimals()&lt;/code&gt; from the contract instead of hardcoding a table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What the confirmation screen should show&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The full recipient address in checksummed form, with the address book label if there is one.&lt;/li&gt;
&lt;li&gt;The network name and icon, next to the address.&lt;/li&gt;
&lt;li&gt;The memo or tag, or a clear note that none is set when the recipient is an exchange.&lt;/li&gt;
&lt;li&gt;The amount in tokens and in fiat, the network fee and the final "you send / they receive" figures from the simulation.&lt;/li&gt;
&lt;li&gt;Warnings for a first-time recipient, a lookalike address, a contract address, or an amount far above the user's usual transfers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Extra safeguards worth adding&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A built-in test transfer flow: send a small amount, save the address, then send the rest to the saved entry.&lt;/li&gt;
&lt;li&gt;Withdrawal allowlists with a waiting period before a newly added address becomes usable, a pattern many exchanges already use.&lt;/li&gt;
&lt;li&gt;For ENS and other names, show the resolved address, normalize names per ENSIP-15 to block homoglyphs, and resolve the name again right before signing.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A checklist for your next send screen
&lt;/h2&gt;

&lt;p&gt;Copy this into your next PR description or design review:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Addresses are validated with the chain's checksum, not a regex.&lt;/li&gt;
&lt;li&gt;[ ] Invalid mixed-case EVM addresses are rejected, not lowercased.&lt;/li&gt;
&lt;li&gt;[ ] The address is validated against the selected network.&lt;/li&gt;
&lt;li&gt;[ ] Solana recipients get an extra "double-check this address" prompt, since the format has no checksum.&lt;/li&gt;
&lt;li&gt;[ ] Contract recipients trigger a warning (&lt;code&gt;eth_getCode&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;[ ] Network selection is explicit and shown next to the address on the confirmation screen.&lt;/li&gt;
&lt;li&gt;[ ] Memo or tag is mandatory for exchange deposits on XRP, XLM, TON, ATOM and EOS; &lt;code&gt;RequireDest&lt;/code&gt; and SEP-29 are checked.&lt;/li&gt;
&lt;li&gt;[ ] Zero-value, dust and unknown-token transfers are hidden or flagged in history.&lt;/li&gt;
&lt;li&gt;[ ] Lookalike recipients are blocked with both addresses shown in full.&lt;/li&gt;
&lt;li&gt;[ ] First-time recipients and unusually large amounts trigger a warning.&lt;/li&gt;
&lt;li&gt;[ ] The transaction is simulated, and the confirmation screen shows the resulting balance changes.&lt;/li&gt;
&lt;li&gt;[ ] Token amounts use &lt;code&gt;decimals()&lt;/code&gt; from the contract.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Does a test transaction protect users from address poisoning?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;On its own, no, and it can make things worse. Poisoning bots watch for small outgoing transfers and respond within minutes with a lookalike address that lands in the sender's history. In the December 2025 case, the attack followed a 50 USDT test transfer. A test transaction protects against typos and wrong-network mistakes. To protect against poisoning, the address for the main transfer has to come from a saved contact or the original source, never from the transaction history. If your app offers a test-transfer flow, save the address after the test and reuse that saved entry for the full amount.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can funds sent on the wrong network be recovered?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It depends on who holds the key to the destination address. If a user sent tokens to their own 0x address on a different EVM chain, they can usually add that network to their wallet and access the funds. If the destination was an exchange deposit address, only the exchange can help, and the outcome depends on its policy: some recover deposits for a fee and after days or weeks, some cannot. Funds sent to a stranger's address on any network are almost always lost. That is why network selection belongs in the UI before the transaction, not in the support queue after it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is checksum validation enough to stop mistakes?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. A checksum catches accidental typos, and for EIP-55 a random typo passes the check only about 0.0247% of the time. Address poisoning and clipboard malware use real, valid addresses that the attacker controls, so they pass every checksum. Validation is the first of several layers. You still need network binding, lookalike detection against the user's history and contacts, a full-address confirmation screen and, ideally, a simulation of what the transaction will do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing thoughts
&lt;/h2&gt;

&lt;p&gt;Users already judge crypto platforms by fees, verification and supported assets, and safety at the moment of sending is part of that comparison. Every layer in this post is a few dozen lines of code or a single design decision. Put together, they turn "transactions are irreversible" from a disclaimer into a constraint your send flow is built around.&lt;/p&gt;

&lt;p&gt;What does your team do on the send screen that is not on this list? Share it in the comments.&lt;/p&gt;

</description>
      <category>web3</category>
      <category>blockchain</category>
      <category>fintech</category>
      <category>ai</category>
    </item>
    <item>
      <title>What Changed in Crypto Product Design in 2026?</title>
      <dc:creator>Azalea</dc:creator>
      <pubDate>Wed, 23 Sep 2026 14:48:18 +0000</pubDate>
      <link>https://dev.to/azaleakuts/what-changed-in-crypto-product-design-in-2026-2i38</link>
      <guid>https://dev.to/azaleakuts/what-changed-in-crypto-product-design-in-2026-2i38</guid>
      <description>&lt;p&gt;Crypto apps are becoming easier to use because more blockchain complexity is moving out of the interface.&lt;/p&gt;

&lt;p&gt;Users still interact with wallets, networks and transactions, but the product increasingly handles the technical steps behind them. A broader overview of &lt;a href="https://techiadd.it.com/crypto-apps-in-2026-whats-actually-changed-for-everyday-users/?k=daa817bf807fdabbf32c28f6c54bd817" rel="noopener noreferrer"&gt;what changed for everyday crypto app users in 2026&lt;/a&gt; points to the same shift across authentication, custody and payments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Passkeys and embedded wallets simplify onboarding.&lt;/li&gt;
&lt;li&gt;Smart accounts can combine several blockchain actions into one flow.&lt;/li&gt;
&lt;li&gt;Gas sponsorship reduces the need to manage network tokens manually.&lt;/li&gt;
&lt;li&gt;Chain abstraction hides some multichain complexity.&lt;/li&gt;
&lt;li&gt;Stablecoins are being integrated into familiar payment flows.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Passkeys Are Changing Onboarding&lt;/p&gt;

&lt;p&gt;Crypto onboarding has often required passwords, email verification and separate wallet setup.&lt;/p&gt;

&lt;p&gt;Passkeys offer a simpler authentication flow based on public-key cryptography. Users can authenticate with a device PIN, fingerprint or face recognition instead of entering a password. Google documents passkeys as a phishing-resistant alternative to traditional password login.&lt;/p&gt;

&lt;p&gt;Embedded wallets take this further by putting wallet functionality inside the application. A user can create or access an onchain wallet without starting with a separate wallet installation and connection flow.&lt;/p&gt;

&lt;p&gt;For developers, authentication and wallet creation are becoming part of the same product experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Smart Accounts Reduce Manual Steps
&lt;/h2&gt;

&lt;p&gt;Traditional Ethereum wallets expose many blockchain operations directly to the user.&lt;/p&gt;

&lt;p&gt;Smart account infrastructure allows developers to combine transactions, sponsor gas and introduce more flexible permissions.&lt;/p&gt;

&lt;p&gt;A flow that previously required several approvals can sometimes become a single user action. The underlying transactions still happen, but the interface can present the whole operation as one task.&lt;/p&gt;

&lt;p&gt;This makes the application responsible for more of the transaction flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gas Can Move Into the Background
&lt;/h2&gt;

&lt;p&gt;Users should not need to understand gas mechanics just to complete a basic action.&lt;/p&gt;

&lt;p&gt;Account abstraction supports sponsored transactions, allowing an application or another party to cover transaction costs in supported flows. EIP-7702 also introduces mechanisms for sponsored execution.&lt;/p&gt;

&lt;p&gt;The fee still exists. The product can decide how it is handled and presented instead of forcing every user to maintain a balance of the network's native token.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chain Switching Is Becoming Less Visible
&lt;/h2&gt;

&lt;p&gt;Multichain products add another layer of friction. An asset can be available on one network while the user needs it on another.&lt;/p&gt;

&lt;p&gt;Chain abstraction and intent-based systems aim to hide some of this complexity. Instead of selecting networks, managing bridges and handling routing manually, users can express the outcome they want while the infrastructure handles execution.&lt;/p&gt;

&lt;p&gt;For developers, this means designing around the user's goal rather than exposing every blockchain step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stablecoin Payments Look More Like Fintech
&lt;/h2&gt;

&lt;p&gt;Stablecoins are increasingly being integrated into payment products rather than presented as a separate crypto experience.&lt;/p&gt;

&lt;p&gt;Stripe, for example, is expanding infrastructure around stablecoin payments, payouts and cards. The customer-facing flow can remain familiar while blockchain settlement happens underneath.&lt;/p&gt;

&lt;p&gt;A payment interface does not need to expose every wallet, network and settlement detail to the customer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compliance Is Part of the Product
&lt;/h2&gt;

&lt;p&gt;Regulation also affects product design.&lt;/p&gt;

&lt;p&gt;Jurisdiction, identity verification, asset availability and transaction limits can change what a user is allowed to do. Those requirements appear directly in onboarding and transaction flows.&lt;/p&gt;

&lt;p&gt;Developers therefore have to account for compliance when designing the product itself, not only during backend implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Design Problem Has Changed
&lt;/h2&gt;

&lt;p&gt;Crypto products are moving toward a simple model:&lt;/p&gt;

&lt;p&gt;User chooses the outcome.&lt;br&gt;
The product handles more of the mechanics.&lt;/p&gt;

&lt;p&gt;Passkeys handle authentication. Embedded wallets reduce onboarding friction. Smart accounts handle more complex transactions. Sponsored gas removes one manual step. Chain abstraction hides network differences. Stablecoin infrastructure can make blockchain payments look like ordinary fintech.&lt;/p&gt;

&lt;p&gt;The challenge for developers is deciding what to hide and what users still need to see before they approve an action.&lt;/p&gt;

&lt;p&gt;The blockchain can stay in the backend without making the product feel opaque.&lt;/p&gt;

</description>
      <category>crypto</category>
      <category>fintech</category>
      <category>web3</category>
      <category>blockchain</category>
    </item>
    <item>
      <title>Your Crypto Wallet Is Not Your Account: Understanding What the App Actually Controls</title>
      <dc:creator>Azalea</dc:creator>
      <pubDate>Mon, 21 Sep 2026 23:23:13 +0000</pubDate>
      <link>https://dev.to/azaleakuts/your-crypto-wallet-is-not-your-account-understanding-what-the-app-actually-controls-1ffp</link>
      <guid>https://dev.to/azaleakuts/your-crypto-wallet-is-not-your-account-understanding-what-the-app-actually-controls-1ffp</guid>
      <description>&lt;p&gt;A crypto wallet can look a lot like a regular banking app. You open it, see a balance, copy an address, and press Send.&lt;/p&gt;

&lt;p&gt;But your wallet app is not your blockchain account, and your crypto is not literally stored inside the app.&lt;/p&gt;

&lt;p&gt;Understanding this difference makes crypto much easier to reason about – especially when something goes wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The wallet is the interface
&lt;/h2&gt;

&lt;p&gt;On Ethereum, an account is an on-chain entity that can hold ETH and tokens and interact with smart contracts. An externally owned account (EOA) is controlled by private keys.&lt;/p&gt;

&lt;p&gt;The wallet app is the software you use to interact with that account.&lt;/p&gt;

&lt;p&gt;It can:&lt;/p&gt;

&lt;p&gt;show your balance by reading blockchain data;&lt;br&gt;
create transactions;&lt;br&gt;
ask you to approve an action;&lt;br&gt;
sign transactions;&lt;br&gt;
broadcast them to the network.&lt;/p&gt;

&lt;p&gt;The assets themselves remain recorded on the blockchain. Ethereum's documentation puts it simply: users don't store the cryptocurrency inside the wallet – they control the keys that allow them to act on the account.&lt;/p&gt;

&lt;p&gt;So deleting a wallet app does not automatically delete your crypto.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then what actually gives you control?
&lt;/h2&gt;

&lt;p&gt;For a typical self-custodial wallet, the important pieces are the Secret Recovery Phrase and the private keys.&lt;/p&gt;

&lt;p&gt;A recovery phrase can be used to restore the wallet and generate its accounts. Each account has its own private key, which is used to control that particular account.&lt;/p&gt;

&lt;p&gt;That's why your wallet password is not the same thing as ownership of the assets.&lt;/p&gt;

&lt;p&gt;For example, in MetaMask, a password can unlock the wallet on a device, while the Secret Recovery Phrase is what allows the wallet to be restored.&lt;/p&gt;

&lt;p&gt;A useful mental model is:&lt;/p&gt;

&lt;p&gt;Wallet app → interface&lt;br&gt;
Account → blockchain identity&lt;br&gt;
Private key → authorization&lt;br&gt;
Blockchain → asset state&lt;/p&gt;

&lt;h2&gt;
  
  
  Self-custody changes the model
&lt;/h2&gt;

&lt;p&gt;This is also why a self-custody wallet feels different from an exchange account.&lt;/p&gt;

&lt;p&gt;With a self-custody wallet, you control the private keys. With a custodial exchange, the platform manages the keys and processes transactions on your behalf.&lt;/p&gt;

&lt;p&gt;That creates a very different recovery model.&lt;/p&gt;

&lt;p&gt;Forgot your exchange password? There may be a standard account recovery process.&lt;/p&gt;

&lt;p&gt;Lost the credentials that control a self-custodial wallet? There may be no company that can simply reset access for you. MetaMask explicitly warns that it cannot recover a wallet when the required recovery credentials are lost.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens when you press Send?
&lt;/h2&gt;

&lt;p&gt;The wallet does not physically move coins from one app to another.&lt;/p&gt;

&lt;p&gt;Instead, it prepares a transaction and asks you to authorize it. For a traditional Ethereum account, the transaction is signed with the account's private key and then submitted to the network.&lt;/p&gt;

&lt;p&gt;The blockchain processes that transaction and updates its state.&lt;/p&gt;

&lt;p&gt;This is why crypto security is about more than protecting an app password. The bigger question is:&lt;/p&gt;

&lt;p&gt;What exactly are you authorizing with your wallet?&lt;/p&gt;

&lt;p&gt;A wallet may be just the interface, but the signature you approve can change what happens to your assets.&lt;/p&gt;

&lt;h2&gt;
  
  
  One more complication: smart wallets
&lt;/h2&gt;

&lt;p&gt;The private-key model is not the only approach anymore.&lt;/p&gt;

&lt;p&gt;Ethereum also supports contract-based accounts, where authorization can be defined by smart-contract code rather than a single traditional private key. This is part of the broader account abstraction model.&lt;/p&gt;

&lt;p&gt;So the statement “your wallet is your private key” is useful as a beginner's shortcut, but it is no longer a complete description of every modern wallet.&lt;/p&gt;

&lt;p&gt;The simplest way to think about it is this:&lt;/p&gt;

&lt;p&gt;Your wallet is the tool you use to access and authorize actions. Your account is the identity on the blockchain. The blockchain records the assets and transactions.&lt;/p&gt;

&lt;p&gt;Once these pieces are separated, crypto wallets become much less mysterious – and the security risks become much easier to understand.&lt;/p&gt;

</description>
      <category>cryptocurrency</category>
      <category>fintech</category>
      <category>blockchain</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
