Server half of the x402 MCP transport: gates a tool handler behind payment.
call/3 inspects an MCP tool-call request for a payment in
_meta["x402/payment"] and drives the full verify → execute → settle flow
against an X402.Facilitator:
- No payment → the payment-required tool result (
isError: truewith thePaymentRequiredobject instructuredContentandcontent[0].text). - Invalid payment (wrong version,
acceptednot matching the advertised requirements, extension echo mismatch, failed verification) → the same payment-required result with the rejection reason. - Valid payment → the wrapped handler runs; on success the payment is
settled and the receipt is attached to result
_meta["x402/payment-response"]. When settlement fails after execution, only the payment error is returned — never the tool's content.
The module is MCP-library agnostic: requests and results are plain maps in the shapes MCP libraries already use, so the wrapper drops into any tool dispatch function. See the MCP guide for integration snippets.
Example
config =
X402.MCP.Server.init(
tool: "premium_search",
accepts: [
%{
price: "10000",
network: "eip155:84532",
asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
pay_to: "0x209693Bc6afc0C5328bA36FaF03C514EF312287C",
extra: %{"name" => "USDC", "version" => "2"}
}
],
facilitator: MyApp.Facilitator
)
X402.MCP.Server.call(request, config, fn _request ->
%{"content" => [%{"type" => "text", "text" => "results..."}]}
end)Replay protection
Pass payment_identifier_cache: (an
X402.Extensions.PaymentIdentifier.ETSCache server, the same option
X402.Plug.PaymentGate takes) to atomically claim each payment proof before
settlement, rejecting concurrent or repeated submissions of the same signed
payment. The claim is released when the handler fails or settlement fails,
so the client may retry with the same payment.
Payment identifier
Advertise the
payment-identifier extension
with extensions: %{"payment-identifier" => X402.Extensions.PaymentIdentifier.extension(required: true)}.
The id echoed under extensions["payment-identifier"]["info"]["id"] must
be 16–128 characters of [A-Za-z0-9_-] (otherwise the payment-required
result carries invalid_payload); with required: true a missing id
yields payment_identifier_required. When a cache is configured, the id
is bound to a fingerprint of the matched requirements and the tool name
(X402.Extensions.PaymentIdentifier.fingerprint/2) under a "pid:" key:
reusing it for a different request yields payment_identifier_conflict,
the same request proceeds normally. The pre-0.7.0 "paymentIdentifier"
format is still accepted but deprecated (removed in 1.0.0) and emits
[:x402, :payment_identifier, :legacy].
Builder code
A payment echoing the
builder-code extension
— whether or not extensions advertises it with
X402.Extensions.BuilderCode.extension/2 — is checked with
X402.Extensions.BuilderCode.validate_echo/2: malformed codes or more
than ten service codes yield invalid_payload, and an app code that
differs from the advertised one is an extension echo mismatch. The codes
travel to the facilitator inside the payload.
Lifecycle hooks
The :hooks module may define the optional resource-server callbacks of
X402.Hooks. X402.Hooks.on_protected_request/2 runs on every call
with an X402.Hooks.RequestContext (transport: :mcp, the request, the
tool name, and the advertised requirements and extensions) before the
payment is inspected: it may continue with replaced requirements or
extensions, halt with {:halt, {status, body}} (answered as an
isError result carrying body in structuredContent), or halt with
{:halt, :skip_payment} to run the handler unpaid (emitting
[:x402, :mcp, :pass_through] with reason: :hook_skipped).
X402.Hooks.on_verified_payment_canceled/2 runs when a verified
payment is not settled: the handler returned an error result
(reason: :handler_failed), the handler raised or threw
(reason: :handler_raised, with :error), or settlement failed
(reason: :settlement_failed, with :error).
Summary
Types
An MCP tool-call handler: request params in, tool result map out.
Functions
Gates an MCP tool-call request behind x402 payment verification.
Validates and compiles paid-tool options.
Builds the payment-required tool result advertised by this configuration.
Types
@type handler() :: (map() -> map() | {:ok, map(), non_neg_integer()} | {:error, term()})
An MCP tool-call handler: request params in, tool result map out.
@type options() :: %{ auth_capture_resource: X402.AuthCapture.Resource.t() | nil, tool: String.t(), facilitator: X402.Facilitator.server(), hooks: module(), payment_identifier_cache: X402.Extensions.PaymentIdentifier.ETSCache.server() | nil, accepts: [map()], resource: map(), extensions: map() }
Configuration map produced by init/1.
Functions
Gates an MCP tool-call request behind x402 payment verification.
request is the tool-call params map (typically with "name",
"arguments", and "_meta" keys). handler receives the request and must
return a tool result map (with a "content" list and optional
"isError"); it only runs after the payment has been verified.
Always returns a tool result map:
- the payment-required result when payment is missing, invalid, rejected by the facilitator, or already settled (replay)
- the handler's result with the settlement receipt attached to
_meta["x402/payment-response"]on success - the handler's error result unchanged (no settlement) when the handler
sets
"isError" => true - the settlement-failure result (payment-required format, without the tool's content) when settlement fails after execution
- an opaque internal error result when the facilitator transport fails
If the handler raises, the replay claim is released and the exception is re-raised for the MCP library to surface.
Validates and compiles paid-tool options.
Raises NimbleOptions.ValidationError for invalid options and
ArgumentError when :accepts is empty, an accept's extra.paymentFlow
is not "authorization", or the advertised data cannot be encoded as JSON.
Options
:auth_capture_resource- Local escrow resource. Paid handlers return{:ok, result_map, actual_amount}or{:error, reason}. Content is withheld until sync settlement or durable deferred metering. Recovery is application-only through the resource. The default value isnil.:tool(String.t/0) - Required. Tool name; used for the defaultmcp://tool/{tool}resource URL.:accepts(list ofmap/0) - Required. Payment options advertised inPaymentRequired.accepts(at least one).:facilitator(term/0) - Facilitator server pid/name used for verification and settlement. The default value isX402.Facilitator.:hooks- Lifecycle hook module implementingX402.Hooks. The default value isX402.Hooks.Default.:payment_identifier_cache- Optional idempotency cache: anETSCacheserver pid/name (the default adapter), or a{module, cache}adapter tuple implementingX402.Extensions.PaymentIdentifier.Cache. When set, the wrapper performs an atomic claim (viaput_new) on a hash of the signed scheme payload before settling, preventing concurrent requests from double-settling the same payment. The default value isnil.:resource_url- Custom ResourceInfo.url (defaults tomcp://tool/{tool}). The default value isnil.:description- ResourceInfo.description (defaults toTool: {tool}). The default value isnil.:mime_type(String.t/0) - ResourceInfo.mimeType. The default value is"application/json".:service_name- ResourceInfo.serviceName (printable ASCII, max 32 characters recommended). The default value isnil.:tags(list ofString.t/0) - ResourceInfo.tags (max 5 recommended). The default value is[].:icon_url- ResourceInfo.iconUrl (absolute http(s) URL). The default value isnil.:extensions- Protocol extensions advertised in PaymentRequired.extensions. The default value is%{}.
Accept option fields (inside :accepts)
:scheme- Payment scheme (exactorupto). The default value is"exact".:price- Required. Payment amount in atomic token units (PaymentRequirementsamount).:network(String.t/0) - Required. Blockchain network in CAIP-2 format (for exampleeip155:84532).:asset(String.t/0) - Required. Token contract address or asset identifier.:pay_to(String.t/0) - Required. Recipient wallet address (payToin the PaymentRequirements schema).:max_timeout_seconds(pos_integer/0) - Maximum time allowed for payment completion. The default value is60.:extra- Scheme-specific extra fields (string or atom keys). The default value is%{}.
Builds the payment-required tool result advertised by this configuration.
Useful for advertising the price of a paid tool outside call/3 (for
example in a tools/list response or documentation).