All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

[Unreleased]

[0.6.0] - 2026-08-28

Added

  • SVM on-chain facilitator — X402.Facilitator.SVMEngine: verify and settle exact payments on solana:* networks yourself, completing the facilitator role for Solana (previously client-signing + structural validation only). X402.Verify.SVM runs the reference static-path checklist locally — mandatory Ed25519 verification of every required signer except the fee-payer slot (simulation runs sigVerify: false, so local verification is the signature check), fee-payer identity and isolation, the instruction whitelist via X402.Scheme.ExactSVM, and simulateTransaction at :full — emitting the TypeScript reference's invalid_exact_svm_* reason strings. Settlement co-signs the fee-payer slot (X402.Solana.Transaction.attach_signature/3), broadcasts with skipPreflight: true, polls getSignatureStatuses to confirmed/finalized, and dedups duplicate settlements atomically (duplicate_settlement, 120s TTL) through any X402.Extensions.PaymentIdentifier.Cache adapter. X402.Solana.RPC provides the underlying Solana JSON-RPC calls over the existing X402.RPC transport. Transactions using address lookup tables are rejected fail-closed

  • Multi-engine X402.Plug.Facilitator: the new engines: option serves several engines from one endpoint, routed by the request's (scheme, network) against each engine's supported/1; GET /supported merges kinds, extensions, and signers across engines. A single EVM + SVM facilitator process is now one Plug

  • ERC-6492 counterfactual settlement in X402.Facilitator.Engine: with the new eip6492_allowed_factories: allowlist (default [] keeps the previous fail-closed behavior), settlement of a payment signed by a not-yet-deployed smart wallet broadcasts the wrapper's factory calldata as its own transaction first — gated by the allowlist and the new max_deploy_gas_limit: ceiling — then settles with the unwrapped inner signature, mirroring the reference facilitators (eip6492_factory_not_allowed / smart_wallet_deployment_failed). X402.Verify.EVM's :simulate option gains :counterfactual_only so settle's independent re-verify keeps the atomic Multicall3 deploy-and-transfer proof even with simulation otherwise off

  • Transfer-event verification on settlement receipts: a confirmed settlement is reported successful only when the receipt carries the matching ERC-20 Transfer(from, to, value) log for the verified payment (invalid_exact_evm_transfer_event_mismatch otherwise), closing the gap between "transaction mined" and "payment delivered"

  • Pending-settlement reconciliationX402.Facilitator.PendingSettlementStore behaviour with a bundled supervised ETS adapter (5-minute TTL): when a broadcast's confirmation cannot be established, both engines record the transaction before returning settlement_pending, and a retried settle reconciles against the already-broadcast transaction (delete-before-reconcile) instead of broadcasting twice. X402.Plug.PaymentGate complements it from the resource-server side by retrying a settlement_pending settle exactly once, mirroring the reference SDKs' settleWithPendingRetry

  • Inline local verification in X402.Plug.PaymentGate — the new local_verification: option runs X402.Verify.EVM (at :structural, :signature, or :full with an X402.RPC config) inside the gate before the facilitator round-trip for exact-EVM payments; rejections answer 402 with the canonical reason strings, infrastructure failures fail closed as 500, and non-EVM kinds skip it (the facilitator remains the authority)

  • X402.Facilitator.NonceManager — serializes fee-payer transaction nonces for concurrent settlements (fetch-once-then-increment, reset on broadcast rejection); pass to X402.Facilitator.Engine.new/1 via nonce_manager:. Without it, concurrent settles race on the pending nonce and a valid payment can fail with an unused authorization

  • Client-side upto payments via Permit2 (ecosystem report §8 P2.2): the new X402.Permit2 module builds and signs the upto-EVM scheme's Permit2 PermitWitnessTransferFrompermitted.amount is the advertised maximum (the server settles for actual usage up to it), the spender is the canonical x402UptoPermit2Proxy (0x4020A4f3b7b90ccA423B9fabCc0CE57C6C240002), and the witness struct Witness(address to,address facilitator,uint256 validAfter) binds the requirements' payTo and extra.facilitatorAddress so only the announced facilitator can settle. Signing hashes against the canonical version-less Permit2 EIP-712 domain (name "Permit2", chain id from the CAIP-2 network, verifying contract 0x000000000022D473030F116dDEE9F6B43aC78BA3); X402.EIP712.domain_separator/1 now supports such version-less domains. X402.Scheme.UptoEVM implements sign/3 and signable?/1 (an eip155:* network plus extra.facilitatorAddress, as delivered by the facilitator's GET /supported), so X402.Client.build_payment/3 and X402.Client.Finch.request/3 pay upto requirements out of the box; entries without a facilitator address are never selected, and signing one returns {:error, {:missing_extra, "facilitatorAddress"}}. See the new Metered upto payments section in the client guide

  • SVM (Solana) exact scheme — X402.Scheme.ExactSVM (ecosystem report §8 P2.3): the client half of exact on solana:* networks plus structural server-side validation, registered as a built-in. sign/3 builds the reference v0 transaction byte-for-byte (SetComputeUnitLimit, SetComputeUnitPrice, SPL Token / Token-2022 TransferChecked to the ATA derived from payTo + asset, and a Memo — the seller's extra.memo or a random nonce), signs it with the payer's Ed25519 key, and leaves the sponsor's (extra.feePayer, required) signature slot as the zeroed placeholder of a partially signed transaction. Blockhash resolution follows the spec (server's extra.recentBlockhash hint, then the new :svm_blockhash / :svm_blockhash_fetcher client options), and :svm_decimals / :svm_token_program cover mints outside the built-in known-asset table. validate_payload/3 enforces the wire shape (Base64, 1232-byte cap, decodable v0/legacy transaction, advertised fee payer as account 0); precheck/3 enforces the facilitator's static-path whitelist (spec §3.1: 3–7 instructions in the reference order, the 5 lamports/CU cap, §2.1.1 fee payer isolation, transfer semantics, memo enforcement) without any RPC. Full on-chain verification and settlement are available through X402.Verify.SVM and X402.Facilitator.SVMEngine. Transactions using address lookup tables bypass the pure precheck/3, then the bundled verifier and engine reject them fail-closed because lookup-table resolution is not implemented

  • X402.Signer.SolanaKey and the optional sign_ed25519/2 signer callback: Ed25519 signing over OTP's :crypto (no new dependencies); new/1 accepts a raw 32-byte seed, a 64-byte solana-keygen keypair, or Base58/Base64 encodings of either. The X402.Signer chain-family callbacks are now both optional — a signer implements the families it supports and the dispatchers return {:error, :unsupported_signer} for the rest

  • Solana primitives, dependency-free: X402.Base58 (Bitcoin-alphabet encode/decode), X402.Solana (address validation, Ed25519 on-curve check, find_program_address/2, associated_token_address/3), and X402.Solana.Transaction (compact-u16, v0 message compilation matching @solana/kit's account ordering byte-for-byte, wire serialization/decoding). Cross-checked against fixtures generated with the official Solana TypeScript stack

  • X402.Extensions.PaymentIdentifier.RedisCache — a Redis-backed X402.Extensions.PaymentIdentifier.Cache adapter for clustered deployments (ROADMAP P1.3b), over the new optional redix dependency. The replay-protection claim is a single atomic SET key value NX PX ttl, so a replayed payment proof is claimed exactly once across all nodes; expiry is server-side (an expired claim never blocks a retry), live claims are never evicted by the adapter, and connection/Redis errors surface as {:error, reason} so X402.Plug.PaymentGate fails closed. The adapter does not own the connection — users supervise Redix themselves and pass the pid/name to RedisCache.new/1 (:ttl_ms, :namespace, and an injectable :command module implementing X402.Extensions.PaymentIdentifier.RedisCache.Command for testing without a live server). A live conformance suite tagged :redis (excluded by default) runs against REDIS_URL

  • Browser paywall (ecosystem report §8 P2.5): new paywall: option on X402.Plug.PaymentGate (default nil — behavior unchanged). When set to a module implementing the new X402.Paywall behaviour, pre-handler 402 responses to requests that look like a browser page load (Accept header containing text/html and User-Agent containing Mozilla, the heuristic shared by the reference Go/TypeScript middlewares) carry a human-usable HTML body instead of the {} JSON body. The PAYMENT-REQUIRED header is identical on both forms, and API clients, absent-Accept requests, 400/500 statuses, and post-handler settlement failures remain byte-identical to previous releases. Ships X402.Paywall.Default, a self-contained few-KB page (inline CSS, no external requests, no build step) that shows the advertised price, asset, network, and recipient, embeds the exact Base64 PAYMENT-REQUIRED value with manual retry instructions, and includes a dependency-free EIP-1193 wallet flow for exact/eip3009 EVM options — sign the TransferWithAuthorization typed data via eth_signTypedData_v4, retry with PAYMENT-SIGNATURE, replace the document — degrading gracefully without a wallet. All interpolated values are HTML-escaped and the embedded config JSON is script-safe, so hostile route descriptions or service names cannot inject markup. A renderer returning {:error, reason} logs a warning and falls back to the JSON body. See the new "Browser Paywall" guide.

  • X402.Scheme behaviour — pluggable payment schemes (ecosystem report §5.3.1/§8 P1.1): everything scheme-specific now dispatches through one behaviour, so adding a chain or scheme means writing one module and passing it as an option instead of editing core modules. Callbacks: scheme/0 and networks/0 (metadata — CAIP-2 patterns with trailing-* wildcards), sign/3 and the optional signable?/1 (client side), validate_payload/3 and precheck/3 (server side). Resolution lives in X402.Scheme.Registry (exact CAIP-2 match beats wildcard, longest wildcard prefix wins, user modules beat built-ins) and is seeded with X402.Scheme.ExactEVM (exact on eip155:*, EIP-3009 signing plus the existing local pre-checks) and X402.Scheme.UptoEVM (upto on eip155:*, the existing ceiling validation) — extracted from X402.Client, X402.Plug.PaymentGate, and X402.PaymentSignature with unchanged external behavior. Custom schemes register via the new schemes: option on X402.Plug.PaymentGate (routes may then use the registered scheme names), X402.Client.build_payment/3 / select_requirements/2, X402.Client.Finch.request/3, and the new X402.PaymentSignature.validate/3 / decode_and_validate/3 — no application environment, no global registration. Kinds with no registered module keep their historical behavior: validation passes through, the gate skips pre-checks, and the client returns {:error, {:unsupported_kind, scheme, network}}. Scheme validate_payload/3 failures shaped {:invalid_scheme_payment, reason} are answered with HTTP 400 by the gate. Shared EVM authorization pre-checks are reusable via X402.Scheme.EVM.authorization_precheck/3. See the new Custom Payment Schemes guide

  • Run your own facilitator (ecosystem report §8 P2.1 — no official SDK ships a runnable facilitator server; this SDK now does):

    • X402.Facilitator.Engine — the facilitator role engine behind the v2 facilitator API wire shapes: verify/3 delegates to X402.Verify.EVM at the :full level and returns the /verify response with canonical invalidReason strings; settle/3 re-verifies independently (normative for exact-EVM), builds the transferWithAuthorization EIP-1559 transaction (batched eth_estimateGas with a safety margin, eth_maxPriorityFeePerGas + eth_feeHistory fees with an eth_gasPrice fallback, pending nonce), signs its digest through the X402.Signer behaviour (27/28 recovery ids normalized to the EIP-1559 yParity), broadcasts via eth_sendRawTransaction, and polls the receipt — returning the spec's non-terminal "settlement_pending" with the transaction hash when confirmation cannot be established; supported/1 derives the GET /supported response from the configured networks. Fee-payer safety is structural: the engine only ever signs transferWithAuthorization calldata built from verified authorization fields with to = the requirements' asset and value 0 — counterfactual ERC-6492 payments are rejected fail-closed at verify and settle (deployed ERC-1271 wallets are fully supported). X402.Hooks wraps both operations, and [:x402, :facilitator_engine, :verify | :settle] telemetry is emitted.
    • X402.Plug.Facilitator — a compile-guarded Plug scaffold serving POST /verify, POST /settle, and GET /supported over an engine: strict v2 wire-object parsing (400 otherwise), an 8KB body cap consistent with the SDK's header caps (413), an optional constant-time bearer-token check (401), and opaque 500 bodies for infrastructure errors. Protocol-level rejections are 200s per the facilitator API convention; the optional /discovery/resources answers 404.
    • X402.RLP and X402.Transaction — minimal pure RLP and EIP-1559 typed-transaction encoders (no new dependencies), tested against the published RLP specification vectors and a signed-transaction sender-recovery proof.
    • X402.EIP3009.transfer_calldata/3 — the transferWithAuthorization calldata builder (both the (v, r, s) and dynamic-bytes variants), extracted from X402.Verify.EVM so verification's simulation and the engine's settlement sign the exact same bytes; plus X402.EIP712.encode_dynamic_bytes/1.
    • examples/facilitator/ — a runnable facilitator (env-driven PRIVATE_KEY / RPC_URL / NETWORK / PORT, Bandit + Finch) mirroring the upstream examples/typescript/facilitator, with a self-contained boot check. Documented in the new "Run Your Own Facilitator" guide.
  • Full local payment verification for EVM exact/eip3009 payments (X402.Verify.EVM, ecosystem report §8 P1.2 and the verification half of P2.1): runs the reference facilitator verify checklist locally instead of trusting a remote facilitator's verdict, at three explicit levels that never silently downgrade — :structural (pure checks: scheme/network/ domain requirements, payload shape, payTo equality, exact amount, timing with the 6-second settlement buffer), :signature (EIP-712 digest recomputation + EOA recovery via the optional crypto deps, else {:error, :missing_dependency}), and :full (on-chain: chain-id cross-check, payer-bytecode signature routing — ECDSA with no code, strict ERC-1271 isValidSignature with code and no ECDSA fallback — asset bytecode presence, balanceOf funding, and transferWithAuthorization eth_call simulation with failure diagnosis mirroring the reference invalidReason set, else {:error, :rpc_not_configured}). ERC-6492 counterfactual signatures copy the reference Go fail-closed design: the deployment factory must be explicitly allowlisted (eip6492_allowed_factories, default [] rejects all) and validity is proven only by an atomic Multicall3 deploy-and-transfer simulation. reason_string/1 maps local reason atoms onto the canonical cross-SDK invalidReason strings. Documented in the new "Local Payment Verification" guide, including the before_verify hook pattern for gating X402.Plug.PaymentGate requests on local verification.

  • X402.RPC — a minimal Ethereum JSON-RPC client over the user's own Finch pool (eth_call, eth_getCode, eth_chainId, and ordered batch requests in one HTTP round-trip), with NimbleOptions-validated configuration, structured errors, [:x402, :rpc, :request] telemetry, and the same https-with-localhost-exemption enforcement as X402.Facilitator.HTTP. Compiles and fails cleanly ({:error, :missing_dependency}) without the optional Finch dependency.

  • X402.ERC6492 — pure parsing and building of ERC-6492 counterfactual signature wrappers (magic-suffix detection, bounds-checked ABI decoding of the factory/calldata/inner-signature tuple); classification policy lives in the verifier, which never treats a wrapper as proof by itself.

  • X402.Extensions.OfferReceipt — the offer-and-receipt extension: servers sign the payment terms they advertise (offers under extensions["offer-receipt"].info.offers[]) and confirm delivery after settlement (a receipt under info.receipt); clients verify both. Supports the spec's two artifact formats — EIP-712 (fixed chain-agnostic domain {name, version: "1", chainId: 1}, canonical Offer/Receipt types, signing through X402.Signer, verification by signer recovery) and compact JWS (ES256K/EdDSA via OTP :crypto in X402.Extensions.OfferReceipt.JWS, with RFC 8785 JCS payload canonicalization and mandatory alg/kid headers). Includes the info/schema declaration builders mirroring the spec's §6 schemas, fail-closed fetch_offers/1 / fetch_receipt/1 extraction, structural validate_offer/1 / validate_receipt/1, the v1-name → CAIP-2 network conversion (to_caip2/1), and payload builders. Boundaries: JWS verification takes an explicit public key (kid DID URLs are never resolved — no network access), and signer authorization (§4.5.1) remains caller policy, supported via :expected_signer

  • Local pre-verification checks in X402.Plug.PaymentGate (option local_prechecks:, default true): before the facilitator round-trip, the gate now validates the EIP-3009-style payload.authorization object against the matched requirements — to must equal payTo (case-insensitive for hex addresses), value must equal the advertised amount on "exact" routes, validAfter must not be in the future, and validBefore must cover now plus a 6-second settlement buffer (mirroring the reference facilitators). Failures answer 402 with reason {:precheck_failed, detail} and never reach the facilitator; payloads without an authorization object (other schemes, Permit2) and absent fields are skipped, so the facilitator remains the authority. (Ecosystem report §6.6.4/§8 P1.2.)

  • X402.Plug.PaymentGate claim_order: option (:after_verify | :before_verify, default :after_verify — unchanged behavior). With :before_verify the gate claims the payment proof before calling the facilitator, rejecting replayed duplicates with 402 without any facilitator round-trip, and releases the claim when verification fails for any reason; release-on-handler-error and release-on-settle-failure semantics are unchanged. Trade-off documented in the moduledoc: :before_verify sheds replay-storm load from the facilitator, but a node crash during verification strands the claim until the cache TTL expires, while :after_verify never strands a claim on verification but pays one verify call per replayed request. Verify-time exits (facilitator call timeout or :noproc) also release the claim before propagating, so a slow or down facilitator cannot strand a payer's replay lock

  • put_new/3 callback on X402.Extensions.PaymentIdentifier.Cache — the atomic first-writer-wins claim used for replay protection is now part of the behaviour contract (TTL semantics and return values documented), so alternative adapters (Redis, Mnesia, database-backed) can be plugged into X402.Plug.PaymentGate; the cache moduledoc includes a Redis SET NX PX adapter sketch. The contract forbids evicting live entries to admit a new claim: at capacity, ETSCache.put_new/3 now purges expired entries and otherwise refuses with {:error, :cache_full} (the gate fails closed) — previously it evicted the soonest-expiring live claim, which let cheap junk claims drop legitimate replay locks

  • X402.Plug.PaymentGate payment_identifier_cache: accepts {:global, name} and {:via, registry, term} GenServer names, normalized to the bundled ETSCache adapter like a bare pid/name

  • X402.Plug.PaymentGate payment_identifier_cache: now also accepts a {module, cache} adapter tuple implementing X402.Extensions.PaymentIdentifier.Cache; a bare pid/name keeps working and is normalized to the bundled ETSCache adapter

  • X402.Facilitator.supported/0..1GET /supported returning the facilitator's payment kinds, extensions, and signers as {:ok, %{kinds: [...], extensions: [...], signers: %{...}}}, validated fail-closed ({:error, %Error{type: :malformed_facilitator_response}} on a malformed body). Unlocks startup route validation, SVM feePayer discovery, and upto facilitatorAddress discovery

  • X402.Facilitator.list_resources/0..2GET /discovery/resources with NimbleOptions-validated filter and pagination parameters (type, pay_to, scheme, network, extensions, limit, offset), returning fail-closed parsed {:ok, %{items: [...], pagination: ..., x402_version: ...}}

  • X402.Facilitator.HTTP.get/3..4 — GET transport with optional :query parameters, sharing the retry/backoff/TLS pipeline with request/5

  • Telemetry spans [:x402, :facilitator, :supported] and [:x402, :facilitator, :list_resources], consistent with the existing verify/settle spans

  • Facilitator auth implementations now receive the real request method (:get for the new endpoints) in request_info, so CDP JWTs bind GET host path in their uris claim

  • X402.Extensions.Bazaar discovery client — list_resources/0..2 queries a facilitator's GET /discovery/resources and parses every discovered entry into a well-typed map (resource URL, accepts PaymentRequirements list, lastUpdated, metadata, extensions), fail-closed on structurally invalid entries; parse_resource/1 for per-entry parsing; and pure filter helpers filter_by_network/2, filter_by_scheme/2, and filter_by_max_price/2

Changed

  • The dialyzer PLT filename now carries the OTP/Elixir versions (priv/plts/project-otp<release>-<version>.plt), and the CI PLT cache no longer falls back to other toolchains' entries — after a toolchain bump, mix dialyzer builds a fresh PLT instead of slowly migrating the old toolchain's file in place (the near-silent churn that read as a hang; note the first run on a new OTP still spends several minutes building the core PLTs)
  • Development and CI toolchain bumped to Elixir 1.20.4 / Erlang OTP 29.0.5; CI now tests both the supported floor (Elixir 1.19 / OTP 27) and the latest stack. The library still requires only ~> 1.19. Bitstring patterns that read a size from an outer variable now use the explicit pin operator (binary-size(^len)), fixing the deprecation warnings the Elixir 1.20 type checker emits for the implicit form. credo updated to 1.7.19 for Elixir 1.20 compatibility
  • Replay/dedup keys are now canonical: X402.Plug.PaymentGate keys its replay claim on signature-covered payment identity — the EIP-3009 from + nonce for exact-EVM, the Permit2 owner + nonce for upto, and the sha256 of the signed message bytes for exact-SVM — instead of the raw header hash, so a re-encoded duplicate of the same authorization (JSON key order, whitespace, Base64 variant) can no longer bypass replay protection. Unknown schemes keep the raw-header-hash behavior. The gate also decodes an echoed payment_identifier extension (malformed → 400) and surfaces the client's paymentId in conn.assigns[:x402_payment_id], the settlement context, and telemetry — deliberately not as the dedup key, which must never derive from unsigned client-controlled fields
  • X402.Facilitator.verify/2..4 and settle/2..4 now execute the HTTP request — including retries with backoff, lifecycle hooks, telemetry spans, and per-request auth header minting — in the calling process. The facilitator GenServer is now a supervised configuration holder consulted only for its settings, so concurrent payment operations no longer serialize behind a single process (previously blocking HTTP plus retry sleeps ran inside handle_call, with a worst case well beyond the default GenServer.call/3 timeout). The public API, option surface, return shapes, telemetry event names, and hook semantics are unchanged; note that X402.Hooks callbacks now run in the caller's process
  • X402.Plug.PaymentGate routes all replay claim/release calls through the X402.Extensions.PaymentIdentifier.Cache behaviour instead of calling ETSCache directly; adapter claim errors other than {:error, :already_exists} fail closed with HTTP 500
  • Implementations of X402.Extensions.PaymentIdentifier.Cache must now export put_new/3; validate_adapter/1 rejects adapters without it

Documentation

  • Documented the clustered-BEAM double-execution hazard of the per-node ETS replay cache in the PaymentGate, Cache, and ETSCache moduledocs
  • Added SECURITY.md — private vulnerability reporting, supported versions, and the SDK's multi-role trust model: delegated versus optional local verification, transport hardening, replay/settlement configuration, and the pre-1.0 independent-audit boundary
  • Corrected the X402.Facilitator.Auth.CDP.headers/2 doc: the JWT is signed fresh per facilitator operation, and transport retries within one operation reuse it inside its 120-second validity window

Security

  • X402.Plug.PaymentGate route matching now runs on decoded conn.script_name ++ conn.path_info segments instead of the raw conn.request_path. Adapters drop empty path segments when building path_info, so //api/resource reached the router as the protected resource while the gate's raw string comparison passed it through unpaid — the same bug class as GHSA-3j63-5h8p-gf7c in the legacy TypeScript middleware. Segments are additionally percent-decoded (malformed sequences match verbatim), so the gate also covers routers that decode; a decoded match a router would 404 merely answers 402 first, which is the fail-safe direction for a paywall. Regression tests cover double-slash, percent-encoded, encoded-slash, glob, and malformed-percent aliases. Telemetry path metadata now reports the decoded path. (Ecosystem report §6.5/§8 P1.4.)

Removed

  • The legacy "v1" validation path in X402.PaymentSignature. It required transactionHash/network/scheme/payerWallet — a shape that matches no published x402 wire format (real v1 payments carry {scheme, network, payload: {signature, authorization}} in the X-PAYMENT header, which this SDK never reads) — so it advertised v1 interop that was exactly zero while accepting payloads no facilitator would settle. Payloads declaring x402Version: 1 or omitting the version now return {:error, {:unsupported_x402_version, 1 | nil}} (mapped to HTTP 400 by X402.Plug.PaymentGate). The {:invalid_format, _} error reason no longer occurs; {:missing_fields, _} remains for v2 accepted objects missing required PaymentRequirements fields. (Ecosystem report §8 P0.1.)

Client and transport additions

  • Payer client (report §8 P0.4): X402.Client — transport-agnostic core with select_requirements/2 (filterable payment-option selection with a max_amount budget guard), build_payment/3 (v2 PaymentPayload assembly with full requirements and extension echo), and encode_payment/1
  • X402.Signer behaviour — the client-side signing seam (address/1 + sign_eip712/3 over the precomputed EIP-712 digest and full typed data), with X402.Signer.LocalKey as the built-in raw-private-key implementation (optional ex_secp256k1/ex_keccak; the key is redacted from inspect/1)
  • X402.EIP3009 — EIP-3009 TransferWithAuthorization building, EIP-712 domain derivation from payment requirements, digest computation, signing, and signer recovery, promoted from test/support/x402_test_payments.ex (which now delegates to it)
  • X402.Client.Finch — HTTP convenience client: on 402 with a PAYMENT-REQUIRED header it decodes, signs, and retries once with PAYMENT-SIGNATURE (never pays twice), returning the decoded PAYMENT-RESPONSE settlement receipt; includes an on_payment_required budget/consent hook and enforces https:// for non-loopback resources
  • [:x402, :client, :select | :sign | :build | :request] telemetry events

  • guides/client.md — "Paying for x402 Resources from Elixir"
  • X402.EIP712 — shared EIP-712 hashing primitives (requirements-derived domain, domain separator, hash_struct/2, digest/2, and the ABI word encoders), extracted from X402.EIP3009 which now delegates to it
  • X402.Extensions.EIP2612GasSponsoring — the eip2612GasSponsoring gas-sponsoring extension (report §8 P2.4): server-side declaration (build_extension/0) and echo validation (extract_info/1 / validate_info/1), plus client-side EIP-2612 Permit signing (sign_permit/3, put_info/2, and enricher/2 for X402.Client.build_payment/3)
  • X402.Extensions.ERC20ApprovalGasSponsoring — the erc20ApprovalGasSponsoring gas-sponsoring extension (report §8 P2.4) for tokens without EIP-2612: server-side declaration and echo validation, plus client-side assembly of the extension data around a pre-signed approve(Permit2, amount) transaction (build_info/1, put_info/2, and enricher/1 for X402.Client.build_payment/3)
  • X402.Client.build_payment/3 and X402.Client.Finch.request/3 :extensions option — client extension enrichers applied to the assembled payload (how gas-sponsoring data is attached opt-in)
  • integration/e2e_server/ — resource-server component for the official x402 cross-language e2e interop harness (X402.Plug.PaymentGate + X402.Facilitator behind Bandit), including a ready-to-copy e2e/servers/elixir/http/bandit/ tree for the foundation repo, the upstream patch list, and a local smoke suite (verify.sh)
  • MCP transport (report §8 P1.6): X402.MCP — library-agnostic pure functions implementing the x402 MCP transport over plain tool-call request/result maps (_meta["x402/payment"] payloads, _meta["x402/payment-response"] receipts, payment-required results with structuredContent + content[0].text, and 402/-32042 JSON-RPC payment errors)
  • X402.MCP.Server — wraps any MCP tool handler with the verify → execute → settle flow against X402.Facilitator, validating payloads as strictly as X402.Plug.PaymentGate (v2 version check, accepted matching, extension echo) with optional replay protection via the same payment_identifier_cache option
  • X402.MCP.Client — drives any tool-call function through the detect → sign → retry-once loop (never pays twice) with the same on_payment_required veto hook and max_amount budget guard as X402.Client.Finch, plus build_payment_meta/3 for manual retries
  • [:x402, :mcp, :payment_required | :payment_verified | :payment_rejected | :call] telemetry events

  • guides/mcp.md — "Paid MCP Tools in Elixir"

[0.5.0] - 2026-08-26

Added

Fixed

  • CDP JWT uris claim now binds to the full request path (facilitator base URL path + endpoint) — the previous host + /verify binding caused the hosted CDP facilitator to reject all requests with 401 (request_info.path is now the fully-qualified path)
  • The auth request host is now derived from the URI host and port (port included only when non-default, matching JavaScript URL.host semantics) instead of the deprecated URI.authority field, which is no longer populated on recent Elixir and failed dialyzer
  • Bazaar text-body declarations now accept string examples and emit a matching string schema
  • Bazaar output schemas now match scalar and array examples instead of always declaring an object

Testing

  • Live smoke tests against the CDP hosted facilitator (cdp_live_test.exs, tagged :smoke, excluded from the default run) covering negative-control, authentication, end-to-end verify, and settlement tiers
  • X402.TestPayments reworked around a Config struct with default values; payment configuration now comes from from_env/1 (facilitator-agnostic X402_* vars only) — no hardcoded sample wallets, and end-to-end receivers default to a fresh burner wallet (never the payer, and never the zero address, which USDC rejects)
  • Removed test_helper.exs compile-time redefinition of X402.Hooks — the real module compiles cleanly and the test suite passes without the override

[0.4.1] - 2026-08-15

Fixed

  • Declare :telemetry as a required runtime dependency so telemetry events and facilitator calls work in downstream installs without optional dependencies
  • Start the OTP :public_key application used by X402.Facilitator.HTTP.secure_pool_opts/0
  • Exercise the library from a minimal downstream Mix project in CI to catch missing runtime dependencies before publishing

[0.4.0] - 2026-08-15

Added

Changed

  • X402.Plug.PaymentGate now verifies before the protected handler and settles only after a successful handler response
  • Facilitator requests now use the v2 {x402Version, paymentPayload, paymentRequirements} wire format
  • "upto" verification uses PaymentRequirements.amount as the authorized maximum; settlement uses it as the actual atomic amount charged
  • Plug route prices and all documentation examples use atomic token units

Fixed

  • Fail closed when facilitator responses omit or mistype isValid, success, transaction, or network
  • Preserve the full request URL, including its query string, in ResourceInfo.url
  • Reject partial or mutated accepted requirements instead of matching only five fields
  • Return HTTP 500 for facilitator transport failures and malformed facilitator responses while retaining HTTP 400 for invalid input and HTTP 402 for payment failure
  • Reject unsupported upfront and escrow flows instead of applying unsafe authorization-flow timing
  • Avoid creating atoms from untrusted string route keys
  • Compile cleanly without optional SIWX crypto dependencies and return :missing_dependency when the default verifier cannot load them

Migration

  • Replace the removed Plug option facilitator_url: with a supervised X402.Facilitator process and pass it via facilitator:.
  • Replace decimal display amounts such as "0.01" with atomic-unit strings such as "10000" for six-decimal USDC.
  • Hook callbacks use context.payload / context.requirements and return {:cont, context}, {:halt, reason}, or {:recover, result} as documented by X402.Hooks.

[0.3.3] - 2026-03-29

Fixed

  • Payment signature format validation and SIWX ETS size cap (#39)
  • Tightened Solana address validation and warn on missing idempotency cache (#36)
  • Enforce https:// scheme on facilitator base_url — prevents plaintext credential leakage (#35)
  • Added 8KB payload size cap to PaymentRequired and PaymentResponse to prevent oversized payloads (#34)
  • TLS peer verification enabled by default and PAYMENT-SIGNATURE header size cap (#32)

Changed

  • Bumped minimum Elixir to ~> 1.19 (#33)
  • Optimized decimal parsing and centralized utility functions (#37)

Added

  • Unit test for HTTP.secure_pool_opts/0 (#38)

[0.3.2] - 2026-03-01

Fixed

  • Safe cache eviction with bounded cleanup to prevent full-table scans under load (#30)
  • Atomic payment claim in PaymentGate plug to prevent double-settlement on concurrent requests (#30)
  • SIWX ETSStorage read consistency — route get through GenServer to prevent revoked session reads (#31)
  • Full-jitter exponential backoff in Facilitator.HTTP to prevent thundering herd on retries (#31)
  • Base.decode64 padding safety in PaymentSignature and PaymentRequired (#31)

[0.3.1] - 2026-02-25

Fixed

  • Fixed unbounded ETS cache growth vulnerability (DoS) — added max_size config with LRU eviction (#17)
  • Fixed expired entries not being deleted during direct ETS reads (#25)
  • Fixed mix format compliance across all files

Added

  • Comprehensive tests for X402.Behaviour.implements?/2 with doctests (#28)
  • Test coverage for facilitator hook exception and throw handling (#24)
  • Optimized ETS cache with direct concurrent reads bypassing GenServer serialization (#25)

[0.3.0] - 2026-02-17

Added

Changed

  • ex_secp256k1 and ex_keccak are now optional dependencies (only needed for SIWX)
  • ETS storage uses :protected access with direct reads bypassing GenServer for better concurrency

Fixed

  • Credo strict compliance: implicit try, redundant with clauses
  • Dialyzer: unreachable pattern matches in PaymentIdentifier and SIWX Verifier

[0.1.0] - 2026-02-14

Added