X402.Client (X402 v0.6.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()
  | {:unsupported_kind, term(), term()}
  | X402.EIP3009.domain_error()
  | X402.EIP3009.encode_error()
  | term()

select_error()

@type select_error() :: :no_acceptable_requirements | :invalid_payment_required

select_opts()

@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

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.

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.

  • :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 [].

  • :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.

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.

  • :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}