X402.MCP (X402 v0.6.0)

Copy Markdown View Source

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:

  1. A paid tool called without payment returns a tool result with "isError" => true whose "structuredContent" (and JSON-encoded content[0].text) carry the PaymentRequired object.
  2. The client retries the tool call with the signed PaymentPayload in request params _meta["x402/payment"].
  3. 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 :status and :paid or :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

fetch_payment(request)

(since 0.6.0)
@spec fetch_payment(map()) :: {:ok, map()} | :error

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

fetch_payment_required(result)

(since 0.6.0)
@spec fetch_payment_required(map()) :: {:ok, map()} | :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

fetch_payment_required_from_error(error)

(since 0.6.0)
@spec fetch_payment_required_from_error(term()) :: {:ok, map()} | :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

fetch_payment_response(result)

(since 0.6.0)
@spec fetch_payment_response(map()) :: {:ok, map()} | :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

payment_meta_key()

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

payment_required_result(payment_required)

(since 0.6.0)
@spec payment_required_result(map()) ::
  {:ok, map()} | {:error, :invalid_payment_required}

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}

payment_response_meta_key()

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

put_payment(request, payment_payload)

(since 0.6.0)
@spec put_payment(map(), map()) :: map()

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" => %{}}}
}

put_payment_response(result, settle_response)

(since 0.6.0)
@spec put_payment_response(map(), map()) :: map()

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