X402.Extensions.BuilderCode (X402 v0.9.0)

Copy Markdown View Source

The x402 builder-code extension: ERC-8021 on-chain attribution codes.

Implements the builder-code extension. A resource server declares the application's app code (a) — and optionally up to 5 of its own service codes (s) — under PaymentRequired.extensions["builder-code"] (extension/2). The client echoes the declaration and attaches up to 5 service codes of its own (enricher/1). At settlement the facilitator adds its wallet code (w), CBOR-encodes a, s, and w as an ERC-8021 Schema 2 suffix, and appends it to the settlement transaction calldata.

# server (PaymentRequired.extensions)
%{"builder-code" => %{"info" => %{"a" => "my_app"}, "schema" => %{...}}}

# client (PaymentPayload.extensions)
%{"builder-code" => %{"info" => %{"a" => "my_app", "s" => ["my_client"]}, "schema" => %{...}}}

Every code matches ^[a-z0-9_]{1,32}$ (valid_code?/1).

What the resource server does with the code

Nothing beyond validation and forwarding: the codes travel inside the PaymentPayload.extensions the gate already sends to the facilitator on POST /verify and POST /settle, and the facilitator is the party that encodes them into calldata. The resource server is, however, the authority for a: validate_echo/2 implements the spec's echo rules — an echoed a must equal the advertised one (and must be absent when the server declared none), every code must be well-formed, and the echoed s may carry at most 10 entries (the combined client and server reservations). X402.Plug.PaymentGate and X402.MCP.Server run these rules on every payment whose payload carries the extension.

The echo may use either wire shape the ecosystem produces: the %{"info" => %{...}} envelope the reference client emits, or the bare %{"a" => ..., "s" => ...} map from the spec's examples.

Summary

Types

A builder code: 1–32 lowercase alphanumeric or underscore characters.

Errors returned by extract/1 and validate_echo/2.

Codes extracted from an echoed builder-code value.

Functions

Builds a client-side enricher for X402.Client.build_payment/3's :extensions option.

Builds the server-side advertisement for PaymentRequired.extensions.

Returns the extension key on the wire.

Extracts the app code and service codes from a builder-code value.

Returns the JSON schema the server advertises for the extension.

Checks whether a value is a well-formed builder code.

Validates a client's echoed builder-code value against the advertised one, as the resource server must before forwarding a payment.

Types

code()

@type code() :: String.t()

A builder code: 1–32 lowercase alphanumeric or underscore characters.

error()

@type error() ::
  :invalid_builder_code_extension
  | {:invalid_builder_code, String.t()}
  | :too_many_service_codes
  | :builder_code_mismatch

Errors returned by extract/1 and validate_echo/2.

extracted()

@type extracted() :: %{app_code: code() | nil, service_codes: [code()]}

Codes extracted from an echoed builder-code value.

Functions

enricher(opts)

(since 0.9.0)
@spec enricher(keyword()) :: (map(), map() | nil -> {:ok, map()})

Builds a client-side enricher for X402.Client.build_payment/3's :extensions option.

The returned function attaches the client's service codes as extensions["builder-code"]["info"]["s"]. When the server advertised builder-code, the advertised info (including a) and schema are echoed unchanged and the client codes are prepended to any server codes (deduplicated, client first, as the reference client merges them). When it did not, only %{"info" => %{"s" => codes}} is attached — never a.

Options

  • :service_codes - Required. The client's service code(s) (s): a code or a list of at most 5 codes.

  • :always (boolean/0) - Attach s even when the server did not advertise builder-code, as the spec's client behaviour prescribes. With false the enricher is a no-op for servers that do not declare the extension. The default value is true.

Examples

iex> enricher = X402.Extensions.BuilderCode.enricher(service_codes: "my_client")
iex> advertised = %{"builder-code" => X402.Extensions.BuilderCode.extension("my_app", service_codes: "sdk")}
iex> {:ok, enriched} = enricher.(%{"extensions" => advertised}, %{"extensions" => advertised})
iex> enriched["extensions"]["builder-code"]["info"]
%{"a" => "my_app", "s" => ["my_client", "sdk"]}

iex> enricher = X402.Extensions.BuilderCode.enricher(service_codes: ["base_mcp", "demo_app"])
iex> {:ok, enriched} = enricher.(%{"extensions" => %{}}, %{"extensions" => %{}})
iex> enriched["extensions"]["builder-code"]
%{"info" => %{"s" => ["base_mcp", "demo_app"]}}

iex> enricher = X402.Extensions.BuilderCode.enricher(service_codes: "my_client", always: false)
iex> enricher.(%{"extensions" => %{}}, %{"extensions" => %{}})
{:ok, %{"extensions" => %{}}}

extension(app_code, opts \\ [])

(since 0.9.0)
@spec extension(
  code(),
  keyword()
) :: map()

Builds the server-side advertisement for PaymentRequired.extensions.

Raises NimbleOptions.ValidationError for a malformed app code or service codes: declaring an invalid code is a configuration error the spec requires rejecting at declaration time.

Options

  • :service_codes - The server's own service code(s) (info.s): a code or a list of at most 5 codes. The default value is [].

Examples

iex> X402.Extensions.BuilderCode.extension("my_app")["info"]
%{"a" => "my_app"}

iex> X402.Extensions.BuilderCode.extension("my_app", service_codes: "sdk_elixir")["info"]
%{"a" => "my_app", "s" => ["sdk_elixir"]}

iex> extension = X402.Extensions.BuilderCode.extension("my_app")
iex> extension["schema"] == X402.Extensions.BuilderCode.schema()
true

extension_key()

(since 0.9.0)
@spec extension_key() :: String.t()

Returns the extension key on the wire.

Examples

iex> X402.Extensions.BuilderCode.extension_key()
"builder-code"

extract(value)

(since 0.9.0)
@spec extract(term()) :: {:ok, extracted() | nil} | {:error, error()}

Extracts the app code and service codes from a builder-code value.

Accepts the value as it appears under extensions["builder-code"] in a PaymentPayload (or PaymentRequired): either the %{"info" => ...} envelope or a bare code map. s may be a single code or a list; it is always returned as a list. Returns {:ok, nil} for nil.

Codes are checked for format only. The count of s entries is not checked here — see validate_echo/2 for the echo rules.

Examples

iex> X402.Extensions.BuilderCode.extract(%{"info" => %{"a" => "my_app", "s" => "my_client"}})
{:ok, %{app_code: "my_app", service_codes: ["my_client"]}}

iex> X402.Extensions.BuilderCode.extract(%{"s" => ["base_mcp", "demo_app"]})
{:ok, %{app_code: nil, service_codes: ["base_mcp", "demo_app"]}}

iex> X402.Extensions.BuilderCode.extract(%{"info" => %{"a" => "My App"}})
{:error, {:invalid_builder_code, "a"}}

iex> X402.Extensions.BuilderCode.extract(%{"info" => %{"s" => [1]}})
{:error, {:invalid_builder_code, "s"}}

iex> X402.Extensions.BuilderCode.extract("my_app")
{:error, :invalid_builder_code_extension}

iex> X402.Extensions.BuilderCode.extract(nil)
{:ok, nil}

schema()

(since 0.9.0)
@spec schema() :: map()

Returns the JSON schema the server advertises for the extension.

Examples

iex> schema = X402.Extensions.BuilderCode.schema()
iex> schema["properties"]["a"]["pattern"]
"^[a-z0-9_]{1,32}$"
iex> schema["properties"]["s"]["maxItems"]
11

valid_code?(code)

(since 0.9.0)
@spec valid_code?(term()) :: boolean()

Checks whether a value is a well-formed builder code.

Examples

iex> X402.Extensions.BuilderCode.valid_code?("my_app_1")
true

iex> X402.Extensions.BuilderCode.valid_code?("My-App")
false

iex> X402.Extensions.BuilderCode.valid_code?("")
false

iex> X402.Extensions.BuilderCode.valid_code?(String.duplicate("a", 33))
false

iex> X402.Extensions.BuilderCode.valid_code?(:my_app)
false

validate_echo(echoed, advertised)

(since 0.9.0)
@spec validate_echo(term(), term()) :: :ok | {:error, error()}

Validates a client's echoed builder-code value against the advertised one, as the resource server must before forwarding a payment.

Both arguments are the values under extensions["builder-code"] (nil when absent). The rules, from the spec:

  • every echoed code must be well-formed ({:invalid_builder_code, field});
  • an echoed a must equal the advertised info.a, and must be absent when the server declared none (:builder_code_mismatch);
  • the echoed s may carry at most 10 entries — the client and server reservations combined (:too_many_service_codes).

A nil echo is always valid: omitting the extension is allowed.

Examples

iex> advertised = X402.Extensions.BuilderCode.extension("my_app")
iex> X402.Extensions.BuilderCode.validate_echo(%{"info" => %{"a" => "my_app", "s" => ["c"]}}, advertised)
:ok

iex> advertised = X402.Extensions.BuilderCode.extension("my_app")
iex> X402.Extensions.BuilderCode.validate_echo(%{"a" => "other_app"}, advertised)
{:error, :builder_code_mismatch}

iex> X402.Extensions.BuilderCode.validate_echo(%{"a" => "my_app"}, nil)
{:error, :builder_code_mismatch}

iex> X402.Extensions.BuilderCode.validate_echo(%{"s" => "my_client"}, nil)
:ok

iex> too_many = Enum.map(1..11, &"code_#{&1}")
iex> X402.Extensions.BuilderCode.validate_echo(%{"s" => too_many}, nil)
{:error, :too_many_service_codes}

iex> X402.Extensions.BuilderCode.validate_echo(nil, nil)
:ok