X402.AuthCapture (X402 v0.9.0)

Copy Markdown View Source

The auth-capture payment scheme: flows, operations, deadlines, lifecycle payloads, and reason strings.

auth-capture adds refundable, two-phase payments to x402 on top of Base's commerce-payments escrow. The client signs one token authorization; how it settles follows extra.paymentFlow:

  • "escrow" (default) — the collect is an authorize that holds the funds; the resource server later relays capture, void, or refund lifecycle payloads through the facilitator.
  • "authorization" — the collect is a terminal charge the server completes with an amount, fee, and authorizer signature; only refund follows.

This module is chain-agnostic in shape but the only binding today is EVM (X402.AuthCapture.EVM, X402.Scheme.AuthCaptureEVM, X402.Verify.AuthCaptureEVM), so the lifecycle builders here produce the EVM binding's wire payloads.

Lifecycle payloads

A resource server authors lifecycle settles from the stored PaymentInfo and client saltNonce:

{:ok, capture} =
  X402.AuthCapture.capture_payload(requirements, payment_info,
    salt_nonce: salt_nonce,
    amount: "750000",
    expected_capturable_amount: "1000000",
    expected_refundable_amount: "0",
    authorizer: receiver_authorizer_signer
  )

These builders do not broadcast or persist the operation. Submit the resulting envelope through an auth-capture-capable facilitator only after authenticating the request and establishing any refund funding agreement. An explicit :authorizer signer is required; implicit receiver-authorizer delegation is not supported.

Reason strings

Every reason this scheme defines is namespaced invalid_auth_capture_evm_*; standard x402 reasons keep their canonical names. See reason_string/1.

Summary

Types

When the post-resource finalize of an escrow route runs.

The escrow call a payload settles as.

The kind of extra.captureAuthorizer.

The settlement lifecycle a requirements entry selects.

A reason atom this scheme reports (see reason_string/1).

deadlines

Checks a client authorization's time window against the capture deadline: validAfter <= now, validBefore > now + skew, and validBefore <= captureDeadline.

Checks the requirements' deadline ordering: now + maxTimeoutSeconds <= captureDeadline <= refundDeadline and captureDeadline > now + skew.

lifecycle

Builds a capture lifecycle payload (v2 envelope) for the facilitator.

The token collector a deployment uses for an asset transfer method.

Completes a client payment payload for a charge settle.

Builds a refund lifecycle payload (v2 envelope) for the facilitator.

Builds a void lifecycle payload (v2 envelope) for the facilitator.

reasons

Converts a reason atom to its canonical wire string.

Every reason atom this scheme reports, standard ones first.

requirements

Resolves extra.assetTransferMethod (default "eip3009").

Resolves extra.captureMode (default "sync"), which is only meaningful under the escrow flow.

Whether the payload is a lifecycle payload (carries payload.type).

Resolves the escrow operation a payload settles as.

Resolves extra.operatorType (default "delegated").

Resolves extra.paymentFlow (default "escrow").

The scheme identifier.

The clock-skew floor (seconds) the deadline checks apply.

Types

capture_mode()

@type capture_mode() :: :sync | :deferred

When the post-resource finalize of an escrow route runs.

operation()

@type operation() :: :authorize | :charge | :capture | :void | :refund

The escrow call a payload settles as.

operator_type()

@type operator_type() :: :delegated | :custom | :policy

The kind of extra.captureAuthorizer.

payment_flow()

@type payment_flow() :: :escrow | :authorization

The settlement lifecycle a requirements entry selects.

reason()

@type reason() :: atom()

A reason atom this scheme reports (see reason_string/1).

deadlines

check_authorization_window(valid_after, valid_before, capture_deadline, opts \\ [])

(since 0.9.0)
@spec check_authorization_window(term(), term(), term(), keyword()) ::
  :ok
  | {:error,
     :authorization_expired
     | :authorization_not_yet_valid
     | :deadline_ordering
     | :payload_format
     | {:invalid_options, String.t()}}

Checks a client authorization's time window against the capture deadline: validAfter <= now, validBefore > now + skew, and validBefore <= captureDeadline.

valid_after is nil for Permit2 (which has no lower bound). The :now and :skew_seconds options must be non-negative integers; the skew is never lowered below six seconds.

Examples

iex> X402.AuthCapture.check_authorization_window("0", "1600", 2_000, now: 1_000)
:ok

iex> X402.AuthCapture.check_authorization_window("0", "1005", 2_000, now: 1_000)
{:error, :authorization_expired}

iex> X402.AuthCapture.check_authorization_window("0", "2500", 2_000, now: 1_000)
{:error, :deadline_ordering}

iex> X402.AuthCapture.check_authorization_window("1100", "1600", 2_000, now: 1_000)
{:error, :authorization_not_yet_valid}

check_deadlines(requirements, opts \\ [])

(since 0.9.0)
@spec check_deadlines(
  map(),
  keyword()
) ::
  :ok
  | {:error,
     :deadline_ordering
     | :capture_deadline_expired
     | :extra
     | {:invalid_options, String.t()}}

Checks the requirements' deadline ordering: now + maxTimeoutSeconds <= captureDeadline <= refundDeadline and captureDeadline > now + skew.

Options

  • :now — Unix seconds (default: current time)
  • :skew_seconds — clock-skew floor (default 6)

Examples

iex> requirements = %{
...>   "maxTimeoutSeconds" => 600,
...>   "extra" => %{"captureDeadline" => 2_000, "refundDeadline" => 3_000}
...> }
iex> X402.AuthCapture.check_deadlines(requirements, now: 1_000)
:ok

iex> X402.AuthCapture.check_deadlines(
...>   %{"maxTimeoutSeconds" => 600, "extra" => %{"captureDeadline" => 3_000, "refundDeadline" => 2_000}},
...>   now: 1_000
...> )
{:error, :deadline_ordering}

iex> X402.AuthCapture.check_deadlines(
...>   %{"maxTimeoutSeconds" => 1_500, "extra" => %{"captureDeadline" => 2_000, "refundDeadline" => 3_000}},
...>   now: 1_000
...> )
{:error, :deadline_ordering}

iex> X402.AuthCapture.check_deadlines(
...>   %{"maxTimeoutSeconds" => 0, "extra" => %{"captureDeadline" => 1_005, "refundDeadline" => 3_000}},
...>   now: 1_000
...> )
{:error, :capture_deadline_expired}

lifecycle

capture_payload(requirements, payment_info, opts)

(since 0.9.0)
@spec capture_payload(map(), map(), keyword()) :: {:ok, map()} | {:error, term()}

Builds a capture lifecycle payload (v2 envelope) for the facilitator.

Options

  • :salt_nonce (String.t/0) - Required. The client's saltNonce (32-byte 0x hex) that reopens the salt binding.

  • :amount - Atomic amount to capture or refund (required for those operations).

  • :fee - Submitted fee for capture: feeAmount (atomic units) on v1.1 or feeBps on v1.0. Defaults to the deployment's minimum (amount * minFeeBps / 10000, or minFeeBps).

  • :fee_receiver (String.t/0) - Submitted fee receiver for capture. Defaults to paymentInfo.feeReceiver.

  • :expected_capturable_amount - The capturableAmount the authorizer expects to find (capture and refund).

  • :expected_refundable_amount - The refundableAmount the authorizer expects to find (capture and refund).

  • :authorizer (term/0) - Required. The receiver authorizer's X402.Signer, required for explicit consent.

  • :void_remainder (boolean/0) - For capture: also sign a Void so the same settle releases the remaining hold (sync partial close-out). Requires :authorizer. The default value is false.

:amount, :expected_capturable_amount, and :expected_refundable_amount are required.

collector(deployment, atom)

(since 0.9.0)
@spec collector(X402.AuthCapture.EVM.deployment(), :eip3009 | :permit2) :: String.t()

The token collector a deployment uses for an asset transfer method.

Examples

iex> deployment = X402.AuthCapture.EVM.deployment(:v1_1)
iex> X402.AuthCapture.collector(deployment, :permit2)
"0xD69831Aed5bfe262067ec4c751f4F830EcdD446e"

complete_charge(envelope, opts \\ [])

(since 0.9.0)
@spec complete_charge(
  map(),
  keyword()
) :: {:ok, map()} | {:error, term()}

Completes a client payment payload for a charge settle.

Appends amount, the deployment's fee field, feeReceiver, and an explicit authorizerSignature over exactly those values, leaving the client's fields untouched. envelope is the v2 envelope whose accepted requirements select the deployment.

Options

  • :amount - Amount to charge, at most requirements.amount (the default).

  • :fee - Submitted fee (feeAmount on v1.1, feeBps on v1.0); defaults to the minimum.

  • :fee_receiver (String.t/0) - Submitted fee receiver; defaults to extra.feeRecipient.

  • :authorizer (term/0) - Required. The receiver authorizer's X402.Signer, required for explicit consent.

refund_payload(requirements, payment_info, opts)

(since 0.9.0)
@spec refund_payload(map(), map(), keyword()) :: {:ok, map()} | {:error, term()}

Builds a refund lifecycle payload (v2 envelope) for the facilitator.

Requires :salt_nonce, :amount, :expected_capturable_amount, and :expected_refundable_amount; the token collector is always the deployment's operator refund collector and is not carried on the wire.

void_payload(requirements, payment_info, opts)

(since 0.9.0)
@spec void_payload(map(), map(), keyword()) :: {:ok, map()} | {:error, term()}

Builds a void lifecycle payload (v2 envelope) for the facilitator.

Accepts :salt_nonce and :authorizer from the lifecycle options.

reasons

reason_string(reason)

(since 0.9.0)
@spec reason_string(reason()) :: String.t()

Converts a reason atom to its canonical wire string.

Standard x402 reasons keep their name; everything else is namespaced invalid_auth_capture_evm_*.

Examples

iex> X402.AuthCapture.reason_string(:nonce_mismatch)
"invalid_auth_capture_evm_nonce_mismatch"

iex> X402.AuthCapture.reason_string(:invalid_network)
"invalid_network"

reasons()

(since 0.9.0)
@spec reasons() :: [reason()]

Every reason atom this scheme reports, standard ones first.

requirements

asset_transfer_method(requirements)

(since 0.9.0)
@spec asset_transfer_method(map()) ::
  {:ok, :eip3009 | :permit2} | {:error, :unsupported_asset_transfer_method}

Resolves extra.assetTransferMethod (default "eip3009").

Examples

iex> X402.AuthCapture.asset_transfer_method(%{"extra" => %{}})
{:ok, :eip3009}

iex> X402.AuthCapture.asset_transfer_method(%{"extra" => %{"assetTransferMethod" => "permit2"}})
{:ok, :permit2}

iex> X402.AuthCapture.asset_transfer_method(%{"extra" => %{"assetTransferMethod" => "eip2612"}})
{:error, :unsupported_asset_transfer_method}

capture_mode(requirements)

(since 0.9.0)
@spec capture_mode(map()) :: {:ok, capture_mode()} | {:error, :extra}

Resolves extra.captureMode (default "sync"), which is only meaningful under the escrow flow.

Examples

iex> X402.AuthCapture.capture_mode(%{"extra" => %{}})
{:ok, :sync}

iex> X402.AuthCapture.capture_mode(%{"extra" => %{"captureMode" => "deferred"}})
{:ok, :deferred}

iex> X402.AuthCapture.capture_mode(%{
...>   "extra" => %{"paymentFlow" => "authorization", "captureMode" => "sync"}
...> })
{:error, :extra}

lifecycle?(payload)

(since 0.9.0)
@spec lifecycle?(map()) :: boolean()

Whether the payload is a lifecycle payload (carries payload.type).

Examples

iex> X402.AuthCapture.lifecycle?(%{"payload" => %{"type" => "capture"}})
true

iex> X402.AuthCapture.lifecycle?(%{"payload" => %{"authorization" => %{}}})
false

operation(payload, requirements)

(since 0.9.0)
@spec operation(map(), map()) ::
  {:ok, operation()} | {:error, :payload_type | :unsupported_payment_flow}

Resolves the escrow operation a payload settles as.

Client payloads settle as :authorize (escrow flow) or :charge (authorization flow); lifecycle payloads name their payload.type, with capture and void admitted only under the escrow flow.

Examples

iex> X402.AuthCapture.operation(%{"payload" => %{"authorization" => %{}}}, %{"extra" => %{}})
{:ok, :authorize}

iex> X402.AuthCapture.operation(
...>   %{"payload" => %{"authorization" => %{}}},
...>   %{"extra" => %{"paymentFlow" => "authorization"}}
...> )
{:ok, :charge}

iex> X402.AuthCapture.operation(%{"payload" => %{"type" => "refund"}}, %{"extra" => %{}})
{:ok, :refund}

iex> X402.AuthCapture.operation(
...>   %{"payload" => %{"type" => "capture"}},
...>   %{"extra" => %{"paymentFlow" => "authorization"}}
...> )
{:error, :payload_type}

iex> X402.AuthCapture.operation(%{"payload" => %{"type" => "settle"}}, %{"extra" => %{}})
{:error, :payload_type}

operator_type(requirements)

(since 0.9.0)
@spec operator_type(map()) ::
  {:ok, operator_type()} | {:error, :unsupported_operator_type}

Resolves extra.operatorType (default "delegated").

Examples

iex> X402.AuthCapture.operator_type(%{"extra" => %{}})
{:ok, :delegated}

iex> X402.AuthCapture.operator_type(%{"extra" => %{"operatorType" => "custom"}})
{:ok, :custom}

iex> X402.AuthCapture.operator_type(%{"extra" => %{"operatorType" => "dao"}})
{:error, :unsupported_operator_type}

payment_flow(requirements)

(since 0.9.0)
@spec payment_flow(map()) ::
  {:ok, payment_flow()} | {:error, :unsupported_payment_flow}

Resolves extra.paymentFlow (default "escrow").

The removed v1.0 autoCapture: true is rejected as unsupported.

Examples

iex> X402.AuthCapture.payment_flow(%{"extra" => %{}})
{:ok, :escrow}

iex> X402.AuthCapture.payment_flow(%{"extra" => %{"paymentFlow" => "authorization"}})
{:ok, :authorization}

iex> X402.AuthCapture.payment_flow(%{"extra" => %{"autoCapture" => true}})
{:error, :unsupported_payment_flow}

iex> X402.AuthCapture.payment_flow(%{"extra" => %{"paymentFlow" => "instant"}})
{:error, :unsupported_payment_flow}

scheme()

(since 0.9.0)
@spec scheme() :: String.t()

The scheme identifier.

Examples

iex> X402.AuthCapture.scheme()
"auth-capture"

skew_seconds()

(since 0.9.0)
@spec skew_seconds() :: pos_integer()

The clock-skew floor (seconds) the deadline checks apply.

Examples

iex> X402.AuthCapture.skew_seconds()
6