<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Swapcore.Exchange</title>
    <description>The latest articles on DEV Community by Swapcore.Exchange (@swapcoreexchange).</description>
    <link>https://dev.to/swapcoreexchange</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4118127%2Fbcdc6133-cb6c-4908-8a3e-3cbdbee4f891.jpg</url>
      <title>DEV Community: Swapcore.Exchange</title>
      <link>https://dev.to/swapcoreexchange</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/swapcoreexchange"/>
    <language>en</language>
    <item>
      <title>"USDT" Is at Least Three Different Tokens. Your Token Registry Should Know That.</title>
      <dc:creator>Swapcore.Exchange</dc:creator>
      <pubDate>Thu, 22 Oct 2026 20:47:40 +0000</pubDate>
      <link>https://dev.to/swapcoreexchange/usdt-is-at-least-three-different-tokens-your-token-registry-should-know-that-14h4</link>
      <guid>https://dev.to/swapcoreexchange/usdt-is-at-least-three-different-tokens-your-token-registry-should-know-that-14h4</guid>
      <description>&lt;p&gt;If your product handles USDT across networks, there's a good chance your token model treats it as one asset with several "networks." That model is wrong in ways that matter for accounting, risk and support.&lt;/p&gt;

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

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

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

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

&lt;p&gt;A registry keyed only by &lt;code&gt;(chain, contract)&lt;/code&gt; is necessary but not sufficient. Add the fields that describe what the token actually is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"symbol"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USDT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"chain"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arbitrum"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"contract"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0x…"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"decimals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"omnichain"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"issuer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"everdawn-labs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"backing"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"native-usdt-locked-ethereum"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"redeemable_with_tether"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why it matters:&lt;/p&gt;

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

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

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

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

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

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

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

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

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before a TRC20 payout, check the sender can actually pay for it.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tronWeb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAccountResources&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hotWallet&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;energyAvailable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;EnergyLimit&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;EnergyUsed&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;energyAvailable&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;estimatedEnergy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;trxNeeded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;estimateTrxBurn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;estimatedEnergy&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;energyAvailable&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;assertBalanceAtLeast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hotWallet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;trxNeeded&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also budget for &lt;strong&gt;first-transfer cost&lt;/strong&gt;. Sending USDT to a Tron address that has never held it costs roughly double, because the transfer has to create the token balance. If you sweep deposits to many fresh addresses, this shows up in your costs.&lt;/p&gt;

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

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

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

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

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

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

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

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

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




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

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

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

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

&lt;p&gt;If a constant like this is still in your codebase, it's overcharging by two orders of magnitude:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Written for a different fee era
&lt;/span&gt;&lt;span class="n"&gt;PAYOUT_GAS_PRICE_GWEI&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;
&lt;span class="n"&gt;MIN_DEPOSIT_USDT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since the London upgrade, fees are EIP-1559: a protocol-set base fee plus a priority tip. Estimate both from live data at the moment of payout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;estimate_fees&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;priority_percentile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;hist&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;w3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;eth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fee_history&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;latest&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;priority_percentile&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;base_fee&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hist&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;baseFeePerGas&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;# next block's base fee
&lt;/span&gt;    &lt;span class="n"&gt;tips&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;hist&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reward&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
    &lt;span class="n"&gt;tip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tips&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tips&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="c1"&gt;# Allow the base fee to rise for a few blocks before the tx lands
&lt;/span&gt;    &lt;span class="n"&gt;max_fee&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base_fee&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;tip&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;maxFeePerGas&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;max_fee&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;maxPriorityFeePerGas&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tip&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

&lt;p&gt;Many services still use a confirmation count carried over from proof-of-work habits. You can do better with the block tags the JSON-RPC API exposes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;latest&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;w3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;eth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_block&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;latest&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;safe&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;w3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;eth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_block&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;safe&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="c1"&gt;# justified, very unlikely to reorg
&lt;/span&gt;&lt;span class="n"&gt;finalized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;w3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;eth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_block&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;finalized&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# economically final
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;is_credited&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deposit_block&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount_usd&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;threshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;safe&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;amount_usd&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;10_000&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;finalized&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;deposit_block&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;number&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;safe&lt;/code&gt; usually trails the head by a few minutes; &lt;code&gt;finalized&lt;/code&gt; by around 13. Tiering by amount gives most users a fast credit and keeps large deposits on economic finality.&lt;/p&gt;

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

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

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

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

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

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

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RESERVE_WEI&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;estimateReserve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;liveFees&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// ~5 future transfers&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;maxSendable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;gasForThisTx&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;RESERVE_WEI&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

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

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

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

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




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

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

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

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

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

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

&lt;p&gt;Make the confirmation threshold a config value, and consider scaling it with amount:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;required_confirmations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount_btc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Illustrative policy. Tune to your own risk tolerance.
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;amount_btc&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;amount_btc&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

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

&lt;p&gt;Watch the address, not the transaction. When a new transaction pays the same deposit address, check whether it conflicts with one you've already seen (spends the same inputs). If it does, treat it as a replacement: update the record and keep the order alive.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_transaction_seen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;deposit_address&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;outputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;outputs&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;deposit_address&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="n"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;find_pending_by_inputs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# RBF: same inputs, new txid
&lt;/span&gt;    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;create_pending_deposit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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




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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createOrder&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;pair&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;refundAddress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;connectedWallet&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;userInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;refundAddress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// never null silently&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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




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

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

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

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

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

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

&lt;/div&gt;



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

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

&lt;h2&gt;
  
  
  Computing the effective rate
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;effective_cost_bps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount_in&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount_out_received&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mid_market_rate&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Total cost in basis points, relative to mid-market.
    Captures spread + service fee + output network fee together.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;amount_in&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;mid_market_rate&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mid_market_rate and amount_in must be positive&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;amount_out_received&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;10_000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

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

&lt;h2&gt;
  
  
  A quote object that doesn't lie
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"pair"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ETH_USDT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"amount_in"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rate_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"floating"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

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

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

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

  &lt;/span&gt;&lt;span class="nl"&gt;"quote_expires_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-09-28T01:50:12Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"minimum_in"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.012"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Points worth arguing about:&lt;/p&gt;

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

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

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

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

&lt;p&gt;&lt;code&gt;minimum_in&lt;/code&gt; exists because of component 2: if the output network fee exceeds the output value, the operation is uneconomic. Network fees move with congestion, so the minimum moves with them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;minimum_input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output_network_fee&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_fee_fraction&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Smallest input where the output fee stays under max_fee_fraction
    of the payout. Recompute per quote — never cache across sessions.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;min_output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;output_network_fee&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;max_fee_fraction&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;min_output&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

&lt;h2&gt;
  
  
  The test worth writing
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_cost_fields_reconcile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount_in&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reference_rate&lt;/span&gt;
    &lt;span class="n"&gt;fee_bps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output_network_fee&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;10_000&lt;/span&gt;
    &lt;span class="n"&gt;derived&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;service_fee_bps&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;spread_bps&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;fee_bps&lt;/span&gt;

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

&lt;/div&gt;



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




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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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