@aeon-ai-pay/aigateway 0.1.5 → 0.2.1
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 +22 -0
- package/bin/cli.mjs +20 -32
- package/docs/exit-codes.md +2 -1
- package/docs/release-process.md +9 -7
- package/package.json +1 -1
- package/skills/aigateway/SKILL.md +369 -267
- package/src/catalog.mjs +38 -0
- package/src/commands/sb-invoke.mjs +407 -0
- package/src/commands/sb-tools.mjs +37 -0
- package/src/commands/wallet-init.mjs +1 -3
- package/src/config.mjs +21 -22
- package/src/error-codes.mjs +11 -3
- package/src/funding.mjs +2 -2
- package/src/inputs-validator.mjs +125 -0
- package/src/output.mjs +1 -1
- package/src/tools-download.mjs +264 -0
- package/src/update-check.mjs +50 -47
- package/src/x402.mjs +1 -1
- package/skills/aigateway/references/check-status.md +0 -68
- package/skills/aigateway/references/create-card.md +0 -114
- package/skills/aigateway/references/store.md +0 -87
- package/src/commands/create-card-status.mjs +0 -67
- package/src/commands/create-card.mjs +0 -352
- package/src/commands/create-image.mjs +0 -428
- package/src/sanitize.mjs +0 -48
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
# Create Virtual Card
|
|
2
|
-
|
|
3
|
-
## Prerequisites
|
|
4
|
-
|
|
5
|
-
Before creating a card, confirm the following:
|
|
6
|
-
|
|
7
|
-
1. Wallet is configured — run `wallet-init`. If not ready, the CLI will auto-create one.
|
|
8
|
-
2. Service URL is configured (built-in default is available; no action needed unless user wants to override)
|
|
9
|
-
3. The `create` command automatically checks allowance and wallet balance before payment. If insufficient, it auto-initiates WalletConnect funding — no need to run `wallet` or `wallet-topup` separately.
|
|
10
|
-
|
|
11
|
-
## Workflow
|
|
12
|
-
|
|
13
|
-
### Step 1: Confirm Amount
|
|
14
|
-
|
|
15
|
-
Ask the user how much to load onto the virtual card.
|
|
16
|
-
|
|
17
|
-
- Amount limits are enforced by the CLI (`amountLimits.min` ~ `amountLimits.max`, from `wallet-init`).
|
|
18
|
-
- Currency: USD (server handles crypto conversion)
|
|
19
|
-
|
|
20
|
-
**If user does not specify an amount**, show the valid range and ask for confirmation (**copy must be verbatim**, variable substitution only):
|
|
21
|
-
> "You can create a card of up to ${min}~${max}. How much would you like to load onto the card?"
|
|
22
|
-
|
|
23
|
-
Once the user specifies an amount, **execute immediately** — no second confirmation needed.
|
|
24
|
-
|
|
25
|
-
### Step 2: Execute
|
|
26
|
-
|
|
27
|
-
```bash
|
|
28
|
-
# Create card and auto-poll status
|
|
29
|
-
npx @aeon-ai-pay/aigateway create --amount <amount> --poll
|
|
30
|
-
|
|
31
|
-
# Optional: specify merchant app ID (defaults to TEST000001)
|
|
32
|
-
npx @aeon-ai-pay/aigateway create --amount <amount> --app-id <merchantAppId> --poll
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
> `--app-id` is the merchant identifier sent with the request; defaults to `TEST000001` when omitted.
|
|
36
|
-
|
|
37
|
-
CLI automatically handles the full flow:
|
|
38
|
-
1. Send `GET /open/ai/x402/card/create?amount=X&appId=Y` → receive HTTP 402 + payment requirements (exact USDT amount)
|
|
39
|
-
2. Check allowance → if insufficient and no BNB, mark BNB needed
|
|
40
|
-
3. Check USDT balance → if insufficient, mark top-up needed
|
|
41
|
-
4. If top-up or BNB needed → auto-initiate WalletConnect funding (opens QR page, waits for user to confirm in wallet app)
|
|
42
|
-
5. After funding completes, auto-continue
|
|
43
|
-
6. Approve authorization (only on first use or when allowance insufficient, costs small amount of BNB)
|
|
44
|
-
7. Sign with the exact amount from the first 402 response using EIP-712
|
|
45
|
-
8. Retry request with `PAYMENT-SIGNATURE` header → receive HTTP 200
|
|
46
|
-
9. With `--poll`, polls up to 42 times (first 5 at 2-second intervals, then every 5 seconds) until card is ready
|
|
47
|
-
|
|
48
|
-
### Step 3: Parse Result
|
|
49
|
-
|
|
50
|
-
**stdout** outputs JSON (parseable), **stderr** outputs progress logs.
|
|
51
|
-
|
|
52
|
-
Successful output:
|
|
53
|
-
```json
|
|
54
|
-
{
|
|
55
|
-
"success": true,
|
|
56
|
-
"data": {
|
|
57
|
-
"code": "0",
|
|
58
|
-
"msg": "success",
|
|
59
|
-
"model": { "orderNo": "300217748668047431791" }
|
|
60
|
-
},
|
|
61
|
-
"paymentResponse": {
|
|
62
|
-
"txHash": "0x...",
|
|
63
|
-
"networkId": "eip155:56"
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
With `--poll`, additional output after card is ready:
|
|
69
|
-
```json
|
|
70
|
-
{
|
|
71
|
-
"pollResult": {
|
|
72
|
-
"orderNo": "300217748668047431791",
|
|
73
|
-
"orderStatus": "SUCCESS",
|
|
74
|
-
"channelStatus": "COMPLETED",
|
|
75
|
-
"orderAmount": 0.6,
|
|
76
|
-
"txHash": "0xabc...def",
|
|
77
|
-
"cardLastFour": "4321",
|
|
78
|
-
"cardBin": "485932",
|
|
79
|
-
"cardScheme": "VISA",
|
|
80
|
-
"cardBalance": 0.6,
|
|
81
|
-
"cardStatus": "ACTIVE"
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
### Step 4: Present to User
|
|
87
|
-
|
|
88
|
-
Fetching card details may take about 30 seconds. Output a waiting prompt first (**copy must be verbatim**):
|
|
89
|
-
```
|
|
90
|
-
> Fetching card details, please wait...
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
Once details are returned, on success (**copy must be verbatim**, variable substitution only):
|
|
94
|
-
```
|
|
95
|
-
Order No: {orderNo}
|
|
96
|
-
Card: {cardScheme} •••• {last4}
|
|
97
|
-
State: Active
|
|
98
|
-
Remaining balance: ${amount} USD
|
|
99
|
-
Usage: 0 / 1 (single-use)
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
Save the `orderNo` for subsequent status queries.
|
|
103
|
-
|
|
104
|
-
## Error Handling
|
|
105
|
-
|
|
106
|
-
| Scenario | CLI Output | Action |
|
|
107
|
-
|------|---------|---------|
|
|
108
|
-
| Amount out of range | Error JSON with allowed range | Relay to user |
|
|
109
|
-
| Wallet not configured | `Wallet not configured` | Run `wallet-init` |
|
|
110
|
-
| Funding signature timeout (5 min) | `Payment approval timed out. Please try again.` | Relay to user, ask if they want to retry |
|
|
111
|
-
| User rejected signature | `Payment approval was rejected. Please try again if you'd like to proceed.` | Relay to user, do not auto-retry |
|
|
112
|
-
| Insufficient balance after funding | `Still insufficient USDT after funding` | Relay to user |
|
|
113
|
-
| Network error | Server error JSON | Retry once, then report to user |
|
|
114
|
-
| Transaction reverted | txHash | Suggest user check on BSCScan |
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
# AEON AI Card Store
|
|
2
|
-
|
|
3
|
-
## When to use
|
|
4
|
-
|
|
5
|
-
Trigger this reference when the user says:
|
|
6
|
-
- "what can I buy"
|
|
7
|
-
- "show me what's available"
|
|
8
|
-
- "what can I do?"
|
|
9
|
-
- "what can I use the card for"
|
|
10
|
-
|
|
11
|
-
Present options conversationally. Do not dump the full list — highlight what's most relevant.
|
|
12
|
-
|
|
13
|
-
---
|
|
14
|
-
|
|
15
|
-
## Virtual Card Use Cases
|
|
16
|
-
|
|
17
|
-
### AEON Agent Card – Supported & Upcoming Use Cases
|
|
18
|
-
|
|
19
|
-
**Coming Soon**
|
|
20
|
-
- Subscriptions (ChatGPT, Claude, Midjourney)
|
|
21
|
-
- Ads (Google, Meta)
|
|
22
|
-
- Travel bookings
|
|
23
|
-
- SaaS tools
|
|
24
|
-
|
|
25
|
-
**Tell us what you want**
|
|
26
|
-
|
|
27
|
-
Submit your request here:
|
|
28
|
-
👉 [Google Form link]
|
|
29
|
-
|
|
30
|
-
---
|
|
31
|
-
|
|
32
|
-
## x402 API Payments
|
|
33
|
-
|
|
34
|
-
Pay these services directly using `x402 fetch`. No card needed — payment goes straight from your wallet.
|
|
35
|
-
|
|
36
|
-
### AI & Data
|
|
37
|
-
|
|
38
|
-
| Service | What it does | Chain |
|
|
39
|
-
|---|---|---|
|
|
40
|
-
| AskClaude | Per-question Claude AI access ($0.01–$0.10/query) | Base |
|
|
41
|
-
| Arch AI Tools | 53 AI tools: web search, image generation, fact-checking | Base |
|
|
42
|
-
| Firecrawl | Web scraping and LLM-ready content extraction | Base |
|
|
43
|
-
| Gloria AI | Real-time news data for agents | Base |
|
|
44
|
-
| Minifetch | Web metadata and content summaries | Base |
|
|
45
|
-
|
|
46
|
-
### Blockchain & DeFi
|
|
47
|
-
|
|
48
|
-
| Service | What it does | Chain |
|
|
49
|
-
|---|---|---|
|
|
50
|
-
| Messari | Crypto research and on-chain data | Base |
|
|
51
|
-
| Nansen | Wallet intelligence and blockchain analytics | Base |
|
|
52
|
-
| DiamondClaws | DeFi yield scoring and protocol risk analysis | Base |
|
|
53
|
-
| Stakevia | Solana validator intelligence | Solana |
|
|
54
|
-
|
|
55
|
-
### Infrastructure
|
|
56
|
-
|
|
57
|
-
| Service | What it does | Chain |
|
|
58
|
-
|---|---|---|
|
|
59
|
-
| Pinata | IPFS file uploads and retrievals, no account required | Base |
|
|
60
|
-
| Run402 | Postgres databases and serverless functions, no signup | Base |
|
|
61
|
-
| Alchemy | Pay-per-request RPC / blockchain API access | Base |
|
|
62
|
-
| Robtex | DNS and network intelligence APIs | Base |
|
|
63
|
-
|
|
64
|
-
### Payments & Commerce
|
|
65
|
-
|
|
66
|
-
| Service | What it does | Chain |
|
|
67
|
-
|---|---|---|
|
|
68
|
-
| Bitrefill | Buy gift cards and prepaid cards with crypto | Base |
|
|
69
|
-
| ClawCredit | Access x402 services on credit, pay later | Base |
|
|
70
|
-
|
|
71
|
-
Full registry: [x402.org/ecosystem](https://www.x402.org/ecosystem) · [x402list.fun](https://x402list.fun)
|
|
72
|
-
|
|
73
|
-
---
|
|
74
|
-
|
|
75
|
-
## How to Present to the User
|
|
76
|
-
|
|
77
|
-
Example response when user has no specific intent:
|
|
78
|
-
|
|
79
|
-
> "Here's what you can do with AEON AI Card:
|
|
80
|
-
>
|
|
81
|
-
> - **Virtual card** — coming soon: subscribe to ChatGPT, Claude, Midjourney, run ads, book travel
|
|
82
|
-
> - **Pay AI APIs** — call Claude, Firecrawl, web search per request via x402
|
|
83
|
-
> - **Access DeFi data** — Nansen, Messari on-chain analytics via x402
|
|
84
|
-
>
|
|
85
|
-
> What would you like to do?"
|
|
86
|
-
|
|
87
|
-
Adapt based on context. For virtual card intent → route to create-card flow. For x402 API intent → route to x402 flow.
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { resolve } from "../config.mjs";
|
|
2
|
-
import { POLL_INTERVAL, MAX_POLLS } from "../constants.mjs";
|
|
3
|
-
import { sanitizeOutput } from "../sanitize.mjs";
|
|
4
|
-
import { emitOk, emitErr, logInfo } from "../output.mjs";
|
|
5
|
-
|
|
6
|
-
export async function status(opts) {
|
|
7
|
-
const { default: axios } = await import("axios");
|
|
8
|
-
const serviceUrl = resolve(opts.serviceUrl, "AIGATEWAY_SERVICE_URL", "serviceUrl");
|
|
9
|
-
const { orderNo, poll, appId } = opts;
|
|
10
|
-
|
|
11
|
-
if (!serviceUrl) {
|
|
12
|
-
emitErr("create-card-status", "SERVICE_URL_MISSING", {
|
|
13
|
-
message: "Missing service URL. Set env AIGATEWAY_SERVICE_URL if you need to override the built-in default.",
|
|
14
|
-
appId,
|
|
15
|
-
});
|
|
16
|
-
return;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const url = `${serviceUrl}/open/ai/x402/card/status?orderNo=${encodeURIComponent(orderNo)}&appId=${encodeURIComponent(appId)}`;
|
|
20
|
-
|
|
21
|
-
if (!poll) {
|
|
22
|
-
try {
|
|
23
|
-
const res = await axios.get(url);
|
|
24
|
-
const sanitized = sanitizeOutput(res.data);
|
|
25
|
-
emitOk("create-card-status", { appId, ...sanitized }, sanitized);
|
|
26
|
-
} catch (error) {
|
|
27
|
-
emitErr("create-card-status", "SERVICE_UNAVAILABLE", {
|
|
28
|
-
message: error.message,
|
|
29
|
-
status: error.response?.status,
|
|
30
|
-
data: error.response?.data,
|
|
31
|
-
appId,
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
logInfo(`Polling ${url} every ${POLL_INTERVAL / 1000}s (max ${MAX_POLLS} times)`);
|
|
38
|
-
|
|
39
|
-
for (let i = 1; i <= MAX_POLLS; i++) {
|
|
40
|
-
try {
|
|
41
|
-
const res = await axios.get(url);
|
|
42
|
-
const model = res.data?.model;
|
|
43
|
-
|
|
44
|
-
logInfo(
|
|
45
|
-
`[${i}/${MAX_POLLS}] orderStatus=${model?.orderStatus} channelStatus=${model?.channelStatus} cardStatus=${model?.cardStatus || "-"}`,
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
if (model?.orderStatus === "SUCCESS" || model?.orderStatus === "FAIL") {
|
|
49
|
-
const sanitized = sanitizeOutput(res.data);
|
|
50
|
-
emitOk("create-card-status", { appId, ...sanitized }, sanitized);
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
} catch (e) {
|
|
54
|
-
logInfo(`[${i}/${MAX_POLLS}] Error: ${e.message}`);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (i < MAX_POLLS) {
|
|
58
|
-
await new Promise((r) => setTimeout(r, POLL_INTERVAL));
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
emitErr("create-card-status", "POLL_TIMEOUT", {
|
|
63
|
-
orderNo,
|
|
64
|
-
appId,
|
|
65
|
-
message: "Polling timeout. Card may still be provisioning.",
|
|
66
|
-
});
|
|
67
|
-
}
|
|
@@ -1,352 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* create-card: issue a one-time virtual card by paying with USDT on BSC over the x402 protocol.
|
|
3
|
-
*
|
|
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
|
-
*/
|
|
9
|
-
import { createX402Api, decodePaymentResponse, fetchPaymentRequirements } from "../x402.mjs";
|
|
10
|
-
import { resolve } from "../config.mjs";
|
|
11
|
-
import { getWalletBalance, getAllowance } from "../balance.mjs";
|
|
12
|
-
import { sanitizeOutput } from "../sanitize.mjs";
|
|
13
|
-
import axios from "axios";
|
|
14
|
-
import {
|
|
15
|
-
MIN_AMOUNT, MAX_AMOUNT, POLL_INTERVAL, MAX_POLLS,
|
|
16
|
-
} from "../constants.mjs";
|
|
17
|
-
import { WalletConnectError } from "../walletconnect.mjs";
|
|
18
|
-
import {
|
|
19
|
-
fundSessionKey,
|
|
20
|
-
promptTopupAmount,
|
|
21
|
-
MIN_TOPUP_USDT,
|
|
22
|
-
TOPUP_PRESETS,
|
|
23
|
-
} from "../funding.mjs";
|
|
24
|
-
import { emitOk, emitErr, logInfo } from "../output.mjs";
|
|
25
|
-
|
|
26
|
-
export async function createCard(opts) {
|
|
27
|
-
logInfo("Creating Agent Card...");
|
|
28
|
-
const serviceUrl = resolve(opts.serviceUrl, "AIGATEWAY_SERVICE_URL", "serviceUrl");
|
|
29
|
-
const privateKey = resolve(opts.privateKey, "EVM_PRIVATE_KEY", "privateKey");
|
|
30
|
-
const { amount, poll, appId, dryRun } = opts;
|
|
31
|
-
const amountNum = parseFloat(amount);
|
|
32
|
-
|
|
33
|
-
if (!serviceUrl) {
|
|
34
|
-
emitErr("create-card", "SERVICE_URL_MISSING", {
|
|
35
|
-
message: "Missing service URL. Set env AIGATEWAY_SERVICE_URL if you need to override the built-in default.",
|
|
36
|
-
appId,
|
|
37
|
-
});
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
if (!privateKey) {
|
|
41
|
-
emitErr("create-card", "WALLET_NOT_CONFIGURED", { appId });
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
if (isNaN(amountNum) || amountNum < MIN_AMOUNT) {
|
|
45
|
-
emitErr("create-card", "AMOUNT_OUT_OF_RANGE", {
|
|
46
|
-
message: `Amount must be at least $${MIN_AMOUNT}. Allowed range: $${MIN_AMOUNT} ~ $${MAX_AMOUNT} USD.`,
|
|
47
|
-
min: MIN_AMOUNT,
|
|
48
|
-
max: MAX_AMOUNT,
|
|
49
|
-
appId,
|
|
50
|
-
});
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
if (amountNum > MAX_AMOUNT) {
|
|
54
|
-
emitErr("create-card", "AMOUNT_OUT_OF_RANGE", {
|
|
55
|
-
message: `Amount must not exceed $${MAX_AMOUNT}. Allowed range: $${MIN_AMOUNT} ~ $${MAX_AMOUNT} USD.`,
|
|
56
|
-
min: MIN_AMOUNT,
|
|
57
|
-
max: MAX_AMOUNT,
|
|
58
|
-
appId,
|
|
59
|
-
});
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const url = `${serviceUrl}/open/ai/x402/card/create?amount=${encodeURIComponent(amount)}&appId=${encodeURIComponent(appId)}`;
|
|
64
|
-
logInfo("Fetching payment requirements...");
|
|
65
|
-
let requiredUsdt;
|
|
66
|
-
let paymentReq;
|
|
67
|
-
try {
|
|
68
|
-
paymentReq = await fetchPaymentRequirements(url);
|
|
69
|
-
requiredUsdt = paymentReq.amountUsdt;
|
|
70
|
-
logInfo(`Required: ${requiredUsdt} USDT (pay to ${paymentReq.payTo})`);
|
|
71
|
-
} catch (e) {
|
|
72
|
-
emitErr("create-card", "PAYMENT_FETCH_FAILED", {
|
|
73
|
-
message: `Failed to fetch payment requirements: ${e.message}`,
|
|
74
|
-
appId,
|
|
75
|
-
});
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
logInfo("Checking wallet...");
|
|
80
|
-
let needTopup = false;
|
|
81
|
-
let needGas = false;
|
|
82
|
-
let sessionAddress;
|
|
83
|
-
let topupAmount = null;
|
|
84
|
-
let balanceInitialUsdt = null;
|
|
85
|
-
let balanceBeforeChargeUsdt = null;
|
|
86
|
-
|
|
87
|
-
try {
|
|
88
|
-
const { address, usdt, bnb, bnbRaw } = await getWalletBalance(privateKey);
|
|
89
|
-
sessionAddress = address;
|
|
90
|
-
balanceInitialUsdt = usdt;
|
|
91
|
-
balanceBeforeChargeUsdt = usdt;
|
|
92
|
-
const usdtNum = parseFloat(usdt);
|
|
93
|
-
logInfo(`Wallet: ${address}`);
|
|
94
|
-
logInfo(`Balance: ${usdt} USDT, ${bnb} BNB`);
|
|
95
|
-
|
|
96
|
-
const allowance = await getAllowance(address);
|
|
97
|
-
const requiredWei = BigInt(paymentReq.amountWei);
|
|
98
|
-
if (requiredWei === 0n) {
|
|
99
|
-
emitErr("create-card", "INVALID_PAYMENT_AMOUNT", {
|
|
100
|
-
message: "Server returned invalid payment amount (0). Please retry later.",
|
|
101
|
-
appId,
|
|
102
|
-
});
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
if (allowance >= requiredWei) {
|
|
106
|
-
logInfo("Allowance sufficient, no approve needed.");
|
|
107
|
-
} else {
|
|
108
|
-
logInfo(`Allowance ${allowance} < required ${requiredWei}; approve needed.`);
|
|
109
|
-
if (bnbRaw === 0n) {
|
|
110
|
-
needGas = true;
|
|
111
|
-
logInfo("No BNB for approve gas, will request BNB transfer.");
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (usdtNum < requiredUsdt) {
|
|
116
|
-
needTopup = true;
|
|
117
|
-
const shortfall = requiredUsdt - usdtNum;
|
|
118
|
-
const minTopup = Math.max(MIN_TOPUP_USDT, Math.ceil(shortfall));
|
|
119
|
-
logInfo(`USDT insufficient: have ${usdtNum}, need ${requiredUsdt}, shortfall ${shortfall.toFixed(6)} (top-up minimum: ${minTopup} USDT)`);
|
|
120
|
-
|
|
121
|
-
if (opts.topupAmount != null && String(opts.topupAmount).trim() !== "") {
|
|
122
|
-
const amt = Number(opts.topupAmount);
|
|
123
|
-
if (!Number.isFinite(amt) || amt <= 0) {
|
|
124
|
-
emitErr("create-card", "AMOUNT_INVALID", {
|
|
125
|
-
message: `Invalid --topup-amount: ${opts.topupAmount}`,
|
|
126
|
-
appId,
|
|
127
|
-
});
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
if (amt < minTopup) {
|
|
131
|
-
emitErr("create-card", "TOPUP_AMOUNT_TOO_SMALL", {
|
|
132
|
-
message: `--topup-amount ${amt} USDT is below the ${minTopup} USDT minimum for this call.`,
|
|
133
|
-
minTopup,
|
|
134
|
-
appId,
|
|
135
|
-
});
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
topupAmount = String(opts.topupAmount);
|
|
139
|
-
logInfo(`Using --topup-amount: ${topupAmount} USDT`);
|
|
140
|
-
} else if (process.stdin.isTTY) {
|
|
141
|
-
topupAmount = await promptTopupAmount(minTopup);
|
|
142
|
-
logInfo(`Selected top-up amount: ${topupAmount} USDT`);
|
|
143
|
-
} else {
|
|
144
|
-
const presets = TOPUP_PRESETS.filter((v) => v >= minTopup);
|
|
145
|
-
emitErr("create-card", "TOPUP_REQUIRED", {
|
|
146
|
-
message: `USDT balance is below the ${minTopup} USDT minimum for this call. Choose a top-up amount and rerun with --topup-amount <usdt>.`,
|
|
147
|
-
minTopup,
|
|
148
|
-
required: requiredUsdt,
|
|
149
|
-
currentBalance: balanceInitialUsdt,
|
|
150
|
-
address: sessionAddress,
|
|
151
|
-
appId,
|
|
152
|
-
presets,
|
|
153
|
-
hint: `Rerun: aigateway wallet-topup --amount <usdt> --app-id ${appId}`,
|
|
154
|
-
});
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
} catch (e) {
|
|
159
|
-
emitErr("create-card", "BALANCE_CHECK_FAILED", {
|
|
160
|
-
message: `Balance check failed: ${e.message}`,
|
|
161
|
-
appId,
|
|
162
|
-
});
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// Dry-run: exit after preflight checks complete
|
|
167
|
-
if (dryRun) {
|
|
168
|
-
const preview = {
|
|
169
|
-
dryRun: true,
|
|
170
|
-
appId,
|
|
171
|
-
url,
|
|
172
|
-
paymentRequirements: {
|
|
173
|
-
amountUsdt: requiredUsdt,
|
|
174
|
-
amountWei: paymentReq.amountWei,
|
|
175
|
-
asset: paymentReq.asset,
|
|
176
|
-
payTo: paymentReq.payTo,
|
|
177
|
-
orderNo: paymentReq.orderNo,
|
|
178
|
-
},
|
|
179
|
-
wallet: { address: sessionAddress },
|
|
180
|
-
decision: { needTopup, needGas, topupAmount },
|
|
181
|
-
will: [
|
|
182
|
-
...(needTopup ? ["fund_usdt_via_walletconnect"] : []),
|
|
183
|
-
...(needGas ? ["fund_bnb_via_walletconnect"] : []),
|
|
184
|
-
"approve_or_skip",
|
|
185
|
-
"sign_payment_eip712",
|
|
186
|
-
"submit_to_facilitator",
|
|
187
|
-
...(poll ? ["poll_status"] : []),
|
|
188
|
-
],
|
|
189
|
-
};
|
|
190
|
-
emitOk("create-card", preview, { success: true, ...preview });
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// WalletConnect top-up
|
|
195
|
-
if (needTopup || needGas) {
|
|
196
|
-
logInfo("Funding flow triggered...");
|
|
197
|
-
try {
|
|
198
|
-
await fundSessionKey({
|
|
199
|
-
sessionAddress,
|
|
200
|
-
usdtAmount: needTopup ? topupAmount : null,
|
|
201
|
-
needGas,
|
|
202
|
-
});
|
|
203
|
-
} catch (e) {
|
|
204
|
-
if (e instanceof WalletConnectError) {
|
|
205
|
-
emitErr("create-card", e.code, { message: e.message, address: sessionAddress, appId });
|
|
206
|
-
} else {
|
|
207
|
-
emitErr("create-card", "FUNDING_FAILED", { message: e.message, address: sessionAddress, appId });
|
|
208
|
-
}
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
logInfo("Re-checking wallet balance...");
|
|
213
|
-
try {
|
|
214
|
-
const { usdt, bnbRaw } = await getWalletBalance(privateKey);
|
|
215
|
-
balanceBeforeChargeUsdt = usdt;
|
|
216
|
-
const usdtNum = parseFloat(usdt);
|
|
217
|
-
if (needGas && bnbRaw === 0n) {
|
|
218
|
-
emitErr("create-card", "INSUFFICIENT_BNB", {
|
|
219
|
-
message: "No BNB for approve transaction after funding. Run 'aigateway wallet-gas' to add BNB manually.",
|
|
220
|
-
address: sessionAddress,
|
|
221
|
-
appId,
|
|
222
|
-
});
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
|
-
if (usdtNum < requiredUsdt) {
|
|
226
|
-
emitErr("create-card", "INSUFFICIENT_USDT", {
|
|
227
|
-
message: "Still insufficient USDT after funding.",
|
|
228
|
-
required: `${requiredUsdt} USDT`,
|
|
229
|
-
available: `${usdt} USDT`,
|
|
230
|
-
address: sessionAddress,
|
|
231
|
-
appId,
|
|
232
|
-
});
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
} catch (e) {
|
|
236
|
-
emitErr("create-card", "BALANCE_CHECK_FAILED", {
|
|
237
|
-
message: `Balance re-check failed: ${e.message}`,
|
|
238
|
-
appId,
|
|
239
|
-
});
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
const { client } = createX402Api(privateKey);
|
|
245
|
-
logInfo(`Creating card: $${amount} USD via ${url}`);
|
|
246
|
-
|
|
247
|
-
try {
|
|
248
|
-
const { x402HTTPClient } = await import("@aeon-ai-pay/core/client");
|
|
249
|
-
const httpClient = new x402HTTPClient(client);
|
|
250
|
-
|
|
251
|
-
const raw402 = paymentReq.raw402Response;
|
|
252
|
-
const getHeader = (name) => {
|
|
253
|
-
const value = raw402.headers[name] ?? raw402.headers[name.toLowerCase()];
|
|
254
|
-
return typeof value === "string" ? value : undefined;
|
|
255
|
-
};
|
|
256
|
-
const paymentRequired = httpClient.getPaymentRequiredResponse(getHeader, raw402.data);
|
|
257
|
-
const paymentPayload = await client.createPaymentPayload(paymentRequired);
|
|
258
|
-
const paymentHeaders = httpClient.encodePaymentSignatureHeader(paymentPayload);
|
|
259
|
-
|
|
260
|
-
const response = await axios.get(url, {
|
|
261
|
-
headers: { ...paymentHeaders, "Access-Control-Expose-Headers": "PAYMENT-RESPONSE" },
|
|
262
|
-
});
|
|
263
|
-
const paymentResponse = decodePaymentResponse(response.headers);
|
|
264
|
-
const orderNo = paymentReq.orderNo || response.data?.model?.orderNo || response.data?.orderNo;
|
|
265
|
-
|
|
266
|
-
let balanceAfterUsdt = null;
|
|
267
|
-
try {
|
|
268
|
-
const after = await getWalletBalance(privateKey);
|
|
269
|
-
balanceAfterUsdt = after.usdt;
|
|
270
|
-
} catch (e) {
|
|
271
|
-
logInfo(`Post-payment balance check failed: ${e.message}`);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
const sanitizedData = sanitizeOutput(response.data);
|
|
275
|
-
const successData = {
|
|
276
|
-
appId,
|
|
277
|
-
orderNo,
|
|
278
|
-
amount,
|
|
279
|
-
data: sanitizedData,
|
|
280
|
-
paymentResponse,
|
|
281
|
-
balance: {
|
|
282
|
-
initial: balanceInitialUsdt,
|
|
283
|
-
before: balanceBeforeChargeUsdt,
|
|
284
|
-
after: balanceAfterUsdt,
|
|
285
|
-
charged: requiredUsdt,
|
|
286
|
-
topup: topupAmount,
|
|
287
|
-
},
|
|
288
|
-
};
|
|
289
|
-
|
|
290
|
-
function findCardStatus(obj) {
|
|
291
|
-
if (!obj || typeof obj !== 'object') return null;
|
|
292
|
-
if (obj.cardStatus) return obj.cardStatus;
|
|
293
|
-
for (const v of Object.values(obj)) {
|
|
294
|
-
const found = findCardStatus(v);
|
|
295
|
-
if (found) return found;
|
|
296
|
-
}
|
|
297
|
-
return null;
|
|
298
|
-
}
|
|
299
|
-
const initialOrderStatus = response.data?.model?.orderStatus;
|
|
300
|
-
const initialCardStatus = findCardStatus(response.data);
|
|
301
|
-
const cardReady = initialOrderStatus === "SUCCESS" || initialOrderStatus === "FAIL" || initialCardStatus === "ACTIVE";
|
|
302
|
-
|
|
303
|
-
if (cardReady) {
|
|
304
|
-
logInfo(`Card ready (orderStatus=${initialOrderStatus}, cardStatus=${initialCardStatus}), no polling needed.`);
|
|
305
|
-
emitOk("create-card", successData, { success: true, ...successData });
|
|
306
|
-
return;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
if (poll && orderNo) {
|
|
310
|
-
logInfo(`\nPolling status for orderNo: ${orderNo}`);
|
|
311
|
-
const pollResult = await pollStatus(serviceUrl, orderNo, appId);
|
|
312
|
-
successData.pollResult = pollResult;
|
|
313
|
-
emitOk("create-card", successData, { success: true, ...successData, pollResult });
|
|
314
|
-
return;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
if (poll && !orderNo) {
|
|
318
|
-
logInfo("Warning: No orderNo available for polling. Query status manually.");
|
|
319
|
-
}
|
|
320
|
-
emitOk("create-card", successData, { success: true, ...successData });
|
|
321
|
-
} catch (error) {
|
|
322
|
-
emitErr("create-card", "PAYMENT_FAILED", {
|
|
323
|
-
message: error.message,
|
|
324
|
-
status: error.response?.status,
|
|
325
|
-
data: error.response?.data,
|
|
326
|
-
appId,
|
|
327
|
-
});
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
async function pollStatus(serviceUrl, orderNo, appId) {
|
|
332
|
-
for (let i = 1; i <= MAX_POLLS; i++) {
|
|
333
|
-
if (i > 1) {
|
|
334
|
-
const delay = i <= 5 ? 2000 : POLL_INTERVAL;
|
|
335
|
-
await new Promise((r) => setTimeout(r, delay));
|
|
336
|
-
}
|
|
337
|
-
try {
|
|
338
|
-
const res = await axios.get(
|
|
339
|
-
`${serviceUrl}/open/ai/x402/card/status?orderNo=${encodeURIComponent(orderNo)}&appId=${encodeURIComponent(appId)}`,
|
|
340
|
-
);
|
|
341
|
-
const model = res.data?.model;
|
|
342
|
-
logInfo(`[${i}/${MAX_POLLS}] orderStatus=${model?.orderStatus} channelStatus=${model?.channelStatus}`);
|
|
343
|
-
if (model?.orderStatus === "SUCCESS" || model?.orderStatus === "FAIL" || model?.cardStatus === "ACTIVE") {
|
|
344
|
-
return sanitizeOutput(model);
|
|
345
|
-
}
|
|
346
|
-
} catch (e) {
|
|
347
|
-
logInfo(`[${i}/${MAX_POLLS}] Poll error: ${e.message}`);
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
logInfo(`Polling timeout after ${MAX_POLLS} attempts. Check manually with: aigateway create-card-status --order-no ${orderNo}`);
|
|
351
|
-
return null;
|
|
352
|
-
}
|