X402.Extensions.Bazaar (X402 v0.6.0)

Copy Markdown View Source

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 a bodyType and an example body.
  • "mcp" — a Model Context Protocol tool, identified by toolName and described by a JSON Schema inputSchema for 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

A discovered x402 resource parsed from GET /discovery/resources.

t()

A built extensions.bazaar discovery extension payload.

Functions

Builds a bazaar discovery extension payload (info + schema).

Facilitator Discovery

filter_by_max_price(resources, max_price)

(since 0.6.0)
@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"]

filter_by_network(resources, network)

(since 0.6.0)
@spec filter_by_network([resource()], String.t()) :: [resource()]

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"]

filter_by_scheme(resources, scheme)

(since 0.6.0)
@spec filter_by_scheme([resource()], String.t()) :: [resource()]

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"]

list_resources(server_or_params \\ Facilitator, params \\ [])

(since 0.6.0)

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)

parse_resource(item)

(since 0.6.0)
@spec parse_resource(term()) :: {:ok, resource()} | {:error, term()}

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

discovery_response()

@type discovery_response() :: %{
  x402_version: integer() | nil,
  items: [resource()],
  pagination: X402.Facilitator.discovery_pagination() | nil
}

Parsed response of list_resources/2.

resource()

@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).

t()

@type t() :: %{required(binary()) => map()}

A built extensions.bazaar discovery extension payload.

Functions

build_extension(opts)

(since 0.5.0)
@spec build_extension(keyword()) :: t()

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 add bodyType and body.
  • :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's queryParams or body property.
  • :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's arguments.
  • :description — human-readable tool description.
  • :transport — MCP transport: "streamable-http" (default) or "sse".
  • :example — example arguments object.

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's example property.

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"