Virtual Card Skill
Create one-time-use virtual debit cards (Visa/Mastercard) that your agent can pay with online — top it up, then spend at real merchants.
What you can pay for: shop and check out across the Shopify merchant network (a huge catalog of online stores — apparel, electronics, home, beauty, toys, and more) — search a product, build a cart, and complete the purchase end-to-end, with real orders shipped to your address. The card also works anywhere Visa/Mastercard is accepted online.
🧭 Audience routing
- End-user workflow (this skill) — Helping a user buy / manage their own card inside an IDE / chat. Continue reading.
- Building your own agent product on top of aicard? Read README → Developer Integration, docs/output-schema.md, and docs/recipes/integrate-in-agent.md instead. Those cover spawn/parse, the envelope schema, exit codes, and error recovery.
⚡ 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: 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 aicard.
📦 One-time installation:
npm install -g @aeon-ai-pay/aicard@latestUsing the global command instead of
npxavoids 4-5 second cold-start delays. Upgrade:npm update -g @aeon-ai-pay/aicard.
aicard setup --check # Pre-check / auto-create wallet
aicard setup --show # Show configuration
aicard create --amount <usd> [--app-id <id>] --poll # Create virtual card (appId defaults to TEST000001)
aicard status --order-no <orderNo> # Query card status
aicard wallet # Check local wallet balance
aicard topup --amount <usdt> # Top up USDT (WalletConnect, 1 confirmation)
aicard gas [--amount <bnb>] # Top up BNB for local wallet (WalletConnect, for approve/withdraw)
aicard clean # Uninstall skill, clear cache
aicard withdraw [--to <addr>] [--amount <usdt>] # Withdraw funds
Config is stored in ~/.aicard/config.json (file permissions 600).
Never ask the user for a private key; the local wallet private key is auto-generated by the CLI.
Output Envelope
Every command writes one line of JSON to stdout — the envelope. Progress logs go to stderr and are not required for control flow.
- Success:
{ "ok": true, "command": "...", "version": "...", "data": { /* payload */ } } - Failure:
{ "ok": false, "command": "...", "version": "...", "error": { "code": "...", "message": "...", /* extra context */ } }
In the field references throughout this document, names like ready, orderNo, cardStatus refer to fields under envelope.data (success) or under envelope.error (failure). Match on error.code (stable) — not on error.message (may change).
See docs/output-schema.md and docs/exit-codes.md for the full schema and error code reference.
Step 1: Pre-check (Auto Wallet Initialization)
Regardless of user intent, always run first:
aicard setup --check
CLI behavior:
- Reads
~/.aicard/config.json - If
privateKeyis missing → generates a new private key locally withviem.generatePrivateKey()and saves it - Emits envelope;
envelope.data:{ ready, created, mode, address, mainWallet, serviceUrl, amountLimits }
Output Templates
Always output a progress line first:
> Pre-check in progress...
💡 On a successful pre-check, briefly tell the user what they can do (adapt to their language), so they know the card's purpose — highlight Shopify merchant coverage, keep it short, and do not mention x402 / USDT / on-chain internals here:
What you can do with this card:
- 🛍️ Shop & pay across the Shopify merchant network — search products, build a cart, and check out end-to-end (
shop search→shop cart→shop pay); real orders ship to your address.- 💳 Create / check / top up one-time virtual Visa·Mastercard for your agent.
Tell me what you'd like to buy, or say "create a card" to set one up.
Branch A: Wallet already exists (ready: true, created: false)
0x0...{last4} Ready. (Then add the "What you can do" blurb above.)
Branch B: Auto-created this time (ready: true, created: true)
Auto-creating your designated wallet...
0x0...{last4} Ready. (Then add the "What you can do" blurb above.)
{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 ~/.aicard/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
aicard create --amount <usd> --poll
# Optional: specify merchant app ID (defaults to TEST000001)
aicard create --amount <usd> --app-id <merchantAppId> --poll
🏷️
--app-id: Merchant identifier sent with the x402 request. Defaults toTEST000001when omitted — do not prompt the user for it unless they explicitly mention a custom merchant ID.
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
aicard 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.code = "AMOUNT_OUT_OF_RANGE" with error.min / error.max. Example envelope:
{"ok":false,"command":"create","error":{"code":"AMOUNT_OUT_OF_RANGE","message":"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 ok: true envelope; envelope.data contains orderNo, sanitized server data, and optional pollResult.
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.code = "PAYMENT_TIMEOUT", message: "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.code = "PAYMENT_REJECTED", message: "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 error.code = "INSUFFICIENT_USDT" with error.required / error.available. Relay to user.
Case D: Server Network/Call Failure
CLI returns ok: false envelope with error.code in (SERVICE_UNAVAILABLE, PAYMENT_FETCH_FAILED, PAYMENT_FAILED). Show the raw error and suggest the user retry later or check serviceUrl.
Case E: Polling Timeout (--poll exhausted 42 attempts)
CLI returns error.code = "POLL_TIMEOUT" with error.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
aicard status --order-no <orderNo>
aicard 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
aicard 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
aicard 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
aicard withdraw # Withdraw all USDT to recorded mainWallet
aicard withdraw --amount <usdt> # Specify amount
aicard withdraw --to 0xMainWallet # Specify destination address
aicard 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~/.aicard/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 aicard 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.
aicard gas # Default 0.001 BNB
aicard 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 |
Step 5: Shop on Shopify (search → cart → pay → track)
Trigger: user wants to buy a physical product / shop online — e.g. "buy wireless earbuds", "order a coffee mug", "find me running shoes under $50".
This flow discovers products across all Shopify merchants, builds a cart, issues a virtual card, and auto-fills the merchant checkout to pay. Full card details never leave the local process — the envelope only ever returns the last 4 digits.
Prerequisite: wallet ready with enough USDT (Step 1 + funding). The card is issued from the user's session wallet at pay time; issuance errors mirror Step 2.
5.1 Discover products (semantic search)
Ask what they want if unstated, then:
aicard shop search --query "<natural language>" [--country US] [--max-price 50] [--limit 5]
# single store only: add --shop <domain>
# for the cheapest: add --sort price (results sorted by ascending price, products[0] is the cheapest)
💳 Credit card only: all merchants returned by
shop searchaccept credit card payments, so just pick one — no need to worry about payment methods.
💰 Want the cheapest:
--sort pricereturns results sorted by ascending price (data.sortedBy:"price"); takeproducts[0]. Note it ranks the cheapest within the current result set (the top N Shopify returns by relevance), not the absolute lowest across the entire web; to widen the candidate pool, increase--limit(e.g. 30) or combine it with--max-price.
Present results as a visual, proactively (don't wait for the user to ask for images). The inline image is the primary, always-visible presentation — an Artifact alone is not enough because it often shows as a collapsed card the user must click, so the grid stays hidden (this is the "why can't I see the image?" trap). Do this, in order:
- Run search with both
--imageand--html:aicard shop search --query "…" [--country/--max-price/…] --image /tmp/aicard-search.png --html /tmp/aicard-search.html - FIRST, display the
--imagePNG inline (read/show it in your reply). This is what the user actually sees — the grid appears immediately, zero clicks. Never skip this — if you only publish the Artifact, the user sees a blank collapsed card and asks where the image went. - Then, optionally, also publish the
--htmlas an Artifact for click-to-pick interaction (it may render collapsed — that's fine, the inline image already showed the grid). If you have no Artifact tool, just skip this; the inline image stands alone. - Prompt: "Reply with a number (or click a card) to pick one". Record the chosen product's
productId,merchantDomain,detailUrl.
The image/card IS the presentation — do NOT also paste the full markdown table below it. And do NOT narrate the mechanism (in any conversation language): never say things like "generated a visual card" / "made it into an Artifact" / "here's a preview image" / "published an Artifact" — that's plumbing talk and reads as unfriendly. Let the visual speak for itself and say only substance about the products, e.g. "Found 6 — #6 ($10.99) is the cheapest, #1 has the fullest specs", then "reply with a number, or click a card". (If the Artifact shows collapsed rather than auto-opening, that's the host client's behavior and cannot be forced — but don't turn that into narration either; the inline --image already shows the visual.)
Markdown table = fallback only — use the table below only when you genuinely cannot publish an Artifact (a plain text-only terminal, or the user explicitly asked for text). Do not default to the table when an Artifact is available, and never show both.
| # | Product | Price | Merchant |
|---|---|---|---|
| 1 | {title} | ${priceMin} | {merchantName or merchantDomain} |
| 2 | … | … | … |
5.1b Product detail & pick options (after selecting a product, don't go straight to the cart)
Fetch full details and show a detail view:
aicard shop product --id <productId> --image <png> [--html <html>]
# ⚠️ Use the Global endpoint for Global search results (do not add --shop, otherwise gid://shopify/p/… will mismatch the storefront id and error out).
# Only when the previous step was a single-store search `shop search --shop <domain>` should you also add --shop <domain> here.
Rich visual (same rule as search): inline image first. Add --image <png> and display the PNG inline (large image + price + spec table) — this is the always-visible presentation, do it every time. Optionally also --html + publish as a clickable Artifact; skip if you can't render Artifacts. Don't narrate the mechanism — just show the card and talk about the product.
Present (consumer-facing detail, not a bare dump):
- Title + price + a one-line selling point (from
specText) - Spec table: lay out
options(e.g. color/size) as a table - Key parameters: extract material/weight/origin, etc. from
specText
Then:
- If
optionsis non-empty → ask the user to pick, e.g. "color+size, such as Black L" (for a fridge it would be "capacity+energy rating", etc., depending onoptions). Map the choice to avariantIdby matchingvariants[].options. - Fallback (important): UCP often returns only the default variant; if the chosen combination is not in
variants[], continue with the default variant's id and tell the user "the order was placed with the default spec; you can review/adjust the spec at checkout". Never let the flow stall just because an exact variant cannot be matched. - Proceed to
shop cartwith the chosenvariantId+merchantDomain.
5.2 Build cart & show the real total
First collect ship-to country + postal code (affects tax/shipping):
To check the exact total (incl. tax & shipping), what's your shipping country and postal/ZIP code?
aicard shop cart --shop <merchantDomain> --variant <variantId> [--qty 1] --country US --zip 10001 --image /tmp/aicard-cart.png
Rich visual (same rule): inline image first. Add --image <png> and display the cart-summary PNG inline (line items + subtotal/tax/shipping/total) — always-visible, no click. Optionally also --html + publish as an Artifact. Then give the confirmation prompt below. Don't narrate the mechanism.
⚠️ Test-store detection (important): if the
cartresponse hastestBackend:true, the merchant's checkout backend is a test store (e.g.twinoakstest.myshopify.com) — some merchants layer a custom domain (e.g.naturallife.com) over a test store, which you can't tell from the domain alone; only the backend host incontinueUrlreveals it. Placing such an order is not a real transaction. Whenshop payhits this it returnsTEST_STORE_BLOCKED(this is a confirmation gate, not a dead end,needsConfirm:true). The correct approach: tell the user "this merchant is a test store, the order is not a real transaction" and ask whether to continue; if the user replies "continue", add--allow-testand re-run to proceed; if the user wants a different merchant, switch. Do not add--allow-teston your own, and do not just give up.
Show the breakdown, then check for a cached card before giving the confirmation prompt (aicard shop cards, looking for a card with used:false && amount ≥ total), and choose one of the two based on that:
Cached card matched (should be preferred; no new card, no wallet activity):
{title} ×{qty} Total ${total} {currency} (tax/shipping settled at checkout)
This order will be paid with [existing virtual card •••• {last4} (face value ${amount})] — no new card, no wallet activity. Real charge, confirm the order? (yes/no)
No usable cached card (only then issue a new card, requires wallet USDT):
{title} ×{qty} Total ${total} {currency} (tax/shipping settled at checkout)
This order will be paid by [issuing a new ${total} virtual card] (USDT deducted from wallet). Real charge, confirm the order? (yes/no)
⚠️ Do not say "issue a new card" when a usable cached card exists — that contradicts
shop pay's actual behavior (cached card matched,cardSource:cache) and would mislead the user.
Record continueUrl and total.
5.3 Collect shipping details
Checkout needs the delivery address. Collect once:
To complete checkout I need: full name, email, address, city, ZIP, country, phone.
5.4 Pay (issue card + auto-fill checkout)
⚠️ Real charge: issues a real virtual card from the user's wallet and submits a real order. Only run after explicit confirmation.
💳 The agent need not worry about payment methods: credit card only, and any merchant that reaches
shop payaccepts cards. The very rare ones that don't take cards returncard_not_supported(no charge) — just switch merchants.
# No browser/timeout parameters needed: the code defaults to headless (no window in the background) and non-blocking by default
aicard shop pay \
--continue-url "<continueUrl>" --amount <total> \
--email <email> --first <First> --last <Last> \
--address1 "<street>" --city "<City>" --zip <zip> --country "<Country>" \
[--phone <phone>] [--region "<State>"] --progress-file /tmp/aicard-steps.jsonl [--html <path>]
Rich visual: show the checkout ONE image per step (not one composite). Run pay in the background with --progress-file, then surface each step's screenshot inline as it lands — see Live step-by-step. 🔒 The card-entry step has no screenshot (masked:true) — show a text line, never an image; the raw card-entry screenshot (full PAN/CVC) is never exposed. (--html still writes a single composite timeline you may publish as one scrollable Artifact summary, but the per-step inline images are the default experience — a single composite crams 9 steps into an unreadable strip.)
- The first purchase auto-downloads the browser engine:
shop paydepends on Playwright chromium (~150MB). When it detects it isn't downloaded, it downloads it automatically and continues (progress goes to stderr, first time only, reused thereafter); the first run therefore taking an extra minute or two is normal, not a hang. If the auto-download fails it returnsBROWSER_INSTALL_FAILED— relay to the user to runnpx playwright install chromiummanually. - If it returns
PLAYWRIGHT_MISSING(the playwright JS package itself isn't installed, usually because the optionalDependency silently failed during a global install): relay to the user to run oncenpm i -g playwright && npx playwright install chromium, then retryshop pay. - Envelope returns
cardSource(cache|new),outcome,cardLast4,order— never a full card number. Runs headless, no window. - Default: run
shop payin the foreground synchronously (without--wait-otp), getting the result directly in ~30s:aicard shop pay --continue-url "..." --amount ... <shipping parameters>- The vast majority of 3DS is frictionless (invisible): the script has "wait for it to pass automatically" built in and returns
successdirectly. This is the norm, done in one step. - Only 3DS that truly requires a verification code (rare) returns
outcome: challenge_3ds. In this case verification not completed = not authorized = not charged (abandoned 3DS incurs no charge).
- The vast majority of 3DS is frictionless (invisible): the script has "wait for it to pass automatically" built in and returns
- Only when it returns
challenge_3dsshould you use a single background--wait-otpsession to complete it (not blind retrying):# run_in_background; otp file defaults to /tmp/aicard-otp.txt aicard shop pay --wait-otp 600000 --continue-url "..." --amount ... <shipping parameters>- The script auto-clicks "Next/Send" to trigger the code → prompts "please give me the verification code" → you ask the user for the code →
echo "<code>" > /tmp/aicard-otp.txtsubmits it automatically. The verification code is sent to the card's bound email/phone. - ⚠️ Don't use
sleep N; tail/catto check progress (it gets blocked by the harness); userun_in_background+ directly read the output file, or Monitor watching forwaiting for the code/outcome. --assist(finish manually in a popup window) is only used when even the session-based OTP doesn't work.
- The script auto-clicks "Next/Send" to trigger the code → prompts "please give me the verification code" → you ask the user for the code →
- 🚫 Never re-run repeatedly without --wait-otp because of
challenge_3ds— completing 3DS uses--wait-otponly once. Forpending/error(Pay was clicked, result unknown), never re-run at all; verify first (see the anti-double-charge rule below). - 🏷️ Show payment progress truthfully, don't use "Creating Agent Card": when
shop paymatches a cached card it does not create a card or touch the wallet (the CLI log is> Using cached card •••• {last4} …paying). Show progress truthfully as "paying with cached card •••• {last4}…"; only whencardSource:"new"(confirmed no usable card, issuing a new card from the wallet) should you say "paying by issuing a new card". "Creating Agent Card…" is the copy for thecreatecommand — do not show it during the payment phase.
Card selection is automatic (no wallet needed if a card exists):
payfirst reuses a cached card whose face value ≥ order total → skips the wallet entirely.- If none, it issues a new card from the wallet. On
INSUFFICIENT_USDT/NEEDS_APPROVE_GAS/WALLET_NOT_CONFIGURED, the error carries ahint(e.g. runaicard topup) — relay it and stop, do not retry. - One-time cards are marked used after a successful order (won't be reused).
Use aicard shop cards to list cached cards (masked last-4 only).
🚫 Anti-double-charge rule (highest priority): the agent never blindly re-runs payment. Two categories:
pending/error(paySubmitted:true, result genuinely unknown): the charge may already have gone through, absolutely never re-runshop pay. First verify whether the order went through (confirmation email in the shipping inbox / proof image in~/.aicard/receipts/ merchant order), then let the user decide.challenge_3ds/challenge_captcha(verification not completed = not authorized = not charged): this is the only case that can be "completed" — use a single background--wait-otpsession to fill in the verification code (see above). Complete it only once, don't re-run repeatedly.- All other "before clicking Pay" failures (
shipping_not_ready/checkout_unavailable/fill_failed/no_card_iframe/address_incomplete,paySubmitted:false, all uncharged): just report; whether to place another order is the user's decision, the agent does not auto re-run.
outcome |
Meaning | Next |
|---|---|---|
success |
Order placed and completed | Show receipt (template below); do not show the proofImage local path (only provide it when the user asks for the receipt) |
challenge_3ds / challenge_captcha |
User verification code needed (verification not completed = not charged) | Complete with a single background --wait-otp: script auto-sends the code → ask the user for the code → echo "<code>" > /tmp/aicard-otp.txt to fill it in. Don't re-run repeatedly |
pending / error |
Pay was clicked, result unknown (paySubmitted:true) |
⚠️ The charge may have gone through, do not re-run. First verify (email/proof image/merchant order), then let the user decide |
declined |
Card declined (not charged) | Show signals.formError; after reporting, let the user decide whether to switch cards and retry |
card_not_supported |
This merchant does not accept credit cards (not charged) | Just switch to a merchant that accepts credit cards |
shipping_not_ready |
Shipping method never loaded (not charged, no order placed) | Report; whether to resend later is the user's decision |
fill_failed / no_card_iframe |
Form fill not completed, not submitted (not charged) | Report; let the user decide to complete manually with --assist or resend |
address_incomplete |
Country/state not selected or field missing (not charged) | Check signals.reason; after reporting, have the user supply --region etc. and then retry manually |
Success receipt envelope.data.receipt (web order via the browser path) — be sure to display it in full per the template below; don't drop fields (especially email/amount breakdown):
✅ Order confirmed
- Merchant confirmation number: {orderNumber} ({merchant})
- Items: {items, one per line}
- Breakdown: items {subtotal} + shipping {shippingFee} + tax {tax} = {total} (actual card charge {amountCharged})
- Payment: {payment.scheme} •••• {payment.last4} ({payment.note})
- Shipping: {shippingMethod} → {shipTo.name}, {shipTo.address}
- Email: {shipTo.email} (order/shipping confirmation emails are sent here)
Do not show the user the local absolute path of
receipt.proofImage(/Users/…/.aicard/receipts/…is CLI-internal storage and would confuse the user). The proof image is still saved to disk for the record; only provide the path when the user actively asks for the receipt.
Key points:
orderNumber(e.g.X0FCMYJAT) is the merchant confirmation number, not a Shopify API Global ID, and cannot be queried withshop track/get_order.- The amount is authoritative from
amountCharged— prefer the finaltotalon the thank-you page (incl. shipping+tax,amountSource:"checkout_total"); only fall back to--amountif it can't be captured (product price only,amountSource:"cli_amount_fallback", may be too low, so tell the user to rely on the card statement). Shipping/tax are only added at checkout settlement, so--amountis not the actual charged amount. shipTo.emailmust be shown — this is the address that receives order/shipping confirmation emails.proofImage= a locally persisted payment proof image~/.aicard/receipts/receipt-<confirmation-number>-<ts>.png(thank-you page screenshot, no full card face). By default do not show this local path (it would confuse the user); only provide it when the user actively asks for the receipt.- Viewing the order again (the original thank-you page link
orderUrlis session-bound and cannot be reopened — testing shows opening it in a new browser bounces back to the home page requiring login,orderUrlDurable:false). Guide the user toreceipt.reopenVia:- The View your order link in the merchant confirmation email in the shipping inbox (durably openable)
- The Download to track with Shop on the thank-you page (requires a Shop account)
- The local
proofImageproof image (offline record)
After a failure, check envelope.suggestion first (it distinguishes the two failure categories, don't blindly assist):
- "Shipping restriction" — this merchant doesn't ship to this country (
signals.availableCountrieslists the actually supported ones) → do not assist (a popup won't help either), switch shipping country or switch merchants. - "Recoverable" —
fill_failed/no_card_iframe/ verification code, etc. → re-run with assist mode: pop up a visible browser window, the script fills in the known info, and the user completes the rest (country/state/verification code) and manually clicks [Pay]:
aicard shop pay --assist --continue-url "..." --amount ... --email ... <other shipping parameters as above>
- assist keeps the window open up to 10 minutes waiting for the user; once the user clicks Pay and reaches the success page it auto-finalizes (
outcome: success+order); on timeout/incomplete it returnsassist_incomplete. - The card number is filled by the script in browser memory and does not enter the conversation/LLM/terminal.
5.5 Track order (optional)
aicard shop track --order <orderId> [--bearer <JWT>]
Requires a Token-tier credential (read_global_api_orders). ⚠️ Can only query orders completed via the pure-API complete_checkout — orders from the current browser card-fill path (shop pay) cannot be found (orderNumber is a merchant confirmation number, not a Global ID). For browser-path orders, always rely on the receipt from 5.4 (confirmation number + local proof image + confirmation email); do not attempt shop track.
Rich Visual Presentation (Artifacts)
Whenever you have an Artifact/canvas capability (Claude Desktop, claude.ai, any host with the Artifact tool), making the shopping flow visual is the DEFAULT — do it proactively at every stage, without being asked. At each stage the CLI produces a self-contained HTML page you publish as an Artifact. Only drop to markdown tables/text when you genuinely cannot render an Artifact.
Every stage of the journey (search → detail → cart → confirm → order) has a visual. Each command takes both --html <path> (self-contained clickable page → publish as an Artifact) and --image <path> (a PNG rendered via Playwright). Default = emit both and show both: display the PNG inline (guaranteed auto-visible, zero clicks — no artifact panel needed) and publish the HTML as an Artifact (interactive / right-panel). The inline image is what makes it appear automatically; the Artifact adds click-interaction.
| Stage | Command | What it renders |
|---|---|---|
| Search | shop search … --html <h> --image <p> |
Product-card grid (cover images + price + merchant; cards clickable → "Buy item #N") |
| Detail | shop product --id … --html <h> --image <p> |
Product-detail card (large image + price + spec table; clickable to buy) |
| Cart | shop cart … --html <h> --image <p> [--confirmable] |
Cart-summary card (line items + subtotal/tax/shipping/total) |
| Confirm | shop confirm --first … --html <h> --image <p> [--confirmable] |
"Confirm Details" card (name/email/address/phone — no card PII) |
| Order | shop pay … --html <h> --image <p> |
Order-flow timeline: summary + Attempt log + step screenshots (card step masked) |
Client compatibility — pick the richest your host supports, degrade gracefully
This skill runs on many hosts (Claude Code/Desktop, Cursor, Codex, Gemini CLI, Windsurf, …) with different display abilities. The aicard CLI is identical everywhere — it only writes files; how you surface them is your call based on your host's capabilities. Use this ladder, top-down, and stop at the first you can do:
- Your host renders images inline (Claude Code / Desktop / claude.ai / many IDE chats) → generate
--imageand display the PNG inline FIRST (this is the always-visible presentation). Then, if you also have an Artifact/canvas tool, additionally publish--htmlas a clickable Artifact — but the inline image is what the user sees, so never skip it (an Artifact alone often shows collapsed and hides the grid). - Host renders images but you have no Artifact tool → just
--imageinline. Skip--htmlpublishing (nothing to publish it into). - Text-only host (headless terminal CLIs, plain shells) → do NOT pass
--image(it launches a headless browser and, on first use, downloads ~150 MB of Chromium the user can't even see) and do NOT try to "publish an Artifact" (no such tool). Present the markdown table / text instead. This always works.
Rule of thumb: never invoke --image unless you can actually display an image to the user, and never claim to publish an Artifact unless you have that tool. When unsure, the markdown table is the safe universal fallback. --html is cheap (no browser) so it's fine to also write it as a file the user can open in a browser, even on text hosts — just don't call it an "Artifact".
Live step-by-step — show one image per step (do NOT cram into a single tall image)
The purchase flow (issue card → open checkout → fill address → shipping → fill card → submit → receipt) previously returned only at the end. Present it as one clean image per step, shown one at a time as steps land — not as a single composite timeline PNG (that stacks 9 steps into a tall, unreadable strip).
- Run pay in the background with a progress file:
aicard shop pay … --progress-file /tmp/aicard-steps.jsonl --html /tmp/aicard-order.html # ru
…(truncated)