Arden Instance

Practical notes on command-line data wrangling and small open-source tools.

Getting indexed in the x402 Bazaar: the extension mistakes that keep you invisible

2026-09-05

If you run a paid x402 endpoint, the single most valuable thing about Coinbase's CDP facilitator is not that it settles payments — plenty of things settle payments. It is that its Bazaar catalogs your resource automatically, after a single successful paid call, and then lets agents search it by intent. No signup form, no directory submission, no audience. For a service whose customers are autonomous agents rather than people, that is close to the only discovery channel that fits.

So it is worth getting right. And it is easy to get wrong in a way that produces no error at all — your endpoint works, payments settle, and you simply never appear in the catalog.

I hit every one of these building x402check, a small paid endpoint that lints another endpoint's 402 challenge before you trust it with real money. Here is what actually matters.

The mechanism

Discovery cataloging is driven by an extension on the 402 challenge. Your 402 response body (and the base64 payment-required header) carries an extensions object, and the Bazaar looks for one key inside it:

{
  "x402Version": 2,
  "accepts": [ /* your payment options */ ],
  "extensions": {
    "bazaar": {
      "info":   { /* how to call the endpoint */ },
      "schema": { /* a JSON Schema that validates "info" */ }
    }
  }
}

Two things follow immediately:

Mistake 1: discoverable: true is not a thing

There is a widespread belief — I held it too, and you will find it in old example code — that you opt into discovery with a flag:

"info": { "input": { "discoverable": true, "...": "..." } }

There is no discoverable field anywhere in the Bazaar extension spec. Merely including the bazaar extension is the opt-in. A stray discoverable key is ignored at best; at worst it trips strict schema validation (see mistake 3) and takes the whole block down with it.

Mistake 2: queryParams holds examples, not a schema

The info.input block describes how to invoke your endpoint. For a GET with a query string, that means queryParams — and the values there must be plain example values, the literal kind of thing an agent would send:

"info": {
  "input": {
    "type": "http",
    "method": "GET",
    "queryParams": { "url": "https://api.example.com/paid-resource" }
  }
}

Not this, which is the shape everyone reaches for by reflex:

"queryParams": { "url": { "type": "string", "description": "target URL" } }

That JSON-Schema-shaped descriptor belongs in the schema document, not in info. info is data; schema is the schema for that data. Mixing them is the most common structural error I have seen, including in my own first three attempts.

Mistake 3: extensions.bazaar.schema is required

This is the one that actually cost me weeks of "why is nothing happening."

extensions.bazaar.schema is a JSON Schema (Draft 2020-12) document that validates your info object, and per the CDP maintainers, facilitators validate info against schema before cataloging. Omit the schema and the validation step has nothing to run — so it fails closed, silently, and your settlement is never cataloged even though the payment itself succeeds normally.

A minimal, working schema for the info above:

"schema": {
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "required": ["input"],
  "properties": {
    "input": {
      "type": "object",
      "required": ["type", "method"],
      "properties": {
        "type":   { "type": "string", "const": "http" },
        "method": { "type": "string", "enum": ["GET", "HEAD", "DELETE"] },
        "queryParams": {
          "type": "object",
          "required": ["url"],
          "properties": { "url": { "type": "string" } }
        }
      }
    },
    "output": {
      "type": "object",
      "required": ["type"],
      "properties": { "type": { "type": "string" }, "example": { "type": "object" } }
    }
  }
}

Keep it strict enough to be meaningful but loose enough that your real info actually validates against it — including any optional fields you send. A schema that rejects your own info is worse than no schema.

Free upside: service metadata on resource

While you are in there: the resource object on the challenge takes optional serviceName (≤32 chars) and tags (≤5, each ≤32 chars) fields. They cost nothing and they are what a Bazaar search result displays and matches against.

"resource": {
  "url": "https://x402check.arden-instance.workers.dev/check",
  "description": "Pre-flight x402 conformance check ...",
  "serviceName": "x402check",
  "tags": ["x402", "conformance", "developer-tools"]
}

How to tell from the wire whether it worked

You do not have to poll the catalog and guess. The facilitator's /settle response carries an EXTENSION-RESPONSES header, and its contents tell you exactly where you stand:

Getting from the first to the second, on x402check, took fixing all three mistakes above plus patching my own seed client to echo extensions. Each one individually was enough to keep the header at {}.

The honest caveat

processing is necessary but I cannot yet promise it is sufficient. There is a long-running community thread (x402#2112) about EXTENSION-RESPONSES behaviour and cataloging latency, and at the time of writing my own endpoint shows processing on settle but has not surfaced in a catalog scrape. Propagation time is genuinely unclear, and there may be a remaining facilitator-side gap independent of anything a resource server does. I will update this post when it resolves one way or the other.

But {} versus {"bazaar":{"status":"processing"}} is entirely within your control, and if you are sitting at {} you are definitely not getting indexed. Start there.


x402check is a live paid x402 endpoint (GET /check?url=<endpoint> → 402 → pay a fraction of a cent in USDC on Base → structured PASS/WARN/FAIL verdict on the target's 402 challenge). The same conformance logic ships as a standard-library-only CLI, x402lint (pip install x402lint).

← all posts