# `X402.Plug.PaymentGate`
[🔗](https://github.com/cardotrejos/x402/blob/v0.9.0/lib/x402/plug/payment_gate.ex#L2)

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](https://github.com/x402-foundation/x402/blob/main/specs/x402-specification-v2.md)
and
[HTTP transport](https://github.com/x402-foundation/x402/blob/main/specs/transports-v2/http.md).

## 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:

* `c: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`.
* `c: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](https://github.com/x402-foundation/x402/blob/main/specs/extensions/builder_code.md)
— 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](https://github.com/x402-foundation/x402/blob/main/specs/extensions/sign-in-with-x.md)
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](https://github.com/x402-foundation/x402/blob/main/specs/extensions/payment_identifier.md)
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 {: .warning}
>
> 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]

| Option       | Default                 | Meaning                                                        |
| ------------ | ----------------------- | -------------------------------------------------------------- |
| `:limit`     | required                | Hits allowed per window                                        |
| `:window_ms` | required                | Window length in milliseconds                                  |
| `:key`       | `:payer`                | `:payer`, `:ip`, or a 1-arity function of the request context  |
| `:store`     | `X402.RateLimiter.ETS`  | `X402.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.

# `claim_order`

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

Claim ordering relative to facilitator verification.

# `dynamic`

```elixir
@type dynamic(value) ::
  value | (Plug.Conn.t() -&gt; value | {:ok, value} | {:error, term()})
```

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

# `options`

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

# `call`
*since 0.1.0* 

```elixir
@spec call(Plug.Conn.t(), options()) :: Plug.Conn.t()
```

Gates matching requests behind x402 v2 payment verification.

# `init`
*since 0.1.0* 

```elixir
@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` (`t: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` (`t: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` (`t: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` (`t:String.t/0`) - Single-option CAIP-2 network (required when `:accepts` is empty).

* `:asset` (`t: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` (`t: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` (`t: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` (`t: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` (`t:String.t/0`) - Required. Blockchain network in CAIP-2 format (for example `eip155:84532`).

* `:asset` (`t: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` (`t: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`
*since 0.4.0* 

```elixir
@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}

---

*Consult [api-reference.md](api-reference.md) for complete listing*
