X402.Plug.PaymentGate (X402 v0.9.0)

Copy Markdown View Source

Plug middleware that gates configured routes behind x402 v2 payment verification.

For matching routes:

  1. Requests without a PAYMENT-SIGNATURE header receive 402 with a Base64-encoded PAYMENT-REQUIRED header (PaymentRequired v2 schema).
  2. Requests with PAYMENT-SIGNATURE are decoded as PaymentPayload v2.
  3. PaymentPayload.accepted and echoed extensions are matched against the complete requirements advertised by the route.
  4. Cheap local pre-checks run for the matched scheme/network — resolved through X402.Scheme.Registry — so certain mismatches answer 402 without a facilitator round-trip. The built-in EVM schemes check the EIP-3009-style authorization object (payTo binding, exact amount equality, validity window); see the :local_prechecks option. Kinds with no registered scheme module skip straight to the facilitator.
  5. Matched requirements are verified before the protected handler runs — optionally preceded by inline local verification through X402.Verify.EVM (see the :local_verification option).
  6. Successful handler responses are settled immediately before they are sent. A settlement_pending settle failure that already carries a transaction hash is retried once, so the facilitator's pending store can reconcile — mirroring the reference resource servers. Successful settlements attach a PAYMENT-RESPONSE header and assign :x402_payment_payload / :x402_payment_requirements on the conn. When the facilitator reports extension outcomes through the EXTENSION-RESPONSES sidechannel (see X402.ExtensionResponses), the verify-time outcomes are assigned as :x402_extension_responses and settle-time outcomes are attached to the [:x402, :plug, :payment_verified] telemetry metadata; the sidechannel is never forwarded to the buyer.

HTTP status mapping (HTTP transport v2):

  • 402 — payment required, no matching requirements, or payment failed
  • 400 — malformed / invalid payment payload (including wrong x402Version)
  • 409 — a payment-identifier id reused for a different request
  • 429 — the payer exceeded the configured :rate_limit (with Retry-After)
  • 500 — facilitator transport failures or malformed facilitator responses

See the official x402 v2 specification and HTTP transport.

Route patterns and path parameters

A route :path is matched against the decoded request path in one of three ways:

  • an exact path (/api/report);
  • a * glob (/api/*), where * matches any run of characters;
  • a template with :param segments (/api/users/:id), where each parameter matches one non-empty path segment. The captured values are assigned as :x402_path_params (%{"id" => "42"}) on every gated request, paid or not.

Parameter matching preserves Plug's script_name ++ path_info segment boundaries while decoding each segment once. An encoded slash such as r%2F1 remains one captured value ("r/1"), not an extra path component.

The advertised resource.url is always the concrete request URL. With a :bazaar route option (a keyword list of X402.Extensions.Bazaar.build_extension/1 options), the 402 response additionally advertises the discovery extension under extensions["bazaar"]; for :param routes it carries the route's template as the top-level routeTemplate catalog key and the captured values as info.input.pathParams, per the bazaar spec:

%{
  method: :get,
  path: "/api/users/:id",
  price: "10000",
  ...,
  bazaar: [method: :get, output: [type: "json"]]
}

Dynamic pricing

The route fields :price, :pay_to, :description, and :accepts (as well as :price and :pay_to inside each :accepts entry) may be 1-arity functions of the Plug.Conn. They are evaluated on every gated request — once, before the 402 advertisement and before the client's accepted requirements are matched, so a single request always sees one set of terms:

%{
  method: :get,
  path: "/api/report/:format",
  price: fn conn ->
    if conn.assigns.x402_path_params["format"] == "pdf", do: "20000", else: "10000"
  end,
  network: "eip155:8453",
  asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
  pay_to: "0x1111111111111111111111111111111111111111"
}

The paying request evaluates the functions again on its own conn; the resulting requirements are what the client's echoed accepted must match. A function returns the plain value or {:ok, value}; returning {:error, reason} (or an invalid value) answers 500 and emits [:x402, :plug, :payment_rejected] with reason: {:dynamic_route_error, reason} — the reason never reaches the client. Dynamic values are validated like their static counterparts (atomic-unit amounts, known schemes, the authorization payment flow).

Lifecycle hooks

Beyond the facilitator hooks (X402.Hooks before_* / after_* / on_*_failure), the :hooks module may define two optional resource-server callbacks:

  • X402.Hooks.on_protected_request/2 runs for every request that matches a gated route, before any payment processing, with an X402.Hooks.RequestContext carrying the conn, the matched route, the resolved requirements, and the extensions about to be advertised. It may continue (optionally replacing requirements or extensions for this request — a per-caller discount, say), halt with {:halt, {status, body}} (the JSON-encoded body is sent as-is), or halt with {:halt, :skip_payment} to let the handler run unpaid, which emits [:x402, :plug, :pass_through] with reason: :hook_skipped.
  • X402.Hooks.on_verified_payment_canceled/2 runs when a payment the facilitator verified is not settled: the handler answered with a status of 400 or above (reason: :handler_failed, with :response_status), or settlement failed before a transaction was broadcast (reason: :settlement_failed, with :error). A handler that raises does not reach the gate's before-send callback; when an error handler renders a 500 for it the callback runs with :handler_failed.

Extension adapters

The :extensions option takes X402.Extension adapters — [module | {module, opts}] — that advertise, validate, and observe one protocol extension each:

plug X402.Plug.PaymentGate,
  routes: [...],
  extensions: [
    {X402.Extensions.PaymentIdentifier.Adapter, required: true},
    {X402.Extensions.BuilderCode.Adapter, app_code: "my_app"}
  ]

Adapter advertisements are merged over each route's static extensions map on every 402, validate/3 runs after the generic echo check (a failure answers 400 invalid_payload with reason {:extension_invalid, key, reason}), and after_verify/4 / after_settle/4 are notified with the facilitator's results. Sign-In-With-X keeps its dedicated :siwx option (see below) and bazaar discovery its :bazaar route option; both compose with adapters.

Builder code

Routes may advertise the builder-code extension — statically through X402.Extensions.BuilderCode.extension/2 or with the adapter above. Whether or not it is advertised, a payload that echoes extensions["builder-code"] is checked with X402.Extensions.BuilderCode.validate_echo/2: malformed codes and more than ten service codes answer 400 invalid_payload (reason {:invalid_builder_code, detail}), and an app code that differs from the advertised one is an :extension_echo_mismatch (400). The codes are then forwarded to the facilitator inside the payload, which encodes them into the settlement calldata.

Browser paywall

With paywall: X402.Paywall.Default (or any X402.Paywall implementation), pre-handler 402 responses to browser page loads — Accept containing text/html and User-Agent containing Mozilla, the heuristic shared by the reference x402 middlewares — carry a human-usable HTML page instead of the {} JSON body. The PAYMENT-REQUIRED header is identical on both forms and all other responses are unchanged. See the "Browser Paywall" guide.

Sign-In-With-X

With :siwx configured (a keyword list of X402.Extensions.SIWX.Server.new/1 options), the gate implements the sign-in-with-x extension so a wallet that already paid can skip payment:

plug X402.Plug.PaymentGate,
  routes: [...],
  siwx: [
    domain: "api.example.com",
    uri: "https://api.example.com",
    supported_chains: [%{chain_id: "eip155:8453"}],
    nonce_cache: MyApp.SIWXNonces,
    storage: {X402.Extensions.SIWX.ETSStorage, MyApp.SIWXStorage}
  ]
  • Every 402 response advertises a fresh challenge under extensions["sign-in-with-x"] (new nonce and timestamps each time). Because it changes per response it is exempt from the extension echo check.
  • A request carrying a SIGN-IN-WITH-X header is verified by X402.Extensions.SIWX.Server.authenticate/3 against the HTTP method and full resource URL. When the address has a payment record for that request the handler runs without payment, :x402_siwx_address and :x402_siwx_chain_id are assigned, and [:x402, :plug, :siwx_authenticated] is emitted. When it has none, the request proceeds through the normal payment flow if it also carries PAYMENT-SIGNATURE, and otherwise receives 402 with a fresh challenge. A proof that fails verification receives 402 with the spec's invalid_siwx_* code as the error string (telemetry reason: {:siwx, code}); a header that cannot be decoded receives 400 invalid_siwx_header.
  • After a successful settlement the payer (the settle response's payer, falling back to the authorization's from) is recorded for the method and resource URL through the configured :storage, for :ttl_ms.

Access keys have the form "GET https://api.example.com/resource?item=1". The full URL, including origin, port, raw path and query, remains part of the key: those fields can identify different paid resources. A GET payment never grants POST access, even when both match an :any route. Configure trusted proxy URL rewriting before this gate. Old URL-only records are not accepted as method-scoped grants.

The deprecated pre-0.7.0 {message, signature} header format is still accepted; each one emits [:x402, :siwx, :legacy] and the first logs a warning. Configure a :nonce_cache in production: without it a proof can be replayed within its issuedAt window.

Replay protection

When :payment_identifier_cache is configured, the gate claims a canonical replay key for the payment proof through the X402.Extensions.PaymentIdentifier.Cache behaviour before settling. The key is derived from signature-covered content, so re-encoding the same signed authorization (JSON key order, whitespace, Base64 variant) cannot mint a fresh key:

  • "exact" on eip155:* — the EIP-3009 authorization's from + nonce, or for the Permit2 transfer method (extra.assetTransferMethod "permit2") the Permit2 authorization's from + nonce
  • "upto" on eip155:* — the Permit2 authorization's from + nonce (Permit2 nonces are per owner across spenders, so an exact-Permit2 and an upto authorization sharing owner and nonce share one key)
  • "exact" on solana:* — the SHA-256 of the transaction's signed message bytes (immune to the mutable fee-payer signature slot)
  • everything else — the SHA-256 hash of the raw PAYMENT-SIGNATURE header, prefixed so families cannot collide. Re-encodings of the same proof are distinct keys here, exactly as before canonical keys existed.

The key is never derived from client-controlled unsigned fields — in particular not from the payment identifier extension's id: a replayer could vary it to mint a fresh key and bypass deduplication, or squat another payment's id to deny it service. Duplicate proofs are rejected with 402 and the claim is released when the protected handler responds with a status >= 400 or settlement fails, so clients may retry a payment whose resource was never delivered.

Payment identifier

Routes may advertise the payment-identifier extension through X402.Extensions.PaymentIdentifier.extension/1:

extensions: %{"payment-identifier" => X402.Extensions.PaymentIdentifier.extension(required: true)}

The id the client echoes under extensions["payment-identifier"]["info"]["id"] is validated (16–128 characters of [A-Za-z0-9_-], otherwise 400 invalid_payload), assigned as :x402_payment_id, and attached as :payment_id to the [:x402, :plug, :payment_verified] telemetry metadata. When the advertisement sets required: true and no id is echoed, the request is rejected with 400 payment_identifier_required.

With :payment_identifier_cache configured, the id is additionally bound to a request fingerprint (X402.Extensions.PaymentIdentifier.fingerprint/2 over the matched scheme, network, asset, amount, payTo, HTTP method, and path) under a "pid:"-prefixed cache key. Reusing an id for a request with a different fingerprint is rejected with 409 payment_identifier_conflict; the same id with the same fingerprint proceeds normally and remains subject to the signature-derived replay key above. A binding this request created is released together with the replay claim (handler status >= 400, settlement failure, verification failure), so the id may be retried. Without a cache the binding is skipped and only the validity and required checks apply.

The pre-0.7.0 "paymentIdentifier" format (a Base64 JSON {"paymentId": ...} string or a %{"paymentId" => ...} map, optionally wrapped in %{"info" => ...}) is still accepted, with the same assign, telemetry, and fingerprint binding, but is deprecated and removed in 1.0.0: each legacy id emits [:x402, :payment_identifier, :legacy] and the first one logs a warning. Legacy ids satisfy required: true and are not subject to the spec's length and character rules.

The :claim_order option controls when the claim is taken relative to facilitator verification:

  • :after_verify (default) — the claim is taken only after the facilitator has verified the proof. A replayed proof can never strand a claim through verification, but every replayed request pays a full facilitator verify round-trip before it is rejected, so a replay storm translates directly into facilitator load.
  • :before_verify — the claim is taken before contacting the facilitator and released again if verification fails for any reason. Duplicates are rejected locally without any facilitator call, which sheds replay-storm load. The trade-off: a node that crashes between claiming and releasing (now including the verify round-trip window) strands the claim until the cache TTL expires, so a legitimate retry of that same payment is rejected with 402 until then.

Both orderings keep the existing release semantics: the claim is released when the handler responds with a status >= 400 or settlement fails, and a duplicate claim is rejected with the same 402 duplicate-payment error.

Clustered deployments can serve one payment twice

The default X402.Extensions.PaymentIdentifier.ETSCache adapter is per-node: every node in a cluster keeps its own claim table, so a replayed proof load-balanced onto two nodes runs the protected handler on each of them even though only one settlement can ultimately succeed. If you deploy more than one node, configure a shared-store adapter instead — see the "Writing a distributed adapter" section in X402.Extensions.PaymentIdentifier.Cache for a Redis sketch.

Rate limiting

With :rate_limit configured, each verified payment counts one hit against its payer before settlement, bounding how many payments a wallet can spend on the gated routes per window:

plug X402.Plug.PaymentGate,
  routes: [...],
  rate_limit: [limit: 60, window_ms: 60_000]
OptionDefaultMeaning
:limitrequiredHits allowed per window
:window_msrequiredWindow length in milliseconds
:key:payer:payer, :ip, or a 1-arity function of the request context
:storeX402.RateLimiter.ETSX402.RateLimiter module or {module, ref}
:on_error:allow:allow or :deny when the store fails

The limiter runs after verification (local and facilitator) succeeded and before the resource handler and settlement. The payer of an unverified payload is whatever the sender typed, so counting it earlier would let anyone exhaust a victim wallet's allowance with forged proofs; enforcing after verification means only proofs the wallet actually signed count against it. The trade-off is that over-limit requests still cost a verify round-trip — pair the gate with an edge/IP limiter to shed unauthenticated floods. A denied payment is never settled and its replay claim is released, so the same proof can be retried once the window rolls over.

:payer keys on the payer the facilitator reported (payer in the verify response) or, failing that, the authorization.from / permit2Authorization.from of the verified payload (lower-cased for EVM addresses), and falls back to the remote IP when neither names one; a :key function receives %{conn: conn, payer: payer, payment_payload: payload, requirements: requirements} and may return nil to exempt the request. Allowed requests get an :x402_rate_limit assign (%{key: key, remaining: n, limit: limit, window_ms: window_ms}); denied requests answer 429 with the usual PAYMENT-REQUIRED header (error rate_limited), a Retry-After header in whole seconds (rounded up), and emit [:x402, :plug, :rate_limited] with :payer, :key, :retry_after_ms, :limit, :window_ms, and the route metadata. The default X402.RateLimiter.ETS store is per-node; see X402.RateLimiter for shared stores.

Summary

Types

Claim ordering relative to facilitator verification.

A route value that is either static or computed from the request.

Configuration map produced by init/1.

Functions

Gates matching requests behind x402 v2 payment verification.

Validates and compiles X402.Plug.PaymentGate options.

Stores the actual atomic amount to settle for an "upto" route.

Types

claim_order()

@type claim_order() :: :after_verify | :before_verify

Claim ordering relative to facilitator verification.

dynamic(value)

@type dynamic(value) ::
  value | (Plug.Conn.t() -> value | {:ok, value} | {:error, term()})

A route value that is either static or computed from the request.

options()

@type options() :: %{
  auth_capture: map() | nil,
  facilitator: X402.Facilitator.server(),
  hooks: module(),
  payment_identifier_cache:
    X402.Extensions.PaymentIdentifier.Cache.adapter() | nil,
  claim_order: claim_order(),
  routes: [compiled_route()],
  schemes: [module()],
  local_prechecks: boolean(),
  local_verification: keyword() | nil,
  paywall: module() | nil,
  siwx: X402.Extensions.SIWX.Server.t() | nil,
  extensions: [X402.Extension.spec()],
  rate_limit: X402.RateLimiter.config() | nil
}

Configuration map produced by init/1.

Functions

call(conn, opts)

(since 0.1.0)
@spec call(Plug.Conn.t(), options()) :: Plug.Conn.t()

Gates matching requests behind x402 v2 payment verification.

init(opts)

(since 0.1.0)
@spec init(keyword()) :: options()

Validates and compiles X402.Plug.PaymentGate options.

Options

  • :auth_capture - Local escrow execution, [resource: resource, handler: fn conn -> conn end]. The handler must set an actual amount with put_settlement_amount/2. Responses and callbacks are buffered. Streaming, files, upgrades and early hints are unsupported. Deferred results do not grant SIWX access. See the auth-capture guide for durable recovery and output limits. The default value is nil.

  • :facilitator (term/0) - Facilitator server pid/name used for verification and settlement. The default value is X402.Facilitator.

  • :hooks - Lifecycle hook module implementing X402.Hooks. The default value is X402.Hooks.Default.

  • :payment_identifier_cache - Optional idempotency cache used for replay protection. Accepts either an ETSCache server pid/name (the default adapter), or a {module, cache} adapter tuple where module implements X402.Extensions.PaymentIdentifier.Cache and cache is passed to its callbacks (for example {MyApp.RedisPaymentCache, MyApp.Redis}). When set, the plug performs an atomic claim (via the adapter's put_new/3) on the payment proof hash before settling, preventing concurrent requests from double-settling the same payment, and binds each echoed payment-identifier id to its request fingerprint (see "Payment identifier" above). The default ETS adapter is per-node — see the "Replay protection" section above for the clustering hazard. The default value is nil.

  • :claim_order - When the replay claim is taken relative to facilitator verification. :after_verify (default) never strands a claim on verification but pays a facilitator verify round-trip per replayed request; :before_verify rejects duplicates before contacting the facilitator (shedding replay-storm load) and releases the claim if verification fails, at the cost that a node crash during verification strands the claim until the cache TTL expires. Only meaningful when :payment_identifier_cache is configured. The default value is :after_verify.

  • :routes - Required. Route gate definitions (see route options below).

  • :schemes - Additional X402.Scheme modules consulted (before the built-ins) for scheme-specific payload validation and local pre-checks — see X402.Scheme.Registry. Routes may use the scheme names these modules declare. The default value is [].

  • :local_prechecks (boolean/0) - Run cheap local checks before calling the facilitator, dispatched to the X402.Scheme module matching the requirements' scheme/network. The built-in EVM schemes check the EIP-3009-style payload.authorization object: to must equal the route's pay_to, value must equal the advertised amount for "exact" routes, and the validAfter/validBefore window must cover now (with a 6s settlement buffer). Fields absent from the payload are skipped — as are kinds with no registered scheme module — so payloads with other shapes pass through untouched. Failures answer 402 without a facilitator round-trip. The default value is true.

  • :local_verification - Optional inline local verification through X402.Verify.EVM, run before the facilitator verify. Accepts a level atom (:structural, :signature, or :full — shorthand for [level: level]) or a keyword list with :level, :rpc (an X402.RPC struct, required for :full), :simulate, :verify_chain_id, :eip6492_allowed_factories, and :multicall_address — see X402.Verify.EVM.verify/3 for their semantics. Local verification only understands exact-EVM payments: it runs when the matched requirements have scheme "exact" and an eip155:* network, and is skipped silently for every other scheme/network combination — the facilitator remains the authority, and still verifies and settles payments local verification accepted. Verification failures answer 402 exactly like a facilitator rejection (carrying the canonical invalidReason string); infrastructure failures — missing crypto dependencies, RPC errors, chain-id mismatches — fail closed with 500. A configured level never silently downgrades. The default value is nil.

  • :paywall - Optional browser paywall renderer implementing X402.Paywall (X402.Paywall.Default ships a self-contained wallet-enabled page). When set, pre-handler 402 responses to requests that look like a browser page load — Accept header containing text/html and User-Agent containing Mozilla, mirroring the reference x402 middlewares — carry the rendered HTML body instead of the default {} JSON body. The PAYMENT-REQUIRED header is identical on both forms, and every other response (API clients, absent Accept headers, 400/500 statuses, post-handler settlement failures) is byte-identical to running without :paywall. The default value is nil.

  • :siwx - Optional Sign-In-With-X configuration, a keyword list of X402.Extensions.SIWX.Server.new/1 options (:domain, :uri, and :supported_chains are required). When set, every 402 response advertises a fresh challenge under extensions["sign-in-with-x"], requests carrying a SIGN-IN-WITH-X header are authenticated against it, and successful settlements record the payer so later proofs from that address skip payment — see "Sign-In-With-X" above. The default value is nil.

  • :extensions - X402.Extension adapters — module or {module, opts} — that advertise, validate, and observe protocol extensions on every gated request (for example X402.Extensions.PaymentIdentifier.Adapter and X402.Extensions.BuilderCode.Adapter). Advertisements are merged over each route's static :extensions map — see "Extension adapters" above. The default value is [].

  • :rate_limit - Optional per-wallet rate limit, a keyword list of X402.RateLimiter options: :limit and :window_ms (both required), :key (:payer — default — :ip, or a 1-arity function of the request context), :store (an X402.RateLimiter module or {module, ref}, default X402.RateLimiter.ETS), and :on_error. The limit is applied to verified payments only — after the facilitator verify round-trip and before settlement — so a forged payer cannot burn a victim's allowance; a denied request answers 429 with a Retry-After header and is never settled — see "Rate limiting" above. The default value is nil.

Route options

  • :method - Required. HTTP method for the route (:any matches all methods).

  • :path - Required. Route path: an exact path, a * glob (/api/*), or a template with :param segments (/api/users/:id) whose captured values are assigned as :x402_path_params.

  • :accepts - Payment options advertised in PAYMENT-REQUIRED.accepts, or a 1-arity function of the Plug.Conn returning them. When empty, a single option is built from the top-level :scheme, :price, :network, :asset, and :pay_to fields. The default value is [].

  • :scheme (String.t/0) - Single-option scheme (used when :accepts is empty): exact, upto, or the scheme name of a module passed in the plug's :schemes option. The default value is "exact".

  • :price - Single-option amount (required when :accepts is empty), or a 1-arity function of the Plug.Conn returning one.

  • :network (String.t/0) - Single-option CAIP-2 network (required when :accepts is empty).

  • :asset (String.t/0) - Single-option asset (required when :accepts is empty).

  • :pay_to - Single-option payTo (required when :accepts is empty), or a 1-arity function of the Plug.Conn returning one.

  • :description - ResourceInfo.description, or a 1-arity function of the Plug.Conn returning it. The default value is "Payment required".

  • :mime_type (String.t/0) - ResourceInfo.mimeType. The default value is "application/json".

  • :service_name - ResourceInfo.serviceName: non-empty printable ASCII, at most 32 characters (X402.Extensions.Bazaar.Metadata.valid_service_name?/1). The default value is nil.

  • :tags - ResourceInfo.tags: at most 5 unique (case-insensitive) entries, each non-empty printable ASCII of at most 32 characters (X402.Extensions.Bazaar.Metadata.sanitize_tags/1). The default value is [].

  • :icon_url - ResourceInfo.iconUrl: absolute http(s) URL, no userinfo, not an IP literal or loopback host, at most 2048 characters (X402.Extensions.Bazaar.Metadata.valid_icon_url?/1). The default value is nil.

  • :max_timeout_seconds (pos_integer/0) - Default maxTimeoutSeconds for single-option routes. The default value is 60.

  • :extra - Default extra map for single-option routes. The default value is %{}.

  • :extensions - Protocol extensions advertised in PaymentRequired.extensions. The default value is %{}.

  • :bazaar - X402.Extensions.Bazaar.build_extension/1 options advertising the route in the bazaar. For :param routes the extension carries the path as routeTemplate and the captured values as info.input.pathParams; globs cannot be advertised. The default value is nil.

Accept option fields (inside :accepts)

  • :scheme (String.t/0) - Payment scheme (exact, upto, or the scheme name of a module passed in the plug's :schemes option). The default value is "exact".

  • :price - Required. Payment amount in atomic token units (PaymentRequirements amount), or a 1-arity function of the Plug.Conn returning one. For exact this is the required amount; for upto it is the maximum authorized amount.

  • :network (String.t/0) - Required. Blockchain network in CAIP-2 format (for example eip155:84532).

  • :asset (String.t/0) - Required. Token contract address or asset identifier.

  • :pay_to - Required. Recipient wallet address (payTo in the PaymentRequirements schema), or a 1-arity function of the Plug.Conn returning one.

  • :max_timeout_seconds (pos_integer/0) - Maximum time allowed for payment completion. The default value is 60.

  • :extra - Scheme-specific extra fields (string or atom keys). The default value is %{}.

put_settlement_amount(conn, amount)

(since 0.4.0)
@spec put_settlement_amount(Plug.Conn.t(), String.t() | non_neg_integer()) ::
  {:ok, Plug.Conn.t()} | {:error, :invalid_settlement_amount}

Stores the actual atomic amount to settle for an "upto" route.

Call this from the protected handler after resource consumption is known. When omitted, the route's advertised maximum is settled.

Examples

iex> conn = Plug.Test.conn(:get, "/paid")
iex> {:ok, conn} = X402.Plug.PaymentGate.put_settlement_amount(conn, "7500")
iex> conn.private[:x402_settlement_amount]
"7500"

iex> X402.Plug.PaymentGate.put_settlement_amount(Plug.Test.conn(:get, "/paid"), "1.5")
{:error, :invalid_settlement_amount}