Client half of the x402 MCP transport: pay for tool calls automatically.
call/3 drives an arbitrary tool-call function (any MCP client library, or
a plain function in tests) through the x402 detect → sign → retry-once loop:
- Perform the tool call. A result that is not payment-required is returned as-is.
- Extract the
PaymentRequiredobject from the payment-required tool result (or from a402/-32042JSON-RPC error). - With
:siwxconfigured and asign-in-with-xchallenge advertised, validate its domain and exact URI pins, obtain:on_payment_requiredconsent, then sign it and retry the call with the proof in_meta["x402/sign-in-with-x"]and no payment; a result that is not payment-required is returned as-is. - A fresh payment-required result needs fresh consent before either its SIWX proof or payment is signed. Without SIWX, obtain consent before payment.
- Build and sign a payment via
X402.Client.build_payment/3, reserve its amount against:budget(when given), and retry the tool call once with the payload in request_meta["x402/payment"]. - Return the retried result with the decoded settlement receipt from
result
_meta["x402/payment-response"], when present.
A tool call is never paid twice: at most one payment retry is made, a
second payment-required result is returned as-is, and requests that already
carry _meta["x402/payment"] are refused.
Example
{:ok, signer} = X402.Signer.LocalKey.new(System.fetch_env!("PAYER_KEY"))
request = %{"name" => "premium_search", "arguments" => %{"query" => "x402"}}
{:ok, %{result: result, payment_response: receipt, paid: true}} =
X402.MCP.Client.call(request, &MyMCP.call_tool/1,
signer: signer,
max_amount: "10000",
on_payment_required: fn payment_required ->
IO.inspect(payment_required["accepts"], label: "about to pay")
:ok
end
)The tool-call function receives the (possibly payment-carrying) request map
and may return the tool result map directly, {:ok, result}, or
{:error, reason}.
Spend controls
Cap what an automated payer signs with :max_amount (per payment),
:policies (X402.Client.Policy filters), and :budget (an
X402.Client.Budget shared across calls; the reservation is released
when the paid retry comes back as another payment-required result
without a successful receipt). With none of them configured a warning is
logged once.
Sign-In-With-X
With siwx: [chain_id: :auto, domain: "mcp.example.com", uri: "https://mcp.example.com/tools/premium_search"] the client
answers a sign-in-with-x challenge advertised in the payment-required
result before paying: the tool call is retried with the proof in
_meta["x402/sign-in-with-x"] (see X402.MCP.put_siwx/2) and no
payment. A result that is not payment-required is returned with
siwx_authenticated: true; another payment-required result continues
with the payment flow, the paid call carrying a proof for the new
challenge. The callback does not expose a verifiable transport origin, so
both :domain and an exact HTTP(S) :uri pin are required. Missing or
mismatched pins fail with :domain_mismatch / :uri_mismatch before consent,
wallet signing, or retry. Never derive these pins from the challenge.
The caller must bind call_fun to the independently trusted server (for
example, a fixed HTTPS endpoint or trusted local process). Pins constrain
the signed audience; they cannot authenticate an arbitrary callback or
prevent a malicious transport from relaying challenges for that same audience.
:on_payment_required can veto each challenge before either kind of signature.
Summary
Functions
Builds the request _meta map paying for a payment-required response.
Performs a tool call, paying for the tool if it requires payment.
Types
@type call_error() :: :payment_already_attempted | :payment_cancelled | :invalid_tool_result | {:transport_error, term()} | {:siwx, X402.Client.SIWX.reason()} | X402.Client.Budget.reserve_error() | X402.Client.build_error()
A tool-call function driven by call/3.
@type response() :: %{ result: map(), payment_response: map() | nil, paid: boolean(), siwx_authenticated: boolean() }
A completed tool call.
:result is the final tool result map; :payment_response holds the
decoded settlement receipt from _meta["x402/payment-response"] when the
server sent one, otherwise nil; :paid tells whether a payment was
signed and submitted; :siwx_authenticated tells whether the server
accepted a Sign-In-With-X proof instead of a payment.
Functions
@spec build_payment_meta(map(), X402.Signer.t(), keyword()) :: {:ok, %{required(String.t()) => map()}} | {:error, X402.Client.build_error()}
Builds the request _meta map paying for a payment-required response.
Accepts either a decoded PaymentRequired map or the payment-required tool
result that carries one, selects and signs a payment option via
X402.Client.build_payment/3, and returns the _meta entries to merge into
the retried tool-call request. Options are forwarded to
X402.Client.build_payment/3.
Use this instead of call/3 when your MCP library exposes request _meta
but you want to drive the retry yourself.
@spec call(map(), call_fun(), keyword()) :: {:ok, response()} | {:error, call_error()}
Performs a tool call, paying for the tool if it requires payment.
See the module documentation for the full flow. Returns {:ok, response()}
with the final tool result, or {:error, reason} when the payment was
cancelled, could not be built, or the tool-call function failed.
Options
:signer- Required. A struct implementingX402.Signer, used to sign the payment.:network(String.t/0) - Payment selection filter — seeX402.Client.select_requirements/2.:scheme(String.t/0) - Payment selection filter — seeX402.Client.select_requirements/2.:asset(String.t/0) - Payment selection filter — seeX402.Client.select_requirements/2.:max_amount- Maximumamount(atomic units) this client will pay — the budget guard for automated payers. Requirements above it are never selected.:policies(list of function of arity 2) - Selection policies forwarded toX402.Client.select_requirements/2— seeX402.Client.Policy. The default value is[].:budget- AnX402.Client.Budgetthe selected amount is reserved against before the paid retry is made. SeeX402.Client.Budgetfor what counts as spent. The default value isnil.:hooks-X402.Client.Hooksmodule forwarded toX402.Client.build_payment/3. The default value isX402.Client.Hooks.Default.:siwx- Automatic Sign-In-With-X: a keyword list ofX402.Client.SIWXoptions (chain_id:required, or:auto;domain:anduri:required when a challenge is advertised).nil/falsedisables it. The default value isnil.:valid_after_buffer(non_neg_integer/0) - Clock-skew buffer for the authorization'svalidAfter, in seconds. The default value is60.:auth_capture- Auth-capture signing options forwarded toX402.Client.build_payment/3. The default value is[].:on_payment_required- Budget/consent hook invoked with the decodedPaymentRequiredmap once per challenge before any SIWX proof or payment is signed. Return:cancelto abort with{:error, :payment_cancelled}; any other return value continues. The default value isnil.