x402card
Resolves ENS-based card policy text records on Ethereum mainnet via ENSIP-10 wildcard resolution and EIP-3668 CCIP-Read, allowing clients to read card status, limits, currencies, MCC allow-lists, and approver information directly from ENS.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@x402cardrequest $50 funding for researcher.x402card.eth"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
x402·card
The name and policy layer for AI agent cards. Give an agent a name, not a card number.
x402card is not a card issuer. Card programs issue the cards (Airwallex is the first one we plug into). x402card sits on top of them and adds the parts an AI agent needs before anyone should let it spend:
A name. Each agent card gets a human-readable ENS name,
researcher.x402card.eth, that stays the same when the card underneath is reissued, rotated or moved to another program.A public policy. Limits, currencies, allowed merchant categories, status and approver are ENS text records. Anyone (a merchant, another agent, an auditor) can check them with any ENS client, and every answer is signed and verified on-chain.
No credentials in the agent. The agent works through an MCP server with a token scoped to its own card. It never sees the card number, CVC, card ID or an issuer API key.
A human in the loop. Spending inside policy goes through. Funding requests above policy wait for the named approver. An anomaly freezes the card, flips
card.statustofrozenin ENS, and only a human can unfreeze it.
The policy is enforced twice: by x402card before anything is sent, and by the card program's own authorization controls when the charge reaches the network.
Demo site | |
ENS resolver (mainnet, verified) | |
CCIP-Read gateway |
|
MCP endpoint |
|
Card rails | Airwallex Issuing (sandbox) |
Sandbox, no real money. ENS resolution is real (Ethereum mainnet). Cards, charges and balances run on the Airwallex sandbox.
Contents
Related MCP server: Shatale MCP Server
How it works
┌──────────────────────────────────────────────┐
any ENS client │ Ethereum mainnet │
(viem, ethers, ───▶ │ ENS registry: x402card.eth │
wallets, apps) │ └─ resolver: OffchainResolver (ENSIP-10) │
│ reverts OffchainLookup(EIP-3668) ─────┼──┐
└──────────────────────────────────────────────┘ │
▼
AI agent ──MCP──▶ ┌────────────────────────────── Cloudflare Worker ──────────┐
(Claude, any │ /mcp tools: issue_card, get_card, pay, │
MCP client) │ request_funding, freeze, list_transactions │
│ /gateway signed answers to text()/addr() lookups │
human / platform │ /api approvals, unfreeze (admin) │
──REST────────▶ │ cron anomaly check every minute → freeze │
│ │
│ policy engine (plain code) KV: records · card refs · │
│ approvals · events │
└───────────────┬────────────────────────────────────────────┘
│ REST (x-client-id / x-api-key → bearer)
▼
Airwallex Issuing (sandbox): cardholders, cards,
authorization controls, transactions, simulationRegister.
issue_card("researcher", policy)creates a DELEGATE cardholder and a non-personalized virtual card on Airwallex, with the policy mapped to Airwallexauthorization_controls. It publishes the policy as ENS text records and returns an agent token scoped to that one name.Pay.
pay(amount, currency, mcc, merchant)is checked against the ENS policy first and refused before it reaches the card network if it breaks a rule. Airwallex then checks the authorization against the card's controls again: per-transaction limit, monthly limit, currencies and MCC allow-list. Either way a refusal carries the rule it hit (MERCHANT_CATEGORY_NOT_ALLOWED,LIMIT_EXCEEDED, …), so the agent can adapt instead of retrying. See How an agent pays.Fund.
request_funding(amount, currency)either raises the monthly allowance right away (withincard.limit.txand above the wallet reserve floor) or creates a pending approval for the human incard.approver. The approval is HMAC-bound to the exact name, amount and currency.Freeze. A cron job checks each card's recent activity. Two disallowed-merchant attempts within 10 minutes, or a burst of authorizations, freezes the card on Airwallex and sets
card.status=frozenin ENS. Agents can freeze their own card; only a human can unfreeze.
How an agent pays without the card number
The agent never holds a payment credential. It asks x402card to pay, and the credential travels from the card program to the merchant without passing through the agent:
agent ──pay(merchant, amount, currency, mcc)──▶ x402card
│ 1. pre-check against the ENS policy:
│ status, card.limit.tx, currency, card.mcc.allow
│ → refuse here, before anything reaches the network
▼
card program (issuer adapter)
│ 2. credential goes to the merchant:
│ network token, or vaulted PAN at checkout
▼
card network authorization
│ 3. issuer enforces the same limits again
▼
approved · or declined with the rule it hitIn production, step 2 uses whatever the card program offers for agent checkout: an agent-scoped network token (as in Visa Intelligent Commerce or Mastercard Agent Pay), or a vault that swaps the real card number into the merchant's checkout on the server side. Either way the agent's MCP token can't be turned into a card number, and the card number can't be used outside its authorization controls.
What runs today: steps 1 and 3 run as described: the agent calls the pay MCP tool, x402card pre-checks it, and the issuer authorizes it against the card's controls. Step 2 is simulated because the sandbox has no real merchants: the adapter uses Airwallex's simulation endpoint, which runs a real authorization against the card's controls. A production adapter swaps in network-token or vaulted checkout behind the same Issuer interface.
pay answers with one of three decisions, so the agent knows which layer stopped it:
| Meaning |
| Pre-check passed and the issuer authorized the charge |
| Refused by x402card ( |
| Passed the pre-check, refused by the issuer ( |
Codes are the same at both layers (MERCHANT_CATEGORY_NOT_ALLOWED, LIMIT_EXCEEDED, CURRENCY_NOT_ALLOWED, CARD_FROZEN, …). Blocked and declined attempts both count toward the anomaly rules, and a refusal runs them right away, so the second disallowed-merchant attempt freezes the card in the same call. Passing the same request_id on a retry replays the first result instead of charging again.
Governance
Who can change what, and what it takes:
Control | What it can do | Held by |
| point every card name at a different resolver | |
| change the gateway URL ( | |
Gateway signing key | sign record answers, valid 5 minutes each | Worker secret; separate from the owner key, revocable by the owner |
Approvals, unfreeze, policy edits | raise limits, unfreeze cards, publish records | Worker admin token → moving to wallet-signed approvals |
Agent token | its own card only: read, request funding, freeze | the agent |
Target model
A 2-of-3 Safe owns the root (done). The name and the resolver are owned by Safe
0x5A578eDdD28Bac066464BB2462ff052a65103602(Safe v1.5.0, threshold 2, three owners). Re-pointing the names, swapping the gateway or trusting a new signer needs two independent signatures, and a single leaked key can't make a frozen card read asactive.Approvals are signed by the approver.
card.approvernames a wallet or a Safe. A funding request above policy, or an unfreeze, applies only with an EIP-712 signature from that approver (ERC-1271 for a Safe), bound to the exact card, amount, currency and request ID. The admin token stops being able to approve on its own.Everything is visible. Policy lives in public, signed ENS records, policy events are published at
/public/feed, and the root's changes are on-chain Safe transactions.
What's already in place: agents hold only scoped tokens, approvals are HMAC-bound to their exact terms and apply once, an agent can freeze but never unfreeze, and the signing key is separate from the owner key. See Security model.
Status
Component | State |
ENS OffchainResolver on mainnet, | ✅ live, source verified |
CCIP-Read gateway (signed records) | ✅ live |
MCP server (6 tools, scoped tokens) | ✅ live |
Policy engine, approvals, anomaly freeze | ✅ live; tested end to end against a fake issuer |
Demo site on IPFS ( | ✅ live |
Airwallex card creation | ⏳ waiting on Airwallex to enable a card program on the sandbox account. Cardholders, config and balances work; |
Root governance: name + resolver owned by a 2-of-3 Safe | ✅ done; registrant, registry owner and resolver owner are the Safe |
Agent | ✅ live; tested end to end against a fake issuer |
Wallet-signed approvals (EIP-712 / ERC-1271) | ⏳ planned |
ENS records (the policy)
Every card name <label>.x402card.eth exposes these text records. They're public, so they hold policy only: never the card number, CVC or card ID.
Key | Example | Meaning |
|
| Max per transaction, in major units of the first currency. Funding requests above this need a human. |
|
| Monthly allowance. Approved funding raises it. |
|
| Allowed transaction currencies, comma-separated. The first is the limit currency. |
|
| Allowed 4-digit merchant category codes. Empty means any. |
|
| Mirrors the Airwallex card state. |
|
| Who approves escalations. |
Reserved labels that can't be issued: demo, www, app, api, gateway. demo.x402card.eth has its own on-chain resolver and hosts the site.
Policy engine
Plain, deterministic TypeScript with no LLM (src/policy/engine.ts):
Input | Rule | Outcome |
Records | → | enforced by Airwallex at authorization time |
Funding request | card frozen, currency not allowed, bad decimals | deny |
Funding request | amount > | escalate to |
Funding request | otherwise | approve: raise |
Payment ( | card frozen, bad amount or MCC, currency not allowed, MCC not in | block before the card network; otherwise pass to the issuer, which also enforces |
Activity (cron, and after every refused payment) | ≥ 2 disallowed-MCC attempts in 10 min, or > 10 payment attempts in 10 min (issuer authorizations + pre-check blocks) | freeze: card |
Amounts are major units (300 = $300), rounded to the currency's decimals. A charge counts as successful only once it reaches CLEARING; declines carry failure_reason.
Integrate: agents over MCP
The MCP server speaks Streamable HTTP (stateless JSON responses; protocol versions 2025-06-18, 2025-03-26, 2024-11-05) and authenticates with a bearer token.
Admin token: can call every tool, including
issue_card. Keep it on the operator side.Agent token (
x4c_…): returned once byissue_card. It only works for its own card, andissue_cardis hidden from it. Give this to the agent.
Claude Code
claude mcp add --transport http x402card https://x402card.dmpay.workers.dev/mcp \
--header "Authorization: Bearer x4c_your_agent_token"Claude Desktop, Cursor, VS Code and other JSON-configured clients
{
"mcpServers": {
"x402card": {
"type": "http",
"url": "https://x402card.dmpay.workers.dev/mcp",
"headers": { "Authorization": "Bearer x4c_your_agent_token" }
}
}
}For clients that only speak stdio, bridge with mcp-remote:
npx mcp-remote https://x402card.dmpay.workers.dev/mcp --header "Authorization: Bearer x4c_…"
Raw JSON-RPC
curl -s https://x402card.dmpay.workers.dev/mcp \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"request_funding","arguments":{"amount":300,"currency":"USD","purpose":"dataset license"}}}'Tools
Tool | Arguments | Returns |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
With an agent token, name is optional and defaults to the token's own card. Tool failures come back as results with isError: true and a code: message the model can read (forbidden, not_found, bad_policy, airwallex_…).
Integrate: read a card's policy from any ENS client
Records resolve through standard ENSIP-10 wildcard resolution with EIP-3668 CCIP-Read. No SDK is needed; any CCIP-Read-capable client works.
viem (CCIP-Read is on by default)
import { createPublicClient, http } from "viem";
import { mainnet } from "viem/chains";
const client = createPublicClient({ chain: mainnet, transport: http() });
const status = await client.getEnsText({ name: "researcher.x402card.eth", key: "card.status" });
const limit = await client.getEnsText({ name: "researcher.x402card.eth", key: "card.limit.tx" });ethers v6
const resolver = await provider.getResolver("researcher.x402card.eth");
const status = await resolver?.getText("card.status"); // CCIP-Read handled by the resolverweb3.py
w3.ens.get_text("researcher.x402card.eth", "card.status") # ccip_read_enabled defaults to TruePlain HTTP (no chain access; same data, unsigned)
curl https://x402card.dmpay.workers.dev/public/cards/researcher.x402card.ethUse it as a pre-flight check. A merchant, marketplace or another agent can refuse to start work for card.status != active, or quote within card.limit.tx, before any payment is attempted.
Integrate: platforms (approvals and oversight)
Admin endpoints (Authorization: Bearer <admin token>):
Method | Path | Body | Purpose |
|
| Pending and decided funding requests (newest first) | |
|
|
| Decide. Applies exactly the signed name, amount and currency; a second decision fails. |
|
| Human-only unfreeze; also flips |
Public, unauthenticated:
Method | Path | Purpose |
|
| The card's public records (same data the gateway signs) |
|
| Recent policy events: issued, funded, escalated, approved, denied, frozen, unfrozen |
Approval page: demo.x402card.eth.limo/approve.html is a static UI over these endpoints. It shows pending requests with an allowance preview, Approve/Deny, the fleet with Unfreeze, and decision history. Escalated request_funding results include an approval_url that deep-links to the request, so the agent can hand it to its human. The admin token is kept in the tab's session storage only. Add ?api=http://127.0.0.1:8787 to point the page at a local Worker.
Method | Path | Purpose |
|
| Fleet: every card with records, status and pending approvals (admin) |
Typical platform flows:
Agent platform / framework: issue one card per agent with the admin token, and pass the scoped agent token into the agent's MCP config. Your platform never stores card numbers.
Finance / ops console: poll
/api/approvals, show the request with its purpose, and post the decision. Watch/public/feedor the ENS records for freezes.Merchant / counterparty: resolve
card.statusand limits over ENS before accepting an order from an agent.
Integrate: bring your own card issuer
x402card never issues the card itself. It's a naming and policy layer: a portable, human-readable identity (<agent>.x402card.eth) with public spend policy, scoped agent access, human approvals and a kill switch. A card from any program can sit behind that name. Agent-card programs from card networks, issuing platforms and wallet/crypto card providers can all attach their cards to an x402card name instead of building naming, policy publication and approval flows themselves.
What an issuer gets by attaching to a name:
A stable handle that outlives card numbers. Reissue, rotate or swap the underlying card; agents, merchants and dashboards keep using
researcher.x402card.eth.Policy anyone can read. Merchants and counterparties check
card.statusand limits over ENS before accepting an order, with no integration with the issuer's API.Approvals and freezes in one place, applied consistently whatever rails the card runs on.
Agents never hold credentials. The agent talks MCP to x402card; the issuer's API keys and PANs stay server-side.
Airwallex is the first adapter. Card operations go through one interface (src/service.ts):
export interface Issuer {
createCardholder(email: string): Promise<{ cardholderId: string }>;
createCard(input: { cardholderId: string; nickName: string; controls: AuthorizationControls; requestId: string }):
Promise<{ cardId: string; last4?: string; status: string }>;
getCardStatus(cardId: string): Promise<string>;
updateCard(cardId: string, input: { status?: "ACTIVE" | "INACTIVE"; controls?: Partial<AuthorizationControls> }): Promise<void>;
listTransactions(cardId: string): Promise<IssuingTransaction[]>;
walletAvailable(currency: string): Promise<number>;
}An adapter for another program maps each record to whatever control that program supports:
x402card record | What the adapter needs from the issuer |
| per-authorization amount limit |
| rolling or calendar-month spend limit, updatable after issuance |
| allowed transaction currencies (or reject in the adapter if unsupported) |
| MCC allow-list (or block-list inverted) |
| freeze / unfreeze without closing the card |
transactions | list authorizations and clearings with decline reasons |
If a program can't enforce one of these at authorization time (for example, no MCC controls), the adapter should leave it to the policy engine's after-the-fact anomaly checks and say so in the card's records. Two ways to integrate:
Adapter in this repo: implement
Issuerfor your API and select it per card. Each card's issuer is recorded privately, never in public records.Attach an existing card: keep issuing on your own platform, and register the card under a name with your policy. x402card publishes the records, runs approvals and calls your freeze/limit endpoints. (Planned:
attach_cardtool and acard.issuerrecord naming the program.)
Compatibility
Area | Supported |
MCP clients | Any Streamable HTTP client: Claude Code, Claude Desktop, Claude.ai connectors, Cursor, VS Code, OpenAI Agents SDK, LangChain/LangGraph MCP adapters. stdio-only clients via |
MCP protocol |
|
ENS clients | Anything with ENSIP-10 + EIP-3668: viem ≥ 1, ethers v6 (and v5 with CCIP enabled), web3.py ≥ 6, ENS App, eth.limo gateways, most wallets with ENS display |
Records |
|
Chains | ENS on Ethereum mainnet. The resolver contract is chain-agnostic (Sepolia works the same). |
Card rails | Airwallex Issuing API ≥ |
Currencies | Any currency enabled in the account's Issuing config (USD, EUR, GBP, AUD, HKD, SGD, JPY, …). Zero-decimal currencies (JPY, KRW, …) are rounded correctly. |
Runtime | Cloudflare Workers + KV; Node ≥ 22.18 for scripts/tests (native TypeScript) |
Gateway protocol (for verifiers)
The resolver reverts with:
OffchainLookup(address(this), [url], callData, resolveWithProof.selector, abi.encode(callData, address(this)))The gateway answers GET /gateway/{sender}/{data}.json (or POST {sender, data}) with { "data": response }, where:
response = abi.encode(bytes result, uint64 expires, bytes signature)
hash = keccak256(0x1900 ‖ sender ‖ expires ‖ keccak256(callData) ‖ keccak256(result))resolveWithProof recovers the signer from hash and checks it against the contract's signers set. Answers are valid for 5 minutes. The owner can rotate the URL (setUrl) and signers (setSigner) without redeploying. Current gateway signer: 0x10d95862957943f0b488CEAb7043adA8d23E123A.
Security model
Agents never see card credentials. Tools return policy, status, last 4 and transactions; Airwallex IDs stay in a private KV prefix the gateway never serves. A test asserts the card ID never appears in tool output.
Scoped tokens. Agent tokens are random 256-bit values stored only as SHA-256 hashes and accepted only for their own name. Only the admin token can issue cards, decide approvals or unfreeze.
Approvals can't be retargeted. Each pending approval is HMAC-signed over
(id, name, amount, currency)and re-verified at decision time; it's marked decided before it is applied, so a double click applies once.Idempotent writes. Every Airwallex write carries a
request_idthat is reused only when retrying the same operation, so a retried create can't issue two cards.Signed ENS answers. Records verify on-chain against the gateway signer; a tampered result or an unknown signer is rejected (covered by
scripts/e2e-resolver.ts).Separate keys. The gateway signing key isn't the deployer key; compromising the Worker can't move funds, and the resolver owner can revoke the signer.
Secrets live in Wrangler secrets /
.dev.vars(gitignored), never in records, logs or responses.
Self-hosting
Prerequisites: an Airwallex sandbox account with Issuing and a card program enabled, a Cloudflare account, an ENS name you control, and a funded deployer key.
git clone https://github.com/RWA-ID/x402card && cd x402card
npm install
cp .dev.vars.example .dev.vars # fill in AWX_CLIENT_ID, AWX_API_KEY, generate the rest
npm run verify:airwallex # login + balances
npm run smoke:issuing # cardholder → card → allowed + declined charge
# Worker
npx wrangler kv namespace create KV # put the id in wrangler.jsonc
for k in GATEWAY_SIGNER_KEY AWX_CLIENT_ID AWX_API_KEY MCP_ADMIN_TOKEN APPROVAL_SECRET; do
npx wrangler secret put $k
done
npx wrangler deploy
# Resolver (from contracts/; reads PRIVATE_KEY + RPC from your env file)
cd contracts && npm install
GATEWAY_URL='https://<your-worker>/gateway/{sender}/{data}.json' \
GATEWAY_SIGNER=<address of GATEWAY_SIGNER_KEY> \
npx hardhat run scripts/deploy.ts --network mainnet
# then setResolver(namehash("<your-name>.eth"), <resolver>) on the ENS registryChange PARENT_NAME in src/ens/gateway.ts and PARENT_ADDR in wrangler.jsonc for your own name. If the parent name has records today (an address, a contenthash), serve them from the gateway before switching resolvers, as PARENT_ADDR does.
Demo runner
scripts/demo.ts runs the six-step demo against a live Worker: as the agent over MCP (with the scoped token it gets at issuance), and as the operator over the admin API (optionally, the approval).
Step | What happens |
01 |
|
02 | agent |
03 | agent |
04 | agent requests $300 → paused ( |
05 | second MCC 7995 attempt → anomaly rule fires in the same call → card frozen, |
06 | $5 at an allowed merchant → |
npm run demo # production; waits for you to approve on the page
npm run demo -- --auto-approve # unattended
npm run demo -- --name researcher2 # each run needs a fresh name (or --reuse)
npm run demo -- --api http://127.0.0.1:8787 --admin-token local-test # local, with ISSUER=fakeOptions: --pace <ms> between beats (default 1400, for screen recording), --no-ens to read records from the gateway instead of mainnet ENS. Operator-side tools: POST /api/cards/:name/simulate has a merchant charge the card directly, skipping the pre-check, to show the issuer layer on its own; POST /api/cards/:name/check runs the anomaly rules immediately instead of waiting for the cron.
Development
npm test # policy engine, approvals, MCP protocol + full demo script (fake issuer)
npm run typecheck
# Full stack locally, no Airwallex: in-memory issuer + throwaway admin token
npx wrangler dev --var ISSUER:fake --var MCP_ADMIN_TOKEN:local-test
(cd site/www && python3 -m http.server 8765) # open /approve.html?api=http://127.0.0.1:8787
cd contracts && npx hardhat node & # then, from the repo root:
node scripts/e2e-resolver.ts # resolver + gateway end to end on a local chainThe test suite runs the whole demo against FakeIssuer (src/testing.ts), which enforces card controls the way the sandbox does: issue researcher, an allowed pay clears, a disallowed MCC is blocked before the issuer, a $25 top-up auto-approves, a $300 request escalates and is approved, and a second disallowed MCC attempt freezes the card.
Repository layout
src/
index.ts Worker routes + cron
mcp.ts MCP server (JSON-RPC over HTTP), auth, tool schemas
service.ts card operations behind the tools (Issuer interface)
store.ts KV layout
ens/gateway.ts CCIP-Read gateway + signing
policy/records.ts ENS record schema + validation
policy/engine.ts records → controls, funding decisions, anomaly rules
policy/approvals.ts HMAC-bound approvals
airwallex/client.ts auth, token cache, retries, request_id
airwallex/issuing.ts cardholders, cards, transactions
airwallex/simulator.ts every sandbox simulation call behind one interface
contracts/ OffchainResolver.sol + Hardhat deploy
scripts/ Airwallex checks, resolver e2e, IPFS pinning
site/www/ demo site + approvals page (static; pinned to IPFS)Roadmap
Production checkout behind
pay: network-token (Visa Intelligent Commerce / Mastercard Agent Pay) or vaulted-PAN checkout in the issuer adapter, replacing the sandbox simulation.Wallet-signed approvals (EIP-712 from
card.approver, ERC-1271 for Safes) replacing the admin token for approvals and unfreezes.Issuer-agnostic attach:
attach_cardfor cards issued elsewhere, acard.issuerrecord, and adapters beyond Airwallex.Paid self-serve issuance over x402: let agents issue their own card name by paying a small fee in USDC, with the payer wallet becoming
card.approver.The same policy for x402 payments: apply a name's ENS policy to x402 / USDC payments on Base, so one name governs both card and stablecoin spending.
Airwallex webhooks for real-time freezes instead of the one-minute cron.
License
MIT. See LICENSE.
This server cannot be deployed
Maintenance
Related MCP Connectors
Prepaid virtual cards for AI agents: one-time cards, spend caps, human approvals.
Payment infrastructure for AI agents: spending rules, approval flows, single-use virtual cards.
Give your AI agent a spending limit: approval controls and single-use virtual cards.
Pre-spend firewall for AI agents. Approves, blocks, flags transactions against policy rules.
Related MCP Servers
- -licenseNot gradedqualityFmaintenanceEnables AI agents to manage and use prepaid virtual Visa cards with hard budget limits for secure online transactions. It provides tools for creating cards, checking balances, and retrieving payment credentials with human-in-the-loop approvals.1-

Shatale MCP Serverofficial
AlicenseAqualityAmaintenanceAI-native payment infrastructure that enables AI agents to make purchases, issue virtual cards, and manage spending within delegated budgets and policy controls.71,125 npmMIT- FlicenseAqualityDmaintenanceEnables AI agents to make contextual, private crypto payments using ENS identity and stealth addresses, with stealth-address privacy and agent spend policy controls.131-

Allowance MCPofficial
FlicenseNot gradedqualityDmaintenanceEnables AI agents to request purchase approval from humans, receive scoped virtual cards, complete checkout, and report receipts for audit.-