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 - Complete v2 payment-requirement and extension-echo validation
- Facilitator
/verifyand/settleclient with retries, hooks, and telemetry - Plug/Phoenix payment gate that settles only after successful resource handling
"exact"and metered"upto"authorization flows- Optional payment-identifier idempotency cache and SIWX support
- EVM and Solana wallet validation
- Optional Finch, Plug, and cryptography dependencies
Installation
Add the library and only the optional integrations your application uses:
def deps do
[
{:x402, "~> 0.4"},
{: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.
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 |
| 500 | Facilitator transport failure, malformed facilitator response, or internal payment-processing error |
Documentation
- Getting Started
- Plug/Phoenix Integration
- API Reference
- Official x402 v2 specification
- Official HTTP transport
License
MIT License — see LICENSE for details.