@aeon-ai-pay/aigateway 0.1.4 → 0.1.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/CHANGELOG.md CHANGED
@@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.1.5] — 2026-05-19
11
+
12
+ ### Changed
13
+ - **Removed all Chinese-language content from source files**. All comments,
14
+ docstrings, log messages, and SKILL.md guidance are now English-only.
15
+ `normalizeWalletError` no longer matches CJK error strings from localised
16
+ wallet apps; those fall through to the generic `WALLET_ERROR` code.
17
+ - SKILL.md's "Wording Discipline" section is now language-neutral: instead
18
+ of listing Chinese phrasings, it instructs translators to keep the "wallet
19
+ top-up" verb / noun lexically distinct from the "card face value" verb /
20
+ noun in every target language.
21
+
22
+ ### Removed
23
+ - `test/create-logic.test.mjs` — stale unit tests inherited from `aicard`
24
+ that referenced internal functions removed during the merge (e.g.
25
+ `inlineWalletConnectTopup`, now replaced by `funding.mjs::fundSessionKey`).
26
+ The file was never published to npm (`test/` is not in `files`).
27
+
10
28
  ## [0.1.4] — 2026-05-19
11
29
 
12
30
  ### Added
@@ -44,14 +62,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
44
62
  ## [0.1.2] — 2026-05-19
45
63
 
46
64
  ### Changed
47
- - **SKILL.md wording discipline**: `wallet-topup` ask and `create-card` ask now have
48
- enforced lexical separation. Step 2 (`wallet-topup`) explicitly says "load USDT
49
- **into your session wallet**" / "往**本地钱包**充值 USDT". Step 3a (`create-card`)
50
- says "card **face value**" / "**开多少美元的卡** / 卡的面额", and explicitly
51
- forbids translating it as "充值". Fixes a UX confusion where a user with a
52
- funded wallet would be asked "请问您想充值多少美元到虚拟卡上?" and reasonably
53
- wonder if it was another wallet top-up prompt.
54
- - New "Wording Discipline" section in SKILL.md `## Copy Constraints` to lock this in.
65
+ - **SKILL.md wording discipline**: the `wallet-topup` and `create-card` prompts
66
+ now enforce strict lexical separation. Step 2 (`wallet-topup`) says "load USDT
67
+ **into your session wallet**"; Step 3a (`create-card`) says "card
68
+ **face value**" / "issue a card with how much". When translating to any
69
+ non-English language, the verb / noun for each concept must remain distinct
70
+ so users cannot conflate them.
71
+ - New "Wording Discipline" section in SKILL.md `## Copy Constraints` to lock
72
+ this in.
55
73
 
56
74
  ## [0.1.1] — 2026-05-19
57
75
 
package/bin/cli.mjs CHANGED
@@ -7,8 +7,9 @@ if (major < 25) {
7
7
  process.exit(1);
8
8
  }
9
9
 
10
- // WalletConnect v2 SDK 已知缺陷:relay 偶发 null WebSocket 帧导致
11
- // isJsonRpcPayload 内部 'id' in null TypeError,不影响业务流程,静默忽略
10
+ // Known WalletConnect v2 SDK quirk: the relay occasionally emits null WebSocket frames,
11
+ // causing `'id' in null` inside isJsonRpcPayload to throw a TypeError. It does not
12
+ // affect business logic, so silently ignore it.
12
13
  process.on("uncaughtException", (err) => {
13
14
  if (
14
15
  err instanceof TypeError &&
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aeon-ai-pay/aigateway",
3
- "version": "0.1.4",
3
+ "version": "0.1.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": {
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * npm install -g 后自动安装 skill 到所有已检测的 AI 编码工具
4
+ * After `npm install -g`, automatically install the skill into every detected AI coding tool.
5
5
  *
6
- * 优先使用 `npx skills add` (Vercel Labs) 统一安装到所有工具
7
- * 失败时 fallback 到手动复制 Claude Code skills 目录
6
+ * Prefer `npx skills add` (Vercel Labs) so the skill is registered across all tools at once.
7
+ * If that fails, fall back to manually copying the skill into the Claude Code skills directory.
8
8
  */
9
9
 
10
10
  import { cpSync, existsSync, mkdirSync } from 'node:fs';
@@ -20,7 +20,7 @@ if (!existsSync(skillSrc)) {
20
20
  process.exit(0);
21
21
  }
22
22
 
23
- // 尝试用 skills CLI 安装到所有工具
23
+ // Try installing into every tool via the skills CLI
24
24
  try {
25
25
  execFileSync('npx', ['skills', 'add', skillSrc, '-g', '-y', '--copy'], {
26
26
  stdio: 'inherit',
@@ -30,10 +30,10 @@ try {
30
30
  console.log('✔ aigateway skill installed via skills CLI (all detected tools)');
31
31
  process.exit(0);
32
32
  } catch {
33
- // skills CLI 不可用或失败,fallback
33
+ // skills CLI unavailable or failed — fall back
34
34
  }
35
35
 
36
- // Fallback: 手动复制到 Claude Code
36
+ // Fallback: copy manually into Claude Code
37
37
  const dest = join(homedir(), '.claude', 'skills', 'aigateway');
38
38
  mkdirSync(dirname(dest), { recursive: true });
39
39
  cpSync(skillSrc, dest, { recursive: true, force: true });
@@ -16,7 +16,7 @@ description: >
16
16
  emoji: "🛰️"
17
17
  homepage: https://github.com/AEON-Project/aigateway
18
18
  metadata:
19
- version: "0.1.4"
19
+ version: "0.1.5"
20
20
  author: AEON-Project
21
21
  openclaw:
22
22
  requires:
@@ -163,12 +163,10 @@ Trigger: `wallet-init` envelope reports `needsTopup: true` (any of `no_prior_fun
163
163
  This is a **session-wallet top-up** in USDT (NOT card face value). Make the wording unambiguously about the wallet to avoid the user confusing it with the card face value asked in Step 3a.
164
164
 
165
165
  - Presets: **5 / 10 / 20 / 50 USDT**. Custom amounts must be ≥ 5 USDT.
166
- - Ask the user **before** running the command. **Make "wallet" / "钱包" explicit in the question:**
166
+ - Ask the user **before** running the command. **Make the noun "wallet" explicit in the question** (when translating to other languages, the translated noun must also unambiguously mean *session wallet*, never *card*):
167
167
 
168
168
  > How much USDT would you like to load **into your session wallet**? (presets: 5 / 10 / 20 / 50, or any custom amount ≥ 5)
169
169
 
170
- (Suggested Chinese phrasing: "请问要往**本地钱包**充值多少 USDT?预设 5 / 10 / 20 / 50,或自定义 ≥ 5。")
171
-
172
170
  - Once the user picks an amount, run:
173
171
 
174
172
  ```bash
@@ -211,13 +209,10 @@ Trigger: user wants to **buy / create / get a virtual card** *and* Step 1 envelo
211
209
 
212
210
  ### Amount confirmation
213
211
 
214
- This is the **card face value** the user wants to issue (NOT a wallet top-up). Amount must be in `amountLimits.min ~ amountLimits.max` (from Step 1; never hardcode). If user did not specify, ask once — **use the word "card face value" / "面额", never "充值" / "top up"** to avoid confusing it with the `wallet-topup` step:
212
+ This is the **card face value** the user wants to issue (NOT a wallet top-up). Amount must be in `amountLimits.min ~ amountLimits.max` (from Step 1; never hardcode). If the user did not specify a value, ask once — use the phrase **"card face value"** (or in any non-English language, the equivalent for *the amount loaded onto a new card*), never the verb used for *wallet top-up*:
215
213
 
216
214
  > What card face value would you like to issue? Allowed range: ${min}~${max} USD.
217
215
 
218
- (Suggested Chinese phrasing: "请问要开多少美元的卡?允许范围 $${min} ~ $${max}。"
219
- **Do not** translate this as "充值多少美元" — that wording belongs to `wallet-topup` and confuses users.)
220
-
221
216
  Once specified, **execute immediately**.
222
217
 
223
218
  ### Execute
@@ -460,11 +455,11 @@ Address rendering: always `0x0...{last4}` (first 3 + ellipsis + last 4 chars).
460
455
 
461
456
  ### Wording Discipline: "wallet top-up" vs "card face value"
462
457
 
463
- These two amounts are **different concepts** asked at **different steps**. Translators must keep them lexically distinct so users don't conflate them:
458
+ These two amounts are **different concepts** asked at **different steps**. Translators (to any non-English language) must keep them **lexically distinct** so users do not conflate them:
464
459
 
465
- | Step | Concept | Required wording (English) | Suggested Chinese | Forbidden mix-ups |
466
- | --- | --- | --- | --- | --- |
467
- | Step 2 (`wallet-topup`) | USDT into the **session wallet** | "load USDT **into your session wallet**" | "往**本地钱包**充值 USDT" | Don't say "充值到卡里" / "load onto the card" — that's Step 3a |
468
- | Step 3a (`create-card`) | USD **face value** loaded onto a new card | "card **face value**" / "issue a card with how much" | "**开多少美元的卡** / 卡的面额" | Don't say "充值多少" / "充值到卡" without strong "card" qualifier — confuses with Step 2 |
460
+ | Step | Concept | Required English wording | Forbidden mix-ups |
461
+ | --- | --- | --- | --- |
462
+ | Step 2 (`wallet-topup`) | USDT into the **session wallet** | "load USDT **into your session wallet**" | Never say "load onto the card" — that's Step 3a |
463
+ | Step 3a (`create-card`) | USD **face value** loaded onto a new card | "card **face value**" / "issue a card with how much" | Never use the same verb you used for *wallet top-up* without a strong "card" qualifier |
469
464
 
470
- **Rule of thumb**: if the agent's question contains the word "充值" (top up), the noun *must* be "钱包" (wallet); if the question is about a card's amount, prefer "**面额**" or "**开多少美元的卡**", never "充值".
465
+ **Rule of thumb**: if a question is about the *wallet* (Step 2), the target noun must mean *session wallet*. If a question is about a *card amount* (Step 3a), prefer wording that means *face value* / *the amount loaded onto a new card*. The verb for one concept must never be reused for the other concept in the same language.
package/src/balance.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 钱包余额查询(共享模块)
2
+ * Wallet balance lookup (shared module)
3
3
  */
4
4
  import { privateKeyToAccount } from "viem/accounts";
5
5
  import { createPublicClient, http, formatUnits } from "viem";
@@ -42,8 +42,8 @@ function getClient() {
42
42
  }
43
43
 
44
44
  /**
45
- * 通过地址查询 BNB USDT 余额(不需要私钥)
46
- * @param {string} address - EVM 地址
45
+ * Query BNB and USDT balance by address (no private key required).
46
+ * @param {string} address - EVM address
47
47
  */
48
48
  export async function getBalanceByAddress(address) {
49
49
  const client = getClient();
@@ -68,7 +68,7 @@ export async function getBalanceByAddress(address) {
68
68
  }
69
69
 
70
70
  /**
71
- * 通过私钥查询钱包 BNB USDT 余额
71
+ * Query a wallet's BNB and USDT balance by private key.
72
72
  * @param {string} privateKey
73
73
  */
74
74
  export async function getWalletBalance(privateKey) {
@@ -77,9 +77,9 @@ export async function getWalletBalance(privateKey) {
77
77
  }
78
78
 
79
79
  /**
80
- * 查询 session key facilitator USDT allowance
81
- * @param {string} ownerAddress - session key 地址
82
- * @returns {bigint} 当前 allowancewei
80
+ * Query the session key's USDT allowance for the facilitator.
81
+ * @param {string} ownerAddress - session key address
82
+ * @returns {bigint} current allowance (in wei)
83
83
  */
84
84
  export async function getAllowance(ownerAddress) {
85
85
  const client = getClient();
@@ -8,7 +8,7 @@ export async function clean() {
8
8
  const home = homedir();
9
9
  const removed = [];
10
10
 
11
- // 1. skills CLI 移除(覆盖所有工具)
11
+ // 1. Remove via the skills CLI (covers every tool)
12
12
  try {
13
13
  execFileSync("npx", ["skills", "remove", "aigateway", "-g", "-y"], {
14
14
  stdio: "inherit",
@@ -17,7 +17,7 @@ export async function clean() {
17
17
  logInfo("Removed aigateway skill via skills CLI");
18
18
  removed.push("skills");
19
19
  } catch {
20
- // skills CLI 不可用,手动清理 Claude Code
20
+ // skills CLI unavailable — clean Claude Code manually
21
21
  const skillDir = join(home, ".claude", "skills", "aigateway");
22
22
  if (existsSync(skillDir)) {
23
23
  rmSync(skillDir, { recursive: true, force: true });
@@ -26,7 +26,7 @@ export async function clean() {
26
26
  }
27
27
  }
28
28
 
29
- // 2. 卸载全局包
29
+ // 2. Uninstall the global package
30
30
  try {
31
31
  execFileSync("npm", ["uninstall", "-g", "@aeon-ai-pay/aigateway"], {
32
32
  stdio: "inherit",
@@ -38,7 +38,7 @@ export async function clean() {
38
38
  logInfo("Global package not installed, skipping uninstall");
39
39
  }
40
40
 
41
- // 3. 清理 npm 缓存
41
+ // 3. Clean npm cache
42
42
  try {
43
43
  execFileSync("npm", ["cache", "clean", "--force"], {
44
44
  stdio: "inherit",
@@ -50,7 +50,7 @@ export async function clean() {
50
50
  logError("Failed to clean npm cache, skipping");
51
51
  }
52
52
 
53
- // 4. 清理 npx 缓存
53
+ // 4. Clean npx cache
54
54
  const npxCache = join(home, ".npm", "_npx");
55
55
  if (existsSync(npxCache)) {
56
56
  rmSync(npxCache, { recursive: true, force: true });
@@ -1,10 +1,10 @@
1
1
  /**
2
- * create-card:通过 x402 协议在 BSC 上用 USDT 支付,发一张一次性虚拟卡
2
+ * create-card: issue a one-time virtual card by paying with USDT on BSC over the x402 protocol.
3
3
  *
4
- * 服务端路径:GET {serviceUrl}/open/ai/x402/card/create?amount=<usd>&appId=<merchant>
5
- * 流程:fetch payment requirements balance + allowance 检查
6
- * (余额不足时)走 funding.mjs/fundSessionKey 充值
7
- * x402 EIP-712 签名提交 可选轮询 status
4
+ * Server endpoint: GET {serviceUrl}/open/ai/x402/card/create?amount=<usd>&appId=<merchant>
5
+ * Flow: fetch payment requirements -> check balance + allowance
6
+ * -> (if balance is insufficient) top up via funding.mjs/fundSessionKey
7
+ * -> submit x402 EIP-712 signature -> optionally poll status
8
8
  */
9
9
  import { createX402Api, decodePaymentResponse, fetchPaymentRequirements } from "../x402.mjs";
10
10
  import { resolve } from "../config.mjs";
@@ -163,7 +163,7 @@ export async function createCard(opts) {
163
163
  return;
164
164
  }
165
165
 
166
- // Dry-run:跑完前置检查就退出
166
+ // Dry-run: exit after preflight checks complete
167
167
  if (dryRun) {
168
168
  const preview = {
169
169
  dryRun: true,
@@ -191,7 +191,7 @@ export async function createCard(opts) {
191
191
  return;
192
192
  }
193
193
 
194
- // WalletConnect 充值
194
+ // WalletConnect top-up
195
195
  if (needTopup || needGas) {
196
196
  logInfo("Funding flow triggered...");
197
197
  try {
@@ -1,10 +1,10 @@
1
1
  /**
2
- * create-image:通过 x402 调用 Skill Boss 生成 AI 图像
2
+ * create-image: generate an AI image via Skill Boss using the x402 protocol.
3
3
  *
4
- * 服务端路径:GET {serviceUrl}/open/ai/x402/skillBoss/create?body=<urlencoded-json>&appId=<merchant>
5
- * 流程:fetch payment requirements balance + allowance 检查
6
- * (余额不足时)走 funding.mjs/fundSessionKey 充值
7
- * x402 EIP-712 签名提交 下载图片到本地
4
+ * Server endpoint: GET {serviceUrl}/open/ai/x402/skillBoss/create?body=<urlencoded-json>&appId=<merchant>
5
+ * Flow: fetch payment requirements -> check balance + allowance
6
+ * -> (if balance is insufficient) top up via funding.mjs/fundSessionKey
7
+ * -> submit x402 EIP-712 signature -> download the image locally
8
8
  */
9
9
  import { createX402Api, decodePaymentResponse, fetchPaymentRequirements } from "../x402.mjs";
10
10
  import { resolve } from "../config.mjs";
@@ -1,5 +1,6 @@
1
1
  /**
2
- * gas 命令:通过 WalletConnect 从主钱包向本地钱包转 BNB(withdraw 时支付 gas)
2
+ * gas command: transfer BNB from the main wallet to the local wallet via WalletConnect
3
+ * (used to pay gas during withdraw).
3
4
  */
4
5
  import { loadConfig } from "../config.mjs";
5
6
  import { getBalanceByAddress } from "../balance.mjs";
@@ -1,16 +1,16 @@
1
1
  /**
2
- * wallet-init:本地 session 钱包 check / 创建 + 链上状态评估
2
+ * wallet-init: check / create the local session wallet and assess its on-chain status.
3
3
  *
4
- * 步骤:
5
- * 1. ~/.aigateway/config.json privateKey → viem.generatePrivateKey() 生成
6
- * 2. USDT / BNB 余额(除非刚 created,则跳过查询直接判定为 needsTopup)
7
- * 3. facilitator allowance(同上规则)
8
- * 4. 综合判定 needsTopup 与原因,返回完整就绪状态供 agent 决策
4
+ * Steps:
5
+ * 1. If ~/.aigateway/config.json has no privateKey → generate one with viem.generatePrivateKey().
6
+ * 2. Query USDT / BNB balance (skipped when the wallet was just created — it must be empty).
7
+ * 3. Query the facilitator allowance (same rule).
8
+ * 4. Decide needsTopup with the reason and return the full readiness state for the agent to act on.
9
9
  *
10
- * 设计意图:agent 跑完 wallet-init 一条命令就拿到决策依据:
11
- * - data.ready=true 表示钱包私钥可用
12
- * - data.needsTopup=true → 必须先 wallet-topupenvelope 里附带 presets / minTopup / reason
13
- * - data.needsTopup=false → 可直接 create-card / create-image
10
+ * Design intent: with a single wallet-init call, the agent gets every decision input it needs:
11
+ * - data.ready=true → the session private key is usable
12
+ * - data.needsTopup=true → wallet-topup must run first (the envelope includes presets / minTopup / reason)
13
+ * - data.needsTopup=false → can proceed directly to create-card / create-image
14
14
  */
15
15
  import { loadConfig, saveConfig } from "../config.mjs";
16
16
  import { MIN_AMOUNT, MAX_AMOUNT } from "../constants.mjs";
@@ -41,7 +41,7 @@ export async function initWallet(opts) {
41
41
  logInfo(`Wallet: ${config.address}`);
42
42
  }
43
43
 
44
- // 链上状态评估
44
+ // On-chain status check
45
45
  let usdt = "0";
46
46
  let bnb = "0";
47
47
  let usdtNum = 0;
@@ -50,7 +50,7 @@ export async function initWallet(opts) {
50
50
  let chainCheckError = null;
51
51
 
52
52
  if (created) {
53
- // 刚建好的钱包余额必然为 0,跳过链上查询节省 ~500ms
53
+ // A freshly created wallet is guaranteed to be empty; skip the chain query to save ~500ms.
54
54
  logInfo("Fresh wallet — skipping balance lookup (assumed empty).");
55
55
  } else {
56
56
  try {
@@ -68,18 +68,19 @@ export async function initWallet(opts) {
68
68
  }
69
69
  }
70
70
 
71
- // 决策:是否需要 topup —— 只看链上真实状态,不依赖 config.mainWallet 字段。
72
- // 之前用 !config.mainWallet 当判断条件是错的:mainWallet 只是 withdraw 默认目标地址,
73
- // 即使为 null(如用户外部转账 / 旧版本未记录),只要链上 USDT/allowance 充足就应该
74
- // 直接允许付费调用,不要强制再走一次 wallet-topup。
71
+ // Decision: needsTopup. Use only real on-chain state — do NOT depend on config.mainWallet.
72
+ // The previous logic `created || !config.mainWallet` was wrong: mainWallet is purely the default
73
+ // destination for withdraw. If USDT / allowance on-chain are sufficient — even when mainWallet
74
+ // is null (external CEX deposit / older versions that didn't record it) — paid calls should be
75
+ // allowed without forcing another wallet-topup round.
75
76
  let needsTopup = false;
76
77
  let topupReason = null;
77
78
  if (created) {
78
- // 刚生成的 session key 必然没钱,无需查链
79
+ // A freshly generated session key has no funds — no point querying the chain.
79
80
  needsTopup = true;
80
81
  topupReason = "first_time";
81
82
  } else if (!chainCheckOk) {
82
- // 链上查询失败 —— 保守标记需要 topup 由用户决定下一步
83
+ // Chain probe failed conservatively flag needsTopup so the user can decide what to do.
83
84
  needsTopup = true;
84
85
  topupReason = "chain_check_failed";
85
86
  } else if (usdtNum < LOW_BALANCE_THRESHOLD) {
@@ -102,7 +103,7 @@ export async function initWallet(opts) {
102
103
  bnb,
103
104
  allowance: allowance.toString(),
104
105
  needsTopup,
105
- topupReason, // "no_prior_funding" | "low_balance" | "no_approve" | null
106
+ topupReason, // "first_time" | "low_balance" | "no_approve" | "chain_check_failed" | null
106
107
  minTopup: MIN_TOPUP_USDT,
107
108
  presets: TOPUP_PRESETS,
108
109
  amountLimits: { min: MIN_AMOUNT, max: MAX_AMOUNT },
@@ -1,15 +1,15 @@
1
1
  /**
2
- * topup:充值 + 一次性 approve facilitator
2
+ * wallet-topup: top up the session wallet and run the one-time facilitator approve.
3
3
  *
4
- * 流程:
5
- * 1. 校验 session key 已存在(来自 aigateway wallet-init
6
- * 2. 检查 USDT 余额 + facilitator allowance
7
- * 3. 如果 USDT < LOW_BALANCE_THRESHOLD1 USDT)或 allowance == 0,触发 WalletConnect 充值
8
- * - 充值 amount: TTY 模式交互选择 presets [5, 10, 20, 50] 或自定义;
9
- *TTY 模式需要传 --amount <usdt>,否则报 TOPUP_REQUIRED
10
- * - 同时按需转 0.0003 BNB 用作 approve gas
11
- * 4. session key 自己广播 ERC20.approve(facilitator, MaxUint256)
12
- * 5. 重新查余额 / allowance,返回最终状态
4
+ * Flow:
5
+ * 1. Verify the session key exists (created earlier by `aigateway wallet-init`).
6
+ * 2. Check USDT balance + facilitator allowance.
7
+ * 3. If USDT < LOW_BALANCE_THRESHOLD (1 USDT) or allowance == 0, open WalletConnect to fund:
8
+ * - Top-up amount: TTY mode picks interactively from presets [5, 10, 20, 50] or a custom value;
9
+ * non-TTY mode requires `--amount <usdt>`, otherwise TOPUP_REQUIRED is emitted.
10
+ * - 0.0003 BNB is transferred too when an approve transaction is needed.
11
+ * 4. The session key broadcasts ERC20.approve(facilitator, MaxUint256) once.
12
+ * 5. Re-query balance / allowance and return the final state.
13
13
  */
14
14
  import { resolve } from "../config.mjs";
15
15
  import { getWalletBalance, getAllowance } from "../balance.mjs";
@@ -71,7 +71,7 @@ export async function topup(opts) {
71
71
  const needApprove = allowance === 0n;
72
72
  const needGas = needApprove && bnbRaw === 0n;
73
73
 
74
- // 已就绪:余额够 + approve
74
+ // Already prepared: balance is sufficient and facilitator is approved
75
75
  if (!needTopup && !needApprove) {
76
76
  logInfo("Wallet already prepared (balance ≥ minimum, facilitator approved).");
77
77
  const data = {
@@ -89,7 +89,7 @@ export async function topup(opts) {
89
89
  return;
90
90
  }
91
91
 
92
- // 决定充值金额
92
+ // Decide the top-up amount
93
93
  let topupAmount = null;
94
94
  if (needTopup) {
95
95
  if (balanceLow) {
@@ -134,7 +134,7 @@ export async function topup(opts) {
134
134
  }
135
135
  }
136
136
 
137
- // WalletConnect 充值(USDT + 按需 BNB gas
137
+ // WalletConnect top-up (USDT + optional BNB for approve gas)
138
138
  if (needTopup || needGas) {
139
139
  const willTransfer = [];
140
140
  if (needTopup) willTransfer.push(`${topupAmount} USDT`);
@@ -161,7 +161,7 @@ export async function topup(opts) {
161
161
  }
162
162
  }
163
163
 
164
- // session key 一次性 approve facilitator
164
+ // Session key broadcasts the one-time facilitator approve
165
165
  let approveTx = null;
166
166
  if (needApprove) {
167
167
  let postBnbRaw = bnbRaw;
@@ -193,7 +193,7 @@ export async function topup(opts) {
193
193
  }
194
194
  }
195
195
 
196
- // 最终查余额 + allowance
196
+ // Final balance + allowance re-check
197
197
  let finalUsdt = usdt;
198
198
  let finalBnb = bnb;
199
199
  let finalAllowance = allowance;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * withdraw 命令:将 session key 中的资金转回主钱包(USDT + BNB
2
+ * wallet-withdraw: move the session key's funds (USDT + BNB) back to the main wallet.
3
3
  */
4
4
  import { createPublicClient, createWalletClient, http, parseUnits, formatUnits, encodeFunctionData } from "viem";
5
5
  import { privateKeyToAccount } from "viem/accounts";
@@ -54,7 +54,7 @@ export async function withdraw(opts) {
54
54
 
55
55
  const isWithdrawAll = !opts.amount;
56
56
 
57
- // 无任何资金
57
+ // No funds at all
58
58
  if (balance.usdtRaw === 0n && balance.bnbRaw === 0n) {
59
59
  emitErr("wallet-withdraw", "NO_FUNDS", { message: "No funds to withdraw.", appId });
60
60
  return;
@@ -63,9 +63,9 @@ export async function withdraw(opts) {
63
63
  let usdtTxHash = null;
64
64
  let bnbTxHash = null;
65
65
 
66
- // 1. 赎回 USDT(有 USDT 才执行)
66
+ // 1. Reclaim USDT (only when USDT balance > 0)
67
67
  if (balance.usdtRaw > 0n) {
68
- // USDT 转账需要 BNB gas
68
+ // USDT transfer needs BNB for gas
69
69
  if (balance.bnbRaw === 0n) {
70
70
  emitErr("wallet-withdraw", "INSUFFICIENT_BNB", {
71
71
  message: "No BNB for gas. Withdraw is a normal on-chain transfer and requires BNB to pay gas.",
@@ -119,7 +119,7 @@ export async function withdraw(opts) {
119
119
  }
120
120
  }
121
121
 
122
- // 2. 赎回剩余 BNB(仅赎回全部时)
122
+ // 2. Reclaim remaining BNB (only when withdrawing everything)
123
123
  if (isWithdrawAll) {
124
124
  const freshBalance = balance.usdtRaw > 0n
125
125
  ? await getBalanceByAddress(sessionAddress)
@@ -128,7 +128,7 @@ export async function withdraw(opts) {
128
128
  if (freshBalance.bnbRaw > 0n) {
129
129
  try {
130
130
  const gasPrice = await publicClient.getGasPrice();
131
- // 预留 20% buffer 应对 gas price 波动
131
+ // Reserve a 20% buffer to absorb gas-price fluctuations
132
132
  const gasCost = BNB_TRANSFER_GAS * (gasPrice * 120n / 100n);
133
133
  const sendable = freshBalance.bnbRaw - gasCost;
134
134
 
@@ -159,7 +159,7 @@ export async function withdraw(opts) {
159
159
  }
160
160
  }
161
161
 
162
- // 查询最终余额
162
+ // Final balance lookup
163
163
  let finalBalance;
164
164
  try {
165
165
  finalBalance = await getBalanceByAddress(sessionAddress);
package/src/config.mjs CHANGED
@@ -1,9 +1,10 @@
1
1
  /**
2
- * 配置管理:~/.aigateway/config.json
3
- * 优先级:CLI 参数 > 环境变量 > config.json
2
+ * Config management: ~/.aigateway/config.json
3
+ * Resolution priority: CLI args > env vars > config.json
4
4
  *
5
- * AEON AI Gateway 统一使用同一个 x402 服务端(ai-api.aeon.xyz),
6
- * 不同能力(虚拟卡 / Skill Boss 调用)走不同的路径前缀。
5
+ * AEON AI Gateway uses a single x402 service (ai-api.aeon.xyz);
6
+ * different capabilities (virtual card / Skill Boss calls) share the host
7
+ * but use distinct path prefixes.
7
8
  */
8
9
  import { readFileSync, writeFileSync, mkdirSync, chmodSync } from "fs";
9
10
  import { join } from "path";
@@ -31,7 +32,7 @@ export function saveConfig(config) {
31
32
  }
32
33
 
33
34
  /**
34
- * 解析配置值,优先级:cliValue > envKey > config[configKey]
35
+ * Resolve a value with priority: cliValue > envKey > config[configKey]
35
36
  */
36
37
  export function resolve(cliValue, envKey, configKey) {
37
38
  if (cliValue) return cliValue;
@@ -1,16 +1,16 @@
1
1
  /**
2
- * 错误码常量表 —— CLI 与文档的单一事实源
2
+ * Error-code registry — single source of truth shared by the CLI and the docs.
3
3
  *
4
- * 退出码语义:
5
- * 0 成功
6
- * 1 用户错误(参数、余额、配置、用户拒绝)
7
- * 2 超时(轮询、WalletConnect、签名、链上)
8
- * 3 服务端 / 网络
9
- * 4 内部错误
4
+ * Exit-code semantics:
5
+ * 0 success
6
+ * 1 user error (bad argument, insufficient balance, configuration, user reject)
7
+ * 2 timeout (polling, WalletConnect, signature, on-chain wait)
8
+ * 3 service / network
9
+ * 4 internal error
10
10
  */
11
11
 
12
12
  export const ERROR_CODES = {
13
- // ===== 用户错误(exit 1)=====
13
+ // ===== User error (exit 1) =====
14
14
  WALLET_NOT_CONFIGURED: { exit: 1, message: "Wallet not configured. Run: aigateway wallet-init" },
15
15
  SERVICE_URL_MISSING: { exit: 1, message: "Service URL not configured." },
16
16
  AMOUNT_INVALID: { exit: 1, message: "Invalid amount." },
@@ -25,13 +25,13 @@ export const ERROR_CODES = {
25
25
  TOPUP_AMOUNT_TOO_SMALL: { exit: 1, message: "Top-up amount is below the minimum." },
26
26
  PAYMENT_REJECTED: { exit: 1, message: "Payment approval was rejected. Please try again if you'd like to proceed." },
27
27
 
28
- // ===== 超时(exit 2)=====
28
+ // ===== Timeout (exit 2) =====
29
29
  PAYMENT_TIMEOUT: { exit: 2, message: "Payment approval timed out. Please try again." },
30
30
  WC_SESSION_EXPIRED: { exit: 2, message: "WalletConnect session expired." },
31
31
  POLL_TIMEOUT: { exit: 2, message: "Polling timed out. Card may still be provisioning." },
32
32
  TX_TIMEOUT: { exit: 2, message: "On-chain transaction timed out." },
33
33
 
34
- // ===== 服务/网络(exit 3)=====
34
+ // ===== Service / network (exit 3) =====
35
35
  SERVICE_UNAVAILABLE: { exit: 3, message: "Service unavailable or network error." },
36
36
  PAYMENT_FETCH_FAILED: { exit: 3, message: "Failed to fetch payment requirements." },
37
37
  BALANCE_CHECK_FAILED: { exit: 3, message: "Failed to check balance." },
@@ -44,7 +44,7 @@ export const ERROR_CODES = {
44
44
  IMAGE_DOWNLOAD_FAILED: { exit: 3, message: "Image download failed." },
45
45
  FUNDING_FAILED: { exit: 3, message: "Funding flow failed." },
46
46
 
47
- // ===== 内部(exit 4)=====
47
+ // ===== Internal (exit 4) =====
48
48
  INTERNAL_ERROR: { exit: 4, message: "Internal error." },
49
49
  WALLET_ERROR: { exit: 1, message: "Wallet operation failed." },
50
50
  };
package/src/output.mjs CHANGED
@@ -1,12 +1,13 @@
1
1
  /**
2
- * 统一输出封装:envelope JSON + 分级日志
2
+ * Unified output envelope: JSON on stdout + tiered stderr logs.
3
3
  *
4
- * stdout: 一行最终 JSONmachine-readable
5
- * - 成功:{ ok: true, command, version, data }
6
- * - 失败:{ ok: false, command, version, error: { code, message, ...context } }
7
- * stderr: 进度日志(human-readable,agent 可忽略)
4
+ * stdout: a single line of final JSON (machine-readable)
5
+ * - success: { ok: true, command, version, data }
6
+ * - failure: { ok: false, command, version, error: { code, message, ...context } }
7
+ * stderr: progress logs (human-readable; agents can ignore them)
8
8
  *
9
- * --legacy-output 模式:保留旧裸字段格式,便于已按旧 JSON 解析的脚本/agent 平滑过渡。
9
+ * --legacy-output mode: emit the pre-envelope shape so scripts / agents that
10
+ * already parse the old JSON can migrate gradually.
10
11
  */
11
12
 
12
13
  import { readFileSync } from "fs";
@@ -30,10 +31,11 @@ export function isLegacyMode() { return LEGACY_MODE; }
30
31
  export function isVerboseMode() { return VERBOSE_MODE; }
31
32
 
32
33
  /**
33
- * 输出成功结果。调用方应在调用后让函数自然返回(不要再 process.exit)。
34
- * @param {string} command - 命令名,如 "create-card" / "wallet-init"
35
- * @param {object} data - envelope 模式下放在 data 字段
36
- * @param {object} [legacyShape] - legacy 模式下直接输出的旧格式对象;省略则使用 data 本身
34
+ * Emit a success result. Callers should let the function return naturally
35
+ * (do not call process.exit afterwards).
36
+ * @param {string} command - command name, e.g. "create-card" / "wallet-init"
37
+ * @param {object} data - placed under envelope.data
38
+ * @param {object} [legacyShape] - the legacy-mode payload; if omitted, `data` is used
37
39
  */
38
40
  export function emitOk(command, data, legacyShape) {
39
41
  if (LEGACY_MODE) {
@@ -44,10 +46,11 @@ export function emitOk(command, data, legacyShape) {
44
46
  }
45
47
 
46
48
  /**
47
- * 输出错误并退出(按错误码对应的 exit 码)。
49
+ * Emit an error and exit with the corresponding exit code.
48
50
  * @param {string} command
49
- * @param {string} code - ERROR_CODES 键名
50
- * @param {object} [details] - 额外字段。message 字段会覆盖默认 message;legacy 字段在 legacy 模式下完全替代输出
51
+ * @param {string} code - key of ERROR_CODES
52
+ * @param {object} [details] - extra fields. `message` overrides the default message;
53
+ * `legacy` fully replaces the output in legacy mode.
51
54
  */
52
55
  export function emitErr(command, code, details = {}) {
53
56
  const info = ERROR_CODES[code] || ERROR_CODES.INTERNAL_ERROR;
@@ -69,17 +72,17 @@ export function emitErr(command, code, details = {}) {
69
72
  process.exit(exit);
70
73
  }
71
74
 
72
- /** 进度日志(quiet 模式压制) */
75
+ /** Progress log (suppressed in quiet mode) */
73
76
  export function logInfo(msg) {
74
77
  if (!QUIET_MODE) console.error(msg);
75
78
  }
76
79
 
77
- /** 详细日志(仅 verbose 模式下输出) */
80
+ /** Verbose log (only emitted in verbose mode) */
78
81
  export function logVerbose(msg) {
79
82
  if (VERBOSE_MODE && !QUIET_MODE) console.error(msg);
80
83
  }
81
84
 
82
- /** 错误日志(quiet 模式下也会输出) */
85
+ /** Error log (still emitted in quiet mode) */
83
86
  export function logError(msg) {
84
87
  console.error(msg);
85
88
  }
package/src/sanitize.mjs CHANGED
@@ -1,14 +1,14 @@
1
1
  /**
2
- * 卡片输出脱敏:隐藏敏感卡片信息(完整卡号→末4位、移除CVV、移除有效期)
3
- * CLI 输出 JSON Agent 解析,Agent 按产品模板展示给用户
2
+ * Card output sanitisation: redact sensitive card data (truncate the full PAN to its last 4 digits, drop CVV, drop expiry).
3
+ * The CLI emits JSON for an agent to parse; the agent then renders the product-specific template to the user.
4
4
  */
5
5
 
6
- // 需要替换为末4位的字段
6
+ // Fields whose value should be replaced with the last-4 representation
7
7
  const CARD_NUMBER_KEYS = new Set([
8
8
  "cardnumber", "cardno",
9
9
  ]);
10
10
 
11
- // 需要完全移除的字段
11
+ // Fields that must be removed entirely
12
12
  const REMOVE_KEYS = new Set([
13
13
  "cvv", "cvv2", "cvc", "cvc2", "securitycode",
14
14
  "expiry", "expirydate", "expiredate", "cardexpiry",
@@ -16,10 +16,10 @@ const REMOVE_KEYS = new Set([
16
16
  ]);
17
17
 
18
18
  /**
19
- * 递归脱敏对象:
20
- * - cardNumber/cardNo → 只保留末4位("•••• 3398"
21
- * - cvv/securityCode → 移除
22
- * - expiry/expireDate → 移除
19
+ * Recursively sanitise an object:
20
+ * - cardNumber / cardNo → keep only the last four digits ("•••• 3398")
21
+ * - cvv / securityCode → drop
22
+ * - expiry / expireDate → drop
23
23
  */
24
24
  export function sanitizeOutput(obj) {
25
25
  if (obj === null || obj === undefined) return obj;
@@ -30,15 +30,15 @@ export function sanitizeOutput(obj) {
30
30
  for (const [key, value] of Object.entries(obj)) {
31
31
  const normalized = key.toLowerCase().replace(/[-_]/g, "");
32
32
 
33
- // 移除 CVV、有效期等敏感字段
33
+ // Drop sensitive fields (CVV, expiry, etc.)
34
34
  if (REMOVE_KEYS.has(normalized)) continue;
35
35
 
36
- // 卡号只保留末4
36
+ // Card number: only keep the last 4 digits
37
37
  if (CARD_NUMBER_KEYS.has(normalized)) {
38
38
  if (typeof value === "string" && value.length >= 4) {
39
39
  result[key] = "•••• " + value.slice(-4);
40
40
  }
41
- // value null 时不输出此字段
41
+ // when value is null, do not emit this field
42
42
  continue;
43
43
  }
44
44
 
@@ -1,11 +1,11 @@
1
1
  /**
2
- * 自动版本检查 + 静默后台升级
2
+ * Auto version check + silent background upgrade.
3
3
  *
4
- * 策略:
5
- * 1. 同步快速检查(npm view)— 发现新版本时输出提示
6
- * 2. spawn 后台子进程执行 npm install -g 升级
7
- * 3. 升级后执行 postinstall.mjsskills CLI 安装到所有工具)
8
- * 不阻塞主进程
4
+ * Strategy:
5
+ * 1. Synchronously poll the npm registry (`npm view`) — print a notice when a newer version is found.
6
+ * 2. Spawn a detached background process to run `npm install -g`.
7
+ * 3. After install, run postinstall.mjs (which re-installs the skill into every detected tool via the skills CLI).
8
+ * Does not block the main process.
9
9
  */
10
10
 
11
11
  import { execFileSync, spawn } from "node:child_process";
@@ -13,11 +13,11 @@ import { execFileSync, spawn } from "node:child_process";
13
13
  const PKG_NAME = "@aeon-ai-pay/aigateway";
14
14
 
15
15
  /**
16
- * 启动时调用:同步检查版本 + 后台升级
16
+ * Called at CLI startup: synchronous version probe + detached upgrade.
17
17
  * @param {string} currentVersion
18
18
  */
19
19
  export function checkForUpdates(currentVersion) {
20
- // 同步快速检查最新版本(超时短,不阻塞太久)
20
+ // Synchronous fast probe (short timeout so it does not block too long)
21
21
  let latest;
22
22
  try {
23
23
  latest = execFileSync("npm", ["view", PKG_NAME, "version"], {
@@ -25,15 +25,15 @@ export function checkForUpdates(currentVersion) {
25
25
  stdio: ["ignore", "pipe", "ignore"],
26
26
  }).toString().trim();
27
27
  } catch {
28
- return; // 网络不可用,静默跳过
28
+ return; // network unavailable — silently skip
29
29
  }
30
30
 
31
31
  if (!latest || latest === currentVersion) return;
32
32
 
33
- // 有新版本:输出提示
33
+ // A newer version exists — print a notice
34
34
  console.error(`[update] ${PKG_NAME} ${currentVersion} → ${latest}, upgrading in background...`);
35
35
 
36
- // 后台执行升级(结果写入日志文件)
36
+ // Run the upgrade in a detached child, writing the result to a log file
37
37
  const script = `
38
38
  const { execFileSync } = require("child_process");
39
39
  const { join } = require("path");
@@ -1,6 +1,6 @@
1
1
  /**
2
- * WalletConnect v2 封装模块
3
- * 用于通过 WalletConnect 协议连接用户钱包并发起交易
2
+ * WalletConnect v2 wrapper.
3
+ * Connects to the user's wallet over WalletConnect and submits transactions.
4
4
  */
5
5
  import { SignClient } from "@walletconnect/sign-client";
6
6
  import { encodeFunctionData, parseUnits } from "viem";
@@ -13,8 +13,9 @@ import { WC_CONNECT_TIMEOUT_MS, ERC20_TRANSFER_ABI, DEFAULT_WC_PROJECT_ID } from
13
13
  import { loadConfig, saveConfig } from "./config.mjs";
14
14
 
15
15
  /**
16
- * WalletConnect 流程抛出的错误,携带稳定 error code(PAYMENT_TIMEOUT / PAYMENT_REJECTED / WALLET_ERROR)。
17
- * Commands 层捕获后通过 emitErr 输出,无需关心底层细节。
16
+ * Error type thrown by the WalletConnect flow, carrying a stable error code
17
+ * (PAYMENT_TIMEOUT / PAYMENT_REJECTED / WALLET_ERROR). The Commands layer
18
+ * catches it and forwards it via emitErr; no caller needs to look at the underlying details.
18
19
  */
19
20
  export class WalletConnectError extends Error {
20
21
  constructor(code, message) {
@@ -24,7 +25,7 @@ export class WalletConnectError extends Error {
24
25
  }
25
26
  }
26
27
 
27
- // ============== 状态同步服务器(供浏览器页面轮询) ==============
28
+ // ============== Status server (polled by the browser-side QR page) ==============
28
29
 
29
30
  let _status = { state: "waiting_scan" };
30
31
  let _server = null;
@@ -65,14 +66,14 @@ export function stopStatusServer() {
65
66
 
66
67
  const BSC_CHAIN_ID = "eip155:56";
67
68
 
68
- // QR 页面倒计时(与 WalletConnect 连接超时一致)
69
+ // QR-page countdown (matches the WalletConnect connection timeout)
69
70
  const QR_EXPIRE_MS = 5 * 60 * 1000;
70
71
 
71
72
  /**
72
- * 生成 QR HTML 页面并在浏览器中打开(按 Figma Ai card v1.2 设计稿)
73
+ * Generate the QR-code HTML page and open it in a browser (follows the Figma "AI card v1.2" design).
73
74
  * @param {string} uri - WalletConnect URI
74
- * @param {number} statusPort - 状态服务端口
75
- * @param {string|null} amount - 用户需要支付的 USDT 数量(如 "0.66"
75
+ * @param {number} statusPort - port of the local status server
76
+ * @param {string|null} amount - USDT amount the user has to pay (e.g. "0.66")
76
77
  */
77
78
  function openQRInBrowser(uri, statusPort, amount, token = "USDT", network = "BNB Chain(BEP20) only", gasAmount = null) {
78
79
  const html = `<!DOCTYPE html>
@@ -165,16 +166,16 @@ function openQRInBrowser(uri, statusPort, amount, token = "USDT", network = "BNB
165
166
  const EXPIRE_MS = ${QR_EXPIRE_MS};
166
167
  const startTime = Date.now();
167
168
 
168
- // 倒计时 SVG 图标(Figma 导出 1:63 clock 16x16)- 使用 currentColor 跟随 .timer 颜色
169
+ // Clock countdown icon (exported from Figma 1:63, 16x16) uses currentColor to track `.timer` color
169
170
  const CLOCK_SVG = '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M14.667 8C14.667 11.68 11.68 14.667 8 14.667C4.32 14.667 1.334 11.68 1.334 8C1.334 4.32 4.32 1.333 8 1.333C11.68 1.333 14.667 4.32 14.667 8Z" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/><path d="M10.476 10.12L8.409 8.887C8.049 8.674 7.756 8.16 7.756 7.74V5.007" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>';
170
- // 提示 info 图标(Figma 导出 1:43 Icon 20x20
171
+ // Info / hint icon (exported from Figma 1:43, 20x20)
171
172
  const INFO_SVG = '<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path opacity="0.2" d="M10 18.333C14.6 18.333 18.331 14.602 18.331 10C18.331 5.397 14.6 1.666 10 1.666C5.395 1.666 1.664 5.397 1.664 10C1.664 14.602 5.395 18.333 10 18.333Z" fill="#737A86"/><path d="M10 13.333V10" stroke="#737A86" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M10 6.666H10.008" stroke="#737A86" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>';
172
- // Loading 旋转图标(Figma 导出 1:50 loading-02 24x24
173
+ // Loading spinner icon (exported from Figma 1:50 loading-02, 24x24)
173
174
  const LOADING_SVG = '<svg class="loading-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path opacity=".7" d="M4.922 5l2.828 2.828" stroke="#191B1F" stroke-width="2.5" stroke-linecap="round"/><path opacity=".6" d="M6 12H2" stroke="#191B1F" stroke-width="2.5" stroke-linecap="round"/><path opacity=".5" d="M4.922 19.078l2.828-2.828" stroke="#191B1F" stroke-width="2.5" stroke-linecap="round"/><path opacity=".4" d="M12 18v4" stroke="#191B1F" stroke-width="2.5" stroke-linecap="round"/><path opacity=".3" d="M19.078 19.078L16.25 16.25" stroke="#191B1F" stroke-width="2.5" stroke-linecap="round"/><path opacity=".2" d="M22 12h-4" stroke="#191B1F" stroke-width="2.5" stroke-linecap="round"/><path opacity=".1" d="M19.078 5L16.25 7.828" stroke="#191B1F" stroke-width="2.5" stroke-linecap="round"/><path opacity=".8" d="M12 2v4" stroke="#191B1F" stroke-width="2.5" stroke-linecap="round"/></svg>';
174
- // 成功勾选
175
+ // Success checkmark
175
176
  const CHECK_SVG = '<div class="check-icon"><svg viewBox="0 0 14 14" fill="none"><path d="M3 7l3 3 5-5" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></div>';
176
177
 
177
- // 超时插图 SVG(从 Figma 导出)
178
+ // "Expired" illustration SVG (exported from Figma)
178
179
  const EXPIRED_SVG = \`<svg width="144" height="129" viewBox="0 0 144 129" fill="none" xmlns="http://www.w3.org/2000/svg">
179
180
  <path opacity="0.1" d="M140.369 64.988C140.369 82.769 133.106 98.797 121.461 110.317C110.066 121.711 94.289 128.598 76.884 128.598C59.604 128.598 43.826 121.586 32.306 110.317C20.661 98.797 13.398 82.769 13.398 64.988C13.398 29.802 41.823 1.377 76.884 1.377C111.944 1.377 140.369 29.927 140.369 64.988Z" fill="#1A72F7"/>
180
181
  <path d="M134.859 23.291C137.695 23.291 139.993 20.992 139.993 18.157C139.993 15.321 137.695 13.023 134.859 13.023C132.024 13.023 129.725 15.321 129.725 18.157C129.725 20.992 132.024 23.291 134.859 23.291Z" fill="#E8F1FE"/>
@@ -194,7 +195,7 @@ function openQRInBrowser(uri, statusPort, amount, token = "USDT", network = "BNB
194
195
  <linearGradient id="pb2" x1="53.891" y1="104.557" x2="103.297" y2="105.207" gradientUnits="userSpaceOnUse"><stop stop-color="#DBDEE3" stop-opacity=".7"/><stop offset="1" stop-color="#DBDEE3"/></linearGradient>
195
196
  </defs></svg>\`;
196
197
 
197
- // 拒绝插图 SVG(基于超时插图,替换时钟为 X 标记,橙色改红色)
198
+ // "Rejected" illustration SVG (variant of the expired illustration: clock swapped for an X, orange swapped for red)
198
199
  const REJECTED_SVG = \`<svg width="144" height="129" viewBox="0 0 144 129" fill="none" xmlns="http://www.w3.org/2000/svg">
199
200
  <path opacity="0.1" d="M140.369 64.988C140.369 82.769 133.106 98.797 121.461 110.317C110.066 121.711 94.289 128.598 76.884 128.598C59.604 128.598 43.826 121.586 32.306 110.317C20.661 98.797 13.398 82.769 13.398 64.988C13.398 29.802 41.823 1.377 76.884 1.377C111.944 1.377 140.369 29.927 140.369 64.988Z" fill="#1A72F7"/>
200
201
  <path d="M134.859 23.291C137.695 23.291 139.993 20.992 139.993 18.157C139.993 15.321 137.695 13.023 134.859 13.023C132.024 13.023 129.725 15.321 129.725 18.157C129.725 20.992 132.024 23.291 134.859 23.291Z" fill="#E8F1FE"/>
@@ -223,7 +224,7 @@ function openQRInBrowser(uri, statusPort, amount, token = "USDT", network = "BNB
223
224
 
224
225
  function fmtAmount(v) {
225
226
  if (v == null || v === '') return '';
226
- // 完整显示原始金额(不做取整),去除末尾多余的 0,整数部分加千分位
227
+ // Render the full original amount (no rounding); trim trailing zeros and add thousands separators to the integer part
227
228
  const s = String(v);
228
229
  const neg = s.startsWith('-') ? '-' : '';
229
230
  const body = neg ? s.slice(1) : s;
@@ -233,7 +234,7 @@ function openQRInBrowser(uri, statusPort, amount, token = "USDT", network = "BNB
233
234
  return neg + intWithComma + (decPart ? '.' + decPart : '') + ' ' + TOKEN;
234
235
  }
235
236
 
236
- // ====== 页面渲染函数 ======
237
+ // ====== Page rendering helpers ======
237
238
 
238
239
  function renderQR(data) {
239
240
  const remaining = Math.max(0, EXPIRE_MS - (Date.now() - startTime));
@@ -250,22 +251,22 @@ function openQRInBrowser(uri, statusPort, amount, token = "USDT", network = "BNB
250
251
 
251
252
  const expireMin = Math.ceil(remaining / 60000);
252
253
 
253
- // QR wrap: 圆角矩形路径从12点(顶部中心)出发,顺时针绘制
254
- // canvas 200 + padding 12*2 = 224, 圆角 r=16, 描边偏移1px
254
+ // QR wrap: rounded-rectangle path starts at 12 o'clock (top center) and goes clockwise.
255
+ // canvas 200 + padding 12*2 = 224, corner radius r=16, stroke offset 1px
255
256
  const S = 224, R = 16, cx = S/2;
256
- // 从顶部中心开始,顺时针绘制一圈回到起点(开放路径,不用 Z 闭合,避免 dash 环绕)
257
+ // Start at top center, draw clockwise back to the start (open path, no Z avoids the dash wrapping around)
257
258
  const qrPath = 'M' + cx + ',1 H' + (S-1-R) + ' A' + R + ',' + R + ' 0 0 1 ' + (S-1) + ',' + (1+R) +
258
259
  ' V' + (S-1-R) + ' A' + R + ',' + R + ' 0 0 1 ' + (S-1-R) + ',' + (S-1) +
259
260
  ' H' + (1+R) + ' A' + R + ',' + R + ' 0 0 1 1,' + (S-1-R) +
260
261
  ' V' + (1+R) + ' A' + R + ',' + R + ' 0 0 1 ' + (1+R) + ',1 H' + cx;
261
- // pathLength=1000 统一长度,避免手算偏差
262
+ // Use pathLength=1000 so the dash math is exact (no hand-calculated drift)
262
263
  const PL = 1000;
263
264
  const progress = remaining / EXPIRE_MS;
264
265
  const dashOffset = -PL * (1 - progress);
265
266
 
266
- // USDT 图标 SVG(绿色圆形 Tether 标志)
267
+ // USDT icon SVG (green circle Tether glyph)
267
268
  const USDT_ICON = '<svg class="usdt-icon" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="9" cy="9" r="9" fill="#26A17B"/><path d="M10.1 9.6c-.06 0-.33.02-.73.02-.32 0-.58-.01-.67-.02-1.32-.06-2.3-.3-2.3-.58 0-.29.98-.52 2.3-.58v.93c.09.01.36.02.68.02.38 0 .66-.01.72-.02v-.93c1.31.06 2.29.3 2.29.58 0 .28-.98.52-2.29.58zm0-.87v-.83h2.05V6.5H5.88v1.4h2.05v.83c-1.48.07-2.6.38-2.6.75 0 .37 1.12.68 2.6.75v2.69h1.07v-2.69c1.48-.07 2.59-.38 2.59-.75 0-.37-1.11-.68-2.59-.75z" fill="#fff"/></svg>';
268
- // BNB 图标 SVG(黄色圆形 BNB 标志)
269
+ // BNB icon SVG (yellow circle BNB glyph)
269
270
  const BNB_ICON = '<svg class="usdt-icon" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="9" cy="9" r="9" fill="#F3BA2F"/><path d="M9 4.5L7.2 6.3l-1.8-1.8L9 1.2l3.6 3.3-1.8 1.8L9 4.5zm-4.5 4.5L2.7 7.2 4.5 5.4l1.8 1.8L4.5 9zm4.5 4.5l-1.8-1.8-1.8 1.8L9 16.8l3.6-3.3-1.8-1.8L9 13.5zm4.5-4.5l1.8 1.8-1.8 1.8-1.8-1.8 1.8-1.8zM10.8 9L9 7.2 7.2 9 9 10.8 10.8 9z" fill="#fff"/></svg>';
270
271
  const TOKEN_ICON = TOKEN === 'BNB' ? BNB_ICON : USDT_ICON;
271
272
 
@@ -307,7 +308,7 @@ function openQRInBrowser(uri, statusPort, amount, token = "USDT", network = "BNB
307
308
  '<div class="result-sub">' + (error || 'Something went wrong.') + '<br>Please try again.</div></div>';
308
309
  }
309
310
 
310
- // ====== 状态机 ======
311
+ // ====== State machine ======
311
312
 
312
313
  const FINAL = ['confirmed', 'rejected', 'failed', 'expired'];
313
314
  let lastState = null;
@@ -339,7 +340,7 @@ function openQRInBrowser(uri, statusPort, amount, token = "USDT", network = "BNB
339
340
  stopTimer();
340
341
  } else {
341
342
  body.innerHTML = renderQR(data);
342
- // 渲染 QR
343
+ // Render the QR code
343
344
  const qrEl = document.getElementById('qr');
344
345
  if (qrEl) {
345
346
  new QRious({ element: qrEl, value: URI, size: 200, backgroundAlpha: 1, background: '#ffffff', foreground: '#000000', level: 'M' });
@@ -361,13 +362,13 @@ function openQRInBrowser(uri, statusPort, amount, token = "USDT", network = "BNB
361
362
  closeTimer = setTimeout(tick, 1000);
362
363
  }
363
364
 
364
- // 初始渲染
365
+ // Initial render
365
366
  render(null);
366
367
 
367
- // 倒计时刷新(每秒更新 timer
368
+ // Countdown refresh (update timer every second)
368
369
  timerInterval = setInterval(() => {
369
370
  const remaining = EXPIRE_MS - (Date.now() - startTime);
370
- // 后端已进入活跃状态(已连接/签名中/交易已提交),不触发页面过期
371
+ // Backend has entered an active state (connected / signing / tx-submitted); do not expire the page
371
372
  const ACTIVE = ['connected', 'signing', 'tx_submitted'];
372
373
  if (remaining <= 0 && !FINAL.includes(lastState) && !ACTIVE.includes(lastState)) {
373
374
  lastState = 'expired';
@@ -375,7 +376,7 @@ function openQRInBrowser(uri, statusPort, amount, token = "USDT", network = "BNB
375
376
  maybeAutoClose('expired');
376
377
  return;
377
378
  }
378
- // 更新倒计时文字
379
+ // Update the countdown label
379
380
  const timerEl = document.querySelector('.timer');
380
381
  if (timerEl) {
381
382
  const expireMin = Math.ceil(Math.max(0, remaining) / 60000);
@@ -383,7 +384,7 @@ function openQRInBrowser(uri, statusPort, amount, token = "USDT", network = "BNB
383
384
  const hintSpan = document.querySelector('.hint-bar span');
384
385
  if (hintSpan) hintSpan.textContent = 'Expire in ' + expireMin + ' mins. This page will close automatically once the transfer is completed';
385
386
  }
386
- // 更新 QR 边框倒计时进度(负值12点顺时针消失)
387
+ // Update the QR-border progress (negative offset the dash recedes clockwise from 12 o'clock)
387
388
  const progressEl = document.getElementById('qr-progress');
388
389
  if (progressEl) {
389
390
  const PL = 1000;
@@ -392,7 +393,7 @@ function openQRInBrowser(uri, statusPort, amount, token = "USDT", network = "BNB
392
393
  }
393
394
  }, 1000);
394
395
 
395
- // 轮询后端状态
396
+ // Poll backend status
396
397
  async function poll() {
397
398
  if (stopped) return;
398
399
  try {
@@ -426,7 +427,7 @@ function openQRInBrowser(uri, statusPort, amount, token = "USDT", network = "BNB
426
427
  }
427
428
 
428
429
  /**
429
- * 初始化 WalletConnect SignClient
430
+ * Initialise the WalletConnect SignClient
430
431
  * @param {string} projectId - WalletConnect Cloud project ID
431
432
  */
432
433
  export async function initSignClient(projectId) {
@@ -446,8 +447,9 @@ export async function initSignClient(projectId) {
446
447
  ),
447
448
  ]);
448
449
 
449
- // WalletConnect relay 偶发 null WebSocket 帧,导致 isJsonRpcPayload('id' in null) 崩溃
450
- // connection 层过滤掉 null payload,避免错误传播到 provider / request 链路
450
+ // WalletConnect relay occasionally emits null WebSocket frames, which crashes
451
+ // isJsonRpcPayload('id' in null). Filter null payloads at the connection layer so
452
+ // the error never reaches the provider / request chain.
451
453
  try {
452
454
  const conn = client.core.relayer.provider.connection;
453
455
  const _origEmit = conn.emit.bind(conn);
@@ -460,7 +462,7 @@ export async function initSignClient(projectId) {
460
462
  };
461
463
  } catch {}
462
464
 
463
- // 清理残留 session + pairing(并行等待完成,确保 relay 状态干净后再建新连接)
465
+ // Tear down stale sessions + pairings (await in parallel to ensure the relay is clean before reconnecting)
464
466
  try {
465
467
  const sessions = client.session.getAll();
466
468
  if (sessions.length > 0) {
@@ -486,10 +488,10 @@ export async function initSignClient(projectId) {
486
488
  }
487
489
 
488
490
  /**
489
- * 连接钱包:展示 QR 码,等待用户扫码授权
491
+ * Connect to a wallet: render the QR code and wait for the user to approve via scan.
490
492
  * @param {SignClient} signClient
491
493
  * @param {number} statusPort
492
- * @param {string|null} amount - 需要展示的 USDT 金额(如 "0.66"
494
+ * @param {string|null} amount - USDT amount to display (e.g. "0.66")
493
495
  * @returns {{ session: object, peerAddress: string }}
494
496
  */
495
497
  export async function connectWallet(signClient, statusPort, amount = null, token = "USDT", gasAmount = null) {
@@ -503,7 +505,7 @@ export async function connectWallet(signClient, statusPort, amount = null, token
503
505
  },
504
506
  });
505
507
 
506
- // 生成 QR 码页面(含状态轮询)并在浏览器中打开
508
+ // Generate the QR-code page (with status polling) and open it in the browser
507
509
  openQRInBrowser(uri, statusPort, amount, token, "BNB Chain(BEP20) only", gasAmount);
508
510
  console.error("QR code opened in browser. Scan it with your wallet app.");
509
511
  console.error("Waiting for wallet approval...");
@@ -528,11 +530,11 @@ export async function connectWallet(signClient, statusPort, amount = null, token
528
530
  }
529
531
 
530
532
  /**
531
- * 请求 ERC-20 代币转账
533
+ * Request an ERC-20 token transfer
532
534
  * @param {SignClient} signClient
533
535
  * @param {object} session - WalletConnect session
534
536
  * @param {{ from: string, to: string, token: string, amount: string, decimals?: number }} params
535
- * @returns {string} 交易 hash
537
+ * @returns {string} transaction hash
536
538
  */
537
539
  export async function requestERC20Transfer(signClient, session, { from, to, token, amount, decimals = 18 }) {
538
540
  const value = parseUnits(amount, decimals);
@@ -554,7 +556,7 @@ export async function requestERC20Transfer(signClient, session, { from, to, toke
554
556
  from,
555
557
  to: token,
556
558
  data,
557
- gas: "0xFDE8", // 65000 — ERC20 transfer 合约调用
559
+ gas: "0xFDE8", // 65000 — ERC20.transfer contract call
558
560
  },
559
561
  ],
560
562
  },
@@ -568,11 +570,11 @@ export async function requestERC20Transfer(signClient, session, { from, to, toke
568
570
  }
569
571
 
570
572
  /**
571
- * 请求原生 BNB 转账
573
+ * Request a native BNB transfer
572
574
  * @param {SignClient} signClient
573
575
  * @param {object} session
574
- * @param {{ from: string, to: string, value: string }} params - value BNB 数量(如 "0.001"
575
- * @returns {string} 交易 hash
576
+ * @param {{ from: string, to: string, value: string }} params - `value` is the BNB amount (e.g. "0.001")
577
+ * @returns {string} transaction hash
576
578
  */
577
579
  export async function requestNativeTransfer(signClient, session, { from, to, value }) {
578
580
  const weiValue = "0x" + parseUnits(value, 18).toString(16);
@@ -589,7 +591,7 @@ export async function requestNativeTransfer(signClient, session, { from, to, val
589
591
  from,
590
592
  to,
591
593
  value: weiValue,
592
- gas: "0x5208", // 21000 — BNB 原生转账固定 gas
594
+ gas: "0x5208", // 21000 — fixed gas for a native BNB transfer
593
595
  },
594
596
  ],
595
597
  },
@@ -605,11 +607,11 @@ export async function requestNativeTransfer(signClient, session, { from, to, val
605
607
  const FINAL_LINGER_MS = 2000;
606
608
 
607
609
  /**
608
- * 通用钱包连接高阶函数:自动管理连接生命周期
609
- * - 启动状态服务器
610
- * - 扫码连接钱包
611
- * - 执行调用方的事务逻辑
612
- * - 无论成功失败,始终断开连接并清理
610
+ * Higher-order wallet-connect helper: manages the full connection lifecycle.
611
+ * - Starts the status server
612
+ * - Waits for the user to scan and connect their wallet
613
+ * - Runs the caller's transaction logic
614
+ * - Always disconnects and cleans up, whether the body succeeded or failed
613
615
  *
614
616
  * @param {{ amount?: string, projectId?: string }} opts
615
617
  * @param {(ctx: { signClient, session, peerAddress }) => Promise<void>} fn
@@ -629,7 +631,7 @@ export async function withWallet(opts, fn) {
629
631
 
630
632
  await fn({ signClient, session, peerAddress });
631
633
 
632
- // 成功:写回 mainWallet
634
+ // Success: persist mainWallet so subsequent withdraws can default to it
633
635
  const config = loadConfig();
634
636
  config.mainWallet = peerAddress;
635
637
  saveConfig(config);
@@ -652,11 +654,11 @@ export async function withWallet(opts, fn) {
652
654
  await new Promise((r) => setTimeout(r, FINAL_LINGER_MS));
653
655
  stopStatusServer();
654
656
  if (signClient) {
655
- // 断开当前 session
657
+ // Disconnect the current session
656
658
  if (session) {
657
659
  await disconnectSession(signClient, session);
658
660
  }
659
- // 断开所有 pairing,确保钱包端不残留连接
661
+ // Disconnect every pairing so the wallet side has no lingering connection
660
662
  try {
661
663
  const pairings = signClient.core.pairing.pairings.getAll({ active: true });
662
664
  await Promise.allSettled(
@@ -672,19 +674,19 @@ export async function withWallet(opts, fn) {
672
674
  }
673
675
 
674
676
  /**
675
- * 标准化钱包错误消息:将已知的中文/多语言错误映射为统一英文
677
+ * Normalise wallet error messages: map known Chinese / multilingual error strings to a unified English form.
676
678
  * @param {Error} error
677
- * @returns {Error} 同一个 error 对象,message 已替换
679
+ * @returns {Error} the same error object with its `message` rewritten
678
680
  */
679
681
  export function normalizeWalletError(error) {
680
682
  const msg = error?.message || "";
681
683
  const patterns = [
682
- // 拒绝类
683
- { test: /拒绝|用户取消|User rejected|User denied|declined/i, replacement: "rejected" },
684
- // 断开连接类
685
- { test: /断开.*连接|断开.*DApp|disconnect.*DApp|session.*expired|session.*disconnected/i, replacement: "rejected" },
686
- // 超时类
687
- { test: /超时|timed?\s*out|timeout/i, replacement: "timed out" },
684
+ // Rejection patterns (English error strings only — localised wallets fall through to WALLET_ERROR)
685
+ { test: /User rejected|User denied|declined/i, replacement: "rejected" },
686
+ // Disconnect patterns
687
+ { test: /disconnect.*DApp|session.*expired|session.*disconnected/i, replacement: "rejected" },
688
+ // Timeout patterns
689
+ { test: /timed?\s*out|timeout/i, replacement: "timed out" },
688
690
  ];
689
691
  for (const { test, replacement } of patterns) {
690
692
  if (test.test(msg)) {
@@ -696,7 +698,7 @@ export function normalizeWalletError(error) {
696
698
  }
697
699
 
698
700
  /**
699
- * 断开 WalletConnect 会话
701
+ * Disconnect a WalletConnect session.
700
702
  * @param {SignClient} signClient
701
703
  * @param {object} session
702
704
  */
@@ -707,6 +709,6 @@ export async function disconnectSession(signClient, session) {
707
709
  reason: { code: 6000, message: "Session complete" },
708
710
  });
709
711
  } catch {
710
- // 静默处理断开错误
712
+ // Silently ignore disconnect errors
711
713
  }
712
714
  }
package/src/x402.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * x402 协议客户端:初始化 EVM signer + x402Client
2
+ * x402 protocol client: initialise an EVM signer and an x402Client.
3
3
  */
4
4
  import { x402Client, wrapAxiosWithPayment, x402HTTPClient } from "@aeon-ai-pay/axios";
5
5
  import { registerExactEvmScheme } from "@aeon-ai-pay/evm/exact/client";
@@ -11,8 +11,8 @@ import { BSC_RPC_URL } from "./constants.mjs";
11
11
  import axios from "axios";
12
12
 
13
13
  /**
14
- * 创建已注册 EVM 签名的 x402 axios 客户端
15
- * @param {`0x${string}`} privateKey - EVM 私钥
14
+ * Build an x402 axios client with the EVM signer pre-registered.
15
+ * @param {`0x${string}`} privateKey - EVM private key
16
16
  * @returns {{ api: AxiosInstance, client: x402Client, address: string, getOrderNo: () => string|null }}
17
17
  */
18
18
  export function createX402Api(privateKey) {
@@ -39,8 +39,9 @@ export function createX402Api(privateKey) {
39
39
 
40
40
  const axiosInstance = axios.create();
41
41
 
42
- // wrapAxiosWithPayment 之前注册拦截器,
43
- // 402 响应体中捕获 orderNo(服务端在 firstRequest 返回)
42
+ // Register the interceptor *before* wrapAxiosWithPayment so it can
43
+ // capture orderNo from the 402 response body (the server returns it
44
+ // on the first request).
44
45
  let capturedOrderNo = null;
45
46
  axiosInstance.interceptors.response.use(
46
47
  (response) => response,
@@ -63,11 +64,13 @@ export function createX402Api(privateKey) {
63
64
  }
64
65
 
65
66
  /**
66
- * 第一次发起 x402 请求(不带签名),从 402 响应中提取实际付款要求。
67
- * 同时保留完整的 402 响应数据和原始请求配置,供后续手动签名使用。
68
- * 字段名与 x402 v2 PaymentRequirements 标准对齐:asset、payTo、amount。
67
+ * Send the first x402 request (unsigned) and extract the real payment requirements
68
+ * from the 402 response.
69
+ * Also keeps the raw 402 response and the original request config so the caller can
70
+ * sign manually later.
71
+ * Field names follow the x402 v2 PaymentRequirements standard: asset, payTo, amount.
69
72
  *
70
- * 兼容 GETcard 路径)和 POSTimage / Skill Boss 路径)。
73
+ * Supports both GET (card path) and POST (image / Skill Boss path).
71
74
  *
72
75
  * @param {string} url
73
76
  * @param {{ method?: "GET"|"POST", data?: any, headers?: object }} [options]
@@ -105,7 +108,7 @@ export async function fetchPaymentRequirements(url, options = {}) {
105
108
  }
106
109
 
107
110
  /**
108
- * 从响应头中解码 PAYMENT-RESPONSEx402 v2
111
+ * Decode the PAYMENT-RESPONSE response header (x402 v2).
109
112
  * @param {object} headers - axios response headers
110
113
  * @returns {object|null}
111
114
  */