x402 v2 payment flows over the Model Context Protocol (MCP transport).
This module implements the x402 MCP transport as pure functions over plain MCP tool-call request/result maps, so it works with any Elixir MCP library (or none). The payment data rides in three well-known places:
- A paid tool called without payment returns a tool result with
"isError" => truewhose"structuredContent"(and JSON-encodedcontent[0].text) carry thePaymentRequiredobject. - The client retries the tool call with the signed
PaymentPayloadin request params_meta["x402/payment"]. - The server verifies, runs the tool, settles, and attaches the settlement
receipt to result
_meta["x402/payment-response"].
X402.MCP.Server wraps a tool handler with verify → execute → settle;
X402.MCP.Client drives an arbitrary tool-call function through the
detect → sign → retry-once loop. The helpers here are shared by both halves
and useful on their own when wiring a concrete MCP library.
All functions accept maps with string keys (as decoded from JSON) or atom
keys, but the x402 _meta entries themselves always use the spec's string
keys "x402/payment" and "x402/payment-response".
Telemetry
The MCP transport emits the following events, each with %{count: 1}
measurements:
[:x402, :mcp, :payment_required]— server advertised payment requirements[:x402, :mcp, :payment_verified]— server verified and settled a payment[:x402, :mcp, :payment_rejected]— server rejected a payment (metadata includes:reason)[:x402, :mcp, :call]— client drove a tool call (metadata includes:statusand:paidor:reason)
Summary
Functions
Fetches the PaymentPayload from a tool-call request's _meta.
Fetches the PaymentRequired object from a payment-required tool result.
Fetches the PaymentRequired object from a JSON-RPC error.
Fetches the settlement receipt from a tool result's _meta.
Returns the request _meta key carrying the client's PaymentPayload.
Builds the payment-required tool result for a PaymentRequired object.
Returns the result _meta key carrying the server's settlement receipt.
Attaches a PaymentPayload to a tool-call request's _meta.
Attaches a settlement receipt to a tool result's _meta.
Functions
Fetches the PaymentPayload from a tool-call request's _meta.
Performs the same minimal structural check as the upstream SDKs — the value
must be a map with x402Version and payload — full validation happens
during verification.
Examples
iex> payment = %{"x402Version" => 2, "accepted" => %{}, "payload" => %{}}
iex> request = %{"name" => "search", "_meta" => %{"x402/payment" => payment}}
iex> X402.MCP.fetch_payment(request)
{:ok, payment}
iex> X402.MCP.fetch_payment(%{"name" => "search"})
:error
Fetches the PaymentRequired object from a payment-required tool result.
Per the MCP transport spec, the result must have isError: true.
structuredContent is preferred; content[0].text is parsed as JSON when
structured content is absent. Returns :error for any other tool result.
Examples
iex> payment_required = %{"x402Version" => 2, "error" => "Payment required", "accepts" => []}
iex> result = %{"isError" => true, "structuredContent" => payment_required, "content" => []}
iex> X402.MCP.fetch_payment_required(result)
{:ok, payment_required}
iex> X402.MCP.fetch_payment_required(%{"content" => [%{"type" => "text", "text" => "hi"}]})
:error
Fetches the PaymentRequired object from a JSON-RPC error.
Some MCP stacks surface payment challenges as JSON-RPC errors instead of
tool results: code 402 (legacy x402) carries PaymentRequired directly in
data, and code -32042 (SEP-1036 UrlElicitationRequired, which MCP
designates for payment/elicitation flows) carries it in data or namespaced
under data.x402.
Examples
iex> payment_required = %{"x402Version" => 2, "accepts" => []}
iex> error = %{"code" => 402, "message" => "Payment required", "data" => payment_required}
iex> X402.MCP.fetch_payment_required_from_error(error)
{:ok, payment_required}
iex> error = %{
...> "code" => -32042,
...> "message" => "Elicitation required",
...> "data" => %{"x402" => %{"x402Version" => 2, "accepts" => []}}
...> }
iex> X402.MCP.fetch_payment_required_from_error(error)
{:ok, %{"x402Version" => 2, "accepts" => []}}
iex> X402.MCP.fetch_payment_required_from_error(%{"code" => -32600})
:error
Fetches the settlement receipt from a tool result's _meta.
The receipt must be a map containing success (the SettlementResponse
schema).
Examples
iex> receipt = %{"success" => true, "transaction" => "0xabc", "network" => "eip155:84532"}
iex> result = %{"content" => [], "_meta" => %{"x402/payment-response" => receipt}}
iex> X402.MCP.fetch_payment_response(result)
{:ok, receipt}
iex> X402.MCP.fetch_payment_response(%{"content" => []})
:error
@spec payment_meta_key() :: String.t()
Returns the request _meta key carrying the client's PaymentPayload.
Examples
iex> X402.MCP.payment_meta_key()
"x402/payment"
Builds the payment-required tool result for a PaymentRequired object.
Per the MCP transport spec the object is provided in both formats:
structuredContent carries it directly and content[0].text carries the
same object JSON-encoded, with isError: true.
Returns {:error, :invalid_payment_required} when the map lacks the
x402Version/accepts structure or cannot be encoded as JSON.
Examples
iex> payment_required = %{"x402Version" => 2, "error" => "Payment required", "accepts" => []}
iex> {:ok, result} = X402.MCP.payment_required_result(payment_required)
iex> {result["isError"], result["structuredContent"] == payment_required}
{true, true}
iex> [%{"type" => "text", "text" => text}] = result["content"]
iex> Jason.decode!(text) == payment_required
true
iex> X402.MCP.payment_required_result(%{"accepts" => []})
{:error, :invalid_payment_required}
@spec payment_response_meta_key() :: String.t()
Returns the result _meta key carrying the server's settlement receipt.
Examples
iex> X402.MCP.payment_response_meta_key()
"x402/payment-response"
Attaches a PaymentPayload to a tool-call request's _meta.
Existing _meta entries are preserved; when the request uses an atom
:_meta key it is kept (avoiding a duplicate key on JSON encoding).
Examples
iex> request = %{"name" => "search", "arguments" => %{"q" => "x402"}}
iex> X402.MCP.put_payment(request, %{"x402Version" => 2, "payload" => %{}})
%{
"name" => "search",
"arguments" => %{"q" => "x402"},
"_meta" => %{"x402/payment" => %{"x402Version" => 2, "payload" => %{}}}
}
Attaches a settlement receipt to a tool result's _meta.
Examples
iex> result = %{"content" => [%{"type" => "text", "text" => "ok"}]}
iex> X402.MCP.put_payment_response(result, %{"success" => true})
%{
"content" => [%{"type" => "text", "text" => "ok"}],
"_meta" => %{"x402/payment-response" => %{"success" => true}}
}