Skip to main content
Independent MeasurementMethodology: L0, L1, L2Version 3 · 2026-09-04
vet402Back to the registerFacts only · no composite scores

What these measurements mean

In 60 seconds

L0 asks whether the payment wall answers. L1 asks whether a real purchase settles. 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 (and equivalent public discovery surfaces). That catalog is the measured population, not the whole x402 economy: discovery surfaces vet402 does not read are outside what we probe 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.

1.What L0 measures

An L0 probe sends one request to a catalog-listed endpoint using the HTTP method the catalog entry itself declares, with no payment attached. 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 — we do not have grounds to publish either of the other two yet. That covers an entry that does not declare enough to measure (no declared method: probing with a guessed method reports false deaths), an entry the rolling probe schedule has not reached, an entry whose failing probe has not met the publication gate in section 3, and an entry we could not reach for a reason of our own (rate limiting, a TLS failure on our side). unverified is not a failure and is never counted as one.

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

  • 4,352 not yet probed: the rolling schedule has not reached them
  • 3,550 with a failing probe that has not met the publication gate in section 3
  • 898 we could not reach for a reason of our own (rate limiting, TLS, or an MPP wall that checks the request before asking for payment: request_shape)
  • 12 whose listed URL still contains an unfilled path parameter (path_template, below)
  • 0 that declare no HTTP method

Those five sum to 8,812, 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 counts above contradict that, so the counts are printed instead of a sentence that can go stale between readings.

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. A 5xx is not treated this way: a server fault is not something our 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 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 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 no payment to have landed afterwards. 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 “under re-check” while we re-check them, and a seller page carries the re-check 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 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 UTC, when we did not add the query the listing declares; or our own limits and errors. “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-09-29.4 (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 the same kind of failure 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 both kinds together: held, a row with a held_reason in the ledger export, and a receipt awaiting on-chain verification that brought a result, whose held_reason is empty). 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 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 every signed attempt is older than the window (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, 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. 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 the listing declares no output schema. 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 and Arc, 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 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 public discovery catalog is re-fetched daily, and every fetch is compared with the catalog's own reported total for that day — both counts are published as latestSnapshot (fetchedCount and totalCount), so a short day is visible rather than smoothed over. 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. On any day our own fetch is incomplete (fetched count below the catalog's reported total), no delisting judgements are made — a gap in our data must never read as a disappearance in yours. 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). 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), the canonical USDC asset for that network, and a price that matches what the catalog declared when we chose the target. Any deviation — a different asset, a different chain, a higher price — is recorded as a refusal, never paid. A hard per-purchase ceiling ($1.00) 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. Once we sign, the spend is recorded whether or not the seller delivers — a signed EIP-3009 authorization is live money the moment it exists.

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 ($1.00 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. That is the harder test, not the easier one. A seller who knows exactly who is watching and still takes the payment without delivering has been measured under the best conditions it will ever get; the record below is what happened anyway. 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.

settled — vet402 re-read the transaction on-chain and found the exact USDC transfer it paid for: from our payer, to the catalog-declared payee, for the declared amount, in the canonical USDC contract. 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 each purchase carries a one-time value we generate ourselves — the EIP-3009 authorization nonce on Base and Arc, our own memo on Solana, the memo of a TIP-20 transfer on Tempo, the hash of the signed blob on XRPL — and the re-read binds the transaction to that value. We publish those rows as nonce-bound: the transaction is the one that paid for this purchase. Rows that settled before that timestamp were matched on amount, payee, asset and chain, with nothing tying the transaction to the purchase it was offered for. We publish those as amount + payee. The gap is not hypothetical: a seller holding several catalog entries at the same price and the same payee could have answered with a transfer it had received earlier and passed that check. At the current reading, 1,614 of 5,353 settled rows are amount + payee and 3,739 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 or vet402_index, the endpoint page marks the row, and the count is live as l1.settledLateLinked. At the current reading, 493 of 5,353 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 last column of the ledger export, 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 goods: the attempt is settled and the paid request answered 2xx. 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 the response arriving. Until 2026-09-04 only 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 we hold differs by chain — on Base and Arc the EIP-3009 authorization nonce, on Solana a memo we wrote ourselves, on Tempo the indexed memo of a TIP-20 TransferWithMemo, on XRPL the hash of the blob we signed (which is the transaction hash itself). A purchase on a chain we have built no re-reader for stays at settle_claimed rather than being promoted on evidence we do not have. When our own RPC cannot answer, or reports a different cluster than the purchase declared, the row stays unverified rather than being called refuted — an instrument we could not read is not a finding about the seller.

7.What L2 measures

L2 runs only when the paid request in the same purchase returned 200. 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 catalog's own declared output schema marks as required. no_declaration — the catalog entry does not declare an output schema; never counted as a failure. match — the response parses and every declared required key is present. mismatch — the body does not parse as JSON, a required key is missing, or the content type is not JSON despite a declaration. not_checked — the paid request did not return 200, 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.

One vocabulary, and its older summary form. The four words above are the canonical set: they are what the ledger column l2_schema stores and what every API returns. Short summaries of the levels have historically used a three-word form (conform / mismatch / undeclared), and a reader comparing the two surfaces could not tell whether they described the same measurement. They do: conform is match, mismatch is mismatch, and undeclared is no_declaration. The summary form has no word for not_checked, because a level summary is not reporting rows that were never checked. Where the two disagree, the four-word set wins.

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.

9.Reuse and citation

The measurements on these pages — the aggregate JSON at /api/v1/observatory/state, the daily series at /api/v1/observatory/history, and the purchase ledger at /api/v1/observatory/export.csv — 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.

The ledger is append-only and ordered by attempted_at, so a row that was published does not change afterwards; a re-download with a wider ?days= window returns the same earlier rows plus older ones. 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 side-effect free, so it runs across the whole catalog; it says nothing about whether the endpoint delivers what it sells.
L1
L1 is a real, budget-capped USDC purchase from the endpoint that asks whether the payment settles on-chain and a response comes back. 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 returned 200, 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 covers entries not yet reached by the rolling schedule, entries whose failing probe has not met the publication gate, entries that declare too little to measure, and entries we could not reach for a reason of our own.
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 our payer, to the catalog-declared payee, for the declared amount, in that chain's canonical settlement asset (USDC on Base, Arc and Solana; USDC.e on Tempo; RLUSD from its fixed issuer on XRPL). 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, where the transaction also carries the one-time value vet402 generated for that purchase (the EIP-3009 authorization nonce on Base and Arc, our own memo on Solana, the indexed memo of a TIP-20 TransferWithMemo on Tempo, the hash of the signed blob on XRPL), so it is that purchase's transfer; and amount-and-payee only, where payer, payee, amount and asset matched but no such value was checked — the rows that settled before that binding shipped at 2026-09-04T12:00:00Z — so 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 2xx. settled is a statement about the money and delivered is a statement about the goods: 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 of them since the last delivery (l1_paid_not_delivered) are the only L1 reason for a BLOCK.

L2 schema results

match
match means the paid response parses as JSON and every key the seller's declared output schema marks as required is present.
mismatch
mismatch means the complete paid response is not JSON at all, is JSON that is not closed, is JSON but not an object, lacks a key the seller's declared output schema marks as required, or has a non-JSON content type despite a declaration. 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 keeps an older row whose listing declares no output as a mismatch with no missing keys unless its stored body is complete and valid JSON. 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 catalog entry declares no output schema, or one with no required keys and no example properties, so there is nothing to check against. It is never counted as a failure.
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 return 200, 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 only when there is evidence the body was cut: a key recorded as missing shows at the top level of the stored start of the body. A complete body that is not valid JSON is a mismatch, not not_checked.

Catalog events

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 USDC purchase we 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 (a failing probe, a schema mismatch, wash-dominated volume, a global 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.