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.9.0] - 2026-09-24
This release includes all changes merged since 0.6.1: the spec-conformance and ecosystem-parity work originally planned for 0.7.0 and 0.8.0, plus auth-capture, authentication, operations, and documentation cleanup. Versions 0.7.0 and 0.8.0 were not published separately.
Added
- EVM auth-capture: EIP-3009/Permit2 signing, v1.0/v1.1 encoders, account-aware pinned verification, explicit-consent execution and receipt reconciliation, durable multi-key store contracts, and metered escrow resources. Plug/MCP adapters withhold output until synchronous settlement or durable deferred metering. Dispatched client budgets retain the maximum on all outcomes. The bundled ETS store is development-only; production durability, recovery scheduling, and refund funding remain application-owned.
- Transaction signer callback: dedicated
X402.Signer.sign_transaction/2dispatch for auth-capture execution, with complete type-2 transaction intent. Existing facilitator execution paths are unchanged. - Authentication extensions:
X402.Extensions.AuthHintsbuilds and readsauth-hintsdeclarations for OAuth2 and SIWX, scoped to accepted payment indexes.X402.Extensions.HTTPMessageSignaturesadvertises signature registration, algorithms, and tags. Both include gate adapters. These are advertisements, not automatic credential acquisition or authentication enforcement. - HTTP message signatures:
X402.HTTPSignatureimplements a bounded RFC 9421 request/response profile with Ed25519, ECDSA P-256, and RSA-PSS-SHA512, structured fields, required-coverage and time checks, response-to-request binding, and caller-controlled key lookup.X402.HTTPSignature.Keyimports/exports public JWKs and generates keys;X402.Plug.HTTPSignatureDirectoryserves a signed public directory with runtime rotation. Negotiation, automatic key discovery, content-digest validation, and nonce replay storage remain application responsibilities. - Operations controls: optional verified-payer/IP/custom-key rate
limits in the payment gate, a per-node ETS limiter, ordered facilitator
failover, optional
Telemetry.Metricsdefinitions, and local telemetry statistics. Denied payments return 429 without settlement. With fallbacks, settlement makes one HTTP attempt per endpoint and only fails over on proven non-delivery; TLS alerts, timeouts, and HTTP 5xx remain ambiguous. Single-endpoint retries are unchanged. - Permit2 transfer method for the
exactEVM scheme: requirements declaringextra.assetTransferMethod: "permit2"now run end to end alongside the default EIP-3009 flow.X402.Scheme.ExactEVMdispatches ontransfer_method/1(absent or"eip3009","permit2"; anything else —"erc7710"included — is{:unsupported_transfer_method, value}and never selected by the client).X402.Client.build_payment/3signs a Permit2PermitWitnessTransferFromthroughX402.Permit2.sign_exact/2— spenderx402ExactPermit2Proxy(exact_proxy_address/0), witnesspayTo, permitted amount the exactamount— producing a%{"signature", "permit2Authorization"}scheme payload;build_exact_authorization/2,domain/1, anddigest/2(the witness shape selects the EIP-712 type) generalise the upto helpers, which remain as aliases.X402.Scheme.EVM.permit2_precheck/3runs the local pre-checks (recipient, amount, token, spender, validity window) before the facilitator round-trip, andX402.Facilitator.verify/2andsettle/2reject an exact-Permit2 payload whosepermitted.amountdiffers from the requirements ({:invalid_exact_payment, :amount_mismatch}) without calling out.X402.Verify.EVMdetects the paymentkind(:eip3009,:permit2_exact,:permit2_upto) and runs the reference facilitator's Permit2 checklist in order at every level — scheme, network, spender, recipient, deadline,validAfter, amount, token, signature — with:fulladditionally requiring the proxy to be deployed, simulating the proxy'ssettle(uptoas the witness facilitator), classifying Permit2 / proxy custom errors by selector or revert text, and diagnosing unexplained reverts viaPERMIT2(),balanceOf, andallowance(payer, Permit2); thepermit2_*andupto_*reasons are new.X402.Facilitator.Enginesettles exact-Permit2 anduptothrough the x402 proxies with calldata fromX402.Permit2.exact_settle_calldata/2/upto_settle_calldata/3(the same encoders verification simulates with),supported/1advertisesexactanduptoper network, anduptois routed only whenextra.facilitatorAddressis the engine's signer.X402.Plug.PaymentGateselects replay identity using the matched requirements' transfer method, ignoring unsigned alternate authorization fields, and derives exact-Permit2 keys from the signed permit'sfrom+nonce; exact and upto share theevm-permit2:prefix because Permit2 nonces are per owner, not per spender. SIWX payer fallback uses that same requirements-bound authorization when settlement omitspayer - Resource-server lifecycle hooks —
X402.Hookson_protected_request/2andon_verified_payment_canceled/2: two optional callbacks invoked byX402.Plug.PaymentGateandX402.MCP.Serverwith anX402.Hooks.RequestContext(transport, conn or MCP request, matched route,method/path/path_paramsortool, and the requirements and extensions about to be advertised), mirroring the reference resource server'sonProtectedRequestandonVerifiedPaymentCanceled.on_protected_request/2runs before any payment processing on every request matching a gated route or paid tool and may continue with{:cont, context}— optionally replacingcontext.requirementsorcontext.extensionsfor that request, for a per-caller discount — answer directly with{:halt, {status, body}}(telemetryreason: {:hook_halted, status}), or let the handler run unpaid with{:halt, :skip_payment}([:x402, :plug, :pass_through]/[:x402, :mcp, :pass_through]withreason: :hook_skipped); an exception or invalid return fails closed with an internal error.on_verified_payment_canceled/2runs when a payment the facilitator verified is not settled — the handler answered a status of 400 or above or returned anisErrorresult (reason: :handler_failed), the MCP handler raised (:handler_raised), or settlement failed before a transaction was broadcast (:settlement_failed) — with its return value ignored and exceptions logged.X402.Hooks.Defaultimplements neither, so existing hook modules keep working unchanged - Extension adapters —
X402.Extension: a behaviour packaging one protocol extension's server-side lifecycle (key/0, optionalinit/1,advertise/2,validate/3,after_verify/4,after_settle/4) so the gate runs it from a single option.X402.Plug.PaymentGategains:extensions—[module | {module, opts}]— whose adapters advertise on every 402 (merged over each route's staticextensionsmap), validate the client's echo after the generic echo check (a failure answers 400invalid_payloadwith reason{:extension_invalid, key, reason}), and are notified with the facilitator's verify and settle results. Bundled adapters:X402.Extensions.PaymentIdentifier.Adapter(required:) andX402.Extensions.BuilderCode.Adapter(app_code:,service_codes:) - Builder-code spec format —
X402.Extensions.BuilderCode: the builder-code extension (ERC-8021 on-chain attribution). Servers advertise the app code and up to 5 service codes withextension/2; clients echo it and attach up to 5 service codes of their own throughenricher/1forX402.Client.build_payment/3's:extensionsoption (always: trueattachesseven when not advertised, nevera). Codes match^[a-z0-9_]{1,32}$(valid_code?/1); servers read an echo withextract/1and enforce the spec's echo rules withvalidate_echo/2.X402.Plug.PaymentGateandX402.MCP.Serverrun those rules on every payload echoingextensions["builder-code"], advertised or not: malformed codes and more than ten service codes are rejected with 400invalid_payload(reason{:invalid_builder_code, detail}), and an app code differing from the advertised one is an:extension_echo_mismatch. The codes travel to the facilitator inside the payload, which encodes them into the settlement calldata - Dynamic route pricing in
X402.Plug.PaymentGate: the route fields:price,:pay_to,:description, and:accepts— and:price/:pay_toinside each:acceptsentry — may be 1-arity functions of thePlug.Conn, evaluated once per gated request before the 402 advertisement and before the client'sacceptedis matched. A function returns the plain value or{:ok, value};{:error, reason}or an invalid value answers 500 and emits[:x402, :plug, :payment_rejected]withreason: {:dynamic_route_error, reason}, never reaching the client :paramroute patterns and the:bazaarroute option inX402.Plug.PaymentGate: a route:pathmay be a template with:paramsegments (/api/users/:id), each matching one non-empty segment; the captured values are assigned as:x402_path_params(%{"id" => "42"}) on every gated request, paid or not, and are available to dynamic pricing functions and hooks. The:bazaarroute option (a keyword list ofX402.Extensions.Bazaar.build_extension/1options) advertises the discovery extension underextensions["bazaar"]on every 402; for:paramroutes it carries the template as the top-levelrouteTemplatecatalog key and the captured values asinfo.input.pathParams. Globs cannot be advertised- Bazaar discovery search —
X402.Facilitator.search_resources/2andX402.Extensions.Bazaar.search/2: the natural-languageGET /discovery/searchendpoint. Parameters (:queryrequired;:type,:pay_to,:scheme,:network,:extensions,:limit,:cursor) are validated withNimbleOptionsand sent as query string parameters; the response (:resources,:partial_results,:paginationwith an opaquecursor) is validated fail-closed.X402.Extensions.Bazaar.search/2parses each entry into the same typed maps aslist_resources/2. Both accept a bare keyword list to target the default facilitator name - Client lifecycle hooks —
X402.Client.Hooks:before_payment/2(after selection, before signing — may replacecontext.requirementsor{:halt, reason}),after_payment/2(may replacecontext.payload), andon_payment_failure/2(may replace the error or{:recover, payload}), mirroring the reference client'sonBeforePaymentCreation/onAfterPaymentCreation/onPaymentCreationFailure, withX402.Client.Hooks.Contextand the no-opX402.Client.Hooks.Default. Passed ashooks:toX402.Client.build_payment/3,X402.Client.Finch.request/3, andX402.MCP.Client.call/3. Hook errors surface as{:hook_halted, callback, reason},{:hook_callback_failed, callback, reason}, or{:hook_invalid_return, callback, value} - Client spend controls —
X402.Client.PolicyandX402.Client.Budget: thepolicies:option ofX402.Client.select_requirements/2(and every function and driver built on it) takes 2-arity functions of the candidate requirements and thePaymentRequiredmap returningtrue,false, or{:error, reason}; every policy must accept an entry for it to be selected. Ready-made ones:max_amount/1,networks/1(trailing*wildcard),assets/1(case-insensitive), andschemes/1.X402.Client.Budgetis a session budget process (start_link/1with:limitand optional:per_asset,reserve/3,release/3,spent/1); thebudget:driver option reserves the selected amount before the paid retry is sent, fails with{:error, {:budget_exceeded, details}}when a limit would be exceeded, and releases the reservation when the payment is not accepted. When none ofmax_amount:,policies:, orbudget:is configured,X402.Client.FinchandX402.MCP.Clientlog a one-time warning that the client will sign any amount a server asks for - Automatic Sign-In-With-X on the client —
X402.Client.SIWX: thesiwx:option ofX402.Client.Finch.request/3andX402.MCP.Client.call/3(chain_id:— a CAIP-2 chain or:autoto pick the first advertisedsupportedChainsentry the signer can sign — plus optionaladdress:,signature_scheme:,domain:, anduri:. MCP requires independent domain and exact URI pins; HTTP defaults its domain to the resource URL's host). MCP consent runs before signing initial or refreshed proofs, once per challenge. When the 402 advertises asign-in-with-xchallenge the client signs it (X402.Client.SIWX.authenticate/4, refusing challenges not bound to the expected origin, or lacking a trusted domain/resource URL; domain matching ignores case without dropping port boundaries) and retries with the proof and no payment; a response that is not payment-required is returned withsiwx_authenticated: true, otherwise the payment flow continues with the proof attached to the paid request too. MCP proofs travel in request_meta["x402/sign-in-with-x"](X402.MCP.siwx_meta_key/0,put_siwx/2,fetch_siwx/1). Each attempt emits[:x402, :client, :siwx]with:transport,:chain_id, and:outcome(:authenticatedor:payment_required) or:reason; failures surface as{:error, {:siwx, reason}} - Sign-In-With-X spec format —
X402.Extensions.SIWX: the sign-in-with-x extension as the spec defines it, for EVM and Solana wallets. Servers advertise a CAIP-122 challenge underPaymentRequired.extensions["sign-in-with-x"](challenge/1/X402.Extensions.SIWX.Challenge: fresh nonce and timestamps per 402,supportedChainswitheip191/ed25519types, and the proof JSON schema); clients sign it with anyX402.Signer(sign/3— EIP-4361 text signed with EIP-191personal_signoneip155:*, Sign-In-With-Solana text signed with Ed25519 onsolana:*, message construction inX402.Extensions.SIWX.Message) and send the proof fields Base64-encoded inSIGN-IN-WITH-X(encode_signed/1); servers decode (decode_signed/1) and verify (verify/2) them in the spec's check order, each failure carrying one of the spec's machine-readable codes (invalid_siwx_domain_mismatch,invalid_siwx_uri_mismatch,invalid_siwx_issued_at_too_old,invalid_siwx_expired,invalid_siwx_nonce,invalid_siwx_unsupported_chain,invalid_siwx_signature, ...). Nonces are tracked through anyX402.Extensions.PaymentIdentifier.Cacheadapter (:nonce_cache): a proof verifies only against a nonce the server issued and is consumed atomically, so it authenticates at most once.X402.Extensions.SIWX.Verifier.Ed25519verifies Solana proofs over OTP:crypto(small-order public keys refused); the existingX402.Extensions.SIWX.Verifier.Defaultcovers EVM X402.Extensions.SIWX.Server: the server side in one struct —new/1validates:domain,:uri,:supported_chains,:statement,:resources,:expiration_seconds,:max_age_seconds,:clock_skew_seconds,:nonce_cache,:storage,:ttl_ms,:verifier, and:ed25519_verifier;challenge/1issues (and records) a challenge,verify/2checks a proof,record_payment/4remembers which address paid for which resource after settlement, andauthenticate/3combines verification with that history ({:error, :not_authorized}when the wallet has not paid). Usable from any framework. EVM addresses are case-insensitive for payment records, lookup and revocation; Solana public keys retain their caseX402.Plug.PaymentGate:siwxoption (a keyword list ofX402.Extensions.SIWX.Server.new/1options): every 402 advertises a fresh challenge (exempt from the extension echo check because it changes per response); a request carryingSIGN-IN-WITH-Xis authenticated against the HTTP method and full resource URL — a previously paying address runs the handler without payment, with:x402_siwx_addressand:x402_siwx_chain_idassigned and[:x402, :plug, :siwx_authenticated]emitted; an address with no record falls through to the normal payment flow whenPAYMENT-SIGNATUREis also present and otherwise receives 402 with a fresh challenge; a proof that fails verification receives 402 with theinvalid_siwx_*code as theerrorstring (telemetryreason: {:siwx, code}); an undecodable header receives 400invalid_siwx_header. After a successful settlement the payer (the settle response'spayer, falling back to the authorization'sfrom) is recorded for the resource URL through:storagefor:ttl_msX402.Signer.sign_message/2and the optionalX402.Signer.sign_message/2signer callback: EIP-191personal_signover an arbitrary message, returning the0x-prefixed 65-byter || s || vhex signature (vnormalized to 27/28).X402.Signer.LocalKeyimplements it; signers without it return{:error, :unsupported_signer}- Payment-identifier spec format —
X402.Extensions.PaymentIdentifier: the payment-identifier extension under its spec key"payment-identifier". Servers advertise it withextension/1(info.requiredplus the JSONschema/0); clients echo the advertisement and addinfo.idthrough the newenricher/1forX402.Client.build_payment/3's:extensionsoption (freshgenerate_id/0per payment, or an explicit:id;always: trueattaches it even when not advertised). Ids are 16–128 characters of[A-Za-z0-9_-](valid_id?/1); servers read them withextract_id/1(tagged{:spec, id}or{:legacy, id}), checkrequired?/1, and bind them to a request throughfingerprint/2(SHA-256 over scheme, network, asset, amount,payTo, HTTP method, path, and MCP tool name) - Payment-identifier enforcement in
X402.Plug.PaymentGateandX402.MCP.Server: a malformed spec id is rejected with 400invalid_payload; a missing id underrequired: truewith 400payment_identifier_required. With a:payment_identifier_cache, each id is bound to its request fingerprint under a"pid:"-prefixed key: reusing it for a different request is rejected with 409payment_identifier_conflict, while the same request proceeds and stays subject to the signature-derived replay key. A binding is released together with the replay claim (handler status >= 400, verification or settlement failure), so the id may be retried. The MCP server reports the same three reasons in its payment-required result and fingerprints the tool name instead of method/path EXTENSION-RESPONSESsidechannel —X402.ExtensionResponses: encode/decode of the facilitator's per-extension outcome header (x402 v2 §7.2.1;encode/1,decode/1,from_headers/1,from_headers_lenient/1, 8 KB cap).X402.Facilitator.verify/2andsettle/2results now carry the response:headersand the decoded sidechannel as:extension_responses(nilwhen absent or malformed — a bad header is dropped with a[:x402, :extension_responses, :decode]telemetry event rather than failing the payment).X402.Plug.PaymentGateassigns the verify-time outcomes as:x402_extension_responsesand adds the settle-time outcomes as:extension_responsesto the[:x402, :plug, :payment_verified]metadata; the sidechannel is never forwarded to the buyer. Engines behindX402.Plug.Facilitatormay return{:ok, wire_response, extension_responses}fromverify/3/settle/3and the plug emits the header (an unencodable map is logged and dropped)- Bazaar service metadata —
X402.Extensions.Bazaar.Metadata: the bazaar spec's validation and soft-drop rules for provider metadata (valid_service_name?/1,sanitize_tags/1,valid_icon_url?/1,sanitize_resource/1) and dynamic-route templates (valid_route_template?/1,extract_route_template/1), mirroring the reference SDKs.X402.Extensions.Bazaar.build_extension/1gains:route_template, published as the extension's top-levelrouteTemplatecatalog key.X402.Plug.PaymentGatenow validates:service_name(non-empty printable ASCII, at most 32 characters),:tags(at most 5 unique case-insensitive entries under the same rule), and:icon_url(absolute http(s), no userinfo, no IP literal or loopback host, at most 2048 characters) at init and rejects invalid values
Changed
X402.Client.Finch.request/3andX402.MCP.Client.call/3responses gain:siwx_authenticated(truewhen the server accepted a Sign-In-With-X proof instead of a payment); code that pattern-matches the whole response map must allow it. Their error unions gain{:siwx, reason},{:budget_exceeded, details}, and theX402.Client.Hookshook errorsX402.Plug.PaymentGateandX402.MCP.Servermay now emit:pass_throughfor a matched route or paid tool — withreason: :hook_skippedwhenon_protected_request/2returned{:halt, :skip_payment}— where previously the event only meant "route did not match".:payment_rejectedgains the reasons{:hook_halted, status}and{:invalid_builder_code, detail}on both transports, and{:dynamic_route_error, reason}and{:extension_invalid, key, reason}on the gateX402.Clientno longer selects requirements whoseextra.paymentFlownames a flow it cannot run: only the default"authorization"flow (explicit or omitted) is recognized, soupfrontandescrowentries — which commit funds before the resource executes — are skipped, per spec §6.1. APAYMENT-REQUIREDoffering only such entries yields{:error, :no_acceptable_requirements}- Upgrading. Custom
X402.Extensions.PaymentIdentifier.Cacheadapters must accept two newvalueforms input_new/3andput/3:{:bound, fingerprint}(payment-id bindings, stored under"pid:"keys) and{:siwx_nonce, :issued | :used}(Sign-In-With-X nonces, under"siwx:issued:"/"siwx:used:"keys). Adapters that serialize values by pattern-matching on:verified/{:rejected, reason}need updating; the bundled ETS and Redis adapters already do.X402.Facilitator.verify/2andsettle/2results gain:headersand:extension_responseskeys (previously%{status: ..., body: ...}); code that pattern-matches the whole map must allow them, and hooks see them incontext.result
Deprecated
- The pre-0.7.0
"paymentIdentifier"extension format (a Base64 JSON{"paymentId": ...}string or a%{"paymentId" => ...}map, optionally wrapped in%{"info" => ...}). It is still accepted by the gate and the MCP server with the same assign, telemetry, and fingerprint binding — each one emits[:x402, :payment_identifier, :legacy]and the first logs a warning — and will be removed in 1.0.0. Legacy ids satisfyrequired: trueand are exempt from the spec's length and character rules.X402.Extensions.PaymentIdentifier.encode/1,decode/1, andfetch_payment_id/1are deprecated with it - The pre-0.7.0
SIGN-IN-WITH-Xheader format (a Base64 JSON{"message", "signature"}object carrying the signed EIP-4361 text).X402.Extensions.SIWX.decode_signed/1still reports it as{:legacy, proof}andverify/2applies the same checks to the exact text that was signed; the gate emits[:x402, :siwx, :legacy]per proof and logs a one-time warning. Removed in 1.0.0 together withX402.Extensions.SIWX.encode_header/1anddecode_header/1(encode/1anddecode/1remain as the EIP-4361 text codec)
Security
- Update the repository lockfile to Mint
1.10.1, which fixes CVE-2026-82672. Applications using Finch should runmix deps.update mintto update their own lockfiles; upgrading x402 alone does not update a locked transitive dependency. - Pin every GitHub Actions step in
.github/workflows/ci.ymlto a commit SHA (with the tag recorded in a comment) and grant the workflow least-privilegepermissions: contents: read
[0.6.1] - 2026-09-16
Fixed
- Accept Solana signers in
X402.Client.FinchandX402.MCP.Clientoption validation. Both clients now requireaddress/1plus eithersign_eip712/3orsign_ed25519/2, allowingX402.Signer.SolanaKeyto reach the SVM payment flow. Scheme-specific callback checks remain in the signer dispatcher.
Security
- Update the repository lockfile to Mint
1.10.0, which fixes CVE-2026-82728 and CVE-2026-82729. Applications using Finch should also runmix deps.update mintto update their own lockfiles; upgrading x402 alone does not ensure this transitive dependency is updated.
[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