Menu

Explorer & Settings

Tempo Explorer Submit Project
Back to all projects
X

x402-facilitator

by alcedo · Updated Apr 2, 2026

x402-facilitator in golang

0
Stars
0
Forks
Go
Language
Mar 28, 2026
Created

In the AI payments ecosystem

x402-facilitator is an early-stage Go project in the AI payments / x402 ecosystem. It currently has 0 GitHub stars and 0 forks.

README.md View on GitHub →

x402-facil

A production-ready x402 facilitator in Go — implements the x402 HTTP payment protocol with multi-chain, multi-ERC20 token support.

What is x402?

x402 is a protocol for machine-native payments over HTTP. When a client requests a resource that costs money, the server responds with 402 Payment Required and a description of what it accepts. The client creates a cryptographically signed payment authorization and retries the request with it in a header. The facilitator is a third-party service that:

  1. Verifies the payment authorization is valid (correct signature, sufficient balance, not expired).
  2. Settles the payment by submitting the on-chain transaction.

Features

  • Full x402 v2 protocol support
  • EIP-3009 transferWithAuthorization — used by USDC, EURC, and other Circle tokens
  • Permit2 support — for any ERC-20 token without native EIP-3009
  • Configurable: add any ERC-20 on any EVM chain via config.yaml
  • EIP-712 signature verification (off-chain, before hitting the RPC)
  • On-chain balance checks before verification passes
  • Structured JSON logging
  • Graceful shutdown

Endpoints

Method Path Description
POST /verify Verify a payment without settling
POST /settle Verify and settle a payment on-chain
GET /supported List supported chains/tokens
GET /health Liveness probe

POST /verify

// Request
{
  "x402Version": 2,
  "paymentPayload": {
    "x402Version": 2,
    "payload": {
      "signature": "0x...",
      "authorization": {
        "from": "0xPAYER",
        "to": "0xRECIPIENT",
        "value": "1000000",
        "validAfter": "1700000000",
        "validBefore": "1700000060",
        "nonce": "0xRANDOM32BYTES"
      }
    },
    "accepted": {
      "scheme": "exact",
      "network": "eip155:8453",
      "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
      "amount": "1000000",
      "payTo": "0xRECIPIENT",
      "maxTimeoutSeconds": 60,
      "extra": {
        "name": "USD Coin",
        "version": "2",
        "assetTransferMethod": "eip3009"
      }
    }
  },
  "paymentRequirements": {
    "scheme": "exact",
    "network": "eip155:8453",
    "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
    "amount": "1000000",
    "payTo": "0xRECIPIENT",
    "maxTimeoutSeconds": 60
  }
}

// Response (valid)
{
  "isValid": true,
  "payer": "0xPAYER"
}

// Response (invalid)
{
  "isValid": false,
  "invalidReason": "insufficient_balance",
  "invalidMessage": "have 500000, need 1000000"
}

POST /settle

Same request shape as /verify, including the top-level x402Version field. On success:

{
  "success": true,
  "payer": "0xPAYER",
  "transaction": "0xTX_HASH",
  "network": "eip155:8453"
}

GET /supported

{
  "kinds": [
    {
      "x402Version": 2,
      "scheme": "exact",
      "network": "eip155:8453",
      "extra": {
        "assetTransferMethod": "eip3009",
        "name": "USD Coin",
        "version": "2"
      }
    }
  ],
  "signers": {
    "eip155:*": ["0xFACILITATOR_ADDRESS"]
  }
}

Configuration

Edit config.yaml. Every field in extra of paymentRequirements is driven by your config.

server:
  host: "0.0.0.0"
  port: 8080

facilitator:
  # Set via env var instead of storing here:
  private_key: "${FACILITATOR_PRIVATE_KEY}"

chains:
  - network: "eip155:8453"          # CAIP-2 identifier
    rpc_url: "https://mainnet.base.org"
    tokens:
      - name: "USD Coin"            # EIP-712 domain name
        symbol: "USDC"
        address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
        decimals: 6
        eip712_version: "2"         # EIP-712 domain version
        transfer_method: "eip3009"  # or "permit2"

      - name: "Wrapped Ether"
        symbol: "WETH"
        address: "0x4200000000000000000000000000000000000006"
        decimals: 18
        eip712_version: "1"
        transfer_method: "permit2"

Adding a new token

Simply add an entry under the relevant chain's tokens array. The only requirements:

  • transfer_method must be "eip3009" (token has transferWithAuthorization) or "permit2" (any ERC-20 with Permit2 approval).
  • eip712_version must match the token contract's EIP-712 domain version — check the token's contract or Circle docs. USDC v2 = "2".
  • name must exactly match the token's EIP-712 domain name (e.g. "USD Coin" not "USDC").

Adding a new chain

Add a new entry to chains:

  - network: "eip155:10"           # Optimism
    rpc_url: "https://mainnet.optimism.io"
    tokens:
      - name: "USD Coin"
        symbol: "USDC"
        address: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85"
        decimals: 6
        eip712_version: "2"
        transfer_method: "eip3009"

The facilitator wallet must hold native gas tokens (ETH, MATIC, etc.) on each chain where settlement is needed.

Running

Prerequisites

  • Go 1.22+
  • A funded Ethereum wallet private key (for settlement; verify-only mode works without it)

Build and run

# Install dependencies
go mod tidy

# Build
go build -o facilitator ./cmd/facilitator

# Run with config
FACILITATOR_PRIVATE_KEY=0xYOUR_PRIVATE_KEY ./facilitator -config config.yaml

Docker

FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY . .
RUN go mod tidy && go build -o facilitator ./cmd/facilitator

FROM alpine:3.19
COPY --from=builder /app/facilitator /facilitator
COPY config.yaml /config.yaml
ENV FACILITATOR_PRIVATE_KEY=""
EXPOSE 8080
ENTRYPOINT ["/facilitator", "-config", "/config.yaml"]

Verify-only mode

If you don't set FACILITATOR_PRIVATE_KEY, the /verify endpoint works fully but /settle returns:

{
  "success": false,
  "errorReason": "no_signer_configured"
}

Payment Flow

Client                    Resource Server              Facilitator
  │                             │                           │
  │──── GET /resource ─────────▶│                           │
  │                             │                           │
  │◀─── 402 Payment Required ───│                           │
  │     PAYMENT-REQUIRED: b64   │                           │
  │                             │                           │
  │  [client signs EIP-712      │                           │
  │   TransferWithAuthorization]│                           │
  │                             │                           │
  │──── GET /resource ─────────▶│                           │
  │     PAYMENT-SIGNATURE: b64  │                           │
  │                             │──── POST /verify ────────▶│
  │                             │◀─── {isValid:true} ───────│
  │                             │──── POST /settle ────────▶│
  │                             │                           │──▶ on-chain tx
  │                             │◀─── {success:true, tx} ───│
  │◀─── 200 OK ─────────────────│                           │
  │     PAYMENT-RESPONSE: b64   │                           │

EIP-3009 Verification Logic

For each incoming payment the facilitator checks (in order):

  1. x402Version == 2 and scheme == "exact"
  2. Token is configured for the given network
  3. authorization.value == requirements.amount (exact match)
  4. authorization.to == requirements.payTo (case-insensitive)
  5. validAfter <= now (not used before window)
  6. now + 6s < validBefore (not expired; 6s buffer for tx landing)
  7. EIP-712 signature recovers to authorization.from
  8. balanceOf(from) >= amount (on-chain RPC call)

Permit2

Tokens without native EIP-3009 support use Uniswap Permit2. The user:

  1. Approves the Permit2 contract (0x000000000022D473030F116dFC393fb4147B33FF) to spend their tokens.
  2. Signs a PermitTransferFrom message.

The facilitator verifies the signature and checks both the ERC-20 allowance to Permit2 and the user's balance.

Note: Permit2 settlement (/settle) is not yet implemented — the signed authorization must be submitted via the x402ExactPermit2Proxy contract. Verify works fully.

Error Codes

Code Meaning
invalid_payload Malformed JSON payload
unsupported_version x402Version ≠ 2
unsupported_scheme scheme ≠ "exact"
no_facilitator_for_network Chain not in config
unsupported_asset Token not in config
invalid_signature Signature verification failed
amount_mismatch Payment amount doesn't match requirement
recipient_mismatch authorization.torequirements.payTo
payment_expired validBefore has passed
payment_not_yet_valid validAfter is in the future
insufficient_balance Payer lacks token balance
no_signer_configured Settlement called without private key
transaction_failed On-chain tx reverted or failed to send
All Go libraries →