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 settleexactpayments onsolana:*networks yourself, completing the facilitator role for Solana (previously client-signing + structural validation only).X402.Verify.SVMruns the reference static-path checklist locally — mandatory Ed25519 verification of every required signer except the fee-payer slot (simulation runssigVerify: false, so local verification is the signature check), fee-payer identity and isolation, the instruction whitelist viaX402.Scheme.ExactSVM, andsimulateTransactionat:full— emitting the TypeScript reference'sinvalid_exact_svm_*reason strings. Settlement co-signs the fee-payer slot (X402.Solana.Transaction.attach_signature/3), broadcasts withskipPreflight: true, pollsgetSignatureStatusesto confirmed/finalized, and dedups duplicate settlements atomically (duplicate_settlement, 120s TTL) through anyX402.Extensions.PaymentIdentifier.Cacheadapter.X402.Solana.RPCprovides the underlying Solana JSON-RPC calls over the existingX402.RPCtransport. Transactions using address lookup tables are rejected fail-closedMulti-engine
X402.Plug.Facilitator: the newengines:option serves several engines from one endpoint, routed by the request's (scheme, network) against each engine'ssupported/1;GET /supportedmerges kinds, extensions, and signers across engines. A single EVM + SVM facilitator process is now one PlugERC-6492 counterfactual settlement in
X402.Facilitator.Engine: with the neweip6492_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 newmax_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:simulateoption gains:counterfactual_onlyso settle's independent re-verify keeps the atomic Multicall3 deploy-and-transfer proof even with simulation otherwise offTransfer-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_mismatchotherwise), closing the gap between "transaction mined" and "payment delivered"Pending-settlement reconciliation —
X402.Facilitator.PendingSettlementStorebehaviour with a bundled supervised ETS adapter (5-minute TTL): when a broadcast's confirmation cannot be established, both engines record the transaction before returningsettlement_pending, and a retried settle reconciles against the already-broadcast transaction (delete-before-reconcile) instead of broadcasting twice.X402.Plug.PaymentGatecomplements it from the resource-server side by retrying asettlement_pendingsettle exactly once, mirroring the reference SDKs'settleWithPendingRetryInline local verification in
X402.Plug.PaymentGate— the newlocal_verification:option runsX402.Verify.EVM(at:structural,:signature, or:fullwith anX402.RPCconfig) 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 toX402.Facilitator.Engine.new/1vianonce_manager:. Without it, concurrent settles race on the pending nonce and a valid payment can fail with an unused authorizationClient-side
uptopayments via Permit2 (ecosystem report §8 P2.2): the newX402.Permit2module builds and signs the upto-EVM scheme's Permit2PermitWitnessTransferFrom—permitted.amountis the advertised maximum (the server settles for actual usage up to it), the spender is the canonicalx402UptoPermit2Proxy(0x4020A4f3b7b90ccA423B9fabCc0CE57C6C240002), and the witness structWitness(address to,address facilitator,uint256 validAfter)binds the requirements'payToandextra.facilitatorAddressso 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 contract0x000000000022D473030F116dDEE9F6B43aC78BA3);X402.EIP712.domain_separator/1now supports such version-less domains.X402.Scheme.UptoEVMimplementssign/3andsignable?/1(aneip155:*network plusextra.facilitatorAddress, as delivered by the facilitator'sGET /supported), soX402.Client.build_payment/3andX402.Client.Finch.request/3payuptorequirements out of the box; entries without a facilitator address are never selected, and signing one returns{:error, {:missing_extra, "facilitatorAddress"}}. See the new Metereduptopayments section in the client guideSVM (Solana)
exactscheme —X402.Scheme.ExactSVM(ecosystem report §8 P2.3): the client half ofexactonsolana:*networks plus structural server-side validation, registered as a built-in.sign/3builds the reference v0 transaction byte-for-byte (SetComputeUnitLimit,SetComputeUnitPrice, SPL Token / Token-2022TransferCheckedto the ATA derived frompayTo+asset, and a Memo — the seller'sextra.memoor 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'sextra.recentBlockhashhint, then the new:svm_blockhash/:svm_blockhash_fetcherclient options), and:svm_decimals/:svm_token_programcover mints outside the built-in known-asset table.validate_payload/3enforces the wire shape (Base64, 1232-byte cap, decodable v0/legacy transaction, advertised fee payer as account 0);precheck/3enforces 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 throughX402.Verify.SVMandX402.Facilitator.SVMEngine. Transactions using address lookup tables bypass the pureprecheck/3, then the bundled verifier and engine reject them fail-closed because lookup-table resolution is not implementedX402.Signer.SolanaKeyand the optionalsign_ed25519/2signer callback: Ed25519 signing over OTP's:crypto(no new dependencies);new/1accepts a raw 32-byte seed, a 64-bytesolana-keygenkeypair, or Base58/Base64 encodings of either. TheX402.Signerchain-family callbacks are now both optional — a signer implements the families it supports and the dispatchers return{:error, :unsupported_signer}for the restSolana 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), andX402.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 stackX402.Extensions.PaymentIdentifier.RedisCache— a Redis-backedX402.Extensions.PaymentIdentifier.Cacheadapter for clustered deployments (ROADMAP P1.3b), over the new optionalredixdependency. The replay-protection claim is a single atomicSET 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}soX402.Plug.PaymentGatefails closed. The adapter does not own the connection — users superviseRedixthemselves and pass the pid/name toRedisCache.new/1(:ttl_ms,:namespace, and an injectable:commandmodule implementingX402.Extensions.PaymentIdentifier.RedisCache.Commandfor testing without a live server). A live conformance suite tagged:redis(excluded by default) runs againstREDIS_URLBrowser paywall (ecosystem report §8 P2.5): new
paywall:option onX402.Plug.PaymentGate(defaultnil— behavior unchanged). When set to a module implementing the newX402.Paywallbehaviour, pre-handler 402 responses to requests that look like a browser page load (Acceptheader containingtext/htmlandUser-AgentcontainingMozilla, the heuristic shared by the reference Go/TypeScript middlewares) carry a human-usable HTML body instead of the{}JSON body. ThePAYMENT-REQUIREDheader is identical on both forms, and API clients, absent-Acceptrequests, 400/500 statuses, and post-handler settlement failures remain byte-identical to previous releases. ShipsX402.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 Base64PAYMENT-REQUIREDvalue with manual retry instructions, and includes a dependency-free EIP-1193 wallet flow forexact/eip3009EVM options — sign theTransferWithAuthorizationtyped data viaeth_signTypedData_v4, retry withPAYMENT-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.Schemebehaviour — 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/0andnetworks/0(metadata — CAIP-2 patterns with trailing-*wildcards),sign/3and the optionalsignable?/1(client side),validate_payload/3andprecheck/3(server side). Resolution lives inX402.Scheme.Registry(exact CAIP-2 match beats wildcard, longest wildcard prefix wins, user modules beat built-ins) and is seeded withX402.Scheme.ExactEVM(exactoneip155:*, EIP-3009 signing plus the existing local pre-checks) andX402.Scheme.UptoEVM(uptooneip155:*, the existing ceiling validation) — extracted fromX402.Client,X402.Plug.PaymentGate, andX402.PaymentSignaturewith unchanged external behavior. Custom schemes register via the newschemes:option onX402.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 newX402.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}}. Schemevalidate_payload/3failures shaped{:invalid_scheme_payment, reason}are answered with HTTP 400 by the gate. Shared EVM authorization pre-checks are reusable viaX402.Scheme.EVM.authorization_precheck/3. See the new Custom Payment Schemes guideRun 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/3delegates toX402.Verify.EVMat the:fulllevel and returns the/verifyresponse with canonicalinvalidReasonstrings;settle/3re-verifies independently (normative for exact-EVM), builds thetransferWithAuthorizationEIP-1559 transaction (batchedeth_estimateGaswith a safety margin,eth_maxPriorityFeePerGas+eth_feeHistoryfees with aneth_gasPricefallback,pendingnonce), signs its digest through theX402.Signerbehaviour (27/28 recovery ids normalized to the EIP-1559yParity), broadcasts viaeth_sendRawTransaction, and polls the receipt — returning the spec's non-terminal"settlement_pending"with the transaction hash when confirmation cannot be established;supported/1derives theGET /supportedresponse from the configured networks. Fee-payer safety is structural: the engine only ever signstransferWithAuthorizationcalldata built from verified authorization fields withto= the requirements'assetandvalue0— counterfactual ERC-6492 payments are rejected fail-closed at verify and settle (deployed ERC-1271 wallets are fully supported).X402.Hookswraps both operations, and[:x402, :facilitator_engine, :verify | :settle]telemetry is emitted.X402.Plug.Facilitator— a compile-guarded Plug scaffold servingPOST /verify,POST /settle, andGET /supportedover 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/resourcesanswers 404.X402.RLPandX402.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— thetransferWithAuthorizationcalldata builder (both the(v, r, s)and dynamic-bytesvariants), extracted fromX402.Verify.EVMso verification's simulation and the engine's settlement sign the exact same bytes; plusX402.EIP712.encode_dynamic_bytes/1.examples/facilitator/— a runnable facilitator (env-drivenPRIVATE_KEY/RPC_URL/NETWORK/PORT, Bandit + Finch) mirroring the upstreamexamples/typescript/facilitator, with a self-contained boot check. Documented in the new "Run Your Own Facilitator" guide.
Full local payment verification for EVM
exact/eip3009payments (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,payToequality, 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-1271isValidSignaturewith code and no ECDSA fallback — asset bytecode presence,balanceOffunding, andtransferWithAuthorizationeth_callsimulation with failure diagnosis mirroring the referenceinvalidReasonset, 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/1maps local reason atoms onto the canonical cross-SDKinvalidReasonstrings. Documented in the new "Local Payment Verification" guide, including thebefore_verifyhook pattern for gatingX402.Plug.PaymentGaterequests 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 asX402.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 underextensions["offer-receipt"].info.offers[]) and confirm delivery after settlement (a receipt underinfo.receipt); clients verify both. Supports the spec's two artifact formats — EIP-712 (fixed chain-agnostic domain{name, version: "1", chainId: 1}, canonicalOffer/Receipttypes, signing throughX402.Signer, verification by signer recovery) and compact JWS (ES256K/EdDSAvia OTP:cryptoinX402.Extensions.OfferReceipt.JWS, with RFC 8785 JCS payload canonicalization and mandatoryalg/kidheaders). Includes theinfo/schemadeclaration builders mirroring the spec's §6 schemas, fail-closedfetch_offers/1/fetch_receipt/1extraction, structuralvalidate_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 (kidDID URLs are never resolved — no network access), and signer authorization (§4.5.1) remains caller policy, supported via:expected_signerLocal pre-verification checks in
X402.Plug.PaymentGate(optionlocal_prechecks:, defaulttrue): before the facilitator round-trip, the gate now validates the EIP-3009-stylepayload.authorizationobject against the matched requirements —tomust equalpayTo(case-insensitive for hex addresses),valuemust equal the advertised amount on"exact"routes,validAftermust not be in the future, andvalidBeforemust 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.PaymentGateclaim_order:option (:after_verify|:before_verify, default:after_verify— unchanged behavior). With:before_verifythe 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_verifysheds replay-storm load from the facilitator, but a node crash during verification strands the claim until the cache TTL expires, while:after_verifynever 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 lockput_new/3callback onX402.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 intoX402.Plug.PaymentGate; the cache moduledoc includes a RedisSET NX PXadapter sketch. The contract forbids evicting live entries to admit a new claim: at capacity,ETSCache.put_new/3now 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 locksX402.Plug.PaymentGatepayment_identifier_cache:accepts{:global, name}and{:via, registry, term}GenServer names, normalized to the bundledETSCacheadapter like a bare pid/nameX402.Plug.PaymentGatepayment_identifier_cache:now also accepts a{module, cache}adapter tuple implementingX402.Extensions.PaymentIdentifier.Cache; a bare pid/name keeps working and is normalized to the bundledETSCacheadapterX402.Facilitator.supported/0..1—GET /supportedreturning 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, SVMfeePayerdiscovery, anduptofacilitatorAddressdiscoveryX402.Facilitator.list_resources/0..2—GET /discovery/resourceswith 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:queryparameters, sharing the retry/backoff/TLS pipeline withrequest/5Telemetry spans
[:x402, :facilitator, :supported]and[:x402, :facilitator, :list_resources], consistent with the existing verify/settle spansFacilitator auth implementations now receive the real request method (
:getfor the new endpoints) inrequest_info, so CDP JWTs bindGET host pathin theirurisclaimX402.Extensions.Bazaardiscovery client —list_resources/0..2queries a facilitator'sGET /discovery/resourcesand parses every discovered entry into a well-typed map (resource URL,acceptsPaymentRequirements list,lastUpdated, metadata, extensions), fail-closed on structurally invalid entries;parse_resource/1for per-entry parsing; and pure filter helpersfilter_by_network/2,filter_by_scheme/2, andfilter_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 dialyzerbuilds 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.credoupdated to 1.7.19 for Elixir 1.20 compatibility - Replay/dedup keys are now canonical:
X402.Plug.PaymentGatekeys its replay claim on signature-covered payment identity — the EIP-3009from+noncefor 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 echoedpayment_identifierextension (malformed → 400) and surfaces the client'spaymentIdinconn.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..4andsettle/2..4now 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 insidehandle_call, with a worst case well beyond the defaultGenServer.call/3timeout). The public API, option surface, return shapes, telemetry event names, and hook semantics are unchanged; note thatX402.Hookscallbacks now run in the caller's processX402.Plug.PaymentGateroutes all replay claim/release calls through theX402.Extensions.PaymentIdentifier.Cachebehaviour instead of callingETSCachedirectly; adapter claim errors other than{:error, :already_exists}fail closed with HTTP 500- Implementations of
X402.Extensions.PaymentIdentifier.Cachemust now exportput_new/3;validate_adapter/1rejects adapters without it
Documentation
- Documented the clustered-BEAM double-execution hazard of the per-node ETS
replay cache in the
PaymentGate,Cache, andETSCachemoduledocs - 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/2doc: 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.PaymentGateroute matching now runs on decodedconn.script_name ++ conn.path_infosegments instead of the rawconn.request_path. Adapters drop empty path segments when buildingpath_info, so//api/resourcereached 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. Telemetrypathmetadata now reports the decoded path. (Ecosystem report §6.5/§8 P1.4.)
Removed
- The legacy "v1" validation path in
X402.PaymentSignature. It requiredtransactionHash/network/scheme/payerWallet— a shape that matches no published x402 wire format (real v1 payments carry{scheme, network, payload: {signature, authorization}}in theX-PAYMENTheader, which this SDK never reads) — so it advertised v1 interop that was exactly zero while accepting payloads no facilitator would settle. Payloads declaringx402Version: 1or omitting the version now return{:error, {:unsupported_x402_version, 1 | nil}}(mapped to HTTP 400 byX402.Plug.PaymentGate). The{:invalid_format, _}error reason no longer occurs;{:missing_fields, _}remains for v2acceptedobjects missing required PaymentRequirements fields. (Ecosystem report §8 P0.1.)
Client and transport additions
- Payer client (report §8 P0.4):
X402.Client— transport-agnostic core withselect_requirements/2(filterable payment-option selection with amax_amountbudget guard),build_payment/3(v2PaymentPayloadassembly with full requirements and extension echo), andencode_payment/1 X402.Signerbehaviour — the client-side signing seam (address/1+sign_eip712/3over the precomputed EIP-712 digest and full typed data), withX402.Signer.LocalKeyas the built-in raw-private-key implementation (optionalex_secp256k1/ex_keccak; the key is redacted frominspect/1)X402.EIP3009— EIP-3009TransferWithAuthorizationbuilding, EIP-712 domain derivation from payment requirements, digest computation, signing, and signer recovery, promoted fromtest/support/x402_test_payments.ex(which now delegates to it)X402.Client.Finch— HTTP convenience client: on402with aPAYMENT-REQUIREDheader it decodes, signs, and retries once withPAYMENT-SIGNATURE(never pays twice), returning the decodedPAYMENT-RESPONSEsettlement receipt; includes anon_payment_requiredbudget/consent hook and enforceshttps://for non-loopback resources[:x402, :client, :select | :sign | :build | :request]telemetry eventsguides/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 fromX402.EIP3009which now delegates to itX402.Extensions.EIP2612GasSponsoring— theeip2612GasSponsoringgas-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-2612Permitsigning (sign_permit/3,put_info/2, andenricher/2forX402.Client.build_payment/3)X402.Extensions.ERC20ApprovalGasSponsoring— theerc20ApprovalGasSponsoringgas-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-signedapprove(Permit2, amount)transaction (build_info/1,put_info/2, andenricher/1forX402.Client.build_payment/3)X402.Client.build_payment/3andX402.Client.Finch.request/3:extensionsoption — 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.Facilitatorbehind Bandit), including a ready-to-copye2e/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 withstructuredContent+content[0].text, and402/-32042JSON-RPC payment errors) X402.MCP.Server— wraps any MCP tool handler with the verify → execute → settle flow againstX402.Facilitator, validating payloads as strictly asX402.Plug.PaymentGate(v2 version check,acceptedmatching, extension echo) with optional replay protection via the samepayment_identifier_cacheoptionX402.MCP.Client— drives any tool-call function through the detect → sign → retry-once loop (never pays twice) with the sameon_payment_requiredveto hook andmax_amountbudget guard asX402.Client.Finch, plusbuild_payment_meta/3for manual retries[:x402, :mcp, :payment_required | :payment_verified | :payment_rejected | :call]telemetry eventsguides/mcp.md— "Paid MCP Tools in Elixir"
[0.5.0] - 2026-08-26
Added
X402.Facilitator.Authbehaviour andX402.Facilitator.Auth.CDP— per-request JWT authentication for the Coinbase Developer Platform x402 facilitator, configured via the newauth:option onX402.Facilitator.start_link/1X402.Facilitatorotp_app:option — Ecto-style runtime configuration whereconfig :app, <name>supplies options (including auth credentials) andconfig/runtime.exsis the single source of truth; explicit options take precedenceX402.Extensions.Bazaar.build_extension/1— factory for thebazaardiscovery extension payload (info+schema), supporting HTTP and MCP inputs
Fixed
- CDP JWT
urisclaim now binds to the full request path (facilitator base URL path + endpoint) — the previoushost + /verifybinding caused the hosted CDP facilitator to reject all requests with 401 (request_info.pathis 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.hostsemantics) instead of the deprecatedURI.authorityfield, 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.TestPaymentsreworked around aConfigstruct with default values; payment configuration now comes fromfrom_env/1(facilitator-agnosticX402_*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.exscompile-time redefinition ofX402.Hooks— the real module compiles cleanly and the test suite passes without the override
[0.4.1] - 2026-08-15
Fixed
- Declare
:telemetryas a required runtime dependency so telemetry events and facilitator calls work in downstream installs without optional dependencies - Start the OTP
:public_keyapplication used byX402.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
- x402 v2
PaymentPayloadvalidation and completePaymentRequirementsmatching - Extension-echo validation for server-advertised extension data
X402.Plug.PaymentGate.put_settlement_amount/2for metered"upto"routes- Multi-option
acceptsand v2ResourceInfosupport inX402.Plug.PaymentGate
Changed
X402.Plug.PaymentGatenow 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 usesPaymentRequirements.amountas 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, ornetwork - 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
upfrontandescrowflows 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_dependencywhen the default verifier cannot load them
Migration
- Replace the removed Plug option
facilitator_url:with a supervisedX402.Facilitatorprocess and pass it viafacilitator:. - 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.requirementsand return{:cont, context},{:halt, reason}, or{:recover, result}as documented byX402.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 facilitatorbase_url— prevents plaintext credential leakage (#35) - Added 8KB payload size cap to
PaymentRequiredandPaymentResponseto prevent oversized payloads (#34) - TLS peer verification enabled by default and
PAYMENT-SIGNATUREheader 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
getthrough 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_sizeconfig with LRU eviction (#17) - Fixed expired entries not being deleted during direct ETS reads (#25)
- Fixed
mix formatcompliance across all files
Added
- Comprehensive tests for
X402.Behaviour.implements?/2with 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
- SIWX (Sign-In-With-X) — Repeat access without repayment (#14)
X402.Extensions.SIWX— CAIP-122 message construction and EIP-4361 (SIWE) formatX402.Extensions.SIWX.Verifier— behaviour for signature verificationX402.Extensions.SIWX.Verifier.Default— EVM signature verification viaex_secp256k1X402.Extensions.SIWX.Storage— behaviour for access record persistenceX402.Extensions.SIWX.ETSStorage— default ETS adapter with TTL and periodic cleanupSIGN-IN-WITH-Xheader encode/decode
- "upto" Scheme — Max-price bidding for flexible payments (#13)
PaymentRequiredencode/decode for"upto"scheme withmaxPricePaymentSignaturevalidation: payment value ≤ maxPrice- Facilitator client support for upto verification with hooks
PaymentGatePlug route config supports upto scheme
- Payment Identifier — Idempotency extension (#12)
X402.Extensions.PaymentIdentifier— encode/decode payment IDs in payloadsX402.Extensions.PaymentIdentifier.Cache— behaviour for deduplication cacheX402.Extensions.PaymentIdentifier.ETSCache— default ETS adapter with TTL
- Lifecycle Hooks — Behaviour-based hooks for verify/settle (#10)
before_verify/2,after_verify/2,before_settle/2,after_settle/2on_verify_failure/2,on_settle_failure/2- Context struct with request metadata, result, and error tracking
Changed
ex_secp256k1andex_keccakare now optional dependencies (only needed for SIWX)- ETS storage uses
:protectedaccess with direct reads bypassing GenServer for better concurrency
Fixed
- Credo strict compliance: implicit
try, redundantwithclauses - Dialyzer: unreachable pattern matches in PaymentIdentifier and SIWX Verifier
[0.1.0] - 2026-02-14
Added
X402.PaymentRequired— encode/decodePAYMENT-REQUIREDheaders (Base64 JSON)X402.PaymentSignature— decode/validatePAYMENT-SIGNATUREheadersX402.PaymentResponse— encodePAYMENT-RESPONSEsettlement headersX402.Facilitator— GenServer client for facilitator/verifyand/settleendpointsX402.Facilitator.HTTP— HTTP transport with retry logic and telemetryX402.Plug.PaymentGate— drop-in Plug middleware for payment gatingX402.Wallet— EVM and Solana wallet address validation- Comprehensive test suite with >90% coverage
- Full ExDoc documentation with guides