<?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: SWARM operator</title>
    <description>The latest articles on DEV Community by SWARM operator (@swarmagentoperator).</description>
    <link>https://dev.to/swarmagentoperator</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%2F4147426%2F1b86803c-2eb1-43ff-a452-b3f0bc9e2868.png</url>
      <title>DEV Community: SWARM operator</title>
      <link>https://dev.to/swarmagentoperator</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/swarmagentoperator"/>
    <language>en</language>
    <item>
      <title>x402 seller gotchas, part 4: one payment, five deliveries, and first buyers that were robots</title>
      <dc:creator>SWARM operator</dc:creator>
      <pubDate>Wed, 21 Oct 2026 08:10:12 +0000</pubDate>
      <link>https://dev.to/swarmagentoperator/x402-seller-gotchas-part-4-one-payment-five-deliveries-and-first-buyers-that-were-robots-2323</link>
      <guid>https://dev.to/swarmagentoperator/x402-seller-gotchas-part-4-one-payment-five-deliveries-and-first-buyers-that-were-robots-2323</guid>
      <description>&lt;p&gt;In &lt;a href="https://swarm-agent.net/x402-seller-gotchas/" rel="noopener noreferrer"&gt;part 1&lt;/a&gt; we listed what broke while selling small services over &lt;a href="https://www.x402.org/" rel="noopener noreferrer"&gt;x402&lt;/a&gt; (pay per request, USDC on Base). &lt;a href="https://swarm-agent.net/x402-seller-gotchas/part-2/" rel="noopener noreferrer"&gt;Part 2&lt;/a&gt; put the shop behind an MCP server, and &lt;a href="https://swarm-agent.net/x402-seller-gotchas/part-3/" rel="noopener noreferrer"&gt;part 3&lt;/a&gt; was about numbers that counted the wrong thing.&lt;/p&gt;

&lt;p&gt;This week we launched two new services at $0.002 each: one checks whether a URL is up, the other returns the readable text of a web page. Launching them surfaced six gotchas. Three are bugs that would have cost money. Three are about who shows up once you are listed.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The lesson that matters most:&lt;/strong&gt; test your payment path with the same tools a stranger would use, at your real price, and several requests at once. Most of our bugs only showed up that way.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  One signed payment, five deliveries
&lt;/h2&gt;

&lt;p&gt;We read Ishween Kaur's "Your Agent Paid. That Does Not Mean It Should Have." (Daily Context, on DEV) and tried the obvious attack on our own shop: send the same signed payment five times, at the same moment.&lt;/p&gt;

&lt;p&gt;We got five full answers and one charge. Our shop wrote the payment's nonce down only at the end, after settling. Five copies all passed the "not seen before" check before the first one finished.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; reserve the payment the moment it arrives, before doing any work. The first copy gets the work. The others get a &lt;code&gt;409 Conflict&lt;/code&gt; and nothing else. We tested it on a copy of the shop the same day: five requests, one delivery, one charge.&lt;/p&gt;

&lt;h2&gt;
  
  
  A price of 0.2 cents rounded to free
&lt;/h2&gt;

&lt;p&gt;Our shop stored prices in whole US cents. That was fine at $0.02 and up. The new services cost $0.002, which is 0.2 cents, and in an integer field 0.2 becomes 0.&lt;/p&gt;

&lt;p&gt;We caught it while building the new routes, before launch. Had it shipped, the route would have quoted a price of zero.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; we now convert prices with fractions instead of whole cents. If you are starting fresh, keep prices in the asset's atomic units (USDC has 6 decimals, so $0.002 is &lt;code&gt;2000&lt;/code&gt;), and check at startup that no paid route quotes 0.&lt;/p&gt;

&lt;h2&gt;
  
  
  The official buyer library refused to pay us
&lt;/h2&gt;

&lt;p&gt;Our own test purchase of the new routes failed, and not on our side. The buyer library we used, &lt;code&gt;@x402/core&lt;/code&gt; 2.28, has a spending check that is on by default. It rounds amounts to two decimals, so it read our $0.002 as $0.00 and refused to pay, with an error that showed the amount as $0.00.&lt;/p&gt;

&lt;p&gt;Any agent using that library with its defaults would have failed at our price, and the error points nowhere useful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; on the buyer side, &lt;code&gt;client.setSpendControls(false)&lt;/code&gt; turns it off. Only do that if you keep your own price check. We did both in our MCP server, so its users can pay sub-cent routes. On the seller side: buy from yourself with the libraries your buyers use, at your real price, before you announce anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your first buyers are robots
&lt;/h2&gt;

&lt;p&gt;Our first paid call from outside came 3.5 hours after the new route appeared in Coinbase's catalog. It looked like a customer. Then we read the wallet's history: 1,000 payments to 973 different addresses in four days. It was a sweeper that pays every new listing once.&lt;/p&gt;

&lt;p&gt;The next two paid calls, eight hours later, came from a client that names itself &lt;code&gt;lumiere-paycheck-prober&lt;/code&gt;: it buys to check that sellers charge and deliver. In the same 24 hours we received 9 to 14 payments built wrong on purpose, mostly from one wallet: a missing field, a header that is not base64, 1 unit instead of 2,000, the wrong &lt;code&gt;payTo&lt;/code&gt;, an authorization already expired or not yet valid. We rejected all of them without charging.&lt;/p&gt;

&lt;p&gt;On 20 Oct a client called &lt;code&gt;x402-list-unfunded-study&lt;/code&gt; tried four of our services from two wallets that had never paid anyone. In three days the two new routes answered about 2,600 price requests. Every top requester was a monitor, a probe, or our own script.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; before you count a buyer, check how many sellers that wallet pays (100+ addresses means a sweeper), read the client's &lt;code&gt;User-Agent&lt;/code&gt;, and keep a list of known probes. A listing gets robots within hours. Customers are a different question.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failed payments left no trace
&lt;/h2&gt;

&lt;p&gt;If someone paid and our shop refused (bad signature, short amount, a repeated nonce), the buyer got an error and we got nothing. No log line, no counter. A real customer could have tried and failed, and we would never have known.&lt;/p&gt;

&lt;p&gt;We noticed this reading Payload's "Paid and blocked" on DEV.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; every refused payment now goes to a log with the time, route, reason and payer address. It shows in our morning report, and a wallet we have never seen before triggers a Telegram alert. That alert is how we caught the unfunded study above in three commands, instead of mistaking it for a buyer who could not pay.&lt;/p&gt;

&lt;h2&gt;
  
  
  Registered is not found
&lt;/h2&gt;

&lt;p&gt;We registered the shop with AgentCash, an agent wallet with its own search. Its validator reads our six routes, prices and guidance with no warnings. We still appear zero times in its search, even for "check if a website is up".&lt;/p&gt;

&lt;p&gt;One of its maintainers explained it to another seller with the same problem: a listing only starts showing in search after it gets some use through trusted sources such as their own wallet. Meanwhile the listing that ranks first for that query shows zero purchases in the directory, while on-chain it has been paid by 72 different addresses in three months.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; none on our side. The listing was never broken. We stopped editing it, made it easy for anyone who already finds us elsewhere to call us through that wallet, and count our buyers on-chain instead of trusting any directory's numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it stands
&lt;/h2&gt;

&lt;p&gt;Customer sales are still zero. Every outside payment so far has been a sweeper, a probe or an inspector. The new routes stay up until 30 Oct, and they stay after that only if two or more outside wallets pay and come back.&lt;/p&gt;

&lt;p&gt;If your agent buys x402 services, we'd like to know what it pays for twice: &lt;a href="mailto:contact@swarm-agent.net"&gt;contact@swarm-agent.net&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The shop: &lt;a href="https://swarm-agent.net/" rel="noopener noreferrer"&gt;swarm-agent.net&lt;/a&gt; · the MCP server is open source (MIT): &lt;a href="https://github.com/swarm-agent-operator/swarm-x402-mcp" rel="noopener noreferrer"&gt;github.com/swarm-agent-operator/swarm-x402-mcp&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>x402</category>
      <category>api</category>
      <category>webdev</category>
      <category>ai</category>
    </item>
    <item>
      <title>One month of an AI agent trying to pay its own way: 0 sales, and the numbers behind it</title>
      <dc:creator>SWARM operator</dc:creator>
      <pubDate>Thu, 15 Oct 2026 11:59:09 +0000</pubDate>
      <link>https://dev.to/swarmagentoperator/one-month-of-an-ai-agent-trying-to-pay-its-own-way-0-sales-and-the-numbers-behind-it-3f0c</link>
      <guid>https://dev.to/swarmagentoperator/one-month-of-an-ai-agent-trying-to-pay-its-own-way-0-sales-and-the-numbers-behind-it-3f0c</guid>
      <description>&lt;p&gt;SWARM is an AI agent that runs 24/7 on a small rented server and sells tiny services to other AI agents at &lt;a href="https://swarm-agent.net" rel="noopener noreferrer"&gt;swarm-agent.net&lt;/a&gt;. It gets paid per request with &lt;a href="https://www.x402.org/" rel="noopener noreferrer"&gt;x402&lt;/a&gt;: the server answers "402 Payment Required" with a price, the client pays in USDC on Base, and gets the result. No accounts, no contracts.&lt;/p&gt;

&lt;p&gt;The goal from day one was an agent that "works alone, finds its own way and pays its own bills". This is the honest report of month one.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Short version:&lt;/strong&gt; the hard technical part is done and it works. In a month, no outside buyer has paid for anything. So the project has moved from "build the shop" to "find out what the market actually buys".&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Where it stands
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The shop works end to end:&lt;/strong&gt; quote, payment check, work, charge only if the work succeeds, receipt. Four services, from $0.02 to $0.99: small utilities, a GitHub repo health check, and two web-research reports with linked sources.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's listed almost everywhere&lt;/strong&gt; an agent might look: Coinbase's Bazaar, x402scan, AgentCash, x402-list, Glama, mcprush, the official MCP registry, and npm (we wrap the shop in an MCP server so Claude or Cursor can use it).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost:&lt;/strong&gt; thinking costs about $1.5 a day (hard cap $2). Server and domain, about €7.50 a month. The owner pays both, for now.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The numbers, and how they add up
&lt;/h2&gt;

&lt;p&gt;Every morning a small script counts what happened. After a reader's comment on Moltbook, we changed it so the buckets visibly add up to the total. "We filtered out our tests" is a claim. A sum that closes is something you can check.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BALANCE: 18 payments in the book (since day one)
        = 0 sales + 17 our own tests + 1 inspector
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;0 sales&lt;/strong&gt; to anyone else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;17 test purchases&lt;/strong&gt; from our own wallet. They're not income. We make them to test the shop, and because Bazaar delists services after 30 days without payments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1 inspector:&lt;/strong&gt; on 1 Oct, vet402, an automated checker, bought a $0.02 call to verify that our shop delivers. We count it as a quality signal, not a sale.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Traffic, last 7 days:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BALANCE: 21,316 requests = 19,371 from known bots + 1,945 from unknown clients
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Almost all of it is directories, monitors and crawlers. There are about 500 MCP downloads a week, mirrors included, and zero purchase attempts through the MCP.&lt;/p&gt;

&lt;h2&gt;
  
  
  What went wrong (the short list)
&lt;/h2&gt;

&lt;p&gt;The full details are in the &lt;a href="https://swarm-agent.net/x402-seller-gotchas/" rel="noopener noreferrer"&gt;x402 seller gotchas&lt;/a&gt; series. The ones that shaped the month:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The agent believed false things and acted on them, rationally.&lt;/strong&gt; Its own prompt told it it had $0.51 left and was about to die, so it went to sleep for 4 hours to save money. The data was wrong, not the reasoning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It shipped a paid endpoint that could be bypassed&lt;/strong&gt; with one word in the URL. In production. We caught it in review. Since then, we assume internal rules are advice. The real controls are outside the agent: firewall, signed spending caps, human review.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The first products did nothing a buyer couldn't get for free.&lt;/strong&gt; The "report" was one model call with no web search. We rebuilt them with real search and sources.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The shop charged before checking it could do the job.&lt;/strong&gt; Fixed before it cost anyone money. Now it charges on delivery: if the work fails, you don't pay.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The API key expired after 30 days and no alert fired.&lt;/strong&gt; The agent was up but couldn't think for about 30 hours.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What we learned about demand
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Visibility is not demand.&lt;/strong&gt; Being in many directories brings bots, not buyers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Thinking is the expensive part, not selling.&lt;/strong&gt; With the agent idle, about 14 sales of $0.02 a day would cover the fixed cost. With it thinking at full speed, you'd need twenty times that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The market is small and concentrated.&lt;/strong&gt; &lt;a href="https://graded.sh/report/2026-08" rel="noopener noreferrer"&gt;graded.sh's August report&lt;/a&gt; counted 292,967 paid calls in 30 days. The top ten services took about 80% of them, and the median price was $0.01. What sells are small pieces an agent uses mid-task: search, page reading, chain reads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Our demand data was worse than we thought.&lt;/strong&gt; Many sellers use one wallet for their whole catalog, so their buyer counts don't tell you which product sells. And "unique buyers" fields are often cumulative, not last 30 days.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The decision: measure before building
&lt;/h2&gt;

&lt;p&gt;The plan was to pick a new product on 16 Oct. To choose, we ran an "arena": a group of AI agents competing to propose the best next step, judged against our own rules. The first round (9 Oct) picked a US trademark checker. When we looked at the evidence again, it didn't hold up, so that one is dropped.&lt;/p&gt;

&lt;p&gt;The second round didn't pick a product. It picked a rule:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Build nothing&lt;/strong&gt; until a comparable x402 seller shows real buyers on-chain: 5 or more distinct buyers in the last 30 days who come back, with our own probes and sweeper wallets excluded.&lt;/li&gt;
&lt;li&gt;If one passes, build it &lt;strong&gt;at no more than that seller's price&lt;/strong&gt;, at a cost under a third of the price, as a &lt;strong&gt;14-day trial&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Note which chain that seller is paid on. A lot of x402 volume today is on Solana, and we only accept Base.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So we wrote a small reader that pulls each candidate seller's payments straight from the chain, keeps only real x402 settlements at the listed price, and drops one-off wallets. We tested it on our own shop first. It found our test payments and, with our own wallets excluded, correctly scored us as a fail.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the chain says
&lt;/h2&gt;

&lt;p&gt;We measured three candidates on Base, 15 Sep to 15 Oct:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Candidate&lt;/th&gt;
&lt;th&gt;Paid at its price&lt;/th&gt;
&lt;th&gt;After removing sweepers&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Site check (is this URL up?)&lt;/td&gt;
&lt;td&gt;465 payments, 32 wallets&lt;/td&gt;
&lt;td&gt;7 buyers who come back, ~23 payments each&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;PASS&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wallet sanctions check&lt;/td&gt;
&lt;td&gt;159 payments, 12 wallets&lt;/td&gt;
&lt;td&gt;1 buyer&lt;/td&gt;
&lt;td&gt;FAIL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Page scrape&lt;/td&gt;
&lt;td&gt;could not measure&lt;/td&gt;
&lt;td&gt;public RPC rate limits and timeouts&lt;/td&gt;
&lt;td&gt;unknown&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two things stand out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sweepers are most of the "buyers".&lt;/strong&gt; Of 12 wallets paying for sanctions checks, 4 were sweepers: wallets that pay 100 or more different addresses, which is not how a buyer with one job behaves. Only one real buyer was left. For site checks, 5 of 12 went the same way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Passing is not the same as money.&lt;/strong&gt; The site-check seller uses one wallet for many products, with at least 10 different prices. So we can't be sure every $0.002 payment is a site check. And the 7 buyers paid about 200 times at $0.002. That's &lt;strong&gt;around $0.40 a month&lt;/strong&gt;. The whole seller, all products together, took in roughly $15 a month in what we could see.&lt;/p&gt;

&lt;p&gt;We also owe a correction. In part 3 we called this seller "single-product" with 35 buyers. Both were wrong: it sells many products, and once you keep only its listed price and remove sweepers, 7 buyers are left. We wrote about that exact trap, and then fell into it.&lt;/p&gt;

&lt;p&gt;So the market for site checks is real, small, and paid by machines that check often. That fits our pilot goal: "someone pays once, even if it's cents". A site check needs no model call, so it costs almost nothing to run. Before building it, we'll put it on trial, the same way we tried the project. If it survives, it ships at $0.002 or less, as a 14-day test.&lt;/p&gt;

&lt;p&gt;One more gap: our reader only reads Base. A lot of x402 volume is on Solana, and we couldn't see any of it. So we're also adding Solana as a second way to pay, next to Base, on all our services.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Track B starts on 26 Oct:&lt;/strong&gt; the same agent, same rules, selling to people instead of machines. That's where the owner thinks the business is today. Track A (agent to agent) stays open. We think it will grow and we want to be there early.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review on 15 Nov.&lt;/strong&gt; It's a checkpoint, not a stop: outside sales, the new product (if any), track B, and how to split the budget.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Still not measured, and it should be:&lt;/strong&gt; total spent since day one, and the owner's hours per week. A simulated "trial" of the project on 11 Oct flagged both as the costs that weigh most.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The agent always identifies itself as an AI, doesn't contact anyone who hasn't contacted it, and doesn't spend money or buy from other sellers. Outreach is done by me, by hand. If you build agents and one of these services would be useful to you, or if you think we're measuring the wrong thing, I'd like to hear it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>buildinpublic</category>
      <category>web3</category>
    </item>
    <item>
      <title>x402 seller gotchas, part 3: miscounted buyers, and an API key that expired quietly</title>
      <dc:creator>SWARM operator</dc:creator>
      <pubDate>Wed, 14 Oct 2026 12:52:17 +0000</pubDate>
      <link>https://dev.to/swarmagentoperator/x402-seller-gotchas-part-3-miscounted-buyers-and-an-api-key-that-expired-quietly-480n</link>
      <guid>https://dev.to/swarmagentoperator/x402-seller-gotchas-part-3-miscounted-buyers-and-an-api-key-that-expired-quietly-480n</guid>
      <description>&lt;p&gt;In &lt;a href="https://swarm-agent.net/x402-seller-gotchas/" rel="noopener noreferrer"&gt;part 1&lt;/a&gt; we listed what broke while selling four small services over &lt;a href="https://www.x402.org/" rel="noopener noreferrer"&gt;x402&lt;/a&gt; (pay per request, USDC on Base). In &lt;a href="https://swarm-agent.net/x402-seller-gotchas/part-2/" rel="noopener noreferrer"&gt;part 2&lt;/a&gt; we wrapped the shop in an MCP server. Part 2 ended with a promise: who actually buys in x402, once we have numbers we trust.&lt;/p&gt;

&lt;p&gt;This part is about why that took longer than planned. Our numbers were wrong three times in one week: twice about other sellers, once about ourselves. There is also one gotcha that has nothing to do with payments, and it took our agent offline for 30 hours.&lt;/p&gt;

&lt;p&gt;The lesson that matters most: before you trust a number, ask what it counts: which product, which time window, which files. Every number below was right about something. Just not about what we had asked.&lt;/p&gt;

&lt;p&gt;A seller's buyers are not a product's buyers&lt;/p&gt;

&lt;p&gt;Before building a new service, we check for demand. Our rule: a similar x402 service must already have 5 or more distinct buyers in the last 30 days, not counting probes. The obvious source is on-chain. x402 payments land on the seller's payTo address, and explorers count buyers per address.&lt;/p&gt;

&lt;p&gt;The catch is that many sellers use one wallet for their whole catalog. A candidate looked like it had real buyers. But the wallet behind it also took payments for security scans, trademark checks and other products. Those buyers proved the seller sells something. They said nothing about that one product.&lt;/p&gt;

&lt;p&gt;Fix: a seller only counts as evidence for a product when it sells just that product, or when the calls can be split by route. The clearest example we found was a single-product site-checking service: 35 buyers, about 41 payments each. That is demand you can read.&lt;/p&gt;

&lt;p&gt;Cumulative is not "last 30 days"&lt;/p&gt;

&lt;p&gt;Same week, second trap. The stats snapshot we used gives three fields per address:&lt;/p&gt;

&lt;p&gt;recipient -&amp;gt; tx_count, unique_buyers, latest_block_timestamp&lt;/p&gt;

&lt;p&gt;We asked for buyers in the last 30 days, and the snapshot can't answer that. unique_buyers counts from the address's first payment, and the timestamp is only the latest one. 500 buyers could be 500 in the first month and none since. Nothing in the field names tells you which.&lt;/p&gt;

&lt;p&gt;Our agent answered "not computable from this data" instead of making up a number. That was the right answer. If you need a time window, you need the payments themselves, each with its timestamp, not a summary.&lt;/p&gt;

&lt;p&gt;For the wider picture, per-service numbers do exist. &lt;a href="https://graded.sh/report/2026-08" rel="noopener noreferrer"&gt;graded.sh's August report&lt;/a&gt;,, built from its own probes, counted 292,967 paid calls in 30 days. The ten biggest services took about 80% of them, and one domain alone took 41%. 81% of the services with any demand had fewer than 10 calls. The median price was $0.01. So who buys in x402? A few agents, buying a few cheap things many times.&lt;/p&gt;

&lt;p&gt;Read every log file, not just the newest&lt;/p&gt;

&lt;p&gt;Then our own numbers were wrong, in the other direction. On 10 Oct our agent reported that no payment had ever completed on our shop: 568 quotes, 0 paid calls. It had read nginx's access.log. But logrotate had moved older days to access.log.1 and compressed the rest. The paid calls were in those files.&lt;/p&gt;

&lt;p&gt;What it read (result: 0):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;' /services/'&lt;/span&gt; /var/log/nginx/access.log | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'$9 == 200'&lt;/span&gt; | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What it should have read:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;zcat &lt;span class="nt"&gt;-f&lt;/span&gt; /var/log/nginx/access.log&lt;span class="k"&gt;*&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;' /services/'&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'$9 == 200'&lt;/span&gt; | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the less flattering part. All 8 paid calls in those files were ours (test purchases from our own wallet) or from an inspector checking that payment works. Not one customer. So "no customer has paid yet" was true. "No payment ever completes" was false. The difference matters: the first is a demand problem, the second a broken checkout. Had we believed it, we would have spent days fixing a checkout that works.&lt;/p&gt;

&lt;p&gt;Fix: count across every rotated file, and take out your own wallet's payments before you count anything. Better still, count settled payments on-chain, which never rotate.&lt;/p&gt;

&lt;p&gt;The API key expired, and nothing alerted&lt;/p&gt;

&lt;p&gt;This one isn't about x402. SWARM thinks through the Anthropic API. When we created its key, we set it to expire after 30 days. That's a sensible precaution, until you forget it. On 11 Oct it expired, and every model call returned:&lt;/p&gt;

&lt;p&gt;401 authentication_error: invalid x-api-key&lt;/p&gt;

&lt;p&gt;The server stayed up, the shop kept answering, and the agent's process kept running and retrying. It just couldn't think, for about 30 hours. We have a Telegram alert for model calls failing, but it only matches one error: out of credit. An invalid key is a different error, so the alert stayed silent. We only noticed at the next morning's check.&lt;/p&gt;

&lt;p&gt;Fix: a new key, with its expiry date in the calendar and a reminder a few days before. The sturdier fix is to alert when success stops, as in "no successful model call in N hours", instead of matching error messages one by one. We haven't built that: for a hobby project, the calendar reminder is enough. If your agent earns money while it thinks, build the heartbeat first.&lt;/p&gt;

&lt;p&gt;Where it stands&lt;/p&gt;

&lt;p&gt;Customer sales are still zero. Every paid call on our shop so far was ours or an inspector's. What changed is that we now trust that zero, and we know what the numbers about other sellers mean. We're choosing the next product by per-product demand.&lt;/p&gt;

&lt;p&gt;If your agent buys x402 services, we'd like to know what it pays for: &lt;a href="mailto:contact@swarm-agent.net"&gt;contact@swarm-agent.net&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The shop: &lt;a href="https://swarm-agent.net/" rel="noopener noreferrer"&gt;swarm-agent.net&lt;/a&gt; · the MCP server is open source (MIT): &lt;a href="https://github.com/swarm-agent-operator/swarm-x402-mcp" rel="noopener noreferrer"&gt;github.com/swarm-agent-operator/swarm-x402-mcp&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>x402</category>
      <category>javascript</category>
    </item>
    <item>
      <title>x402 seller gotchas, part 2: putting a paid API behind an MCP server</title>
      <dc:creator>SWARM operator</dc:creator>
      <pubDate>Thu, 08 Oct 2026 10:07:56 +0000</pubDate>
      <link>https://dev.to/swarmagentoperator/x402-seller-gotchas-part-2-putting-a-paid-api-behind-an-mcp-server-2bd6</link>
      <guid>https://dev.to/swarmagentoperator/x402-seller-gotchas-part-2-putting-a-paid-api-behind-an-mcp-server-2bd6</guid>
      <description>&lt;p&gt;In &lt;a href="https://swarm-agent.net/x402-seller-gotchas/" rel="noopener noreferrer"&gt;part 1&lt;/a&gt; we listed every error we hit selling four small services over &lt;a href="https://www.x402.org/" rel="noopener noreferrer"&gt;x402&lt;/a&gt; (pay per request, USDC on Base). That was about being found, getting paid and delivering.&lt;/p&gt;

&lt;p&gt;This one is about the next step. Most people who could buy from us are building agents, and agents increasingly pick their tools from MCP directories. So we wrapped the shop in an MCP server, &lt;code&gt;swarm-x402-mcp&lt;/code&gt;, and shipped it to npm, the official MCP Registry, GitHub, Glama and mcprush. It took three releases in four days to get the directories to show it correctly. These are the gotchas, in the order we hit them.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The lesson that matters most:&lt;/strong&gt; most directories never run your server. They read your package, your repo or a registry record, and each one reads a different source. If your tools only exist at runtime, a directory can't see them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Tools that only exist at runtime
&lt;/h2&gt;

&lt;p&gt;Our first two versions built the tool list from the live shop: on startup (0.1.0) or on the first &lt;code&gt;tools/list&lt;/code&gt; (0.1.1), they fetched &lt;code&gt;/offers&lt;/code&gt; and &lt;code&gt;/openapi.json&lt;/code&gt; and turned each offer into a tool. It felt right. Add a service to the shop and every client gets it, no release needed.&lt;/p&gt;

&lt;p&gt;A directory that reads the published package saw this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0 tools
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;mcprush, for one, reads tools from the published source when it can't query a running server. There was nothing to read: the tools were a loop over a fetch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; declare each tool in the code, with its schema and price in the description. We lost "new services appear by themselves"; now a new service needs a release. That's the right trade: a buyer's agent should know what it's installing.&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="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utilities&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SWARM utilities&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// abridged&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`UUIDs, hashes, base64, URL encoding, passwords, timestamps. Price: $0.02 per call. &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;PAID&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;op&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Operation, e.g. uuid, hash, base64&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SWARM utilities&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;REACH&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/services/utilities&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Paid tools are not read-only
&lt;/h2&gt;

&lt;p&gt;Our tools don't change anything for the caller, so it's tempting to call them read-only. They aren't: every call goes over the network and, with a wallet configured, spends money. MCP has annotations for exactly this, and a client deciding whether to ask its user before a call reads them:&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;// Every tool reaches the network and may spend USDC: not read-only, open world.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;REACH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;readOnlyHint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;destructiveHint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;idempotentHint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;openWorldHint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After 0.1.2, mcprush still showed all four tools as "read". We opened a ticket. Their answer, the next day: the fault was theirs. Their source reader dropped annotations declared beside each tool, including a shared constant spread in like our &lt;code&gt;REACH&lt;/code&gt;, and guessed the kind from the tool name. They fixed the reader, and the tools now show as "write", their highest kind, which includes reaching the network. Credit to them for a fast, clear reply.&lt;/p&gt;

&lt;p&gt;Two takeaways. Declare the annotations even if nobody reads them yet. And if a directory shows them wrong, check your code, then report it: in our case the directory was wrong, and fixed it for everyone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check the quote before you pay
&lt;/h2&gt;

&lt;p&gt;An MCP client can call a tool without a human looking at the price. So the server reads the 402 quote first, without paying, and refuses anything off Base mainnet or above a cap the user sets (&lt;code&gt;SWARM_MAX_USD&lt;/code&gt;, default $1). Only then does it pay with the official client:&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;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;BASE&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;init&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                       &lt;span class="c1"&gt;// no payment yet&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;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;402&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Expected a 402 quote, got HTTP &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&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;accept&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nx"&gt;accepts&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;network&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;eip155:8453&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;No payment option on Base mainnet; not paying.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;usd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;accept&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="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;e6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                         &lt;span class="c1"&gt;// atomic units → dollars&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;usd&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;MAX_USD&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Price $&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;usd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt; is above SWARM_MAX_USD; not paying.`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;pay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;BASE&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;init&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                         &lt;span class="c1"&gt;// now pay&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;!(usd &amp;lt;= MAX_USD)&lt;/code&gt; rather than &lt;code&gt;usd &amp;gt; MAX_USD&lt;/code&gt; on purpose: if the amount is missing or garbage, &lt;code&gt;usd&lt;/code&gt; is &lt;code&gt;NaN&lt;/code&gt;, every comparison is false, and the first form refuses while the second would pay.&lt;/p&gt;

&lt;h2&gt;
  
  
  No wallet is a normal state, not an error
&lt;/h2&gt;

&lt;p&gt;Most people who install the server won't have a wallet key set. If the tool just fails, their agent learns nothing. So without a key it quotes and explains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;This tool costs $0.02 per call, paid in USDC on Base via x402. Set SWARM_WALLET_KEY to the
private key of a wallet holding a little USDC on Base (use a dedicated wallet with only a few
dollars) and call again.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Every directory reads a different source
&lt;/h2&gt;

&lt;p&gt;This is what we had to push on each release:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Where&lt;/th&gt;
&lt;th&gt;What it reads&lt;/th&gt;
&lt;th&gt;What a release takes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;npm&lt;/td&gt;
&lt;td&gt;the package&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;npm publish&lt;/code&gt; (2FA)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Official MCP Registry&lt;/td&gt;
&lt;td&gt;a &lt;code&gt;server.json&lt;/code&gt; record pointing at the npm package&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mcp-publisher publish&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub&lt;/td&gt;
&lt;td&gt;the repo&lt;/td&gt;
&lt;td&gt;push + a Release&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Glama&lt;/td&gt;
&lt;td&gt;the repo, built in a Docker sandbox&lt;/td&gt;
&lt;td&gt;in our case, by hand: sync, then Build &amp;amp; Release&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mcprush&lt;/td&gt;
&lt;td&gt;the npm package (when it can't run the server)&lt;/td&gt;
&lt;td&gt;cut the release in their studio, then "Read the tools again"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PulseMCP&lt;/td&gt;
&lt;td&gt;the official registry&lt;/td&gt;
&lt;td&gt;nothing: submissions are paused, and they say they'll pick up registry entries when they reopen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Smithery&lt;/td&gt;
&lt;td&gt;for a local stdio server, an &lt;code&gt;.mcpb&lt;/code&gt; bundle&lt;/td&gt;
&lt;td&gt;not done yet&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two of these caught us out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Glama kept building the old commit.&lt;/strong&gt; Its last sync was days old and a pinned commit SHA was set, so "new version" meant "the version we already had". Syncing the repo and clearing the pinned SHA fixed it. Its first rebuild then failed with an internal error on their side; the retry passed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Versions are spent once.&lt;/strong&gt; npm and mcprush both refuse a version number that has shipped. mcprush's release form even comes pre-filled with the next minor (0.2.0 for us) instead of the next patch, so read it before you press publish.&lt;/p&gt;

&lt;h2&gt;
  
  
  Count calls, not reads
&lt;/h2&gt;

&lt;p&gt;We log requests from our own MCP server by its User-Agent. The day after 0.1.2 shipped, the counter for the past week said 44 requests from 11 different addresses. It looked like eleven people using our tools.&lt;/p&gt;

&lt;p&gt;It was zero. Grouping by version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;     34 swarm-x402-mcp/0.1.0
     10 swarm-x402-mcp/0.1.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All of it was the old versions fetching &lt;code&gt;/offers&lt;/code&gt; and &lt;code&gt;/openapi.json&lt;/code&gt; at startup: old installs, and directories starting the server to inspect it. None of the eleven called a paid route. The fix is in what we count: requests to &lt;code&gt;/services/*&lt;/code&gt;, not catalog reads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the 2FA in the loop
&lt;/h2&gt;

&lt;p&gt;People who use the server put a wallet's private key in its environment. Whoever can publish a new version of our package can read that key on the user's next &lt;code&gt;npx&lt;/code&gt;. So we don't keep an npm automation token anywhere: every publish goes through 2FA on a phone. It makes releases slower and we're keeping it that way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it stands
&lt;/h2&gt;

&lt;p&gt;The server is live everywhere above except PulseMCP (paused) and Smithery (bundle not done yet). Sales through it so far: zero, same as through the shop directly. Being listed is not being bought, and &lt;a href="https://swarm-agent.net/x402-seller-gotchas/part-3/" rel="noopener noreferrer"&gt;part 3&lt;/a&gt; is about who actually buys in x402, and why our first numbers were wrong.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The server is open source (MIT): &lt;a href="https://github.com/swarm-agent-operator/swarm-x402-mcp" rel="noopener noreferrer"&gt;github.com/swarm-agent-operator/swarm-x402-mcp&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>x402</category>
      <category>ai</category>
      <category>javascript</category>
    </item>
    <item>
      <title>We put our x402 shop behind an MCP server</title>
      <dc:creator>SWARM operator</dc:creator>
      <pubDate>Mon, 05 Oct 2026 08:00:31 +0000</pubDate>
      <link>https://dev.to/swarmagentoperator/we-put-our-x402-shop-behind-an-mcp-server-39om</link>
      <guid>https://dev.to/swarmagentoperator/we-put-our-x402-shop-behind-an-mcp-server-39om</guid>
      <description>&lt;p&gt;After quite a lot of research and testing, we've come to one conclusion: x402 buyers don't show up on their own. Behind every agent that buys, there's a person who set it up. That's why we've published an MCP server, so any agent can use our services and pay per use, with no account and no API key.&lt;/p&gt;

&lt;p&gt;Before signing anything, it reads the price and compares it with the limit set by the wallet's owner. It pays on Base, and if no wallet is configured it doesn't pay, it only shows the price. The tools are read live from the shop, so a new product shows up without updating anything.&lt;/p&gt;

&lt;p&gt;As of today we haven't made a real sale, one that isn't a test, but the experiment is on the right track, and that's why we're sharing how the process is going.&lt;/p&gt;

&lt;p&gt;To try it, just follow this link: &lt;a href="https://swarm-agent.net/mcp/" rel="noopener noreferrer"&gt;https://swarm-agent.net/mcp/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Source code: &lt;a href="https://github.com/swarm-agent-operator/swarm-x402-mcp" rel="noopener noreferrer"&gt;https://github.com/swarm-agent-operator/swarm-x402-mcp&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Previous post: what broke our x402 shop and how we fixed it → &lt;a href="https://dev.to/swarmagentoperator/x402-seller-gotchas-what-broke-our-402-and-how-we-fixed-it-2ao1"&gt;https://dev.to/swarmagentoperator/x402-seller-gotchas-what-broke-our-402-and-how-we-fixed-it-2ao1&lt;/a&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>x402</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>x402 seller gotchas: what broke our 402, and how we fixed it</title>
      <dc:creator>SWARM operator</dc:creator>
      <pubDate>Mon, 28 Sep 2026 15:11:20 +0000</pubDate>
      <link>https://dev.to/swarmagentoperator/x402-seller-gotchas-what-broke-our-402-and-how-we-fixed-it-2ao1</link>
      <guid>https://dev.to/swarmagentoperator/x402-seller-gotchas-what-broke-our-402-and-how-we-fixed-it-2ao1</guid>
      <description>&lt;p&gt;&lt;em&gt;Updated 2 Oct 2026: three new gotchas, all ours — why we were never listed in the Coinbase Bazaar, an expiry check this article recommended that rejected the official client, and a quote that grew too big for nginx.&lt;/em&gt;&lt;br&gt;
We run SWARM, an autonomous agent that sells four small services over x402, paid per request in USDC on Base. We spent a month getting it right. By the end it was listed on x402scan and graded A on x402-list, 14 of 14 checks.&lt;/p&gt;

&lt;p&gt;And on the morning we sat down to write this up, we found that a buyer using the official x402 v2 client could not pay us at all. Grade A and all. This is every error we hit, with the exact message where we have it, what was actually going on, and the fix.&lt;/p&gt;

&lt;p&gt;The lesson that matters most: directories don't pay. They check your quote, not your checkout. The only test that catches payment bugs is buying from yourself, with the official client, the way a stranger would. Our own test buyer didn't catch them either, because we wrote it in the same outdated dialect as our server.&lt;/p&gt;

&lt;p&gt;Being found&lt;br&gt;
x402scan finds no discovery document&lt;br&gt;
The first thing x402scan told us when we registered: it couldn't find a discovery document, a machine-readable list of what you sell. A working 402 on its own isn't enough to be listed.&lt;/p&gt;

&lt;p&gt;Fix: serve /openapi.json describing each paid route, with an x-payment-info block per operation. We also serve /.well-known/x402. Generate both from the same list of offers your 402 uses, so they can never disagree with it.&lt;/p&gt;

&lt;p&gt;Two warnings x402scan shows once it does find it, both worth fixing:&lt;/p&gt;

&lt;p&gt;Add info.contact.email to your openapi.json to verify ownership and let users contact you.&lt;br&gt;
Serve a /favicon.ico at your API root to display an icon.&lt;br&gt;
[404] No valid x402 response found&lt;br&gt;
Our paid routes were POST-only. The prober asked with a different method and got a 404, so as far as it was concerned the route wasn't there.&lt;/p&gt;

&lt;p&gt;Fix: answer the 402 quote on GET and HEAD too, not only on the method that does the work. Quoting a price costs nothing, so there's no reason to hide it behind a method.&lt;/p&gt;

&lt;p&gt;[402] No valid x402 response found&lt;br&gt;
The confusing one: the tool tells you it found no valid 402 while your server is returning a 402. Ours had the payment requirements in the JSON body only. x402 v2 puts them in a header. From the x402 docs:&lt;/p&gt;

&lt;p&gt;PAYMENT-REQUIRED: Contains the Base64-encoded payment requirements from the server. This header is returned alongside the 402 status code.&lt;/p&gt;

&lt;p&gt;Other people hit the mirror image: requirements in the header only, empty body, and clients that read the body can't pay. Send both. And expose the header, or browser-based clients can't read it:&lt;/p&gt;

&lt;p&gt;js&lt;br&gt;
const body = JSON.stringify(paymentRequired);&lt;br&gt;
res.writeHead(402, {&lt;br&gt;
  "Content-Type": "application/json",&lt;br&gt;
  "PAYMENT-REQUIRED": Buffer.from(body, "utf8").toString("base64"),&lt;br&gt;
  "Access-Control-Expose-Headers": "PAYMENT-REQUIRED",&lt;br&gt;
});&lt;br&gt;
res.end(body);&lt;br&gt;
Dollars in one place, atomic units in the other&lt;br&gt;
The same price is written two different ways, and mixing them up gets you flagged as a malformed amount:&lt;/p&gt;

&lt;p&gt;Where   0.02 USDC is written as&lt;br&gt;
openapi.json → x-payment-info.price.amount    "0.020000" (US dollars, decimal)&lt;br&gt;
the 402 → accepts[].amount    "20000" (atomic units, USDC has 6 decimals)&lt;br&gt;
json&lt;br&gt;
"x-payment-info": {&lt;br&gt;
  "price": { "mode": "fixed", "currency": "USD", "amount": "0.020000" },&lt;br&gt;
  "protocols": [{ "x402": {} }]&lt;br&gt;
}&lt;br&gt;
Relative resource URLs&lt;br&gt;
Behind a reverse proxy, our server only saw its own path and put /services/summary in the 402's resource field. Anything that indexes your 402 needs the full address. Build it from your public base URL (or X-Forwarded-Proto + Host if you trust your proxy), never from what the app sees locally.&lt;/p&gt;

&lt;p&gt;Let the prober call you&lt;br&gt;
x402scan's own hint when an endpoint fails is worth reading closely:&lt;/p&gt;

&lt;p&gt;They need to return a 402 payment challenge — ensure the x402 paywall runs before request validation, or mark the required parameters in your OpenAPI spec so we can probe automatically.&lt;/p&gt;

&lt;p&gt;Two fixes in one sentence. Quote before you validate: a request with no payment and no body should get the 402, not a 400 about missing fields. And describe your inputs (an input schema and a real example per offer) so a prober, or a buyer's agent, knows how to call you.&lt;br&gt;
Settled, but never in the Coinbase Bazaar&lt;/p&gt;

&lt;p&gt;Twelve test purchases settled through Coinbase's facilitator, and the Bazaar never listed us. There are open issues from other sellers in the same spot, so for a week we called it Coinbase's bug and stopped looking. Two of the causes were ours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One: our discovery extension was incomplete.&lt;/strong&gt; It had &lt;code&gt;info.input&lt;/code&gt; but no &lt;code&gt;schema&lt;/code&gt;. Coinbase's own validator (&lt;a href="https://agentic.market/validate" rel="noopener noreferrer"&gt;agentic.market/validate&lt;/a&gt;) said so plainly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;invalid discovery configuration
v2 discovery extension validation failed: [Schema validation failed: schema is invalid]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Two: the indexer reads it from the payment, not from your 402.&lt;/strong&gt; At settle time it looks in &lt;code&gt;PaymentPayload.resource&lt;/code&gt; and &lt;code&gt;PaymentPayload.extensions&lt;/code&gt;. The official client doesn't copy them there, and our hand-written adapter to the facilitator only passed &lt;code&gt;x402Version&lt;/code&gt;, &lt;code&gt;accepted&lt;/code&gt; and &lt;code&gt;payload&lt;/code&gt;. A perfect 402 never reached the indexer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; generate the extension with the official helper instead of writing it by hand, and attach &lt;code&gt;resource&lt;/code&gt; and &lt;code&gt;extensions&lt;/code&gt; to the payload you hand the facilitator. That doesn't touch the buyer's signature, which only covers the transfer authorization.&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;declareDiscoveryExtension&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@x402/extensions&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;extensions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;declareDiscoveryExtension&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;bodyType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;op&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;uuid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;          &lt;span class="c1"&gt;// a real example request&lt;/span&gt;
  &lt;span class="na"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* JSON Schema of the body */&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// in the 402:   { x402Version: 2, accepts: [...], resource: {...}, extensions }&lt;/span&gt;
&lt;span class="c1"&gt;// at verify/settle:&lt;/span&gt;
&lt;span class="nx"&gt;facilitator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;settle&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;paymentPayload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;extensions&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;requirements&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then validate (the validator probes with &lt;code&gt;GET&lt;/code&gt; by default; pick your route's method or it flags a mismatch) and make one real purchase. Ours was listed the next morning. Each route needs its own settlement: only the one we had paid for showed up.&lt;/p&gt;

&lt;h3&gt;
  
  
  Your quote outgrows nginx
&lt;/h3&gt;

&lt;p&gt;x402 v2 sends the whole quote twice: in the JSON body and, base64-encoded, in the &lt;code&gt;PAYMENT-REQUIRED&lt;/code&gt; header. Everything you add to the quote, like the discovery extension from the section above, makes that header bigger, and base64 adds a third on top.&lt;/p&gt;

&lt;p&gt;We added a real response example to each route's discovery extension, so buyers and inspectors can see what they get before paying. On three of our four routes that pushed the quote header past 4,000 characters (the largest was 4,504). nginx reads an upstream's response headers into one buffer of 4 KB or 8 KB by default, one memory page, and those three routes stopped answering:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;upstream sent too big header while reading response header from upstream
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The route with the smallest example still fit, so it looked fine. The other three were down for about eight minutes, until we checked all four from outside. Our handler for nginx errors turned it into a polite 503 saying nothing was charged, which was true. Nobody could buy anything either.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; give nginx room for the header, and after any change to what the quote carries, check every paid route from outside, not one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;proxy_buffer_size&lt;/span&gt; &lt;span class="mi"&gt;16k&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;proxy_buffers&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="mi"&gt;16k&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;proxy_busy_buffers_size&lt;/span&gt; &lt;span class="mi"&gt;32k&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And keep the quote lean: short examples, and anything long belongs in &lt;code&gt;/openapi.json&lt;/code&gt;, not in every 402. Other proxies in the chain have header limits of their own.&lt;br&gt;
Getting paid&lt;br&gt;
X-PAYMENT header is required&lt;br&gt;
Our 402 said "x402Version": 2. Our server read the payment from X-PAYMENT, which is the v1 name. A v2 client takes you at your word and sends the payment in PAYMENT-SIGNATURE. We didn't see it, answered with another 402, and the buyer was stuck in a loop: pay, get asked to pay. Nothing was charged, but nothing was sold either.&lt;/p&gt;

&lt;p&gt;v1  v2&lt;br&gt;
buyer sends payment in  X-PAYMENT   PAYMENT-SIGNATURE&lt;br&gt;
seller returns receipt in   X-PAYMENT-RESPONSE  PAYMENT-RESPONSE&lt;br&gt;
Source: x402 migration guide, v1 to v2.&lt;/p&gt;

&lt;p&gt;Fix: accept both, new name first. And allow it through CORS, or a browser won't even send it:&lt;/p&gt;

&lt;p&gt;js&lt;br&gt;
const raw = req.headers["payment-signature"] ?? req.headers["x-payment"];&lt;br&gt;
// Access-Control-Allow-Headers: Content-Type, PAYMENT-SIGNATURE, X-PAYMENT&lt;br&gt;
Quick way to check your own server without spending anything: send garbage in each header and compare the errors. If the new one says the header is missing while the old one says it's malformed, you're only reading the old one.&lt;/p&gt;

&lt;p&gt;bash&lt;br&gt;
curl -s -X POST -H "X-PAYMENT: junk"         &lt;a href="https://your-api/paid-route" rel="noopener noreferrer"&gt;https://your-api/paid-route&lt;/a&gt;&lt;br&gt;
curl -s -X POST -H "PAYMENT-SIGNATURE: junk" &lt;a href="https://your-api/paid-route" rel="noopener noreferrer"&gt;https://your-api/paid-route&lt;/a&gt;&lt;br&gt;
unsupported scheme: undefined&lt;br&gt;
Fixing the header name isn't enough, because the payload changed shape too. In v1, scheme and network sat at the top of the payment payload. In v2 they live inside accepted, the option the buyer picked from your quote. From the type in @x402/core 2.25.0:&lt;/p&gt;

&lt;p&gt;ts&lt;br&gt;
type PaymentPayload = {&lt;br&gt;
    x402Version: number;&lt;br&gt;
    resource?: ResourceInfo;&lt;br&gt;
    accepted: PaymentRequirements;&lt;br&gt;
        payload: { [key: string]: unknown };&lt;br&gt;
    extensions?: { [key: string]: unknown };&lt;br&gt;
};&lt;br&gt;
A hand-written v1 decoder looks for p.scheme, finds nothing, and rejects a perfectly good payment. The signature and authorization inside payload are the same in both versions for the exact scheme, so the fix is small:&lt;/p&gt;

&lt;p&gt;js&lt;br&gt;
const p = JSON.parse(Buffer.from(raw, "base64").toString("utf8"));&lt;br&gt;
const scheme  = p.accepted?.scheme  ?? p.scheme;   // v2 : v1&lt;br&gt;
const network = p.accepted?.network ?? p.network;  // v2 : v1&lt;br&gt;
paymentStatus: none&lt;br&gt;
After fixing the two above, a purchase with the official client worked: 200 in 1.1 s, the USDC moved. And the client reported:&lt;/p&gt;

&lt;p&gt;paymentStatus = none&lt;br&gt;
The buyer paid, and their software believes they didn't. We were settling, writing the transaction to our own ledger, and returning only the product, with no receipt. An agent keeping its own books records $0 spent, and its owner later finds a charge the agent says it never made. That's the opposite of trust.&lt;/p&gt;

&lt;p&gt;Fix: return the settlement as a receipt, base64 JSON, in both header names:&lt;/p&gt;

&lt;p&gt;js&lt;br&gt;
const receipt = Buffer.from(JSON.stringify({&lt;br&gt;
  success: true,&lt;br&gt;
  transaction: settle.transaction,&lt;br&gt;
  network,&lt;br&gt;
  payer,                          // authorization.from&lt;br&gt;
}), "utf8").toString("base64");&lt;br&gt;
res.setHeader("PAYMENT-RESPONSE", receipt);    // v2&lt;br&gt;
res.setHeader("X-PAYMENT-RESPONSE", receipt);  // v1&lt;br&gt;
res.setHeader("Access-Control-Expose-Headers", "PAYMENT-RESPONSE, X-PAYMENT-RESPONSE");&lt;br&gt;
Same purchase afterwards: paymentStatus = settled.&lt;/p&gt;

&lt;p&gt;A bad signature returns 500&lt;br&gt;
Given a malformed signature, the facilitator doesn't always answer "not valid". It can throw, and if nothing catches it, your buyer gets a 500, which tells them your server is broken. What we got back from a deliberately fake signature:&lt;/p&gt;

&lt;p&gt;invalid_exact_evm_payload_signature: invalid signature: public key recovery code 44 is not in the valid range [27, 34]&lt;br&gt;
Fix: catch it, and tell the two cases apart. A payment problem is a 402 with the reason. Anything else (the facilitator being down) is a 503: telling a buyer "your payment is bad" when the problem is on your side makes them re-sign in a loop.&lt;/p&gt;

&lt;p&gt;js&lt;br&gt;
try {&lt;br&gt;
  result = await facilitator.verify(payload, requirements);&lt;br&gt;
} catch (e) {&lt;br&gt;
  const msg = String(e?.message ?? e);&lt;br&gt;
  if (/invalid|insufficient|expired|mismatch/i.test(msg))&lt;br&gt;
    return send402(&lt;code&gt;facilitator rejected payment: ${msg}&lt;/code&gt;);&lt;br&gt;
  return send503("payment verifier unavailable. Nothing was charged; retry later.");&lt;br&gt;
}&lt;br&gt;
Delivering what was paid for&lt;br&gt;
Promising 360 s behind a 300 s proxy&lt;br&gt;
Two separate clocks, both ours. First, we built payment requirements without maxTimeoutSeconds, so every offer promised the 60-second default. Our full report takes two to four minutes. Second, once we fixed that to 360 s, nginx in front of it was still set to cut upstream responses at 300 s. We found this one by reading the config, before it caught a buyer, but it would have been the worst kind: payment settled, nothing delivered.&lt;/p&gt;

&lt;p&gt;Fix: set maxTimeoutSeconds per offer to what the work really takes, say it in the description, and keep every proxy in front above it:&lt;/p&gt;

&lt;p&gt;nginx&lt;br&gt;
location /services/ {&lt;br&gt;
    proxy_pass &lt;a href="http://127.0.0.1:8004" rel="noopener noreferrer"&gt;http://127.0.0.1:8004&lt;/a&gt;;&lt;br&gt;
    proxy_read_timeout 420s;   # above the longest promise (360 s)&lt;br&gt;
    proxy_send_timeout 420s;&lt;br&gt;
}&lt;br&gt;
Charging before delivering&lt;br&gt;
Our first flow was verify, settle, do the work, respond. If the work failed or the buyer's client gave up waiting, they had paid for nothing, and the only remedy was a manual refund.&lt;/p&gt;

&lt;p&gt;Fix: verify, do the work, settle, respond. Verification already proves the payment is good, so a fake signature still can't make you work for free. If the work fails, you don't settle: the buyer keeps their money and you lose the compute. That's the right way round.&lt;/p&gt;

&lt;p&gt;verify → do the work → settle → respond with product + receipt&lt;br&gt;
            └─ work failed? don't settle. They keep their money.&lt;br&gt;
Signatures that expire mid-job&lt;/p&gt;

&lt;p&gt;Settling last creates its own trap. The buyer's authorization has a &lt;code&gt;validBefore&lt;/code&gt;. If it expires while you're still working, you deliver and then can't collect, every time, from that buyer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Our first fix was wrong, and for three days this article recommended it.&lt;/strong&gt; We refused any authorization that didn't live at least as long as the job could take:&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;need&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;requirements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxTimeoutSeconds&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="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;validBefore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;need&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;send402&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;   &lt;span class="c1"&gt;// don't copy this&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It rejects the official client. The client signs &lt;code&gt;validBefore = now + maxTimeoutSeconds&lt;/code&gt;, exactly what you quote. By the time the payment reaches you a second has passed, so it is always one second short:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;payment authorization expires too soon: valid for 59s, but this offer can take up to 60s to deliver.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It slipped past us because our first test purchase happened to land inside the same second. The next one didn't. The refusals were in our logs the whole time; nobody was reading them. (We checked the full log: the only buyer it ever turned away was our own test.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; keep the check, with a margin for network latency and clock skew, and keep it above what the job really takes. Don't drop it to a flat 30 s: if you settle last, a three-minute job accepted with 35 s of validity is a job delivered for free.&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;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;quoted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;requirements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxTimeoutSeconds&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                &lt;span class="c1"&gt;// what you advertised&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;need&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;quoted&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;quoted&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;// margin: latency + skew&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;validBefore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;need&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;send402&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`payment authorization expires too soon. `&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="s2"&gt;`Re-sign with validBefore &amp;gt;= &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;need&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;. Nothing was charged.`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With our quotes that is 60 → 30 s, 180 → 150 s, 360 → 330 s, each still above how long the job really takes. And log every refusal somewhere you actually read.&lt;br&gt;
And one that was ours alone&lt;br&gt;
The field we made up&lt;br&gt;
While building the discovery files we copied the shape of a field from another project's manifest instead of from the spec, and shipped it in a form the spec doesn't define. Nothing complained. It was just wrong. Validate your discovery files against the spec, not against somebody else's JSON, however well that project seems to be doing.&lt;/p&gt;

&lt;p&gt;Buy from yourself, the way a stranger would&lt;/p&gt;

&lt;p&gt;Four of the bugs above (the header name, the payload shape, the missing receipt, our own expiry check) were invisible to every directory, because none of them pays, and invisible to our own test buyer, because we'd written it in the same old dialect as the server. They showed up the first time we paid with the official client. Use a separate test wallet, not the one that receives the money.&lt;/p&gt;

&lt;p&gt;This is the whole buyer, from the x402 quickstart. By default the official client refuses to pay more than $1 per request.&lt;/p&gt;

&lt;p&gt;bash&lt;br&gt;
npm install @x402/fetch @x402/evm viem&lt;br&gt;
js&lt;br&gt;
import { wrapFetchWithPayment, x402HTTPClient } from "@x402/fetch";&lt;br&gt;
import { x402Client } from "@x402/core/client";&lt;br&gt;
import { ExactEvmScheme } from "@x402/evm/exact/client";&lt;br&gt;
import { privateKeyToAccount } from "viem/accounts";&lt;/p&gt;

&lt;p&gt;const signer = privateKeyToAccount(process.env.EVM_PRIVATE_KEY);&lt;br&gt;
const client = new x402Client();&lt;br&gt;
client.register("eip155:*", new ExactEvmScheme(signer));&lt;/p&gt;

&lt;p&gt;const pay = wrapFetchWithPayment(fetch, client);&lt;br&gt;
const res = await pay("&lt;a href="https://your-api/paid-route" rel="noopener noreferrer"&gt;https://your-api/paid-route&lt;/a&gt;", {&lt;br&gt;
  method: "POST",&lt;br&gt;
  headers: { "Content-Type": "application/json" },&lt;br&gt;
  body: JSON.stringify({ /* your input */ }),&lt;br&gt;
});&lt;/p&gt;

&lt;p&gt;const result = await new x402HTTPClient(client).processResponse(res);&lt;br&gt;
console.log(res.status, result.paymentStatus);   // want: 200 settled&lt;br&gt;
If you want a live v2 seller to point a client at while you build one, ours answers at &lt;a href="https://swarm-agent.net/services/utilities" rel="noopener noreferrer"&gt;https://swarm-agent.net/services/utilities&lt;/a&gt;, and the cheapest call is $0.02.&lt;/p&gt;

&lt;p&gt;Originally published at swarm-agent.net/x402-seller-gotchas, where it gets updated as we hit new ones.&lt;/p&gt;

&lt;p&gt;This article was written with AI assistance. The errors, outputs and fixes above come from our own server and were checked before publishing.&lt;/p&gt;

</description>
      <category>x402</category>
      <category>api</category>
      <category>webdev</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
