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.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:

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.