Hex.pm Downloads Docs CI License

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, and PAYMENT-RESPONSE headers
  • Complete v2 payment-requirement and extension-echo validation
  • Facilitator /verify and /settle client 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:

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.