@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.
- package/README.md +20 -0
- package/docs/env-vars.md +12 -14
- package/docs/exit-codes.md +25 -15
- package/docs/ide-setup.md +2 -2
- package/docs/output-schema.md +73 -58
- package/docs/recipes/cron-issue-cards.md +34 -16
- package/docs/recipes/error-recovery.md +47 -16
- package/docs/recipes/integrate-in-agent.md +37 -16
- package/docs/recipes/merchant-integration.md +61 -32
- package/docs/troubleshooting.md +61 -13
- package/package.json +11 -5
- package/skills/aigateway/SKILL.md +5 -2
- package/src/commands/sb-invoke.mjs +13 -1
- package/src/commands/wallet-init.mjs +56 -1
- package/src/config.mjs +13 -0
- package/src/constants.mjs +3 -0
- package/src/coupon.mjs +108 -0
- package/templates/cline/.clinerules +36 -18
- package/templates/codex/AGENTS.md +48 -16
- package/templates/cursor/.cursor/rules/aigateway.mdc +27 -14
- package/templates/windsurf/.windsurfrules +24 -13
|
@@ -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
|
-
> ⚠️
|
|
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
|
|
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:
|
|
37
|
-
const { envelope, exitCode } = await
|
|
37
|
+
// Example: invoke an AI tool (image generation) via the x402 paid entry point
|
|
38
|
+
const { envelope, exitCode } = await runAigateway([
|
|
38
39
|
"--quiet",
|
|
39
|
-
"
|
|
40
|
-
"--
|
|
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 {
|
|
47
|
-
console.log(
|
|
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([
|
|
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
|
-
|
|
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
|
-
##
|
|
92
|
+
## Discovering Models — `sb tools`
|
|
85
93
|
|
|
86
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
70
|
+
showImages(envelope.data.downloaded); // [{ localPath, format, width, height, sizeHuman, ... }]
|
|
65
71
|
} else if (envelope.error.code === "TOPUP_REQUIRED") {
|
|
66
|
-
|
|
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
|
|
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/
|
|
111
|
-
const { userId,
|
|
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
|
|
124
|
+
if (!await canSpend(userId)) return res.status(403).end();
|
|
115
125
|
|
|
116
|
-
const { code, envelope } = await runCmd([
|
|
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
|
-
|
|
133
|
+
const [first] = envelope.data.downloaded;
|
|
134
|
+
await db.generations.insert({
|
|
120
135
|
userId,
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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,
|
|
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 +
|
|
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 "
|
|
175
|
-
|
|
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 "
|
|
178
|
-
|
|
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
|
-
###
|
|
232
|
+
### Validation-only probes
|
|
201
233
|
|
|
202
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
package/docs/troubleshooting.md
CHANGED
|
@@ -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
|
|
80
|
-
3. You funded in the wrong network (BSC vs. ETH). aigateway only reads USDT on **BSC** (chain id 56
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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` / `
|
|
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 `
|
|
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
|
|
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
|
+
"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
|
-
"
|
|
47
|
-
"
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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});
|