Transport-agnostic payer client for x402 v2 payments.
Pure functions covering the client half of the protocol: selecting a
payment option from a server's PAYMENT-REQUIRED response, signing it
through the X402.Signer behaviour, assembling the v2 PaymentPayload
envelope, and encoding it to a PAYMENT-SIGNATURE header value. Bring your
own HTTP client, or use X402.Client.Finch for a ready-made
402 → sign → retry flow.
Signing dispatches through X402.Scheme.Registry: out of the box this
client signs the exact scheme on EVM (eip155:*) networks via EIP-3009
(X402.Scheme.ExactEVM), on Solana (solana:*) networks via a
partially signed v0 transaction (X402.Scheme.ExactSVM), and the
upto scheme on EVM networks via Permit2 (X402.Scheme.UptoEVM);
other scheme/network combinations return
{:error, {:unsupported_kind, scheme, network}} unless a matching
X402.Scheme module is passed with the :schemes option.
Example
{:ok, signer} = X402.Signer.LocalKey.new(System.fetch_env!("PAYER_KEY"))
with {:ok, payment_required} <- X402.PaymentRequired.decode(header_value),
{:ok, payload} <- X402.Client.build_payment(payment_required, signer),
{:ok, header} <- X402.Client.encode_payment(payload) do
# retry the request with {"payment-signature", header}
end
Summary
Functions
Builds a complete v2 PaymentPayload for a payment-required response.
Encodes a PaymentPayload map to a PAYMENT-SIGNATURE header value.
Selects one payment requirements entry from a PAYMENT-REQUIRED payload.
Types
@type build_error() :: select_error() | {:unsupported_kind, term(), term()} | X402.EIP3009.domain_error() | X402.EIP3009.encode_error() | term()
@type select_error() :: :no_acceptable_requirements | :invalid_payment_required
@type select_opts() :: [ network: String.t(), scheme: String.t(), asset: String.t(), max_amount: String.t() | non_neg_integer(), schemes: [module()] ]
Selection options — see select_requirements/2.
Functions
@spec build_payment(map() | [map()], X402.Signer.t(), keyword()) :: {:ok, map()} | {:error, build_error()}
Builds a complete v2 PaymentPayload for a payment-required response.
Accepts either a decoded PaymentRequired map — in which case one entry is
chosen via select_requirements/2 and the server's resource and
extensions are echoed — or a single requirements map, which skips
selection (and carries no resource/extensions echo).
The chosen requirements are echoed in full (including extra) as
accepted, and server-advertised extensions are echoed unchanged,
following the spec's append-only rule: the client must preserve every
advertised value and may only add to them. :extensions enrichers run
after assembly and may add extension data on top of the echo — for
example X402.Extensions.EIP2612GasSponsoring.enricher/2 for
gas-sponsored Permit2 approvals.
Signing dispatches on the scheme and network of the chosen requirements
through X402.Scheme.Registry; out of the box this supports exact on
eip155:* networks (EIP-3009) and on solana:* networks (partially
signed v0 transactions), plus upto on eip155:* networks (Permit2).
Other combinations return
{:error, {:unsupported_kind, scheme, network}} unless a matching
module is passed with the :schemes option. Scheme modules receive the
validated build options, so options like :valid_after_buffer (EVM) and
:svm_blockhash (SVM) reach X402.Scheme.sign/3.
Options
:network(String.t/0) - Only select requirements on this CAIP-2 network. A trailing*acts as a prefix wildcard (for example"eip155:*").:scheme(String.t/0) - Only select requirements using this scheme (for example"exact").:asset(String.t/0) - Only select requirements paying with this asset (compared case-insensitively).:max_amount- Only select requirements whoseamount(in atomic units) does not exceed this value — the budget guard for automated payers.:schemes- AdditionalX402.Schememodules consulted (before the built-ins) when deciding which requirements this client can sign and how to sign them — seeX402.Scheme.Registry. The default value is[].:valid_after_buffer(non_neg_integer/0) - Seconds subtracted from the current time for the EVM authorization'svalidAfter(clock-skew tolerance). The default value is60.:extensions(list of function of arity 2) - Client extension enrichers applied, in order, to the assembled payload. Each function receives the payload and the originalPaymentRequiredmap (nilwhen building from a bare requirements map) and returns{:ok, payload}or{:error, reason}— seeX402.Extensions.EIP2612GasSponsoring.enricher/2andX402.Extensions.ERC20ApprovalGasSponsoring.enricher/1. The default value is[].:svm_blockhash(String.t/0) - Base58 recent blockhash for SVM (Solana) payments, used when the server'sextra.recentBlockhashhint is absent — seeX402.Scheme.ExactSVM.:svm_blockhash_fetcher(function of arity 1) - 1-arity fun receiving the CAIP-2 network and returning{:ok, blockhash}for SVM payments (for example a wrapper around an RPC client'sgetLatestBlockhash).:svm_decimals(non_neg_integer/0) - The SVM asset's decimals forTransferChecked, for mints outsideX402.Scheme.ExactSVM's known-asset table.:svm_token_program(String.t/0) - The SVM asset's owning token program (SPL Token or Token-2022 address), for mints outsideX402.Scheme.ExactSVM's known-asset table.
Encodes a PaymentPayload map to a PAYMENT-SIGNATURE header value.
The header value is Base64-encoded JSON, compatible with
X402.PaymentSignature.decode/1 on the validation side.
Examples
iex> payload = %{"x402Version" => 2, "accepted" => %{"scheme" => "exact"}, "payload" => %{}}
iex> {:ok, header} = X402.Client.encode_payment(payload)
iex> X402.PaymentSignature.decode(header)
{:ok, payload}
iex> X402.Client.encode_payment(nil)
{:error, :invalid_payload}
@spec select_requirements(map() | [map()], select_opts()) :: {:ok, map()} | {:error, select_error()}
Selects one payment requirements entry from a PAYMENT-REQUIRED payload.
Accepts a decoded PaymentRequired map (its accepts list is used) or a
bare list of requirements maps. Returns the first entry that passes the
option filters and that this client can sign: structurally valid per
X402.PaymentRequirements.validate/1 and resolved by
X402.Scheme.Registry to a scheme module with a sign callback — by
default exact on an eip155:* network via X402.Scheme.ExactEVM
(which additionally requires the EIP-712 domain fields extra.name /
extra.version) and upto on an eip155:* network via
X402.Scheme.UptoEVM (which requires extra.facilitatorAddress). Pass
additional schemes with the :schemes option.
The selected entry is returned exactly as the server sent it, so it can be
echoed verbatim as the payload's accepted value.
Options
:network(String.t/0) - Only select requirements on this CAIP-2 network. A trailing*acts as a prefix wildcard (for example"eip155:*").:scheme(String.t/0) - Only select requirements using this scheme (for example"exact").:asset(String.t/0) - Only select requirements paying with this asset (compared case-insensitively).:max_amount- Only select requirements whoseamount(in atomic units) does not exceed this value — the budget guard for automated payers.:schemes- AdditionalX402.Schememodules consulted (before the built-ins) when deciding which requirements this client can sign and how to sign them — seeX402.Scheme.Registry. The default value is[].
Examples
iex> requirements = %{
...> "scheme" => "exact",
...> "network" => "eip155:84532",
...> "amount" => "10000",
...> "asset" => "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
...> "payTo" => "0x209693Bc6afc0C5328bA36FaF03C514EF312287C",
...> "maxTimeoutSeconds" => 60,
...> "extra" => %{"name" => "USDC", "version" => "2"}
...> }
iex> {:ok, selected} =
...> X402.Client.select_requirements(%{"x402Version" => 2, "accepts" => [requirements]})
iex> selected == requirements
true
iex> X402.Client.select_requirements(%{"x402Version" => 2, "accepts" => []})
{:error, :no_acceptable_requirements}