Behaviour for resource-server extension adapters run by X402.Plug.PaymentGate.
An adapter packages one protocol extension's server-side lifecycle so the gate can run it from a single option instead of the user wiring each step by hand:
plug X402.Plug.PaymentGate,
routes: [...],
extensions: [
{X402.Extensions.PaymentIdentifier.Adapter, required: true},
{X402.Extensions.BuilderCode.Adapter, app_code: "my_app"}
]Every callback but key/0 is optional:
init/1validates the adapter's options once, when the gate's own options are validated; the returned options are what the remaining callbacks receive.advertise/2returns the value to advertise under the extension key in everyPaymentRequired.extensions(merged over the route's staticextensionsmap), ornilto advertise nothing for this request.validate/3runs after the generic extension echo check on every payment, with the value the client echoed (ornil) and the value that was advertised (ornil). An error rejects the payment with 400invalid_payloadand a{:extension_invalid, key, reason}telemetry reason.after_verify/4andafter_settle/4are notified with the facilitator's result once verification succeeded and once settlement succeeded. Their return values are ignored and exceptions are logged.
advertise/2 and validate/3 are part of the request's control flow,
so an exception there propagates like any other programmer error.
The sign-in-with-x extension keeps its dedicated :siwx gate option:
its challenge is regenerated per response and it changes the request flow
(a proof can skip payment entirely), which the generic advertise/validate
shape does not express. Bazaar discovery metadata likewise has its own
:bazaar route option because it depends on the matched route pattern.
Summary
Callbacks
Builds the value advertised under the extension key, or nil.
Notified after a successful facilitator settlement.
Notified after a successful facilitator verification.
Validates the adapter options at gate initialization.
Returns the extension key on the wire (for example "payment-identifier").
Validates the client's echoed value against the advertised one.
Functions
Merges every adapter's advertisement over a base extensions map.
Notifies every adapter defining after_settle/4.
Notifies every adapter defining after_verify/4.
Runs every adapter's validate/3 over the echoed and advertised maps.
Validates one :extensions entry and normalizes it to {module, opts}.
Types
Callbacks
@callback advertise( keyword(), X402.Hooks.RequestContext.t() ) :: map() | nil
Builds the value advertised under the extension key, or nil.
@callback after_settle( payload :: map(), requirements :: map(), result :: map(), keyword() ) :: term()
Notified after a successful facilitator settlement.
@callback after_verify( payload :: map(), requirements :: map(), result :: map(), keyword() ) :: term()
Notified after a successful facilitator verification.
Validates the adapter options at gate initialization.
@callback key() :: String.t()
Returns the extension key on the wire (for example "payment-identifier").
Validates the client's echoed value against the advertised one.
Functions
@spec advertise_all([entry()], X402.Hooks.RequestContext.t(), map()) :: map()
Merges every adapter's advertisement over a base extensions map.
Adapters without advertise/2, and those returning nil, leave the
map untouched.
Examples
iex> context = X402.Hooks.RequestContext.new(requirements: [%{"scheme" => "exact"}])
iex> entries = [{X402.Extensions.PaymentIdentifier.Adapter, required: true}]
iex> advertised = X402.Extension.advertise_all(entries, context, %{"other" => %{}})
iex> {Map.keys(advertised) |> Enum.sort(), advertised["payment-identifier"]["info"]}
{["other", "payment-identifier"], %{"required" => true}}
Notifies every adapter defining after_settle/4.
Return values are ignored; an exception is logged and the remaining adapters still run.
Notifies every adapter defining after_verify/4.
Return values are ignored; an exception is logged and the remaining adapters still run.
@spec validate_all([entry()], term(), map()) :: :ok | {:error, {:extension_invalid, String.t(), term()}}
Runs every adapter's validate/3 over the echoed and advertised maps.
Stops at the first error, tagging it with the adapter's key.
Examples
iex> entries = [{X402.Extensions.BuilderCode.Adapter, app_code: "my_app"}]
iex> advertised = %{"builder-code" => X402.Extensions.BuilderCode.extension("my_app")}
iex> X402.Extension.validate_all(entries, %{"builder-code" => %{"a" => "my_app"}}, advertised)
:ok
iex> entries = [{X402.Extensions.BuilderCode.Adapter, app_code: "my_app"}]
iex> advertised = %{"builder-code" => X402.Extensions.BuilderCode.extension("my_app")}
iex> X402.Extension.validate_all(entries, %{"builder-code" => %{"a" => "other"}}, advertised)
{:error, {:extension_invalid, "builder-code", :builder_code_mismatch}}
Validates one :extensions entry and normalizes it to {module, opts}.
Designed for NimbleOptions custom validation. The module must define
key/0; when it defines init/1 the options are passed through it.
Examples
iex> X402.Extension.validate_spec(X402.Extensions.PaymentIdentifier.Adapter)
{:ok, {X402.Extensions.PaymentIdentifier.Adapter, [required: false]}}
iex> X402.Extension.validate_spec({X402.Extensions.PaymentIdentifier.Adapter, required: true})
{:ok, {X402.Extensions.PaymentIdentifier.Adapter, [required: true]}}
iex> X402.Extension.validate_spec(:not_an_adapter)
{:error, "expected a module implementing X402.Extension (key/0), got: :not_an_adapter"}