<?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>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>
