# Aicard

> Trigger this skill when the user expresses intent to create, manage, or query a virtual card, or wants to know what they can buy / do with the card. This includes intents such as: - "get a virtual card" - "create a card" - "card status" - "set up a card for an agent" - "what can I buy" - "show me what's available" - "what can I do?" - "what can I use the card for" - "buy <product> / shop online / order something / find me <product> under $X" Also, any request involving the creation of a one-time-use virtual Visa/Mastercard funded with cryptocurrency for agent use, or shopping on Shopify merchants and paying with that card.

- Skill: `aeon-project/aicard` (Agent Skill, multi-file: 5 files)
- Install (CLI): `npx skillmds@latest add aeon-project/aicard`
- Raw SKILL.md: https://api.skillmd.com/api/skills/aeon-project/aicard/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: aeon-project (https://skillmd.com/u/aeon-project)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/aeon-project/aicard

---


# 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](../../README.md#developer-integration), [docs/output-schema.md](../../docs/output-schema.md), and [docs/recipes/integrate-in-agent.md](../../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 `approve` authorization (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**:
> ```bash
> npm install -g @aeon-ai-pay/aicard@latest
> ```
> Using the global command instead of `npx` avoids 4-5 second cold-start delays.
> Upgrade: `npm update -g @aeon-ai-pay/aicard`.

```bash
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](../../docs/output-schema.md) and [docs/exit-codes.md](../../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:

```bash
aicard setup --check
```

CLI behavior:
1. Reads `~/.aicard/config.json`
2. If `privateKey` is missing → generates a new private key locally with `viem.generatePrivateKey()` and saves it
3. 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 returned `address`
> - 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

```bash
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 to `TEST000001` when omitted — do not prompt the user for it unless they explicitly mention a custom merchant ID.

CLI executes the following steps internally:
1. Parameter and limit validation
2. Fetch payment requirements from server (exact USDT amount)
3. Check allowance → if insufficient and local wallet has no BNB, mark BNB needed
4. Check USDT balance → if insufficient, mark top-up needed
5. **If top-up or BNB needed** → auto-initiate WalletConnect funding (opens QR page, waits for user to confirm in wallet app, 5-minute timeout)
6. After funding completes, auto-continue
7. `approve` authorization (on-chain tx, costs small amount of BNB, only on first use or when allowance insufficient)
8. EIP-712 signature (gasless) → server submits actual transfer
9. 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 `create` was 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:
> 1. Run `aicard wallet` to confirm USDT has arrived
> 2. If arrived, re-run the original `create --amount <usd> --poll`
> 3. If not arrived (user didn't actually scan), re-run `create` in 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:
```json
{"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](references/create-card.md) for detailed field descriptions.

---

## Step 3: Query Card Status

Trigger: User wants to **check / query card status**.

### 3.1 Command

```bash
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](references/check-status.md) for detailed field descriptions.

---

## Step 4: Wallet Management

Trigger: User wants to **check balance / top up / withdraw funds**.

### 4.1 Check Local Wallet Balance

```bash
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

```bash
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 `create` command auto-requests 0.0003 BNB when it detects insufficient allowance and no BNB.

### 4.3 Withdraw Funds to Main Wallet

```bash
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), `withdraw` is 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

1. CLI argument `--to <address>`
2. `mainWallet` in `~/.aicard/config.json` (**only available after user has used `topup`**)

#### 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.

```bash
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:

```bash
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 search` accept credit card payments, so just pick one — no need to worry about payment methods.

> 💰 **Want the cheapest**: `--sort price` returns results sorted by ascending price (`data.sortedBy:"price"`); take `products[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:

1. Run search with **both** `--image` and `--html`:
   `aicard shop search --query "…" [--country/--max-price/…] --image /tmp/aicard-search.png --html /tmp/aicard-search.html`
2. **FIRST, display the `--image` PNG 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.
3. *Then, optionally,* also publish the `--html` as 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.
4. 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 | … | … | … |

See [Rich Visual Presentation](#rich-visual-presentation-artifacts).

### 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**:

```bash
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 `options` is 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 on `options`). Map the choice to a `variantId` by matching `variants[].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 cart` with the chosen `variantId` + `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?

```bash
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 `cart` response has `testBackend: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 in `continueUrl` reveals it. **Placing such an order is not a real transaction.** When `shop pay` hits this it returns `TEST_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-test` and re-run to proceed; if the user wants a different merchant, switch. Do not add `--allow-test` on 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 pay` accepts cards. The very rare ones that don't take cards return `card_not_supported` (**no charge**) — just switch merchants.

```bash
# 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](#live-step-by-step--show-one-image-per-step-do-not-cram-into-a-single-tall-image). 🔒 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 pay` depends 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 returns `BROWSER_INSTALL_FAILED` — relay to the user to run `npx playwright install chromium` manually.
- **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 once `npm i -g playwright && npx playwright install chromium`, then retry `shop pay`.
- Envelope returns `cardSource` (`cache`|`new`), `outcome`, `cardLast4`, `order` — **never a full card number**. Runs headless, no window.
- **Default: run `shop pay` in the foreground synchronously (without `--wait-otp`), getting the result directly in ~30s**:
  ```bash
  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 `success` directly. 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).
- **Only when it returns `challenge_3ds` should you use a single background `--wait-otp` session to complete it** (not blind retrying):
  ```bash
  # 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.txt` submits it automatically. The verification code is sent to the card's bound email/phone.
  - ⚠️ Don't use `sleep N; tail/cat` to check progress (it gets blocked by the harness); use `run_in_background` + directly read the output file, or Monitor watching for `waiting for the code`/`outcome`.
  - `--assist` (finish manually in a popup window) is only used when even the session-based OTP doesn't work.
- 🚫 **Never re-run repeatedly without --wait-otp because of `challenge_3ds`** — completing 3DS uses `--wait-otp` **only once**. For `pending`/`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 pay` matches 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 when `cardSource:"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 the `create` command — **do not show it during the payment phase**.

**Card selection is automatic (no wallet needed if a card exists)**:
1. `pay` first reuses a **cached card** whose face value ≥ order total → skips the wallet entirely.
2. If none, it **issues a new card** from the wallet. On `INSUFFICIENT_USDT` / `NEEDS_APPROVE_GAS` / `WALLET_NOT_CONFIGURED`, the error carries a `hint` (e.g. run `aicard topup`) — relay it and stop, do not retry.
3. 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-run** `shop 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-otp` session 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 with `shop track`/`get_order`**.
- **The amount is authoritative from `amountCharged`** — prefer the final `total` on the thank-you page (incl. shipping+tax, `amountSource:"checkout_total"`); only fall back to `--amount` if 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 **`--amount` is not the actual charged amount**.
- `shipTo.email` **must 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 `orderUrl` is 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 to `receipt.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 `proofImage` proof 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.availableCountries` lists 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]:

```bash
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 returns `assist_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)

```bash
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:

1. **Your host renders images inline** (Claude Code / Desktop / claude.ai / many IDE chats) → generate `--image` and **display the PNG inline FIRST** (this is the always-visible presentation). *Then, if* you also have an Artifact/canvas tool, additionally publish `--html` as 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).
2. **Host renders images but you have no Artifact tool** → just `--image` inline. Skip `--html` publishing (nothing to publish it into).
3. **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).

1. Run pay in the **background** with a progress file:
   ```bash
   aicard shop pay … --progress-file /tmp/aicard-steps.jsonl --html /tmp/aicard-order.html   # ru

…(truncated)
