> ## Documentation Index
> Fetch the complete documentation index at: https://docs.payai.network/llms.txt
> Use this file to discover all available pages before exploring further.

# Bazaar Discovery

> How resources get listed, refreshed, and found in the PayAI Bazaar

The Bazaar is the PayAI facilitator's public catalog of x402 resources. AI agents and clients browse it via [`/discovery/resources`](#get-/discovery/resources) to find paid APIs and MCP tools they can use.

Listing is automatic: there is no registration form, account, or manual submission. The facilitator indexes your resource from the payments it processes — if they carry your discovery declaration.

## How listing works

Three parties cooperate to get a resource listed:

1. **Your server declares.** Your 402 response includes a bazaar discovery declaration describing the endpoint (method, input/output shape, service metadata).
2. **The buyer's client echoes.** The client copies your declaration from the 402 response into the payment payload it sends to the facilitator. This is required client behavior in x402 v2 — the client must include at least the extension info it received.
3. **The facilitator catalogs.** On `/verify` and `/settle`, the facilitator extracts the declaration from the payment payload and upserts your catalog entry asynchronously.
4. **The catalog worker admits the resource.** Before a resource is listed for the first time, the worker checks the URL against the host policy below and sends one read-only probe (`HEAD`, then `GET` on `404`/`405`; the probe never sends `POST`, `PUT`, `PATCH` or `DELETE`). A resource is listed when either proof holds: **the indexing payment was a settlement** (a buyer only pays after receiving your `402`), or the read-only probe itself answers **`402 Payment Required`**. A verify-only first listing of a `POST`/`PUT`/`PATCH`/`DELETE` or MCP resource (or one with no declared method) is deferred with `resource_needs_settlement` until its first settled payment; a `GET` resource answering something other than `402` unpaid is refused as `resource_not_x402`. Unreachable hosts, `GET` paths answering `404`/`405`/`410`, and hosts resolving to private address space are refused regardless. Existing entries are refreshed without a probe.

<Note>
  Step 2 is where most "my resource never appears" reports come from. If the buyer's client drops the `extensions` object when building its payment payload, the facilitator never sees your declaration — no matter how many payments settle successfully. Use the [`EXTENSION-RESPONSES` header](#reading-extension-responses) to tell the cases apart.
</Note>

Since 2026-07-29, cataloging runs on `/verify` as well as `/settle`. Verification moves no funds, so you can list or refresh a resource without a settled payment.

## Declaring your resource

**x402 v2** servers declare via the `bazaar` extension on the 402 response. The `@x402/extensions` package builds a valid declaration for you (`declareDiscoveryExtension`), including the JSON schema the facilitator validates against. The declaration lives in the 402 body's top-level `extensions` object:

```json theme={null}
{
  "x402Version": 2,
  "resource": {
    "url": "https://api.example.com/convert",
    "description": "Convert PDFs to markdown",
    "mimeType": "application/json",
    "serviceName": "Example Converter",
    "tags": ["pdf", "markdown"],
    "iconUrl": "https://api.example.com/icon.png"
  },
  "accepts": [ ... ],
  "extensions": {
    "bazaar": {
      "info": {
        "input": { "type": "http", "method": "POST", "bodyType": "json", "body": { ... } },
        "output": { "type": "json", "example": { ... } }
      },
      "schema": { ... }
    }
  }
}
```

The optional `serviceName`, `tags`, and `iconUrl` fields on the `resource` object are service-level metadata the Bazaar uses to present your listing; they are persisted along with `description` and `mimeType`.

**x402 v1** servers declare through `outputSchema.input` on the payment requirements themselves (with `type` and `method` required). Because v1 discovery info rides inside the payment requirements — which your server controls end to end — v1 listing does not depend on the buyer's client echoing anything.

**MCP servers** declare per tool. The catalog key is `(resource, toolName)`, so a server exposing several tools at one URL gets one entry per tool.

## Host policy

The resource URL must be an absolute `http` or `https` URL on a public host. Declarations that fail these rules are rejected on the payment response itself (see [rejection reasons](#rejection-reasons)) and never enter the catalog:

| `rejectedReason` | Rule |
| - | - |
| `resource_url_invalid` | Not a parseable absolute URL |
| `resource_scheme_not_allowed` | Any scheme other than `http` or `https` (for example `monopoly://`, `solana-transfer://`) |
| `resource_host_loopback`, `resource_host_private` | `localhost`, `*.localhost`, loopback, RFC 1918, carrier-grade NAT, link-local, reserved and documentation ranges, or a hostname that resolves to any of them |
| `resource_host_ip_literal` | IP-literal hosts (v4 or v6) — list a hostname instead |
| `resource_host_dotless` | Bare service names such as `http://backend:8080/` |
| `resource_host_ephemeral` | Tunnel and preview hosts (`trycloudflare.com`, `ngrok`, `serveo`, `loca.lt`, `replit.dev` previews, and similar) |
| `resource_userinfo`, `resource_control_chars` | Credentials or control characters in the URL |

Reachability is checked asynchronously by the catalog worker, never on the payment path. Its verdict is visible through [`/discovery/listing-status`](#get-/discovery/listing-status): `resource_unreachable` (DNS failure, connection error, timeout, or a `5xx`), `resource_not_found` (`404`/`405`/`410` on a `GET`/`HEAD` resource), `resource_not_x402` (a `GET`/`HEAD` resource answered unpaid without a `402`), and `resource_needs_settlement` (a `POST`/`PUT`/`PATCH`/`DELETE`, MCP, or method-less resource seen only through `/verify`; it lists on its first settlement). All probes are read-only, so a listing can never cause a request to reach your application handler.

## Reading EXTENSION-RESPONSES

Every `/verify` and `/settle` response from the facilitator reports what happened to your declaration via the `EXTENSION-RESPONSES` header — a base64-encoded JSON object keyed by extension:

```
EXTENSION-RESPONSES: eyJiYXphYXIiOnsic3RhdHVzIjoicHJvY2Vzc2luZyJ9fQ==
                     → {"bazaar":{"status":"processing"}}
```

| What you get | Meaning |
| - | - |
| `{"bazaar":{"status":"processing"}}` | Declaration accepted and queued for cataloging. Your entry appears or refreshes within seconds. |
| `{"bazaar":{"status":"rejected","rejectedReason":"..."}}` | Declaration received but not catalogued. The reason says why (truncated to 256 characters) — see [rejection reasons](#rejection-reasons). Payment is unaffected. |
| No header at all | The payment payload carried no bazaar extension — the buyer's client dropped your declaration. |

A rejected or missing declaration never affects the payment itself: verification and settlement succeed or fail on their own terms.

### Rejection reasons

| `rejectedReason` | What it means | What to do |
| - | - | - |
| `Bazaar extension validation failed: …` / `info failed schema validation` | Your `info` does not satisfy the `schema` you shipped next to it, or the declaration breaks a structural rule of the protocol. The validator's own error paths are included in the reason text. | Validate locally with `validateDiscoveryExtension` from `@x402/extensions` and fix the declaration on your server. |
| `resource url missing or not absolute (payload.resource.url)` | The buyer's client echoed your extension but dropped or rewrote the `resource` object, so the payload carries no absolute URL to catalog under. Your 402 is fine. | Pay once through a client that echoes `resource` verbatim (any `@x402/*` 2.x, `x402-solana` ≥ 2.0.5). If you wrote the client, echo the 402's `resource` object unchanged. |
| `declaration could not be extracted` | The declaration validated but the extractor failed on it. This should not happen for a spec-valid declaration. | Open an issue with your 402 body. |
| `failed to enqueue discovery cataloging` | A transient facilitator-side queue error. Nothing is wrong with your declaration. | Retry later. The payment itself was processed normally. |

## What gets catalogued

Each entry in `/discovery/resources` carries:

| Field | Description |
| - | - |
| `resource` | The endpoint URL |
| `toolName` | MCP tool name; `null` for HTTP resources |
| `type` | `http` or `mcp` |
| `x402Version` | Protocol version of the indexing payment |
| `accepts` | Payment requirements from the indexing payment |
| `description`, `mimeType`, `serviceName`, `tags`, `iconUrl` | Service metadata from your declaration (`null` when not declared) |
| `method` | HTTP method (`null` for MCP) |
| `extensions` | Your declaration verbatim (`extensions.bazaar.info` / `.schema`) — the field the upstream `@x402/extensions` discovery client reads to build a request. `null` on entries last written before this field was persisted; refills on the next extension-carrying payment |
| `inputSchema`, `outputSchema` | PayAI's flattened view of the same declaration, kept for compatibility |
| `lastUpdated` | ISO timestamp of the last refresh |

## Refresh semantics

* Entries are **upserted from every payment that carries the extension** — `accepts`, schemas, metadata, and `lastUpdated` all refresh. Identical repeats of an unchanged listing are collapsed so a busy endpoint does not rewrite its row on every payment; a payment whose declaration, `accepts`, or service metadata changed is written straight away.
* Refresh is **forward-only**. Correcting your declaration does not rewrite the catalog until the next extension-carrying payment arrives.
* Listed resources are **probed on an ongoing basis** with read-only `HEAD`/`GET` requests (every listed row at least once a day; recently settling resources more often). A resource that fails three consecutive probes spanning at least 48 hours — connection failures, timeouts, `5xx`, or `404`/`405`/`410` on a `GET`/`HEAD` resource — is **hidden** from `/discovery/resources` and `/discovery/search`; the first healthy probe, or a successful settlement through the facilitator (identical settlements are coalesced, so allow up to five minutes), relists it automatically. A `404`/`405` from a `POST`/`PUT`/`PATCH`/`DELETE`, MCP, or method-less resource is inconclusive and changes nothing. A hostname that starts resolving to private address space is hidden immediately and is relisted only by a healthy probe. Hidden rows keep their data and are visible through `/discovery/listing-status`.
* There is **no re-index endpoint**. To force a refresh, make one verify-shaped payment against your own endpoint through a client that echoes extensions — `/verify` catalogs and moves no funds. Confirm with the `processing` header status.

## Endpoints

### GET /discovery/resources

| Parameter | Type | Required | Description | Default |
| - | - | - | - | - |
| `type` | `string` | Optional | Resource type: `http` or `mcp` | — |
| `payTo` | `string` | Optional | Payment recipient address, matched inside `accepts` | — |
| `scheme` | `string` | Optional | Payment scheme, e.g. `exact`, matched inside `accepts` | — |
| `network` | `string` | Optional | Network as declared by the resource, e.g. `eip155:8453` or `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` (v1 entries use plain names such as `base`) | — |
| `extensions` | `string` | Optional | Only entries carrying this extension key, e.g. `bazaar` | — |
| `limit` | `number` | Optional | Results per page (1–1000) | 100 |
| `offset` | `number` | Optional | Results to skip | 0 |

These are the filters the x402 Bazaar specification defines for `ListDiscoveryResourcesParams`, so `bazaar.listResources({ type: "mcp" })` from `@x402/extensions` works unchanged. The `payTo`, `scheme`, and `network` filters must all be satisfied by a **single** `accepts` entry: a resource that takes `exact` on Base and `upto` on Solana is not returned for `scheme=exact&network=solana:…`, because it does not accept that pair.

Returns `{ items, pagination: { limit, offset, total }, x402Version }`, newest first. The unfiltered listing is cached for about a minute; filtered requests are served from indexes and not cached. The legacy `/list` path redirects here permanently.

### GET /discovery/search

Natural-language search over the visible catalog, as defined by the Bazaar specification (`bazaar.search()` in `@x402/extensions` targets it).

| Parameter | Type | Required | Description |
| - | - | - | - |
| `query` | `string` | Yes | 1–256 characters; matched against service name, description, tags, MCP tool names and input descriptions, and the resource URL |
| `type`, `payTo`, `scheme`, `network`, `extensions` | `string` | Optional | Same semantics as `/discovery/resources` |
| `limit` | `number` | Optional | Advisory; default 20, at most 50 |
| `cursor` | `string` | Optional | Accepted and ignored |

Returns `{ resources, partialResults, pagination: null, x402Version }`. `resources` uses the same item shape as the list endpoint, ranked by relevance then recency. `partialResults` is `true` when more matches exist than were returned. If no whole-word match exists, a prefix match is tried before returning an empty list. Responses are never cached.

### GET /discovery/listing-status

Answers "is my resource listed, and if not, why" without a support thread.

| Parameter | Type | Required | Description |
| - | - | - | - |
| `resource` | `string` | Yes | The exact resource URL as catalogued |
| `toolName` | `string` | Optional | MCP tool name; omit for HTTP resources |

Returns `{ resource, toolName, listed, hidden, hiddenReason, lastUpdated, lastProbe: { at, status, httpStatus } | null, lastWrite: { status, reason, detail, at, source } | null, policy }`. `lastWrite` is the catalog worker's most recent outcome for this resource (kept for 30 days): `listed`, `updated`, or `rejected` with the reason. `policy` is the static host-policy verdict for the URL you passed. Responds `404` (RFC 9457 problem) when the resource has neither a catalog row nor a recent write outcome.

### GET /discovery/stats

Aggregate catalog and settlement statistics. Note the three catalog counts:

```json theme={null}
"merchants": {
  "hosts": 1505,          // distinct services — the "how many merchants" number
  "resources": 25086,     // distinct paid endpoint URLs
  "catalogEntries": 25086, // rows: one per (resource, toolName)
  "total": 25086          // deprecated alias of catalogEntries
}
```

An MCP server with N tools contributes N `catalogEntries` but one `resource` and at most one new `host`.

## Opting out and delisting

* **v1**: declare `discoverable: false` inside `outputSchema.input` and the facilitator will not index the resource.
* **v2**: simplest is to omit the `bazaar` extension from your 402 — with nothing to echo, nothing is indexed.
* **Dead resources leave on their own.** Take the endpoint down (connection refused, DNS gone, or `404`/`410` on a `GET` resource) and the probes above hide it within about two days; it comes back automatically if it reappears. Entries on hosts that can never be reached from the public internet were removed in September 2026 and are refused at write time.
* **Immediate removal of a live entry** (for example a path you have retired but still serve) is still a manual request — reach out on Discord or open an issue with proof of resource ownership. The x402 specification defines no delisting semantics yet.

## Troubleshooting

| Symptom | Likely cause | What to do |
| - | - | - |
| Payments settle but the resource never appears | Buyer's client is not echoing `extensions` into the payment payload | Check a payment response: no `EXTENSION-RESPONSES` header confirms it. Pay once through an echoing client (any `@x402/*` 2.x, `x402-solana` ≥ 2.0.5) |
| Entry exists but is stale after you changed your declaration | No extension-carrying payment since the change | Trigger a refresh via `/verify` with an echoing client; look for `processing` |
| Header says `rejected` | See [rejection reasons](#rejection-reasons) — only the validation reasons point at your declaration | Fix the declaration per `rejectedReason`; validate locally with `@x402/extensions` |
| Header says `rejected` with `resource url missing or not absolute` | Buyer's client dropped or rewrote the `resource` object while echoing your extension | Pay once through a client that echoes `resource` verbatim; your 402 needs no change |
| Header says `rejected` with a `resource_host_*` or `resource_scheme_*` reason | Your resource URL fails the [host policy](#host-policy) | List a public hostname over `http(s)`; tunnels and preview hosts are not catalogued |
| Header said `processing` but the entry never appeared | The catalog worker could not reach the resource on first listing | Query `/discovery/listing-status?resource=…` — `lastWrite.reason` says `resource_unreachable` or `resource_not_found` with detail; fix and pay once more |
| Entry disappeared from the listing | Probes failed for two days, or the host now resolves to private space | `/discovery/listing-status` shows `hidden: true` with `hiddenReason` and `lastProbe`; the first healthy probe relists it |
| Entry shows `null` for `serviceName`, `tags`, `iconUrl`, or `extensions` | Entry was last refreshed before those fields were persisted, or the declaration omits them | Any extension-carrying payment after declaring them populates the fields; identical repeats do not rewrite the row, so change something or wait for the next real payment |

## Need help?

<Card title="Join our Community" icon="discord" href="https://discord.gg/eWJRwMpebQ">
  Have questions or want to connect with other developers? Join our Discord server.
</Card>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.