X402
The Elixir SDK for the x402 HTTP payment protocol.
X402 is a library, not an application. It provides protocol headers, a facilitator client, and optional Plug middleware without tying an application to a specific facilitator, chain, or web framework.
Features
- x402 v2
PAYMENT-REQUIRED,PAYMENT-SIGNATURE, andPAYMENT-RESPONSEheaders with complete requirement and extension-echo validation - Payer client signing
"exact"(EIP-3009 or Permit2, selected by the server'sassetTransferMethod), metered"upto"(Permit2), and Solana"exact"payments, with an automatic402 → sign → retryFinch flow, spend controls (per-payment caps, selection policies, a shared session budget), lifecycle hooks, and automatic Sign-In-With-X - Plug/Phoenix payment gate with signature-bound replay protection, optional inline local verification, settlement only after successful resource handling, dynamic per-request pricing,
:paramroute templates, and request lifecycle hooks (on_protected_request,on_verified_payment_canceled) - Local payment verification without trusting a facilitator: EVM (EIP-712 + ERC-1271/6492, balance and simulation checks) and Solana (Ed25519, fee-payer isolation, instruction whitelist)
- Facilitator
/verifyand/settleclient with retries, hooks, telemetry, ordered failover with conservative settlement handling, and theEXTENSION-RESPONSESsidechannel - A runnable facilitator server for EVM and Solana from one Plug — EIP-3009, Permit2
exact, anduptosettlement through the x402 Permit2 proxies, ERC-6492 counterfactual settlement, ERC-20 Transfer-event proof of delivery, pending-settlement reconciliation, per-extension outcome reporting - Paid MCP tools over the x402 MCP transport, server and client side
- Browser paywall: a self-contained HTML 402 page with an EIP-1193 wallet flow
- Pluggable payment schemes through the
X402.Schemebehaviour - Extensions in their spec formats: payment-identifier idempotency with request binding (ETS or Redis), Sign-In-With-X wallet authentication for EVM and Solana (pay once, sign in after), builder-code attribution (ERC-8021), signed offers and receipts, gas sponsoring, Bazaar discovery with validated service metadata, dynamic-route templates, and natural-language search
- Extension adapters (
X402.Extension) that advertise, validate, and observe an extension from a single gate option - Authentication hints and HTTP message signature advertisements, with bounded RFC 9421 signing/verification and an optional public key-directory Plug. Applications enforce authentication, key trust, and replay policy
- Optional verified-payer rate limits, telemetry metric definitions, and local statistics, without a Phoenix dependency
- EVM auth-capture: explicit-consent execution, durable-store contracts, escrow resources, and local Plug/MCP adapters with synchronous or deferred metering. Production storage and recovery scheduling remain application-owned
- Optional Finch, Plug, Redix, and cryptography dependencies
Installation
Add the library and only the optional integrations your application uses:
def deps do
[
{:x402, "~> 0.9.0"},
{:finch, "~> 0.19"}, # facilitator HTTP calls
{:plug, "~> 1.14"} # PaymentGate
]
end
Add ex_secp256k1 and ex_keccak only when using the default EVM SIWX
signature verifier.
Phoenix quick start
Start Finch, the facilitator client, and the idempotency cache in your application supervision tree:
children = [
{Finch,
name: MyApp.Finch,
pools: %{default: X402.Facilitator.HTTP.secure_pool_opts()}},
{X402.Facilitator,
name: MyApp.Facilitator,
url: "https://facilitator.example.com",
finch: MyApp.Finch},
{X402.Extensions.PaymentIdentifier.ETSCache, name: MyApp.PaymentCache}
]
Configure the Plug with the facilitator process, not a URL:
plug X402.Plug.PaymentGate,
facilitator: MyApp.Facilitator,
payment_identifier_cache: MyApp.PaymentCache,
routes: [
%{
method: :get,
path: "/api/weather",
scheme: "exact",
price: "10000", # atomic units: 0.01 USDC when the asset has 6 decimals
network: "eip155:8453",
asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
pay_to: "0xYourWalletAddress",
description: "Weather data API"
}
]
An unpaid request receives HTTP 402 and a Base64-encoded v2
PAYMENT-REQUIRED header. A paid request is decoded and matched against the
complete advertised requirement, verified, passed to the protected handler,
and settled immediately before a successful response is sent. Handler responses
with status 400 or greater are not settled.
The verified payload and matched requirement are available to the handler as
conn.assigns.x402_payment_payload and
conn.assigns.x402_payment_requirements.
Run your own facilitator
The SDK also implements the facilitator role itself — verify and settle payments on-chain instead of delegating to a hosted service:
{:ok, engine} =
X402.Facilitator.Engine.new(rpc: rpc, signer: signer, networks: ["eip155:84532"])
children = [
{Bandit, plug: {X402.Plug.Facilitator, engine: engine}, port: 4022}
]
That serves POST /verify, POST /settle, and GET /supported over the
standard facilitator wire protocol. Pass engines: [evm_engine, svm_engine]
instead of engine: to serve EVM and Solana (X402.Facilitator.SVMEngine)
from the same endpoint. See the
Run Your Own Facilitator guide.
Metered "upto" payments
For an "upto" route, price is the maximum authorization in atomic token
units:
%{
method: :post,
path: "/api/generate",
scheme: "upto",
price: "1000000", # authorize up to 1 USDC for a 6-decimal asset
network: "eip155:8453",
asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
pay_to: "0xYourWalletAddress"
}
After measuring resource use, store the actual amount on the connection before building the response:
def create(conn, params) do
result = generate(params)
actual_atomic_amount = billable_amount(result)
{:ok, conn} =
X402.Plug.PaymentGate.put_settlement_amount(conn, actual_atomic_amount)
json(conn, %{result: result})
end
The facilitator receives the advertised maximum during /verify and the actual
amount during /settle. An amount above the authorized maximum fails closed.
If no actual amount is supplied, the advertised maximum is settled.
This release implements the post-handler authorization flow used by current
EVM exact and upto schemes. Route options declaring paymentFlow: "upfront"
or paymentFlow: "escrow" are rejected because those flows require different
handler and cancellation semantics.
Lifecycle hooks
Hooks receive an X402.Hooks.Context and must use the return contract defined by
X402.Hooks:
defmodule MyApp.PaymentHooks do
@behaviour X402.Hooks
@impl true
def before_verify(context, _metadata) do
IO.inspect(context.payload, label: "Incoming payment")
{:cont, context}
end
@impl true
def after_verify(context, _metadata), do: {:cont, context}
@impl true
def on_verify_failure(context, _metadata), do: {:cont, context}
@impl true
def before_settle(context, _metadata), do: {:cont, context}
@impl true
def after_settle(context, _metadata), do: {:cont, context}
@impl true
def on_settle_failure(context, _metadata), do: {:cont, context}
end
Pass the module with hooks: MyApp.PaymentHooks. Before hooks may return
{:halt, reason}; failure hooks may return {:recover, result}.
Multiple payment options
Use accepts to advertise more than one valid requirement:
%{
method: :get,
path: "/api/data",
accepts: [
%{
scheme: "exact",
price: "10000",
network: "eip155:8453",
asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
pay_to: "0xYourWalletAddress"
},
%{
scheme: "exact",
price: "5000",
network: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
asset: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
pay_to: "YourSolanaAddress"
}
]
}
Every core field must match exactly. Client-added metadata is allowed only
under accepted.extra and cannot remove or mutate values advertised by the
server.
Facilitator API
The lower-level client can be called directly:
case X402.Facilitator.verify(
MyApp.Facilitator,
payment_payload,
payment_requirements
) do
{:ok, %{status: 200, body: %{"isValid" => true} = result}} ->
{:ok, result}
{:ok, %{status: 200, body: %{"isValid" => false} = result}} ->
{:error, result}
{:error, reason} ->
{:error, reason}
end
Facilitator requests use the v2 wire object:
%{"x402Version" => 2, "paymentPayload" => payload, "paymentRequirements" => requirements}.
HTTP outcomes
X402.Plug.PaymentGate follows the v2 HTTP transport mapping:
| Status | Meaning |
|---|---|
| 400 | Malformed or invalid payment input |
| 402 | Payment required, unmatched terms, or verification/settlement failure |
| 429 | Verified payment exceeds the configured request rate limit |
| 500 | Facilitator transport failure, malformed facilitator response, or internal payment-processing error |
Documentation
- Getting Started
- Paying for Resources
- Authentication Extensions
- Plug/Phoenix Integration
- Custom Payment Schemes
- Paid MCP Tools
- Browser Paywall
- Local Payment Verification
- Run Your Own Facilitator
- Live Smoke Tests
- API Reference
- Official x402 v2 specification
- Official HTTP transport
License
MIT License — see LICENSE for details.