@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.
- package/CHANGELOG.md +60 -0
- package/LICENSE +21 -0
- package/README.md +116 -0
- package/bin/cli.mjs +155 -0
- package/docs/env-vars.md +73 -0
- package/docs/exit-codes.md +65 -0
- package/docs/ide-setup.md +60 -0
- package/docs/output-schema.md +188 -0
- package/docs/recipes/cron-issue-cards.md +69 -0
- package/docs/recipes/error-recovery.md +53 -0
- package/docs/recipes/integrate-in-agent.md +108 -0
- package/docs/recipes/merchant-integration.md +243 -0
- package/docs/release-process.md +98 -0
- package/docs/troubleshooting.md +200 -0
- package/package.json +58 -0
- package/scripts/postinstall.mjs +40 -0
- package/skills/aigateway/SKILL.md +370 -0
- package/skills/aigateway/references/check-status.md +68 -0
- package/skills/aigateway/references/create-card.md +114 -0
- package/skills/aigateway/references/store.md +87 -0
- package/skills/aigateway/references/x402-protocol.md +143 -0
- package/src/balance.mjs +92 -0
- package/src/commands/clean.mjs +65 -0
- package/src/commands/create-card-status.mjs +67 -0
- package/src/commands/create-card.mjs +333 -0
- package/src/commands/create-image.mjs +428 -0
- package/src/commands/wallet-balance.mjs +47 -0
- package/src/commands/wallet-gas.mjs +99 -0
- package/src/commands/wallet-init.mjs +42 -0
- package/src/commands/wallet-topup.mjs +221 -0
- package/src/commands/wallet-withdraw.mjs +183 -0
- package/src/config.mjs +50 -0
- package/src/constants.mjs +22 -0
- package/src/error-codes.mjs +50 -0
- package/src/funding.mjs +216 -0
- package/src/output.mjs +85 -0
- package/src/sanitize.mjs +48 -0
- package/src/update-check.mjs +69 -0
- package/src/walletconnect.mjs +712 -0
- package/src/x402.mjs +120 -0
- package/templates/cline/.clinerules +53 -0
- package/templates/codex/AGENTS.md +56 -0
- package/templates/cursor/.cursor/rules/aigateway.mdc +60 -0
- 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
|