Behaviour for lifecycle hooks around facilitator verify and settle operations.
Hooks run around each operation in this order:
before_verify/2orbefore_settle/2after_verify/2orafter_settle/2on successon_verify_failure/2oron_settle_failure/2on failure
before_* callbacks can continue with {:cont, context} or abort with
{:halt, reason}.
on_*_failure callbacks can continue failure handling with {:cont, context}
or recover the operation with {:recover, result}.
Hook callbacks are invoked in the process that calls
X402.Facilitator.verify/2 or X402.Facilitator.settle/2 (for example a
Plug request process), not in the facilitator process. Hooks that touch
process-bound state should account for running concurrently across callers.
Resource-server hooks
Two further callbacks are optional and are only invoked by the
resource-server transports (X402.Plug.PaymentGate and X402.MCP.Server)
when the hook module defines them. They mirror the reference resource
server's onProtectedRequest and onVerifiedPaymentCanceled hooks:
on_protected_request/2runs for every request that matches a gated route (or paid tool) before any payment processing, with anX402.Hooks.RequestContext. It may continue with{:cont, context}— optionally replacingcontext.requirementsorcontext.extensionsfor this request, for example to apply a per-caller discount — answer the request directly with{:halt, {status, body}}, or let the protected handler run unpaid with{:halt, :skip_payment}(an allowlisted API key, for instance). An exception or an unexpected return value fails closed: the transport answers with an internal error.on_verified_payment_canceled/2runs when a payment the facilitator already verified is not settled: the protected handler answered with a status of 400 or above (or, for MCP, returned an error result or raised), or settlement failed before a transaction was broadcast. Its return value is ignored and exceptions are caught and logged, so it is the place for compensating side effects (undo a quota increment, log a refundable authorization).
X402.Hooks.Default implements neither, so existing hook modules keep
working unchanged.
Summary
Types
Return type for after_* callbacks.
Return type for before_* callbacks.
Hook callback identifier.
Metadata passed to on_verified_payment_canceled/2.
Why a verified payment was not settled.
Hook execution error tuple returned by X402.Facilitator.
Lifecycle callback metadata passed to hooks.
Return type for on_*_failure callbacks.
Return type for on_protected_request/2.
Metadata passed to the resource-server request hooks.
Callbacks
Runs after a successful settle request.
Runs after a successful verify request.
Runs before a settle request is sent.
Runs before a verify request is sent.
Runs before any payment processing for a request that matches a gated route or paid tool. Optional.
Runs after a failed settle request.
Runs when a verified payment is not settled. Optional; the return value is ignored.
Runs after a failed verify request.
Functions
Invokes the optional on_protected_request/2 callback of a hook module.
Invokes the optional on_verified_payment_canceled/2 callback of a hook
module.
Validates that a value is a module implementing X402.Hooks.
Types
@type after_result() :: {:cont, X402.Hooks.Context.t()}
Return type for after_* callbacks.
@type before_result() :: {:cont, X402.Hooks.Context.t()} | {:halt, term()}
Return type for before_* callbacks.
@type callback_name() ::
:before_verify
| :after_verify
| :on_verify_failure
| :before_settle
| :after_settle
| :on_settle_failure
| :on_protected_request
| :on_verified_payment_canceled
Hook callback identifier.
@type cancel_metadata() :: %{ :transport => X402.Hooks.RequestContext.transport(), :hook_module => module(), :reason => cancellation_reason(), optional(:error) => term(), optional(:response_status) => pos_integer(), optional(:method) => atom(), optional(:path) => String.t(), optional(:route) => String.t(), optional(:tool) => String.t() }
Metadata passed to on_verified_payment_canceled/2.
@type cancellation_reason() :: :handler_failed | :handler_raised | :settlement_failed
Why a verified payment was not settled.
:handler_failed— the protected handler answered with a status of 400 or above (HTTP) or returned anisErrorresult (MCP);:response_statuscarries the HTTP status.:handler_raised— the MCP handler raised, threw, or exited;:errorcarries{kind, reason}.:settlement_failed— settlement failed before a transaction was broadcast;:errorcarries the failure reason.
@type hook_error() :: {:hook_halted, callback_name(), term()} | {:hook_callback_failed, callback_name(), term()} | {:hook_invalid_return, callback_name(), term()}
Hook execution error tuple returned by X402.Facilitator.
Lifecycle callback metadata passed to hooks.
@type on_failure_result() :: {:cont, X402.Hooks.Context.t()} | {:recover, map()}
Return type for on_*_failure callbacks.
@type protected_request_result() :: {:cont, X402.Hooks.RequestContext.t()} | {:halt, {pos_integer(), map()}} | {:halt, :skip_payment}
Return type for on_protected_request/2.
@type request_metadata() :: %{ :transport => X402.Hooks.RequestContext.transport(), :hook_module => module(), optional(:method) => atom(), optional(:path) => String.t(), optional(:route) => String.t(), optional(:tool) => String.t() }
Metadata passed to the resource-server request hooks.
Callbacks
@callback after_settle(X402.Hooks.Context.t(), metadata()) :: after_result()
Runs after a successful settle request.
@callback after_verify(X402.Hooks.Context.t(), metadata()) :: after_result()
Runs after a successful verify request.
@callback before_settle(X402.Hooks.Context.t(), metadata()) :: before_result()
Runs before a settle request is sent.
@callback before_verify(X402.Hooks.Context.t(), metadata()) :: before_result()
Runs before a verify request is sent.
@callback on_protected_request(X402.Hooks.RequestContext.t(), request_metadata()) :: protected_request_result()
Runs before any payment processing for a request that matches a gated route or paid tool. Optional.
@callback on_settle_failure(X402.Hooks.Context.t(), metadata()) :: on_failure_result()
Runs after a failed settle request.
@callback on_verified_payment_canceled(X402.Hooks.RequestContext.t(), cancel_metadata()) :: term()
Runs when a verified payment is not settled. Optional; the return value is ignored.
@callback on_verify_failure(X402.Hooks.Context.t(), metadata()) :: on_failure_result()
Runs after a failed verify request.
Functions
@spec run_protected_request(module(), X402.Hooks.RequestContext.t(), map()) :: protected_request_result() | {:error, hook_error()}
Invokes the optional on_protected_request/2 callback of a hook module.
Returns {:cont, context} untouched when the module does not define the
callback. A callback that raises, or returns anything other than a
protected_request_result/0 with a valid replacement context (see
X402.Hooks.RequestContext.valid?/1), yields a hook_error/0 so the
transport can fail closed. The :transport and :hook_module metadata
keys are filled in from the context and the module.
Examples
iex> context = X402.Hooks.RequestContext.new(requirements: [%{"scheme" => "exact"}])
iex> X402.Hooks.run_protected_request(X402.Hooks.Default, context, %{path: "/api"})
{:cont, context}
@spec run_verified_payment_canceled(module(), X402.Hooks.RequestContext.t(), map()) :: :ok
Invokes the optional on_verified_payment_canceled/2 callback of a hook
module.
A no-op when the module does not define the callback. The return value is
discarded; exceptions are caught and logged so a failing hook never masks
the response already being sent. The :transport and :hook_module
metadata keys are filled in from the context and the module; the caller
supplies :reason (a cancellation_reason/0) and its details.
Examples
iex> context = X402.Hooks.RequestContext.new(transport: :mcp, tool: "search")
iex> metadata = %{reason: :handler_failed, tool: "search"}
iex> X402.Hooks.run_verified_payment_canceled(X402.Hooks.Default, context, metadata)
:ok
Validates that a value is a module implementing X402.Hooks.
This function is designed for NimbleOptions custom validation.