@aeon-ai-pay/aigateway 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/LICENSE +21 -0
  3. package/README.md +116 -0
  4. package/bin/cli.mjs +155 -0
  5. package/docs/env-vars.md +73 -0
  6. package/docs/exit-codes.md +65 -0
  7. package/docs/ide-setup.md +60 -0
  8. package/docs/output-schema.md +188 -0
  9. package/docs/recipes/cron-issue-cards.md +69 -0
  10. package/docs/recipes/error-recovery.md +53 -0
  11. package/docs/recipes/integrate-in-agent.md +108 -0
  12. package/docs/recipes/merchant-integration.md +243 -0
  13. package/docs/release-process.md +98 -0
  14. package/docs/troubleshooting.md +200 -0
  15. package/package.json +58 -0
  16. package/scripts/postinstall.mjs +40 -0
  17. package/skills/aigateway/SKILL.md +370 -0
  18. package/skills/aigateway/references/check-status.md +68 -0
  19. package/skills/aigateway/references/create-card.md +114 -0
  20. package/skills/aigateway/references/store.md +87 -0
  21. package/skills/aigateway/references/x402-protocol.md +143 -0
  22. package/src/balance.mjs +92 -0
  23. package/src/commands/clean.mjs +65 -0
  24. package/src/commands/create-card-status.mjs +67 -0
  25. package/src/commands/create-card.mjs +333 -0
  26. package/src/commands/create-image.mjs +428 -0
  27. package/src/commands/wallet-balance.mjs +47 -0
  28. package/src/commands/wallet-gas.mjs +99 -0
  29. package/src/commands/wallet-init.mjs +42 -0
  30. package/src/commands/wallet-topup.mjs +221 -0
  31. package/src/commands/wallet-withdraw.mjs +183 -0
  32. package/src/config.mjs +50 -0
  33. package/src/constants.mjs +22 -0
  34. package/src/error-codes.mjs +50 -0
  35. package/src/funding.mjs +216 -0
  36. package/src/output.mjs +85 -0
  37. package/src/sanitize.mjs +48 -0
  38. package/src/update-check.mjs +69 -0
  39. package/src/walletconnect.mjs +712 -0
  40. package/src/x402.mjs +120 -0
  41. package/templates/cline/.clinerules +53 -0
  42. package/templates/codex/AGENTS.md +56 -0
  43. package/templates/cursor/.cursor/rules/aigateway.mdc +60 -0
  44. package/templates/windsurf/.windsurfrules +48 -0
@@ -0,0 +1,188 @@
1
+ # Output Schema
2
+
3
+ Every `aigateway` command emits **exactly one line of JSON** to **stdout** —— the *envelope*. Human-readable progress logs go to **stderr** and can be safely ignored by programmatic consumers.
4
+
5
+ > Pass `--quiet` to suppress non-error stderr. Pass `--legacy-output` to fall back to the pre-envelope shape (see [Legacy mode](#legacy-mode)).
6
+
7
+ ## Envelope
8
+
9
+ ### Success
10
+
11
+ ```json
12
+ {
13
+ "ok": true,
14
+ "command": "create",
15
+ "version": "0.9.0",
16
+ "data": { /* command-specific payload */ }
17
+ }
18
+ ```
19
+
20
+ ### Failure
21
+
22
+ ```json
23
+ {
24
+ "ok": false,
25
+ "command": "create",
26
+ "version": "0.9.0",
27
+ "error": {
28
+ "code": "AMOUNT_OUT_OF_RANGE",
29
+ "message": "Amount must be at least $0.6. Allowed range: $0.6 ~ $800 USD.",
30
+ "min": 0.6,
31
+ "max": 800
32
+ }
33
+ }
34
+ ```
35
+
36
+ - `error.code` is a stable identifier from [`src/error-codes.mjs`](../src/error-codes.mjs) — see [exit-codes.md](./exit-codes.md) for the full list.
37
+ - `error.message` is human-readable and may change between versions; **do not** match on it for control flow.
38
+ - Additional fields under `error` are command-specific context (e.g. `min` / `max` / `address` / `required` / `available`).
39
+
40
+ ## Per-Command `data` Payloads
41
+
42
+ ### `wallet-init`
43
+
44
+ ```json
45
+ {
46
+ "ready": true,
47
+ "created": false,
48
+ "appId": "TEST000001",
49
+ "mode": "private-key",
50
+ "address": "0x...",
51
+ "mainWallet": "0x..." | null,
52
+ "serviceUrl": "https://...",
53
+ "amountLimits": { "min": 0.6, "max": 800 }
54
+ }
55
+ ```
56
+
57
+ ### `wallet-topup`
58
+
59
+ ```json
60
+ {
61
+ "ready": true,
62
+ "appId": "TEST000001",
63
+ "address": "0x...",
64
+ "initialUsdt": "0",
65
+ "usdt": "5",
66
+ "bnb": "0.0003",
67
+ "allowance": "115792...max",
68
+ "topup": "5" | null,
69
+ "approveTx": "0x..." | null
70
+ }
71
+ ```
72
+
73
+ ### `create-card`
74
+
75
+ ```json
76
+ {
77
+ "appId": "TEST000001",
78
+ "orderNo": "...",
79
+ "data": { /* sanitized server response */ },
80
+ "paymentResponse": { /* decoded PAYMENT-RESPONSE header */ },
81
+ "pollResult": { /* present when --poll succeeds */ }
82
+ }
83
+ ```
84
+
85
+ ### `create-image`
86
+
87
+ ```json
88
+ {
89
+ "appId": "TEST000001",
90
+ "prompt": "...",
91
+ "aspectRatio": "16:9",
92
+ "outputFormat": "png",
93
+ "model": "...",
94
+ "transaction": "0x...",
95
+ "images": [
96
+ { "url": "https://...", "localPath": "/.../...png", "format": "png", "width": 1024, "height": 576, "sizeBytes": 123456, "sizeHuman": "120.6 KB" }
97
+ ],
98
+ "balance": { "initial": "5", "before": "5", "after": "4.99", "charged": 0.01, "topup": null }
99
+ }
100
+ ```
101
+
102
+ **Dry-run (`--dry-run`)** — preflight checks complete but no signing/transaction occurs:
103
+
104
+ ```json
105
+ {
106
+ "dryRun": true,
107
+ "url": "...",
108
+ "paymentRequirements": { "amountUsdt": 0.66, "amountWei": "660000000000000000", "asset": "0x...", "payTo": "0x...", "orderNo": "..." },
109
+ "wallet": { "address": "0x..." },
110
+ "decision": { "needTopup": true, "needGas": false, "topupAmount": "0.660000" },
111
+ "will": ["fund_usdt_via_walletconnect", "approve_or_skip", "sign_payment_eip712", "submit_to_facilitator", "poll_status"]
112
+ }
113
+ ```
114
+
115
+ ### `create-card-status`
116
+
117
+ ```json
118
+ {
119
+ "success": true,
120
+ "model": {
121
+ "orderNo": "...",
122
+ "orderStatus": "SUCCESS" | "FAIL" | "PROCESSING" | "...",
123
+ "channelStatus": "...",
124
+ "cardStatus": "ACTIVE" | "PENDING" | "...",
125
+ "cardScheme": "VISA" | "MASTERCARD",
126
+ "cardNumber": "•••• 1234",
127
+ "...": "(sensitive fields like CVV / expiry are stripped)"
128
+ }
129
+ }
130
+ ```
131
+
132
+ ### `wallet`
133
+
134
+ ```json
135
+ {
136
+ "mode": "private-key",
137
+ "address": "0x...",
138
+ "usdt": "12.34",
139
+ "network": "BSC Mainnet (Chain ID: 56)",
140
+ "mainWallet": { "address": "0x...", "usdt": "..." }
141
+ }
142
+ ```
143
+
144
+ ### `wallet-gas`
145
+
146
+ ```json
147
+ {
148
+ "appId": "TEST000001",
149
+ "localWallet": { "address": "0x...", "bnb": "..." },
150
+ "transaction": "0x..."
151
+ }
152
+ ```
153
+
154
+ ### `wallet-withdraw`
155
+
156
+ ```json
157
+ {
158
+ "to": "0x...",
159
+ "transactions": { "usdt": "0x..." | null, "bnb": "0x..." | null },
160
+ "remaining": { "usdt": "0.0", "bnb": "0.0" }
161
+ }
162
+ ```
163
+
164
+ ### `clean`
165
+
166
+ ```json
167
+ {
168
+ "removed": ["skills", "npm-global", "npm-cache", "npx-cache"]
169
+ }
170
+ ```
171
+
172
+ ## Logging Flags
173
+
174
+ | Flag | Effect |
175
+ | ---- | ------ |
176
+ | `--verbose` | Enable verbose stderr logs. |
177
+ | `--quiet` | Suppress non-error stderr logs. The stdout envelope is unaffected. |
178
+ | `--legacy-output` | See below. |
179
+
180
+ ## Legacy Mode
181
+
182
+ For consumers still parsing the pre-envelope JSON shape, pass `--legacy-output` to get the old format on stdout (and errors on **stderr** as before):
183
+
184
+ ```bash
185
+ aigateway --legacy-output create-card --amount 5
186
+ ```
187
+
188
+ Legacy mode is kept for one or two minor releases as a migration aid. New integrations should use the envelope.
@@ -0,0 +1,69 @@
1
+ # Recipe — Schedule Recurring Card Issuance
2
+
3
+ Use this when an agent needs to issue cards on a schedule — for example, "create a $5 card every Monday at 09:00 to seed an autonomous shopping flow."
4
+
5
+ ## Prerequisites
6
+
7
+ 1. **Pre-funded session wallet.** WalletConnect requires a browser; cron jobs run headless. Top up the local wallet manually first:
8
+ ```bash
9
+ aigateway wallet-topup --amount 50 # adds USDT + a tiny amount of BNB for approve gas
10
+ ```
11
+ 2. **One-time `approve`.** Run `aigateway create-card --amount 0.6 --poll` once interactively so the approve transaction is on-chain. Subsequent creates won't need WalletConnect again until USDT is depleted.
12
+
13
+ ## A Minimal Wrapper Script
14
+
15
+ `~/bin/issue-card.sh`:
16
+
17
+ ```bash
18
+ #!/usr/bin/env bash
19
+ set -euo pipefail
20
+
21
+ AMOUNT="${1:-5}"
22
+ LOG_DIR="${HOME}/.aigateway/logs"
23
+ mkdir -p "$LOG_DIR"
24
+ TS=$(date -u +"%Y-%m-%dT%H-%M-%SZ")
25
+ LOG="$LOG_DIR/issue-${TS}.log"
26
+
27
+ # Capture envelope on stdout; quiet stderr noise
28
+ ENVELOPE=$(aigateway --quiet create-card --amount "$AMOUNT" --poll 2>"$LOG")
29
+ EXIT=$?
30
+
31
+ echo "$ENVELOPE" > "${LOG%.log}.json"
32
+
33
+ if [ "$EXIT" -ne 0 ]; then
34
+ CODE=$(echo "$ENVELOPE" | jq -r '.error.code // "UNKNOWN"')
35
+ echo "[$(date -u)] FAILED code=$CODE exit=$EXIT — see $LOG" >&2
36
+ # Surface failure via your alerting channel (Slack, email, etc.) here
37
+ exit "$EXIT"
38
+ fi
39
+
40
+ ORDER=$(echo "$ENVELOPE" | jq -r '.data.orderNo')
41
+ echo "[$(date -u)] OK orderNo=$ORDER amount=$AMOUNT"
42
+ ```
43
+
44
+ ```bash
45
+ chmod +x ~/bin/issue-card.sh
46
+ ```
47
+
48
+ ## Cron Entry
49
+
50
+ ```cron
51
+ # minute hour dom mon dow command
52
+ 0 9 * * 1 /Users/me/bin/issue-card.sh 5 >> /Users/me/.aigateway/logs/cron.log 2>&1
53
+ ```
54
+
55
+ > ⚠️ On macOS, `cron` may lack PATH access to `aigateway`. Use an absolute path (`/Users/me/.nvm/versions/node/v25/bin/aigateway`) or source your shell rc inside the wrapper.
56
+
57
+ ## Failure Modes to Watch
58
+
59
+ | Code | Likely cause in cron context | Fix |
60
+ | ---- | ---------------------------- | --- |
61
+ | `INSUFFICIENT_USDT` | Wallet ran dry. | Top up via `aigateway wallet-init` on a workstation. |
62
+ | `INSUFFICIENT_BNB` | Approve allowance expired or BNB depleted by retries. | Run `aigateway wallet-gas` to add a small amount. |
63
+ | `PAYMENT_TIMEOUT` | WalletConnect was triggered (unexpected — should be fully approved). | Run an interactive `aigateway wallet-init` once to refresh allowance. |
64
+ | `SERVICE_UNAVAILABLE` | Upstream outage. | Cron will retry next tick. Alert if 3+ consecutive failures. |
65
+
66
+ ## See Also
67
+
68
+ - [integrate-in-agent.md](./integrate-in-agent.md) — Node.js / Python subprocess wrapper.
69
+ - [error-recovery.md](./error-recovery.md) — Full code-by-code recovery table.
@@ -0,0 +1,53 @@
1
+ # Recipe — Error Recovery Strategy
2
+
3
+ Map each `error.code` returned by the envelope to a concrete recovery action. Use this table when wiring `aigateway` into an agent prompt or a control-flow layer.
4
+
5
+ | `error.code` | Exit | Recommended Recovery |
6
+ | ------------ | :--: | -------------------- |
7
+ | `WALLET_NOT_CONFIGURED` | 1 | Run `aigateway wallet-init` once (auto-creates a local session wallet). |
8
+ | `SERVICE_URL_MISSING` | 1 | Override via env `AIGATEWAY_SERVICE_URL`. The default service URL is wired into the CLI for production; this should never trigger in normal use. |
9
+ | `AMOUNT_INVALID` | 1 | Caller bug — input must be a numeric string. |
10
+ | `AMOUNT_OUT_OF_RANGE` | 1 | Re-prompt user with `error.min` ~ `error.max`. Do **not** silently clamp. |
11
+ | `AMOUNT_EXCEEDS_BALANCE` | 1 | Use the smaller of requested vs. `error.available`. |
12
+ | `INSUFFICIENT_USDT` | 1 | Top-up failed or partial. Surface `error.required` / `error.available` to user, ask whether to retry with a new amount. |
13
+ | `INSUFFICIENT_BNB` | 1 | Run `aigateway wallet-gas` to top up a small amount of BNB via WalletConnect, then retry. |
14
+ | `NO_FUNDS` | 1 | Nothing to withdraw. Inform the user, possibly suggest `wallet-topup`. |
15
+ | `NO_MAIN_WALLET` | 1 | Caller must pass `--to <address>`. |
16
+ | `PAYMENT_REJECTED` | 1 | User cancelled in their wallet. **Do not auto-retry** — ask user first. |
17
+ | `PAYMENT_TIMEOUT` | 2 | WalletConnect approval expired (5 min). Ask user whether to retry. **Do not auto-retry.** |
18
+ | `WC_SESSION_EXPIRED` | 2 | Reconnect required. Re-run the original command. |
19
+ | `POLL_TIMEOUT` | 2 | Card may still provision. Surface `error.orderNo` and query later with `aigateway create-card-status --order-no <n>`. |
20
+ | `TX_TIMEOUT` | 2 | The on-chain transfer is likely still pending — query the chain or retry the status command. |
21
+ | `SERVICE_UNAVAILABLE` | 3 | Exponential backoff: 1 s → 4 s → 16 s, max 3 attempts. |
22
+ | `PAYMENT_FETCH_FAILED` | 3 | Same as above. Check network connectivity. |
23
+ | `BALANCE_CHECK_FAILED` | 3 | BSC RPC hiccup. Retry once after 2 s. |
24
+ | `TX_REVERTED` | 3 | On-chain failure. Capture `error.message` for diagnosis; do not retry blindly. |
25
+ | `WITHDRAW_FAILED` | 3 | Withdraw transaction failed. Check `aigateway wallet-balance` and retry. |
26
+ | `INVALID_PAYMENT_AMOUNT` | 3 | Server-side issue (returned amount = 0). Retry after a short delay. |
27
+ | `PAYMENT_FAILED` | 3 | Service rejected the signed request. Surface `error.data` to the user / log. |
28
+ | `INTERNAL_ERROR` | 4 | File a bug. Don't retry. |
29
+ | `WALLET_ERROR` | 1 | Generic wallet failure. Surface to user, ask whether to retry. |
30
+
31
+ ## Generic Retry Helper (Node.js)
32
+
33
+ ```js
34
+ async function withRetry(fn, { codes, attempts = 3, baseDelayMs = 1000 } = {}) {
35
+ for (let i = 0; i < attempts; i++) {
36
+ const { exitCode, envelope } = await fn();
37
+ if (envelope.ok) return envelope;
38
+ if (!codes.includes(envelope.error.code)) return envelope; // non-retryable
39
+ if (i < attempts - 1) await new Promise(r => setTimeout(r, baseDelayMs * 4 ** i));
40
+ }
41
+ }
42
+
43
+ // Only retry transient service/network errors
44
+ await withRetry(() => runAEON AI Gateway(["create", "--amount", "5", "--poll"]), {
45
+ codes: ["SERVICE_UNAVAILABLE", "PAYMENT_FETCH_FAILED", "BALANCE_CHECK_FAILED", "INVALID_PAYMENT_AMOUNT"],
46
+ });
47
+ ```
48
+
49
+ ## Anti-patterns
50
+
51
+ - ❌ Don't retry `PAYMENT_REJECTED` or `PAYMENT_TIMEOUT` automatically — the user actively cancelled or walked away.
52
+ - ❌ Don't match on `error.message` text — messages may change between versions. Match on `error.code`.
53
+ - ❌ Don't ignore exit code in favour of envelope. Stack-level proxies sometimes mangle stdout; the exit code is a redundant safety net.
@@ -0,0 +1,108 @@
1
+ # Recipe — Integrate `aigateway` Inside Your Agent
2
+
3
+ This recipe shows how to invoke `aigateway` from inside an agent product (Node.js, Python, or anything that can spawn a subprocess) and parse the JSON envelope reliably.
4
+
5
+ ## Prerequisites
6
+
7
+ - `@aeon-ai-pay/aigateway` installed (globally for system-wide use, or as a dependency in your project).
8
+ - A working session wallet — run `aigateway wallet-init` once on the host.
9
+
10
+ > ⚠️ The CLI uses **WalletConnect for funding**, which opens a browser window with a QR code. If your agent runs headless or in containers without a display, fund the session wallet ahead of time (`aigateway wallet-init`) on a workstation, then ship the `~/.aigateway/config.json` to the runtime host. Agents should never embed user main-wallet private keys.
11
+
12
+ ## Node.js — Spawn & Parse Envelope
13
+
14
+ ```js
15
+ import { spawn } from "node:child_process";
16
+
17
+ function runAEON AI Gateway(args) {
18
+ return new Promise((resolve, reject) => {
19
+ const child = spawn("aigateway", args, { stdio: ["ignore", "pipe", "pipe"] });
20
+ let stdout = "";
21
+ let stderr = "";
22
+ child.stdout.on("data", (b) => { stdout += b; });
23
+ child.stderr.on("data", (b) => { stderr += b; });
24
+ child.on("close", (code) => {
25
+ let envelope;
26
+ try {
27
+ envelope = JSON.parse(stdout.trim().split("\n").pop());
28
+ } catch {
29
+ return reject(new Error(`Could not parse envelope. stderr: ${stderr}`));
30
+ }
31
+ resolve({ exitCode: code, envelope, stderr });
32
+ });
33
+ });
34
+ }
35
+
36
+ // Example: create a $5 card and poll until terminal
37
+ const { envelope, exitCode } = await runAEON AI Gateway([
38
+ "--quiet",
39
+ "create",
40
+ "--amount", "5",
41
+ "--app-id", "MY_AGENT_001",
42
+ "--poll",
43
+ ]);
44
+
45
+ if (envelope.ok) {
46
+ const { orderNo, data } = envelope.data;
47
+ console.log("Card ready:", data.model?.cardNumber, "order:", orderNo);
48
+ } else {
49
+ // See docs/recipes/error-recovery.md for code-by-code guidance
50
+ console.error(`Failed [${envelope.error.code}] (exit ${exitCode}):`, envelope.error.message);
51
+ }
52
+ ```
53
+
54
+ ### Why `--quiet`?
55
+
56
+ `--quiet` silences progress logs on stderr. The stdout envelope is one line of JSON either way — but suppressing stderr makes child-process orchestration cleaner.
57
+
58
+ ### Why `.split("\n").pop()`?
59
+
60
+ The envelope is **always the last line on stdout**. Tools that wrap the binary (npx, npm, asdf, fnm) may inject preamble lines; taking the last line is robust.
61
+
62
+ ## Python — Spawn & Parse Envelope
63
+
64
+ ```python
65
+ import json, subprocess
66
+
67
+ def run_aigateway(args):
68
+ result = subprocess.run(
69
+ ["aigateway", "--quiet", *args],
70
+ capture_output=True,
71
+ text=True,
72
+ check=False,
73
+ )
74
+ envelope = json.loads(result.stdout.strip().splitlines()[-1])
75
+ return result.returncode, envelope
76
+
77
+ exit_code, env = run_aigateway(["create", "--amount", "5", "--poll"])
78
+ if env["ok"]:
79
+ print("Card ready:", env["data"]["data"]["model"]["cardNumber"])
80
+ else:
81
+ print(f"Failed [{env['error']['code']}] exit={exit_code}: {env['error']['message']}")
82
+ ```
83
+
84
+ ## Probing Without Cost — `--dry-run`
85
+
86
+ To validate inputs, balances, and allowance **without** signing or transacting, use `--dry-run` on `create-card`:
87
+
88
+ ```bash
89
+ aigateway --quiet create-card --amount 5 --dry-run | jq '.data.will, .data.decision'
90
+ ```
91
+
92
+ This is ideal for integration tests, configuration smoke checks, and "is everything ready?" probes.
93
+
94
+ ## Exit Code Strategy
95
+
96
+ Treat exit codes as a fast filter, then branch on `error.code` for nuance:
97
+
98
+ ```js
99
+ switch (exitCode) {
100
+ case 0: /* success */ break;
101
+ case 1: /* user / config — surface to caller for correction */ break;
102
+ case 2: /* timeout — safe to retry; card may still be provisioning */ break;
103
+ case 3: /* network / service — exponential backoff retry */ break;
104
+ case 4: /* internal — log + fail loud */ break;
105
+ }
106
+ ```
107
+
108
+ See [exit-codes.md](../exit-codes.md) for the full mapping.
@@ -0,0 +1,243 @@
1
+ # Recipe — Merchant Integration
2
+
3
+ This recipe shows how a merchant integrates AEON AI Gateway into their own product (SaaS, agent, mobile app, IDE extension) via the CLI. The core pattern is the same as the [generic spawn integration](./integrate-in-agent.md), with one key addition: **every call carries your merchant `--app-id`** so the backend can attribute usage, settle billing, and route customer support.
4
+
5
+ ## 1. Prerequisites
6
+
7
+ ### 1.1 Get your `appId`
8
+
9
+ Ask the AEON team for a merchant `appId` (e.g. `MERCHANT_ACME_001`) to replace the public default `TEST000001`. All CLI calls should be made with `--app-id <yourId>`. The backend uses it for:
10
+
11
+ - Per-merchant USDT accounting / settlement.
12
+ - Usage analytics & rate limiting.
13
+ - Revenue share / referral.
14
+ - Support ticket correlation (`appId` echoes back in every envelope).
15
+
16
+ ### 1.2 Install the CLI on your target host
17
+
18
+ ```bash
19
+ npm install -g @aeon-ai-pay/aigateway
20
+ ```
21
+
22
+ ### 1.3 Pick a wallet model
23
+
24
+ | Model | Who owns the session key | Who pays | Use when |
25
+ | --- | --- | --- | --- |
26
+ | **A. User-managed** | User's machine (`~/.aigateway/`) | The end-user, via WalletConnect | Your product runs locally on the user's machine (IDE plugin, desktop CLI, agent) |
27
+ | **B. Merchant-custodial** | Your backend (Vault / KMS) | You pre-fund, then resell | Your product is SaaS / web; users pay you in fiat, you pay USDT to upstream |
28
+
29
+ Both modes use the same CLI; only the source of the session key (`EVM_PRIVATE_KEY`) differs.
30
+
31
+ ## 2. Wallet Model A — User-Managed
32
+
33
+ Your product walks the user through:
34
+
35
+ ```bash
36
+ aigateway wallet-init # one-time, auto-creates local key
37
+ aigateway wallet-topup --amount 5 # user scans QR to load 5 USDT
38
+ ```
39
+
40
+ Subsequent paid calls are spawned by your product (agent / IDE plugin / app):
41
+
42
+ ```js
43
+ import { spawn } from "node:child_process";
44
+
45
+ const APP_ID = "MERCHANT_ACME_001";
46
+
47
+ function runAigateway(args) {
48
+ return new Promise((resolve, reject) => {
49
+ const child = spawn("aigateway", ["--quiet", ...args, "--app-id", APP_ID]);
50
+ let stdout = "";
51
+ child.stdout.on("data", (b) => { stdout += b; });
52
+ child.on("close", (exitCode) => {
53
+ try {
54
+ resolve({ exitCode, envelope: JSON.parse(stdout.trim().split("\n").pop()) });
55
+ } catch (e) {
56
+ reject(new Error(`parse failed (exit ${exitCode}): ${e.message}`));
57
+ }
58
+ });
59
+ });
60
+ }
61
+
62
+ const { envelope } = await runAigateway(["create-card", "--amount", "5", "--poll"]);
63
+ if (envelope.ok) {
64
+ showCard(envelope.data); // card number already redacted to "•••• 4242"
65
+ } else if (envelope.error.code === "TOPUP_REQUIRED") {
66
+ promptUserToTopup(envelope.error.presets); // [5, 10, 20, 50]
67
+ }
68
+ ```
69
+
70
+ ## 3. Wallet Model B — Merchant-Custodial (most common SaaS)
71
+
72
+ Your backend holds a session private key (pre-generated, in Vault / AWS Secrets Manager / GCP Secret Manager). Pre-fund it once; route all customer requests through it.
73
+
74
+ ### 3.1 Inject the key via env var
75
+
76
+ aigateway already supports `EVM_PRIVATE_KEY` — no config file needed:
77
+
78
+ ```bash
79
+ EVM_PRIVATE_KEY=0xYourMerchantSessionKey \
80
+ aigateway create-card --amount 5 --app-id MERCHANT_ACME_001 --poll
81
+ ```
82
+
83
+ ### 3.2 Express backend example
84
+
85
+ ```js
86
+ import express from "express";
87
+ import { spawn } from "node:child_process";
88
+
89
+ const app = express();
90
+ app.use(express.json());
91
+
92
+ const APP_ID = process.env.AIGATEWAY_APP_ID; // e.g. MERCHANT_ACME_001
93
+ const SESSION_KEY = process.env.AIGATEWAY_SESSION_KEY; // fetched from Vault on boot
94
+
95
+ function runCmd(args) {
96
+ return new Promise((resolve, reject) => {
97
+ const child = spawn("aigateway", ["--quiet", ...args, "--app-id", APP_ID], {
98
+ env: { ...process.env, EVM_PRIVATE_KEY: SESSION_KEY },
99
+ });
100
+ let stdout = "";
101
+ child.stdout.on("data", (b) => { stdout += b; });
102
+ child.on("close", (code) => {
103
+ try { resolve({ code, envelope: JSON.parse(stdout.trim().split("\n").pop()) }); }
104
+ catch (e) { reject(e); }
105
+ });
106
+ });
107
+ }
108
+
109
+ // User paid you in fiat upstream, now you fulfil via aigateway
110
+ app.post("/api/issue-card", async (req, res) => {
111
+ const { userId, amount } = req.body;
112
+
113
+ // Your KYC / fraud / balance checks
114
+ if (!await canIssueCard(userId, amount)) return res.status(403).end();
115
+
116
+ const { code, envelope } = await runCmd(["create-card", "--amount", String(amount), "--poll"]);
117
+
118
+ if (envelope.ok) {
119
+ await db.cards.insert({
120
+ userId,
121
+ orderNo: envelope.data.orderNo,
122
+ cardNumber: envelope.data.data.model.cardNumber,
123
+ amount,
124
+ issuedAt: new Date(),
125
+ });
126
+ res.json({ ok: true, orderNo: envelope.data.orderNo });
127
+ } else {
128
+ await logErr(userId, envelope.error);
129
+ res.status(500).json({ error: envelope.error.code, message: envelope.error.message });
130
+ }
131
+ });
132
+
133
+ app.listen(3000);
134
+ ```
135
+
136
+ ### 3.3 Pre-funding the merchant pool
137
+
138
+ Run **once on a workstation with a real wallet app**, not on the production server:
139
+
140
+ ```bash
141
+ EVM_PRIVATE_KEY=<your-merchant-session-key> \
142
+ aigateway wallet-topup --amount 50 --app-id MERCHANT_ACME_001
143
+ ```
144
+
145
+ Scan QR with your treasury wallet, confirm two transactions (USDT + 0.0003 BNB for one-time approve). After this, the production server can issue cards / images **without ever opening WalletConnect**.
146
+
147
+ ### 3.4 Low-balance alerting
148
+
149
+ ```js
150
+ const { envelope } = await runCmd(["wallet-balance"]);
151
+ if (parseFloat(envelope.data.usdt) < 10) {
152
+ notifySlack(`aigateway USDT pool low: ${envelope.data.usdt}`);
153
+ }
154
+ ```
155
+
156
+ Schedule it via cron / your monitoring stack.
157
+
158
+ ## 4. Standard envelope handling
159
+
160
+ ```js
161
+ function handleEnvelope(envelope, exitCode) {
162
+ if (envelope.ok) return { ok: true, data: envelope.data };
163
+
164
+ switch (envelope.error.code) {
165
+ case "WALLET_NOT_CONFIGURED":
166
+ case "SERVICE_URL_MISSING":
167
+ throw new ConfigError(envelope.error);
168
+
169
+ case "INSUFFICIENT_USDT":
170
+ case "TOPUP_REQUIRED":
171
+ await yourTopupFlow();
172
+ return { ok: false, retry: true };
173
+
174
+ case "AMOUNT_OUT_OF_RANGE":
175
+ return { ok: false, userMessage: `Amount must be $${envelope.error.min}~${envelope.error.max}` };
176
+
177
+ case "POLL_TIMEOUT":
178
+ await scheduleStatusCheck(envelope.error.orderNo);
179
+ return { ok: false, asyncPending: true };
180
+
181
+ case "SERVICE_UNAVAILABLE":
182
+ case "PAYMENT_FETCH_FAILED":
183
+ case "BALANCE_CHECK_FAILED":
184
+ return { ok: false, retryWithBackoff: true };
185
+
186
+ case "PAYMENT_REJECTED":
187
+ case "PAYMENT_TIMEOUT":
188
+ return { ok: false, userMessage: "Payment not confirmed" };
189
+
190
+ default:
191
+ throw new UnexpectedError(envelope.error);
192
+ }
193
+ }
194
+ ```
195
+
196
+ Full code table: [exit-codes.md](../exit-codes.md). Concrete recovery actions per code: [error-recovery.md](./error-recovery.md).
197
+
198
+ ## 5. Sandbox / test environments
199
+
200
+ ### Dry-run (no real USDT)
201
+
202
+ ```bash
203
+ aigateway create-card --amount 5 --dry-run --app-id TEST000001
204
+ # envelope.data.dryRun = true; preflight passes, nothing signed
205
+ ```
206
+
207
+ ### Staging service URL
208
+
209
+ ```bash
210
+ AIGATEWAY_SERVICE_URL=https://staging-x402.aeon.xyz \
211
+ aigateway create-card --amount 5 --app-id YOUR_TEST_APPID --poll
212
+ ```
213
+
214
+ ### CI
215
+
216
+ Never invoke real paid commands in CI. Use `--dry-run` to validate your subprocess wrapper + envelope parser only.
217
+
218
+ ## 6. Operational checklist for merchants
219
+
220
+ | Item | Frequency | Command |
221
+ | --- | --- | --- |
222
+ | Session wallet USDT balance | Realtime / hourly | `wallet-balance` |
223
+ | Session wallet BNB balance | When needed (withdraw only) | `wallet-balance` |
224
+ | Top-up (manual, workstation) | When USDT is low | `wallet-topup --amount <n>` |
225
+ | Withdraw to merchant treasury | Monthly / quarterly | `wallet-withdraw --to <treasury>` |
226
+ | Auto version upgrade | Automatic | (handled by `src/update-check.mjs`) |
227
+ | Reconciliation | Per settlement | Use `envelope.data.orderNo` / `transaction` to match on-chain + backend records |
228
+
229
+ ## 7. Security checklist
230
+
231
+ - **Session key (custodial mode)** = funds. **Store in Vault / KMS / encrypted env var only**. Never commit to git or write to plaintext `.env` files in production.
232
+ - **`appId` is not a secret.** Leaking it doesn't lose money, but could let someone burn API quota under your identity. If the backend requires authenticated calls, the AEON team will issue a separate API key — confirm with them.
233
+ - **Card PII**: `envelope.data.data.model.cardNumber` is already redacted to `"•••• 4242"`. The full PAN / CVV / expiry is retrievable only via the merchant-side API with strong auth (not exposed in this CLI). Don't try to capture full card data from the CLI.
234
+ - **WalletConnect**: only run `wallet-topup` / `wallet-gas` / `wallet-init` on a workstation with a wallet app. Don't let your production server auto-spawn QR windows.
235
+ - **`EVM_PRIVATE_KEY` env var**: scope it to the process that needs it (e.g. systemd unit, k8s secret-mounted file). Don't echo it to logs.
236
+
237
+ ## 8. See also
238
+
239
+ - [integrate-in-agent.md](./integrate-in-agent.md) — Generic spawn-and-parse template (no merchant-specific bits)
240
+ - [error-recovery.md](./error-recovery.md) — Recovery per `error.code`
241
+ - [cron-issue-cards.md](./cron-issue-cards.md) — Scheduled paid calls
242
+ - [../exit-codes.md](../exit-codes.md) — Full error code reference
243
+ - [../output-schema.md](../output-schema.md) — envelope schema per command