Hex.pm Downloads Docs CI Coverage License

The Elixir SDK for the x402 HTTP payment protocol.

x402 is an open standard for internet-native payments built around the HTTP 402 Payment Required status code. This library provides everything you need to accept or make x402 payments in Elixir.

Features

  • Protocol primitives — encode/decode PAYMENT-REQUIRED, PAYMENT-SIGNATURE, and PAYMENT-RESPONSE headers
  • Facilitator client — verify and settle payments via any x402 facilitator
  • Plug middleware — drop-in payment gate for Phoenix and Plug apps
  • Lifecycle hooks — before/after/failure callbacks for verify & settle flows
  • Payment identifier — idempotency extension with pluggable cache
  • "upto" scheme — max-price bidding for flexible payments
  • SIWX (Sign-In-With-X) — wallet-authenticated repeat access without repayment
  • Wallet validation — EVM (Ethereum, Base, Polygon) and Solana address validation
  • Zero lock-in — works with any facilitator, any chain, any framework
  • Fully typed — comprehensive typespecs and Dialyzer-clean

Installation

Add x402 to your list of dependencies in mix.exs:

def deps do
  [
    {:x402, "~> 0.3"},
    {:finch, "~> 0.19"},        # HTTP client (required for facilitator calls)
    {:ex_secp256k1, "~> 0.8"},  # Only if using SIWX signature verification
    {:ex_keccak, "~> 0.7"}      # Only if using SIWX signature verification
  ]
end

finch, ex_secp256k1, and ex_keccak are optional. Only add what you need.

Quick Start

Accept payments in Phoenix

# In your router or endpoint
plug X402.Plug.PaymentGate,
  facilitator_url: "https://x402-facilitator-app.fly.dev",
  routes: %{
    "GET /api/weather" => %{
      price: "0.005",
      network: "eip155:8453",
      pay_to: "0xYourWalletAddress",
      description: "Weather data API"
    }
  }

That's it. Requests without payment get a 402 response with payment instructions. Requests with a valid PAYMENT-SIGNATURE header are verified and passed through.

With lifecycle hooks

defmodule MyApp.PaymentHooks do
  @behaviour X402.Hooks

  @impl true
  def before_verify(context, _metadata) do
    IO.inspect(context.payment, label: "Incoming payment")
    {:ok, context}
  end

  @impl true
  def after_verify(context, _metadata) do
    # Log successful verification, trigger webhooks, etc.
    {:ok, context}
  end

  @impl true
  def after_settle(context, _metadata) do
    # Post-settlement logic: update DB, send receipt
    {:ok, context}
  end

  @impl true
  def on_verify_failure(context, _metadata), do: {:ok, context}

  @impl true
  def before_settle(context, _metadata), do: {:ok, context}

  @impl true
  def on_settle_failure(context, _metadata), do: {:ok, context}
end

# Use in PaymentGate
plug X402.Plug.PaymentGate,
  facilitator_url: "https://x402-facilitator-app.fly.dev",
  hooks: MyApp.PaymentHooks,
  routes: %{...}

"upto" scheme — flexible pricing

# Server: accept up to a max price (agent bids what they're willing to pay)
plug X402.Plug.PaymentGate,
  facilitator_url: "https://x402-facilitator-app.fly.dev",
  routes: %{
    "GET /api/premium" => %{
      scheme: "upto",
      maxPrice: "1.00",
      network: "eip155:8453",
      pay_to: "0xYourWallet",
      description: "Premium data — pay what you want up to $1"
    }
  }

# Encode/decode upto payment requirements
{:ok, header} = X402.PaymentRequired.encode(%{
  accepts: [%{
    scheme: "upto",
    network: "eip155:8453",
    maxPrice: "1.00",
    pay_to: "0xYourWallet"
  }],
  description: "Pay up to $1"
})

Payment identifier — idempotency

Prevent duplicate payments by attaching unique payment IDs:

# Start the ETS cache in your supervision tree
children = [
  {X402.Extensions.PaymentIdentifier.ETSCache, []}
]

# Encode a payment ID into a payload
{:ok, encoded} = X402.Extensions.PaymentIdentifier.encode("pay-abc-123")

# Decode it back
{:ok, payment_id} = X402.Extensions.PaymentIdentifier.decode(encoded)
#=> {:ok, "pay-abc-123"}

# Check/store in cache for deduplication
:ok = X402.Extensions.PaymentIdentifier.ETSCache.put("pay-abc-123")
{:ok, true} = X402.Extensions.PaymentIdentifier.ETSCache.exists?("pay-abc-123")

SIWX — Sign-In-With-X (repeat access)

Let paying users prove wallet ownership to access content again without repaying:

# 1. Build a SIWX challenge message (CAIP-122 / EIP-4361)
message = X402.Extensions.SIWX.build_message(%{
  domain: "api.example.com",
  address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
  uri: "https://api.example.com/premium",
  chain_id: 8453,
  nonce: X402.Extensions.SIWX.generate_nonce(),
  statement: "Sign in to access premium content"
})

# 2. Verify the wallet's signature (EVM)
{:ok, true} = X402.Extensions.SIWX.Verifier.Default.verify_signature(
  message,
  signature_hex,
  "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
)

# 3. Store access record with TTL
{:ok, _pid} = X402.Extensions.SIWX.ETSStorage.start_link([])

:ok = X402.Extensions.SIWX.ETSStorage.put(
  "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
  "/premium",
  %{tx: "0xabc..."},
  :timer.hours(24)  # 24-hour access
)

# 4. Check if wallet has valid access
{:ok, record} = X402.Extensions.SIWX.ETSStorage.get(
  "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
  "/premium"
)

Encode/decode headers manually

# Build a PAYMENT-REQUIRED response
{:ok, header} = X402.PaymentRequired.encode(%{
  accepts: [%{
    scheme: "exact",
    network: "eip155:8453",
    price: "0.01",
    pay_to: "0xYourWallet"
  }],
  description: "Premium API access"
})

# Decode an incoming PAYMENT-SIGNATURE
{:ok, payload} = X402.PaymentSignature.decode(signature_header)

# Verify payment via facilitator
{:ok, result} = X402.Facilitator.verify(payload, requirements)

Verify payments programmatically

# Start the facilitator client in your supervision tree
children = [
  {Finch, name: MyApp.Finch},
  {X402.Facilitator, name: MyApp.Facilitator, finch: MyApp.Finch}
]

# Then verify payments
{:ok, %{status: 200}} = X402.Facilitator.verify(
  MyApp.Facilitator,
  payment_payload,
  payment_requirements
)

Architecture

          
  AI Agent      Your API + x402       Facilitator 
  (payer)       PaymentGate Plug      (verify/    
                                   settle)    
                               
                       Hooks            
                       PaymentID        
                       SIWX Storage     
                          
                    

Documentation

Full documentation is available at HexDocs.

x402 Protocol

x402 is an open standard by Coinbase for HTTP-native payments:

  1. Client requests a paid resource
  2. Server returns 402 Payment Required with pricing info
  3. Client pays (USDC on Base, Solana, etc.)
  4. Client retries with PAYMENT-SIGNATURE header
  5. Server verifies via facilitator and serves the resource

Learn more at x402.org and docs.x402.org.

License

MIT License — see LICENSE for details.