X402.Hooks behaviour (X402 v0.9.0)

Copy Markdown View Source

Behaviour for lifecycle hooks around facilitator verify and settle operations.

Hooks run around each operation in this order:

  1. before_verify/2 or before_settle/2
  2. after_verify/2 or after_settle/2 on success
  3. on_verify_failure/2 or on_settle_failure/2 on 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/2 runs for every request that matches a gated route (or paid tool) before any payment processing, with an X402.Hooks.RequestContext. It may continue with {:cont, context} — optionally replacing context.requirements or context.extensions for 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/2 runs 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.

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.

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.

Validates that a value is a module implementing X402.Hooks.

Types

after_result()

@type after_result() :: {:cont, X402.Hooks.Context.t()}

Return type for after_* callbacks.

before_result()

@type before_result() :: {:cont, X402.Hooks.Context.t()} | {:halt, term()}

Return type for before_* callbacks.

callback_name()

@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.

cancel_metadata()

@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.

cancellation_reason()

@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 an isError result (MCP); :response_status carries the HTTP status.
  • :handler_raised — the MCP handler raised, threw, or exited; :error carries {kind, reason}.
  • :settlement_failed — settlement failed before a transaction was broadcast; :error carries the failure reason.

hook_error()

@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.

metadata()

@type metadata() :: %{
  operation: :verify | :settle,
  endpoint: String.t(),
  hook_module: module()
}

Lifecycle callback metadata passed to hooks.

on_failure_result()

@type on_failure_result() :: {:cont, X402.Hooks.Context.t()} | {:recover, map()}

Return type for on_*_failure callbacks.

protected_request_result()

@type protected_request_result() ::
  {:cont, X402.Hooks.RequestContext.t()}
  | {:halt, {pos_integer(), map()}}
  | {:halt, :skip_payment}

Return type for on_protected_request/2.

request_metadata()

@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

after_settle(t, metadata)

@callback after_settle(X402.Hooks.Context.t(), metadata()) :: after_result()

Runs after a successful settle request.

after_verify(t, metadata)

@callback after_verify(X402.Hooks.Context.t(), metadata()) :: after_result()

Runs after a successful verify request.

before_settle(t, metadata)

@callback before_settle(X402.Hooks.Context.t(), metadata()) :: before_result()

Runs before a settle request is sent.

before_verify(t, metadata)

@callback before_verify(X402.Hooks.Context.t(), metadata()) :: before_result()

Runs before a verify request is sent.

on_protected_request(t, request_metadata)

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

on_settle_failure(t, metadata)

@callback on_settle_failure(X402.Hooks.Context.t(), metadata()) :: on_failure_result()

Runs after a failed settle request.

on_verified_payment_canceled(t, cancel_metadata)

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

on_verify_failure(t, metadata)

@callback on_verify_failure(X402.Hooks.Context.t(), metadata()) :: on_failure_result()

Runs after a failed verify request.

Functions

run_protected_request(hooks_module, context, metadata)

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

run_verified_payment_canceled(hooks_module, context, metadata)

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

validate_module(module)

(since 0.1.0)
@spec validate_module(term()) :: {:ok, term()} | {:error, String.t()}

Validates that a value is a module implementing X402.Hooks.

This function is designed for NimbleOptions custom validation.