@aeon-ai-pay/aigateway 0.2.3 → 0.2.5

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.
@@ -6,15 +6,16 @@ This recipe shows how to invoke `aigateway` from inside an agent product (Node.j
6
6
 
7
7
  - `@aeon-ai-pay/aigateway` installed (globally for system-wide use, or as a dependency in your project).
8
8
  - A working session wallet — run `aigateway wallet-init` once on the host.
9
+ - First-time funding done with `aigateway wallet-topup --amount 5` (one-time WalletConnect flow + facilitator `approve`). After this, all `sb invoke` calls are gasless and headless-friendly.
9
10
 
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
+ > ⚠️ `wallet-topup` opens a browser window with a WalletConnect QR code. If your agent runs headless or in containers without a display, fund the session wallet ahead of time on a workstation, then ship the `~/.aigateway/config.json` (or its `privateKey`) to the runtime host. Agents should never embed user main-wallet private keys.
11
12
 
12
13
  ## Node.js — Spawn & Parse Envelope
13
14
 
14
15
  ```js
15
16
  import { spawn } from "node:child_process";
16
17
 
17
- function runAEON AI Gateway(args) {
18
+ function runAigateway(args) {
18
19
  return new Promise((resolve, reject) => {
19
20
  const child = spawn("aigateway", args, { stdio: ["ignore", "pipe", "pipe"] });
20
21
  let stdout = "";
@@ -33,18 +34,20 @@ function runAEON AI Gateway(args) {
33
34
  });
34
35
  }
35
36
 
36
- // Example: create a $5 card and poll until terminal
37
- const { envelope, exitCode } = await runAEON AI Gateway([
37
+ // Example: invoke an AI tool (image generation) via the x402 paid entry point
38
+ const { envelope, exitCode } = await runAigateway([
38
39
  "--quiet",
39
- "create",
40
- "--amount", "5",
40
+ "sb", "invoke",
41
+ "--model", "replicate/black-forest-labs/flux-schnell",
42
+ "--inputs", JSON.stringify({ prompt: "a cyberpunk fox", aspect_ratio: "1:1" }),
41
43
  "--app-id", "MY_AGENT_001",
42
- "--poll",
43
44
  ]);
44
45
 
45
46
  if (envelope.ok) {
46
- const { orderNo, data } = envelope.data;
47
- console.log("Card ready:", data.model?.cardNumber, "order:", orderNo);
47
+ const { model, transaction, downloaded, balance } = envelope.data;
48
+ console.log(`Done (${model}) tx=${transaction}`);
49
+ for (const d of downloaded) console.log(`Saved: ${d.localPath} (${d.sizeHuman})`);
50
+ console.log(`Charged ${balance.charged} USDT, remaining ${balance.after}`);
48
51
  } else {
49
52
  // See docs/recipes/error-recovery.md for code-by-code guidance
50
53
  console.error(`Failed [${envelope.error.code}] (exit ${exitCode}):`, envelope.error.message);
@@ -74,22 +77,40 @@ def run_aigateway(args):
74
77
  envelope = json.loads(result.stdout.strip().splitlines()[-1])
75
78
  return result.returncode, envelope
76
79
 
77
- exit_code, env = run_aigateway(["create", "--amount", "5", "--poll"])
80
+ exit_code, env = run_aigateway([
81
+ "sb", "invoke",
82
+ "--model", "replicate/black-forest-labs/flux-schnell",
83
+ "--inputs", json.dumps({"prompt": "a cyberpunk fox"}),
84
+ ])
78
85
  if env["ok"]:
79
- print("Card ready:", env["data"]["data"]["model"]["cardNumber"])
86
+ for d in env["data"]["downloaded"]:
87
+ print("Saved:", d["localPath"])
80
88
  else:
81
89
  print(f"Failed [{env['error']['code']}] exit={exit_code}: {env['error']['message']}")
82
90
  ```
83
91
 
84
- ## Probing Without Cost `--dry-run`
92
+ ## Discovering Models — `sb tools`
85
93
 
86
- To validate inputs, balances, and allowance **without** signing or transacting, use `--dry-run` on `create-card`:
94
+ Before calling `sb invoke`, fetch the live catalog with `sb tools` to pick a valid `model` and learn the expected `inputs` schema:
87
95
 
88
96
  ```bash
89
- aigateway --quiet create-card --amount 5 --dry-run | jq '.data.will, .data.decision'
97
+ # Single model + effectiveSchema (most useful for agents)
98
+ aigateway --quiet sb tools --model replicate/black-forest-labs/flux-schnell | jq '.data'
99
+
100
+ # All models in a category
101
+ aigateway --quiet sb tools --category image | jq '.data.categories[0].models[].id'
102
+
103
+ # Cheapest tier across all categories
104
+ aigateway --quiet sb tools --tier price
90
105
  ```
91
106
 
92
- This is ideal for integration tests, configuration smoke checks, and "is everything ready?" probes.
107
+ The catalog is fetched live from the server each call (no local cache). Each model carries `price`, `priceUnit`, `tier`, and an optional `inputsOverride`; each category carries `defaultInputsSchema`.
108
+
109
+ ## Probing Without Cost
110
+
111
+ `sb invoke` performs client-side validation of `--model` and `--inputs` against the live catalog **before** any x402 round-trip. Invalid model ids and missing/invalid fields return `INVALID_MODEL_ID` / `MISSING_INPUTS` / `INVALID_INPUTS` locally, with zero USDT spent.
112
+
113
+ For wallet-only smoke tests, `aigateway wallet-balance` is read-only and never signs anything.
93
114
 
94
115
  ## Exit Code Strategy
95
116
 
@@ -99,7 +120,7 @@ Treat exit codes as a fast filter, then branch on `error.code` for nuance:
99
120
  switch (exitCode) {
100
121
  case 0: /* success */ break;
101
122
  case 1: /* user / config — surface to caller for correction */ break;
102
- case 2: /* timeout — safe to retry; card may still be provisioning */ break;
123
+ case 2: /* timeout — safe to retry the same command */ break;
103
124
  case 3: /* network / service — exponential backoff retry */ break;
104
125
  case 4: /* internal — log + fail loud */ break;
105
126
  }
@@ -33,8 +33,8 @@ Both modes use the same CLI; only the source of the session key (`EVM_PRIVATE_KE
33
33
  Your product walks the user through:
34
34
 
35
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
36
+ aigateway wallet-init # one-time, auto-creates local session key
37
+ aigateway wallet-topup --amount 5 # user scans QR to load 5 USDT + tiny BNB for first-time approve
38
38
  ```
39
39
 
40
40
  Subsequent paid calls are spawned by your product (agent / IDE plugin / app):
@@ -59,11 +59,18 @@ function runAigateway(args) {
59
59
  });
60
60
  }
61
61
 
62
- const { envelope } = await runAigateway(["create-card", "--amount", "5", "--poll"]);
62
+ const inputs = { prompt: "a cyberpunk fox", aspect_ratio: "1:1" };
63
+ const { envelope } = await runAigateway([
64
+ "sb", "invoke",
65
+ "--model", "replicate/black-forest-labs/flux-schnell",
66
+ "--inputs", JSON.stringify(inputs),
67
+ ]);
68
+
63
69
  if (envelope.ok) {
64
- showCard(envelope.data); // card number already redacted to "•••• 4242"
70
+ showImages(envelope.data.downloaded); // [{ localPath, format, width, height, sizeHuman, ... }]
65
71
  } else if (envelope.error.code === "TOPUP_REQUIRED") {
66
- promptUserToTopup(envelope.error.presets); // [5, 10, 20, 50]
72
+ // Headless / non-TTY ran out of USDT. Use envelope.error.presets and rerun.
73
+ promptUserToTopup(envelope.error.presets); // [5, 10, 20, 50]
67
74
  }
68
75
  ```
69
76
 
@@ -77,7 +84,10 @@ aigateway already supports `EVM_PRIVATE_KEY` — no config file needed:
77
84
 
78
85
  ```bash
79
86
  EVM_PRIVATE_KEY=0xYourMerchantSessionKey \
80
- aigateway create-card --amount 5 --app-id MERCHANT_ACME_001 --poll
87
+ aigateway sb invoke \
88
+ --model replicate/black-forest-labs/flux-schnell \
89
+ --inputs '{"prompt":"a cyberpunk fox"}' \
90
+ --app-id MERCHANT_ACME_001
81
91
  ```
82
92
 
83
93
  ### 3.2 Express backend example
@@ -107,23 +117,30 @@ function runCmd(args) {
107
117
  }
108
118
 
109
119
  // 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;
120
+ app.post("/api/generate-image", async (req, res) => {
121
+ const { userId, prompt } = req.body;
112
122
 
113
123
  // Your KYC / fraud / balance checks
114
- if (!await canIssueCard(userId, amount)) return res.status(403).end();
124
+ if (!await canSpend(userId)) return res.status(403).end();
115
125
 
116
- const { code, envelope } = await runCmd(["create-card", "--amount", String(amount), "--poll"]);
126
+ const { code, envelope } = await runCmd([
127
+ "sb", "invoke",
128
+ "--model", "replicate/black-forest-labs/flux-schnell",
129
+ "--inputs", JSON.stringify({ prompt }),
130
+ ]);
117
131
 
118
132
  if (envelope.ok) {
119
- await db.cards.insert({
133
+ const [first] = envelope.data.downloaded;
134
+ await db.generations.insert({
120
135
  userId,
121
- orderNo: envelope.data.orderNo,
122
- cardNumber: envelope.data.data.model.cardNumber,
123
- amount,
124
- issuedAt: new Date(),
136
+ model: envelope.data.model,
137
+ transaction: envelope.data.transaction,
138
+ localPath: first?.localPath,
139
+ url: first?.url,
140
+ charged: envelope.data.balance.charged,
141
+ createdAt: new Date(),
125
142
  });
126
- res.json({ ok: true, orderNo: envelope.data.orderNo });
143
+ res.json({ ok: true, transaction: envelope.data.transaction, file: first?.localPath });
127
144
  } else {
128
145
  await logErr(userId, envelope.error);
129
146
  res.status(500).json({ error: envelope.error.code, message: envelope.error.message });
@@ -142,7 +159,7 @@ EVM_PRIVATE_KEY=<your-merchant-session-key> \
142
159
  aigateway wallet-topup --amount 50 --app-id MERCHANT_ACME_001
143
160
  ```
144
161
 
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**.
162
+ Scan QR with your treasury wallet, confirm two transactions (USDT + a small amount of BNB for the one-time `approve`). After this, the production server can issue `sb invoke` calls **without ever opening WalletConnect** until USDT is depleted.
146
163
 
147
164
  ### 3.4 Low-balance alerting
148
165
 
@@ -168,18 +185,29 @@ function handleEnvelope(envelope, exitCode) {
168
185
 
169
186
  case "INSUFFICIENT_USDT":
170
187
  case "TOPUP_REQUIRED":
171
- await yourTopupFlow();
188
+ await yourTopupFlow(envelope.error.presets); // [5, 10, 20, 50]
189
+ return { ok: false, retry: true };
190
+
191
+ case "INSUFFICIENT_BNB":
192
+ await yourGasTopupFlow(); // aigateway wallet-gas
172
193
  return { ok: false, retry: true };
173
194
 
174
- case "AMOUNT_OUT_OF_RANGE":
175
- return { ok: false, userMessage: `Amount must be $${envelope.error.min}~${envelope.error.max}` };
195
+ case "MISSING_MODEL":
196
+ case "INVALID_MODEL_ID":
197
+ return { ok: false, userMessage: "Pick a model first (run `aigateway sb tools`)" };
198
+
199
+ case "MISSING_INPUTS":
200
+ case "INVALID_INPUTS":
201
+ case "INVALID_INPUTS_JSON":
202
+ // envelope.error.errors[] = [{ field, kind, message }], plus required[] / properties[]
203
+ return { ok: false, validation: envelope.error };
176
204
 
177
- case "POLL_TIMEOUT":
178
- await scheduleStatusCheck(envelope.error.orderNo);
179
- return { ok: false, asyncPending: true };
205
+ case "MODEL_PRICING_NOT_CONFIGURED":
206
+ return { ok: false, userMessage: "This model is not yet priced; pick another" };
180
207
 
181
208
  case "SERVICE_UNAVAILABLE":
182
209
  case "PAYMENT_FETCH_FAILED":
210
+ case "CATALOG_FETCH_FAILED":
183
211
  case "BALANCE_CHECK_FAILED":
184
212
  return { ok: false, retryWithBackoff: true };
185
213
 
@@ -187,6 +215,10 @@ function handleEnvelope(envelope, exitCode) {
187
215
  case "PAYMENT_TIMEOUT":
188
216
  return { ok: false, userMessage: "Payment not confirmed" };
189
217
 
218
+ case "DOWNLOAD_FAILED":
219
+ // Generation succeeded; original URL still in envelope.error.url (if surfaced) or downloaded[].url
220
+ return { ok: false, partial: true };
221
+
190
222
  default:
191
223
  throw new UnexpectedError(envelope.error);
192
224
  }
@@ -197,23 +229,20 @@ Full code table: [exit-codes.md](../exit-codes.md). Concrete recovery actions pe
197
229
 
198
230
  ## 5. Sandbox / test environments
199
231
 
200
- ### Dry-run (no real USDT)
232
+ ### Validation-only probes
201
233
 
202
- ```bash
203
- aigateway create-card --amount 5 --dry-run --app-id TEST000001
204
- # envelope.data.dryRun = true; preflight passes, nothing signed
205
- ```
234
+ `sb invoke` performs client-side schema validation against the live catalog **before** any payment round-trip — so an invalid `--model` or malformed `--inputs` fails locally with zero USDT spent. Use this to validate your subprocess wrapper + envelope parser in CI without burning real budget.
206
235
 
207
236
  ### Staging service URL
208
237
 
209
238
  ```bash
210
239
  AIGATEWAY_SERVICE_URL=https://staging-x402.aeon.xyz \
211
- aigateway create-card --amount 5 --app-id YOUR_TEST_APPID --poll
240
+ aigateway sb invoke --model <id> --inputs '<json>' --app-id YOUR_TEST_APPID
212
241
  ```
213
242
 
214
243
  ### CI
215
244
 
216
- Never invoke real paid commands in CI. Use `--dry-run` to validate your subprocess wrapper + envelope parser only.
245
+ Never invoke real paid commands in CI. Wrap your test fixtures around invalid-input probes (so the call short-circuits with `MISSING_INPUTS` / `INVALID_MODEL_ID`) or use `wallet-balance` (read-only).
217
246
 
218
247
  ## 6. Operational checklist for merchants
219
248
 
@@ -224,15 +253,15 @@ Never invoke real paid commands in CI. Use `--dry-run` to validate your subproce
224
253
  | Top-up (manual, workstation) | When USDT is low | `wallet-topup --amount <n>` |
225
254
  | Withdraw to merchant treasury | Monthly / quarterly | `wallet-withdraw --to <treasury>` |
226
255
  | 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 |
256
+ | Reconciliation | Per settlement | Use `envelope.data.transaction` / `paymentResponse.txHash` to match on-chain + backend records |
228
257
 
229
258
  ## 7. Security checklist
230
259
 
231
260
  - **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
261
  - **`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
262
  - **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
263
  - **`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.
264
+ - **Generated artifacts**: `sb invoke` saves binary outputs (images / videos / audio) under `~/aigateway-{images,videos,audio}/` by default. In custodial deployments, override with `--output <dir>` and treat the contents as user data (retention, access control).
236
265
 
237
266
  ## 8. See also
238
267
 
@@ -58,11 +58,11 @@ If you're in a custodial / server context, inject the key via env var instead:
58
58
  EVM_PRIVATE_KEY=0x... aigateway wallet-balance
59
59
  ```
60
60
 
61
- ### QR window doesn't open during `wallet-topup`
61
+ ### QR window doesn't open during `wallet-topup` / `wallet-gas`
62
62
 
63
63
  The CLI tries `open` (macOS) / `start` (Windows) / `xdg-open` (Linux). In headless environments, it prints the file path. Open `file:///tmp/aigateway-qr.html` manually in any browser. If you're on a remote VM, port-forward `127.0.0.1:<status-port>` (printed in stderr) and open the file locally instead.
64
64
 
65
- **Headless servers should never run `wallet-topup` interactively** — see [merchant-integration.md § 3.3](./recipes/merchant-integration.md) for the workstation pre-funding pattern.
65
+ **Headless servers should never run `wallet-topup` / `wallet-gas` interactively** — see [merchant-integration.md § 3.3](./recipes/merchant-integration.md) for the workstation pre-funding pattern.
66
66
 
67
67
  ### `error.code: PAYMENT_TIMEOUT`
68
68
 
@@ -76,12 +76,13 @@ You (or the user) tapped "Reject" in the wallet app. Same as above — re-run on
76
76
 
77
77
  Likely causes:
78
78
  1. The `wallet-topup` you ran was on a **different** session key. Verify `aigateway wallet-balance` shows the same address you funded.
79
- 2. The on-chain tx hasn't confirmed yet. BSC normally confirms within 3 seconds; if longer, check `~/.aigateway/update.log` and the explorer.
80
- 3. You funded in the wrong network (BSC vs. ETH). aigateway only reads USDT on **BSC** (chain id 56, contract `0x55d3...955`).
79
+ 2. The on-chain tx hasn't confirmed yet. BSC normally confirms within 3 seconds; if longer, check the explorer.
80
+ 3. You funded in the wrong network (BSC vs. ETH). aigateway only reads USDT on **BSC** (chain id 56).
81
+ 4. The call's required USDT exceeded your topped-up amount. `error.required` shows what the model needed; rerun `sb invoke` with a larger `--topup-amount`, or run `wallet-topup --amount <n>` separately.
81
82
 
82
83
  ### `error.code: INSUFFICIENT_BNB`
83
84
 
84
- `wallet-withdraw` and the one-time approve in `wallet-topup` are direct on-chain transactions and need BNB for gas. Run:
85
+ `wallet-withdraw` and the one-time approve inside `wallet-topup` are direct on-chain transactions and need BNB for gas. Run:
85
86
 
86
87
  ```bash
87
88
  aigateway wallet-gas --amount 0.001
@@ -104,6 +105,48 @@ sleep 3 && aigateway wallet-balance
104
105
 
105
106
  If reproducible, file an issue.
106
107
 
108
+ ## `sb invoke` / `sb tools`
109
+
110
+ ### `error.code: MISSING_MODEL` / `INVALID_MODEL_ID`
111
+
112
+ You didn't pass `--model`, or the id you passed isn't in the live catalog. Run:
113
+
114
+ ```bash
115
+ aigateway sb tools # list everything
116
+ aigateway sb tools --category image # narrow to a category
117
+ aigateway sb tools --model <id> # confirm a specific id + see its effectiveSchema
118
+ ```
119
+
120
+ Never hard-code model ids in long-lived prompts — vendors rename, the gateway catalog is the source of truth.
121
+
122
+ ### `error.code: MISSING_INPUTS` / `INVALID_INPUTS`
123
+
124
+ `sb invoke` validates `--inputs` against the live catalog **before** any payment round-trip. Inspect `error.errors[]` — each item carries `{ field, kind, message }`, with `kind ∈ {missing, enum, type, range}`. `error.required[]` lists every required field for the chosen model.
125
+
126
+ Common cases:
127
+ - `video` model without `inputs.duration_seconds` (1–300).
128
+ - `stt` model without `inputs.duration_minutes` (1–360).
129
+ - `tts` model without `inputs.text`.
130
+ - Image `aspect_ratio` not in the enum (e.g. `"square"` instead of `"1:1"`).
131
+
132
+ Re-pull the schema with `aigateway sb tools --model <id>` and fix the JSON.
133
+
134
+ ### `error.code: INVALID_INPUTS_JSON`
135
+
136
+ Your `--inputs` string couldn't be parsed. From a shell, use `JSON.stringify(...)` in your wrapper (Node.js / Python) and pass the result with single quotes around the whole argument. Alternatively pass `--inputs @path/to/inputs.json` to read from a file.
137
+
138
+ ### `error.code: MODEL_PRICING_NOT_CONFIGURED`
139
+
140
+ The catalog lists the model but the gateway has no price entry for it yet (operator-side gap). Pick another model from the same category; mention it to the AEON team if you specifically need that one.
141
+
142
+ ### `error.code: DOWNLOAD_FAILED` / `IMAGE_DOWNLOAD_FAILED`
143
+
144
+ The paid call succeeded — money was charged — but the local file save failed (URL 404, timeout, disk full). The URL is still available in `data.downloaded[].url`. Re-fetch the URL directly, or rerun the same command with `--raw` to skip auto-download and inspect `raw`. **Do not re-invoke the model** unless you're prepared to pay again.
145
+
146
+ ### `error.code: CATALOG_FETCH_FAILED`
147
+
148
+ `sb tools` couldn't reach the server. `sb invoke` will still run (server-side validation is the safety net), it'll just skip the local pre-validation step. Retry once; if it persists, check network connectivity to the configured `AIGATEWAY_SERVICE_URL`.
149
+
107
150
  ## Skill / Agent integration
108
151
 
109
152
  ### Skill not picked up by Cursor / Windsurf / Cline / Codex
@@ -122,18 +165,22 @@ The skill version is locked to `metadata.version` in the frontmatter. If you edi
122
165
 
123
166
  ```yaml
124
167
  metadata:
125
- version: "0.1.1" # was 0.1.0
168
+ version: "0.2.5" # was 0.2.5
126
169
  ```
127
170
 
128
171
  Then `npx skills add AEON-Project/aigateway -g -y -f` (force) or re-run postinstall.
129
172
 
130
173
  ## Auto-upgrade (`update-check.mjs`)
131
174
 
175
+ ### `error.code: UPDATE_APPLIED`
176
+
177
+ The CLI detected a newer published version, upgraded itself in-place, and **did not execute your command**. The envelope carries `error.from` / `error.to`. Rerun the same command verbatim on the new version. Do not treat this as a generic timeout.
178
+
132
179
  ### Auto-upgrade doesn't seem to run
133
180
 
134
- Check `~/.aigateway/update.log`. The upgrade is a detached background process and may take 1–2 minutes after the CLI is invoked. If the log shows a failure, the most common reasons are:
181
+ Check `~/.aigateway/update.log`. The upgrade runs synchronously at startup; if the log shows a failure, the most common reasons are:
135
182
 
136
- - Your global npm registry needs auth and the background process can't prompt.
183
+ - Your global npm registry needs auth and the foreground process can't prompt.
137
184
  - The user lacks permission to write to the global node_modules (run with sudo, or use nvm).
138
185
 
139
186
  You can force-upgrade manually:
@@ -144,10 +191,10 @@ npm install -g @aeon-ai-pay/aigateway@latest
144
191
 
145
192
  ### Block the auto-upgrade
146
193
 
147
- Currently no flag for this — but the upgrade is **non-blocking** and won't affect the current command. If you need a deterministic version (e.g. in CI), pin the install:
194
+ Currently no flag for this. If you need a deterministic version (e.g. in CI), pin the install:
148
195
 
149
196
  ```bash
150
- npm install -g @aeon-ai-pay/aigateway@0.1.0
197
+ npm install -g @aeon-ai-pay/aigateway@<version>
151
198
  ```
152
199
 
153
200
  The check still runs but the install is idempotent at the same version.
@@ -167,20 +214,21 @@ Causes:
167
214
 
168
215
  ### Spawning hangs forever
169
216
 
170
- If you spawn `wallet-topup` / `create-card` / `create-image` in a context where the QR window can't be shown (CI, server, headless), the CLI waits 5 minutes for the WalletConnect signature, then exits with `PAYMENT_TIMEOUT`. **Never spawn these without pre-funded session keys** — see [merchant-integration.md § 3](./recipes/merchant-integration.md).
217
+ If you spawn `wallet-topup` / `wallet-gas` (or `sb invoke` against an empty wallet) in a context where the QR window can't be shown (CI, server, headless), the CLI waits 5 minutes for the WalletConnect signature, then exits with `PAYMENT_TIMEOUT`. **Never spawn these without a pre-funded session key** — see [merchant-integration.md § 3](./recipes/merchant-integration.md).
171
218
 
172
219
  ## Service / network
173
220
 
174
221
  ### `error.code: SERVICE_UNAVAILABLE`
175
222
 
176
- Upstream is degraded. Retry with exponential backoff (1s → 4s → 16s, max 3 attempts). If it persists for more than 5 minutes, contact AEON support with the `appId` and an example `orderNo`.
223
+ Upstream is degraded. Retry with exponential backoff (1s → 4s → 16s, max 3 attempts). If it persists for more than 5 minutes, contact AEON support with the `appId` and an example `transaction` hash.
177
224
 
178
225
  ### Staging vs. production
179
226
 
180
227
  The default service URL is `https://ai-api.aeon.xyz` (production). To use a staging endpoint:
181
228
 
182
229
  ```bash
183
- AIGATEWAY_SERVICE_URL=https://staging-x402.aeon.xyz aigateway create-card --amount 5
230
+ AIGATEWAY_SERVICE_URL=https://staging-x402.aeon.xyz \
231
+ aigateway sb tools --category image
184
232
  ```
185
233
 
186
234
  ## Getting more diagnostics
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aeon-ai-pay/aigateway",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "AI Agents discover, invoke, and settle paid LLMs, APIs, and Skills — starting with Skill Boss. No manual key setup. No prepayment. Pay-per-call via x402 or Agent Card.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,11 +19,11 @@
19
19
  ],
20
20
  "scripts": {
21
21
  "postinstall": "node scripts/postinstall.mjs",
22
- "create-card": "node bin/cli.mjs create-card",
23
- "create-image": "node bin/cli.mjs create-image",
24
22
  "wallet-init": "node bin/cli.mjs wallet-init",
25
23
  "wallet-topup": "node bin/cli.mjs wallet-topup",
26
24
  "wallet-balance": "node bin/cli.mjs wallet-balance",
25
+ "sb-tools": "node bin/cli.mjs sb tools",
26
+ "sb-invoke": "node bin/cli.mjs sb invoke",
27
27
  "release": "node scripts/release.mjs"
28
28
  },
29
29
  "dependencies": {
@@ -43,8 +43,14 @@
43
43
  "aigateway",
44
44
  "agent-skills",
45
45
  "x402",
46
- "virtual-card",
47
- "ai-image",
46
+ "ai-tools",
47
+ "image-generation",
48
+ "video-generation",
49
+ "text-to-speech",
50
+ "speech-to-text",
51
+ "web-search",
52
+ "web-scraping",
53
+ "embeddings",
48
54
  "skill-boss",
49
55
  "crypto-payment",
50
56
  "evm",
@@ -26,7 +26,7 @@ description: >
26
26
  emoji: "🛰️"
27
27
  homepage: https://github.com/AEON-Project/aigateway
28
28
  metadata:
29
- version: "0.2.3"
29
+ version: "0.2.5"
30
30
  author: AEON-Project
31
31
  openclaw:
32
32
  requires:
@@ -256,7 +256,10 @@ Approve: {approveTx truncated or "already approved"}
256
256
 
257
257
  ⭐ **默认模式**:**AI 不擅自选 model**,而是把候选 + 预估总价列给用户,让用户拍板。**推荐默认选最便宜的**(按 `tier: "price"` 优先排序)。
258
258
 
259
- **例外**:用户原话已经指定了 model(`"用 flux-2-max 画"`) → 直接用,跳过列表。
259
+ **跳过候选展示的场景**(任一命中即跳过):
260
+
261
+ 1. 用户原话已经指定了 model(`"用 flux-2-max 画"`) → 直接用
262
+ 2. **任务匹配后候选只剩 1 个 model** → 直接用该 model,不要再渲染单行"列表"和"输入序号"引导。若 `priceUnit` 需要用量字段(`per_second` / `per_minute`),只问用量;其它直接调用。用一行 `✨ 选用 {model_id}(${unitPrice}{unit-cn} × {quantity} = ${total} USDT),开始生成…` 替代候选表格。
260
263
 
261
264
  #### Step A: 看 `priceUnit` 决定是否前置询问用量
262
265
 
@@ -27,6 +27,7 @@ import { emitOk, emitErr, logInfo } from "../output.mjs";
27
27
  import { extractOutputs, resolveOutputDir, downloadOutputs } from "../tools-download.mjs";
28
28
  import { fetchCatalog, findModel } from "../catalog.mjs";
29
29
  import { validateInputs } from "../inputs-validator.mjs";
30
+ import { CAMPAIGN_TOKEN_ADDRESS } from "../constants.mjs";
30
31
 
31
32
  /**
32
33
  * Parse `--inputs` value: either a JSON literal or `@path/to/file.json`.
@@ -151,10 +152,19 @@ export async function invoke(opts) {
151
152
  logInfo("Fetching payment requirements...");
152
153
  let paymentReq;
153
154
  let requiredUsdt;
155
+ let paymentMethod = "USDT"; // "USDT" | "COUPON"
154
156
  try {
155
157
  paymentReq = await fetchPaymentRequirements(url);
156
158
  requiredUsdt = paymentReq.amountUsdt;
157
- logInfo(`Required: ${requiredUsdt} USDT (pay to ${paymentReq.payTo})`);
159
+ // 服务端在 402 响应里的 asset 字段如果等于活动 token 合约 → 本次走优惠券支付
160
+ if (paymentReq.asset && paymentReq.asset.toLowerCase() === CAMPAIGN_TOKEN_ADDRESS.toLowerCase()) {
161
+ paymentMethod = "COUPON";
162
+ }
163
+ if (paymentMethod === "COUPON") {
164
+ logInfo(`💳 Paid with coupon: ${requiredUsdt} (token ${paymentReq.asset})`);
165
+ } else {
166
+ logInfo(`Required: ${requiredUsdt} USDT (pay to ${paymentReq.payTo})`);
167
+ }
158
168
  } catch (e) {
159
169
  // Server may return HTTP 400 with structured { code, msg } for pricing / body errors.
160
170
  // Surface that code as-is so the agent can react (e.g. MODEL_PRICING_NOT_CONFIGURED).
@@ -382,6 +392,8 @@ export async function invoke(opts) {
382
392
  // unwrap server envelope: { payer, transaction, data: <upstream-response> } → <upstream-response>
383
393
  raw: response.data?.data ?? response.data,
384
394
  paymentResponse,
395
+ paymentMethod, // "USDT" | "COUPON" — agent / CLI 可据此显示扣款方式
396
+ paymentToken: paymentReq.asset,
385
397
  balance: {
386
398
  initial: balanceInitialUsdt,
387
399
  before: balanceBeforeChargeUsdt,
@@ -12,7 +12,7 @@
12
12
  * - data.needsTopup=true → wallet-topup must run first (the envelope includes presets / minTopup / reason)
13
13
  * - data.needsTopup=false → can proceed directly to sb invoke
14
14
  */
15
- import { loadConfig, saveConfig } from "../config.mjs";
15
+ import { loadConfig, saveConfig, getOrCreateDeviceId, resolve } from "../config.mjs";
16
16
  import { getWalletBalance, getAllowance } from "../balance.mjs";
17
17
  import {
18
18
  LOW_BALANCE_THRESHOLD,
@@ -20,6 +20,7 @@ import {
20
20
  TOPUP_PRESETS,
21
21
  } from "../funding.mjs";
22
22
  import { emitOk, logInfo } from "../output.mjs";
23
+ import { checkCouponStatus, claimCoupon } from "../coupon.mjs";
23
24
 
24
25
  export async function initWallet(opts) {
25
26
  const config = loadConfig();
@@ -90,12 +91,65 @@ export async function initWallet(opts) {
90
91
  topupReason = "no_approve";
91
92
  }
92
93
 
94
+ // device id 在首次 init 时生成,后续 claim / 审计复用
95
+ const deviceId = getOrCreateDeviceId();
96
+
97
+ // AEON x BNB Chain AI Agent Campaign — 两步同步流程:
98
+ // 1) status check:已领 → 跳过,直接走后续流程
99
+ // 2) 未领 → 输出"申请中..." → claim 同步阻塞 → 输出 mint 结果
100
+ // 任何网络异常都安静 log,不阻塞 wallet-init 主流程。
101
+ let coupon = null;
102
+ const serviceUrl = resolve(opts.serviceUrl, "AIGATEWAY_SERVICE_URL", "serviceUrl");
103
+ if (serviceUrl && config.address) {
104
+ const status = await checkCouponStatus({ serviceUrl, userAddress: config.address });
105
+ if (status.ok && status.claimed) {
106
+ // 已领过 → 直接走后续流程
107
+ logInfo(`Coupon already claimed (status=${status.mintStatus}${status.mintTxHash ? ", tx=" + status.mintTxHash : ""}).`);
108
+ coupon = {
109
+ ok: status.mintStatus === "SUCCESS",
110
+ code: status.mintStatus === "SUCCESS" ? "ALREADY_CLAIMED_SUCCESS" : "ALREADY_CLAIMED_" + status.mintStatus,
111
+ tokenAddress: status.tokenAddress,
112
+ tokenAmount: status.tokenAmount,
113
+ txHash: status.mintTxHash,
114
+ campaignId: status.campaignId,
115
+ };
116
+ } else if (status.ok && !status.claimed) {
117
+ // 未领 → 申请,同步阻塞
118
+ logInfo("🎁 Claiming AEON x BNB campaign coupon, please wait...");
119
+ coupon = await claimCoupon({
120
+ serviceUrl,
121
+ userAddress: config.address,
122
+ deviceId,
123
+ appId,
124
+ });
125
+ if (coupon?.ok) {
126
+ logInfo(`✅ Coupon claimed: ${coupon.tokenAmount} credits (tx: ${coupon.txHash})`);
127
+ } else if (coupon?.code === "ALREADY_CLAIMED") {
128
+ // status 和 claim 之间有人抢着领过(罕见 race),按已领处理
129
+ logInfo("Coupon already claimed (race with status check).");
130
+ } else if (coupon?.code === "CAMPAIGN_QUOTA_EXHAUSTED") {
131
+ logInfo("⚠️ Coupon campaign quota exhausted.");
132
+ } else if (coupon?.code === "MINT_FAILED") {
133
+ logInfo(`❌ Coupon mint failed: ${coupon.errorMsg}`);
134
+ } else if (coupon?.code === "CLAIM_NETWORK_ERROR") {
135
+ logInfo(`Coupon claim network error: ${coupon.errorMsg}`);
136
+ } else if (coupon?.code) {
137
+ logInfo(`Coupon claim skipped: ${coupon.code} — ${coupon.errorMsg || ""}`);
138
+ }
139
+ } else {
140
+ // status check 自己网络失败 — 静默,不阻塞 wallet-init
141
+ logInfo(`Coupon status check unreachable: ${status.errorMsg}`);
142
+ coupon = { ok: false, code: status.code, errorMsg: status.errorMsg };
143
+ }
144
+ }
145
+
93
146
  const data = {
94
147
  ready: true,
95
148
  created,
96
149
  appId,
97
150
  mode: config.mode || null,
98
151
  address: config.address || null,
152
+ deviceId,
99
153
  mainWallet: config.mainWallet || null,
100
154
  serviceUrl: config.serviceUrl || null,
101
155
  usdt,
@@ -106,6 +160,7 @@ export async function initWallet(opts) {
106
160
  minTopup: MIN_TOPUP_USDT,
107
161
  presets: TOPUP_PRESETS,
108
162
  chainCheck: chainCheckOk ? "ok" : { error: chainCheckError },
163
+ coupon, // null(未尝试) | { ok, code, tokenAmount?, txHash?, errorMsg? }
109
164
  };
110
165
  emitOk("wallet-init", data, data);
111
166
  }
package/src/config.mjs CHANGED
@@ -5,6 +5,7 @@
5
5
  * AEON AI Gateway uses a single x402 service (ai-api.aeon.xyz).
6
6
  */
7
7
  import {readFileSync, writeFileSync, mkdirSync, chmodSync} from "fs";
8
+ import {randomUUID} from "crypto";
8
9
  import {join} from "path";
9
10
  import {homedir} from "os";
10
11
 
@@ -24,6 +25,18 @@ export function loadConfig() {
24
25
  }
25
26
  }
26
27
 
28
+ /**
29
+ * 读取或生成 deviceId(持久化到 config.json 复用)。
30
+ * 用于活动优惠券防刷审计 + 后端识别同设备多钱包。
31
+ */
32
+ export function getOrCreateDeviceId() {
33
+ const cfg = loadConfig();
34
+ if (cfg.deviceId) return cfg.deviceId;
35
+ const deviceId = randomUUID();
36
+ saveConfig({...cfg, deviceId});
37
+ return deviceId;
38
+ }
39
+
27
40
  export function saveConfig(config) {
28
41
  mkdirSync(CONFIG_DIR, {recursive: true});
29
42
  writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), {mode: 0o600});