Bazaar discovery extension for x402 v2: extension builder and discovery client.
Discovery extension builder
Resource servers advertise their endpoint specification by placing the
extension under extensions.bazaar in a PAYMENT-REQUIRED response. The
extension carries the actual discovery data (info) together with a JSON
Schema (Draft 2020-12) that validates it (schema).
info.input is a discriminated union selected by the type field:
"http"— an HTTP endpoint. Query methods (GET,HEAD,DELETE) describe example query parameters; body methods (POST,PUT,PATCH) add abodyTypeand an examplebody."mcp"— a Model Context Protocol tool, identified bytoolNameand described by a JSON SchemainputSchemafor its arguments.
info.output describes the expected response format. It is always present
(defaulting to a "json" content type). The schema's output property
declares type (and optionally format) as strings and infers the JSON
type of example, optionally refined by the :schema option.
The factory returns a plain, string-keyed map ready for JSON encoding:
extensions = %{"bazaar" => X402.Extensions.Bazaar.build_extension(method: :get)}Discovery client
list_resources/2 queries a facilitator's GET /discovery/resources
through X402.Facilitator.list_resources/2 and parses each discovered
entry into a well-typed map (see resource/0). Parsing is fail-closed:
a structurally invalid entry returns
{:error, %X402.Facilitator.Error{type: :malformed_facilitator_response}}
identifying the offending entry, rather than partial data. Use
X402.Facilitator.list_resources/2 directly and parse_resource/1
per-entry to build a lenient listing instead.
The pure filter helpers filter_by_network/2, filter_by_scheme/2, and
filter_by_max_price/2 narrow a parsed listing client-side:
{:ok, %{items: items}} = X402.Extensions.Bazaar.list_resources(MyFacilitator)
items
|> X402.Extensions.Bazaar.filter_by_network("eip155:8453")
|> X402.Extensions.Bazaar.filter_by_max_price("100000")See the bazaar extension spec.
Summary
Facilitator Discovery
Keeps the resources with at least one payment option at or below a price.
Keeps the resources that accept payment on the given CAIP-2 network.
Keeps the resources that accept payment with the given scheme.
Lists discoverable x402 resources from a facilitator's bazaar as typed maps.
Parses one raw discovered-resource map into a typed map.
Types
Parsed response of list_resources/2.
A discovered x402 resource parsed from GET /discovery/resources.
A built extensions.bazaar discovery extension payload.
Functions
Builds a bazaar discovery extension payload (info + schema).
Facilitator Discovery
@spec filter_by_max_price([resource()], String.t() | non_neg_integer()) :: [ resource() ]
Keeps the resources with at least one payment option at or below a price.
The price is compared in atomic token units against each accepted
PaymentRequirements entry's amount (falling back to the legacy
maxAmountRequired). Entries without a parsable amount never match.
Raises ArgumentError when max_price itself is not a non-negative
integer or decimal string, since that is a programmer error.
Examples
iex> resources = [
...> %{resource: "https://a.example", accepts: [%{"scheme" => "exact", "amount" => "10000"}]},
...> %{resource: "https://b.example", accepts: [%{"scheme" => "exact", "amount" => "250000"}]}
...> ]
iex> resources
...> |> X402.Extensions.Bazaar.filter_by_max_price("100000")
...> |> Enum.map(& &1.resource)
["https://a.example"]
Keeps the resources that accept payment on the given CAIP-2 network.
Examples
iex> resources = [
...> %{resource: "https://a.example", accepts: [%{"scheme" => "exact", "network" => "eip155:8453"}]},
...> %{resource: "https://b.example", accepts: [%{"scheme" => "exact", "network" => "solana:mainnet"}]}
...> ]
iex> resources
...> |> X402.Extensions.Bazaar.filter_by_network("eip155:8453")
...> |> Enum.map(& &1.resource)
["https://a.example"]
Keeps the resources that accept payment with the given scheme.
Examples
iex> resources = [
...> %{resource: "https://a.example", accepts: [%{"scheme" => "exact", "network" => "eip155:8453"}]},
...> %{resource: "https://b.example", accepts: [%{"scheme" => "upto", "network" => "eip155:8453"}]}
...> ]
iex> resources
...> |> X402.Extensions.Bazaar.filter_by_scheme("upto")
...> |> Enum.map(& &1.resource)
["https://b.example"]
@spec list_resources( X402.Facilitator.server() | keyword(), keyword() ) :: {:ok, discovery_response()} | {:error, X402.Facilitator.Error.t() | NimbleOptions.ValidationError.t() | term()}
Lists discoverable x402 resources from a facilitator's bazaar as typed maps.
Queries GET /discovery/resources through
X402.Facilitator.list_resources/2 (accepting the same filter and
pagination parameters) and parses every discovered entry with
parse_resource/1. Parsing is fail-closed: a structurally invalid entry
returns {:error, %X402.Facilitator.Error{type: :malformed_facilitator_response, reason: {:invalid_resource, index, reason}}} instead of partial data.
When called with just a keyword list — list_resources(limit: 20) — the
parameters apply to the default X402.Facilitator process name.
Examples
{:ok, %{items: items, pagination: pagination}} =
X402.Extensions.Bazaar.list_resources(MyFacilitator,
network: "eip155:8453",
limit: 20
)
Enum.map(items, & &1.resource)
Parses one raw discovered-resource map into a typed map.
Validates the required fields from the v2 specification (§8.3): resource,
type, x402Version, accepts (a list of PaymentRequirements maps),
and lastUpdated (Unix timestamp or ISO 8601 string). Optional
description, mimeType, metadata, and extensions fields default to
nil when absent and are rejected when mistyped.
Examples
iex> {:ok, parsed} = X402.Extensions.Bazaar.parse_resource(%{
...> "resource" => "https://api.example.com/premium-data",
...> "type" => "http",
...> "x402Version" => 2,
...> "accepts" => [%{"scheme" => "exact", "network" => "eip155:8453", "amount" => "10000"}],
...> "lastUpdated" => 1_703_123_456
...> })
iex> {parsed.resource, parsed.x402_version, parsed.last_updated}
{"https://api.example.com/premium-data", 2, 1703123456}
iex> X402.Extensions.Bazaar.parse_resource(%{"type" => "http"})
{:error, {:missing_field, "resource"}}
iex> X402.Extensions.Bazaar.parse_resource(%{
...> "resource" => "https://api.example.com",
...> "type" => "http",
...> "x402Version" => 2,
...> "accepts" => "exact",
...> "lastUpdated" => 1
...> })
{:error, {:invalid_field, "accepts"}}
Types
@type discovery_response() :: %{ x402_version: integer() | nil, items: [resource()], pagination: X402.Facilitator.discovery_pagination() | nil }
Parsed response of list_resources/2.
@type resource() :: %{ resource: String.t(), type: String.t(), x402_version: integer(), accepts: [map()], last_updated: integer() | String.t(), description: String.t() | nil, mime_type: String.t() | nil, metadata: map() | nil, extensions: map() | nil }
A discovered x402 resource parsed from GET /discovery/resources.
:accepts entries are the raw, string-keyed PaymentRequirements maps
from the wire. :last_updated is either a Unix timestamp (per the v2
specification) or an ISO 8601 string (as emitted by some facilitators).
A built extensions.bazaar discovery extension payload.
Functions
Builds a bazaar discovery extension payload (info + schema).
Accepts either an HTTP endpoint config or an MCP tool config.
HTTP options
:method— (required) HTTP method::get,:head,:delete,:post,:put,:patch(or the uppercase string). Query methods produce a read-only signature; body methods addbodyTypeandbody.:input— example input values (a map of query parameters for query methods, a map for"json"/"form-data"bodies, or a string for"text"bodies).:input_schema— JSON Schema merged into the schema'squeryParamsorbodyproperty.:body_type— request body content type for body methods:"json"(default),"form-data", or"text".:headers— example custom header values.:path_params— concrete path parameter values (dynamic routes).:path_params_schema— JSON Schema for path parameters.
MCP options
:tool_name— (required) MCP tool name.:input_schema— (required) JSON Schema for the tool'sarguments.:description— human-readable tool description.:transport— MCP transport:"streamable-http"(default) or"sse".:example— exampleargumentsobject.
Output options (:output)
A keyword list or map describing the expected response:
:type— response content type (default"json").:format— additional format information.:example— example response value.:schema— JSON Schema merged into the schema'sexampleproperty.
Examples
iex> ext = X402.Extensions.Bazaar.build_extension(method: :get, input: %{"city" => "San Francisco"})
iex> ext["info"]["input"]["type"]
"http"
iex> ext["info"]["input"]["method"]
"GET"
iex> ext["info"]["input"]["queryParams"]
%{"city" => "San Francisco"}
iex> ext = X402.Extensions.Bazaar.build_extension(method: :post, input: %{"query" => "example"})
iex> ext["info"]["input"]["bodyType"]
"json"
iex> ext = X402.Extensions.Bazaar.build_extension(
...> tool_name: "financial_analysis",
...> input_schema: %{"type" => "object", "properties" => %{"ticker" => %{"type" => "string"}}, "required" => ["ticker"]}
...> )
iex> ext["info"]["input"]["type"]
"mcp"
iex> ext["info"]["input"]["toolName"]
"financial_analysis"