X402.Client (X402 v0.9.0)

Copy Markdown View Source

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

build_error()

@type build_error() ::
  select_error()
  | X402.Client.Hooks.hook_error()
  | {:unsupported_kind, term(), term()}
  | X402.EIP3009.domain_error()
  | X402.EIP3009.encode_error()
  | term()

select_error()

@type select_error() ::
  :no_acceptable_requirements
  | :invalid_payment_required
  | {:invalid_policy_result, term()}
  | term()

select_opts()

@type select_opts() :: [
  network: String.t(),
  scheme: String.t(),
  asset: String.t(),
  max_amount: String.t() | non_neg_integer(),
  policies: [X402.Client.Policy.t()],
  schemes: [module()]
]

Selection options — see select_requirements/2.

Functions

build_payment(payment_required_or_requirements, signer, opts \\ [])

(since 0.6.0)
@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.

Lifecycle hooks

The :hooks module (X402.Client.Hooks, default X402.Client.Hooks.Default) runs before_payment/2 once requirements are selected — it may replace them or halt — after_payment/2 once the payload is built — it may replace the payload — and on_payment_failure/2 when signing or enrichment fails — it may replace the error or recover with a payload. Selection failures return before any hook runs.

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 whose amount (in atomic units) does not exceed this value — the budget guard for automated payers.

  • :policies (list of function of arity 2) - Selection policies, each a function of the candidate requirements and the PaymentRequired map (nil for a bare list) returning true to accept, false to skip the entry, or {:error, reason} to abort selection with that error. Every policy must accept an entry for it to be selected — see X402.Client.Policy for ready-made ones. The default value is [].

  • :schemes - Additional X402.Scheme modules consulted (before the built-ins) when deciding which requirements this client can sign and how to sign them — see X402.Scheme.Registry. The default value is [].

  • :auth_capture - Auth-capture signing options (:now, :salt, :salt_nonce). The default value is [].

  • :hooks - Module implementing X402.Client.Hooks, run around payment creation. The default value is X402.Client.Hooks.Default.

  • :valid_after_buffer (non_neg_integer/0) - Seconds subtracted from the current time for the EVM authorization's validAfter (clock-skew tolerance). The default value is 60.

  • :extensions (list of function of arity 2) - Client extension enrichers applied, in order, to the assembled payload. Each function receives the payload and the original PaymentRequired map (nil when building from a bare requirements map) and returns {:ok, payload} or {:error, reason} — see X402.Extensions.EIP2612GasSponsoring.enricher/2 and X402.Extensions.ERC20ApprovalGasSponsoring.enricher/1. The default value is [].

  • :svm_blockhash (String.t/0) - Base58 recent blockhash for SVM (Solana) payments, used when the server's extra.recentBlockhash hint is absent — see X402.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's getLatestBlockhash).

  • :svm_decimals (non_neg_integer/0) - The SVM asset's decimals for TransferChecked, for mints outside X402.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 outside X402.Scheme.ExactSVM's known-asset table.

encode_payment(payload)

(since 0.6.0)
@spec encode_payment(map()) ::
  {:ok, String.t()} | {:error, :invalid_payload | :invalid_json}

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}

select_requirements(payment_required, opts \\ [])

(since 0.6.0)
@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.

Entries whose extra.paymentFlow names a flow this client does not recognize are skipped: the protocol requires clients never to construct a payment for an unknown flow (spec §6.1). Only the default "authorization" flow (explicit or omitted) is recognized; upfront and escrow entries settle before the resource executes and are not selected.

:policies run last, on the signable candidates in order: the first entry every policy accepts is selected. A policy returning {:error, reason} aborts with {:error, reason} (for example a budget policy refusing to continue); any other non-boolean return aborts with {:error, {:invalid_policy_result, value}}.

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 whose amount (in atomic units) does not exceed this value — the budget guard for automated payers.

  • :policies (list of function of arity 2) - Selection policies, each a function of the candidate requirements and the PaymentRequired map (nil for a bare list) returning true to accept, false to skip the entry, or {:error, reason} to abort selection with that error. Every policy must accept an entry for it to be selected — see X402.Client.Policy for ready-made ones. The default value is [].

  • :schemes - Additional X402.Scheme modules consulted (before the built-ins) when deciding which requirements this client can sign and how to sign them — see X402.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}

iex> requirements = %{
...>   "scheme" => "exact",
...>   "network" => "eip155:84532",
...>   "amount" => "10000",
...>   "asset" => "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
...>   "payTo" => "0x209693Bc6afc0C5328bA36FaF03C514EF312287C",
...>   "maxTimeoutSeconds" => 60,
...>   "extra" => %{"name" => "USDC", "version" => "2"}
...> }
iex> X402.Client.select_requirements([requirements], policies: [X402.Client.Policy.max_amount("100")])
{:error, :no_acceptable_requirements}