index: back off failed manifest probes, read /agents.json, surface the discovery gap - #657
Merged
Merged
Conversation
…son, show the seller the gap Our crawler re-probed /.well-known/x402 on a 5-minute interval regardless of outcome, so an origin that had 404'd hundreds of times in a row was asked again exactly as often as a healthy one. One origin took 686 identical requests in a week and answered 404 to every one, while serving a complete 17-endpoint catalogue at /agents.json that we never asked for. We spent their bandwidth to re-learn a fact we already had, and listed them thinly for it. Three changes, one per half of that: - Backoff on the manifest probe: three consecutive failures are free (a transient blip must not cost a seller listing freshness), then 30m, 2h, 6h and no further. Any success clears it immediately, so fixing a manifest is picked up on the next crawl rather than punished. Scoped to the PROBE, not the origin - gating the whole origin would have cost us the catalogue we can still reach in order to save them one request. - /agents.json joins the fallback chain, behind the same payment gate as openapi.json: accepted only if the registry already proves the origin settles, or the document itself carries a payment signal. The spec naming one path does not make the wild uniform, and an index that reads only one path indexes only the sellers who read the same page we did. - discoveryPath is recorded per crawl and rendered on the seller card as one line: which surface answered, and which one buyers following the spec will not find. `source` cannot express this - it collapses both fallbacks to "openapi-fallback", which would send a seller to fix the wrong file. Null for sellers already on the spec path, so it stays a signal rather than decoration. Lives in a leaf module because x402-index.js already imports market-page.js and the reverse edge takes the marketplace down. discoveryPath rides beside originResponded on all three accessors: this file has twice shipped a field present on two of three, inert on whichever surface happened to render. scripts/test-crawl-backoff.js (11) and scripts/test-discovery-note.js (11), both in CI. Every assertion mutation-tested: removing the spec-path exemption, collapsing the two fallback notes, silencing the registry-only case, and dropping the clear-on-success each kill assertions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The second half of the fallback request behind the previous commit. /llms.txt is the riskier of the two surfaces: agents.json is structured, llms.txt is prose, and a greedy markdown scrape would turn marketing copy into phantom "tools" nobody can buy. A thin listing is recoverable; a fabricated one is not. So normaliseLlmsTxtTools reads exactly one shape, the priced link-list entry the convention is built on, and requires all of: a SAME-ORIGIN absolute URL (a link to someone else's docs is a reference, never this seller's tool), an explicit price on the line (which is what separates a buyable endpoint from an about page), and a route that survives the same non-tool path filter openapi uses. A bare URL in a sentence is not read at all. Everything less structured is left unread on purpose. Tried last, only when the manifest, openapi.json and agents.json have all yielded nothing, and it sets discoveryPath so the seller card names it. 12 assertions added to scripts/test-discovery-note.js (23 total), weighted toward the refusals rather than the acceptances: cross-origin link, unpriced link, the discovery path itself, a static asset, a duplicate route, a bare URL in prose, and empty/null/unparseable-origin inputs. Removing the same-origin check, the price requirement, or the path filter each kills assertions. Checked against a real document rather than only fixtures: 629 entries read from our own llms.txt, zero junk routes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…eir own manifest
We parsed /.well-known/x402 for identity and payment and threw its tool list
away. The tool list came from the seller's openapi.json plus registry rows, so
a seller whose manifest enumerated every endpoint, with names, prices and
summaries, could still be listed thinly from a third-party row.
Measured rather than assumed: of 44 reachable manifest-sourced sellers
sampled, 5 advertise more entries than we list. Live results from the parser
against those manifests: 16 -> 17, 1 -> 14, 11 -> 12, 3 -> 5.
There is no single shape in the wild, so the parser is tolerant about FORM and
strict about ATTRIBUTION. Four dialects, all taken from real manifests:
objects with {name, endpoint, price_usd, summary}; bare same-origin URLs;
"POST /path" verb-plus-path strings; and {resource|url|route|path} objects.
SAME-ORIGIN ONLY is the load-bearing rule. Some manifests list other origins.
Attributing those to the publisher would put another seller's tools under this
seller's payTo, which is a payment error rather than a cosmetic one, and would
double-count one catalogue across two hosts. Two sampled origins parse to zero
for exactly this reason and that is the correct answer for both: one is an
aggregator pointing outward, the other republishes another host's catalogue
verbatim. Those origins are crawled on their own account.
Dedupe keys on method + route INCLUDING the query string. Two entries that
differ only by ?product= are two products; collapsing them to the pathname is
how one 17-tool seller read as 16.
Precedence is openapi > manifest > registry: the manifest is the seller's own
statement about themselves so it outranks a third-party row, and the openapi
is more structured still so it outranks both.
12 assertions added (35 total in scripts/test-discovery-note.js). Removing the
same-origin check, the query-string distinction, the non-tool path filter, or
the catalogue read itself each kills assertions.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
MikeyPetrillo
temporarily deployed
to
agent402 / production
August 2, 2026 16:51 — with
GitHub Actions
Inactive
MikeyPetrillo
marked this pull request as ready for review
August 2, 2026 16:54
This was referenced Aug 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Our crawler re-probed
/.well-known/x402on a fixed 5-minute interval regardless of outcome. An origin that had 404'd hundreds of times in a row was asked again exactly as often as a healthy one. One origin took 686 identical requests in a week, answered 404 to every one, and served a complete 17-endpoint catalogue at/agents.jsonthat we never asked for.Both halves are our defect: we spent their bandwidth to re-learn a fact we already had, and we listed them thinly for it.
Changes
Backoff on the manifest probe. Three consecutive failures are free (a transient blip must not cost a seller listing freshness), then 30m / 2h / 6h, capped. Any success clears it immediately, so fixing a manifest is picked up on the next crawl rather than punished. Scoped to the probe, not the origin: gating the whole origin would cost us the catalogue we can still reach in order to save them one request.
/agents.jsonand/llms.txtjoin the fallback chain.agents.jsonsits behind the same payment gate asopenapi.json(the registry proves the origin settles, or the document itself carries a payment signal). The spec naming one path does not make the wild uniform; an index that reads only one path indexes only the sellers who read the same page we did.llms.txtis the riskier of the two, because it is prose rather than structure. A greedy markdown scrape would turn marketing copy into phantom "tools" nobody can buy, and a fabricated listing is worse than a thin one. So the parser reads exactly one shape, the priced link-list entry, and requires a same-origin absolute URL, an explicit price on the line, and a route that survives the same non-tool path filter openapi uses. A bare URL in a sentence is not read at all.The gap is now visible to the seller.
discoveryPathis recorded per crawl and rendered on the seller card as one line: which surface answered, and which one spec-following buyers will not find.sourcecannot express this, it collapses both fallbacks toopenapi-fallback, which would send a seller to fix the wrong file. Null for sellers already on the spec path, so it stays a signal rather than decoration.The manifest's own catalogue is now read. Chasing the seller-facing note exposed a larger gap: we parsed
/.well-known/x402for identity and payment and threw its tool list away, so a seller enumerating every endpoint in their manifest could still be listed thinly from a third-party registry row.Measured, not assumed: of 44 reachable manifest-sourced sellers sampled, 5 advertise more entries than we list. Live parser results against those manifests: 16 to 17, 1 to 14, 11 to 12, 3 to 5.
Four dialects handled, all taken from real manifests:
{name, endpoint, price_usd, summary}objects, bare same-origin URLs,"POST /path"strings, and{resource|url|route|path}objects. Same-origin only is load-bearing: some manifests list other origins, and attributing those to the publisher would put another seller's tools under this seller's payTo and double-count one catalogue across two hosts. Two sampled origins parse to zero for that reason, which is correct for both.Dedupe keys on method + route including the query string. Two entries differing only by
?product=are two products; collapsing them to the pathname is how a 17-tool seller read as 16.Precedence is openapi > manifest > registry.
Verification
scripts/test-crawl-backoff.js(11) andscripts/test-discovery-note.js(35), both wired into CIllms.txtassertions are weighted toward refusals rather than acceptances: cross-origin link, unpriced link, the discovery path itself, a static asset, a duplicate route, a bare URL in prose, and empty/null/unparseable-origin inputsllms.txt, zero junk routesllms.txtguards, and removing each of the four manifest-parser guards all kill assertionsCloses #645