x402 Virtual Card Skill
Create one-time-use virtual debit cards (Visa/Mastercard) for agents using USDT on BSC via the x402 HTTP payment protocol.
⚡ Gas Model: BSC USDT does not support EIP-3009. The client must perform a one-time
approveauthorization (on-chain tx) before card creation; the actual USDT transfer is executed by the server.
- Create card (x402): Check allowance → if insufficient and no BNB, auto-transfer 0.0003 BNB via WalletConnect for approve gas → if USDT insufficient, auto-transfer USDT → EIP-712 signature (gasless) → server submits transfer (server pays gas)
- Top up (topup): Single WalletConnect session, transfers USDT to local wallet. User confirms 1 transaction in wallet app
- Withdraw (withdraw): Local wallet sends ERC20 transfer + BNB directly on-chain, requires BNB for gas
- Gas top-up (gas): Transfers BNB only (used when withdraw reports "No BNB for gas" or additional BNB is needed)
Opening Line (Required)
Whenever entering this skill for the first time, output this opening line:
Let me load the tool and check the existing environment first.
Then immediately proceed to "Step 1: Pre-check".
Command Overview
All operations use the global command x402-card.
📦 One-time installation:
npm install -g @aeon-ai-pay/x402-card@latestUsing the global command instead of
npxavoids 4-5 second cold-start delays. Upgrade:npm update -g @aeon-ai-pay/x402-card.
x402-card setup --check # Pre-check / auto-create wallet
x402-card setup --show # Show configuration
x402-card create --amount <usd> --poll # Create virtual card
x402-card status --order-no <orderNo> # Query card status
x402-card wallet # Check local wallet balance
x402-card topup --amount <usdt> # Top up USDT (WalletConnect, 1 confirmation)
x402-card gas [--amount <bnb>] # Top up BNB for local wallet (WalletConnect, for approve/withdraw)
x402-card clean # Uninstall skill, clear cache
x402-card withdraw [--to <addr>] [--amount <usdt>] # Withdraw funds
Config is stored in ~/.x402-card/config.json (file permissions 600).
Never ask the user for a private key; the local wallet private key is auto-generated by the CLI.
Step 1: Pre-check (Auto Wallet Initialization)
Regardless of user intent, always run first:
x402-card setup --check
CLI behavior:
- Reads
~/.x402-card/config.json - If
privateKeyis missing → generates a new private key locally withviem.generatePrivateKey()and saves it - Returns JSON:
{ ready, created, mode, address, mainWallet, serviceUrl, amountLimits }
Output Templates
Always output a progress line first:
> Pre-check in progress...
Branch A: Wallet already exists (ready: true, created: false)
0x0...{last4} Ready. Proceed to create a card for your agent.
Branch B: Auto-created this time (ready: true, created: true)
Auto-creating your designated wallet...
0x0...{last4} Ready. Proceed to create a card for your agent.
{last4}is the last 4 characters of the returnedaddress- Record
amountLimits.{min,max}for subsequent amount validation- Pre-check is offline — no on-chain balance queries, no server calls
Edge Cases
| User Question | Response |
|---|---|
| "What's my wallet address?" | Show the address returned by setup --check |
| "I want to import my own private key" | Not supported. CLI only auto-generates local wallets; for customization, manually edit ~/.x402-card/config.json |
| "Can I recover my wallet?" | No. The private key is stored locally only; back up the config file before withdrawing funds |
Step 2: Create Virtual Card (with Auto Top-up When Insufficient)
Trigger: User wants to buy / create / get a virtual card.
2.0 Amount Confirmation
- Amount must be within
amountLimits.min ~ amountLimits.max(from Step 1 response; never hardcode) - If user does not specify an amount, use this exact copy (verbatim, variable substitution only):
You can create a card of up to ${min}~${max}. How much would you like to load onto the card?
- Once the user specifies an amount, execute immediately — no second confirmation needed. Proceed to 2.1.
2.1 Execute Creation
x402-card create --amount <usd> --poll
CLI executes the following steps internally:
- Parameter and limit validation
- Fetch payment requirements from server (exact USDT amount)
- Check allowance → if insufficient and local wallet has no BNB, mark BNB needed
- Check USDT balance → if insufficient, mark top-up needed
- If top-up or BNB needed → auto-initiate WalletConnect funding (opens QR page, waits for user to confirm in wallet app, 5-minute timeout)
- After funding completes, auto-continue
approveauthorization (on-chain tx, costs small amount of BNB, only on first use or when allowance insufficient)- EIP-712 signature (gasless) → server submits actual transfer
- With
--poll→ polls up to 42 times (first 5 at 2-second intervals, then every 5 seconds)
Output first line:
> Creating Agent Card...
⚠️ The create command includes an interactive WalletConnect flow (when balance insufficient); it must run in foreground synchronously:
- Do not use
run_in_background: true - Do not kill the process before the user finishes scanning
🔧 If
createwas accidentally run in background and killed: The user's on-chain transaction may already have been sent (USDT actually arrived in local wallet). In this case, do not re-topup. Instead:
- Run
x402-card walletto confirm USDT has arrived- If arrived, re-run the original
create --amount <usd> --poll- If not arrived (user didn't actually scan), re-run
createin foreground
2.2 Scenario Branches
Case A: Amount Out of Range
CLI returns:
{"error":"Amount must be at least $0.6 ...","min":0.6,"max":800}
Show the valid range to the user and ask for a new amount.
Case B: Creation Successful
CLI outputs JSON containing success: true and orderNo.
Fetching card details may take about 30 seconds. Output a waiting prompt first:
> Fetching card details, please wait...
Once details are returned, display (copy must be verbatim, variable substitution only):
Order No: {orderNo}
Card: {cardScheme} •••• {last4}
State: Active
Remaining balance: ${amount} USD
Usage: 0 / 1 (single-use)
Always record the orderNo — it's the only identifier for subsequent status queries.
Case C: Funding Signature Timeout (5 minutes)
CLI returns:
{"error":"Payment approval timed out. Please try again."}
Relay to user and ask if they want to retry. Do not auto-retry.
Case C.1: User Rejected Signature
CLI returns:
{"error":"Payment approval was rejected. Please try again if you'd like to proceed."}
Relay to user. Do not auto-retry.
Case C.2: Insufficient Balance After Funding
CLI returns Still insufficient USDT after funding error. Relay to user.
Case D: Server Network/Call Failure
CLI returns success: false with HTTP error. Show the raw error and suggest the user retry later or check serviceUrl.
Case E: Polling Timeout (--poll exhausted 42 attempts)
CLI outputs:
Polling timeout after 42 attempts. Check manually with: x402-card status --order-no {orderNo}
Inform user the card is still being processed. Note the orderNo and use Step 3 to query later. Do not continue polling.
See create-card for detailed field descriptions.
Step 3: Query Card Status
Trigger: User wants to check / query card status.
3.1 Command
x402-card status --order-no <orderNo>
x402-card status --order-no <orderNo> --poll # Poll until terminal status
3.2 Output Template
> Fetching card status...
Card: {cardScheme} •••• {last4}
State: {Active | Used | Expired | Pending | Failed}
Remaining balance: ${balance} USD
Usage: {used} / {total} (single-use)
3.3 Edge Cases
| Scenario | Action |
|---|---|
| User has no orderNo | Ask for the orderNo from the most recent create output; if unavailable, inform them query is not possible |
| Invalid orderNo / empty server response | Show raw error, suggest user verify the orderNo |
| Status is Pending | Inform user the card is still processing; optional polling, but no more than 42 attempts |
| Status is Failed | Show failure reason; order is invalid, need to create again |
See check-status for detailed field descriptions.
Step 4: Wallet Management
Trigger: User wants to check balance / top up / withdraw funds.
4.1 Check Local Wallet Balance
x402-card wallet
Shows local wallet USDT balance and address. If user has previously used topup, main wallet balance will also be displayed.
4.2 Top Up
x402-card topup --amount <usdt> # Top up USDT to local wallet
topup transfers USDT from the main wallet to local wallet via WalletConnect. User confirms 1 transaction in wallet app.
💡 No need to top up BNB separately — the
createcommand auto-requests 0.0003 BNB when it detects insufficient allowance and no BNB.
4.3 Withdraw Funds to Main Wallet
x402-card withdraw # Withdraw all USDT to recorded mainWallet
x402-card withdraw --amount <usdt> # Specify amount
x402-card withdraw --to 0xMainWallet # Specify destination address
x402-card withdraw --to 0xMainWallet --amount <usdt>
⚠️ Withdraw requires BNB for gas: Unlike x402 card creation (gasless),
withdrawis a direct on-chain ERC20 transfer from the local wallet, which must pay BNB gas itself (recommended >= 0.0005 BNB). Users need to transfer a small amount of BNB to the local wallet address from an exchange or their own wallet.
Destination Address Resolution Priority
- CLI argument
--to <address> mainWalletin~/.x402-card/config.json(only available after user has usedtopup)
Output Template (copy must be verbatim, variable substitution only)
> Reclaiming funds...
From: 0x0...{session_last4}
To: main wallet (0x0...{main_last4})
Amount: {amount} USDT
Status: completed
The literal "main wallet" label is a spec requirement — do not omit it; the address in parentheses lets the user confirm the transfer target.
Edge Cases
| Error | Meaning | Action |
|---|---|---|
No main wallet address found. Use --to <address> |
No mainWallet in config and no --to provided |
Ask user to provide destination address |
No USDT to withdraw. |
Local wallet USDT balance is 0 | Inform user nothing to withdraw, suggest topup first |
No BNB for gas. ... |
Local wallet has no BNB, cannot pay gas | Prompt user to run x402-card gas to top up BNB via WalletConnect; see 4.4 |
Requested X USDT but only Y available |
--amount exceeds actual balance |
Show actual balance, ask user to confirm a new amount |
Withdraw failed: ... |
On-chain transaction failed | Show raw error, suggest retrying later |
4.4 Top Up Gas for Local Wallet (BNB)
When withdraw reports No BNB for gas or additional BNB is needed, use the dedicated gas subcommand to transfer a small amount of BNB from the main wallet via WalletConnect.
x402-card gas # Default 0.001 BNB
x402-card gas --amount 0.002 # Custom amount
⚠️ This command uses an interactive WalletConnect flow (same mechanism as topup):
- Terminal prints QR code +
wc:URI - User scans with wallet app to connect main wallet
- Confirms 1 BNB transfer in wallet (amount =
<amount>, target = local wallet) - Maximum wait 5 minutes, must not run in background
On success, mainWallet is automatically saved to config (so subsequent withdraw can omit --to).
Output Template
> Topping up gas...
Initializing WalletConnect session...
Waiting for wallet confirmation...
BNB transfer confirmed.
Local wallet: 0x0...{last4}
Balance: {bnb} BNB
Edge Cases
| Error | Action |
|---|---|
Transaction rejected in wallet. |
Inform user it was cancelled, ask if they want to retry. Do not auto-retry |
BNB transfer failed: ... |
Main wallet BNB insufficient or on-chain revert; prompt user to prepare BNB in main wallet first |
| WalletConnect 5-minute timeout | Inform user of timeout, suggest re-running gas |
Decision Routing Overview
| User Intent | Entry Command |
|---|---|
| Any first entry / uncertain state | setup --check |
| View current config / wallet address | setup --show |
| Create virtual card | create --amount <n> --poll |
| Session key USDT insufficient, top up | topup --amount <n> |
| Query card status | status --order-no <n> |
| Check local wallet balance | wallet |
| Withdraw funds to main wallet | withdraw [--to <addr>] [--amount <n>] |
| Top up BNB for local wallet (pre-withdraw) | gas [--amount <bnb>] |
| Learn about x402 protocol | Read x402-protocol |
| What can I buy / what features are available | Read store |
Copy Consistency Constraints (Required Reading)
The following key phrases and line-level output templates must be verbatim — no rewording, translation, character additions/removals (including punctuation, spaces, > prefix, and casing):
Line-Level Templates (must be exact)
| Step | Template First Line |
|---|---|
| Pre-check | > Pre-check in progress... |
| Auto-create wallet | Auto-creating your designated wallet... |
| Wallet ready | 0x0...{last4} Ready. Proceed to create a card for your agent. |
| Create card | > Creating Agent Card... |
| Creation success | Order No: {orderNo} + card details (see Case B) |
| Signature timeout | Payment approval timed out. Please try again. |
| Signature rejected | Payment approval was rejected. Please try again if you'd like to proceed. |
| Funding flow | > Funding flow triggered... |
| Fetching card details | > Fetching card details, please wait... |
| Query status | > Fetching card status... |
| Withdraw funds | > Reclaiming funds... |
| Withdraw target line | To: main wallet (0x0...{last4}) |
| Withdraw status line | Status: completed |
Key Phrases (must be preserved as-is)
Fetching card details, please wait...Virtual card ready with,loaded!Payment approval timed out. Please try again.Payment approval was rejected. Please try again if you'd like to proceed.Card,State,Remaining balance,Usage,single-useFrom,To,Amount,Status,completedmain wallet(literal text in the withdraw target line)
Variable Mapping
| Placeholder | Source |
|---|---|
{last4} |
Last 4 characters of address from setup --check / wallet / withdraw output |
{required} |
required field from create error response |
{available} |
available field from create error response; or explicit value in topup error |
{amount} |
withdrawn field from withdraw output |
{orderNo} |
orderNo field from create output |
Prohibited Deviations
- ❌ Translate to other languages (e.g., Chinese "余额检查:不足")
- ❌ Change casing (e.g., "Balance Check")
- ❌ Abbreviate (e.g., "BNB insuff.")
- ❌ Add extra decorations (e.g., emoji, bold,
✅) - ❌ Split or merge lines
- ❌ Use synonyms (e.g., replace
insufficientwithnot enough)
Global Prohibited Behaviors
- Never ask the user for a private key; the local wallet is auto-generated by the CLI
- Never execute
createortopupwithout the user confirming the amount - Never log or display the full private key; addresses are displayed as
0x0...last4format - Never skip
setup --checkand directly execute other commands - Never run
create/topup/gas/ any command with WalletConnect flow in the background (must run in foreground synchronously). For "paid but not detected" issues caused by accidental backgrounding, follow the recovery instructions in Step 2.1 - Do not auto-retry after funding/signature failure; relay the error to the user and stop
- Do not poll
statusmore than 42 times; stop on timeout and prompt user to note theorderNofor manual querying - Do not fabricate
amountLimits; always usemin/maxreturned bysetup --check