Behaviour for pluggable x402 payment schemes.
A scheme is x402's unit of extension: the pairing of a payment scheme
name ("exact", "upto", ...) with a family of CAIP-2 networks
("eip155:*", "solana:*", ...). Everything scheme-specific this library
does — deciding whether the client can sign an advertised requirement,
producing the signed scheme payload, structurally validating an incoming
PAYMENT-SIGNATURE, and running cheap local pre-checks before the
facilitator round-trip — dispatches through this behaviour. Adding support
for a new chain or scheme means writing one module and passing it as
an option, not editing X402.Client, X402.PaymentSignature, or
X402.Plug.PaymentGate.
The built-in schemes are X402.Scheme.ExactEVM (exact on eip155:*
networks via EIP-3009), X402.Scheme.ExactSVM (exact on solana:*
networks via partially signed v0 transactions), and X402.Scheme.UptoEVM
(upto on eip155:* networks via Permit2). Resolution — including
wildcard CAIP-2 matching and exact-match precedence — is handled by
X402.Scheme.Registry.
Callbacks and the roles they serve
| Callback | Role | Consulted by |
|---|---|---|
scheme/0 | metadata | X402.Scheme.Registry.resolve/3 |
networks/0 | metadata | X402.Scheme.Registry.resolve/3 |
signable?/1 | client | X402.Client.select_requirements/2 |
sign/3 | client | X402.Client.build_payment/3 |
validate_payload/3 | server | X402.PaymentSignature.validate/3 |
precheck/3 | server | X402.Plug.PaymentGate (:local_prechecks) |
Only scheme/0 and networks/0 are required. A scheme module
implements the callbacks for the roles it plays: a client-only scheme can
omit validate_payload/3 and precheck/3; a server-only scheme can
omit sign/3. Missing optional callbacks are
neutral — signing falls back to the {:unsupported_kind, scheme, network}
error, while validation and pre-checks pass through with :ok (the
facilitator remains the authority).
Adding a chain or scheme in one module
Implement the behaviour:
defmodule MyApp.CashScheme do
@behaviour X402.Scheme
@impl X402.Scheme
def scheme, do: "cash"
@impl X402.Scheme
def networks, do: ["local:*"]
# Client side: produce the scheme payload carried as
# PaymentPayload.payload.
@impl X402.Scheme
def sign(requirements, _signer, _opts) do
{:ok, %{"note" => "IOU " <> requirements["amount"]}}
end
# Server side: structural validation of the decoded payload.
@impl X402.Scheme
def validate_payload(payload, _requirements, _opts) do
case get_in(payload, ["payload", "note"]) do
note when is_binary(note) -> :ok
_missing -> {:error, {:invalid_scheme_payment, :missing_note}}
end
end
# Server side: cheap local pre-checks before the facilitator call.
@impl X402.Scheme
def precheck(payload, _requirements, _opts) do
case get_in(payload, ["payload", "counterfeit"]) do
true -> {:error, {:precheck_failed, :counterfeit_note}}
_other -> :ok
end
end
endThen pass it where you use the SDK — every entry point takes a :schemes
option (a list of modules consulted before the built-ins):
# Resource server: gate routes may now use scheme "cash".
plug X402.Plug.PaymentGate,
schemes: [MyApp.CashScheme],
routes: [
%{
method: :get,
path: "/paid",
scheme: "cash",
price: "5",
network: "local:test",
asset: "note",
pay_to: "till"
}
]
# Payer client: "cash" requirements become selectable and signable.
X402.Client.build_payment(payment_required, signer, schemes: [MyApp.CashScheme])
# Standalone header validation.
X402.PaymentSignature.validate(payload, requirements, schemes: [MyApp.CashScheme])There is no global registration and no application environment: scheme
modules are passed explicitly as options, so two gates (or two clients) in
the same VM can support different scheme sets. A user module listed in
:schemes is consulted before the built-ins and can override them — see
X402.Scheme.Registry for the exact precedence rules.
Error conventions
sign/3returns{:ok, scheme_payload}wherescheme_payloadis the map carried as the v2PaymentPayload.payload, or{:error, reason}.validate_payload/3failures should be{:error, {:invalid_scheme_payment, reason}}, whichX402.Plug.PaymentGateanswers with HTTP 400 Invalid Request (the built-inuptoscheme keeps its historical{:invalid_upto_payment, reason}tuples, mapped the same way). Unrecognized error shapes fail closed as HTTP 500.precheck/3failures should be{:error, {:precheck_failed, reason}}so the gate answers 402 without a facilitator round-trip. Pre-checks must only fail fast on certain mismatch — the facilitator remains the authority.
Summary
Types
A module implementing X402.Scheme.
Callbacks
The CAIP-2 network patterns this module supports.
Cheap local pre-checks run by X402.Plug.PaymentGate before the
facilitator round-trip.
The x402 scheme name this module implements (for example "exact").
Signs the scheme-specific payment payload for the given requirements.
Whether the client can sign this specific requirements entry.
Structurally validates a decoded PAYMENT-SIGNATURE payload.
Functions
Invokes precheck/3, defaulting to :ok when not implemented.
Invokes signable?/1, defaulting to true when not implemented.
Whether a scheme module implements the client-side sign/3 callback.
Validates that a term is a module implementing X402.Scheme.
Invokes validate_payload/3, defaulting to :ok when not implemented.
Types
@type t() :: module()
A module implementing X402.Scheme.
Callbacks
@callback networks() :: [String.t()]
The CAIP-2 network patterns this module supports.
A pattern is either an exact CAIP-2 identifier ("eip155:8453") or a
prefix wildcard ending in * ("eip155:*", or "*" for every network).
@callback precheck(payload :: map(), requirements :: map(), opts :: keyword()) :: :ok | {:error, term()}
Cheap local pre-checks run by X402.Plug.PaymentGate before the
facilitator round-trip.
Failures should be {:error, {:precheck_failed, reason}} (answered with
HTTP 402). Optional — when not implemented, the gate skips straight to
the facilitator.
@callback scheme() :: String.t()
The x402 scheme name this module implements (for example "exact").
@callback sign(requirements :: map(), signer :: X402.Signer.t(), opts :: keyword()) :: {:ok, map()} | {:error, term()}
Signs the scheme-specific payment payload for the given requirements.
Receives the requirements entry exactly as the server advertised it, the
X402.Signer, and the client's validated build options (schemes that use
options — such as :valid_after_buffer — should Keyword.take/2 what
they need). Returns the map carried as PaymentPayload.payload.
Optional — when not implemented, the scheme cannot be selected or signed
by X402.Client.
Whether the client can sign this specific requirements entry.
Called during X402.Client.select_requirements/2 after structural
validation, letting a scheme reject entries that are missing
scheme-specific data (for example EIP-712 domain fields in extra).
Optional — when not implemented, every entry for a matching
scheme/network is considered signable.
@callback validate_payload(payload :: map(), requirements :: map(), opts :: keyword()) :: :ok | {:error, term()}
Structurally validates a decoded PAYMENT-SIGNATURE payload.
Called by X402.PaymentSignature.validate/3 after envelope validation,
with the effective requirements (the caller-supplied requirements, or the
payload's own accepted object when none were given). Optional — when
not implemented, validation passes through with :ok.
Functions
Invokes precheck/3, defaulting to :ok when not implemented.
Examples
iex> X402.Scheme.precheck(X402.Scheme.ExactEVM, %{"payload" => %{}}, %{}, [])
:ok
Invokes signable?/1, defaulting to true when not implemented.
Examples
iex> X402.Scheme.signable?(X402.Scheme.UptoEVM, %{
...> "network" => "eip155:84532",
...> "extra" => %{"facilitatorAddress" => "0x2222222222222222222222222222222222222222"}
...> })
true
iex> X402.Scheme.signable?(X402.Scheme.ExactEVM, %{})
false
Whether a scheme module implements the client-side sign/3 callback.
Examples
iex> X402.Scheme.signs?(X402.Scheme.ExactEVM)
true
iex> X402.Scheme.signs?(X402.Scheme.UptoEVM)
true
Validates that a term is a module implementing X402.Scheme.
Returns {:ok, module} for use as a NimbleOptions {:custom, ...}
validator — this is what backs the :schemes option on
X402.Client.build_payment/3, X402.Plug.PaymentGate, and
X402.PaymentSignature.validate/3.
Examples
iex> X402.Scheme.validate_module(X402.Scheme.ExactEVM)
{:ok, X402.Scheme.ExactEVM}
iex> X402.Scheme.validate_module(:not_a_scheme)
{:error, "expected a module implementing X402.Scheme"}
Invokes validate_payload/3, defaulting to :ok when not implemented.
Examples
iex> X402.Scheme.validate_payload(X402.Scheme.ExactEVM, %{}, %{}, [])
:ok