Skip to main content
Rail402 catalogs a resource when a payment for it settles and the payment carries the x402 bazaar extension. There is no registration step and no separate API for sellers. The catalog is kept in Postgres, off-chain, and cataloging adds no transaction to the payment. Everything in the payment payload is echoed by the buyer and treated as untrusted. The rules on this page decide what reaches the catalog. They are implemented in packages/bazaar and listed with their codes and tests in Errors and verification rules.

When it runs

Cataloging never changes the payment result. /verify waits at most 250 ms for the preview and /settle at most 2 seconds for cataloging; past that, the response reports processing with awaiting_settlement (/verify) or cataloging_in_progress (/settle), and cataloging finishes in the background. A payment without a bazaar extension produces no outcome at all. A settled payment with a bazaar extension is written to a queue before cataloging starts and removed once cataloging reached an outcome. If the service stops in between, a background worker (every ORIGIN_CHECK_INTERVAL_MS, 30 seconds after the settlement at the earliest) finishes it; cataloging is idempotent per settlement, so nothing is cataloged twice. When the catalog store cannot take it, the worker retries after 10 seconds, 1 minute, 5 minutes, 30 minutes and 2 hours. A new HTTP resource is cataloged at once but listed only after its own 402 response confirms it (see Origin verification): until then it is pending, visible only through GET /discovery/resources/{id}. An MCP tool is listed on settlement.

What is read from the payment

  • paymentPayload.extensions.bazaar: info (the resource’s input and optional output), schema (a JSON Schema that info must satisfy) and optional routeTemplate. Sellers produce it with declareDiscoveryExtension from @x402/extensions/bazaar.
  • paymentPayload.resource: url, and optional description, mimeType, serviceName, tags and iconUrl.
  • paymentRequirements: the settled scheme, network, asset, payTo, amount, maxTimeoutSeconds and extra.

Validation

Checked in this order. The first failure rejects cataloging with its code; the payment is unaffected.
1

Protocol and structure

The payment is x402 version 2 (bazaar_unsupported_version). The extension is an object with info and schema objects (bazaar_extension_malformed) and passes the upstream structural check for an HTTP or MCP discovery description (bazaar_info_unsupported).
2

Schema, in a sandbox

Before anything is compiled, schema and info must each be at most 32 KiB serialized and 32 levels deep (bazaar_schema_too_large), and schema may contain no external $ref or $id; only same-document references (#…) are allowed (bazaar_schema_external_reference). The schema is then compiled as JSON Schema Draft 2020-12 (bazaar_schema_invalid) and info validated against it (bazaar_info_invalid) in an isolated worker thread with a 250 ms budget; a worker that runs over is terminated (bazaar_schema_timeout).
3

Method or tool name

For info.input.type: "http" the method is info.input.method, which upstream’s structural check accepts only in upper case. The schema that declareDiscoveryExtension produces requires method, so its listings always declare one. When an extension’s own schema leaves method optional and it is omitted, the method is POST if info.input.bodyType is present and GET otherwise. For info.input.type: "mcp", info.input.toolName must be 1 to 128 visible ASCII characters, with no spaces or control characters (bazaar_info_unsupported).
4

Resource URL

resource.url must be an absolute URL of at most 2,048 characters, without credentials or control characters, using https, or on testnet also http (MCP tools may also use mcp://) (bazaar_resource_invalid). It is canonicalized: lower-case scheme and host, IDN host as punycode, default port removed, percent-encoding normalized (escapes of unreserved characters decoded, others in upper case, so /%61pi is /api), dot segments resolved (including encoded ones), query and fragment removed. http and https, and a path with and without a trailing slash, remain different resources.
5

Public host

For http and https URLs, the host must be public (bazaar_resource_unsafe): not localhost or a name ending in .localhost, .local, .internal, .home.arpa or .lan, not a single-label name, and not an IP literal in a private, loopback, link-local, shared, documentation, benchmarking, multicast or reserved range (IPv4 and IPv6).
6

Owner

The owner is the account the settlement paid: payTo, or the base G… account of an M… muxed address. A payer paying its own payTo catalogs nothing (bazaar_self_payment). This is defence in depth: the facilitator already refuses such a payment with invalid_exact_stellar_payload_self_payment, so it never settles and the code cannot be reached through the service.

Soft-dropped metadata

Invalid optional metadata is left out rather than failing the listing. The names of the dropped fields are returned in the outcome’s dropped array.

Route templates

An HTTP seller whose route has path parameters can declare routeTemplate, for example /users/:id, so that /users/1 and /users/2 are one listing. Rail402 uses it only if both hold:
  1. It passes the upstream isValidRouteTemplate check. The template must match ^/[a-zA-Z0-9_/:.\-~%]+$, and it is percent-decoded repeatedly (at most 5 passes) before the traversal checks, so an encoded %2e%2e is caught: the decoded template may not contain .. or ://. Rail402 also refuses a template that decodes to NUL, CR, LF, a backslash or an empty segment (//).
  2. It describes the path that was actually paid for: the same number of segments, every static segment equal to the paid segment after percent-decoding, and every :param segment matching a non-empty paid segment.
When both hold, the listing’s resource is the origin plus the template, percent-encoding normalized like a path. Otherwise the template is ignored, routeTemplate appears in dropped, and the listing uses the concrete path. The pathParams in a templated listing’s info.input are the example of the first path paid: paying another path of the same route is recorded against the listing and changes nothing else.

Identity and deduplication

A listing is identified by: MCP tools are scoped to their owner because nothing proves who runs an MCP server: two sellers declaring the same tool get two listings, and neither can claim the other’s. Settlements for the same identity update one listing. The network is part of the identity because a settlement proves a payTo on its own network only: a resource sold on testnet and pubnet has a listing on each, which discovery and search show as one resource. A settlement transaction is counted once, and it catalogs at most one resource: replaying a settled payment with a different resource is refused (bazaar_settlement_reused).

Ownership and changes

Each listing belongs to the payTo that its first settlement paid. Later settlements are handled like this: A settlement proposes a change when its payment option or its metadata differs from the listing. Options are compared by scheme, network, asset and payTo: an option with the same four values and a different amount, timeout or extra is a change, and an option the listing does not have yet is a change. The proposal itself is never published. For an HTTP listing it only schedules an origin check, and what is then published comes from the resource’s own 402.

Origin verification

For HTTP listings, the metadata echoed by a buyer is never the last word. After a listing is created, after a change is proposed, and after an ownership conflict, a background task (every ORIGIN_CHECK_INTERVAL_MS, 5 seconds by default) requests the resource itself, without payment:
  • It sends the listing’s method to the concrete paid URL (a POST, PUT or PATCH carries the body {}), with the user agent rail402-bazaar-origin-check/1 (+https://rail402.dev).
  • Every DNS answer must be a public address, checked in the connection’s own lookup. Redirects are not followed.
  • It expects 402 with a PAYMENT-REQUIRED header. When the resource cannot be reached, times out, or answers 408, 429 or 5xx, the check is retried 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours later; when every retry fails, the listing is withdrawn (quarantined). Any other answer, or a 402 without an accepts array, withdraws it at once: the resource does not sell what is listed.
  • Each check is independent: a malformed response only affects its own listing.
  • A worker claims a due check for 60 seconds before fetching it, so with several replicas each check is fetched once. A check requested again while it is being fetched runs again afterwards.
  • One host receives at most 30 origin requests a minute from each replica; a check over the budget waits for the next minute without spending a retry.
From the 402, it takes the bazaar extension, the keys of any other extensions it declares (such as payment-identifier, which the extensions discovery filter matches), and the payment options on the listing’s network. Only well-formed Stellar options are considered: a C… asset contract, a G…, C… or M… payTo, an integer amount and a positive integer maxTimeoutSeconds; others are ignored. When the listing is confirmed, its accepts become the options that pay its owner: A pending or quarantined listing is not returned by discovery or search. A later origin check that succeeds publishes it; every settlement for it by its owner, and every settlement by a different payTo, triggers one. Every one of these changes is a new version in the listing’s history.

Trust

Search breaks score ties in favour of domain_verified, then origin_verified listings, then the longest-listed. None of these can be bought by repeating payments.

Stellar facts and domain claims

A background task (every ORIGIN_CHECK_INTERVAL_MS) reads, for each published listing, and again six hours later:
  • Each payment option’s token, from the token contract itself: symbol(), name() and decimals().
  • Whether its payTo can receive that token now. A contract (C…) account always can. For a Stellar Asset Contract, a G… or M… account can if it is the asset’s issuer or holds an authorized trustline (for native XLM, if it exists). For another SEP-41 token this is left unknown.
  • The domain’s claim. Rail402 reads https://<resource host>/.well-known/stellar.toml (SEP-1; at most 100 KB, same DNS and redirect rules as origin checks). If its ACCOUNTS list contains the listing’s owner, trust becomes domain_verified; if a later read no longer lists the owner, trust falls back to origin_verified (HTTP) or settled (MCP). Each change is a domain_verification version.
The facts appear on every discovery item: token facts under rail402.options, one per accepts entry, and the check time and domain claim under each listing’s stellar. They are not part of the listing’s versioned content.

Version history

Every change to a listing’s published content, owner, trust or state creates a version with a cause: The history is public at GET /discovery/resources/{id}/versions.

EXTENSION-RESPONSES

The outcome is returned to the seller’s resource server in the EXTENSION-RESPONSES response header of /verify and /settle: base64-encoded JSON keyed by extension name. Stock HTTPFacilitatorClient decodes it into extensionResponses and logs status, code, reason and rejectedReason. The header is at most 4,096 bytes, so every HTTP client accepts it: reasons are cut to 300 characters, and an outcome that would still be larger is reduced to its status, code, listingId and version.