The x402 ecosystem has at least four discovery directories that an AI agent might query before invoking a service. They all parse the same 402 challenge response, and they all parse it differently. This is the recipe that gets you into all four.
When an AI agent decides to invoke a paid service via x402, it usually checks one or more discovery indexes first — to find the service, inspect its pricing, and confirm the schema. The indexes today are:
api.cdp.coinbase.com/platform/v2/x402/discovery/resources.All four parse the same 402 challenge response that your service emits when an agent calls without payment. They all parse it differently. Getting indexed in all four required a response shape that none of the docs cover end-to-end. Here's what actually works.
x402scan's parser wants the JSON Schema for your endpoint inside outputSchema.input.schema. If you put it at the top level as inputSchema, registration fails with:
{ "error": { "type": "parseErrors", "parseErrors": ["Missing input schema"] } }
The error message is misleading — inputSchema exists at the top level, but the validator only looks inside outputSchema. Move it.
There's also a second gotcha: registerFromOrigin fetches /.well-known/x402 without the extension before it tries /.well-known/x402.json. If you only serve the .json variant, registerFromOrigin fails with noDiscovery — while checkDiscovery on the same origin succeeds. Most services ship one or the other; you need both.
Minimum schema for the 402 challenge body:
{
"outputSchema": {
"input": {
"type": "http",
"method": "POST",
"discoverable": true,
"bodyType": "json",
"schema": {
"type": "object",
"properties": { /* your body fields */ },
"required": ["..."]
}
},
"output": {
"type": "json",
"example": { /* your response shape */ }
}
}
}
Plus serve the simple form at /.well-known/x402 (no extension):
{ "version": 1, "resources": ["POST /your-endpoint", "GET /your-other-endpoint"] }
CDP Bazaar reads from your 402 challenge during settlement, and the v1 facilitator extracts discovery metadata to populate the catalog item. The fields it reads aren't quite the same ones x402scan reads:
outputSchema.input.body for example values — not a JSON Schema. The actual values an agent would POST.outputSchema.output as the catalog item's example field itself. So if you wrap it as { "type": "json", "example": {...} }, you end up with a double-wrapped example.example in the listing.
Translation: ship both shapes simultaneously. Keep outputSchema.input.schema for x402scan. Add outputSchema.input.body with example values for CDP. Drop the { type, example } wrapping on outputSchema.output — make it the bare example object.
Also: Bazaar indexing only fires after a full settlement, not on verify alone. You need an actual paid call — or a low-priced self-test against your own wallet — for the catalog to pick you up. The CDP facilitator returns an EXTENSION-RESPONSES header on settle responses; base64-decode it and you should see:
{"bazaar": {"status": "processing"}}
That's CDP confirming acceptance. Catalog visibility lags by minutes-to-hours after that.
mcp-publisher is the CLI that ships your npm package's metadata into MCP Registry. It verifies namespace ownership by querying GitHub for org membership. If you're a member of the org but your membership visibility is set to private (the default in many orgs), mcp-publisher publish fails with a 403 and a misleading error:
"You do not have permission to publish this server. You have permission to publish:
io.github.<your-username>/*. Attempting to publish: io.github.<org>/<package>."
The fix isn't in the publisher's error text:
github.com/orgs/<your-org>/peoplemcp-publisher logout && mcp-publisher login to refresh the JWT (the visibility is baked in at login time)mcp-publisher publishPulseMCP polls MCP Registry and re-publishes every entry in their own UI. There's nothing to do beyond getting MCP Registry right; PulseMCP picks you up within a day.
After applying all four fixes, you can confirm each surface programmatically:
# MCP Registry
curl https://registry.modelcontextprotocol.io/v0/servers?search=<your-name>
# x402scan checkDiscovery
curl -X POST https://www.x402scan.com/api/trpc/public.resources.checkDiscovery \
-H "Content-Type: application/json" \
-d '{"json":{"origin":"https://your-service.com"}}'
# x402scan registerFromOrigin
curl -X POST https://www.x402scan.com/api/trpc/public.resources.registerFromOrigin \
-H "Content-Type: application/json" \
-d '{"json":{"origin":"https://your-service.com"}}'
# CDP Bazaar paginated catalog
curl "https://api.cdp.coinbase.com/platform/v2/x402/discovery/resources?limit=100"
# agentic.market service search
curl "https://api.agentic.market/v1/services?q=<your-name>"
If any return empty, work back through the corresponding section.
This is what week one of building on x402 actually looked like. The protocol is real, the rails work, but the discovery layer is uneven and the failure modes aren't documented. Total time once you know the recipe: maybe thirty minutes of code changes plus async catalog refresh time. Without the recipe: about a day, in my case.
The reference implementation is worker.js at github.com/archonics/mcp-audit — search for buildPaymentRequirements for the 402 challenge shape that ended up being accepted by all four directories. The full Archonics methodology treats this as Dimension Zero, the prerequisite all the others ride on.
If you've shipped on x402 and hit different walls, I'd love to hear about them.