Skip to content

index: back off failed manifest probes, read /agents.json, surface the discovery gap - #657

Merged
MikeyPetrillo merged 3 commits into
mainfrom
claude/sweet-brown-i99jl3
Aug 2, 2026
Merged

index: back off failed manifest probes, read /agents.json, surface the discovery gap#657
MikeyPetrillo merged 3 commits into
mainfrom
claude/sweet-brown-i99jl3

Conversation

@MikeyPetrillo

@MikeyPetrillo MikeyPetrillo commented Aug 2, 2026

Copy link
Copy Markdown
Owner

What

Our crawler re-probed /.well-known/x402 on 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.json that 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.json and /llms.txt join the fallback chain. agents.json sits behind the same payment gate as openapi.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.txt is 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. discoveryPath is recorded per crawl and rendered on the seller card as one line: which surface answered, and which one spec-following buyers 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.

The manifest's own catalogue is now read. Chasing the seller-facing note exposed a larger gap: we parsed /.well-known/x402 for 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) and scripts/test-discovery-note.js (35), both wired into CI
  • The llms.txt assertions 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 inputs
  • Checked against a real document, not only fixtures: 629 entries read from our own llms.txt, zero junk routes
  • Every assertion mutation-tested: removing the spec-path exemption, collapsing the two fallback notes, silencing the registry-only case, dropping clear-on-success, removing each of the three llms.txt guards, and removing each of the four manifest-parser guards all kill assertions
  • Full sweeps clean: 210 strict / 296 lenient tools, 526/526 through MCP, plus the 24-script offline battery
  • Parser checked against the live manifests of all six sampled sellers, not only fixtures

Closes #645

MikeyPetrillo and others added 3 commits August 2, 2026 12:24
…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
MikeyPetrillo temporarily deployed to agent402 / production August 2, 2026 16:51 — with GitHub Actions Inactive
@railway-app
railway-app Bot temporarily deployed to agent402 / production August 2, 2026 16:51 Inactive
@MikeyPetrillo
MikeyPetrillo marked this pull request as ready for review August 2, 2026 16:54
@MikeyPetrillo
MikeyPetrillo merged commit 8fa84cb into main Aug 2, 2026
32 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Crawler feedback: 686 requests to /.well-known/x402 that were 404 - now live, and a suggestion for the index

1 participant