X402.MCP.Client (X402 v0.9.0)

Copy Markdown View Source

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:

  1. Perform the tool call. A result that is not payment-required is returned as-is.
  2. Extract the PaymentRequired object from the payment-required tool result (or from a 402/-32042 JSON-RPC error).
  3. With :siwx configured and a sign-in-with-x challenge advertised, validate its domain and exact URI pins, obtain :on_payment_required consent, 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.
  4. A fresh payment-required result needs fresh consent before either its SIWX proof or payment is signed. Without SIWX, obtain consent before payment.
  5. 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"].
  6. 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

Types

A tool-call function driven by call/3.

A completed tool call.

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

call_error()

@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()

call_fun()

@type call_fun() :: (map() -> map() | {:ok, map()} | {:error, term()})

A tool-call function driven by call/3.

response()

@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

build_payment_meta(payment_required_or_result, signer, opts \\ [])

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

call(request, call_fun, opts)

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