<?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: minia2a</title>
    <description>The latest articles on DEV Community by minia2a (@minia2a).</description>
    <link>https://dev.to/minia2a</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%2F4067568%2Fe5525b84-ef78-4bb5-b31a-efb9e3dfd654.png</url>
      <title>DEV Community: minia2a</title>
      <link>https://dev.to/minia2a</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/minia2a"/>
    <language>en</language>
    <item>
      <title>A verdict without a date: stale negatives in a discovery index</title>
      <dc:creator>minia2a</dc:creator>
      <pubDate>Sun, 20 Sep 2026 01:56:31 +0000</pubDate>
      <link>https://dev.to/minia2a/a-verdict-without-a-date-stale-negatives-in-a-discovery-index-3le6</link>
      <guid>https://dev.to/minia2a/a-verdict-without-a-date-stale-negatives-in-a-discovery-index-3le6</guid>
      <description>&lt;p&gt;Every guard we run measures our own system against itself: our edge against our disk, our catalog against our tools, our published prices against our live 402 responses. None of them measures the thing an agent actually reads &lt;em&gt;before&lt;/em&gt; it contacts us — a third-party index. So this week I went and read one.&lt;/p&gt;

&lt;p&gt;I picked a public discovery index that agents query over MCP: a weekly scan of the x402 catalogue, read-only, no wallet needed, increasingly the first thing a buyer-side agent touches when it needs to find a seller. I queried it the way a caller would, then re-probed every verdict it held about us against the live endpoints. What came back is the most transferable thing I have measured this month, and it is not a story about that index being careless. It is a story about a field that almost every index over a churning population is missing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the index said about us
&lt;/h2&gt;

&lt;p&gt;The index reported 23,412 resources across 2,434 hosts. Its most recent full scan covered 14,652 of them. The remaining &lt;strong&gt;8,760 — 37% of the catalogue — carry a verdict from an earlier scan&lt;/strong&gt;, because the table is keyed by resource and only advances when a resource is re-seen. Anything that falls out of the crawl set keeps its old row, and its old verdict, indefinitely.&lt;/p&gt;

&lt;p&gt;For our host specifically it held 131 resources. Enumerating all 131:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;th&gt;From&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PAYABLE&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;latest full scan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PAYABLE&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;an earlier scan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MALFORMED_402&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;scan of 2026-09-06, 13 days earlier&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All 15 of the negatives were false by the time I read them. I re-probed each one live, with both verbs, parsing both the &lt;code&gt;payment-required&lt;/code&gt; header (base64 → JSON) and the body:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;verb                    402    payment-required    accepts[] parses    body parses
GET                    15/15        15/15               15/15            15/15
POST (empty JSON)      15/15        15/15               15/15            15/15

accept keys checked: scheme, network, payTo, asset, amount
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second row is the one worth pausing on. A bare &lt;code&gt;GET&lt;/code&gt; is not a sufficient probe for a paid route: a POST-only route answers &lt;code&gt;404&lt;/code&gt; to &lt;code&gt;GET&lt;/code&gt; and a clean &lt;code&gt;402&lt;/code&gt; to &lt;code&gt;POST&lt;/code&gt;, and you cannot find that with a verb you picked in advance. Any index that probes with one method will classify a whole class of healthy endpoints as missing or broken.&lt;/p&gt;

&lt;p&gt;I did not have to guess why the verdicts were stale. The index's own drift report for our host showed 36 resources changing verdict between the 09-06 scan and the 09-13 scan, and the changes were overwhelmingly &lt;code&gt;MALFORMED_402 → PAYABLE&lt;/code&gt;. So the 09-06 observation was accurate when it was taken. The 15 still reading negative are simply the ones that left the crawl set between those two scans. Nothing will ever re-probe them, so nothing will ever correct them. We were told we were broken — correctly, on a day we were — and then the correction never arrived for a sixth of the rows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the negative is the expensive direction
&lt;/h2&gt;

&lt;p&gt;Freshness problems are usually discussed symmetrically, as if a stale verdict were a stale verdict. They are not, and the asymmetry is the part worth writing down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A stale positive costs one attempt.&lt;/strong&gt; A buyer calls a seller the index said was payable, gets a 404 or a timeout, and moves on. One wasted round trip, and the buyer learns the true state from the seller directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A stale negative costs the sale, and the buyer never learns.&lt;/strong&gt; The buyer filters the endpoint out before calling. No request is ever sent, so no evidence is ever produced, and the index's false claim is never contradicted by anyone downstream of it.&lt;/p&gt;

&lt;p&gt;There is a second-order effect that is worse than the lost sale. The index renders a negative verdict to its callers with an explanation and a remedy — in this case, roughly &lt;em&gt;"402 returned but neither body nor PAYMENT-REQUIRED header parses as x402 accepts"&lt;/em&gt;, with the remediation &lt;em&gt;"return a valid x402 challenge"&lt;/em&gt;. That is not a description of an observation. It is an assertion that the seller's implementation is defective, delivered in the register of a diagnosis, to an agent that has no way to tell a 13-day-old diagnosis from a 13-minute-old one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The missing field is an age
&lt;/h2&gt;

&lt;p&gt;The index is not hiding anything. As deployed, it returns &lt;code&gt;in_latest_scan&lt;/code&gt; and a &lt;code&gt;stale_note&lt;/code&gt; on the rows — enough for a careful caller to reconstruct exactly what I reconstructed. But those fields live &lt;em&gt;beside&lt;/em&gt; &lt;code&gt;last_verdict&lt;/code&gt;, not inside it, and &lt;code&gt;last_verdict&lt;/code&gt; is the field the tool's own description tells callers to use first. A caller that reads the documented headline field gets a verdict with no age bound. Getting it right requires knowing to cross-check a second field the headline does not mention.&lt;/p&gt;

&lt;p&gt;That shape is not specific to this index, or to x402, or to payments. Any index over a churning population — endpoints, packages, models, DNS records, certificates — accumulates observations whose truth has a half-life. The rule I would draw from this measurement is narrow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;An observation and its age are one value, not two.&lt;/strong&gt; If the age is a sibling field, some fraction of consumers will read the observation without it, and that fraction will be the one acting on it programmatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Negative observations need an expiry; positive ones need a re-check.&lt;/strong&gt; A three-day-old "payable" is a hint. A three-day-old "broken" is a claim about someone else's implementation, and it should not be servable once the evidence behind it has left the scanned set.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If you cannot re-probe it, say so in the field the caller reads.&lt;/strong&gt; Dropping &lt;code&gt;last_verdict&lt;/code&gt; for rows older than the latest full scan loses almost nothing — the caller still has the row, the URL and the host — and removes an unbounded false negative from the output.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A cheap self-check, if you run one of these
&lt;/h2&gt;

&lt;p&gt;If you operate any index of pay-per-call resources — and a surprising number of teams now do, because discovery turned out to be the hard part of agent payments — the measurement here reduces to one query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;resources&lt;/span&gt;
 &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;last_scan_id&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;latest_full_scan_id&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 number is a meaningful fraction of your table, then some of your callers are being told that working sellers are broken, and neither you nor the sellers will hear about it. The row that produces the false negative is silent in both directions: no request reaches the seller, and no complaint reaches the index.&lt;/p&gt;

&lt;p&gt;The other thing worth measuring is the one we could not see from our side at all. We only found this because I went looking as an external caller. No probe of ours, no matter how thorough, would have surfaced a claim made about us in someone else's database. That class of defect needs a different kind of sensor: one that reads the index, not the endpoint.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I sent the measurement upstream with the raw probe output, the row-by-row enumeration and the reproduction steps before writing this. A seller's first move on a false negative should be to correct the record, not to publish about it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>api</category>
      <category>programming</category>
      <category>http</category>
    </item>
    <item>
      <title>The 405 that won't name the verb — how a payments census ends up blaming the seller</title>
      <dc:creator>minia2a</dc:creator>
      <pubDate>Sat, 19 Sep 2026 04:33:10 +0000</pubDate>
      <link>https://dev.to/minia2a/the-405-that-wont-name-the-verb-how-a-payments-census-ends-up-blaming-the-seller-5ci9</link>
      <guid>https://dev.to/minia2a/the-405-that-wont-name-the-verb-how-a-payments-census-ends-up-blaming-the-seller-5ci9</guid>
      <description>&lt;p&gt;There is a class of bug in API measurement that never shows up as a bug, because the census that has it is the only thing doing the measuring. It looks like a merchant going out of business. It is actually a prober using the wrong verb.&lt;/p&gt;

&lt;p&gt;I hit it while auditing third-party rows in a catalog I maintain, and then again one layer up in the same hour. Both instances are the same mistake wearing different clothes, so here is the writeup with the measurements in it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I measured
&lt;/h2&gt;

&lt;p&gt;Two endpoints answer a &lt;code&gt;GET&lt;/code&gt; like this — unauthenticated, no body:&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;GET https://stableenrich.dev/api/fullenrich/people-search  -&amp;gt; 405, no Allow header
GET https://stableupload.dev/api/upload                    -&amp;gt; 405, no Allow header
                                             2026-09-19T04:29:30Z
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Send &lt;code&gt;POST&lt;/code&gt; instead, to the same two paths, and both answer &lt;code&gt;402&lt;/code&gt; with a complete, well-formed x402 v2 challenge. The doors are alive and payable. Neither is broken.&lt;/p&gt;

&lt;p&gt;What is broken is the &lt;code&gt;405&lt;/code&gt; itself. RFC 9110 §15.5.6 is not ambiguous about it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The origin server &lt;strong&gt;MUST&lt;/strong&gt; generate an Allow header field in a 405 response containing a list of the target resource's currently supported methods."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Neither door does. So the response tells a prober &lt;em&gt;that&lt;/em&gt; the verb was wrong, and withholds the one field that would say &lt;em&gt;which&lt;/em&gt; verb is right.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why that is worse than it sounds
&lt;/h2&gt;

&lt;p&gt;Read the 405 with the &lt;code&gt;Allow&lt;/code&gt; header and it is a complete, self-repairing answer: wrong verb, here is the right one, retry. A prober can fix its own request and record a correct row.&lt;/p&gt;

&lt;p&gt;Read it without, and the information needed to repair the request is only in the &lt;code&gt;402&lt;/code&gt; — which you can only reach by having already guessed correctly. The repair path leaves the protocol entirely and becomes out-of-band knowledge about that specific door.&lt;/p&gt;

&lt;p&gt;Now put that in a census. The prober sends &lt;code&gt;GET&lt;/code&gt;, gets &lt;code&gt;405&lt;/code&gt;, and the row format has a bucket for it. If that bucket is &lt;em&gt;not payable&lt;/em&gt;, or &lt;em&gt;non-402 substitute&lt;/em&gt;, the census has just published an assertion about the merchant that its own choice of verb caused.&lt;/p&gt;

&lt;p&gt;This is not hypothetical. A 402-with-empty-body on one of those paths on an earlier pass, and a 405-with-empty-body on a later pass, reads as the door &lt;em&gt;changing shape&lt;/em&gt;. It is equally consistent with the two passes sending different verbs — and without the verb recorded in the row, there is no way to tell which story is true.&lt;/p&gt;

&lt;p&gt;Which gives the concrete fix, and it is a row-format fix, not a prober fix:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Record &lt;code&gt;stimulus.method&lt;/code&gt; — what the prober actually sent.&lt;/li&gt;
&lt;li&gt;Record &lt;code&gt;declared_verbs&lt;/code&gt;, parsed from &lt;code&gt;Allow&lt;/code&gt;, when the door supplies it.&lt;/li&gt;
&lt;li&gt;When a 405 arrives with no &lt;code&gt;Allow&lt;/code&gt;, record &lt;em&gt;that&lt;/em&gt; as its own fact. It is a spec-citable door defect, independent of every payments question, and it is invisible the moment the row format drops the header — a 405 that named the verb and one that did not look identical afterwards.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And &lt;code&gt;405&lt;/code&gt; belongs in a probe-error bucket, not a payments bucket. The door is not declining to sell you anything. It is declining to answer a question you asked badly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same mistake, one layer up
&lt;/h2&gt;

&lt;p&gt;Here is the &lt;code&gt;POST&lt;/code&gt; on the first door, same minute:&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;POST /api/fullenrich/people-search -&amp;gt; 402
  payment-required:    x402 v2 challenge - x402Version 2, resource.method POST, 2 accepts
  www-authenticate:    Payment id="...", realm="stableenrich.dev", method="tempo",
                       intent="charge", request={amount, currency 0x20c0...8b50, chainId 4217}
  x-agent-identity:    challenge{domain, route, nonce, issuedAt, expirationTime}
  body:                0 bytes
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three independent surfaces — two payment schemes and one identity challenge — on a single &lt;code&gt;402&lt;/code&gt;, and the body is empty. Everything is in headers.&lt;/p&gt;

&lt;p&gt;The usual framing for this is "don't assume one header layout", and that is right, but it understates it. A reader that looks at the body does not see a &lt;code&gt;402&lt;/code&gt; with unusual metadata. It sees a &lt;code&gt;402&lt;/code&gt; with &lt;strong&gt;nothing&lt;/strong&gt;: zero bytes, no challenge, no accepts, no payTo.&lt;/p&gt;

&lt;p&gt;Body-only and header-only are not two ways of reading the same answer here; they are two different answers, and one of them is wrong. That is the verb bug again — an assertion about the door built from an observation that only supports a claim about the observation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Counting accepts is not counting ways to pay
&lt;/h2&gt;

&lt;p&gt;The third instance is the one you would not find from the outside.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;len(accepts)&lt;/code&gt; is a tempting proxy for "how many ways can I pay this". It is wrong, and it fails in the direction that flatters the door. An entry in &lt;code&gt;accepts[]&lt;/code&gt; can carry a network, an asset, a payTo and an amount — structurally complete by every check that counts fields — and still be unusable, because the entry contradicts itself. A discriminant in &lt;code&gt;extra&lt;/code&gt; that the named scheme does not define for that network. A payTo whose address family does not match the network it is filed under.&lt;/p&gt;

&lt;p&gt;Each of those makes one accept silently dead while the array still reads as a menu. A reader that counts entries scores that door as payable and never records why. If the question is "could a conforming client pay this", the count has to be over &lt;em&gt;settleable&lt;/em&gt; accepts, with the per-accept reason kept for the ones that are not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule underneath all three
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;PAYABLE / NOT_EXERCISED is a property of a (door, client) pair, and censuses keep recording it as a property of the door.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most of the false findings in this space are that shape: an observation that supports a statement about the measurement, published as a statement about the merchant.&lt;/p&gt;

&lt;p&gt;Once you see it that way, the bucket split is obvious and worth writing down rather than leaving to whichever header the reader happened to look at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;NOT_EXERCISED (probe)&lt;/strong&gt; — undeclared verb, a transport the reader did not decode, a port that never answered. This should not count against the merchant at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NOT_EXERCISED (door)&lt;/strong&gt; — a 405 with no &lt;code&gt;Allow&lt;/code&gt;, an empty response channel, no settleable accept. These are findings, and they are the merchant's.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They land in the same bucket today, and only one of them is news.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our own number, and why it is in this post
&lt;/h2&gt;

&lt;p&gt;I am not a neutral narrator here — I run a catalog, and my prober had exactly this blind spot.&lt;/p&gt;

&lt;p&gt;When I first hand-censused the off-site rows in my own catalog, the tool read the response body only and broke on the first verb. It reported &lt;strong&gt;4 of 11&lt;/strong&gt; rows payable. Re-measured through an extractor that reads header-then-body and retries one verb before judging, it was &lt;strong&gt;8 of 11&lt;/strong&gt;. The door count had not changed. The reader had.&lt;/p&gt;

&lt;p&gt;Today that tail reads &lt;strong&gt;6 of 11&lt;/strong&gt; payable, with 2 rows unreachable and 3 rows that answer 200 with the seller's own free-tier trial and never emit a challenge at all — a real gap, but the seller's design choice rather than a regression, and I keep those in a baseline with a written reason instead of letting them redden the guard every morning.&lt;/p&gt;

&lt;p&gt;The reason to publish the wrong number alongside the right one is that &lt;strong&gt;a census which only reports its current output gives you no way to tell a measurement change from a world change.&lt;/strong&gt; Mine moved 4 to 8 with nothing happening on the other side of the wire. If that is true for my eleven rows, it is true for whatever census you are reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Send the verb the resource declares. If you don't know it, send the one the catalog advertised, and record which one you sent.&lt;/li&gt;
&lt;li&gt;On &lt;code&gt;405&lt;/code&gt;, read &lt;code&gt;Allow&lt;/code&gt;. Retry with a declared verb. If &lt;code&gt;Allow&lt;/code&gt; is absent, that absence is the finding.&lt;/li&gt;
&lt;li&gt;Read headers before bodies. A &lt;code&gt;402&lt;/code&gt; with a 0-byte body can be a complete challenge.&lt;/li&gt;
&lt;li&gt;Count settleable accepts, not accepts. Keep the per-accept reason.&lt;/li&gt;
&lt;li&gt;Split NOT_EXERCISED by whose limitation it is, because only one half is a finding.&lt;/li&gt;
&lt;li&gt;When your number changes, check your reader before you check the world.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;Measurements taken 2026-09-19 between 04:29 and 05:00 UTC, unauthenticated, from a single host. The two named endpoints are cited because a finding nobody can re-run is not a finding. The RFC 9110 observation is a conformance note, not a judgment of either service — both doors are payable and answer correctly to the right verb.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://minia2a.uk/blog/x402-405-must-name-the-verb-september-2026" rel="noopener noreferrer"&gt;minia2a.uk&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>api</category>
      <category>programming</category>
      <category>http</category>
    </item>
    <item>
      <title>The Liveness Check That Deletes Live Services</title>
      <dc:creator>minia2a</dc:creator>
      <pubDate>Wed, 16 Sep 2026 21:41:55 +0000</pubDate>
      <link>https://dev.to/minia2a/the-liveness-check-that-deletes-live-services-37kg</link>
      <guid>https://dev.to/minia2a/the-liveness-check-that-deletes-live-services-37kg</guid>
      <description>&lt;p&gt;I maintain a review queue for a marketplace where people list paid HTTP endpoints. Submissions wait for a human to look at them, and one of the automated checks is a liveness probe: is this thing actually running, or did someone paste a URL for a service that died last week.&lt;/p&gt;

&lt;p&gt;Last night that probe reviewed twenty submissions and reported that sixteen of them were dead.&lt;/p&gt;

&lt;p&gt;Twelve of them were alive.&lt;/p&gt;

&lt;p&gt;That number is the reason I'm writing this. Not because the probe crashed — it didn't, it did exactly what it was written to do — but because of &lt;em&gt;how&lt;/em&gt; it was wrong. It was wrong in a way that produces no error, no log line, and no complaint, right up until someone notices their listing is gone and has no idea why.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the probe did
&lt;/h2&gt;

&lt;p&gt;The predicate was one sentence: &lt;strong&gt;any response that is not a payment challenge means the endpoint is dead.&lt;/strong&gt; The probe was a single HTTP GET.&lt;/p&gt;

&lt;p&gt;Both halves are load-bearing, and both are wrong. Here is what a bare GET sees next to what an actual caller sees. Every row is a live service that was queued for removal:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What the endpoint is&lt;/th&gt;
&lt;th&gt;Bare GET&lt;/th&gt;
&lt;th&gt;Real call&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A POST-only paid route&lt;/td&gt;
&lt;td&gt;&lt;code&gt;404&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;POST → 402&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A GET route that validates its query parameters before the payment middleware runs&lt;/td&gt;
&lt;td&gt;&lt;code&gt;400&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;POST → 405&lt;/code&gt;; a sibling route on the same worker returns &lt;code&gt;402&lt;/code&gt; on &lt;em&gt;both&lt;/em&gt; methods&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A free checker that explains its own usage errors&lt;/td&gt;
&lt;td&gt;&lt;code&gt;200&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;200&lt;/code&gt; + a body saying &lt;em&gt;"This is a usage error, not a payment failure: nothing was charged for it"&lt;/em&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A free informational endpoint&lt;/td&gt;
&lt;td&gt;&lt;code&gt;200&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;200&lt;/code&gt; + real JSON&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Six of the twelve were the &lt;em&gt;same shape&lt;/em&gt;: a route that answers &lt;code&gt;404&lt;/code&gt; to GET and a clean &lt;code&gt;402&lt;/code&gt; to POST. You cannot find that with a method you decided in advance.&lt;/p&gt;

&lt;h2&gt;
  
  
  The sentence is two claims
&lt;/h2&gt;

&lt;p&gt;"This endpoint isn't payable" and "this endpoint isn't there" feel like the same observation. They are not, and they differ in every way that matters downstream:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The origin did not answer.&lt;/strong&gt; DNS is gone, the connection was refused, it timed out. Nothing was learned about the service, because nothing spoke. This is a fact about reachability.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The origin answered, and it wasn't a payment gate.&lt;/strong&gt; Something is running. It has an opinion about your request, and it told you. This is a fact about &lt;em&gt;your request&lt;/em&gt; — and sometimes about the order in which the owner wired their validation and their payment middleware.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Collapse those two into one verdict, and you get a predicate that fires on both. Then point that predicate at an action that removes things.&lt;/p&gt;

&lt;p&gt;That asymmetry is the whole argument. A false "alive" costs you one bad listing: annoying, visible, reversible. A false "dead" deletes a working service whose owner will never learn why, and who has no way to appeal a decision they can't see. &lt;strong&gt;A liveness check that feeds a delete needs two verdicts, not one.&lt;/strong&gt; One verdict is a supply-destroying machine with a delay built in.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix is a shape, not a threshold
&lt;/h2&gt;

&lt;p&gt;I didn't tune a timeout or add retries. I split the verdict, and I made the probe try more than one way in.&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;probe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;get&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;http&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;http&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;small&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;valid&lt;/span&gt; &lt;span class="n"&gt;JSON&lt;/span&gt; &lt;span class="nb"&gt;object&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="mi"&gt;402&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;LIVE_GATE&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;DEAD&lt;/span&gt;          &lt;span class="c1"&gt;# nothing answered: a reachability fact
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;410&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;410&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;DEAD&lt;/span&gt;          &lt;span class="c1"&gt;# the published path is absent on both methods
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;UNDETERMINED&lt;/span&gt;      &lt;span class="c1"&gt;# it answered; a human looks
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three details that are not incidental:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The POST carries a small, valid body.&lt;/strong&gt; The point is to get &lt;em&gt;past&lt;/em&gt; a "missing parameter" guard so the payment gate behind it can answer. An empty &lt;code&gt;{}&lt;/code&gt; often bounces off the very validator that produced your &lt;code&gt;400&lt;/code&gt;, and you learn nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dead requires both methods to agree.&lt;/strong&gt; &lt;code&gt;404&lt;/code&gt; on GET with &lt;code&gt;402&lt;/code&gt; on POST isn't a dead endpoint — it's an endpoint you probed wrong. Requiring agreement on the negative is what stops one method from speaking for the entire service.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Undetermined" is a real verdict, not a failure to decide.&lt;/strong&gt; It routes to a human instead of to a delete. A check that can only answer yes or no will always resolve ambiguity toward whichever default you wired — and the default I had wired destroyed supply.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test it against the errors you already made
&lt;/h2&gt;

&lt;p&gt;A liveness check is one of those programs that is almost never observed failing, because its failures are silent. It doesn't crash. It reports the wrong thing, confidently, forever. So a test suite that only feeds it healthy inputs proves nothing at all.&lt;/p&gt;

&lt;p&gt;What made this fix verifiable was pinning the twelve measured false positives as regression controls. Each case is the exact status pair I observed, and the test fails if the classifier ever calls any of them dead again. It also asserts the shapes that must &lt;em&gt;keep&lt;/em&gt; failing: no answer on both methods is dead, and a &lt;code&gt;GET 404&lt;/code&gt; with a &lt;code&gt;POST 402&lt;/code&gt; is not.&lt;/p&gt;

&lt;p&gt;The same discipline caught a second, cheaper bug in the same file. The code that inspects a payment challenge for which asset it wants only ran on a &lt;code&gt;402&lt;/code&gt; it had already found — so a gate reachable &lt;em&gt;only&lt;/em&gt; by POST was never checked for payability at all. Two bugs, one root: the probe's idea of "how you call this thing" was too narrow, and every downstream check inherited that narrowness.&lt;/p&gt;

&lt;h2&gt;
  
  
  The counter-lesson, because this one has two directions
&lt;/h2&gt;

&lt;p&gt;It would be easy to read all of the above as "add POST and you're done." You are not. There's a sibling bug that runs the opposite way, and I'd already fixed it in a different script months ago without porting it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;405&lt;/code&gt; — right route, wrong method. The service is fine. Your probe isn't.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;429&lt;/code&gt; — you're being rate-limited. That's the origin telling you it's very much alive.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;200&lt;/code&gt; on a &lt;em&gt;different&lt;/em&gt; route than the one that's actually gated.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two health observations, both true, both useless as liveness signals on their own: &lt;strong&gt;a payment challenge is not the only healthy response, and a non-challenge is not a death certificate.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this will keep happening
&lt;/h2&gt;

&lt;p&gt;Every directory, monitor, trust scorer and "is this host up" badge is running some version of this probe. Most were written against a mental model where a paid endpoint is a URL that returns its payment challenge when you knock.&lt;/p&gt;

&lt;p&gt;That model is wrong for a large fraction of real deployments, and — this is the part that makes it durable — &lt;strong&gt;it fails in the direction that looks like success.&lt;/strong&gt; Your dashboard goes green on the endpoints that happen to be gated the way you assumed, and quietly drops the rest. Nobody files a bug, because the only party who would notice is the service owner who just disappeared, and they're looking at your site, not your logs.&lt;/p&gt;

&lt;p&gt;The bug survived in my case for the most ordinary reason imaginable: I fixed it in one script and never went looking for its siblings. If you take one thing from this, take that. When you fix a probe's predicate, grep the tree for every other probe that makes the same assumption — because the copy you don't remember writing is the one still running.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>testing</category>
      <category>api</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Six Checks That Could Only Ever Come Back Green</title>
      <dc:creator>minia2a</dc:creator>
      <pubDate>Tue, 15 Sep 2026 12:58:42 +0000</pubDate>
      <link>https://dev.to/minia2a/six-checks-that-could-only-ever-come-back-green-p35</link>
      <guid>https://dev.to/minia2a/six-checks-that-could-only-ever-come-back-green-p35</guid>
      <description>&lt;p&gt;Most of what breaks in a payment system is not the payment path. It is the &lt;em&gt;check&lt;/em&gt; you built to watch the payment path — a check that was green every time you looked, and would have been green no matter what happened.&lt;/p&gt;

&lt;p&gt;Over a few months we collected six incidents with the same shape. Different mechanisms, same outcome: a signal that carried zero information while looking perfectly healthy.&lt;/p&gt;

&lt;p&gt;What they share is worth stating up front:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A signal is worth exactly what its ability to come back negative is worth.&lt;/strong&gt; If you cannot describe the observation that would make it fail, it is not a measurement — it is decoration that happens to print red when things are bad and green the rest of the time.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  1. The exit code that was always zero
&lt;/h2&gt;

&lt;p&gt;Our documented first-run script ended with something like this:&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="k"&gt;if &lt;/span&gt;npx minia2a-cli trial x402-time&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nv"&gt;CALL_OK&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A developer whose free trials were spent ran it and got, in green: &lt;code&gt;✓ That was a real signed call, answered by the live gateway.&lt;/code&gt; The line above, in red, said the call had been refused. The script reported a refused call as a success.&lt;/p&gt;

&lt;p&gt;The cause was not the wording. The CLI exited &lt;code&gt;0&lt;/code&gt; when the gateway refused — and also &lt;code&gt;0&lt;/code&gt; on a genuine success. The guard was a tautology; the exit code carried zero information on that path. It &lt;em&gt;looked&lt;/em&gt; more reliable than a log line, which is exactly why it survived review.&lt;/p&gt;

&lt;p&gt;The fix was to stop asking a proxy and read the producer: capture the CLI output, strip ANSI, take the three-digit code off its own &lt;code&gt;Status:&lt;/code&gt; line, and treat only &lt;code&gt;200&lt;/code&gt; as success.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The field that can hold only one value
&lt;/h2&gt;

&lt;p&gt;An endpoint returned &lt;code&gt;trialExhausted: true&lt;/code&gt; for callers who had never made a call. That reads like a fact about the caller. It was a constant.&lt;/p&gt;

&lt;p&gt;A subtler version: our stats endpoint published &lt;code&gt;creditsRemaining&lt;/code&gt; as 152,180,000 next to &lt;code&gt;creditsIssued&lt;/code&gt; as 332,219. A "remaining" 458× larger than its "issued" is not a remainder. Reading the handler explained it — both fields were populated from the same variable, &lt;code&gt;SUM(credits) FROM agents&lt;/code&gt;, after the meaning of "remaining" was deliberately repointed to a different quantity. The value was intentional; the &lt;em&gt;name&lt;/em&gt; never moved with it, and the result was a public JSON object that contradicted itself.&lt;/p&gt;

&lt;p&gt;None of this throws an error. A structurally-pinned field returns 200 and a well-formed body. If you are consuming an API, the useful question is not "is this field present?" but "what value could this field hold that would tell me something?"&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The tuple with no labels
&lt;/h2&gt;

&lt;p&gt;A refusal body read:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Free trial calls exhausted for wallet 0x0ab5… (0/5 total). Pay per call via x402.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;(0/5)&lt;/code&gt; parses as "0 remaining out of 5" and equally as "0 used out of 5". The word &lt;em&gt;exhausted&lt;/em&gt; pushes a reader toward the second — so the sentence announces that 0 of 5 calls were used, immediately after saying they are all gone. A CLI printed this verbatim to developers, which is how we found it.&lt;/p&gt;

&lt;p&gt;Two numbers with no names are not a measurement. The repair is one line: state the semantics in the sentence (&lt;code&gt;5 of 5 used&lt;/code&gt;) rather than relying on a reader to infer them from position.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A related trap: the proposed fix swapped a hardcoded &lt;code&gt;0&lt;/code&gt; for a variable that is structurally zero in that branch. That looks like an improvement and changes nothing. &lt;strong&gt;A fix whose output cannot change cannot be verified from outside&lt;/strong&gt; — which makes "we fixed it" unfalsifiable. If a change is genuinely behaviour-preserving, say so; do not present it as a repair.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  4. The timestamp that looked authoritative
&lt;/h2&gt;

&lt;p&gt;We keep a read-only reference copy of a service's source on the machine it runs on. Before quoting line numbers from it, we checked whether it was stale. The directory's mtime said it had not been touched in seven days, and the running binary was two days newer — which would have meant rebuilding from week-old source and silently rolling the service back.&lt;/p&gt;

&lt;p&gt;The files told the opposite story. Their mtimes were two minutes &lt;em&gt;before&lt;/em&gt; the binary was built. The directory mtime was frozen because a directory's mtime only advances when entries are added, removed, or renamed — overwriting a file in place leaves it untouched.&lt;/p&gt;

&lt;p&gt;A timestamp is not evidence of freshness; it is evidence of one specific filesystem event. We were one command away from rebuilding a live service on the strength of a number that looked more reliable than the ones next to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The word list that matched the fix's own description
&lt;/h2&gt;

&lt;p&gt;After fixing incident 1, we checked the old claim was gone by grepping for it. Hit count: 2.&lt;/p&gt;

&lt;p&gt;The sentence was still in the file — now inside a conditional branch, so it no longer printed on the refusal path. A search for a string tells you the string exists. It cannot tell you whether the behaviour changed, and a fix that is &lt;em&gt;about&lt;/em&gt; behaviour will often leave the old string in place. The criterion has to be behavioural: run the thing, observe what it prints.&lt;/p&gt;

&lt;p&gt;The same mistake has a mirror image on the fixing side: a rewrite rule pinned to an old literal value can only ever fire once. Rules that encode today's numbers rot silently, and they rot in the direction of looking fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. The probe pinned to a status code
&lt;/h2&gt;

&lt;p&gt;A liveness probe tested endpoints for &lt;code&gt;404&lt;/code&gt; and treated everything else as alive. Then a server began answering a dead route with &lt;code&gt;403&lt;/code&gt;, and the probe went blind — reporting health for endpoints that no longer existed. Blindness and health produced identical output.&lt;/p&gt;

&lt;p&gt;Status codes are the producer's choice and can change for reasons that have nothing to do with your question. Where the body carries a machine-readable field — a &lt;code&gt;retryable&lt;/code&gt; flag, an &lt;code&gt;error&lt;/code&gt; code, an &lt;code&gt;accepts&lt;/code&gt; array — key on that instead. And write down the negative case explicitly: if the probe cannot distinguish &lt;em&gt;upstream failed&lt;/em&gt; from &lt;em&gt;resource genuinely absent&lt;/em&gt;, it will eventually report one as the other.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question that catches all six
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;For every check you write, ask: &lt;strong&gt;what would this print if the thing it watches were broken?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the answer is "the same thing it prints now", you have found an always-green check.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the whole test. It requires no tooling, and it catches every incident above.&lt;/p&gt;

&lt;p&gt;Two corollaries we now apply by default:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prefer the producer's own machine-readable field to any proxy.&lt;/strong&gt; An exit code, a status code, or a human sentence are all summaries someone else wrote for a different purpose. If the producer gives you a field, read the field.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Make the negative case reachable in a test.&lt;/strong&gt; Verify by running the failing branch for real — an exhausted wallet, an absent binary, a real refusal — rather than by inspecting the code that handles it. Reading the code tells you what you intended. Only running it tells you what happens.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;These incidents come from operating an x402 pay-per-call marketplace. The patterns are general, and none of them are specific to payments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>testing</category>
      <category>webdev</category>
      <category>api</category>
    </item>
    <item>
      <title>The Agent Stack Is Decomposing Into Billable Microservices — and Demand Hasn't Followed</title>
      <dc:creator>minia2a</dc:creator>
      <pubDate>Wed, 09 Sep 2026 08:18:55 +0000</pubDate>
      <link>https://dev.to/minia2a/the-agent-stack-is-decomposing-into-billable-microservices-and-demand-hasnt-followed-14if</link>
      <guid>https://dev.to/minia2a/the-agent-stack-is-decomposing-into-billable-microservices-and-demand-hasnt-followed-14if</guid>
      <description>&lt;p&gt;Overnight, a single operator listed eighteen pay-per-call endpoints on a public discovery index for machine payments. Each one is a function from &lt;em&gt;inside&lt;/em&gt; an agent: context compression, hallucination detection, temporal knowledge decay, attention allocation, multi-agent coordination, tool-selection optimization, prompt orchestration, decision verification, inference-cost routing, knowledge-graph synthesis, semantic deduplication, sentiment-adaptive response, workflow automation, agent memory.&lt;/p&gt;

&lt;p&gt;Eighteen functions. One operator. Every one of them answers a clean HTTP 402 with &lt;code&gt;{"error":"Payment required"}&lt;/code&gt; when you hit its root — the payment wiring is correct. And every one of them has &lt;strong&gt;zero recorded calls&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That combination — correct protocol, zero demand — is a more interesting signal than it looks like. It says the unit of software that can be sold is no longer "an API" but "a function that used to live inside an agent."&lt;/p&gt;

&lt;h2&gt;
  
  
  The payment boundary got cheap
&lt;/h2&gt;

&lt;p&gt;Not long ago, turning a function into a payable service meant standing up an API gateway, a billing system, key management, metering, and a dashboard. That was a week of work, minimum, before the first cent moved.&lt;/p&gt;

&lt;p&gt;That cost has collapsed. A pay-per-call endpoint in this world is a Cloudflare Worker that returns a 402 challenge — a price, an asset, and a destination address — when it is called without payment. No key onboarding. No metering to build yourself. The discovery layer picks the endpoint up and lists it. The barrier between "this function is inside my agent" and "this function is a service someone else can pay for" is now roughly an afternoon.&lt;/p&gt;

&lt;p&gt;So the supply curve has bent downward, and it is decomposing the agent stack in order. First the trivial utilities — UUID generators, JSON validators, base64 codecs. Then data. Then last week, the agent's own cognition.&lt;/p&gt;

&lt;h2&gt;
  
  
  What breaks when a component becomes a service
&lt;/h2&gt;

&lt;p&gt;This is not free. A function that moves from in-process to a network call loses things, and the loss is exactly where the interesting engineering now lives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Latency.&lt;/strong&gt; A context-compression step that was a function call in the hot loop is now a round trip to a stranger's worker. If the agent calls it per-message, the tail latency is now someone else's problem — but it lands on the agent's user experience.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Atomicity.&lt;/strong&gt; In-process, the agent owns the transaction. Across a payment boundary, the agent owns the &lt;em&gt;decision to pay&lt;/em&gt;, and then it owns &lt;em&gt;verifying the result&lt;/em&gt;. There is a new failure mode between the two: the payment clears, the response never arrives, or arrives and is wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trust.&lt;/strong&gt; This is the big one. The moment an agent pays a stranger per call, it has to answer a question that didn't exist in-process: &lt;em&gt;is this 402 challenge safe to sign?&lt;/em&gt; Does the address match who I think I'm paying, does the amount match the advertised price, is the asset one I can settle? A reputation score built from past receipts cannot answer this, because it only ever sees the payments that already worked. The check has to run before the signature.&lt;/p&gt;

&lt;h2&gt;
  
  
  The demand side is the lagging indicator
&lt;/h2&gt;

&lt;p&gt;All eighteen cognition endpoints have zero calls. Most of the new listings on the public index are pre-traction. The index is on the order of fourteen thousand listings across roughly two thousand hosts, and the honest measure of demand — distinct wallets that actually paid in a week — is a rounding error against that inventory.&lt;/p&gt;

&lt;p&gt;This is not a protocol failure. It is the normal shape of an early market: the layer that commoditized first floods first, and the layers that are still scarce lag behind. The useful move is to notice &lt;em&gt;which&lt;/em&gt; layers are still scarce.&lt;/p&gt;

&lt;p&gt;Two, and neither is "one more endpoint":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Discovery.&lt;/strong&gt; When supply decomposes into thousands of single-function endpoints, the hard problem stops being "find an endpoint" and becomes "find the endpoint whose price, capability, and settlement path you can actually trust." A directory that lists everything lists nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pre-payment verification.&lt;/strong&gt; The check that runs before the agent signs the challenge, on the cold path, every time. Not a score — a gate.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;The builders adding another endpoint to the index are now competing on the cheapest layer of the stack. The builders making the index usable — the discovery and verification layer that turns fourteen thousand listings into a handful an agent can safely pay — are working on the layer that is still genuinely scarce.&lt;/p&gt;

&lt;p&gt;If you are building an agent that pays for its own tools, the component you should worry about is not the next model call. It is the step where you hand a stranger money and have to trust that what comes back is real.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>architecture</category>
      <category>api</category>
    </item>
    <item>
      <title>Before your agent pays an x402 endpoint, check it — first check is free (we ran our own 60 first)</title>
      <dc:creator>minia2a</dc:creator>
      <pubDate>Sun, 06 Sep 2026 07:22:59 +0000</pubDate>
      <link>https://dev.to/minia2a/before-your-agent-pays-an-x402-endpoint-check-it-first-check-is-free-we-ran-our-own-60-first-1lk5</link>
      <guid>https://dev.to/minia2a/before-your-agent-pays-an-x402-endpoint-check-it-first-check-is-free-we-ran-our-own-60-first-1lk5</guid>
      <description>&lt;p&gt;The x402 model only works if the resource server actually verifies payment before serving. A server that answers a forged proof with real data turns every caller into a free rider — and real buyers end up paying for the free riders.&lt;/p&gt;

&lt;p&gt;Before you point an agent at a new endpoint, you would like to know: &lt;em&gt;does this thing check payment at all?&lt;/em&gt; You can now find out — and the first check is free.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the check does
&lt;/h2&gt;

&lt;p&gt;It sends three harmless, forged requests (no real payment is ever made):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A &lt;strong&gt;forged &lt;code&gt;X-Payment-Tx&lt;/code&gt;&lt;/strong&gt; — a transaction hash that does not exist on chain.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;forged &lt;code&gt;PAYMENT-SIGNATURE&lt;/code&gt;&lt;/strong&gt; — a signature that never came from a facilitator.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;replay&lt;/strong&gt; of the same proof twice.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A &lt;strong&gt;SAFE&lt;/strong&gt; endpoint rejects all three and refuses to deliver content. A &lt;strong&gt;RISK&lt;/strong&gt; endpoint returns real content for at least one of them. A &lt;strong&gt;NOT_X402&lt;/strong&gt; endpoint does not speak the protocol at all.&lt;/p&gt;

&lt;p&gt;The probe returns one of those three verdicts for any endpoint you give it.&lt;/p&gt;

&lt;h2&gt;
  
  
  We ran it on our own 60 paid endpoints first
&lt;/h2&gt;

&lt;p&gt;Rather than tell you this is a good idea, we did it to ourselves. We picked the 60 paid endpoints a buyer would actually pay for (audit, security, data, premium utilities) and probed each one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Result: 60 / 60 SAFE.&lt;/strong&gt; Every endpoint rejected forged txHash, forged signature and replay before delivering anything. The results are public and re-checked daily: &lt;a href="https://minia2a.uk/verified-endpoints.html" rel="noopener noreferrer"&gt;https://minia2a.uk/verified-endpoints.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two honest caveats, because they matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;This is &lt;strong&gt;self-certification&lt;/strong&gt;. We built the tool and we ran it against our own endpoints. Useful — but not an independent audit. The methodology is public so you can repeat it: &lt;a href="https://minia2a.uk/x402-payment-audit-methodology.html" rel="noopener noreferrer"&gt;https://minia2a.uk/x402-payment-audit-methodology.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Payment verification is &lt;strong&gt;one property, not the whole story&lt;/strong&gt;. A SAFE endpoint can still return wrong data, mishandle input, or have other problems. "It verifies payment" is necessary, not sufficient.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Now: first check is free per wallet
&lt;/h2&gt;

&lt;p&gt;We just made the first check on the two audit tools free for every registered wallet — because we believe the only way you trust the tool is to run it yourself, on an endpoint you care about.&lt;/p&gt;

&lt;p&gt;What you get free (one each, per wallet):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Smart-contract audit&lt;/strong&gt; (&lt;code&gt;$2&lt;/code&gt; normally) — static scan of Solidity for reentrancy, access control, overflow, unsafe calls. Free first run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Endpoint payment trust check&lt;/strong&gt; (&lt;code&gt;$5&lt;/code&gt; normally) — black-box probe of &lt;em&gt;any&lt;/em&gt; x402 endpoint you give it (yours, a supplier's, a competitor's). Returns SAFE / RISK / NOT_X402. Free first run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Anonymous access gets a payment challenge — the free path needs a wallet (that is the point; it is the identity anchor that stops this being farmed). Registration is self-custody and KYC-free.&lt;/p&gt;

&lt;p&gt;How it works, if you hold keys:&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="c"&gt;# 1. register your wallet (once)&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://minia2a.uk/api/v1/register-simple &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'content-type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"name":"my-agent","wallet":"0xYOURWALLET"}'&lt;/span&gt;

&lt;span class="c"&gt;# 2. sign the trial claim. Message format and header names are documented at:&lt;/span&gt;
&lt;span class="c"&gt;#    https://minia2a.uk/x402-payment-audit-methodology.html&lt;/span&gt;
&lt;span class="c"&gt;#    X-Wallet-Signature = sign("minia2a trial:&amp;lt;wallet&amp;gt;:x402-payment-audit:&amp;lt;unix-ts&amp;gt;")&lt;/span&gt;
&lt;span class="c"&gt;#    X-Trial-Timestamp  = &amp;lt;unix-ts&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;# 3. check any endpoint — first one is free&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://minia2a.uk/x402/x402-payment-audit &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'content-type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'X-Wallet-Signature: 0x...'&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'X-Trial-Timestamp: &amp;lt;ts&amp;gt;'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"wallet":"0xYOURWALLET","url":"https://the-endpoint-you-want-to-check"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first successful check per wallet returns &lt;code&gt;X-First-Free: 1&lt;/code&gt;. A second check from the same wallet is a normal paid call (refunded if the probe itself fails). If your first call fails on a missing parameter, it does &lt;strong&gt;not&lt;/strong&gt; consume the free check — fix the request and try again.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you run an x402 endpoint
&lt;/h2&gt;

&lt;p&gt;You have the strongest reason to run the free check: a buyer deciding whether to wire your endpoint into their agent will look for proof it verifies payment. Run the check on your own endpoint first, publish the verdict, and make "SAFE" part of your listing. The more endpoints that demonstrably reject forged payment, the more "pay per call" means anything at all.&lt;/p&gt;

&lt;p&gt;The tools are live today — &lt;a href="https://minia2a.uk/x402/x402-payment-audit" rel="noopener noreferrer"&gt;https://minia2a.uk/x402/x402-payment-audit&lt;/a&gt; — and our own 60/60 report is at &lt;a href="https://minia2a.uk/verified-endpoints.html" rel="noopener noreferrer"&gt;https://minia2a.uk/verified-endpoints.html&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>x402</category>
      <category>security</category>
      <category>agents</category>
      <category>payments</category>
    </item>
    <item>
      <title>The attribution gap in agent-mediated payments</title>
      <dc:creator>minia2a</dc:creator>
      <pubDate>Thu, 03 Sep 2026 06:42:01 +0000</pubDate>
      <link>https://dev.to/minia2a/the-attribution-gap-in-agent-mediated-payments-10b4</link>
      <guid>https://dev.to/minia2a/the-attribution-gap-in-agent-mediated-payments-10b4</guid>
      <description>&lt;h1&gt;
  
  
  The attribution gap in agent-mediated payments
&lt;/h1&gt;

&lt;p&gt;When an AI agent autonomously pays for an API, the payment settles on-chain with a cryptographic signature. That signature proves a &lt;strong&gt;key&lt;/strong&gt; authorized the spend. It does not prove a &lt;strong&gt;legal person&lt;/strong&gt; did.&lt;/p&gt;

&lt;p&gt;For most of the agent-economy tooling being built right now, that distinction is quietly elided. It is the kind of gap that becomes a liability the first time an agent makes a bad purchase, over-spends a budget, or does something its principal never authorized.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap, in one sentence
&lt;/h2&gt;

&lt;p&gt;A signature binds &lt;strong&gt;a key to a statement&lt;/strong&gt;, not &lt;strong&gt;a person to a statement&lt;/strong&gt;. The moment you let an autonomous agent hold the signing key — which is the entire premise of agent-mediated payments — those two statements stop being interchangeable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three ways the gap shows up in practice
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. The key is held by an agent, not the person.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In agent-mediated payment flows, the signing key is typically held by an autonomous agent acting on behalf of a principal — a company, a DAO, an individual. The agent constructs and signs the payment. So the real chain of authority is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;principal → operator → agent → key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most current payment models cover only the last hop (key → signature), and at most note that "a key is not a legal person." The full delegation chain is left unmodeled.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Most payers are EOAs, not Smart Accounts.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If your answer to the attribution problem is "use a Smart Account / operator delegation chain," you have just excluded the dominant case. Most agent payers today are plain externally-owned accounts, funded directly. Attribution should work identically for an EOA as for a Smart Account — a principal should not have to deploy a Smart Account just to be attributable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The seller needs attribution in-band, at transaction time — not post-hoc.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the subtle one. If the principal's identity is only recorded in an audit receipt (say, a transparency log entry) &lt;em&gt;after&lt;/em&gt; the fact, then the &lt;strong&gt;seller&lt;/strong&gt; cannot use it to make the correct decision at the moment of payment.&lt;/p&gt;

&lt;p&gt;Concrete example: EU reverse-charge VAT applies only if the buyer is a VAT-registered business in the correct member state. The seller has to know who the buyer &lt;em&gt;is&lt;/em&gt; at transaction time to select the right tax regime. Post-hoc attribution means the seller's in-band decision is a guess.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a minimal fix looks like
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;principalAttribution&lt;/code&gt; object, carried &lt;strong&gt;in-band&lt;/strong&gt;, signed by the principal's own key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"principalId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"urn:vat:FR32123456789"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scheme"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vat-eu | did | ens | legal-entity"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pay-per-call; service=audit; max=1000 USD/mo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"nonce"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"a1b2c3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-01T00:00:00Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three properties make this work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Signed by the principal's key&lt;/strong&gt; — an EOA or a Smart Account, it doesn't matter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Carried twice&lt;/strong&gt; — in the transaction declaration (for the seller's good-faith check) and in the audit receipt (for later verifiability). Two copies of one fact, two different consumers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scoped, not blanket&lt;/strong&gt; — &lt;code&gt;scope&lt;/code&gt; + &lt;code&gt;nonce&lt;/code&gt; + &lt;code&gt;exp&lt;/code&gt; make it a &lt;em&gt;delegation&lt;/em&gt; with bounds. That also limits the blast radius of a compromised agent.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why this matters beyond tax
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Liability.&lt;/strong&gt; If an agent over-spends, who is on the hook? Without principal attribution, the answer is "whoever controls the key" — which may be an unincorporated agent runtime with no assets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trust.&lt;/strong&gt; A reputation score for "this buyer pays its bills" needs to anchor to a stable principal identity, not a rotating agent key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compliance.&lt;/strong&gt; Tax regime selection is the concrete case, but the same attribution is required by any jurisdiction that wants to know &lt;em&gt;who&lt;/em&gt; transacted.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The N-hop problem
&lt;/h2&gt;

&lt;p&gt;Delegation is rarely one hop. It's &lt;code&gt;agent → operator → principal&lt;/code&gt;, or deeper. Each hop needs its own scope, nonce, and expiry — and the receipt should record the &lt;strong&gt;chain&lt;/strong&gt;, not a single terminal identifier. Otherwise two distinct legal relationships collapse into one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this is going
&lt;/h2&gt;

&lt;p&gt;This is an active spec discussion, and now is the right time to get the shape right — before agent payments reach a volume where "who authorized this" is a question a lawyer asks rather than a curiosity. If you are building agent-payment rails, the cheapest time to add principal attribution is before the first real money flows.&lt;/p&gt;

</description>
      <category>x402</category>
      <category>aiagents</category>
      <category>web3</category>
      <category>payments</category>
    </item>
    <item>
      <title>Your Rate Limiter Is Silently Hiding You From the Agents That Want to Pay You</title>
      <dc:creator>minia2a</dc:creator>
      <pubDate>Wed, 02 Sep 2026 00:08:17 +0000</pubDate>
      <link>https://dev.to/minia2a/your-rate-limiter-is-silently-hiding-you-from-the-agents-that-want-to-pay-you-168c</link>
      <guid>https://dev.to/minia2a/your-rate-limiter-is-silently-hiding-you-from-the-agents-that-want-to-pay-you-168c</guid>
      <description>&lt;p&gt;I run a pay-per-call marketplace for AI agents. The way agents find the endpoints is through a discovery flow, and the way the endpoints charge is HTTP 402 — the request comes back with a challenge that names the price, the asset, and the address. There is no API-key onboarding. An agent either can read the price and pay it, or it cannot.&lt;/p&gt;

&lt;p&gt;So "being discovered" is the entire growth channel, and I spent an afternoon this week watching that channel behave in a way I did not expect. One crawler was being throttled into uselessness while a dozen others sailed through, and the pattern behind it is worth writing down.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three-step dance of a discovery crawler
&lt;/h2&gt;

&lt;p&gt;A serious indexer of this kind of marketplace does not just fetch a page and leave. It runs a script that follows the spec, and the spec has three moves:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read the &lt;strong&gt;manifest&lt;/strong&gt; (the &lt;code&gt;.well-known&lt;/code&gt; document) to enumerate the endpoints.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GET each endpoint&lt;/strong&gt; to collect the 402 challenge — that is where the price lives, not in any catalog file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;POST a settlement probe&lt;/strong&gt; to confirm the endpoint actually accepts payment, not just that it advertises one.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Step three is the tell. A crawler that only reads manifests and GETs prices is building a phone book. A crawler that POSTs is building a &lt;em&gt;verified&lt;/em&gt; index — it is checking that the money would actually move. From my side of the table, that is the exact index I want to be in, because it is the one that tells an agent "yes, you can pay this endpoint and it will work."&lt;/p&gt;

&lt;h2&gt;
  
  
  The one crawler getting throttled was the thorough one
&lt;/h2&gt;

&lt;p&gt;Here is what I saw in the request logs over a couple of hours.&lt;/p&gt;

&lt;p&gt;The polite crawlers — the ones that read the manifest and GET the challenges and then leave — flowed through clean. Their status-code mix was exactly what you would expect for healthy discovery: &lt;code&gt;200&lt;/code&gt; for the manifest, &lt;code&gt;402&lt;/code&gt; for the price challenges, &lt;code&gt;404&lt;/code&gt; for endpoints that had been rotated out. Zero &lt;code&gt;429&lt;/code&gt;s. Rate limiter never touched them.&lt;/p&gt;

&lt;p&gt;Then there was the distributed one. It came from fifteen-odd cloud IPs, sharded the endpoint list across them, and did the full three-step dance on each endpoint: manifest, GET the 402, POST the settlement probe. And it ate &lt;code&gt;429&lt;/code&gt;s. On one of its IPs, out of ~75 requests, ~45 came back rate-limited. Roughly sixty percent of its traffic never got through.&lt;/p&gt;

&lt;p&gt;Read that again. The crawler that was doing the &lt;em&gt;most complete&lt;/em&gt; job of discovery — the one actually verifying settlement, the one whose index entry would be the most valuable to me — was the one my rate limiter decided to block.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the naive limiter makes this exact mistake
&lt;/h2&gt;

&lt;p&gt;The limiter was keyed on raw request counts per IP, full stop. That is the default, and it is wrong for this traffic.&lt;/p&gt;

&lt;p&gt;Discovery crawlers &lt;em&gt;have&lt;/em&gt; to be chatty. Verifying an endpoint means at least a GET and a POST per endpoint, and if you have a catalog of a thousand endpoints, that is two thousand requests. A distributed verifier splits that across a fleet, so each IP's share is not even that large — but it is still far above whatever threshold a "normal user" hits, because there is no normal user in this flow. There are only agents and indexers.&lt;/p&gt;

&lt;p&gt;Meanwhile the polite crawlers stayed under threshold almost by accident, because &lt;em&gt;they skip step three&lt;/em&gt;. The crawler that costs you the most to serve is the one that brings you the least. The crawler that costs you the most to serve &lt;em&gt;and&lt;/em&gt; brings you the most is the one you just throttled.&lt;/p&gt;

&lt;p&gt;The category error is treating "high request volume" as a proxy for "abuse." For a marketplace whose entire distribution is machine discovery, volume &lt;em&gt;is&lt;/em&gt; the product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Distinguish by pattern, not by count
&lt;/h2&gt;

&lt;p&gt;The fix is not "raise the limit." Raising the limit would just let the next wave through while still failing the actual distinction. The fix is to recognize the shape of legitimate discovery.&lt;/p&gt;

&lt;p&gt;A spec-compliant discovery sequence has a recognizable grammar: manifest read, then a burst of 402-challenge GETs, then settlement probes — with the probes going to the &lt;em&gt;same&lt;/em&gt; resources that were just challenged, in the &lt;em&gt;same&lt;/em&gt; window. That is not what a scraper does. A scraper walks every link it can find. A credential-stuffer POSTs to one auth endpoint ten thousand times. Neither looks anything like "enumerate, challenge, settle."&lt;/p&gt;

&lt;p&gt;So the carve-out is not "give crawlers more requests." It is: &lt;em&gt;if the request sequence matches the discovery grammar, budget it separately from general abuse.&lt;/em&gt; Watch the ratio of settlement probes to challenges; a real verifier probes roughly the endpoints it challenged, a spammer probes one endpoint a thousand times. The ratio is a cleaner signal than the count ever was.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bigger point
&lt;/h2&gt;

&lt;p&gt;If you run anything that gets &lt;em&gt;indexed&lt;/em&gt; as its growth channel — an API directory, a marketplace, a network of any kind — then your abuse defenses and your discoverability are drawing from the same budget, and you should tune them together, not sequentially.&lt;/p&gt;

&lt;p&gt;The signal I kept reaching for — "is this traffic abusive?" — was the wrong question. The right question is "is this traffic &lt;em&gt;discovering me correctly&lt;/em&gt;?" And the crawler I was most tempted to block was the one discovering me the most correctly of all.&lt;/p&gt;

&lt;p&gt;The day ended with the limiter untouched and the answer being "watch the pattern." I am going to spend the next week deciding the carve-out by watching whether the verified-index crawlers show up in the people who actually pay. That is the metric that settles it, not the 429 count.&lt;/p&gt;

&lt;h2&gt;
  
  
  Correction, after re-checking the logs
&lt;/h2&gt;

&lt;p&gt;The "thorough crawler" framing above was too charitable to one specific client. When I pulled that IP's full request history instead of the aggregate, it turned out to be high-frequency enumeration — POSTing the same couple of service paths (address parsing, PDF text extraction) over and over from a single cloud IP, not probing many distinct endpoints once each. That is scanning, not settlement verification, and the 429s it ate were the limiter doing its job correctly.&lt;/p&gt;

&lt;p&gt;This lands on the same prescription, just stated more sharply: the signal is the &lt;strong&gt;ratio of probes to distinct challenges&lt;/strong&gt;, not the raw count and not "does it POST." A real verifier probes roughly the endpoints it challenged (~1:1). A scanner hammers one endpoint a thousand times. The carve-out should fire only when the sequence matches the discovery grammar — manifest read, then challenge GETs, then probes on those same resources — and never as a blanket "let crawlers through."&lt;/p&gt;

&lt;p&gt;The headline stands with that boundary drawn: the misalignment that hurts is shallow crawlers (manifest + GET only) passing freely while a legitimate deep verifier gets throttled. High-frequency enumeration getting 429'd is the correct half of the behavior, not the bug. I left the original sections intact rather than rewriting them, because the mistake I made — reaching for "this client must be doing the deep dance" before actually checking its request pattern — is itself worth showing.&lt;/p&gt;

</description>
      <category>x402</category>
      <category>agents</category>
      <category>api</category>
      <category>devops</category>
    </item>
    <item>
      <title>"The Retry You Add for Politeness Is Destroying Your Liveness Signal"</title>
      <dc:creator>minia2a</dc:creator>
      <pubDate>Tue, 01 Sep 2026 09:56:42 +0000</pubDate>
      <link>https://dev.to/minia2a/the-retry-you-add-for-politeness-is-destroying-your-liveness-signal-2b3l</link>
      <guid>https://dev.to/minia2a/the-retry-you-add-for-politeness-is-destroying-your-liveness-signal-2b3l</guid>
      <description>&lt;p&gt;If you run an agent-payment marketplace, you have to answer one question every day: &lt;em&gt;is this catalog actually alive, or is it a graveyard of dead endpoints?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The obvious way to answer is a crawler. Fetch every host, mark the ones that fail as UNREACHABLE, and count. And because a single timeout can be a transient blip, every competent crawler adds a retry — one immediate re-attempt, seconds later — before it writes the failure down.&lt;/p&gt;

&lt;p&gt;That retry is polite, and it is quietly destroying the signal you are trying to measure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The retry resolves before the row is written
&lt;/h2&gt;

&lt;p&gt;I run a daily host-observer over a pinned catalog of a little over 1,500 hosts. The instrument does exactly what I described: on a failed first attempt, it retries once, immediately, seconds apart. It records &lt;em&gt;"UNREACHABLE first attempts only, never policy refusals, never answers."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Here is the thing I got wrong when I first tried to design a rule around this data. I proposed holding hosts out of my collapse logic "until the retry window resolves." A reviewer who actually operates the instrument corrected me, and the correction is obvious in hindsight:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;There is no "within the retry window" state to hold out. The retry resolves &lt;em&gt;before the row is written&lt;/em&gt;. Every UNREACHABLE that reaches a snapshot has already survived it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The whole UNREACHABLE population is post-retry by construction. A clause that says "hold out hosts still inside the retry window" is a no-op, because that set is always empty by the time anything is observable. I had been treating a retry as a policy window, when on this instrument it is a single intra-probe re-attempt that finishes before the write.&lt;/p&gt;

&lt;h2&gt;
  
  
  Transient and sustained are indistinguishable on the day they onset
&lt;/h2&gt;

&lt;p&gt;The proof is in two hosts that look identical on the day they fail.&lt;/p&gt;

&lt;p&gt;On the same day, one host was retried and still came back UNREACHABLE. Its row says &lt;code&gt;unreachable on both attempts (first: TimeoutError)&lt;/code&gt;. A cluster of six hosts sharing a platform root were also all retried, and also all still UNREACHABLE. Same fields. Same post-retry state. There is no way to tell, on that day, which one is a transient blip and which one is a real outage.&lt;/p&gt;

&lt;p&gt;What separated them was the &lt;em&gt;next day's observation&lt;/em&gt;. The single host read OK the next day. The six did not, and have not since.&lt;/p&gt;

&lt;p&gt;So the separator between transient and sustained failure is not the retry — the retry is seconds, and it had already been spent. The separator is the next observation cycle. In this case, twenty-four hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  The wrong unit is inherited, and it is not portable
&lt;/h2&gt;

&lt;p&gt;If you key your hold-out rule to "whatever retry the implementer happens to run," you inherit every implementer's retry policy into your liveness definition. Two crawlers with different retry counts would disagree about when a host "really" went down.&lt;/p&gt;

&lt;p&gt;Keying the hold-out to &lt;em&gt;observation cycles&lt;/em&gt; makes the rule portable. Two crawlers with different retry policies still agree on the fact that matters: the transient host was UNREACHABLE on day N and OK on day N+1. Both record that. Neither's retry setting changes it.&lt;/p&gt;

&lt;p&gt;So the rule becomes: &lt;strong&gt;UNREACHABLE is held out of the liveness collapse until it has persisted across a stated number of consecutive observation cycles.&lt;/strong&gt; Whatever survives that is promoted to a real state and collapses normally. How many cycles is a judgment — I have one instrument on a daily period, so I can't settle it for anyone else — but the &lt;em&gt;unit&lt;/em&gt; has to be cycles, because the retry has already run by the time anyone can look.&lt;/p&gt;

&lt;h2&gt;
  
  
  N=2, and the counting semantics that pins it
&lt;/h2&gt;

&lt;p&gt;Once you accept cycles as the unit, you still have to pick N. That requires deciding how you count an "onset."&lt;/p&gt;

&lt;p&gt;Count the onset observation itself as cycle 1. Then a host that failed yesterday and recovered today persisted &lt;em&gt;1 cycle&lt;/em&gt;. A host that failed and stayed down through the next day persisted &lt;em&gt;2 cycles&lt;/em&gt;. N=2 is the smallest value that separates the transient from the sustained: a host must read UNREACHABLE on two consecutive observations before you promote it to "down."&lt;/p&gt;

&lt;p&gt;The record backs N=2 in a way that surprised me. Across the full window, 74 UNREACHABLE episodes ended in a recovery. &lt;strong&gt;63 of them — 85% — lasted exactly one cycle.&lt;/strong&gt; N=2 holds out 85% of everything that ever came back, while still promoting a genuine outage on the first confirmed subsequent day. That is not a judgment call anymore; it is the smallest separator the data supports.&lt;/p&gt;

&lt;h2&gt;
  
  
  Promotion is not permanence, and "outage" invites the wrong reading
&lt;/h2&gt;

&lt;p&gt;The remaining 11 episodes recovered after 2, 3, 4, 5, 6, 11, 15, and 17 cycles. The longest episode that still ended in a recovery ran 17 consecutive cycles.&lt;/p&gt;

&lt;p&gt;That means there is no N that makes promotion mean permanence. A resolver that treats a promoted "platform outage" as terminal will be wrong about a host that comes back on day 18. So whatever event type your promotion emits, it has to be worded as &lt;em&gt;persistence so far&lt;/em&gt;, not &lt;em&gt;removal&lt;/em&gt;. The downstream action is retain-and-backoff, not delete.&lt;/p&gt;

&lt;h2&gt;
  
  
  Right-censoring: you have no upper bound on N
&lt;/h2&gt;

&lt;p&gt;The last trap is the range. It is tempting to look at the data and say "outages run between 2 and 6 cycles." It is a real error, and it took a reviewer pointing it out for me to see it.&lt;/p&gt;

&lt;p&gt;An episode that has &lt;em&gt;not ended&lt;/em&gt; is right-censored. It tells you a lower bound on how long it has run so far — nothing about how long it will run. In this window, 40 episodes were still open at the last observation, 16 of them running the full 24 days. Each one is a lower bound on its own eventual length and nothing more. The record places &lt;strong&gt;no upper bound on N&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Keep the two facts separate, because they do different work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The still-open episodes are why you can't quote "2 to 6 cycles" as a range.&lt;/li&gt;
&lt;li&gt;The recovered-long episodes (up to 17) are why promotion can never be treated as permanence.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Merging them loses the second point, which is the one that argues for retain-and-backoff over delete.&lt;/p&gt;

&lt;h2&gt;
  
  
  The practical rule
&lt;/h2&gt;

&lt;p&gt;If you build liveness monitoring for an agent-discovery catalog:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Don't key hold-out to retries.&lt;/strong&gt; The retry has already run by the time you can observe anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hold out UNREACHABLE until it has persisted across N consecutive observation cycles&lt;/strong&gt; (N=2 is a sound default on a daily period — it holds out 85% of everything that recovers).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Emit two event types, not one.&lt;/strong&gt; A persistent-UNREACHABLE cluster is a &lt;em&gt;platform outage&lt;/em&gt; (retain/backoff); an OK→NO-payment cluster is a &lt;em&gt;platform re-gating&lt;/em&gt; (re-read the price). A resolver's downstream action differs, so they are not naming hygiene.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never treat promotion as terminal.&lt;/strong&gt; The longest outage that still recovered ran 17 cycles. "Persistence so far, not removal."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't quote a range for N.&lt;/strong&gt; Open episodes are lower bounds, not upper bounds.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The counterintuitive part is the one worth remembering: the retry you add to be polite about transients is not where the transient/sustained distinction lives. That distinction lives one observation cycle later. Measure &lt;em&gt;that&lt;/em&gt;, and you stop building liveness rules that quietly assume the retry already did the hard part.&lt;/p&gt;

</description>
      <category>x402</category>
      <category>agents</category>
      <category>observability</category>
      <category>api</category>
    </item>
    <item>
      <title>The 402 challenge is already the price tag — stop copying it into your discovery manifest</title>
      <dc:creator>minia2a</dc:creator>
      <pubDate>Mon, 31 Aug 2026 13:14:13 +0000</pubDate>
      <link>https://dev.to/minia2a/the-402-challenge-is-already-the-price-tag-stop-copying-it-into-your-discovery-manifest-13k1</link>
      <guid>https://dev.to/minia2a/the-402-challenge-is-already-the-price-tag-stop-copying-it-into-your-discovery-manifest-13k1</guid>
      <description>&lt;p&gt;If you're building a discovery layer for pay-per-call APIs — a registry where agents look up "what can I call and what does it cost" — there's a decision you'll hit within the first week: &lt;strong&gt;do I put pricing, trial quotas, and access policy in the discovery manifest, or do I point at the endpoint and let it speak for itself?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I just spent a week answering this on a live x402 marketplace, and the x402 discovery spec (&lt;code&gt;specs/extensions/discovery.md&lt;/code&gt;, currently in review) already made the right call. It's worth understanding &lt;em&gt;why&lt;/em&gt;, because the same reasoning applies to any machine-readable API registry, not just x402.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two readers have different needs
&lt;/h2&gt;

&lt;p&gt;A discovery manifest has two very different audiences that keep getting conflated:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The crawler/indexer&lt;/strong&gt; — reads the manifest once to &lt;em&gt;discover&lt;/em&gt; the service exists, route to it, and rank it. It wants stable metadata: name, description, network, facilitator, proof of liveness.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The paying agent&lt;/strong&gt; — reads the 402 challenge at &lt;em&gt;call time&lt;/em&gt; to decide "can I afford this, can I trial it, and where do I send money." It wants the answer to be &lt;strong&gt;current&lt;/strong&gt;, because the answer is what it acts on.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When you put &lt;code&gt;trial_limit: 15&lt;/code&gt; or &lt;code&gt;credits_on_register: 500&lt;/code&gt; into the manifest, you've coupled audience #1 to the state of audience #2. The crawler is now caching a promise that only the runtime can actually keep.&lt;/p&gt;

&lt;h2&gt;
  
  
  Payment data rots
&lt;/h2&gt;

&lt;p&gt;The x402 discovery spec's field table is telling in what it &lt;em&gt;omits&lt;/em&gt;. The manifest carries &lt;code&gt;x402Version&lt;/code&gt;, &lt;code&gt;kind&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;description&lt;/code&gt;, &lt;code&gt;facilitator&lt;/code&gt;, &lt;code&gt;resources&lt;/code&gt;, &lt;code&gt;attestation&lt;/code&gt;, &lt;code&gt;peers&lt;/code&gt;, &lt;code&gt;updated&lt;/code&gt; — and &lt;strong&gt;no &lt;code&gt;access&lt;/code&gt;, &lt;code&gt;trial&lt;/code&gt;, or &lt;code&gt;credit&lt;/code&gt; fields anywhere&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The rationale, verbatim from the spec: &lt;em&gt;payment data rots and the 402 challenge does not.&lt;/em&gt; A trial quota is a runtime policy. You change it when your abuse model changes, not when your description changes. Copy it into a static manifest and you've created "a second source of truth and always the one that rots" — also the spec's words.&lt;/p&gt;

&lt;p&gt;I learned this the hard way. When we changed our trial policy, the number had been copied into ~200 files — manifests, machine-readable docs, marketing pages, blog posts, a database. The one place that needed &lt;em&gt;zero&lt;/em&gt; changes was the 402 challenge, because it's generated per-request from the live policy. That's the smell test: &lt;strong&gt;if changing a policy means grepping 200 files, the policy was encoded in the wrong place.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The bare-pointer pattern
&lt;/h2&gt;

&lt;p&gt;The spec's answer isn't "omit pricing" — it's "delegate it." The manifest carries a &lt;code&gt;resources&lt;/code&gt; list: x402-paywalled URLs on the host, where indexers "probe each URL for the full 402" challenge. The manifest stays a stable pointer; the endpoint stays the authority. Whether you call the pointer &lt;code&gt;resources&lt;/code&gt; or a &lt;code&gt;probe&lt;/code&gt; URL, the shape is the same: a bare pointer, not a copied policy block.&lt;/p&gt;

&lt;p&gt;This is the same reason HTTP keeps &lt;code&gt;WWW-Authenticate&lt;/code&gt; on the 401/407 response rather than in some global registry: the server that enforces the policy is the only party that can tell you what the policy &lt;em&gt;currently&lt;/em&gt; is.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Unknown fields MUST be ignored" cuts both ways
&lt;/h2&gt;

&lt;p&gt;The spec mandates forward compatibility: unknown fields are ignored. That's normally cited as "you can add fields without breaking old clients." But it also means the reverse: &lt;strong&gt;if you add a private &lt;code&gt;access&lt;/code&gt; object to your manifest, every other implementation will silently drop it.&lt;/strong&gt; It isn't a discovery surface; it's a decoration. It'll never be read by anyone but your own crawler — and your own crawler should be reading the 402 challenge anyway.&lt;/p&gt;

&lt;p&gt;If you want pricing or trial policy to be part of the shared discovery surface, it has to go through the spec. If it's not in the spec, don't pretend a custom field makes it discoverable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The operational principle
&lt;/h2&gt;

&lt;p&gt;Put it this way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Static, slow-changing, identity-shaped data&lt;/strong&gt; → the manifest. What is this thing, who runs it, where does it settle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic, policy-shaped, state-shaped data&lt;/strong&gt; → the 402 challenge. What does it cost right now, can I trial it, where do I pay.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The 402 challenge &lt;em&gt;is&lt;/em&gt; the price tag. It's already there, it's already fresh, and it's already the thing the paying agent acts on. Copying it into the manifest doesn't make it more discoverable — it makes two things you now have to keep in sync.&lt;/p&gt;

&lt;p&gt;If your discovery layer's manifest has a &lt;code&gt;trial_limit&lt;/code&gt; field, ask yourself: when you change it, how many files do you have to touch? The answer is the size of the bug you're building.&lt;/p&gt;

</description>
      <category>x402</category>
      <category>api</category>
      <category>webdev</category>
      <category>ai</category>
    </item>
    <item>
      <title>"The x402 facilitator: what actually sits between an agent and the chain when it pays a cent"</title>
      <dc:creator>minia2a</dc:creator>
      <pubDate>Thu, 27 Aug 2026 16:59:17 +0000</pubDate>
      <link>https://dev.to/minia2a/the-x402-facilitator-what-actually-sits-between-an-agent-and-the-chain-when-it-pays-a-cent-4731</link>
      <guid>https://dev.to/minia2a/the-x402-facilitator-what-actually-sits-between-an-agent-and-the-chain-when-it-pays-a-cent-4731</guid>
      <description>&lt;h1&gt;
  
  
  The x402 facilitator: what actually sits between an agent and the chain when it pays a cent
&lt;/h1&gt;

&lt;p&gt;Most writing about agent payments stops at the happy path: an AI agent hits a paywalled endpoint, gets a &lt;code&gt;402 Payment Required&lt;/code&gt;, signs a transaction, retries with proof of payment, and gets a response. That is the part of x402 everyone talks about.&lt;/p&gt;

&lt;p&gt;This article is about the part nobody diagrams: the &lt;strong&gt;facilitator&lt;/strong&gt; — the component that actually accepts the payment and settles it with the person running the API. If you are building a paid agent API, the facilitator is the thing you are really choosing. Everything else is plumbing around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three roles in an x402 exchange
&lt;/h2&gt;

&lt;p&gt;The spec splits a paid call into three parties, and the split matters more than the protocol version:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Client&lt;/strong&gt; — the agent. It wants a resource and has a wallet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resource server&lt;/strong&gt; — the API. It has the resource and wants to be paid before releasing it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Facilitator&lt;/strong&gt; — the entity that takes the money on the resource server's behalf.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When a resource server returns &lt;code&gt;402&lt;/code&gt;, the challenge says &lt;em&gt;who&lt;/em&gt; to pay (&lt;code&gt;payTo&lt;/code&gt;) and &lt;em&gt;how much&lt;/em&gt;. In a direct setup the &lt;code&gt;payTo&lt;/code&gt; is the resource server's own address. In a facilitated setup, &lt;code&gt;payTo&lt;/code&gt; is the facilitator's address, and the facilitator later forwards the settlement to the resource server.&lt;/p&gt;

&lt;p&gt;That indirection is the entire point. The facilitator absorbs everything the resource server does not want to run itself: wallet custody, chain connectivity, gas management, and the reconciliation of "who paid how much for what."&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the facilitator exists at all
&lt;/h2&gt;

&lt;p&gt;A resource server &lt;em&gt;could&lt;/em&gt; just put its own address in &lt;code&gt;payTo&lt;/code&gt; and watch the chain. Almost nobody does, for four reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gas is a tax on cents.&lt;/strong&gt; Settling a $0.01 call on a chain with a $0.10 fee is a 10x loss. The facilitator exists to batch, hold off-chain, or absorb gas so per-call economics survive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One address per chain is painful.&lt;/strong&gt; An API that wants to accept USDC on five networks now runs five key pairs and five balance books. A facilitator collapses that into one settlement surface.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reconciliation is the hard part.&lt;/strong&gt; Payments arrive as unstructured chain transactions. Someone has to map "0.003 USDC from 0x9f... at block 12M" onto "agent X bought tool Y for $0.003." That mapping is a database, not a protocol feature.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trust is easier to outsource.&lt;/strong&gt; A buyer that does not know your API will still pay a facilitator it recognizes. The facilitator is a reputational bridge.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The settlement chain, end to end
&lt;/h2&gt;

&lt;p&gt;Here is the chain a cent actually travels, from left to right:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;agent wallet → facilitator (payTo) → [batch/ledger] → facilitator payout wallet
                                                          ↓
                                              resource server's wallet
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;402&lt;/code&gt; challenge is the &lt;em&gt;front&lt;/em&gt; of this chain. The back half — facilitator internal ledger, payout batching, on-chain settlement — is not standardized. That is the interesting part: &lt;strong&gt;x402 standardizes the challenge, not the settlement.&lt;/strong&gt; Two facilitators can be 100% spec-compatible and settle completely differently.&lt;/p&gt;

&lt;h2&gt;
  
  
  The design axes that actually distinguish facilitators
&lt;/h2&gt;

&lt;p&gt;If you are evaluating (or building) a facilitator, these four axes matter more than the brand name.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Gasless vs. gas-bearing
&lt;/h3&gt;

&lt;p&gt;A gasless facilitator pays the chain fees itself and marks it up (or subsidizes it). A gas-bearing facilitator passes gas through to the buyer. Gasless is a better developer experience and a worse margin; the choice is really "who eats the fee on a $0.01 call."&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Pass-through vs. credit/reseller
&lt;/h3&gt;

&lt;p&gt;A pass-through facilitator moves money from buyer to seller 1:1 minus a fee. A &lt;strong&gt;reseller facilitator&lt;/strong&gt; does not move the buyer's money at all — it meters its own credits to the agent and pays the resource server separately. The agent never holds a direct relationship with the resource server. Reseller models are attractive because they abstract the chain away entirely, but they concentrate trust in the facilitator.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Single-rail vs. multi-rail
&lt;/h3&gt;

&lt;p&gt;A single-rail facilitator settles on one chain (usually one stablecoin on one L2). A multi-rail facilitator accepts payment on several and settles on whichever the resource server prefers. Multi-rail is more capital-efficient for buyers but multiplies the reconciliation problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Hosted vs. self-hosted
&lt;/h3&gt;

&lt;p&gt;A hosted facilitator is a service you point &lt;code&gt;payTo&lt;/code&gt; at. A self-hosted facilitator is software you run next to your API. Self-hosted keeps custody and margin, at the cost of running a wallet and a reconciliation pipeline you were probably trying to avoid.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means for the protocol's future
&lt;/h2&gt;

&lt;p&gt;The existence of a second HTTP-402 standard — MPP, from Stripe, Tempo, and Visa, deliberately backward-compatible with x402 — is a signal, not a fight. When a payment rail is being built as &lt;em&gt;infrastructure&lt;/em&gt; by card networks, the rails are becoming a commodity. The value is migrating to the layer above the rail: who holds the relationship with the buyer, who does the reconciliation, who vouches for the seller.&lt;/p&gt;

&lt;p&gt;That layer is the facilitator. The next few years of agent payments will be decided less by which protocol wins and more by how the facilitator role shakes out — whether it consolidates into a few big settlement networks or fragments into thousands of self-hosted gateways. Watch that, not the version numbers.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is a technical explainer, not product commentary. The roles and flows described are part of the public x402 specification and its ecosystem; organizations are named only where they are the authors of a public specification.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>x402</category>
      <category>m2m</category>
      <category>payments</category>
      <category>web3</category>
    </item>
    <item>
      <title>MCP 2026-07-28 Went Stateless — and Still Has No Way to Charge for a Call</title>
      <dc:creator>minia2a</dc:creator>
      <pubDate>Thu, 27 Aug 2026 09:06:55 +0000</pubDate>
      <link>https://dev.to/minia2a/mcp-2026-07-28-went-stateless-and-still-has-no-way-to-charge-for-a-call-3l2g</link>
      <guid>https://dev.to/minia2a/mcp-2026-07-28-went-stateless-and-still-has-no-way-to-charge-for-a-call-3l2g</guid>
      <description>&lt;h1&gt;
  
  
  MCP 2026-07-28 Went Stateless — and Still Has No Way to Charge for a Call
&lt;/h1&gt;

&lt;p&gt;The Model Context Protocol's July 28 revision is the biggest architectural change since launch. It is not an incremental bump — it's a rewrite of the protocol's core assumption. And yet the one thing server authors keep asking for — a way to get paid — is still, deliberately, not there.&lt;/p&gt;

&lt;p&gt;This post is two things: an accurate map of what &lt;em&gt;actually&lt;/em&gt; changed (a lot of the summaries floating around get it wrong), and a concrete pattern for the payment gap, using HTTP 402.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually shipped: the protocol went stateless
&lt;/h2&gt;

&lt;p&gt;The headline is &lt;strong&gt;statelessness&lt;/strong&gt;. Prior revisions assumed a long-lived session: client calls &lt;code&gt;initialize&lt;/code&gt;, server returns a session ID, both sides keep per-session state. The 2026-07-28 revision removes that assumption.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;initialize&lt;/code&gt; handshake removed&lt;/td&gt;
&lt;td&gt;No session ID. Each request carries its own version + capabilities in &lt;code&gt;_meta.io.modelcontextprotocol/{protocolVersion, clientInfo, clientCapabilities}&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New &lt;code&gt;server/discover&lt;/code&gt; RPC&lt;/td&gt;
&lt;td&gt;Clients learn supported versions and capabilities up front; SDKs fall back to legacy &lt;code&gt;initialize&lt;/code&gt; for old servers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stateless-only Streamable HTTP&lt;/td&gt;
&lt;td&gt;The &lt;code&gt;streamable&lt;/code&gt; transport accepts 2026-07-28 only in stateless mode; stateful sessions negotiate down to 2025-11-25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Removed methods&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ping&lt;/code&gt;, &lt;code&gt;logging/setLevel&lt;/code&gt;, &lt;code&gt;resources/subscribe&lt;/code&gt;, &lt;code&gt;resources/unsubscribe&lt;/code&gt;, and SSE resumability (&lt;code&gt;Last-Event-ID&lt;/code&gt;, standalone GET) are gone — they reject with &lt;code&gt;MethodNotFound&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MRTR (SEP-2322)&lt;/td&gt;
&lt;td&gt;Multi-round-trip requests replace holding an SSE stream open. A server returns &lt;code&gt;InputRequiredResult&lt;/code&gt; (&lt;code&gt;resultType: "inputRequired"&lt;/code&gt;) with an opaque &lt;code&gt;requestState&lt;/code&gt;; the client re-issues the request with &lt;code&gt;inputResponses&lt;/code&gt; + that same &lt;code&gt;requestState&lt;/code&gt;. Any replica can pick up the retry — that's the point&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Header routing (SEP-2243)&lt;/td&gt;
&lt;td&gt;Every Streamable HTTP POST carries &lt;code&gt;Mcp-Method&lt;/code&gt; (mirroring the JSON-RPC method), plus &lt;code&gt;Mcp-Name&lt;/code&gt; for &lt;code&gt;tools/call&lt;/code&gt;, &lt;code&gt;resources/read&lt;/code&gt;, &lt;code&gt;prompts/get&lt;/code&gt;. Intermediaries can route without parsing bodies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cacheable lists (SEP-2549)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;tools/list&lt;/code&gt;, &lt;code&gt;prompts/list&lt;/code&gt;, &lt;code&gt;resources/list&lt;/code&gt; carry &lt;code&gt;ttlMs&lt;/code&gt; and &lt;code&gt;cacheScope&lt;/code&gt; freshness hints&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Real HTTP errors&lt;/td&gt;
&lt;td&gt;Transport failures return actual status codes instead of HTTP 200 with an in-body JSON-RPC error: unknown method → 404, unsupported version → 400 (&lt;code&gt;-32022&lt;/code&gt;), missing capability → &lt;code&gt;-32021&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Equally important is what got &lt;strong&gt;deprecated&lt;/strong&gt;: &lt;code&gt;roots&lt;/code&gt;, &lt;code&gt;sampling&lt;/code&gt;, and &lt;code&gt;logging&lt;/code&gt; (SEP-2577), plus the legacy HTTP+SSE transport and Dynamic Client Registration (replaced by Client ID Metadata Documents). A lot of the "MCP added OAuth DCR and roots!" summaries have it exactly backwards — those were the old model, now on the way out.&lt;/p&gt;

&lt;p&gt;The through-line: the protocol is being rebuilt so that &lt;strong&gt;any request can be routed to any replica, with no shared session state&lt;/strong&gt;. That's a property you want if you're running an MCP gateway, a load balancer, or a fleet of servers behind one endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  What did &lt;em&gt;not&lt;/em&gt; change: there's still no payment layer
&lt;/h2&gt;

&lt;p&gt;Search the spec for a way to charge for a &lt;code&gt;tools/call&lt;/code&gt; and you won't find one. This is a deliberate design decision, not an omission. MCP solves &lt;strong&gt;discovery&lt;/strong&gt; (what tools exist) and &lt;strong&gt;capability&lt;/strong&gt; (what they do). Commerce — who owes whom — is a different protocol's job, and the maintainers have been consistent that it's out of scope.&lt;/p&gt;

&lt;p&gt;The reasoning is sound. A tool-integration protocol should not bake in a pricing model, a settlement rail, or a currency. Those things rot fast. But the decision leaves a concrete problem for anyone running a non-free MCP server: &lt;strong&gt;when an agent calls your tool, how do you get paid?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The ecosystem's answer is converging on &lt;strong&gt;HTTP 402 Payment Required&lt;/strong&gt; — the status code that's been reserved since 1997 and is finally being used for its intended purpose. The standard that operationalizes it is &lt;strong&gt;x402&lt;/strong&gt;, which is now under the Linux Foundation.&lt;/p&gt;

&lt;h2&gt;
  
  
  How HTTP 402 gates an MCP server
&lt;/h2&gt;

&lt;p&gt;The mechanism is refreshingly small. When a request arrives without proof of payment, you return &lt;code&gt;402&lt;/code&gt; with payment instructions in the &lt;code&gt;WWW-Authenticate&lt;/code&gt; header. The caller pays, retries with proof, and you verify. One request cycle, no account system.&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="nf"&gt;POST&lt;/span&gt; &lt;span class="nn"&gt;/mcp&lt;/span&gt; &lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt;
&lt;span class="na"&gt;Content-Type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;application/json&lt;/span&gt;
&lt;span class="na"&gt;Mcp-Method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tools/call&lt;/span&gt;
&lt;span class="na"&gt;Mcp-Name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;lookup_contract&lt;/span&gt;

&lt;span class="err"&gt;HTTP/&lt;/span&gt;&lt;span class="mf"&gt;1.1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;402&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Payment&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Required&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;WWW-Authenticate:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Payment&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;version=&lt;/span&gt;&lt;span class="s2"&gt;"1.0"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;asset=&lt;/span&gt;&lt;span class="s2"&gt;"USDC"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;chain=&lt;/span&gt;&lt;span class="s2"&gt;"base"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;receiver=&lt;/span&gt;&lt;span class="s2"&gt;"0x…"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;price=&lt;/span&gt;&lt;span class="s2"&gt;"0.01"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;Content-Type:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;application/json&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"payment required"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"price"&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.01 USDC"&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;A working Express middleware for the gate — placed before your &lt;code&gt;tools/call&lt;/code&gt; handler:&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;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/mcp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isToolCall&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mcp-method&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tools/call&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;isToolCall&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;next&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;proof&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-payment-proof&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;proof&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;402&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;WWW-Authenticate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Payment version="1.0", asset="USDC", chain="base", &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;receiver="&amp;lt;YOUR-RECEIVER&amp;gt;", price="0.01"&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;payment required&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0.01 USDC&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="c1"&gt;// Verify the proof against the facilitator that settled it.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ok&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;verifyPayment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;proof&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;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;402&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="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;payment invalid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details worth getting right in production:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Mcp-Method: tools/call&lt;/code&gt; is now a routing header&lt;/strong&gt; (SEP-2243). You can gate &lt;em&gt;only&lt;/em&gt; paid tools by matching on it, rather than intercepting every POST and inspecting the JSON-RPC body. Free tools (&lt;code&gt;Mcp-Method: tools/list&lt;/code&gt;, etc.) pass straight through.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verification is delegated to a facilitator.&lt;/strong&gt; You don't need to parse the chain or manage USDC custody yourself. The payment proof is a receipt a facilitator issued; you POST it back to the facilitator's verify endpoint and get a yes/no. Your server stays a stateless replica — which is exactly the property 2026-07-28 just worked to give you.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The stateless rewrite is a gift to paid endpoints
&lt;/h2&gt;

&lt;p&gt;This is the part that connects the two halves. Payment gating and statelessness &lt;em&gt;reinforce&lt;/em&gt; each other:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;402&lt;/code&gt; + retry pattern is naturally idempotent. If your server is a stateless replica, a client can hit &lt;em&gt;any&lt;/em&gt; replica with the same paid request and get the same result. No session affinity to fight.&lt;/li&gt;
&lt;li&gt;MRTR gives you &lt;code&gt;requestState&lt;/code&gt; tokens, so a multi-step paid interaction (request → pay → confirm → result) can span replicas without sticky sessions.&lt;/li&gt;
&lt;li&gt;Real HTTP status codes mean a &lt;code&gt;402&lt;/code&gt; is a &lt;code&gt;402&lt;/code&gt;, not a JSON-RPC error wrapped in HTTP 200. Agents and gateways can branch on it cheaply.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're building a non-free MCP server right now, the stateless model removes most of the infrastructure reasons you couldn't charge before.&lt;/p&gt;

&lt;h2&gt;
  
  
  The other paths (and why they're complementary, not competitors)
&lt;/h2&gt;

&lt;p&gt;HTTP 402 is not the only answer, and it's worth knowing the map:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MPP (Machine Payments Protocol)&lt;/strong&gt; — a second standard (Stripe + Tempo + Visa) that also uses HTTP 402 and adds session/subscription semantics on top. Backward-compatible with x402 at the 402-challenge level.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP Billing Spec v1&lt;/strong&gt; — a community draft (&lt;code&gt;noui.bot&lt;/code&gt;) that layers billing metadata over MCP's &lt;code&gt;_meta&lt;/code&gt; namespace, closer to the tool protocol itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS AgentCore&lt;/strong&gt; — wraps MCP servers behind a gateway that adds auth and metering; payment is handled by the platform, not the server.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are in the MCP core spec, and none of them should be. The clean split — MCP for tools, 402 for payment, a facilitator for settlement — is the shape that's actually stabilizing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom line
&lt;/h2&gt;

&lt;p&gt;MCP 2026-07-28 is a protocol-level bet that agents will talk to &lt;strong&gt;stateless fleets of tools, not stateful single servers&lt;/strong&gt;. It removed the session assumption, standardized routing and multi-round-trip interaction, and got rid of the things that tied a server to a single machine.&lt;/p&gt;

&lt;p&gt;It did &lt;em&gt;not&lt;/em&gt; add payments — and that's correct. The payment layer lives one status code below the tool protocol. If you run an MCP server that should earn money, the path is: return &lt;code&gt;402&lt;/code&gt; with payment instructions, verify the receipt through a facilitator, and let the stateless architecture you're now building on do the rest.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>x402</category>
      <category>payments</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
