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
@type code() :: String.t()
A builder code: 1–32 lowercase alphanumeric or underscore characters.
@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.
Codes extracted from an echoed builder-code value.
Functions
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) - Attachseven when the server did not advertisebuilder-code, as the spec's client behaviour prescribes. Withfalsethe enricher is a no-op for servers that do not declare the extension. The default value istrue.
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" => %{}}}
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
@spec extension_key() :: String.t()
Returns the extension key on the wire.
Examples
iex> X402.Extensions.BuilderCode.extension_key()
"builder-code"
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}
@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
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
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
amust equal the advertisedinfo.a, and must be absent when the server declared none (:builder_code_mismatch); - the echoed
smay 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