<?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: Jiahui Miao</title>
    <description>The latest articles on DEV Community by Jiahui Miao (@jiahui-miao).</description>
    <link>https://dev.to/jiahui-miao</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%2F4145931%2Fe3c68af8-ab73-40d1-8b95-a0c2db245cac.jpg</url>
      <title>DEV Community: Jiahui Miao</title>
      <link>https://dev.to/jiahui-miao</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jiahui-miao"/>
    <language>en</language>
    <item>
      <title>Building an x402 Settlement Client in Python: Lessons from a Real Testnet Transaction</title>
      <dc:creator>Jiahui Miao</dc:creator>
      <pubDate>Sun, 27 Sep 2026 19:36:14 +0000</pubDate>
      <link>https://dev.to/jiahui-miao/building-an-x402-settlement-client-in-python-lessons-from-a-real-testnet-transaction-3b51</link>
      <guid>https://dev.to/jiahui-miao/building-an-x402-settlement-client-in-python-lessons-from-a-real-testnet-transaction-3b51</guid>
      <description>&lt;p&gt;The demo takes ten minutes. I know, because I ran one: an AI agent paying another AI agent over HTTP, settled on-chain, no accounts, no API keys. The x402 protocol makes the happy path genuinely easy — a payment requirements object, an EIP-712/EIP-3009 signature, two calls to the facilitator (&lt;code&gt;/verify&lt;/code&gt;, then &lt;code&gt;/settle&lt;/code&gt;), and 0.01 USDC moves on Base Sepolia. The transaction is real and verifiable on a block explorer. (Testnet only — test coins, no real value.)&lt;/p&gt;

&lt;p&gt;Then I tried to turn the demo into something I'd trust with real money, and the demo fell apart in exactly the places every "integrate x402 in 30 lines" tutorial skips.&lt;/p&gt;

&lt;p&gt;This piece is about those places. The tutorials teach the API. Production lives in the boring parts: how you represent money, how you track a settlement's state, and how you prove what happened afterward. Get any of them wrong and you don't have a payments integration — you have a demo with a mainnet accident waiting to happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  The transaction
&lt;/h2&gt;

&lt;p&gt;Here's what the happy path actually looked like. x402 v2, &lt;code&gt;exact&lt;/code&gt; scheme: the buyer agent builds payment requirements (network &lt;code&gt;eip155:84532&lt;/code&gt;, the testnet USDC asset, amount in atomic units), signs an EIP-3009 &lt;code&gt;TransferWithAuthorization&lt;/code&gt; offline, and hands the payload to the official facilitator at &lt;code&gt;x402.org/facilitator&lt;/code&gt;. The facilitator's &lt;code&gt;/verify&lt;/code&gt; checks the signature and the balance; &lt;code&gt;/settle&lt;/code&gt; executes the transfer on-chain. Ten minutes, end to end, first try.&lt;/p&gt;

&lt;p&gt;That ease is the trap. Two HTTP calls look like an integration. They're a demo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 1: Money is integers, or it's wrong
&lt;/h2&gt;

&lt;p&gt;0.01 USDC is not &lt;code&gt;0.01&lt;/code&gt; in our system. It's the integer &lt;code&gt;10000&lt;/code&gt; — ten thousand micro-units of a six-decimal token. This is the first thing the tutorials skip, and it's the one that will cost you actual money: &lt;strong&gt;never let a float touch an amount.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Floats can't represent most decimals exactly. &lt;code&gt;0.1 + 0.2&lt;/code&gt; is the famous example, but in settlement code the failure mode is worse: a rounding discrepancy of one atomic unit between what you authorized and what you settled means your reconciliation breaks, your signature doesn't match the value, or — worst case — you authorize slightly more than you meant to. At 0.01 USDC nobody notices. At volume, it's a slow leak you can't audit.&lt;/p&gt;

&lt;p&gt;Our rule is enforced at the type level, not by convention: the money module accepts decimal strings or &lt;code&gt;Decimal&lt;/code&gt; and raises &lt;code&gt;TypeError&lt;/code&gt; on floats. Not a warning — an exception. If a float can reach your amount, it eventually will.&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;# simplified from our money module
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;to_micro_usdc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&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="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;TypeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;float is not allowed to represent money: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pass a decimal string or Decimal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This looks paranoid until the first time a JSON payload arrives with &lt;code&gt;0.30000000000000004&lt;/code&gt; in it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 2: Verify and settle are two failure domains
&lt;/h2&gt;

&lt;p&gt;The tutorials present &lt;code&gt;/verify&lt;/code&gt; then &lt;code&gt;/settle&lt;/code&gt; as one step. They're two network calls to a remote service, which means they fail independently — and the interesting failures live in the gap between them.&lt;/p&gt;

&lt;p&gt;Verify passes, settle fails: did the money move? You don't know until you check. Settle times out: retrying blindly risks a double payment. The EIP-3009 authorization you're settling carries a nonce, and nonces are one-shot — you can't just re-sign the identical payload and try again. So the retry path needs its own logic: check chain state first, then decide whether to re-authorize.&lt;/p&gt;

&lt;p&gt;This is why we built an explicit settlement state machine: &lt;code&gt;pending → verifying → verified → settling → settled&lt;/code&gt;, with a &lt;code&gt;failed&lt;/code&gt; state reachable from every step, and illegal transitions raising instead of silently proceeding. Settling an order that's already &lt;code&gt;settled&lt;/code&gt; throws a duplicate-settlement error. These aren't whiteboard abstractions — each one is a bug we wrote the test for before meeting it in the wild. (14 dedicated x402 tests; 139+1 unit tests across the stack.)&lt;/p&gt;

&lt;p&gt;The mental model: a settlement isn't a function call, it's a little saga. Treat it like one and the failure modes become boring. Treat it like an API call and they become incidents.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 3: If you can't reproduce the bytes, you can't dispute anything
&lt;/h2&gt;

&lt;p&gt;After settlement, we produce a canonical ledger record: the same logical receipt serialized to JSON must produce the exact same bytes every time, so its hash is stable and verifiable by anyone. That means deterministic serialization — sorted keys, no ambiguous whitespace — as a single shared function the whole codebase uses, not something each module does its own way.&lt;/p&gt;

&lt;p&gt;Why does this matter before you have any disputes? Because the receipt is the only thing both sides of a machine-to-machine transaction share. When the buyer agent's log says &lt;code&gt;10000&lt;/code&gt; and the seller agent's log says &lt;code&gt;10000&lt;/code&gt; but the bytes differ, you have two truths and no tiebreaker. Canonical form is what turns "my log says" into "the record says." Dispute resolution, refunds, and auditing all stand on this one boring function.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 4: Hard-lock the testnet
&lt;/h2&gt;

&lt;p&gt;The scariest line in our codebase isn't in the settlement logic. It's the guard at the top of the adapter: if the network isn't Base Sepolia, refuse. If the asset isn't the known testnet USDC, refuse. The mainnet allowlist is empty, and enabling mainnet requires an explicit, deliberate act — not a config flag someone flips in a demo.&lt;/p&gt;

&lt;p&gt;I've seen too many "test" setups where the only thing separating test money from real money is an environment variable. That's not a safety boundary; that's a typo waiting to happen. Make the dangerous path structurally impossible and the safe path the default, and you'll never have the 2 a.m. realization that your test just spent real funds.&lt;/p&gt;

&lt;h2&gt;
  
  
  The boring parts are the integration
&lt;/h2&gt;

&lt;p&gt;None of this is specific to x402. Every payment rail — Stripe webhooks, bank transfers, on-chain settlement — eventually teaches the same four lessons: integer money, explicit settlement state, deterministic records, and a hard line between test and real funds. The tutorials skip them because they're not about the protocol. They're about the discipline around the protocol.&lt;/p&gt;

&lt;p&gt;So here's my rule of thumb for evaluating any "agent payments integration," ours included: ignore the demo. Read the money module, the state machine, and the receipt format. If those three are boring, precise, and a little paranoid, the integration is real. If they don't exist, what you're looking at is thirty lines of API calls and a production incident with your name on it.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Sources &amp;amp; further reading:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The testnet settlement (Basescan, Base Sepolia): &lt;a href="https://sepolia.basescan.org/tx/0x2f81733dfcd4eff4e0db990c09a8f11b9cd19e36835e509ec540bb4568106304" rel="noopener noreferrer"&gt;https://sepolia.basescan.org/tx/0x2f81733dfcd4eff4e0db990c09a8f11b9cd19e36835e509ec540bb4568106304&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;x402 protocol (Coinbase/x402-foundation): &lt;a href="https://github.com/coinbase/x402" rel="noopener noreferrer"&gt;https://github.com/coinbase/x402&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;EIP-3009: Transfer With Authorization: &lt;a href="https://eips.ethereum.org/EIPS/eip-3009" rel="noopener noreferrer"&gt;https://eips.ethereum.org/EIPS/eip-3009&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;About the author:&lt;/strong&gt; Jiahui Miao participates in 3GPP working on 6G core network standards and is the founder of vertciti, building procurement infrastructure for AI agents. Disclosure: the author is building a company in the agent-commerce space.&lt;/p&gt;

</description>
      <category>python</category>
      <category>web3</category>
      <category>ai</category>
      <category>blockchain</category>
    </item>
  </channel>
</rss>
