Blockchain payment infrastructure for AI Agents
moltspay is an early-stage TypeScript project in the AI payments / x402 ecosystem, focused on agent-to-agent, ai-agents, blockchain, cdp. It currently has 8 GitHub stars and 4 forks, and sits alongside related tools like piprail, x402-payments-mcp, gold-402, x402charity, moltspay-python, dna-x402.
Blockchain payment infrastructure for AI Agents. Turn any skill into a paid service with one JSON file.
MoltsPay enables agent-to-agent commerce using the x402 protocol - HTTP-native payments where AI agents can pay each other for services without human intervention. Built on USDC stablecoins with gasless transactions powered by Coinbase CDP.
| Problem | MoltsPay Solution |
|---|---|
| AI agents can't pay for services | x402 protocol - HTTP 402 Payment Required flow |
| Blockchain payments need gas | Gasless - CDP facilitator handles all gas fees |
| Complex wallet integration | One JSON file - add moltspay.services.json to any skill |
| Payment verification is hard | Automatic on-chain verification included |
moltspay.services.json to any existing skill2.0.0) - Accept CNY via Alipay AI Pay from China mainland merchants. CLI-only (Node), browser unsupported. See docs/ALIPAY-RAIL.md2.1.0) - Accept CNY via WeChat Pay v3 Native (scan-to-pay). SDK-managed recoverable sessions persist QR/order context, poll in the background, and fulfill idempotently. See docs/WECHAT-RAIL-DESIGN.md2.2.0) - Top up once, then pay with no signature or QR per transaction. Server-custodied SQLite ledger with atomic deducts, hard spending limits, and idempotent retries. See docs/WECHAT-BALANCE-PASSWORDLESS-DESIGN.md2.4.0) - The balance rail gets a real user: accounts are anchored to the WeChat payer's openid at top-up, and each deduction is authorized by a per-request signature (auth_mode: off | shadow | enforce). Knowing a buyer_id is no longer enough to spend — closes the bearer hole. See Balance Authentication belownpm install moltspay@latest
1. Have an existing skill with exported functions:
// index.js (your existing skill)
export async function textToVideo({ prompt }) {
// your implementation
return { video_url: "https://..." };
}
2. Add moltspay.services.json:
{
"provider": {
"name": "My Video Service",
"wallet": "0xYOUR_EVM_WALLET_ADDRESS",
"solana_wallet": "YOUR_SOLANA_ADDRESS",
"chains": ["base", "polygon", "solana", "bnb"]
},
"services": [{
"id": "text-to-video",
"function": "textToVideo",
"price": 0.99,
"currency": "USDC"
}]
}
Note: solana_wallet is optional - only needed if accepting Solana payments.
3. Start server:
npx moltspay start ./my-skill --port 3000
That's it! Your skill now accepts x402 payments.
1. Initialize wallet (one time):
npx moltspay init # EVM wallet (Base, Polygon, BNB, etc.)
npx moltspay init --chain solana # Solana wallet (mainnet & devnet)
2. Fund your wallet (US users):
npx moltspay fund 5
# Opens Coinbase Pay - use debit card or Apple Pay
# USDC arrives in < 1 minutes. No ETH needed!
Or send USDC directly to your wallet address from any exchange.
3. Use paid services:
# Pay on Base (default)
npx moltspay pay https://server.com text-to-video --prompt "a cat dancing"
# Pay on Polygon
npx moltspay pay https://server.com text-to-video --chain polygon --prompt "a cat dancing"
Want to test before using real money? Use our testnet faucets:
# 1. Create wallet (if you don't have one)
npx moltspay init # EVM wallet (Base, Polygon, BNB, etc.)
npx moltspay init --chain solana # Solana wallet (mainnet & devnet)
# 2. Get free testnet tokens (pick one or all!)
npx moltspay faucet # Base Sepolia (1 USDC, once per 24h)
npx moltspay faucet --chain solana_devnet # Solana devnet (1 USDC)
npx moltspay faucet --chain bnb_testnet # BNB testnet (1 USDC + 0.001 tBNB for gas)
npx moltspay faucet --chain tempo_moderato # Tempo testnet (1 pathUSD)
# 3. Test payments on different chains
# Base Sepolia (CDP, gasless)
npx moltspay pay https://moltspay.com/a/zen7 text-to-video \
--chain base_sepolia --prompt "a robot dancing"
# Solana devnet (SPL transfer)
npx moltspay pay https://moltspay.com/a/zen7 text-to-video \
--chain solana_devnet --prompt "a cat playing piano"
# BNB testnet (gas sponsored)
npx moltspay pay https://moltspay.com/a/zen7 text-to-video \
--chain bnb_testnet --prompt "a sunset timelapse"
# Tempo Moderato (gas-free, MPP protocol)
npx moltspay pay https://server.com service-id \
--chain tempo_moderato --prompt "test"
moltspay@1.6.0 adds a browser client. Connect any EIP-1193 wallet (MetaMask, Rainbow, Frame, …) for EVM or a @solana/wallet-adapter wallet (Phantom, Solflare, Backpack, …) for Solana and pay for x402 services directly from the page — no private key ever in browser memory, no CLI wrapper.
Install:
npm install moltspay
Pay with MetaMask:
import { MoltsPayWebClient, eip1193Signer } from 'moltspay/web';
const client = new MoltsPayWebClient({
signer: eip1193Signer(window.ethereum),
});
// Discover what the provider accepts
const { provider, services } = await client.getServices('https://provider.example.com');
// Run a paid call — user sees one wallet signature prompt
const result = await client.pay(
'https://provider.example.com',
'text-to-video',
{ prompt: 'a cat dancing' },
{ chain: 'base' }
);
Pay with Phantom (Solana):
import { MoltsPayWebClient, solanaSigner } from 'moltspay/web';
import { useWallet } from '@solana/wallet-adapter-react';
const wallet = useWallet();
const client = new MoltsPayWebClient({ signer: solanaSigner(wallet) });
await client.pay(serverUrl, 'text-to-video', params, { chain: 'solana_devnet' });
EVM + Solana in one client — use composeSigners so the same MoltsPayWebClient instance routes to whichever signer matches the picked chain:
import { MoltsPayWebClient, composeSigners, eip1193Signer, solanaSigner } from 'moltspay/web';
const client = new MoltsPayWebClient({
signer: composeSigners(
eip1193Signer(window.ethereum),
solanaSigner(phantomAdapter),
),
});
Chain coverage. All 8 chains the CLI supports work from the browser with one signature prompt each:
| Chain | Scheme | User gas? | Notes |
|---|---|---|---|
base / polygon / base_sepolia |
EIP-3009 transferWithAuthorization |
No (gasless) | Provider submits on success |
tempo_moderato |
EIP-2612 permit |
No (gasless) | Browser never switches to Tempo; server's settler submits permit + transferFrom |
bnb / bnb_testnet |
MoltsPay PaymentIntent |
One-time approve | Call client.approveBnb({ chain, spender, token }) once, then intent signatures are gasless |
solana / solana_devnet |
SPL transfer | No if provider sets a fee payer | wallet.signTransaction signs, provider submits |
BNB approval flow. The first payment on BNB throws NeedsApprovalError with the details needed to approve — catch it, call approveBnb(), and retry:
try {
await client.pay(url, service, params, { chain: 'bnb' });
} catch (err) {
if (err instanceof NeedsApprovalError) {
await client.approveBnb({
chain: 'bnb',
spender: err.details.spender,
token: err.details.token,
});
// User paid ~0.001 BNB gas for the approve. Retry now succeeds without further approve.
await client.pay(url, service, params, { chain: 'bnb' });
}
}
Tempo note. Tempo Moderato pathUSD is a native precompile that implements EIP-2612 permit but not EIP-3009. The web client dispatches to the permit path automatically when the server advertises scheme: "permit". The user signs typed data; the provider's settler submits the permit() + transferFrom() transactions on-chain. MetaMask never prompts for a chain switch because the browser wallet doesn't touch Tempo at all.
Solana mainnet RPC. The public api.mainnet-beta.solana.com endpoint returns 403 to browser requests, so any Solana-mainnet traffic needs an authenticated RPC. Supply one via solanaRpc:
const client = new MoltsPayWebClient({
signer: composeSigners(
eip1193Signer(window.ethereum),
solanaSigner(phantomAdapter),
),
solanaRpc: {
solana: 'https://mainnet.helius-rpc.com/?api-key=YOUR_KEY',
// solana_devnet falls back to `api.devnet.solana.com` automatically
},
});
Accepts any Helius / QuickNode / Alchemy / Triton / self-hosted URL. Per-chain — override only solana if you only use mainnet. Devnet (api.devnet.solana.com) still serves browsers and does not need an override.
Error classes — every error exposes a code field so you can branch without string-matching:
import {
NeedsApprovalError, // code: 'NEEDS_APPROVAL' — BNB, call approveBnb()
UnsupportedChainError, // code: 'UNSUPPORTED_CHAIN' — user picked a chain the server doesn't accept
PaymentRejectedError, // code: 'PAYMENT_REJECTED' — user cancelled in the wallet
InsufficientBalanceError, // code: 'INSUFFICIENT_BALANCE' — not enough native gas for BNB approve
SpendingLimitExceededError,// code: 'SPENDING_LIMIT_EXCEEDED' — only when you opt in to SpendingLedger
ServerError, // code: 'SERVER_ERROR' — provider returned non-2xx
MoltsPayError, // base class
} from 'moltspay/web';
Spending limits (opt-in). Off by default. External wallets already prompt per-signature; per-browser localStorage limits don't sync across devices. If you want a session-level cap anyway:
const client = new MoltsPayWebClient({
signer: eip1193Signer(window.ethereum),
spendingLimits: { maxPerTx: 5, maxPerDay: 50 },
});
Provider CORS. Providers must enable CORS on MoltsPayServer for browser callers — without it the 402 challenge header is invisible to the browser:
new MoltsPayServer({
// ...
cors: true, // allow all origins, or
cors: ['https://myapp.example.com'], // explicit allowlist, or
cors: { origins: [...], maxAge: 86400 }, // fine-grained
});
Servers advertising for the web need cors enabled; CLI callers are unaffected either way.
Security posture. No private key ever enters browser memory. No filesystem access, no ~/.moltspay/ — the wallet is always external. The signer object the client receives only has permission to sign typed data and (for BNB) submit one approve transaction, which the user explicitly confirms in the wallet UI.
Reference demo. examples/web/ is a runnable React + Vite app that exercises every path above. cd examples/web && npm install && npm run dev — it connects to https://moltspay.com/a/zen7 by default. See examples/web/README.md for the full matrix of tested wallets + chains.
MoltsPay ships an MCP (Model Context Protocol) stdio server that lets MCP-compatible hosts (Cursor, Windsurf, Claude Code, Zed, etc.) browse services, check wallet status, and pay for x402 services on your behalf.
It is a thin wrapper around MoltsPayClient — wallet custody, spending limits, and all payment protocols (x402, MPP, Solana, BNB) are reused from the SDK.
1. Create a wallet and set spending limits (the MCP server refuses to start without a wallet):
npx moltspay init
npx moltspay config --max-per-tx 2 --max-per-day 10
npx moltspay fund 5 # or: npx moltspay faucet for testnet
2. Point your MCP host at the moltspay-mcp binary over stdio:
npx -y moltspay-mcp # normal mode
npx -y moltspay-mcp --dry-run # preview payments without signing
Each host has its own config file for registering stdio MCP servers — check your host's docs for the exact location. For a safer first run, use --dry-run so moltspay_pay returns a preview instead of spending real funds.
| Tool | What it does | Destructive? |
|---|---|---|
moltspay_status |
Wallet address, balances across all supported chains, spending limits | No |
moltspay_services |
Fetch services manifest from a provider URL; optional query/maxPrice filter |
No |
moltspay_pay |
Execute an x402/MPP/SOL/BNB payment and return the service result | Yes |
moltspay_config |
Read or update maxPerTx / maxPerDay limits |
Updates config file |
moltspay_pay is the only tool that moves money. Three guards stack on top of the MCP host's own tool-approval prompt:
maxPerTx / maxPerDay enforced before signing.--dry-run and payments return a preview instead of signing.MOLTSPAY_MCP_REQUIRE_CONFIRM=1 to require a second tool call (confirmed: true) for any payment exceeding maxPerTx / 10.Private keys and mnemonics are never exposed over MCP — wallet creation stays on the CLI (npx moltspay init) by design. See docs/MCP-USAGE.md for full tool arguments and troubleshooting.
MoltsPay supports multiple payment protocols, each optimized for different chains:
| Protocol | Chains | Gas | Description |
|---|---|---|---|
| x402 + CDP | Base, Polygon | Gasless (CDP pays) | HTTP 402 + EIP-3009 signatures |
| x402 + SOL | Solana | Gasless (server pays) | HTTP 402 + SPL transfer |
| x402 + BNB | BNB | Gasless (server pays) | HTTP 402 + EIP-712 intent signing |
| MPP | Tempo | Gas-free native | HTTP 402 + WWW-Authenticate |
| x402 + Alipay | Alipay | No blockchain gas | HTTP 402 + Alipay AI Pay proof |
| x402 + WeChat Pay | WeChat Pay Native | No blockchain gas | HTTP 402 + scan-to-pay QR, poll-based verify |
Client Server CDP Facilitator
| | |
| POST /execute | |
| --------------------------------------------------> | |
| | |
| 402 + payment requirements | |
| <-------------------------------------------------- | |
| | |
| [Sign EIP-3009 - NO GAS] | |
| | |
| POST + X-Payment header | |
| --------------------------------------------------> | Verify signature |
| | --------------------------------------------------> |
| | |
| | Execute transfer (pays gas) |
| | <-------------------------------------------------- |
| | |
| 200 OK + result | |
| <-------------------------------------------------- | |
Key insight: Client signs a payment authorization, server submits it. Neither party pays gas - the CDP facilitator handles settlement.
MPP (Machine Payments Protocol) is simpler - client executes the transfer directly:
Client Server
| |
| POST /service |
| --------------------------------------------------> |
| |
| 402 + WWW-Authenticate |
| <-------------------------------------------------- |
| |
| [Execute TIP-20 transfer] |
| [No gas needed on Tempo] |
| |
| POST + Authorization: Payment|
| --------------------------------------------------> |
| |
| [Server verifies on-chain] |
| |
| 200 OK + Payment-Receipt |
| <-------------------------------------------------- |
Key insight: On Tempo, the client executes the transfer directly (gas-free), then retries with the transaction hash. No CDP facilitator needed.
Client Server (Fee Payer) Solana Network
| | |
| POST /execute | |
| --------------------------------------------------> | |
| | |
| 402 + payment requirements | |
| (includes solana_wallet) | |
| <-------------------------------------------------- | |
| | |
| [Sign SPL Transfer] | |
| [NO GAS - just signing] | |
| | |
| POST + X-Payment (signature) | |
| --------------------------------------------------> | Execute transfer |
| | (server pays ~$0.001 SOL) |
| | --------------------------------------------------> |
| | |
| 200 OK + result | |
| <-------------------------------------------------- | |
Key insight: Client only signs the SPL transfer (gasless). Server acts as fee payer and executes the transaction on-chain.
Client Server BNB Network
| | |
| POST /execute | |
| --------------------------------------------------> | |
| | |
| 402 + payment requirements | |
| (includes bnbSpender) | |
| <-------------------------------------------------- | |
| | |
| [Sign EIP-712 intent] | |
| [NO GAS - just signing] | |
| | |
| POST + X-Payment (signature) | |
| --------------------------------------------------> | Execute transferFrom |
| | (server pays ~$0.0001 gas) |
| | --------------------------------------------------> |
| | |
| 200 OK + result | |
| <-------------------------------------------------- | |
Key insight: Client only signs an intent (gasless). Server executes the actual transfer and pays the minimal gas (~$0.0001). This is the "pay-for-success" model - payment only happens if service succeeds.
MoltsPay reads your skill's existing structure:
my-skill/
+------ package.json # MoltsPay reads "main" field
+------ index.js # Your existing exports
+------ moltspay.services.json # Only file you add!
Your functions stay untouched. Just add the JSON config.
{
"$schema": "https://moltspay.com/schemas/services.json",
"provider": {
"name": "Service Name",
"description": "Optional description",
"wallet": "0x...",
"chains": ["base", "polygon"]
},
"services": [{
"id": "service-id",
"name": "Human Readable Name",
"description": "What it does",
"function": "exportedFunctionName",
"price": 0.99,
"currency": "USDC",
"input": {
"prompt": { "type": "string", "required": true }
},
"output": {
"result_url": { "type": "string" }
}
}]
}
Accept payments on multiple chains by specifying a chains array:
{
"provider": {
"wallet": "0x...",
"chains": ["base", "polygon"]
}
}
Clients can then choose which chain to pay on:
npx moltspay pay https://server.com service-id --chain polygon --prompt "..."
If no --chain is specified, the client uses the first chain in the provider's list.
npx moltspay validate ./my-skill
1. Get CDP credentials from https://portal.cdp.coinbase.com/
2. Create ~/.moltspay/.env:
CDP_API_KEY_ID=your-key-id
CDP_API_KEY_SECRET=your-secret
3. Configure chains in your manifest:
{
"provider": {
"wallet": "0x...",
"chains": [
{ "chain": "base", "network": "eip155:8453", "tokens": ["USDC"] },
{ "chain": "base_sepolia", "network": "eip155:84532", "tokens": ["USDC"] }
]
}
}
4. Start server:
npx moltspay start ./my-skill --port 3000
Server does NOT need a private key - the x402 facilitator handles settlement.
The server automatically detects which chain to verify payments on based on the client's payment header:
--chain base -> Server verifies on Base mainnet--chain base_sepolia -> Server verifies on Base SepoliaNo USE_MAINNET env var needed! Just configure your accepted chains in the manifest.
To accept testnet payments, add base_sepolia to your chains array:
{
"provider": {
"wallet": "0x...",
"chains": ["base", "base_sepolia"]
}
}
Clients can then pay using --chain base_sepolia and get free testnet USDC via npx moltspay faucet.
# === Client Commands ===
npx moltspay init # Create wallet (EVM + Solana)
npx moltspay fund <amount> # Fund wallet via Coinbase (US)
npx moltspay faucet # Get free testnet USDC (Base Sepolia)
npx moltspay faucet --chain solana_devnet # Get Solana devnet USDC
npx moltspay faucet --chain bnb_testnet # Get BNB testnet USDC + tBNB
npx moltspay faucet --chain tempo_moderato # Get Tempo testnet tokens
npx moltspay status # Check balance (all chains)
npx moltspay transfer <to> <amount> # Transfer USDC/USDT out to any address (e.g. an exchange)
npx moltspay config # Update limits
npx moltspay services <url> # List provider's services
npx moltspay pay <url> <service> # Pay and execute service
# === Transfer / Withdraw (2.4.0) ===
# Move USDC/USDT out of your wallet to any address (e.g. an exchange deposit address).
npx moltspay transfer <to> <amount> # 5 USDC on Base (default), interactive confirm
npx moltspay transfer <to> 10 --token USDT --chain bnb # USDT on BNB Chain
npx moltspay transfer <to> 5 --chain polygon --yes --json # non-interactive (agents/scripts)
# EVM chains only (base/polygon/bnb/...); a normal on-chain transfer, so the wallet needs a
# little native gas (ETH/BNB/POL). The transfer network must match the receiver's deposit network.
# === Service Discovery ===
npx moltspay services # List all from registry
npx moltspay services https://provider.com # List from specific provider
npx moltspay services -q "video" # Search by keyword
npx moltspay services --max-price 1.00 # Filter by max price
npx moltspay services --type api_service # Filter by type
npx moltspay services --tag ai # Filter by tag
npx moltspay services --json # Output as JSON
# === Pay with Chain Selection ===
npx moltspay pay <url> <service> --chain base # Pay on Base (default)
npx moltspay pay <url> <service> --chain polygon # Pay on Polygon
npx moltspay pay <url> <service> --chain base_sepolia # Pay on Base testnet
npx moltspay pay <url> <service> --chain solana # Pay on Solana
npx moltspay pay <url> <service> --chain solana_devnet # Pay on Solana devnet
npx moltspay pay <url> <service> --chain bnb # Pay on BNB
npx moltspay pay <url> <service> --chain bnb_testnet # Pay on BNB testnet
npx moltspay pay <url> <service> --chain tempo_moderato # Pay on Tempo
npx moltspay pay <url> <service> --rail alipay # Pay with Alipay AI Pay
npx moltspay pay <url> <service> --rail wechat # Pay with WeChat Pay Native QR
npx moltspay pay <url> <service> --rail balance # Pay password-free from prepaid balance
# === BNB One-Time Approve ===
# BNB payments need a one-time spender approval first (the spender comes from the 402 response).
npx moltspay approve --spender <address> --chain bnb # Approve, then `pay --chain bnb` is gasless
npx moltspay approve --spender <address> # --chain defaults to bnb_testnet
# === WeChat Recoverable Sessions ===
npx moltspay wechat start <url> <service> --prompt "..." # Start order, return QR metadata immediately
npx moltspay wechat status <session-or-out_trade_no> # Query/recover a pending session
npx moltspay wechat fulfill <session-or-out_trade_no> # Idempotently fulfill if paid
npx moltspay wechat cancel <session-or-out_trade_no> # Mark local session cancelled
npx moltspay wechat list # List persisted WeChat sessions
# === Custodial Balance (Password-Free) ===
npx moltspay balance set-buyer <id> # Persist the buyer id (account label)
npx moltspay balance query <url> # Balance, limits, today's spend, bound signer/openid
npx moltspay balance transactions <url> # Ledger history, newest first
npx moltspay balance topup <url> <amount> --rail crypto --tx-hash 0x... # Credit from a settled payment (crypto/wechat/alipay)
# --- Identity & auth (2.4.0) ---
npx moltspay balance whoami [url] # Show local signer address (and, vs a server, the account's bound signer/openid)
npx moltspay balance bind <url> # Bind the local signer to an account (runs a minimal top-up to establish identity)
# --- Recoverable WeChat-funded top-up (for chat agents; non-blocking) ---
npx moltspay balance topup-order <url> --pack 2.00 # Mint a WeChat pack order, emit the QR, exit immediately
npx moltspay balance topup-confirm <out_trade_no> # Confirm + credit a pending order later (idempotent on out_trade_no)
npx moltspay balance topup-status <out_trade_no> # Query one pending order
npx moltspay balance topup-list # List pending top-up orders
npx moltspay balance topup-pack <url> # Blocking variant: mint pack, wait for the scan, credit inline
# === Server Commands ===
npx moltspay start <skill-dir> # Start server
npx moltspay stop # Stop server
npx moltspay validate <path> # Validate manifest
# === Options ===
--port <port> # Server port (default 3000)
--chain <chain> # Chain: base, polygon, solana, bnb, tempo_moderato, + testnets
--rail <rail> # Rail: alipay, wechat, or balance (password-free)
--token <token> # Token: USDC, USDT
--max-per-tx <amount> # Spending limit per transaction
--max-per-day <amount> # Daily spending limit
--config-dir <dir> # Custom wallet directory
import { MoltsPayClient } from 'moltspay/client';
// Initialize client (uses wallet from ~/.moltspay/wallet.json)
const client = new MoltsPayClient();
// Standard service call (params wrapped in { params: {...} })
const result = await client.pay(
'https://server.com',
'text-to-video',
{ prompt: 'a cat dancing' },
{ chain: 'base' }
);
console.log(result.video_url);
Some services have custom input formats instead of the standard { params: { prompt } }.
Use rawData: true to send your data at the top level:
// Service expects: { text: "...", target_lang: "..." }
// NOT: { params: { text: "...", target_lang: "..." } }
const result = await client.pay(
'https://server.com',
'translate',
{ text: 'Hello world', target_lang: 'es' },
{
chain: 'base_sepolia',
rawData: true // Send data at top level
}
);
// Server receives: { service: "translate", text: "Hello world", target_lang: "es", chain: "base_sepolia" }
interface PayOptions {
token?: 'USDC' | 'USDT'; // Token to pay with (default: USDC)
autoSelect?: boolean; // Auto-select token based on balance
chain?: string; // Chain: base, polygon, solana, bnb, tempo_moderato, + testnets
rawData?: boolean; // Send data at top level (for custom input formats)
}
# Standard format (uses { params: { prompt } })
npx moltspay pay https://server.com text-to-video --prompt "a cat dancing"
# Custom format (uses rawData, sends at top level)
npx moltspay pay https://server.com translate --data '{"text": "Hello", "target_lang": "es"}'
import { MoltsPayServer } from 'moltspay/server';
const server = new MoltsPayServer('./moltspay.services.json');
// Register custom handler (optional - usually loaded from skill)
server.skill('text-to-video', async (params) => {
// implementation
return { video_url: '...' };
});
server.listen(3000);
| Chain | ID | Type | Facilitator | Gas Model |
|---|---|---|---|---|
| Base | 8453 | Mainnet | CDP | Gasless (CDP pays) |
| Polygon | 137 | Mainnet | CDP | Gasless (CDP pays) |
| Base Sepolia | 84532 | Testnet | CDP | Gasless (CDP pays) |
| Solana | - | Mainnet | SOL | Gasless (server pays) |
| Solana Devnet | - | Testnet | SOL | Gasless (server pays) |
| BNB | 56 | Mainnet | BNB | Gasless (server pays) |
| BNB Testnet | 97 | Testnet | BNB | Gasless (server pays) |
| Tempo Moderato | 42431 | Testnet | Tempo | Gas-free native |
Notes:
A facilitator is the entity that executes the on-chain payment and pays the gas fees. MoltsPay supports two types:
| Type | Facilitator | Who Pays Gas? | Trust Model |
|---|---|---|---|
| External | CDP (Coinbase) | Coinbase | Trust Coinbase infrastructure |
| Self-hosted | SOL, BNB, Tempo | Your server | Trust your own server |
External Facilitator (CDP):
Self-hosted Facilitator (SOL, BNB, Tempo):
Why Self-hosted is More Decentralized:
| Aspect | CDP (External) | Self-hosted |
|---|---|---|
| Single point of failure | Coinbase down = everyone stuck | Each provider independent |
| Censorship risk | Coinbase can block accounts | Cannot be censored |
| Dependency | Relies on third-party | Fully autonomous |
This self-hosted approach is a key innovation: any service provider can become their own facilitator without relying on third-party infrastructure. Unlike CDP where all users depend on Coinbase, self-hosted facilitators create a truly decentralized network with no single point of failure.
Note: Clients never need to know the facilitator's private keys. They only sign their own payments - the facilitator handles settlement transparently.
| Facilitator | Chains | How It Works |
|---|---|---|
| CDP | Base, Polygon | Client signs EIP-3009, Coinbase executes transfer |
| SOL | Solana | Client signs SPL transfer, server executes as fee payer |
| BNB | BNB | Client signs EIP-712 intent, server executes transfer |
| Tempo | Tempo Moderato | Client executes TIP-20 transfer (native gas-free) |
Solana uses the SolanaFacilitator with SPL token transfers. Key differences:
~/.moltspay/wallet-solana.json# Initialize includes Solana wallet automatically
npx moltspay init
# Check Solana balance
npx moltspay status
# Get free devnet USDC
npx moltspay faucet --chain solana_devnet
# Pay on Solana
npx moltspay pay https://server.com service-id --chain solana --prompt "test"
USDC Addresses:
| Network | Mint Address |
|---|---|
| Mainnet | EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v |
| Devnet | 4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU |
BNB uses the BNBFacilitator with a pre-approval flow. Since CDP doesn't support BNB, we use a different approach:
approve, costs ~0.001 BNB gas)# Get free testnet USDC + tBNB for gas
npx moltspay faucet --chain bnb_testnet
# One-time approve (required before the first BNB payment).
# The spender address comes from the server's 402 response; a first `pay` also
# prints it if you haven't approved yet.
npx moltspay approve --spender 0xSPENDER --chain bnb_testnet
# Pay on BNB (client pays no gas after the approve!)
npx moltspay pay https://server.com service-id --chain bnb_testnet --prompt "test"
# Check BNB balance
npx moltspay status
Important: BNB tokens use 18 decimals (not 6 like Base/Polygon).
Token Addresses (BNB Mainnet):
| Token | Address |
|---|---|
| USDC | 0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d |
| USDT | 0x55d398326f99059fF775485246999027B3197955 |
Tempo Moderato is a gas-free testnet that supports the MPP (Machine Payments Protocol). Perfect for testing agent-to-agent payments without any gas fees.
Tempo Stablecoins:
0x20c00000000000000000000000000000000000000x20c0000000000000000000000000000000000001# Get free Tempo testnet tokens
npx moltspay faucet --chain tempo_moderato
# Pay on Tempo (gas-free!)
npx moltspay pay https://server.com service-id --chain tempo_moderato --prompt "test"
# Check Tempo balance
npx moltspay status
Explorer: https://explore.testnet.tempo.xyz
Since 2.0.0, MoltsPay supports a fiat payment rail via Alipay AI Pay, settling in CNY alongside the existing USDC crypto rails. It uses the same HTTP 402 flow, so a service can price in USDC, CNY, or both — a purely additive change that leaves existing crypto-only services untouched.
Unlike the crypto rails, alipay is a fiat rail, not a blockchain: there is no gas and no on-chain settlement — the buyer pays in the Alipay app and the merchant settles in CNY. The scheme is alipay-aipay, signed with RSA2.
⚠️ Node CLI only. The Alipay client wraps the
alipay-botCLI and requires Node ≥ 22. The browser build (moltspay/web) throwsUnsupportedChainErrorforalipay. Provider/server config is 100% native TypeScript.
Add "alipay" to chains, configure provider.alipay with your merchant keys, and add a per-service alipay block with the CNY price. Crypto and fiat can coexist:
{
"provider": {
"name": "My Service",
"wallet": "0x...",
"chains": ["base", "alipay"],
"alipay": {
"seller_id": "2088xxxxxxxxxxxx",
"app_id": "2021xxxxxxxxxxxx",
"seller_name": "My Shop",
"service_id_default": "API_XXXXXXXXXXXXXXXX",
"private_key_path": "./cert/app_private_key.pem",
"alipay_public_key_path": "./cert/alipay_public_key.pem"
}
},
"services": [{
"id": "my-service",
"function": "handleRequest",
"price": 0.50,
"currency": "USDC",
"alipay": { "price_cny": "7.00", "goods_name": "My Service" }
}]
}
provider.alipay (required when chains includes alipay):
| Field | Req | Description |
|---|---|---|
seller_id |
✅ | Merchant Alipay ID (16 digits) |
app_id |
✅ | Application ID from the Alipay Open Platform (16 digits) |
seller_name |
✅ | Merchant display name (shown in the 402 challenge) |
service_id_default |
✅ | Default service_id for this provider (e.g. API_XXXXXXXXXXXXXXXX) |
private_key_path |
✅ | Path to the RSA2 merchant private key PEM (relative to the manifest) |
alipay_public_key_path |
✅ | Path to the Alipay platform public key PEM |
gateway_url |
— | Open API gateway (default production; use the sandbox URL for testing) |
sign_type |
— | Signature algorithm — only RSA2 is supported |
services[].alipay (set on each service that accepts CNY):
| Field | Req | Description |
|---|---|---|
price_cny |
✅ | CNY price as a decimal string in yuan, e.g. "7.00" = 7 CNY (NOT cents) |
goods_name |
✅ | Goods name shown to the buyer in the Alipay app |
service_id |
— | Per-service override (defaults to provider.alipay.service_id_default) |
⚠️
price(USDC) andprice_cny(CNY) are two independent prices — MoltsPay does no FX conversion. You decide whether USDC0.50is equivalent to CNY7.00. The merchant-backend unit price must matchprice_cnyor the challenge is rejected.
The Alipay client relies on the alipay-bot CLI, which is auto-provisioned on install from the official Alipay CDN (postinstall, best-effort). Set MOLTSPAY_SKIP_CLI_INSTALL=1 to skip it, or install it manually later:
# Manual / CI install of the bundled CLI
npx -y @alipay/agent-payment install-cli
alipay-bot --version # expect >= 0.3.15
# One-time: open & authorize the Alipay wallet (scan the returned QR in the app)
npx moltspay alipay apply
npx moltspay alipay bind -c "<auth-code>"
npx moltspay alipay check
# Pay a service in CNY
npx moltspay pay https://server.com my-service --rail alipay --prompt "..."
🔐 Keep keys safe. The RSA2 private key authorizes collection on your merchant account. Store the PEM files outside version control and reference them by path in the manifest.
See docs/ALIPAY-RAIL.md for the full reference (402 flow, error codes, end-to-end example, and known issues).
Since 2.1.0, MoltsPay supports a second fiat payment rail via WeChat Pay v3 Native (scan-to-pay), settling in CNY alongside USDC and Alipay. It uses the same HTTP 402 flow, so a service can price in USDC, Alipay-CNY, WeChat-CNY, or any combination — purely additive.
Like alipay, wechat is a fiat rail, not a blockchain. The scheme is wechatpay-native, requests are signed with SHA256-RSA (WECHATPAY2-SHA256-RSA2048), and amounts are sent to WeChat in fen (the manifest still uses yuan decimal strings for consistency).
⚠️ Not a fully autonomous payer. WeChat Pay has no agent-payment product equivalent to Alipay's
alipay-bot. The flow is: the server issues a payer-agnosticcode_url(Native, noopenid— anyone can scan), the SDK client persists the session and polls, a human scans and pays, and the server verifies the order (trade_state === SUCCESS). It is one code, one payment — issue a new code to collect again. Seedocs/WECHAT-RAIL-DESIGN.md.
Add "wechat" to chains, configure provider.wechat with your merchant credentials, and add a per-service wechat block with the CNY price:
{
"provider": {
"name": "My Service",
"wallet": "0x...",
"chains": ["base", "wechat"],
"wechat": {
"mchid": "1900000001",
"appid": "wx8888888888888888",
"serial_no": "5157F09EFDC096DE15EBE81A47057A72...",
"private_key_path": "./cert/wechat_apiclient_key.pem",
"platform_public_key_path": "./cert/wechat_platform_cert.pem",
"apiv3_key": "your32byteapiv3keyhere0123456789",
"notify_url": "https://your.host/wechat/notify"
}
},
"services": [{
"id": "my-service",
"function": "handleRequest",
"price": 0.50,
"currency": "USDC",
"wechat": { "price_cny": "7.00", "description": "My Service" }
}]
}
provider.wechat (required when chains includes wechat):
| Field | Req | Description |
|---|---|---|
mchid |
✅ | Merchant id |
appid |
✅ | App id (official account / mini-program / app) |
serial_no |
✅ | Merchant API certificate serial number |
private_key_path |
✅ | Path to the merchant RSA private key PEM (relative to the manifest) |
notify_url |
✅ | Async result notify URL (required by Native order create even when polling) |
platform_public_key_path |
— | WeChat platform certificate / public key PEM — enables response signature verification |
apiv3_key |
— | APIv3 key (32 bytes) — only needed for callback decryption (Phase 2) |
api_base |
— | Open API base URL (default https://api.mch.weixin.qq.com) |
services[].wechat (set on each service that accepts CNY):
| Field | Req | Description |
|---|---|---|
price_cny |
✅ | CNY price as a decimal string in yuan, e.g. "7.00" = 7 CNY (NOT fen) |
description |
✅ | Order description shown to the payer in the WeChat app |
⚠️
price(USDC) andprice_cny(CNY) are independent prices — MoltsPay does no FX conversion.
/execute returns 402 with a wechatpay-native entry in accepts[], carrying extra.code_url (weixin://wxpay/bizpayurl?pr=...) and extra.out_trade_no.MEDIA image and continue independently of the original tool call.code_url is a QR payload, not a normal HTTPS checkout link./execute with an X-Payment payload echoing out_trade_no; the server verifies via order query and, on SUCCESS, runs the skill and confirms settlement.payment_session_id or out_trade_no; fulfill is idempotent and returns the stored result once completed.Blocking terminal-style payment:
import { MoltsPayClient } from 'moltspay/client';
const client = new MoltsPayClient();
const result = await client.pay(
'https://server.com',
'my-service',
{ prompt: 'hello' },
{
rail: 'wechat',
onPaymentPending: ({ paymentUrl, tradeNo }) => {
console.log('Render this as a QR:', paymentUrl, tradeNo);
},
},
);
Recoverable channel/session payment:
const session = await client.startWechatPayment(
'https://server.com',
'my-service',
{ prompt: 'hello' },
{
rail: 'wechat',
autoPoll: true,
onPaymentPending: ({ paymentUrl, tradeNo }) => {
// Render paymentUrl as QR and send it to the current channel.
},
onWechatPaymentCompleted: async (completed) => {
// Send completed.resultBody, parsed as needed, back to the channel.
},
onWechatPaymentFailed: async (failed) => {
// Notify timeout/cancel/failure.
},
},
);
console.log(session.paymentSessionId, session.outTradeNo);
CLI recoverable flow:
npx moltspay wechat start https://server.com my-service --prompt "hello" --json
npx moltspay wechat status mpay_sess_...
npx moltspay wechat fulfill mpay_sess_...
See examples/wechat-native-pay.ts for a runnable scenario-A demo (mock by default; WECHAT_REAL=1 hits the live gateway).
🔐 Keep keys safe. The merchant private key and APIv3 key authorize collection on your merchant account. Store them outside version control and reference them by path.
Since 2.2.0, MoltsPay supports a third payment mode: a custodial balance rail (password-free). A buyer tops up once — via any existing rail — and subsequent purchases are deducted from a server-custodied balance: no signature, no QR scan, no password per transaction. It uses the same HTTP 402 flow (scheme/network balance), so a service can offer crypto, Alipay-CNY, WeChat-CNY, and balance simultaneously — purely additive.
Unlike the other rails, nothing settles externally at purchase time: the ledger lives in SQLite on the provider server (Node's built-in node:sqlite, zero new dependencies). Enabling the rail requires Node.js >= 22.5 on the server; servers that don't enable it keep the package's node >= 18 floor.
⚠️ Custodial trust model. Buyer funds are held by the provider. Since
2.4.0, spending is authorized by a per-request signature, not a barebuyer_id— see Balance Authentication below. Withauth_mode: enforce, knowing abuyer_idis not enough to spend. (auth_modedefaults toofffor backward compatibility, where the id retains bearer semantics.) Seedocs/WECHAT-BALANCE-PASSWORDLESS-DESIGN.md.
2.4.0)Before 2.4.0 the balance rail identified a buyer by a single plaintext buyer_id: anyone who knew it could spend that balance (bearer semantics), and the same person writing it two ways split into two accounts. 2.4.0 gives the rail a real identity, in two complementary halves:
openid. On a WeChat-funded top-up, the server extracts payer.openid from the (signature-verified) order query and records it on the account. Accounts are anchored to the person who actually paid, not to a self-reported string — this removes the account-splitting class of bug at the source. Binding is observational and never overwrites a conflicting openid.moltspay-balance-auth:v1 / balance-deduct / buyer_id / request_id / service / timestamp). The server recovers the signer address, TOFU-binds it to the account on first use, and verifies it thereafter. The amount is deliberately not signed (the service id fixes the price server-side); a request_id + a ±5-minute timestamp window bound replay.provider.balance.auth_mode — staged rollout, not on by default:
| Mode | Behavior |
|---|---|
off (default) |
Signature is not checked. Backward-compatible; buyer_id keeps bearer semantics. |
shadow |
Verify and log what it would reject, without blocking. Use this to confirm every client is signing before tightening. |
enforce |
Reject unsigned / invalid / wrong-signer requests with 401. |
The client signs with its EVM wallet, or — for balance-only clients with no crypto wallet — a per-configDir identity key auto-created at <configDir>/balance-identity.key (0600). Inspect and bind identity with moltspay balance whoami / moltspay balance bind.
Foundation — WeChat response verification. The
openidanchor is only trustworthy if the order-query response is verified against the WeChat platform certificate. Configureprovider.wechat.platform_public_key_path(present ⇒ every response is verified, and a verification failure throws); without it, the identity anchor is not trustworthy.
Trust model — one agent key spends for all its users. The shipped model is agent-custodial: a single agent key is bound to every account it tops up (
signer_addressis one-to-many). Accounts stay separated by openid, but whoever holds the agent's key can spend every bound user's balance — protectbalance-identity.keyaccordingly. There is currently no revocation/freeze API (operator-only, via the ledger DB); per-user key isolation is a future upgrade.
Since 2.3.0, the WeChat rail can fund the balance instead of charging per transaction. The buyer scans once to load a top-up pack (e.g. ¥20); every subsequent purchase deducts from the balance with no scan, no password, until it runs low and one more scan tops it up. WeChat has no autonomous payer, so "password-free" lives on the balance side — the first purchase against an empty balance still needs one scan, but it buys a pack, not a single item.
Set the ledger currency to CNY (WeChat payer_total credits 1:1 as fen — no FX), define top-up packs, and the client handles the rest:
{
"provider": {
"name": "My Service",
"wallet": "0x...",
"chains": ["balance", "wechat"],
"balance": {
"db_path": "./data/balance-cny.sqlite",
"currency": "CNY",
"single_limit": "50.00",
"daily_limit": "200.00",
"topup_packs": ["20.00", "50.00"],
"default_pack": "20.00",
"auto_topup_max": "50.00"
},
"wechat": {
"mchid": "1900000001",
"appid": "wx8888888888888888",
"serial_no": "...",
"private_key_path": "/abs/apiclient_key.pem",
"notify_url": "https://your-host/wechat/notify",
"platform_public_key_path": "/abs/wechat_platform_cert.pem",
"apiv3_key": "<32-byte apiv3 key>"
}
},
"services": [{
"id": "my-service",
"function": "handleRequest",
"price": 3.99,
"currency": "CNY",
"balance": { "price": "3.99" }
}]
}
New provider.balance fields (2.3.0):
| Field | Req | Description |
|---|---|---|
currency |
— | Set to "CNY" to fund via WeChat 1:1 (fen). A ledger remembers its currency and refuses to start on a mismatch |
topup_packs |
— | Offered recharge amounts (yuan strings), e.g. ["20.00","50.00"] |
default_pack |
— | Pack the client auto-selects when funds are short (must be in topup_packs) |
auto_topup_max |
— | Ceiling on client auto-top-up without explicit pack selection |
Top-ups are confirmed by polling the WeChat order query (trade_state === SUCCESS) and credited idempotently on out_trade_no. The async callback webhook (POST /wechat/notify) is not implemented yet — polling is the only confirmation path today, which is safe but adds latency. Configure platform_public_key_path regardless: it makes the server verify WeChat's response signatures, which is what makes the payer.openid behind balance identity trustworthy. Usage — a single pay command:
npx moltspay balance set-buyer my-buyer-id
# First purchase: prints a pack QR, waits for the scan, credits, completes.
# Later purchases: password-free, no QR — deducted from the balance.
npx moltspay pay https://server.com my-service '{"prompt":"hello"}'
UPDATE ... WHERE balance_sat >= ?); a balance can never go negative under concurrency.request_id, top-ups replay on the external settlement reference, refunds replay per deduct. No retry can double-charge, double-credit, or double-refund.5.00) and daily (default 10.00).Add "balance" to chains, configure provider.balance, and add a per-service balance block (its price defaults to the service's top-level price):
{
"provider": {
"name": "My Service",
"wallet": "0x...",
"chains": ["base", "balance"],
"balance": {
"db_path": "./data/balance.sqlite",
"currency": "USD",
"single_limit": "5.00",
"daily_limit": "10.00"
}
},
"services": [{
"id": "my-service",
"function": "handleRequest",
"price": 3.99,
"currency": "USDC",
"balance": { "price": "3.99" }
}]
}
provider.balance (required when chains includes balance):
| Field | Req | Description |
|---|---|---|
db_path |
✅ | SQLite ledger file path (relative to the manifest); created on first start |
currency |
— | Ledger quote currency (default USD) |
single_limit |
— | Default per-transaction limit for new buyers (default "5.00") |
daily_limit |
— | Default daily limit for new buyers (default "10.00") |
services[].balance:
| Field | Req | Description |
|---|---|---|
price |
— | Price in the ledger currency as a decimal string; defaults to the service's price |
The server also exposes balance-management endpoints:
| Endpoint | Description |
|---|---|
GET /balance?buyer_id= |
Balance, limits, and today's spend |
POST /balance/topup |
Verify an externally settled payment and credit the ledger |
POST /balance/refund |
Reverse a deduct (operator/agent use) |
GET /balance/transactions?buyer_id= |
Ledger history, newest first |
⚠️ Protect these endpoints (reverse-proxy auth / IP allowlist). In particular, the
alipaytop-up path is operator-trusted in this MVP: Alipay AI-Pay verification requires the buyer-sidepayment_proof, so the server credits a reportedtrade_nowithout gateway verification. Thecryptopath verifies thetx_hashon-chain (receipt + transfer to the provider wallet + amount) and theSUCCESS; all three are idempotent on the external reference.
/execute returns 402 with a balance entry in accepts[] (pure — nothing is minted).X-Payment payload carrying { buyer_id, request_id }, where request_id is a fresh client-generated UUID.request_id), then runs the skill, and refunds automatically if the skill fails. This is the inverse of the QR rails (verify paid → run → confirm).X-Payment-Response.# One-time: persist your buyer id (account label). Under auth_mode=enforce, spending also
# requires the bound signing key — see "Balance Authentication" above.
npx moltspay balance set-buyer my-buyer-id
# Top up once, via any settled rail
npx moltspay balance topup https://server.com 10.00 --rail crypto --tx-hash 0xabc... --chain base
npx moltspay balance topup https://server.com 10.00 --rail wechat --out-trade-no MP1719...
npx moltspay balance topup https://server.com 10.00 --rail alipay --trade-no 2026...
# Then pay password-free — no wallet, no QR
npx moltspay pay https://server.com my-service '{"prompt":"hello"}' --rail balance
# Check balance and history
npx moltspay balance query https://server.com
npx moltspay balance transactions https://server.com
SDK equivalent:
import { MoltsPayClient } from 'moltspay/client';
const client = new MoltsPayClient();
client.setBuyerId('my-buyer-id'); // persisted in config.json
await client.topupBalance('https://server.com', {
rail: 'crypto', amount: '10.00', txHash: '0xabc...', chain: 'base',
});
const result = await client.pay('https://server.com', 'my-service',
{ prompt: 'hello' }, { rail: 'balance' });
const info = await client.getBuyerBalance('https://server.com');
console.log(info.balance, info.today_spent);
Live service at https://moltspay.com/a/zen7
Services:
text-to-video - $0.01 USDC - Generate video from text promptimage-to-video - $1.49 USDC - Animate a static imageSupported Chains: Base, Polygon, Solana, BNB, Tempo (mainnet & testnet)
Try it:
# List services
npx moltspay services https://moltspay.com/a/zen7
# Pay on Base (default)
npx moltspay pay https://moltspay.com/a/zen7 text-to-video --prompt "a happy cat"
# Pay on different chains
npx moltspay pay https://moltspay.com/a/zen7 text-to-video --chain polygon --prompt "a happy cat"
npx moltspay pay https://moltspay.com/a/zen7 text-to-video --chain bnb_testnet --prompt "a happy cat"
npx moltspay pay https://moltspay.com/a/zen7 text-to-video --chain solana_devnet --prompt "a happy cat"
Join our Discord for help, feedback, and updates:
--> MoltsPay Discord
Or visit the #moltspay-support channel directly.
MIT
x402 (HTTP 402 Payment Required) SDK + MCP server: let any API charge for itself and any AI agent pay for itself, USDC & stablecoins across EVM, Solana & 8 more chain families, in a couple of lines. Backendless, no fee, self-custodial, paid straight to your wallet. TypeScript, MIT.
Give AI agents a wallet — x402 payment tools over Model Context Protocol.
⚡ The gold standard for x402 resources. 300+ projects, SDKs, tools, facilitators, and ecosystem data for the HTTP 402 Payment Required protocol. Curated by 24K Labs.
Automatic micro-donations powered by x402. Drop-in Express/Next.js middleware that sends USDC to charities on every user action. npm install x402charity.
Python SDK for MoltsPay - Agent-to-Agent Payments
DNA — Payment rails for AI agents. x402 micropayment protocol on Solana. Netting, transfer, stream settlement. Receipt anchoring on-chain.
A payments protocol for the internet. Built on HTTP.
Self-healing infrastructure for AI agent payments. 90.3% auto-recovery.
Daydreams is a set of tools for building agents for commerce
The best way to monetize AI applications & MCP, using x402, CDP Smart Wallets & Paymaster
URL shortener
AWS payment demo of x402 using Bedrock AgentCore, Strands SDK, and CloudFront.