Skip to main content
Independent MeasurementMethodology: L0, L1, L2Version 3 · last changed 2026-11-02
vet402Back to the registerFacts only · no composite scores

What these measurements mean

Chain pages for the purchase lanes are at x402 and MPP payments by chain.

In 60 seconds

L0 asks whether the payment wall answers. L1 asks whether a real purchase settles and comes back with a non-empty 2xx. L2 asks whether the response matches the seller's declaration. Unverified is not a failure. A 0–100 score is a different API and is never an L0–L2 result. Every word this page publishes measurements in is defined once, in one line each, in section 10.

0.Where the catalog comes from

Every endpoint on this page was discovered through the CDP x402 Bazaar, the Circle x402 discovery, or the MPP directory (and equivalent public discovery surfaces). Those catalogs are the measured population, not the whole x402 economy: discovery surfaces vet402 does not read are outside the routes vet402 probes or purchase. A chain can be absent from byChain for that reason, and it can also sit there with a small count because part of its listings reach this catalog and part do not. Either way the figure is a coverage limit of this observatory, not a finding about that chain. The catalog is an input; the measurements are the record. For a listing read from the Circle x402 discovery, the network vet402 stores is the first accept on a mainnet that vet402 can sign, excluding GatewayWalletBatched. When no such accept exists, the network is Arc if the listing has an Arc GatewayWalletBatched accept. Otherwise the network is the first accept. Listings read from the CDP Bazaar keep the first accept as the stored network. Listings from the Circle x402 discovery can be Base purchase candidates when that stored network is Base. They share the first-purchase quota for a UTC day and the day's budget with listings from the CDP Bazaar. In the C1 run, listings from the Circle x402 discovery with no probe yet take at most one third of the remaining places, so a listing that already has a probe keeps a place in that run.

1.What L0 measures

An L0 probe sends one request to a catalog-listed endpoint, with no payment attached. The request uses the HTTP method the catalog entry declares when that method is GET, POST, HEAD, or DELETE. When the entry declares no method, or a method outside that set, the request uses GET. Two failures in a row on that request can be published as fail (section 3). Under the x402 protocol a compliant server answers HTTP 402 Payment Required with an accepts array before executing anything, so the probe is free, side-effect free, and observable. We record: whether 402 came back, whether accepts parses, whether the advertised price, asset, network and receiving address agree with what the catalog declares, and the latency — each with a timestamp and a response digest kept as evidence.

2.The verdict vocabulary

pass — the probe received 402 and the challenge was consistent with the catalog declaration. fail — the probe contradicted that: no 402 (any other status), a DNS, timeout or connection failure, a challenge with no payable accept, or a challenge whose price or receiving address contradicts the catalog; the specific reason code is always recorded, and each code is defined in the table below. unverified. unverified means vet402 does not have grounds to publish either pass or fail yet. It is not a failure and is never counted as one: it is an entry not yet reached by the rolling schedule, an entry whose failing probe has not met the publication gate, an entry recorded as path_template, or the remainder after the three above. The remainder includes a latest reason of rate_limited, tls, request_shape, or unsafe_target. It also includes a latest reason of method_undeclared, for which no request was sent before 2026-09-02, and body_over_cap, for which a 402 came back and the body was not read to the end. A listing that declares no HTTP method, or a method other than GET, POST, HEAD, or DELETE, is measured with GET, and two consecutive failures of that probe can be published as fail.

What that bucket is actually made of, over the endpoints on record at the time this page was rendered:

  • 37,998 not yet probed: the rolling schedule has not reached them
  • 6,791 with a failing probe that has not met the publication gate in section 3
  • 12 whose listed URL still contains an unfilled path parameter (path_template, below)
  • 1,750 are the remainder after the three counts above. The remainder includes a latest reason of rate_limited, tls, request_shape, or unsafe_target. It also includes a latest reason of method_undeclared, for which no request was sent before 2026-09-02, and body_over_cap, for which a 402 came back and the body was not read to the end.

Those four sum to 46,551, which is the same figure publishedUnverified reports at /api/v1/observatory/state. Until 2026-09-04 this section said the usual cause was the undeclared method. The four counts above are what the bucket is made of. A listing with no declared HTTP method does not have a line of its own in this breakdown. A listing with no declared HTTP method, or a method other than GET, POST, HEAD, or DELETE, is probed with GET. Two failures in a row on that probe can be published as fail.

path_template — the listed URL still contains an unfilled path parameter (/v1/entreprise/:siren, /items/{id}, /files/*); we do not know the real value, so no request is sent, the probe is recorded as unverified with this reason, and the endpoint is never purchased from — a 4xx from a request we could not have formed correctly is our limitation, not the seller's failure. Since 2026-09-17 a path segment with a modifier (/airline/:rest*) counts as well.

request_shape — an MPP endpoint (Tempo) that answers 400 or 422 with no Payment challenge validated the shape of the request before asking for payment. vet402 does not guess a request body or query: an L0 probe stays one request, with an empty JSON body at most, so the endpoint has not been measured and the probe is recorded unverified with this reason — the same principle as path_template. Since 2026-09-29 the same applies to any unpaid POST: the probe carries {}, and a 400 or 422 with no payment challenge says the endpoint checked an input we did not send, which is how paid requests are already treated (section on request bodies below). We chose this over sending the declared body: a body the seller's handler accepts could make it run (a sign-up, a message) if the wall did not ask for payment first, and an L0 probe must not cause work. Earlier probes recorded those answers as a no_402 fail; the rows stay as they were, and the endpoint's page says so next to each one. MPP probes send Accept-Payment: tempo/charge, the header the reference client sends, because some walls answer with plain content without it. An MPP endpoint that answers 402 with an x402 envelope and no Payment challenge is a failure with reason no_mpp_challenge: the wall is not one an MPP client can pay.

L0 reason codes

A probe that is not a pass records one of these codes. The endpoint's page prints the code with one line saying what was wrong, from what the probe recorded: which part of the envelope was missing, or the declared and the offered values side by side. Since 2026-09-29 a probe reads up to 64 KB of the 402 body. Before that it read 4,000 bytes, so an x402 v1 envelope longer than that (v1 carries the envelope in the body, not in a header) could not be parsed and was recorded as accepts_invalid: a measuring error on our side, not the seller's. Those rows are kept and marked on the endpoint's page; the next probe replaces the published verdict.

Re-measuring what the earlier rules published. A published fail whose two newest probes include a row recorded under the rules before 2026-09-29 — an unpaid POST answered 400 or 422 counted as no_402, an accepts_invalid read from a body cut at 4,000 bytes, or a price_mismatch or metadata_mismatch recorded without the values compared — is measured again first. The scheduled C1 probe run puts those endpoints ahead of the rest of its list, up to 25 per host per run with the hosts taken in turn, and a one-off pass re-probes the rest at no more than one request per second to any one domain. We do not re-label the recorded rows instead: they do not hold what the current rules look at (whether a 400 carried a payment challenge, how long the body was, the values compared). The old rows stay. When the re-probe changes the published verdict, the change is written to the correction log with the reason reverify (after.trigger rule_change_reprobe), in the same database statement as the new probe row. The rule for publishing a fail is unchanged — the two newest probes both failed — so an endpoint that fails again under the current rules stays a fail, now with the values compared on its page, and keeps that priority until both of its newest probes were recorded under the current rules.

L0 reason codes
CodeVerdictMeaning
no_402failThe endpoint answered with a status other than 402: content without payment (2xx), an API key demand (401/403), 404, a server error (5xx), or a 400/422 to a GET.
accepts_invalidfailThe endpoint answered 402, but neither the PAYMENT-REQUIRED header nor the JSON body carried an accepts[] entry with an amount (amount or maxAmountRequired), a payTo and an asset. The record says which part was missing.
price_mismatchfailNo accept in the 402 carried the amount and asset the catalog listing declares. The record shows both sides.
metadata_mismatchfailNo accept in the 402 carried the receiving address (payTo) and network the catalog listing declares. The record shows both sides.
no_mpp_challengefailAn MPP (Tempo) endpoint answered 402 with an x402 envelope and no Payment challenge, which an MPP client cannot pay.
dnsfailThe host name did not resolve.
timeoutfailNo response headers arrived within 10 seconds.
networkfailThe connection failed (refused or reset) before a response.
redirect_limitfailThe endpoint redirected more times than vet402 follows.
tlsunverifiedThe TLS handshake failed; vet402 cannot tell its own side from the seller's, so nothing is published against the seller.
unsafe_targetunverifiedThe listed URL points at (or redirects to) a non-public address, so vet402 sent nothing.
rate_limitedunverifiedThe endpoint answered 429; a rate limit is not a verdict on the payment wall.
path_templateunverifiedThe listed URL still contains an unfilled path parameter, so no request was sent.
request_shapeunverifiedAn unpaid POST (empty JSON body) or an MPP request was answered 400 or 422 with no payment challenge: the endpoint checked the input before asking for payment, and vet402 does not guess a request body.
body_over_capunverifiedThe 402 body was larger than the 64 KB vet402 reads and no PAYMENT-REQUIRED header was readable, so the envelope was not read in full.
method_undeclaredunverifiedBefore 2026-09-02 a listing without an HTTP method was not probed; since then it is probed with GET.

That principle is about the request, not about the URL, so it applies to the body and the authentication header too. A bare listing declares a URL, a price and a payee; it does not hand us an API key, and we carry no credential of the seller's. What JSON body a POST expects is a different matter: a listing that carries the Bazaar discovery extension declares it as extensions.bazaar.info.input.body, and the same declaration rides on the 402 the endpoint answers our unpaid request with. Since 2026-09-17, when that 402 declares a JSON object or array of at most 16 KB, the paid POST sends that body as declared, without editing it, and only to the seller's own origin (a redirect that would carry it to another origin is not followed; the row keeps the redirect's status), and the row records requestBody: declared; otherwise we send {} and record requestBody: empty. Before that date every paid POST carried {}, declaration or not. The ledger export carries that record as the column request_body: declared, empty, or none for a paid request formed with no body, which rows record from 2026-09-20. A blank cell means the row holds no record — rows before 2026-09-17, bodiless requests before 2026-09-20, and rows that ended before a paid request went out — and is not the same as empty. From 2026-09-20 a declared row also carries request_body_sha256, the SHA-256 of the exact bytes we sent. We do not republish the body: it is the seller's text, and the seller's own 402 shows it to anyone who asks. So when a paid request comes back 400 (the request is malformed), 401 or 403 (not authenticated), 404 or 422, the most likely explanation is the same one we already accept for a template URL: we could not form the request correctly. Those rows are labelled inconclusive and are not counted against the seller: not toward a BLOCK, and not in the denominator for delivered. The label follows the reason, not whether money moved. Since 2026-09-05 it covered a settled payment answered with a 4xx; since 2026-09-17 it also covers a paid request answered with a 4xx other than 402 and no settlement receipt, which is what a seller that checks the request before settling it returns. Before that change the seller that declined to charge for a request we got wrong was published in the harsher bucket. A 402 is different: it says the seller did not accept our payment, and it stays counted, with one exception we caused ourselves. From 2026-09-13 00:00 to 2026-09-15 23:49 UTC our Base payer wallet had run out of USDC, sellers answered our unfunded payments with 402 (and some with a 5xx, which during that window we cannot separate from a seller failing on an empty payment), and those rows are held as inconclusive with the reason payer_unfunded; since 2026-09-17 the runner reads the payer's USDC balance before signing and does not sign when it is short or cannot be read. The rows are not deleted, not hidden and not corrected away — the status and the HTTP code stay on the endpoint's page, the count is published as l1.inconclusive with the split in l1.inconclusiveByReason, and the ledger export carries the reason in its held_reason column. Since 2026-11-08, /sellers also puts a settled held row on the seller's side when the listing's 402 declares an input, vet402 sent every declared input, and the paid request got 400 or 422 with a JSON answer whose bodyHead parses as a structured input validation error (FastAPI/pydantic detail[].loc, zod issues[].path, or errors[].field|path|param) with every cited location on the request input, not free text. Answers about payment, x402, authentication, headers or cookies stay held. Such a row counts on the seller's side once the listing has a seller-side failure on 2 different UTC days (any kind). Other held rows stay not sorted. /api/v1/sellers/export.csv follows /sellers. The ledger export.csv does not change. The export's held_reason and status columns are unchanged. A 5xx is never held: a server fault is not something the paid request shape explains. Before 2026-09-05 every 4xx was published as settled-and-not-delivered, which reads as “this named company took the money and did not deliver” — see /corrections.

A seller on Base can look up its own purchase rows by domain at /sellers, with what we saw, what to fix and whose side each failure was on; /sellers/fix-first groups the failures by kind.

Whose side. Since 2026-09-29 a failed purchase is put on the seller's side when the row itself shows that vet402 was not at fault, and not otherwise. The row has to show each of these, checked in this order: (a) vet402 signed a payment. A row where it did not (no 402 to the unpaid request, a 402 offering no option vet402 can sign such as an upto scheme, or a price or payTo that differs from the listing) is marked not bought: vet402 did not pay and is not an L1 result. (b) It signed the exact scheme for USDC at the price and payTo the listing declares; the signed amount, asset and payTo are on the row. (c) Its payer wallet held at least the price when it signed. Since 2026-09-16 23:25:55 UTC the runner reads the balance before signing and does not sign when it is short; for earlier purchases on Base the balance is rebuilt from the wallet's own USDC transfers on-chain. The payer on Base has been 0x6777…3986 for the purchases from 2026-08-14 to 2026-09-04, and 0xc9c7…1670 since 2026-09-04; a transaction from August 2026 shows the first of these as the sender. (d) It sent the input the listing declares (body, query, headers and path parameters), and the row records what it sent. vet402 sends no request headers of the seller's and does not fill path parameters, so a listing that declares them is not sorted; a row from before vet402 recorded the query it sent on Base (2026-09-27 23:27:16 UTC) is not sorted. (e) The seller answered explicitly (an HTTP status, a receipt that does not match the chain, a signed payment refused with 402) or had its full declared time: when the paid request got no answer within 20 seconds, the seller's side needs the listing's maxTimeoutSeconds to be no longer than that and the row to name no transfer. A paid request on Celo waits 45 seconds, since 2026-10-19, because some Celo facilitators answer only after the on-chain receipt. A signed payment refused with 402 on a paid request that carried the declared body or query (which the unpaid request did not) is not sorted either, because the terms for that request may differ from the ones vet402 paid. The seller page shows, for each row, the terms vet402 signed or the options the 402 offered, the input it sent, and the listing's maxTimeoutSeconds.

Two days before the seller's side. A row that shows all of (a) to (e) is not yet on the seller's side on its own. It goes there only when the same listing has such a failure on at least two different days (UTC); until then it reads not sorted: one failure so far, with what happened and the fix shown as they are. The rows on the seller's side are marked “attribution under review” while that review runs (it has no set date), and a seller page carries the review notice when it has such a row. For each signed row the seller page publishes what the row recorded about the payment and the answer: the scheme, amount, payTo and one-time nonce vet402 signed (validBefore is not recorded, and the page says so), and the paid request's HTTP status, Content-Type, and whether a PAYMENT-RESPONSE came back, with its success and errorReason. The answer's body and other header values are not published. Each row links to the dispute form on the listing's record page, with the purchase time filled in.

Errors against the seller. Three kinds of row used to read worse for the seller than the record allows, and are sorted as follows. A payment that settled before the seller refused an input we had not sent (the body or query periods below) reads not sorted: charged, then rejected an input vet402 had not sent: both facts stand, and it is counted against neither side. A paid request that answered 2xx while the seller named a settlement transaction, although its receipt did not say success, awaits on-chain verification like any other receipt instead of reading as a failure. A 2xx or 4xx with no receipt, for which our payer's transfer of exactly the price to the listing's address landed in the window, is linked to that transfer even when our index did not hold it (see below). When the export's held_reason and the page name different causes for the same row (for example unsettled_4xx in the export, our wallet holding less than the price on the page), the row says both.

vet402's side: the row shows the cause was ours or a limit of ours. Our payer wallet had run out of USDC (payer_unfunded, 2026-09-13 to 2026-09-15), or held less than the price by the rebuilt balance (for example on 2026-09-12 18:02 UTC); a paid POST was refused with 400, 415 or 422 before 2026-09-16 23:25:55 UTC, when we sent an empty JSON body although the listing declares one; a paid request on Base was refused with 400 or 422 before 2026-09-27 23:27:16 UTC, when we did not add the query the listing declares; or our own limits and errors. On XRPL before 2026-09-21 06:00:30 UTC, on Solana before 2026-10-01 12:00:20 UTC, on Arc, Celo, Stellar and BNB Chain before 2026-10-19 04:05:17 UTC, and on Arbitrum One and Avalanche C-Chain before 2026-11-02 04:55:29 UTC, a signed paid request that the seller refused with 400 or 422, with no record of the query we sent, is on our side when the listing declares a query. A row at or after that time on its chain, with no record of the query, is not on our side for this reason. “Declares” is read from the same place our request is built from: the example values in the listing's extensions.bazaar.info.input, as well as the names its schema marks required. Not sorted: a failed purchase that cannot show (b) to (e); a failure at a listing whose declared input is an example domain (example.com, .org, .net, or a name under .example, in the listing's input schema, its declared input, or its URL), because vet402 sends the values the listing declares and cannot show a placeholder was a valid input; a held row (a held_reason in the export) or a row awaiting on-chain verification; and a failure where no payment was taken, meaning no settlement receipt and no transaction on the row, whether the paid request got a 4xx or a 2xx. A seller that declares a miss is free is not failing when it does not charge for one.

The seller's side is the group these pages count as the seller's fault, and the decision API reads the same sorting. Rules 2026-11-13.1 (rules_version). Each paid attempt (one L1 purchase row) is sorted by one function, the same one /sellers, /api/v1/sellers/export.csv and this decision read: whether money moved, whether it delivered, and whose side a failure is on. Money moved when the payment settled on-chain, or when a settle_failed row carries a transaction vet402 verified; it did not move when vet402 signed nothing or the transaction the seller named was not found; otherwise it is not yet known. The outcome is one of: delivered (settled on-chain, then a 2xx other than 202 Accepted, with a non-empty body); pending (not yet known whether money moved: a receipt awaiting on-chain verification, a 2xx naming a transaction, or a signed attempt that failed less than 90 minutes ago while vet402 links late settlements; the ledger status is left as recorded); seller (the seller's side: vet402 can show from the row that the failure was not its own, and such a failure, of any kind, was seen at the listing on 2 or more different UTC days); vet402 (vet402's side: its wallet was short, it did not send the declared input, or one of its own limits or errors); unsorted (not sorted: held for vet402's own request shape, no charge taken, a paid answer with no result in it, seen on one UTC day so far, or vet402 cannot show the failure was not its own); not_bought (vet402 did not sign a payment, so it is not a purchase result). An attempt counts for or against the seller only when it delivered or is on the seller's side; every other signed attempt is left out with an l1_not_counted_* code (l1_not_counted_held counts held rows except seller_fixable_input, plus a receipt awaiting on-chain verification whose held_reason is empty). A settled 400 or 422 whose paid request carried every declared input and whose bodyHead parses as JSON with a structured input validation error (FastAPI/pydantic detail[].loc, zod issues[].path, or errors[].field|path|param with in or location on body or query, not free text, with every cited location on the request input, not payment, headers, cookies or authentication) is counted on the seller's side once seen on 2 different UTC days. In the decision it counts as a seller-side attempt (l1_latest_failed or l1_never_delivered, WARN) but never as l1_paid_not_delivered and never toward a BLOCK. /api/v1/sellers/export.csv follows /sellers. The ledger export.csv does not change. L1: after the last delivery in the 30-day window, an attempt that moved money and did not deliver adds l1_paid_not_delivered; it is a BLOCK only when two or more of them are on the seller's side (seen on 2 different UTC days), and a WARN otherwise, including when /sellers leaves them not sorted. A failure where no money moved is at most a WARN (l1_never_delivered when nothing was ever delivered). When no result came back and it is not yet known whether money moved (an empty 2xx with no settlement linked, a receipt still awaiting on-chain verification, or a signed attempt that failed less than 90 minutes ago), l1_empty_2xx_settlement_unknown makes it a WARN, never an ALLOW, until it settles. l1_latest_failed (something delivered, but the latest counted attempt did not) is a WARN. ALLOW needs a delivery within 30 days (l1_basis.fresh_days); otherwise l1_stale and a WARN, also the word when the only delivery is older than the window or every signed attempt is (l1_not_attempted means none was ever signed). l1_basis carries the counts, the last attempt, signed attempt and delivery, and the days since each. L0: l0_fail (two measured failures in a row) is a BLOCK. l0_unverified (not probed yet, no probe in the last 30 days, a URL with a path parameter vet402 cannot fill, a probe that could not complete, or one failure so far) is a WARN with degraded: true, next to l0_unverified_<cause>; unverified is not a failure. L2: l2_mismatch is a BLOCK when the evidence names missing keys (mismatch_kind missing_keys), otherwise l2_mismatch_unexplained and a WARN. L2 reads the latest delivery at any age, so a mismatch holds until a later delivery replaces it. A listing that declares an output schema but whose latest delivery vet402 has not checked against it (no delivery, a body over 256 KiB or cut off) is l2_not_checked, a WARN; l2_undeclared means neither the catalog nor the purchase row of the latest delivery gives an output declaration to check against. l2_conform_no_required_keys next to l2_conform means the declaration marks no key as required, so vet402 checked that the body is a JSON object or array, not which keys it carries. verified_terms.amount is in 6-decimal ledger units: multiply it by 10^12 on BNB Chain and by 10 on Stellar before comparing it with a 402. Every code, its effect and whether the seller can fix it: https://vet402.com/docs/api#reason-codes. Each listing on the seller page shows the decision API's answer at the time the page was read, with the codes that decided it first and the meaning of each from the reason-code table.

A transaction the seller named that does not appear. A receipt's transaction that is not on-chain yet is re-read on later runs of the verifier and does not count either way; a read that fails (a timeout or a rate limit) is not counted as “not found”. On Base, Arc and Celo, when the chain has answered that the transaction does not exist on at least 3 different days (UTC), it is 7 days after the purchase, the payment authorization vet402 signed shows as unused on-chain, and one more read still finds nothing, the row is recorded as seller-named tx not found, with the date. A receipt's id that is not a transaction hash at all is recorded the same way after the same number of days, once the authorization shows as unused. A transfer vet402 linked itself, and a receipt that did not say success, are not closed this way. The change is logged on /corrections. Until the seller pages leave noindex, the record page of a listing with any purchase that did not deliver is also noindex and is left out of the endpoint sitemap, and so is the record page of a listing whose latest published L0 verdict is not pass.

How long we wait for a payment. The runner waits up to 20 seconds for each HTTP answer, including the answer to the paid request, and records the purchase from that answer; it does not wait on-chain at that point. A paid request on Celo waits 45 seconds, since 2026-10-19, because some Celo facilitators answer only after the on-chain receipt. A transfer that lands later is found by our index of on-chain settlements if it falls between 2 minutes before and 30 minutes after the attempt. The index reads transfers to the receiving addresses in the catalog as it goes, so a transfer to an address that joined the catalog later is not in it; for such a row we read that payer's transfers to that address on-chain directly. When several purchases with the same payer, address and price have the same number of matching transfers in their windows, and no nonce was recorded (before 2026-09-04), each is linked to one of them in time order: every one of them was charged, and the re-read can check amount and payee, not which is which. The index and the re-read of each transaction (32 confirmations on an EVM chain) run later, on their own schedule. Until then a 2xx or 4xx with no receipt reads as no charge, not as a seller failure; once a transfer is linked, the row awaits verification, and after the re-read it reads as settled.

The same declaration also states the query. A listing that carries the Bazaar discovery extension declares query arguments as extensions.bazaar.info.input.queryParams, names with example values, and that declaration rides on the same 402 we took the payment terms from. Since 2026-09-20 the runner carries this behind a per-network allow-list: on a network that list names, the paid request appends to the listed URL those declared names the URL does not already carry, with the values the seller wrote. Nothing else is turned into a value — the schema's required, enum, default and description are not read for one, so a name the schema marks as required and the declaration leaves out goes unsent. A value that is not a string, a finite number or a boolean; an empty name; more than our caps (32 names, 2 KB of appended query, 4 KB of URL); two declared names that collide; a host or a path that would move — any one of these and the declaration goes unused whole. A name the listed URL already carries is handled differently: the listing's own query stands, so that one name is dropped and the declaration's other pairs are still appended. In that case the declaration goes unused only when nothing is left to append. Names are folded before they are compared: trimmed, lower-cased, with spaces, dots and [ turned into _, because a back end that reads query names case-insensitively, or folds them the way PHP does, would let a declared name overwrite one the listing published; against the listed URL a declared name is also tried cut at its first [.

The row records requestQuery: declared when the pairs went out, empty when the seller declared none, and refused when a declaration was there and these rules did not use it. The ledger export carries that record as the column request_query, from 2026-09-21. A blank cell means the row holds no record — a row on a network the allow-list does not name, a Tempo (MPP) row, a row that ended before a paid request went out, and rows before 2026-09-20 — and the column alone does not separate those from one another. This page names no chain because the ledger already does. Within the window the export returns, the rows whose request_query is not blank are the networks this has been in effect on, and the declared ones are where pairs were actually appended; a window with no such row at all means it did not run on any network inside that window — and a response carrying x-vet402-truncated: true is missing its newest rows, which is where a lane that just started would show. A network can be on the allow-list and still show nothing but empty and refused, which says the sellers reached there declared no usable query — not that the runner was off.

A declared row also carries requestQuerySha256, published as the column request_query_sha256: the SHA-256 of the appended pairs written as one form-urlencoded string — the appended pairs by themselves, in the declaration's own key order, except that an integer-like name sorts ahead of the rest in ascending order, by the rule JavaScript applies to object keys; a space as +; and no leading ? or &. What that hash supports is matching: whether two rows on the same endpoint appended the same pairs, or whether a fresh 402 still declares the same names and values. It covers the appended pairs alone, not the listed URL, the body, or which endpoint was bought, so two rows on different endpoints can carry one hash. It is not a way of hiding the query — the names and values are the seller's own, and the 402 that answered our unpaid request is where they were read from. The row does not carry the string; that 402 is where to read it. The unpaid request goes to the URL the catalog lists, because the declaration arrives with the 402 that answers it — and the amount and the recipient we sign, and the resource.url inside the payment envelope, come from the catalog and are unchanged.

3.Publication gate

A single failing probe is never published as fail: transient network conditions — including ours — are indistinguishable from a dead endpoint in one sample. The register shows fail only after 2 consecutive failing probes; until then the published state is unverified. Every underlying probe, including single fails, remains visible in the endpoint's history with its evidence.

4.Delisting detection

The Bazaar catalog is read in runs scheduled roughly two hours apart (00:05 UTC to 22:05 UTC). Each run continues from the page where the previous run stopped, so one full read of the catalog takes several runs. When a full read closes, the listings it saw and the catalog's own reported total are published as latestSnapshot (fetchedCount and totalCount). A read in which more than 5% of the pages could not be read is marked incomplete. A later incomplete run does not replace a complete snapshot of that day. A Bazaar listing is not marked delisted at present, and that includes a listing that is also in the Circle x402 discovery. The rules below apply to the MPP directory and the Circle x402 discovery. The MPP directory is fetched once a day, at 01:30 UTC. A listing that leaves the MPP directory is not judged delisted until the next complete 01:30 UTC fetch. The Circle x402 discovery is fetched at 01:30 UTC, on the same run. A listing that leaves the Circle x402 discovery is not judged delisted until the next complete fetch of that catalog. A key that another catalog still lists is not recorded as delisted. An incomplete fetch counts the keys in that fetch and the keys in each fetch since the previous complete fetch. An endpoint present on an earlier day and absent from a complete fetch is recorded as delisted, with the before/after values kept on the event, unless another catalog still lists that key. On any day vet402's own fetch is incomplete (vet402 stopped before the end of the catalog), no delisting judgements are made. A gap in vet402 data must never read as a disappearance in yours. For the MPP directory, the fetched count can sit below the catalog's reported total because duplicate keys are counted once. Reappearance is recorded as relisted. A fall in the catalog-reported 30-day call count of 70% or more, from a base of at least 100 calls, is recorded as settle_drop — a factual observation of the catalog's own telemetry, not a judgement.

5.What L0 cannot measure

Without purchasing, we cannot observe whether the endpoint actually delivers what it sells, the quality of what it returns, or settlement behaviour after payment. An endpoint with L0: pass has a standing payment wall — nothing more is claimed. L1 and L2 below cover settlement and conformance; L3, opinion on the quality of what is delivered, is not built and nothing on this site presents one.

6.What L1 measures

An L1 purchase is a real transaction: one purchase per endpoint, at most once per 6-day window for the catalog at large — see the priority list below for the 4 hosts bought from more often, and the paragraph after it for endpoints we have already settled with 3 times — targeting endpoints whose most recent L0 verdict is pass, prioritised by real observed demand (30-day payer and call counts reported by the catalog). Endpoints with no purchase row of any status go before endpoints we have attempted before; within each group, ties on demand go to the one that has waited longest (no row yet: earliest in the catalog; attempted before: longest since our last attempt of any status), then the lower internal id. We request unpaid first to read the 402 challenge, then select a payment option and refuse to proceed unless every one of these holds: scheme exact, a network whose payer lane is switched on for that run (Base is on by default; each other lane sits behind its own flag, and the chains L1 has actually bought on are the rows of the by-chain table in the State of x402 report), that chain's pinned asset (USDC on Base, Arc, Celo, Solana, Stellar, Arbitrum One and Avalanche C-Chain, USDC.e on Tempo, RLUSD from its fixed issuer on XRPL, and USD1 on BNB Chain). The price matches what the catalog declared when the target was chosen. Any deviation (a different asset, a different chain, a higher price) is recorded as a refusal, never paid. A hard per-purchase ceiling ($0.25 since 2026-10-09, $1.00 before that; 337 signed rows in the ledger were above $0.25 when read on 2026-11-03) and a daily budget ($25) are checked against a database ledger, not memory, before every signature, so a restart or a concurrent run cannot double-spend. A signed spend stays on the record whether or not the seller delivers. On Base, Arc, Celo, Arbitrum One, Avalanche C-Chain and BNB Chainthe signature is an EIP-3009 authorization, and a signed EIP-3009 authorization is live money the moment it exists.

Sellers without a track record. Since 2026-11-02 a seller has a track record once its payee or its registered domain has delivered purchases on at least 3 different UTC days, and the first of those is at least 14 days old. A shared suffix where different owners cannot be told apart does not earn a track record as a domain. Until then a purchase from that seller is limited to $0.10, and sellers without a track record share $3 per UTC day, inside the $25 budget for that day. A listing above that price, or one whose declared price cannot be read, is not bought for now and gets no row: this is a limit on our spending, not a finding about the seller. Purchases above $0.10 share $15 per UTC day, with or without a track record, on the same terms.

Tempo (MPP). Tempo has no x402 wall. Services listed in the MPP directory (mpp.dev) answer 402 with a WWW-Authenticate: Payment challenge — the Machine Payments Protocol (Stripe + Tempo). Since 2026-09-17 the catalog carries those endpoints under the source mpp_directory, and L0 records the challenge as dialect mpp: a pass requires a tempo/charge challenge on chain eip155:4217, in USDC.e, at the price the directory declared, naming a valid recipient (the directory lists no recipient; L0 learns it from the challenge). The Tempo payer ships behind its own flag; while that flag is on, Tempo purchases are made and Tempo settlements are re-read on-chain by a Tempo-specific verifier, and the count reached so far is the Tempo row of the by-chain table in the State of x402 report. The same ceilings apply ($0.25 per purchase, the shared $25 per UTC day, and a $2 per UTC day lane for Tempo) together with the same refusal funnel: anything other than a tempo/charge challenge on 4217 in USDC.e, at the declared price, to the recipient L0 learned, is refused before the budget is reserved.

We buy under our own name. Every request in this pipeline — the unpaid L0 probe, the unpaid read of the 402 challenge, and the paid request itself — carries a User-Agent that says who we are and links back to this page: vet402-observatory-l0/1.0, vet402-observatory-l0-recheck/1.0 and vet402-observatory-l1/1.0, each with (+https://vet402.com/observatory/methodology). There is no rotation, no disguise and no attempt to look like an ordinary buyer. A seller who wants to treat vet402 differently can, and can do so from the first byte of the request. A purchase that succeeds for vet402 does not stand in for another buyer. The same seller can return a different answer to a request that does not carry this User-Agent, and this row does not show what that buyer received. Until 2026-09-05 this page said purchases were made “covertly” — that was never true of the implementation, and it is corrected on /corrections.

The priority list, and why it exists. The 4 hosts named below are not on the 6-day window. They may be re-purchased once every day, and they are pinned to the head of candidate selection: x402.twit.sh, x402.tavily.com, stableenrich.dev, api.exa.ai. The reason is that a settle-through record is worth more as a series than as a single row, and the list was picked from independently reported demand rather than from our own ledger (internal record of 2026-08-14). We are not restating that third party's figure here: we have not re-measured it, and we cannot point you at a source you could check. What we can point at is current demand — the per-endpoint 30-day call count the catalog reports, a column on the register, which is the number to judge this list against. Two things this does not change: the measurement is the identical pipeline with the identical gates, and the result publishes exactly as anyone else's does, pass and fail alike. What differs is how often we buy, and that is stated here rather than left for a reader to infer from the timestamps. Until 2026-09-04 this section said “at most once per 6-day window” with no exception named, which was false for these four.

Endpoints already proved, and the daily intake limit. Once an endpoint has 3 purchases we re-read on-chain and confirmed (settled — a seller claim we have not verified does not count), its window widens from 6 days to 30. A fourth confirmed row on the same endpoint does not change what this site says about it, and the budget it would have taken goes to endpoints nobody has measured yet. The priority hosts above are exempt and stay on their daily window, because there the series itself is the measurement. Separately, at most 120 endpoints get their first purchase in any one UTC day: when the catalog grows in a single jump, first purchases would otherwise crowd repeat purchases out of the daily budget, and an endpoint held back is bought the next day rather than dropped. Neither rule removes anything from the catalog, from L0, or from the published ledger — only the interval changes.

One row for each seller: the census. Since 2026-09-28, a seller on Base (a host name in the catalog, with the port ignored) that we have not yet bought from at all, with no purchase row of any status on any of its endpoints, goes ahead of the demand order. We buy from it once, using the listing whose catalog price is lowest among that seller's listings that name Base first and offer Base USDC (ties go to the lower internal id). That catalog price is the amount we pay, because the payment is checked against it; a listing with no such price, or one above the per-purchase ceiling, is not picked. At most 40 such sellers enter a single run, placed after the per-chain lanes and after the priority hosts above, so both keep their place at the head. The aim is that each seller has one row of its own on the published ledger, which it can look up and act on. Everything else stays as it was: the per-purchase ceiling, the daily budget, the per-chain lanes, the daily limit on first purchases, the reservation before signing and the stop switch all apply unchanged, and the result is published like any other purchase. After that one purchase, the seller is on the ordinary sweep.

A second try when the failure was ours: the retest. Since 2026-09-28, a seller on Base whose most recent row failed for a reason on our side is bought from once more, before any census entry and inside the same limits. Three reasons count as ours: our payer wallet had run out of USDC (the rows held as payer_unfunded); a paid POST was refused with HTTP 400, 415 or 422 before we began sending the request body the seller declares, while the seller's current listing does declare one; or, on Base, a paid request was refused with 400 or 422 during the period before we began adding the query parameters the seller declares there (2026-09-27), while the current listing declares query parameters (example values, or required names). Such a refusal from a listing that declares no body, or no query parameter, is not treated as ours, and neither is a settled row or any failure the seller's own answer explains. When the failure was the missing body or query, we buy that same listing again, as long as it can still be bought; otherwise, and for an empty wallet, the listing is chosen as in the census: one per seller, the lowest catalog price that names Base first, within the per-purchase ceiling. The census and the retest share the 40-seller limit of a run. The aim is that no seller is judged, when this ledger is announced, on a failure it did not cause. The earlier row stays published; the new one is added after it.

A fresh look at a missing-key mismatch. Since 2026-11-02, a listing that names Base first, whose latest delivery missed keys its own schema requires, and which vet402 last tried more than 30 days ago, is bought again, ahead of the census and inside the same limits. This re-buy comes back at most once per 30 days while the listing stays like this; the ordinary sweep can buy it sooner. The row is marked retest. vet402 buys that listing itself and not another listing of the same seller. A seller who fixed the response should not wait on the sweep to have it read again. The earlier row stays published.

settled — vet402 re-read the transaction on-chain and found the transfer it paid for: from the vet402 payer, to the payee the catalog declares (on Tempo, the recipient L0 learned from the challenge), for the declared amount, in that chain's pinned settlement asset (USDC on Base, Arc, Celo, Solana, Stellar, Arbitrum One and Avalanche C-Chain, USDC.e on Tempo, RLUSD from its fixed issuer on XRPL, and USD1 on BNB Chain). Through Circle Gateway on Arc one batch transaction can settle many payments and there is no separate transfer to the payee: Circle's Gateway API names the batch for the purchase's nonce, and vet402 checks the buyer's debit in that batch. On Base that means an ERC-20 Transfer log matching all four of those with at least 32 confirmations; on Solana it means the transaction is finalized, succeeded, and the USDC token-balance deltas in it show the payee's wallet receiving at least the declared amount while our payer's wallet loses it — read from balances rather than from instructions, and only after the RPC's own genesis hash confirms we are reading the cluster the purchase declared. settle_claimed — the seller returned a settlement receipt with a well-formed transaction id, and we have not re-read it on-chain yet. settle_claim_refuted — we re-read it and that transfer is not there. settle_claimed_unverifiable — the id returned is not even well-formed for that chain. delivered_no_receipt — the seller returned 200 but the response carried no settlement receipt. settle_failed — no successful paid response came back at all. inconclusive — the paid request answered 4xx (settled, or refused with no settlement receipt and not a 402), or it answered 402 or 5xx while our own payer wallet was unfunded, so the judgement is held rather than counted against the seller (§2, the same principle as path_template); the row still publishes, it is simply out of the denominator for delivered and does not count toward a BLOCK. Every attempt, including refusals before any money moved, is visible on the endpoint's page with its evidence.

l1_not_attempted — we have not signed a paid attempt against that resource, so what it sells is unverified rather than refuted. Since 2026-09-05 the runner can be stopped mid-batch by a runtime spending halt, and while that halt is on, nothing new is signed. So a decision document carries spending_halted beside its facts, and facts.l1.last_attempt_at says when we last looked — a halted week and a quiet week are otherwise the same picture. When the gap is ours to explain, not_attempted_reason names it: spending_halted, or no_eligible_accept when the wall offered nothing machine-payable. We do not write our own halt into the seller's record. The other ways an attempt ends before the signature — over cap, price mismatch, payee mismatch — leave the field null here and stay where they already are, one row per attempt in the public decision ledger.

l1_inconclusive — since 2026-09-08, the neighbouring case: we did sign, but each paid attempt is held as inconclusive (§2): a 4xx we attribute to our own request shape, or a 402 or 5xx while our own payer wallet was unfunded, so there is no paid response to judge. Those rows are counted in facts.l1.n_attempts (and in n_settled when the payment settled) — the same set the per-endpoint purchases route reports — and disclosed as n_inconclusive; the decision rules read conclusive attempts as n_attempts − n_inconclusive, so they do not count toward a BLOCK. Before that date the same rows were left out of n_attempts, and a seller with ten settled purchases was published as l1_not_attempted — a gap in our measurement, not evidence against the seller, and now named as such.

settled comes at two evidence strengths, and both counts are published. Since 2026-09-04 12:00 UTC a purchase on Base, Arc, Celo, Solana, Arbitrum One, Avalanche C-Chain, Tempo, XRPL or BNB Chainstores a one-time value for that purchase (the EIP-3009 authorization nonce on Base, Arc, Celo, Arbitrum One, Avalanche C-Chain and BNB Chain, a memo written on Solana, the memo of a TIP-20 transfer on Tempo, the hash of the signed blob on XRPL) and the re-read matches that value. Those rows are published as nonce-bound: the re-read matched the one-time value stored for that purchase, so the transaction is the one for this purchase. Stellar is not in this count. The Stellar re-read checks payer, payee, amount and asset. Rows that settled before that timestamp, and Stellar rows, matched on payer, payee, amount and asset, and the re-read did not bind a one-time value. Those rows are published as amount + payee. A seller holding several catalog entries at the same price and the same payee could have answered with a transfer received earlier and passed that check. At the current reading, 1,975 of 15,051 settled rows are amount + payee and 13,076 are nonce-bound. The split is live at /api/v1/observatory/state as l1.settledNonceBound and l1.settledAmountPayeeOnly, which sum to l1.settled, and per chain in l1.byChain.

Why the older rows keep the label. Demoting them would assert something we cannot show — that those transfers were not the ones bought — and we hold no evidence for that assertion. Refuting a seller on evidence we do not have is a worse error than the one we are disclosing here, so the count of settled is unchanged and the strength is stated beside it. One check does work retroactively: the settlement block time against the moment we attempted the purchase. A transaction the seller had received earlier would sit far outside that band. On 2026-09-05 the 1,589 settled rows with a block time on record ran from 1 second before the attempt to 62 seconds after it, and none sat outside a -5/+15 minute window. That window is reported as l1.settledTimeWindowOk, with l1.settledTimeWindowUnknown for the rows whose block time we do not hold — those are neither inside nor outside it, and we do not count them as either.

Who named the transaction. On most settled rows the seller named it: the paid response carried a settlement receipt, and we re-read the transaction it pointed to. Some sellers settle and return no receipt — the paid request times out or answers an error, and the transfer lands anyway. Since 2026-09-04 our own index of on-chain settlements looks for that transfer: from our payer, to the endpoint's payee, for the exact amount, between 2 minutes before the attempt and 30 minutes after it, and not already tied to another purchase. Since 2026-09-19 it is attached to a row only when that row is its single candidate, and on XRPL only when it is the hash of the blob we signed. Since 2026-09-29, when two or more of our rows on an EVM chain are candidates for the same transfer (the same payee and price a few seconds apart), we read the transaction's receipt and attach it to the one row whose signed authorization nonce it consumed, and to none if no single row matches. The row then goes through the same re-read as a seller's claim before it reads settled. The seller did not name that transaction; we did, and the record says so: the ledger export carries settlement_source as seller_claim, vet402_index or circle_gateway, the endpoint page marks the row, and the count is live as l1.settledLateLinked. At the current reading, 1,260 of 15,051 settled rows carry a transaction our index attached. When the re-read rejects a transaction our index attached, the row goes back to what it was before, and the seller is not marked settle_claim_refuted for a match that was ours.

Signed is not moved. In the ledger export, spent_units is the amount we signed for on an attempt — what we put at stake — and a settle_failed row with no transaction carries the price there too. The amount we confirmed on-chain is the column confirmed_units (since 2026-09-29): spent_units on a settled row, 0 on any other row, including a settle_claimed row still waiting for the re-read. A 0 there means we hold no confirmed transfer for that row, not that we showed none happened. The CSV response carries both definitions in its x-vet402-column-notes header. The column l2_reading (since 2026-09-29) is l2_schema read again with today's rules, the value the record page and the decision use; l2_schema keeps the value recorded at the time.

Joining a correction to its row. The ledger export's column purchase_id (since 2026-09-29), is the purchase's id — a random UUID the database gives each attempt, carrying no payer, payee or amount. A row of the correction log about a purchase (subject_type purchase on /api/v1/observatory/corrections) carries the same value as its subject_id, so a ledger status change can be matched to the row it changed.

settled is not delivered. settled is a statement about the money: we confirmed the transfer on-chain. delivered is a statement about the answer: the attempt is settled and the paid request answered HTTP 2xx other than 202 Accepted with a non-empty body. It does not show the content is what was paid for: a 200 with an error in its body can count as delivered, and L2 looks at the shape alone. A seller can take the payment and answer 400, and that row is settled and not delivered. Both counts are published side by side on the endpoint page, on the register, in the badge, and at /api/v1/observatory/state(l1.settled and l1.delivered), so the difference between them is money that moved without a delivered response: a non-2xx answer (most often a 4xx or 5xx), a 202, an empty body, or no recorded answer. Until 2026-09-04 just settled was published, which let an endpoint whose every paid request answered 400 read as a full settle-through record.

What changed on 2026-08-23, and what is still open. Until that date, settled meant only that the seller had asserted success in its own PAYMENT-RESPONSE header — we published that assertion without ever re-reading the chain. It is now a measurement we make: the definition of settled is “vet402 confirmed it on-chain”. The gap named here until 2026-09-04 — L1 purchases ran on both Base and Solana, but Solana settlements were never re-read, so a Solana purchase reached settle_claimed and stayed there — is closed: Solana settlements are now re-read on-chain by a Solana-specific verifier, and a Solana purchase is promoted to settled only on the same evidence Base requires. What remains open is narrower and still worth naming: the re-read is chain-specific, because what binds a transfer to the signature vet402 holds differs by chain: on Base, Arc, Celo, Arbitrum One, Avalanche C-Chain and BNB Chain the EIP-3009 authorization nonce, on Solana a memo vet402 wrote, on Tempo the indexed memo of a TIP-20 TransferWithMemo, on XRPL the hash of the blob vet402 signed (which is the transaction hash itself). Stellar is re-read for payer, payee, amount and asset, and is published as amount and payee. A purchase on a chain with no re-reader stays at settle_claimed rather than being promoted on evidence that re-read does not exist. When vet402's own RPC cannot answer, or reports a different cluster than the purchase declared, the row stays unverified rather than being called refuted. An instrument vet402 could not read is not a finding about the seller.

Checking a Circle Gateway batch as the buyer

Circle Gateway can settle many x402 payments in one batch transaction, which keeps the onchain cost of each payment low. The tradeoff is that a buyer does not get a separate transaction for each payment. vet402 runs the check below on its own Gateway purchases, and a buyer can follow the same steps.

First, keep the nonce of each payment authorization the buyer signs. The nonce is authorization.nonce in the payment payload. The Circle batching client passes that payload to hooks registered with onAfterPaymentCreation. The transaction field in the x402 settlement response is the Gateway transfer id, and the Arc hash comes from the next step.

Second, ask the Circle Gateway API which batch transaction charged each nonce. For Arc mainnet (eip155:5042) the read is GET https://gateway-api.circle.com/v1/x402/transfers?network=eip155:5042&nonce=<nonce>. On 2026-10-26 this read answered without an API key. A transfer whose status is confirmed or completed names the batch transaction in txHash. received and batched are earlier states, and failed means the transfer did not settle. Group the nonces by txHash. The sum for a batch adds the value of each authorization whose transfer names that batch.

Third, read that transaction on Arc. Check that it succeeded and that it was sent to GatewayWallet 0x77777777dcc4d5a8b6e418fd04d8997ef11000ee. vet402 decodes the calldata as submitBatch(bytes, bytes), and the first argument as ((address depositor, int256 value)[], bytes32, uint32, address token, address wallet). This layout comes from vet402 decoding public transactions. The token is Arc USDC 0x3600000000000000000000000000000000000000. The negative int256 balance change for the buyer is the amount the batch asked for. If the receipt has an InsufficientBalance event from GatewayWallet for the buyer and USDC, the amount actually taken is available plus withdrawing from that event, and that amount is the debit. The event is InsufficientBalance(address token, address depositor, uint256 value, uint256 available, uint256 withdrawing), with token and depositor indexed. Otherwise the requested amount is the debit. This check assumes the buyer is not also paid in the same batch.

vet402 calls the batch settled when the debit equals the sum. A smaller debit is refuted, because the Gateway API reports a charge that the batch did not take in full. A short buyer balance at settlement is one cause, and GatewayWallet records it in an InsufficientBalance event. A larger debit stays deferred, because the extra amount may be an authorization of the same buyer that this check has not read yet. A debit that cannot be read stays deferred.

The signature time rule matters for an authorization whose Gateway record has not been read yet. vet402 holds back settled while such an authorization could sit in the batch. Signature time is validAfter plus 600 seconds, because the Circle batching client signs with validAfter set to the current second minus 600. vet402 treats an authorization as outside a batch when its signature time is later than the block time minus 60 seconds. The debit comparison still applies, so an authorization that did enter the batch makes the debit larger and the batch stays deferred.

Two public batch transactions on Arc are the worked examples, both for buyer 0xa65c4F0Ab6f61537D22a8E95b940E8bD634394fA. 0x3a78bad401267d8f0914878db7b15e81a27b44637330f619eefef863209d6616 debits that buyer 4.119 USDC for 50 authorizations. The calldata holds a single entry for the buyer, and its value is negative 4119000 in 6-decimal units. 0x6ae70bf1ac146895b1d39342cbea49bb82037768e2b04dbc4109c2105ffc791c debits that buyer 3.214 USDC for 40 authorizations. The calldata holds a single entry for the buyer, and its value is negative 3214000 in 6-decimal units.

On the seller side, one batch mixes this buyer with other buyers. A seller credit in that batch is a total across buyers.

7.What L2 measures

L2 runs only when the paid request in the same purchase answered 2xx other than 202 Accepted. It is a minimal structural check, not a full JSON-Schema validation: the response must parse as JSON and carry the top-level keys the declaration vet402 uses marks as required. vet402 uses the current 402 from the seller first and the catalog schema only when that 402 declares no output. no_declaration: the listing's output schema gave vet402 nothing to check this response against (no schema, a schema with no required keys and no example properties, or example properties with a content type that is not JSON). An output example in the 402 (extensions.bazaar.info.output.example) counts as a declaration. When the catalog also declares an output, the example's top-level keys are required. When it does not, vet402 checks that the response is a JSON object or array, and the record page reads match (no required keys). It is never counted as a failed delivery. match: the response parses and each key the declaration marks as required is present. match (no required keys): the declaration vet402 used marks no key as required, so vet402 checked that the response is a JSON object or array, not which keys it carries. mismatch: the body does not parse as JSON, is JSON but not an object or array, a required key is missing, or the content type is not JSON despite declared required keys. not_checked: the paid request did not answer 2xx other than 202 Accepted, so there was nothing to check. L2 does not verify that the values are correct or that the content is any good — that judgement is L3, and L3 is not built.

Ledger words and decision words. The ledger column l2_schema stores match, mismatch, no_declaration, or not_checked. The decision document and the facts document store conform, mismatch, undeclared, or not_checked. conform is written when the re-read L2 value (l2_reading) of the latest delivery is match. mismatch is written when that re-read value is mismatch. An older mismatch row with no recorded reason is re-read first, and a cut body reads not_checked. In the other cases the decision word is not_checked when the current declaration names output (a required key or an example property), or when the purchase row of the latest delivery recorded a declaration and its re-read value is not_checked, and undeclared when neither holds. The quickstart resource https://api.exa.ai/search is an example of where to read that resource's decision and its purchase rows. Read facts.l2.status on its decision and on its facts document, and l2_schema on its purchase rows.

ERC-8004 matches

The erc8004 object in the state API counts exact address equality between the payTo of a vet402 purchase row and the owner of an agent in vet402's index of the ERC-8004 Identity Registry (or the agent's agentWallet, once that index holds rows). Addresses are compared in lowercase and nothing else is compared. The top-level counts are Base. byChain gives Base, Ethereum and BNB Chain each on its own, with the agents scanned, the agents matched and the block that chain's index has read to. A chain whose index cannot be read yet says unavailable with a reason and no counts. It does not cover similar names or the endpoints in an agent's registration file. A read that fails says unavailable and carries no counts.

8.Fairness commitments

vet402's own endpoints, when listed in the catalog, run through exactly the same measurement pipeline as everyone else's, and vet402's own rows are excluded from the aggregate rates — a verifier that grades itself is not a neutral party in its own numbers. What is not uniform is purchase frequency: the hosts in the priority list in section 6 are bought from more often than the rest of the catalog. The pipeline, the gates and the publication rules are identical for them; only the cadence differs, and the list is named there rather than left implicit. No operator gets a different measurement, a suppressed result, or a softer word for the same finding, and that is what partners are told they cannot buy. These observatory pages publish facts with reason codes and timestamps; they do not publish composite scores, rankings, or evaluative language about any operator. Corrections follow the site-wide corrections policy.

Other measurement services

settlement_30d_real counts indexed settlements whose wash_flag is none. A payer that vet402 pays from stays test and is published as settlement_30d_test. A payer on the list below is stored as wash_flag measurement and published as settlement_30d_measurement. Both leave settlement_30d_real, and both leave the wash_dominated denominator. The denominator is raw minus test minus measurement. A payer is listed when the public page of that service describes the work as measurement or inspection, and a seller report names the address. A generic User-Agent, with no such page, does not put an address on this list. Crosspeel says the wallet that pays for probes is rotated between runs, so this row is the address named in the seller report together with that page. A later wallet is a separate row when a page of the same kind names it. Rows already in the index take the new flag when the backfill for this list runs. The first run moved 1,632 payments in 840 rows of the per-day rollup on 2026-10-24. On the buyer side, the purchases a listed service makes still count in the 30-day history of that service, because they are real payments it made.

Payers listed as other measurement services
NameChainAddressSourceAddedBasis
Crosspeeleip155:84530x8d2a47cbd262e1bc5ee0645c81899f9896957d37https://crosspeel.com/method2026-10-24Seller report in kzmttkc/vet402 issue 43. The method page says the method measures how many agreements hold, and that the wallet pays for probes. On 2026-10-25 vet402 read the 24 settlement transactions the seller listed on Base: each is a USDC transfer from this address to that seller. The method looks for those agreements, measures how many of them hold, and compares the prices of the endpoints that hold them.
Mizaneip155:84530xe3badbd4f38214b9eae528a1a5398f6678f63fb3https://mizan-bazar.higgsfield.app2026-10-24Seller report in kzmttkc/vet402 issue 43. The field station page calls the work a field record of knocks and paid buys, and names measurement errors in that record. On 2026-10-25 vet402 read the 1 settlement transaction the seller listed on Base: a USDC transfer from this address to that seller. Ten measurement errors have been found and fixed so far

9.Reuse and citation

The measurements on these pages, including the aggregate JSON at /api/v1/observatory/state, the daily series at /api/v1/observatory/history, the purchase ledger at /api/v1/observatory/export.csv, the L0 export at /api/v1/observatory/l0/export.csv, the sellers export at /api/v1/sellers/export.csv, and the figures rendered on /impact, are published under CC BY 4.0. Redistribute them, chart them, put them in a paper or a grant memo, commercially or not; the one condition is that the source is named. The code is MIT, in the repository. No permission is needed and none is granted selectively: an operator whose numbers these are may reuse them on exactly the same terms as anyone measuring us.

Every number here moves, so a measurement without its retrieval date is not a measurement. Cite it as: KIZUNA Creation. vet402 observatory. Dataset, retrieved YYYY-MM-DD. https://vet402.com/api/v1/observatory/state The JSON carries license, licenseUrl, retrievedAt and cite in the response body, and the CSV — which has nowhere to put a comment — carries x-vet402-license, x-vet402-retrieved-at, x-vet402-rows, x-vet402-window-days and a Link header pointing at the licence and at this page. A file that ends up on someone else's disk still knows where it came from. /api/v1/observatory/export.csv defaults to the last 90 days; ?days= accepts 1 through 366. /api/v1/observatory/state counts all time. To recount from the CSV, pass days and read x-vet402-window-days on the response.

Dated copies of the purchase ledger, the aggregate JSON and the daily series are kept at github.com/kzmttkc/vet402-observatory-data and archived on Zenodo, so a figure can be checked against the file it came from. The DOI 10.5281/zenodo.24195171 resolves to the latest snapshot. Each snapshot also has its own DOI, listed in the repository's README and CITATION.cff.

Purchase rows are ordered by attempted_at. A published row can change later. A settlement re-read can confirm the transfer or withdraw the claim. Before that re-read, the settlements index can set the status to settle_claimed. The rows it can move are delivered_no_receipt, settle_failed, settle_claimed_unverifiable, and a request_error row that carries an authorization nonce. That status change is written in one database statement and appears at /corrections as a ledger status change (settlement_backfill). It is not counted in the correction count. A request_error row sits outside the file. The index brings it in by setting settle_claimed. If the re-read sends that row back to request_error, the next file leaves it out. The export also omits budget_denied, halted, and in_flight. A later download with a wider ?days= window returns the rows the export still includes from that window, with status as it stands at retrieval. The query accepts a day count from 1 to 366, written without a leading zero. A digit string of 1 to 9 digits that is not a count from 1 to 366 written without a leading zero answers 308 and points at the count inside that range. A longer digit string, and a value that is not a digit string, answers 400. To line the file up with l1.attempts on /api/v1/observatory/state, count rows whose status is settled, settle_failed, delivered_no_receipt, settle_claimed_unverifiable, settle_claimed, or settle_claim_refuted. l1.attempts counts those statuses over the whole ledger, with no day window. The count of those statuses in the file matches it when x-vet402-truncated is false, the window reaches the oldest such row, and no purchase in that count is missing its endpoint row. When the oldest such row is more than 366 days before retrieval, the file does not contain it, so the counts can differ while x-vet402-truncated is false. Rows in other statuses sit outside l1.attempts. Rows are capped at 50,000 per request and the cap is declared in x-vet402-truncated rather than passed off as the whole ledger. If we get a number wrong we correct it in public under the corrections policy — the licence is not conditioned on the number being flattering to us.

Where a piece of evidence came from. Each row in an evidence[] array names its own source, because a decision document can carry observations from two different ledgers. evidence.source=vet402 is our own record: a probe we ran, a purchase we paid for, a schema check on what came back. evidence.source=subgraph is The Graph’s x402 subgraph, read by the caller with their own Graph Gateway API key rather than proxied through us; that row carries the subgraphId, the block.number and deployment it was read at, and the queriedAt timestamp, so a reader can tell live index data apart from a static snapshot. A caller can ask for evidence.source=both and be refused if either source cannot be read — but no single row wears that label, because the two sources count different things and adding them would produce a number that means nothing. Our engine and the subgraph can disagree about the same wallet; the rows stay apart so a reader sees the disagreement instead of an average of it.

Your rule, applied next to ours. A decision can also carry a caller_policy block when the caller names what the 402 asks (amount_usd), a ceiling (max_per_tx_usd) or a floor on our delivered L1 purchases (min_l1_deliveries). The server applies that rule in the same order and the same words as the payOrRefuse SDK: price_above_ceiling when the price is above the ceiling, evidence_unavailable when the decision is degraded, payee_recommendation_block when our recommendation is BLOCK, payee_recommendation_not_allow when it is a WARN and the caller did not waive it (require_vet402_allow defaults to true, as in the SDK; waiving needs a floor in its place), and insufficient_delivery_evidence when our ledger has fewer deliveries than the floor. The block sits beside recommendation and never rewrites it: a WARN stays a WARN, and a floor never lifts a BLOCK. What the server did not check is listed in not_evaluated — always the subgraph floor, because The Graph is read only with the caller’s own key.

10.Definitions, one line each

The sections above define these words in context. This index states each of them once, in a single sentence, so a reader — or a machine reading this page — can take one definition without reconstructing it from a paragraph. The same list is published as structured data on this page and in /llms-full.txt.

Verification levels

L0
L0 is one unpaid HTTP probe that asks whether a catalog-listed x402 endpoint answers HTTP 402 with a challenge consistent with what the catalog declares. It is free and asks for no work from a compliant server, and it runs on a rolling schedule across the catalog; it says nothing about whether the endpoint delivers what it sells.
L1
L1 is a real, budget-capped purchase of that chain's pinned asset (USDC on Base, Arc, Celo, Solana, Stellar, Arbitrum One and Avalanche C-Chain, USDC.e on Tempo, RLUSD on XRPL, USD1 on BNB Chain). It is bought under vet402's own User-Agent, at most once per endpoint per sweep window, and every refusal before a signature is recorded alongside every purchase.
L2
L2 is a minimal structural check asking whether the paid response parses as JSON and carries the top-level keys the seller's own declared output schema marks as required. It runs only when the paid request answered 2xx other than 202 Accepted, and it does not judge whether the values are correct.
L3
L3 would be an opinion on the quality of what was delivered. vet402 has not built it, and nothing on this site presents one.

L0 verdicts

pass
pass means the L0 probe received HTTP 402 and the challenge was consistent with the catalog declaration. It means the endpoint has a standing payment wall — nothing more is claimed.
fail
fail means an L0 probe contradicted the catalog declaration: no 402, a DNS, timeout or connection failure, a challenge with no payable accept, or a price or receiving address that disagrees with the catalog. It is published only after 2 consecutive failing probes, because one sample cannot tell a dead endpoint from a transient network condition — including ours.
unverified
unverified means vet402 does not have grounds to publish either pass or fail yet. It is not a failure and is never counted as one: it is an entry not yet reached by the rolling schedule, an entry whose failing probe has not met the publication gate, an entry recorded as path_template, or the remainder after the three above. The remainder includes a latest reason of rate_limited, tls, request_shape, or unsafe_target. It also includes a latest reason of method_undeclared, for which no request was sent before 2026-09-02, and body_over_cap, for which a 402 came back and the body was not read to the end. A listing that declares no HTTP method, or a method other than GET, POST, HEAD, or DELETE, is measured with GET, and two consecutive failures of that probe can be published as fail.
path_template
path_template means the listed URL still contains an unfilled path parameter, so no request was sent at all. A 4xx from a request we could not have formed correctly is our limitation, not the seller's failure, so the endpoint is recorded unverified and never purchased from; the same principle applies to the request body and the authentication header, where it is recorded as inconclusive.
request_shape
request_shape means an MPP endpoint, or any endpoint probed with an unpaid POST, answered 400 or 422 with no payment challenge: the endpoint validated the input before asking for payment. vet402 does not guess a request body or query — an L0 probe is one request with an empty JSON body at most — so the endpoint has not been measured and the probe is recorded unverified rather than as a failure.
no_mpp_challenge
no_mpp_challenge means an endpoint listed in the MPP directory answered 402 with an x402 envelope but no MPP Payment challenge, so an MPP client cannot pay it; the probe is a failure and the dialect recorded is the x402 envelope that was observed.

L1 settlement statuses

settled
settled means vet402 re-read the transaction on-chain and found a transfer from the vet402 payer, to the payee the catalog declares, for the declared amount, in that chain's pinned settlement asset. The asset is USDC on Base, Arc, Celo, Solana, Stellar, Arbitrum One and Avalanche C-Chain, USDC.e on Tempo, RLUSD from its fixed issuer on XRPL, and USD1 on BNB Chain. On Tempo the directory lists no recipient, so the payee is the one L0 learned from the challenge. Through Circle Gateway on Arc one batch transaction can settle many payments and there is no separate transfer to the payee: Circle's Gateway API names the batch for the purchase's nonce, and vet402 checks the buyer's debit in that batch. How tightly that transfer is tied to the one purchase is published at two strengths, and each settled row is in exactly one. Nonce-bound means the re-read matched the one-time value stored for that purchase, so the transaction is the one for this purchase. That value is the EIP-3009 authorization nonce on Base, Arc, Celo, Arbitrum One, Avalanche C-Chain and BNB Chain, a memo vet402 wrote on Solana, the indexed memo of a TIP-20 TransferWithMemo on Tempo, or the hash of the signed blob on XRPL. Stellar is not in this count. The Stellar re-read checks payer, payee, amount and asset. Amount and payee means the re-read matched payer, payee, amount and asset and did not bind a one-time value. That tier holds the rows that settled before the binding shipped at 2026-09-04T12:00:00Z, and it holds Stellar rows. A transfer of the same amount between the same two wallets would also have matched. The counts are l1.settledNonceBound and l1.settledAmountPayeeOnly in /api/v1/observatory/state. It is a statement about the money, and it is never inferred from the seller's own claim.
delivered
delivered means the attempt is settled and the paid request also answered HTTP 2xx other than 202 Accepted, with a non-empty body. settled is a statement about the money and delivered is a statement about the answer, not its content: vet402 does not check that the content is right, so a 200 with an error in its body can count as delivered. A seller can take the payment and answer 400, and that row is settled and not delivered.
inconclusive
inconclusive means vet402 holds a paid attempt rather than counting it against the seller, because the paid request answered 4xx or ran while vet402's own payer wallet was unfunded. The 4xx case covers a settled payment and a seller that refused with no settlement receipt (a 402 excepted); the unfunded case is a 402 or 5xx between 2026-09-13T00:00Z and 2026-09-15T23:49Z, when vet402's Base payer wallet had run out of USDC. A 4xx says the request was not one the server would accept, and vet402 buys with no API key of the seller's and sends {} as the POST body when the seller declares none, so it cannot rule out that the request was its own to get wrong; the rows stay published with their status and HTTP code, and they do not count toward a BLOCK or against delivered.
settle_claimed
settle_claimed means the seller returned a settlement receipt with a well-formed transaction id and vet402 has not re-read it on-chain yet. It is the seller's assertion, held as an assertion.
settle_claim_refuted
settle_claim_refuted means vet402 re-read the transaction the seller pointed at and that transfer is not there.
settle_claimed_unverifiable
settle_claimed_unverifiable means the transaction id the seller returned is not even well-formed for that chain, so there is nothing to re-read.
delivered_no_receipt
delivered_no_receipt means the seller returned 200 but the response carried no settlement receipt.
settle_failed
settle_failed means no successful paid response came back at all.
l1_not_attempted
l1_not_attempted means vet402 has not signed a paid attempt against this resource, so what it sells is unverified rather than refuted. When the same decision document reports spending_halted true, the missing attempt reflects vet402's own spending halt rather than anything about the seller, and facts.l1.last_attempt_at says when we last looked.
l1_inconclusive
l1_inconclusive means vet402 has signed paid attempts against this resource, but none of them counts either way, so there is no paid response to judge; this is a gap in our measurement, not evidence against the seller. Held attempts are a 4xx we attribute to our own request shape (no API key, or {} as the POST body where the seller declares none) or a 402 or 5xx while our own payer wallet was unfunded. It sits between l1_not_attempted (no paid attempt was signed) and l1_never_delivered (a counted paid attempt existed and nothing was delivered; a WARN, since rules 2026-09-29.3): facts.l1.n_inconclusive carries the count of held attempts. The decision also leaves out attempts where the row shows vet402's side of the fault, attempts awaiting on-chain verification and attempts that took no payment, and, since rules 2026-09-29.3, a failure where no money moved unless /sellers puts it on the seller's side (confirmed on two different UTC days). So l1_inconclusive can appear with n_inconclusive 0; the decision document's l1_basis.n_not_counted carries the full count, and when nothing was delivered the reason codes l1_not_counted_vet402_side, l1_not_counted_held, l1_not_counted_no_charge, l1_not_counted_unproven (no money moved and the row cannot show vet402 was not at fault) and l1_not_counted_unconfirmed (no money moved, on the seller's side on one day only) say which were left out. Where no money moved, the decision counts only what /sellers puts on the seller's side, and then only toward a WARN. Where money moved, it is cautious for the payer: a paid attempt that took payment and did not deliver counts even when /sellers leaves it not sorted, and two or more of them on the seller's side since the last delivery (l1_paid_not_delivered) are the only L1 reason for a BLOCK; not sorted ones stay a WARN.

L2 schema results

match
match means the paid response parses as JSON and every key the declaration vet402 uses marks as required is present.
mismatch
mismatch means the paid response has a non-JSON content type despite declared required keys, or the paid response read in full is not JSON at all, is JSON that is not closed, is JSON but not an object or array, or lacks a key the seller's declared output schema marks as required. Since 2026-09-29 the row's raw_response_meta.l2.reason says which (not_json_body, unparseable, not_object, missing_keys or not_json_content_type), and missing keys are listed only when the body was read as JSON. Until 2026-09-29 vet402 read only the first 16,000 bytes of a paid response, so a longer JSON response could not be parsed and was recorded as a mismatch listing every declared required key as missing; the decision does not count such an older row as a mismatch when a key recorded as missing shows at the top level of the stored start of the body, and does not count an older row with no recorded reason as a mismatch when the listing declares no output now (it reads no_declaration), or when no missing key was recorded and its JSON body ran past the stored start (it reads not_checked). In the decision, a mismatch with recorded missing keys is a BLOCK, and one without is a WARN (l2_mismatch_unexplained).
no_declaration
no_declaration means the listing's output schema gave vet402 nothing to check the response against: no schema, a schema with no required keys and no example properties, or example properties and a response whose content type is not JSON. An output example in the 402 (extensions.bazaar.info.output.example) counts as a declaration. When the catalog also declares an output, the example's top-level keys are required. When it does not, vet402 checks that the response is a JSON object or array, and the record page reads match (no required keys). It is never counted as a failed delivery.
not_checked
not_checked means there was no complete response body to check, so it is never counted as a failure. The paid request did not answer 2xx other than 202 Accepted, or (since 2026-09-29) the body was longer than the 256 KiB vet402 reads (raw_response_meta.l2.reason body_over_cap, with bodyTruncated true), or it stopped after some bytes arrived (body_timeout or body_read_error). For an older row read at 16,000 bytes, the decision treats a mismatch as not checked when a key recorded as missing shows at the top level of the stored start of the body, or when no missing key was recorded and its JSON body ran past the stored start, since the row cannot show whether the body was cut or broken. A complete body that is not valid JSON is a mismatch, not not_checked.

Catalog events

active
active means vet402 has not closed this record. A Bazaar record is not closed at present, so it stays active after it leaves the catalog. The count seen in the latest full read of the Bazaar is published separately as latestSnapshot.fetchedCount, and the record page shows when each endpoint was last seen.
delisted
delisted means an endpoint present on an earlier day is absent from a complete fetch of the public discovery catalog. On any day our own fetch is incomplete, no delisting judgements are made at all — a gap in our data must never read as a disappearance in yours.
relisted
relisted means a previously delisted endpoint reappeared in a complete catalog fetch.
settle_drop
settle_drop means the catalog's own reported 30-day call count for an endpoint fell sharply from a meaningful base. It is a factual observation of the catalog's telemetry, not a judgement about the seller.

Evidence sources

evidence.source=vet402
evidence.source=vet402 means the evidence row was observed in vet402's own L0–L2 record: an unpaid probe, a real purchase vet402 made, or the schema check on what that purchase returned. It carries our purchase id and the public receipt URL, and it asks the reader to trust our measurement.
evidence.source=subgraph
evidence.source=subgraph means the evidence row was read from The Graph's x402 subgraph with the caller's own Graph Gateway API key, not proxied through vet402. Such a row carries subgraphId, block.number, deployment and queriedAt, which is what lets a reader tell live index data apart from a static snapshot.
evidence.source=both
evidence.source=both means the caller asked payOrRefuse to read the vet402 ledger and The Graph subgraph before deciding, and to refuse if either could not be read. It is a request about which sources to consult, not a label a row can wear: a row from "both" would be two ledgers merged into one number.

Caller policy words

price_above_ceiling
price_above_ceiling means the amount the 402 asks (amount_usd) is above the ceiling the caller named (max_per_tx_usd, default 1 USD), so the caller's own policy refuses before anything else is looked at. It is the first gate in the payOrRefuse SDK and in the caller_policy block of /decision, and it says nothing about the seller.
insufficient_delivery_evidence
insufficient_delivery_evidence means vet402's own ledger of delivered L1 purchases for this resource (facts.l1.n_delivered) is below the floor the caller named (min_l1_deliveries). It is a shortfall against the caller's floor, not a verdict on the seller; the same word is used by the payOrRefuse SDK and by the caller_policy block of /decision.
payee_recommendation_block
payee_recommendation_block means vet402's recommendation for the resource or payee is BLOCK, and a caller's policy never lifts that. BLOCK is an operator-level refusal (two failed probes in a row, two paid failures on the seller's side, a mismatch with named missing keys, wash-dominated volume, a block list), not an opinion a floor can outweigh. WARN is an opinion and can be waived by a declared floor; BLOCK cannot.
payee_recommendation_not_allow
payee_recommendation_not_allow means vet402's recommendation for the resource or payee is something other than ALLOW (a WARN) and the caller's policy requires ALLOW, which is the default in the payOrRefuse SDK (requireVet402Allow true) and in the caller_policy block of /decision (require_vet402_allow=true). A caller may waive it with require_vet402_allow=false only by naming a floor in its place (min_l1_deliveries of at least 1); without one the request is refused as invalid_policy, because waiving the verdict must not leave nothing to judge.
evidence_unavailable
evidence_unavailable means the decision could not be read or was marked degraded, so there is no measurement to apply a policy to, and the gate fails closed. Not measuring is not the same as not finding a problem; a caller's floor does not fill in a measurement that was never made.