X402

Hex.pmDownloadsDocsCILicense

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

Installation

Add the library and only the optional integrations your application uses:

def deps do
[
{:x402, "~> 0.6.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:

StatusMeaning
400Malformed or invalid payment input
402Payment required, unmatched terms, or verification/settlement failure
500Facilitator transport failure, malformed facilitator response, or internal payment-processing error

Documentation

License

MIT License — see LICENSE for details.