Accept instant blockchain micropayments in your Rails applications using the x402 payment protocol.
x402-rails is a growing Ruby project in the AI payments / x402 ecosystem. It currently has 36 GitHub stars and 2 forks.
⚠️ Note: This gem now defaults to x402 protocol v2. If you need v1 compatibility, set
config.version = 1in your initializer. See Protocol Versions for details on the differences.
Accept instant blockchain micropayments in your Rails applications using the x402 payment protocol.
Supports 18 networks including Base, Polygon, Avalanche, Sei, Solana, and more.
https://github.com/user-attachments/assets/05983bb3-7422-4c06-97ab-2fb53d6428cc
Add to your Gemfile:
gem 'x402-rails'
Then run:
bundle install
Generate the initializer:
bin/rails generate x402:install
Then edit config/initializers/x402.rb:
X402.configure do |config|
config.wallet_address = ENV['X402_WALLET_ADDRESS'] # Your recipient wallet
config.facilitator = "https://www.x402.org/facilitator"
config.chain = "base-sepolia" # or "base" for base mainnet
config.currency = "USDC"
config.optimistic = false # Forces to check for settlement before giving response.
end
Use x402_paywall in any controller action:
class ApiController < ApplicationController
def weather
x402_paywall(amount: 0.001) # $0.001 in USD
return if performed?
render json: {
temperature: 72,
paid_by: request.env['x402.payment'][:payer]
}
end
end
That's it! Your endpoint now requires payment.
Declare discovery metadata and agents can find your route in facilitator catalogs (see Bazaar Discovery):
class ApiController < ApplicationController
x402_discovery only: :weather,
input: { "city" => "San Francisco" },
input_schema: { "properties" => { "city" => { "type" => "string" } } },
output: { example: { "temperature" => 72 } }
end
Indexing happens when a client pays: the extension rides the 402, the paying client echoes it, and the facilitator catalogs the route on settle.
Call x402_paywall in any action:
def show
x402_paywall(amount: 0.01)
return if performed?
# Action continues after payment verified
render json: @data
end
Protect multiple actions:
class PremiumController < ApplicationController
before_action :require_payment, only: [:show, :index]
def show
# Payment already verified
render json: @premium_content
end
private
def require_payment
x402_paywall(amount: 0.001, chain: "base")
return if performed?
end
end
Different prices for different actions:
def basic_data
x402_paywall(amount: 0.001)
return if performed?
render json: basic_info
end
def premium_data
x402_paywall(amount: 0.01)
return if performed?
render json: premium_info
end
Set defaults in config/initializers/x402.rb:
X402.configure do |config|
# Required: Your wallet address where payments will be received
config.wallet_address = ENV['X402_WALLET_ADDRESS']
# Facilitator service URL (default: "https://www.x402.org/facilitator")
config.facilitator = ENV.fetch("X402_FACILITATOR_URL", "https://www.x402.org/facilitator")
# Blockchain network (default: "base-sepolia")
# Built-in: base, base-sepolia, polygon, polygon-amoy, avalanche, avalanche-fuji,
# sei, sei-testnet, iotex, peaq, xlayer, xlayer-testnet,
# skale-base, skale-base-sepolia, kiteai, kiteai-testnet,
# solana, solana-devnet
config.chain = ENV.fetch("X402_CHAIN", "base-sepolia")
# Payment token (default: "USDC")
# Currently only USDC is supported
config.currency = ENV.fetch("X402_CURRENCY","USDC")
# Optimistic mode (default: false)
# true: Fast response, settle payment after response is sent
# false: Wait for blockchain settlement before sending response
config.optimistic = ENV.fetch("X402_OPTIMISTIC", "false") == "true"
end
| Attribute | Required | Default | Description |
|---|---|---|---|
wallet_address |
Yes | - | Your Ethereum wallet address where payments will be received |
facilitator |
No | "https://www.x402.org/facilitator" |
Facilitator service URL for payment verification and settlement |
chain |
No | "base-sepolia" |
Blockchain network (see built-in list above) |
currency |
No | "USDC" |
Payment token symbol (currently only USDC supported) |
optimistic |
No | false |
true: respond before settlement; false: settle before responding |
version |
No | 2 |
Protocol version (1 or 2). See Protocol Versions section |
cdp_api_key_id |
No | ENV["CDP_API_KEY_ID"] |
Coinbase CDP API key id — only used when the facilitator is CDP |
cdp_api_key_secret |
No | ENV["CDP_API_KEY_SECRET"] |
Coinbase CDP API key secret (ECDSA PEM or Ed25519 base64) |
You can register custom EVM chains and tokens beyond the built-in options.
Add support for any EVM-compatible chain beyond the 18 built-in networks:
X402.configure do |config|
config.wallet_address = ENV['X402_WALLET_ADDRESS']
# Register Arbitrum (not built-in)
config.register_chain(
name: "arbitrum",
chain_id: 42161,
standard: "eip155"
)
# Register the token for that chain
config.register_token(
chain: "arbitrum",
symbol: "USDC",
address: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
decimals: 6,
name: "USD Coin",
version: "2"
)
config.chain = "arbitrum"
config.currency = "USDC"
end
⚠️ Note: The Facilitator used must support the specified chain and token to ensure proper functionality.
Accept different tokens on existing chains:
X402.configure do |config|
config.wallet_address = ENV['X402_WALLET_ADDRESS']
# Accept WETH on Base instead of USDC
config.register_token(
chain: "base",
symbol: "WETH",
address: "0x4200000000000000000000000000000000000006",
decimals: 18,
name: "Wrapped Ether",
version: "1"
)
config.chain = "base"
config.currency = "WETH"
end
| Parameter | Required | Description |
|---|---|---|
chain |
Yes | Chain name (built-in or custom registered) |
symbol |
Yes | Token symbol (e.g., "USDC", "WETH") |
address |
Yes | Token contract address |
decimals |
Yes | Token decimals (e.g., 6 for USDC, 18 for WETH) |
name |
Yes | Token name for EIP-712 domain |
version |
No | EIP-712 version (default: "1") |
Note: Custom chains and tokens are only supported for EVM (eip155) networks. Solana chains use a different implementation.
Allow clients to pay on any of several supported chains by using config.accept():
X402.configure do |config|
config.wallet_address = ENV['X402_WALLET_ADDRESS']
# Register a custom chain not included in the built-in list
config.register_chain(name: "arbitrum", chain_id: 42161, standard: "eip155")
config.register_token(
chain: "arbitrum",
symbol: "USDC",
address: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
decimals: 6,
name: "USD Coin",
version: "2"
)
# Accept payments on multiple chains (built-in + custom)
config.accept(chain: "base-sepolia", currency: "USDC")
config.accept(chain: "polygon-amoy", currency: "USDC")
config.accept(chain: "arbitrum", currency: "USDC")
end
When config.accept() is used, the 402 response will include all accepted payment options:
{
"accepts": [
{ "network": "eip155:84532", "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", ... },
{ "network": "eip155:80002", "asset": "0x41E94Eb019C0762f9Bfcf9Fb1E58725BfB0e7582", ... }
]
}
Clients can then choose which chain to pay on based on their preferences or available funds.
Per-accept wallet addresses: You can specify different recipient addresses per chain:
config.accept(chain: "base-sepolia", currency: "USDC", wallet_address: "0xWallet1")
config.accept(chain: "polygon-amoy", currency: "USDC", wallet_address: "0xWallet2")
Fallback behavior: If no config.accept() calls are made, the default config.chain and config.currency are used.
x402-rails supports both v1 and v2 of the x402 protocol. v2 is the default.
| Feature | v1 (Legacy) | v2 (Default) |
|---|---|---|
| Network format | Simple names (base-sepolia) |
CAIP-2 (eip155:84532) |
| Payment header | X-PAYMENT |
PAYMENT-SIGNATURE |
| Response header | X-PAYMENT-RESPONSE |
PAYMENT-RESPONSE |
| Requirements | Body only | PAYMENT-REQUIRED header + body |
| Amount field | maxAmountRequired |
amount |
X402.configure do |config|
config.wallet_address = ENV['X402_WALLET_ADDRESS']
config.version = 2 # Default, can be omitted
end
v2 uses CAIP-2 network identifiers (eip155:84532) and the PAYMENT-SIGNATURE header. Payment requirements are sent in both the PAYMENT-REQUIRED header (base64-encoded) and the response body (JSON).
X402.configure do |config|
config.wallet_address = ENV['X402_WALLET_ADDRESS']
config.version = 1
end
v1 uses simple network names (base-sepolia) and the X-PAYMENT header. Payment requirements are sent only in the response body.
Override the version for specific endpoints:
def premium_v2
x402_paywall(amount: 0.001, version: 2)
return if performed?
render json: { data: "v2 endpoint" }
end
def legacy_v1
x402_paywall(amount: 0.001, version: 1)
return if performed?
render json: { data: "v1 endpoint" }
end
Facilitators with a discovery layer (PayAI, Coinbase CDP Bazaar) index your route into their public catalog when a payment carries the x402 Bazaar discovery extension. Declare it per action and the gem attaches it to every v2 402 for that action; paying clients echo it into their payment payload, and the gem forwards the echo to the facilitator on verify/settle — that's what creates the catalog entry.
Note: Parameterized routes (e.g.
/weather/:city) are cataloged per concrete URL — the gem does not emit the optionalrouteTemplatefield.
Omit body_type for GET/HEAD/DELETE — input describes query params:
class WeatherController < ApplicationController
x402_discovery only: :show,
input: { "city" => "San Francisco", "units" => "celsius" },
input_schema: {
"type" => "object",
"properties" => {
"city" => { "type" => "string", "description" => "City name" },
"units" => { "type" => "string", "enum" => ["celsius", "fahrenheit"] },
},
"required" => ["city"],
},
output: { example: { "weather" => "foggy", "temperature" => 15 } }
def show
x402_paywall(amount: 0.001)
return if performed?
render json: { weather: "foggy", temperature: 15 }
end
end
Pass body_type: "json" — input is now an example request body. Give every field a description: it's what agents read in the catalog to call you correctly.
class SearchController < ApplicationController
x402_discovery only: :create,
body_type: "json",
input: {
"query" => "solar panels",
"filters" => { "max_price" => 100 },
},
input_schema: {
"type" => "object",
"properties" => {
"query" => { "type" => "string", "description" => "Search terms" },
"filters" => {
"type" => "object",
"properties" => { "max_price" => { "type" => "number" } },
},
},
"required" => ["query"],
},
output: {
example: { "results" => [{ "title" => "…", "price" => 42 }] },
schema: { "properties" => { "results" => { "type" => "array" } } },
}
def create
x402_paywall(amount: 0.005)
return if performed?
render json: { results: search_results }
end
end
Each action gets its own declaration; undeclared actions emit no extension:
class ReportsController < ApplicationController
x402_discovery only: :create, body_type: "json",
input: { "ticker" => "AAPL" },
input_schema: { "properties" => { "ticker" => { "type" => "string" } }, "required" => ["ticker"] },
output: { example: { "report_id" => "rep_123" } }
x402_discovery only: :summary,
input: { "report_id" => "rep_123" },
input_schema: { "properties" => { "report_id" => { "type" => "string" } }, "required" => ["report_id"] },
output: { example: { "summary" => "…" } }
end
Build the extension yourself (e.g. to share schemas with your validators), or attach per-call:
x402_discovery only: :create, extensions: X402::DiscoveryExtension.declare(
body_type: "json",
input: MyApi::EXAMPLE_BODY,
input_schema: MyApi::BODY_SCHEMA,
output: { example: MyApi::EXAMPLE_RESPONSE },
)
# or, inside an action:
x402_paywall(amount: 0.005, extensions: my_extensions)
input must validate against input_schema — facilitators silently skip routes whose extension fails validation. Keep example and schema in sync.method is stamped from the actual request at render time — omit it; a declared value is overwritten.description: on x402_discovery sets the 402's resource.description — the text catalogs display for the route. A description alone just names the route; declaring input/output metadata is what makes it discoverable.Verify a listing:
X402::FacilitatorClient.new.discovery_resources(type: "http")
# or against a specific facilitator:
X402::FacilitatorClient.new("https://facilitator.payai.network").discovery_resources
Any x402 facilitator works via X402_FACILITATOR_URL / config.facilitator. Auth is applied automatically:
| Facilitator | URL | Auth |
|---|---|---|
| x402.org (default) | https://www.x402.org/facilitator |
none |
| PayAI | https://facilitator.payai.network |
none |
| Coinbase CDP | https://api.cdp.coinbase.com/platform/v2/x402 |
Bearer JWT, built in |
X402_FACILITATOR_URL=https://api.cdp.coinbase.com/platform/v2/x402
CDP_API_KEY_ID=your-key-id
CDP_API_KEY_SECRET=your-key-secret
Or configure explicitly:
X402.configure do |config|
config.facilitator = "https://api.cdp.coinbase.com/platform/v2/x402"
config.cdp_api_key_id = Rails.application.credentials.dig(:cdp, :api_key_id)
config.cdp_api_key_secret = Rails.application.credentials.dig(:cdp, :api_key_secret)
end
Every verify/settle/supported/discovery request then carries a per-request, URI-bound Bearer JWT (2-minute expiry), matching @coinbase/cdp-sdk. Note: CDP's Bazaar discovery currently covers Base and Base Sepolia USDC.
Configure via environment variables:
# Required
X402_WALLET_ADDRESS=0xYourAddress
# Optional (with defaults)
X402_FACILITATOR_URL=https://www.x402.org/facilitator
X402_CHAIN=base-sepolia
X402_CURRENCY=USDC
X402_OPTIMISTIC=true # "true" or "false"
# Coinbase CDP facilitator auth (only needed when X402_FACILITATOR_URL is CDP)
CDP_API_KEY_ID=
CDP_API_KEY_SECRET=
# Solana fee payer overrides (required when using a non-default facilitator)
# The default fee payer is for the Coinbase facilitator (x402.org).
# Each facilitator manages its own fee payer — check your facilitator's /supported endpoint.
X402_FEE_PAYER= # Global override for all Solana chains
X402_SOLANA_FEE_PAYER= # Solana mainnet override
X402_SOLANA_DEVNET_FEE_PAYER= # Solana devnet override
# Example: PayAI facilitator (https://facilitator.payai.network)
# X402_FACILITATOR_URL=https://facilitator.payai.network
# X402_SOLANA_FEE_PAYER=2wKupLR9q6wXYppw8Gr2NvWxKBUqm4PPJKkQfoxHDBg4
# X402_SOLANA_DEVNET_FEE_PAYER=2wKupLR9q6wXYppw8Gr2NvWxKBUqm4PPJKkQfoxHDBg4
Fee payer lookup priority:
config.fee_payer (programmatic, global)X402_SOLANA_DEVNET_FEE_PAYER)X402_FEE_PAYER (ENV, global)class WeatherController < ApplicationController
def current
x402_paywall(amount: 0.001)
return if performed?
render json: { temp: 72, condition: "sunny" }
end
def forecast
x402_paywall(amount: 0.01)
return if performed?
render json: { forecast: [...] }
end
end
┌──────────┐ ┌──────────┐ ┌─────────────┐
│ Client │─────▶│ Rails │─────▶│ Facilitator │
│ │ │ x402 │ │ (x402.org) │
└──────────┘ └──────────┘ └─────────────┘
│ │ │
│ │ ▼
│ │ ┌──────────────┐
│ │ │ Blockchain │
│ │ │ (Base) │
└──────────────────┴─────────────┴──────────────┘
The gem raises these errors:
X402::ConfigurationError - Invalid configurationX402::InvalidPaymentError - Invalid payment payloadX402::FacilitatorError - Facilitator communication issuesMIT License. See LICENSE.txt.
Your AI trading terminal assistant for US stocks, commodities, forex, and crypto.
The agent-native LLM router for autonomous agents. 55+ models (8 free), <1ms local routing, USDC payments on Base & Solana via x402.
A payments protocol for the internet. Built on HTTP.
A local-first AI agent with persistent memory, emotional intelligence, and a peer-to-peer skills economy.
The trust layer for agent-to-agent commerce — natural-language mandates, ERC-7710 delegated permissions, x402 payments, escrow, and dispute resolution as one open, catch-all Agent Skill / Claude Code plugin.
The AI agent with a wallet — spends USDC autonomously to get real work done. Apache-2.0, TypeScript.