@quackai/q402-mcp 0.7.1 → 0.7.3
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 +49 -66
- package/dist/index.js +214 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,16 +9,14 @@
|
|
|
9
9
|
>
|
|
10
10
|
> **Trial-scope policy:** API keys minted under the free-trial program (`plan: "trial"`) are restricted to BNB Chain with USDC/USDT — server-side enforcement, returns `403 TRIAL_BNB_ONLY` otherwise. **Paid API keys see the full 9-chain matrix at all times.**
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
Quote → route → (optional) settle stablecoin payments across 9 EVM chains, from any MCP client. Recipient gets the full amount; sender pays $0 gas via [Q402](https://q402.quackai.ai)'s EIP-7702 relayer.
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
16
16
|
## Quick start
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
1. Register the MCP server with your client (one-line install per client).
|
|
21
|
-
2. Open your client and say: **"Set up Q402"**. The agent calls `q402_doctor` which walks you through creating a single secrets file at `~/.q402/mcp.env` and pasting in your API key + private key. Nothing else.
|
|
18
|
+
1. Register the server with your client (one-line per client).
|
|
19
|
+
2. Say **"Set up Q402"** to your agent. It runs `q402_doctor` → creates `~/.q402/mcp.env` → walks you through pasting keys.
|
|
22
20
|
|
|
23
21
|
### 1. Register the server
|
|
24
22
|
|
|
@@ -30,12 +28,12 @@ Two steps:
|
|
|
30
28
|
| **Cline** | Cline → Settings → MCP Servers → Edit JSON. Same shape as Cursor. |
|
|
31
29
|
| **Any other stdio MCP client** | Point it at `npx -y @quackai/q402-mcp`. No client-specific code. |
|
|
32
30
|
|
|
33
|
-
|
|
31
|
+
Secrets are NOT in this config. The server reads them from `~/.q402/mcp.env` (same pattern as AWS / Stripe / gh CLIs).
|
|
34
32
|
|
|
35
33
|
<details>
|
|
36
34
|
<summary>Windows: <code>codex mcp add</code> returns "Access is denied"</summary>
|
|
37
35
|
|
|
38
|
-
|
|
36
|
+
Some Windows setups block `codex.exe` from writing its own config. Add the stanza to `~/.codex/config.toml` by hand:
|
|
39
37
|
|
|
40
38
|
```toml
|
|
41
39
|
[mcp_servers.q402]
|
|
@@ -49,34 +47,30 @@ Then restart Codex. Same effect as `codex mcp add q402 -- npx -y @quackai/q402-m
|
|
|
49
47
|
|
|
50
48
|
### 2. First-time setup
|
|
51
49
|
|
|
52
|
-
Restart your client,
|
|
53
|
-
|
|
54
|
-
> *"Set up Q402"*
|
|
50
|
+
Restart your client, ask: > *"Set up Q402"*
|
|
55
51
|
|
|
56
|
-
The agent
|
|
52
|
+
The agent runs `q402_doctor`. On first install:
|
|
57
53
|
|
|
58
|
-
1.
|
|
59
|
-
2.
|
|
60
|
-
3.
|
|
61
|
-
4. Restart
|
|
54
|
+
1. Creates `~/.q402/mcp.env` (placeholders)
|
|
55
|
+
2. Opens it in your editor
|
|
56
|
+
3. Walks you through pasting an API key + a signing path **into the file, not into chat**
|
|
57
|
+
4. Restart + re-run `q402_doctor` to verify
|
|
62
58
|
|
|
63
|
-
🔒 **
|
|
59
|
+
🔒 **Keys never paste into chat.** Local modes sign on your machine; the key never leaves the device. Mode C (server-managed) needs no PK on the client.
|
|
64
60
|
|
|
65
61
|
### Pick a signing mode
|
|
66
62
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
| Mode | Env you set | Signer | Notes |
|
|
63
|
+
| Mode | Env | Signer | Notes |
|
|
70
64
|
|---|---|---|---|
|
|
71
|
-
| **A** | `Q402_PRIVATE_KEY` |
|
|
72
|
-
| **B** | `Q402_AGENTIC_PRIVATE_KEY` |
|
|
73
|
-
| **C** | (just an API key) |
|
|
65
|
+
| **A** | `Q402_PRIVATE_KEY` | MetaMask EOA, local | Simplest. Shows "Smart account" after first use (reversible via `q402_clear_delegation`). |
|
|
66
|
+
| **B** | `Q402_AGENTIC_PRIVATE_KEY` | Agent Wallet, local | Export PK from the [dashboard](https://q402.quackai.ai/dashboard) Agent tab. MetaMask untouched. |
|
|
67
|
+
| **C** | (just an API key) | Agent Wallet, server-managed | No PK on the client. One-shot pays accept Trial or Multichain keys; recurring needs Multichain on every chain (BNB included). |
|
|
74
68
|
|
|
75
|
-
When more than one
|
|
69
|
+
When more than one mode is set, `q402_pay` asks the user which to use. Picker: `walletMode = "agentic-server" \| "agentic-local" \| "eoa"`.
|
|
76
70
|
|
|
77
71
|
### Manual setup (no AI)
|
|
78
72
|
|
|
79
|
-
Create `~/.q402/mcp.env` yourself
|
|
73
|
+
Create `~/.q402/mcp.env` yourself with the template below. Live mode only flips when an API key + a signing path are populated, so saving the template as-is stays in sandbox. `Q402_ENABLE_REAL_PAYMENTS=0` forces sandbox even with real keys.
|
|
80
74
|
|
|
81
75
|
```bash
|
|
82
76
|
# ~/.q402/mcp.env
|
|
@@ -157,52 +151,43 @@ Then export the values in `~/.zshrc` / `~/.bashrc`. See the [Codex config refere
|
|
|
157
151
|
|
|
158
152
|
---
|
|
159
153
|
|
|
160
|
-
## Wallet modes — which signing path?
|
|
161
|
-
|
|
162
|
-
Three signing paths. Pick one when `q402_doctor` asks on first install. You can change later by editing `~/.q402/mcp.env` and restarting your client.
|
|
163
|
-
|
|
164
|
-
| Mode | Best for | Env to set | Private key in env? |
|
|
165
|
-
|---|---|---|---|
|
|
166
|
-
| **C — Server-managed** (recommended) | Most users. AI agents, automations, anyone who wants payments to "just work". Q402 holds an encrypted Agent Wallet for you; no MetaMask popup, no Smart-account marker. One-shot pays accept either key; **recurring schedules require the paid Multichain key on every chain (BNB included)**. | `Q402_MULTICHAIN_API_KEY` (paid, full surface) **or** `Q402_TRIAL_API_KEY` (free BNB, one-shot only) | **No** |
|
|
167
|
-
| **B — Local Agent Wallet PK** | You want Agent Wallet automation but prefer to hold the PK yourself. Export from the dashboard once. MCP signs locally — key never leaves your machine. Your MetaMask is never touched. | `Q402_AGENTIC_PRIVATE_KEY` + an API key | Yes (Agent Wallet's exported PK) |
|
|
168
|
-
| **A — Your own EOA** | Power users who want their existing MetaMask address to be the on-chain payer. Your EOA signs directly via EIP-7702; the "Smart account" marker after first use is normal + reversible with `q402_clear_delegation`. **Use a fresh wallet.** | `Q402_PRIVATE_KEY` + an API key | Yes (your EOA's PK) |
|
|
169
|
-
|
|
170
|
-
`q402_doctor` on first install reads your env and recommends one of these based on what's already configured. The default for an empty install is Mode C — simplest path. If multiple modes are configured at once (e.g. both `Q402_PRIVATE_KEY` and `Q402_AGENTIC_PRIVATE_KEY` set), `q402_pay` will ask which one to use before sending so the agent never silently picks the wrong wallet.
|
|
171
|
-
|
|
172
|
-
---
|
|
173
|
-
|
|
174
154
|
## Tools exposed
|
|
175
155
|
|
|
156
|
+
**16 tools** — read-only by default; live mode needs an API key + signing path + `Q402_ENABLE_REAL_PAYMENTS=1`.
|
|
157
|
+
|
|
176
158
|
| Tool | Auth | Purpose |
|
|
177
159
|
|---|---|---|
|
|
178
|
-
| `q402_doctor` | none |
|
|
179
|
-
| `q402_quote` | none | Compare gas
|
|
180
|
-
| `q402_balance` |
|
|
181
|
-
| `q402_pay` |
|
|
182
|
-
| `q402_batch_pay` |
|
|
183
|
-
| `q402_receipt` | none |
|
|
184
|
-
| `q402_wallet_status` | private key | Per-chain EIP-7702
|
|
185
|
-
| `
|
|
186
|
-
| `
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
`
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
>
|
|
160
|
+
| `q402_doctor` | none | First-install onboarding + ongoing health check (per-scope quota, EIP-7702 state, relay reachability, slot-mismatch warnings). |
|
|
161
|
+
| `q402_quote` | none | Compare gas + supported tokens across chains. |
|
|
162
|
+
| `q402_balance` | api key | Verify key + remaining quota. |
|
|
163
|
+
| `q402_pay` | live mode | Single-recipient gasless transfer. Sandbox by default. |
|
|
164
|
+
| `q402_batch_pay` | live mode | Up to 20 recipients per call (trial: 5). Same auto-routing as `q402_pay`. 6+ BNB batches with Trial set return `status="ambiguous"` so the agent asks how to split. xlayer + stable not batchable — use `q402_pay` in a loop. |
|
|
165
|
+
| `q402_receipt` | none | Fetch + locally verify a Trust Receipt (`rct_…` id, ECDSA against the relayer EOA). |
|
|
166
|
+
| `q402_wallet_status` | private key | Per-chain EIP-7702 state for the EOA derived from `Q402_PRIVATE_KEY`. |
|
|
167
|
+
| `q402_clear_delegation` | private key | Clear EIP-7702 delegation; Q402 sponsors the on-chain TX. |
|
|
168
|
+
| `q402_agentic_info` | api key | Agent Wallet info (addresses, per-wallet caps, daily-spend used, ERC-8004 id). Drives Mode C. |
|
|
169
|
+
| `q402_recurring_list` | api key | List scheduled rules. |
|
|
170
|
+
| `q402_recurring_create` | api key | Author a recurring rule. Paid Multichain on EVERY chain (BNB included). |
|
|
171
|
+
| `q402_recurring_fires` | api key | Last 50 fires per rule (timestamp + txHashes + amount). |
|
|
172
|
+
| `q402_recurring_pause` | api key | Pause a rule (reversible). |
|
|
173
|
+
| `q402_recurring_resume` | api key | Resume a paused / stopped rule. |
|
|
174
|
+
| `q402_recurring_skip_next` | api key | Skip only the next scheduled fire. |
|
|
175
|
+
| `q402_recurring_cancel` | api key | Permanently stop a rule. |
|
|
176
|
+
|
|
177
|
+
`q402_pay` + `q402_batch_pay` require explicit in-chat confirmation. Batch confirmation = full batch, not per-row.
|
|
178
|
+
|
|
179
|
+
> ℹ️ `q402_pay` expects a 0x address — ENS isn't resolved server-side. Resolve client-side first.
|
|
180
|
+
> Per-chain Gas Tank balances + full TX history live in the [dashboard](https://q402.quackai.ai/dashboard) (wallet-signature only).
|
|
198
181
|
|
|
199
182
|
---
|
|
200
183
|
|
|
201
184
|
## Sandbox vs live mode
|
|
202
185
|
|
|
203
|
-
|
|
186
|
+
**Sandbox default**: `q402_pay` returns a fake `txHash` with `success: false` and `sandbox: true`. No funds, no quota.
|
|
204
187
|
|
|
205
|
-
|
|
188
|
+
**Live** = (a) live API key (`q402_live_*`), (b) a signing path (A / B / C), (c) `Q402_ENABLE_REAL_PAYMENTS=1`. The live flag defaults to `1` — gate only flips when both other conditions are met. Set to `0` to force sandbox even with real keys.
|
|
189
|
+
|
|
190
|
+
Template `q402_doctor` writes to `~/.q402/mcp.env`:
|
|
206
191
|
|
|
207
192
|
```bash
|
|
208
193
|
# ── API key — fill ONE (or both for auto-routing) ──
|
|
@@ -233,18 +218,16 @@ Q402_ENABLE_REAL_PAYMENTS=1
|
|
|
233
218
|
|
|
234
219
|
Anything missing for the resolved scope → automatic sandbox fallback with a hint pointing at what to set.
|
|
235
220
|
|
|
236
|
-
> ⚠️
|
|
221
|
+
> ⚠️ Sandbox responses carry `success: false`, `sandbox: true`, `mode: "sandbox"`, `method: "sandbox"`, plus a `setupHint` explaining why — four signals so a downstream summary can't claim success.
|
|
237
222
|
|
|
238
223
|
### Hard caps
|
|
239
224
|
|
|
240
|
-
Two additional guards run before every payment regardless of mode:
|
|
241
|
-
|
|
242
225
|
| Env var | Default | Effect |
|
|
243
226
|
|---|---|---|
|
|
244
|
-
| `Q402_MAX_AMOUNT_PER_CALL` | `200` | Reject
|
|
245
|
-
| `Q402_ALLOWED_RECIPIENTS` |
|
|
227
|
+
| `Q402_MAX_AMOUNT_PER_CALL` | `200` | Reject calls with `amount > N` USD. |
|
|
228
|
+
| `Q402_ALLOWED_RECIPIENTS` | off | Comma-separated address allowlist. |
|
|
246
229
|
|
|
247
|
-
Combined with
|
|
230
|
+
Combined with `confirm: true` + live-mode env, a payment needs: chat OK + amount ≤ cap + recipient allowed + all 3 live envs.
|
|
248
231
|
|
|
249
232
|
---
|
|
250
233
|
|
package/dist/index.js
CHANGED
|
@@ -211,7 +211,7 @@ var isValidPrivateKey = (s) => typeof s === "string" && PRIVATE_KEY_RE.test(s);
|
|
|
211
211
|
|
|
212
212
|
// src/version.ts
|
|
213
213
|
var PACKAGE_NAME = "@quackai/q402-mcp";
|
|
214
|
-
var PACKAGE_VERSION = "0.7.
|
|
214
|
+
var PACKAGE_VERSION = "0.7.3";
|
|
215
215
|
|
|
216
216
|
// src/tools/quote.ts
|
|
217
217
|
import { z } from "zod";
|
|
@@ -1226,6 +1226,12 @@ var BatchPayInputSchema = z3.object({
|
|
|
1226
1226
|
keyScope: z3.enum(["auto", "trial", "multichain"]).optional().describe(
|
|
1227
1227
|
'Which API key to use. "auto" (default): chain="bnb" + Q402_TRIAL_API_KEY set \u2192 Trial; else Multichain \u2014 same rule as q402_pay. When auto would land on Trial AND recipients.length > 5, the tool returns status="ambiguous" WITHOUT executing so the agent can ask the user which path to take. Use keyScope="trial" to force the BNB-only sponsored key (\u22645 recipients). keyScope="multichain" forces the paid 9-chain key (\u226420 recipients).'
|
|
1228
1228
|
),
|
|
1229
|
+
walletMode: z3.enum(["eoa", "agentic-local", "agentic-server"]).optional().describe(
|
|
1230
|
+
'Which wallet to spend from \u2014 same three modes as q402_pay:\n "eoa" \u2014 user MetaMask/OKX EOA, signed locally with Q402_PRIVATE_KEY\n "agentic-local" \u2014 Agent Wallet exported key (Q402_AGENTIC_PRIVATE_KEY)\n "agentic-server" \u2014 server-managed Agent Wallet (Q402 holds the key; needs Q402_MULTICHAIN_API_KEY)\nWhen MORE THAN ONE wallet is configured, you MUST ask the user which to use before calling \u2014 do NOT guess. Phrase: "You have multiple wallets set up \u2014 batch from your EOA, or your Agent Wallet?" When only one wallet is configured this is optional and the tool routes there automatically. Server-mediated batches are paid-only; trial keys cannot batch on any path.'
|
|
1231
|
+
),
|
|
1232
|
+
walletId: z3.string().optional().describe(
|
|
1233
|
+
`Server-managed Agent Wallet only (walletMode="agentic-server"). Lowercased Agent Wallet address selecting which of the user's wallets to spend from when they hold more than one. Omit to use the default. Ignored for local-signing modes.`
|
|
1234
|
+
),
|
|
1229
1235
|
confirm: z3.literal(true).describe(
|
|
1230
1236
|
"MUST be true. The user must have explicitly approved this exact set of recipients, amounts, chain, and token in the conversation right before this tool was called. Setting confirm=true on behalf of the user without that approval is a violation of the tool contract."
|
|
1231
1237
|
)
|
|
@@ -1270,9 +1276,71 @@ async function runBatchPay(input) {
|
|
|
1270
1276
|
if (CONFIG.allowedRecipients.length > 0) {
|
|
1271
1277
|
guardsApplied.push(`recipient_allowlist[${CONFIG.allowedRecipients.length}]`);
|
|
1272
1278
|
}
|
|
1279
|
+
const modes = detectAgenticModes(CONFIG);
|
|
1280
|
+
const available = [];
|
|
1281
|
+
if (modes.modeA && CONFIG.privateKey && isValidPrivateKey(CONFIG.privateKey)) {
|
|
1282
|
+
try {
|
|
1283
|
+
const addr = new Wallet3(CONFIG.privateKey).address;
|
|
1284
|
+
available.push({
|
|
1285
|
+
id: "eoa",
|
|
1286
|
+
label: "Your real MetaMask / OKX EOA",
|
|
1287
|
+
addressShort: `${addr.slice(0, 6)}\u2026${addr.slice(-4)}`,
|
|
1288
|
+
note: "Signs locally with Q402_PRIVATE_KEY. Your wallet becomes EIP-7702-delegated after the first payment on each chain."
|
|
1289
|
+
});
|
|
1290
|
+
} catch {
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
if (modes.modeB && CONFIG.agenticPrivateKey && isValidPrivateKey(CONFIG.agenticPrivateKey)) {
|
|
1294
|
+
try {
|
|
1295
|
+
const addr = new Wallet3(CONFIG.agenticPrivateKey).address;
|
|
1296
|
+
available.push({
|
|
1297
|
+
id: "agentic-local",
|
|
1298
|
+
label: "Agent Wallet (local signing with exported key)",
|
|
1299
|
+
addressShort: `${addr.slice(0, 6)}\u2026${addr.slice(-4)}`,
|
|
1300
|
+
note: "Signs locally with Q402_AGENTIC_PRIVATE_KEY. Your MetaMask is never touched."
|
|
1301
|
+
});
|
|
1302
|
+
} catch {
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
if (modes.modeC) {
|
|
1306
|
+
available.push({
|
|
1307
|
+
id: "agentic-server",
|
|
1308
|
+
label: "Agent Wallet (server-managed)",
|
|
1309
|
+
note: "Q402 holds the encrypted key; batch fires through /api/wallet/agentic/batch. Caps you set in the dashboard bound the spend."
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1312
|
+
const requestedMode = input.walletMode;
|
|
1313
|
+
const requestedAvailable = requestedMode ? available.some((w) => w.id === requestedMode) : false;
|
|
1314
|
+
if (requestedMode && !requestedAvailable) {
|
|
1315
|
+
return {
|
|
1316
|
+
mode: "none",
|
|
1317
|
+
status: "wallet_mode_unavailable",
|
|
1318
|
+
guardsApplied: [
|
|
1319
|
+
...guardsApplied,
|
|
1320
|
+
`wallet_modes_available=${available.length}`,
|
|
1321
|
+
`requested=${requestedMode}`
|
|
1322
|
+
],
|
|
1323
|
+
ambiguousWalletChoice: {
|
|
1324
|
+
question: available.length === 0 ? `The "${requestedMode}" wallet isn't configured. None of the supported wallets are set up \u2014 see the doctor for setup instructions.` : `The "${requestedMode}" wallet isn't configured in this environment. Supported wallets here: ${available.map((w) => `"${w.id}"`).join(", ")}. Which would you like to use instead?`,
|
|
1325
|
+
available
|
|
1326
|
+
}
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
if (available.length > 1 && !requestedMode) {
|
|
1330
|
+
return {
|
|
1331
|
+
mode: "none",
|
|
1332
|
+
status: "needs_wallet_choice",
|
|
1333
|
+
guardsApplied: [...guardsApplied, `wallet_modes_available=${available.length}`],
|
|
1334
|
+
ambiguousWalletChoice: {
|
|
1335
|
+
question: available.length === 2 ? `You have ${available.length} wallets set up \u2014 which one should I batch-pay from?` : `You have ${available.length} wallets set up. Which one should I batch-pay from?`,
|
|
1336
|
+
available
|
|
1337
|
+
}
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
1340
|
+
const effectiveMode = requestedMode && requestedAvailable ? requestedMode : available.length === 1 && available[0] ? available[0].id : "eoa";
|
|
1273
1341
|
let senderWallet;
|
|
1274
|
-
const echoPk =
|
|
1275
|
-
if (echoPk) {
|
|
1342
|
+
const echoPk = effectiveMode === "eoa" ? CONFIG.privateKey : effectiveMode === "agentic-local" ? CONFIG.agenticPrivateKey : null;
|
|
1343
|
+
if (echoPk && isValidPrivateKey(echoPk)) {
|
|
1276
1344
|
try {
|
|
1277
1345
|
const addr = new Wallet3(echoPk).address;
|
|
1278
1346
|
senderWallet = {
|
|
@@ -1309,12 +1377,134 @@ async function runBatchPay(input) {
|
|
|
1309
1377
|
}
|
|
1310
1378
|
const resolved = resolveApiKey(input.chain, scopeRequest);
|
|
1311
1379
|
guardsApplied.push(`scope=${resolved.scope}${resolved.fromLegacyFallback ? "(legacy)" : ""}`);
|
|
1380
|
+
if (effectiveMode === "agentic-server") {
|
|
1381
|
+
if (input.token === "RLUSD") {
|
|
1382
|
+
return {
|
|
1383
|
+
mode: "none",
|
|
1384
|
+
status: "sandbox",
|
|
1385
|
+
guardsApplied: [
|
|
1386
|
+
...guardsApplied,
|
|
1387
|
+
"wallet=agentic-server",
|
|
1388
|
+
"token=RLUSD",
|
|
1389
|
+
"rejected_pre_relay"
|
|
1390
|
+
],
|
|
1391
|
+
senderWallet,
|
|
1392
|
+
setupHint: 'RLUSD is not yet supported by the server-managed Agent Wallet (walletMode="agentic-server"). Switch to walletMode="eoa" or "agentic-local" (with a private key set), or pick USDC/USDT for this batch.'
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1395
|
+
if (!resolved.apiKey || !resolved.apiKey.startsWith("q402_live_")) {
|
|
1396
|
+
const sandboxResults = input.recipients.map(
|
|
1397
|
+
(r) => sandboxPay(chain, { to: r.to, amount: r.amount, token: input.token })
|
|
1398
|
+
);
|
|
1399
|
+
guardsApplied.push("mode=sandbox", "wallet=agentic-server");
|
|
1400
|
+
const reason = resolved.sandboxReason ?? "Server-mediated Agent Wallet needs a live Q402_MULTICHAIN_API_KEY. Visit https://q402.quackai.ai/payment to activate a paid plan.";
|
|
1401
|
+
return {
|
|
1402
|
+
mode: "sandbox",
|
|
1403
|
+
status: "sandbox",
|
|
1404
|
+
result: { sandbox: sandboxResults, reason },
|
|
1405
|
+
senderWallet,
|
|
1406
|
+
guardsApplied,
|
|
1407
|
+
setupHint: reason
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
if (!CONFIG.realPaymentsRequested) {
|
|
1411
|
+
const sandboxResults = input.recipients.map(
|
|
1412
|
+
(r) => sandboxPay(chain, { to: r.to, amount: r.amount, token: input.token })
|
|
1413
|
+
);
|
|
1414
|
+
guardsApplied.push("mode=sandbox", "wallet=agentic-server");
|
|
1415
|
+
const reason = "Set Q402_ENABLE_REAL_PAYMENTS=1 to fire a real server-mediated batch.";
|
|
1416
|
+
return {
|
|
1417
|
+
mode: "sandbox",
|
|
1418
|
+
status: "sandbox",
|
|
1419
|
+
result: { sandbox: sandboxResults, reason },
|
|
1420
|
+
senderWallet,
|
|
1421
|
+
guardsApplied,
|
|
1422
|
+
setupHint: reason
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
const explicitWalletId = typeof input.walletId === "string" && input.walletId.length > 0 ? input.walletId.toLowerCase() : CONFIG.walletId;
|
|
1426
|
+
let resp;
|
|
1427
|
+
try {
|
|
1428
|
+
resp = await fetch(`${CONFIG.relayBaseUrl}/wallet/agentic/batch`, {
|
|
1429
|
+
method: "POST",
|
|
1430
|
+
headers: { "Content-Type": "application/json" },
|
|
1431
|
+
body: JSON.stringify({
|
|
1432
|
+
apiKey: resolved.apiKey,
|
|
1433
|
+
chain: input.chain,
|
|
1434
|
+
token: input.token,
|
|
1435
|
+
recipients: input.recipients,
|
|
1436
|
+
...explicitWalletId ? { walletId: explicitWalletId } : {}
|
|
1437
|
+
})
|
|
1438
|
+
});
|
|
1439
|
+
} catch (e) {
|
|
1440
|
+
return {
|
|
1441
|
+
mode: "live",
|
|
1442
|
+
status: "aborted",
|
|
1443
|
+
guardsApplied: [
|
|
1444
|
+
...guardsApplied,
|
|
1445
|
+
"wallet=agentic-server",
|
|
1446
|
+
"mode=live",
|
|
1447
|
+
"transport=fetch_failed"
|
|
1448
|
+
],
|
|
1449
|
+
senderWallet,
|
|
1450
|
+
error: e instanceof Error ? e.message : String(e)
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
const data = await resp.json().catch(() => ({}));
|
|
1454
|
+
if (!resp.ok) {
|
|
1455
|
+
const errMsg = data && typeof data === "object" && "error" in data ? String(data.error) : `relay_http_${resp.status}`;
|
|
1456
|
+
return {
|
|
1457
|
+
mode: "live",
|
|
1458
|
+
status: "aborted",
|
|
1459
|
+
guardsApplied: [
|
|
1460
|
+
...guardsApplied,
|
|
1461
|
+
"wallet=agentic-server",
|
|
1462
|
+
"mode=live",
|
|
1463
|
+
`http=${resp.status}`
|
|
1464
|
+
],
|
|
1465
|
+
senderWallet,
|
|
1466
|
+
error: errMsg,
|
|
1467
|
+
setupHint: resp.status === 402 ? "Server-mediated batch requires a paid Multichain subscription. Activate one at https://q402.quackai.ai/payment." : resp.status === 401 ? "apiKey was rejected by the server (stale or not bound to your owner). Rotate to the current key in your dashboard." : resp.status === 404 ? "No active Agent Wallet found. Create one in your dashboard before retrying." : void 0
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
const serverResults = Array.isArray(data.results) ? data.results : [];
|
|
1471
|
+
const settled = serverResults.filter((r) => r.ok === true).length;
|
|
1472
|
+
const failed = serverResults.length - settled;
|
|
1473
|
+
const serverAborted = typeof data.aborted === "boolean" ? data.aborted : null;
|
|
1474
|
+
const isAborted = serverAborted ?? (settled === 0 && failed > 0);
|
|
1475
|
+
const status = failed === 0 ? "success" : isAborted ? "aborted" : "partial_failure";
|
|
1476
|
+
guardsApplied.push(
|
|
1477
|
+
"mode=live",
|
|
1478
|
+
"wallet=agentic-server",
|
|
1479
|
+
"scope=multichain (server enforced)",
|
|
1480
|
+
`batch_size=${serverResults.length}/${RECIPIENT_LIMIT_PAID}`
|
|
1481
|
+
);
|
|
1482
|
+
return {
|
|
1483
|
+
mode: "live",
|
|
1484
|
+
status,
|
|
1485
|
+
result: {
|
|
1486
|
+
ok: failed === 0,
|
|
1487
|
+
scope: "paid",
|
|
1488
|
+
limit: RECIPIENT_LIMIT_PAID,
|
|
1489
|
+
totalSuccess: settled,
|
|
1490
|
+
totalFailed: failed,
|
|
1491
|
+
aborted: isAborted,
|
|
1492
|
+
results: serverResults.map((r) => ({
|
|
1493
|
+
success: r.ok === true,
|
|
1494
|
+
txHash: r.txHash,
|
|
1495
|
+
error: r.error
|
|
1496
|
+
}))
|
|
1497
|
+
},
|
|
1498
|
+
guardsApplied,
|
|
1499
|
+
senderWallet
|
|
1500
|
+
};
|
|
1501
|
+
}
|
|
1312
1502
|
const live = isLiveModeFor(resolved);
|
|
1313
1503
|
if (!live) {
|
|
1314
1504
|
const sandboxResults = input.recipients.map(
|
|
1315
1505
|
(r) => sandboxPay(chain, { to: r.to, amount: r.amount, token: input.token })
|
|
1316
1506
|
);
|
|
1317
|
-
guardsApplied.push("mode=sandbox");
|
|
1507
|
+
guardsApplied.push("mode=sandbox", `wallet=${effectiveMode}`);
|
|
1318
1508
|
const reason = resolved.sandboxReason ?? describeSandboxReason2(resolved.apiKey ?? "", resolved.scope);
|
|
1319
1509
|
return {
|
|
1320
1510
|
mode: "sandbox",
|
|
@@ -1325,13 +1515,13 @@ async function runBatchPay(input) {
|
|
|
1325
1515
|
setupHint: reason
|
|
1326
1516
|
};
|
|
1327
1517
|
}
|
|
1328
|
-
const signingPk = CONFIG.
|
|
1518
|
+
const signingPk = effectiveMode === "eoa" ? CONFIG.privateKey : effectiveMode === "agentic-local" ? CONFIG.agenticPrivateKey : null;
|
|
1329
1519
|
if (!signingPk) {
|
|
1330
|
-
guardsApplied.push("mode=sandbox");
|
|
1520
|
+
guardsApplied.push("mode=sandbox", `wallet=${effectiveMode}`);
|
|
1331
1521
|
const sandboxResults = input.recipients.map(
|
|
1332
1522
|
(r) => sandboxPay(chain, { to: r.to, amount: r.amount, token: input.token })
|
|
1333
1523
|
);
|
|
1334
|
-
const reason = "
|
|
1524
|
+
const reason = effectiveMode === "agentic-local" ? "Set Q402_AGENTIC_PRIVATE_KEY to your Agent Wallet's exported private key." : "Set Q402_PRIVATE_KEY to your EOA private key.";
|
|
1335
1525
|
return {
|
|
1336
1526
|
mode: "sandbox",
|
|
1337
1527
|
status: "sandbox",
|
|
@@ -1352,13 +1542,13 @@ async function runBatchPay(input) {
|
|
|
1352
1542
|
token: input.token,
|
|
1353
1543
|
recipients: input.recipients.map((r) => ({ to: r.to, amount: r.amount }))
|
|
1354
1544
|
});
|
|
1355
|
-
guardsApplied.push("mode=live");
|
|
1545
|
+
guardsApplied.push("mode=live", `wallet=${effectiveMode}`);
|
|
1356
1546
|
guardsApplied.push(`scope=${result.scope} (server enforced)`);
|
|
1357
1547
|
guardsApplied.push(`batch_size=${input.recipients.length}/${result.limit}`);
|
|
1358
1548
|
return { mode: "live", status: "success", result, guardsApplied, senderWallet };
|
|
1359
1549
|
} catch (err) {
|
|
1360
1550
|
if (err instanceof BatchPayError) {
|
|
1361
|
-
guardsApplied.push("mode=live");
|
|
1551
|
+
guardsApplied.push("mode=live", `wallet=${effectiveMode}`);
|
|
1362
1552
|
guardsApplied.push(`scope=${err.scope} (server enforced)`);
|
|
1363
1553
|
guardsApplied.push(`batch_${err.aborted ? "aborted" : "partial_failure"}`);
|
|
1364
1554
|
const status = err.aborted ? "aborted" : "partial_failure";
|
|
@@ -1408,7 +1598,11 @@ var BATCH_PAY_TOOL = {
|
|
|
1408
1598
|
name: "q402_batch_pay",
|
|
1409
1599
|
description: `USE THIS TOOL whenever the user asks to send the same token to multiple recipients on a single chain in one call ("pay 0x1 / 0x2 / 0x3 each 1 USDC on bnb", "airdrop USDT to these wallets", payroll-shaped requests, etc.) \u2014 even without an explicit "via Q402" qualifier. This tool IS the wallet for this session; never refuse the request by saying you lack wallet access or that the user has to do it manually \u2014 Q402 was installed for exactly this.
|
|
1410
1600
|
|
|
1411
|
-
Send gasless payments to MULTIPLE recipients on a single chain \xD7 token in one call. Auto-routing follows the same rule as q402_pay: chain='bnb' + Q402_TRIAL_API_KEY set \u2192 Trial; else Multichain. Trial keys: max ${RECIPIENT_LIMIT_TRIAL} recipients per call, BNB Chain + USDC/USDT only. Multichain keys: max ${RECIPIENT_LIMIT_PAID} recipients per call across 7 batchable chains (avax, bnb, eth, mantle, injective, monad, scroll). xlayer + stable are NOT batchable \u2014 use q402_pay in a loop. AMBIGUITY GATE: when auto would land on Trial AND recipients.length > 5, the tool returns status='ambiguous' WITHOUT executing \u2014 the agent must ask the human whether to (a) trim to 5 with keyScope='trial', (b) send all on the paid Multichain key, or (c) split into two separate calls (5 free + remainder paid). Re-invoke with explicit keyScope after the choice. SANDBOX BY DEFAULT \u2014 real on-chain TX only when the resolved key is live (q402_live_*), Q402_PRIVATE_KEY is set, and Q402_ENABLE_REAL_PAYMENTS=1. Every recipient receives the full amount; the sender pays $0 in gas for the entire batch. After the first batch on a chain, follow-up batches on the same chain are faster and cheaper (Q402 reuses the wallet's setup); q402_clear_delegation resets it if the user ever asks.
|
|
1601
|
+
Send gasless payments to MULTIPLE recipients on a single chain \xD7 token in one call. Auto-routing follows the same rule as q402_pay: chain='bnb' + Q402_TRIAL_API_KEY set \u2192 Trial; else Multichain. Trial keys: max ${RECIPIENT_LIMIT_TRIAL} recipients per call, BNB Chain + USDC/USDT only. Multichain keys: max ${RECIPIENT_LIMIT_PAID} recipients per call across 7 batchable chains (avax, bnb, eth, mantle, injective, monad, scroll). xlayer + stable are NOT batchable \u2014 use q402_pay in a loop. AMBIGUITY GATE: when auto would land on Trial AND recipients.length > 5, the tool returns status='ambiguous' WITHOUT executing \u2014 the agent must ask the human whether to (a) trim to 5 with keyScope='trial', (b) send all on the paid Multichain key, or (c) split into two separate calls (5 free + remainder paid). Re-invoke with explicit keyScope after the choice. SANDBOX BY DEFAULT \u2014 real on-chain TX only when the resolved key is live (q402_live_*), Q402_PRIVATE_KEY is set, and Q402_ENABLE_REAL_PAYMENTS=1. Every recipient receives the full amount; the sender pays $0 in gas for the entire batch. After the first batch on a chain, follow-up batches on the same chain are faster and cheaper (Q402 reuses the wallet's setup); q402_clear_delegation resets it if the user ever asks.
|
|
1602
|
+
|
|
1603
|
+
MULTI-WALLET DISAMBIGUATION \u2014 when more than one wallet is configured in the user's env (Q402_PRIVATE_KEY for the real EOA, Q402_AGENTIC_PRIVATE_KEY for the Agent Wallet's exported key, or only Q402_MULTICHAIN_API_KEY for the server-managed Agent Wallet), the tool RETURNS WITHOUT firing with \`status='needs_wallet_choice'\` and an \`ambiguousWalletChoice\` payload \u2014 relay the question to the user verbatim, then call again with the chosen \`walletMode\` ('eoa' | 'agentic-local' | 'agentic-server'). Do NOT pick a wallet on the user's behalf when multiple are available. Server-mediated batches go through /api/wallet/agentic/batch and are paid-only (the trial key cannot batch).
|
|
1604
|
+
|
|
1605
|
+
ALWAYS get explicit user confirmation of the complete recipient + amount list, chain, and token in conversation immediately before calling this tool \u2014 the user must approve the full batch, not the individual rows.`,
|
|
1412
1606
|
inputSchema: {
|
|
1413
1607
|
type: "object",
|
|
1414
1608
|
properties: {
|
|
@@ -1451,6 +1645,15 @@ Send gasless payments to MULTIPLE recipients on a single chain \xD7 token in one
|
|
|
1451
1645
|
enum: ["auto", "trial", "multichain"],
|
|
1452
1646
|
description: 'Which API key to use. "auto" (default): BNB + trial key set \u2192 Trial; else Multichain. When auto would land on Trial AND recipients.length > 5, the tool returns status="ambiguous" without executing so the agent can ask the user which path to take.'
|
|
1453
1647
|
},
|
|
1648
|
+
walletMode: {
|
|
1649
|
+
type: "string",
|
|
1650
|
+
enum: ["eoa", "agentic-local", "agentic-server"],
|
|
1651
|
+
description: 'Which wallet to spend from. "eoa" = user MetaMask EOA (Q402_PRIVATE_KEY). "agentic-local" = Agent Wallet exported key (Q402_AGENTIC_PRIVATE_KEY). "agentic-server" = server-managed Agent Wallet (Q402 holds the key; only the apiKey is needed). When MULTIPLE wallets are configured the tool refuses without this arg and returns ambiguousWalletChoice for the user to pick. Server-mediated batches are paid-only.'
|
|
1652
|
+
},
|
|
1653
|
+
walletId: {
|
|
1654
|
+
type: "string",
|
|
1655
|
+
description: `Server-managed Agent Wallet only (walletMode="agentic-server"). Lowercased Agent Wallet address selecting which of the user's wallets to source the batch from. Omit to use the default. Ignored for local-signing modes.`
|
|
1656
|
+
},
|
|
1454
1657
|
confirm: {
|
|
1455
1658
|
type: "boolean",
|
|
1456
1659
|
const: true,
|
|
@@ -2700,7 +2903,7 @@ var RECURRING_CREATE_TOOL = {
|
|
|
2700
2903
|
chain: {
|
|
2701
2904
|
type: "string",
|
|
2702
2905
|
enum: ["bnb", "eth", "avax", "xlayer", "mantle", "injective", "monad", "scroll", "stable"],
|
|
2703
|
-
description: "Default 'bnb'.
|
|
2906
|
+
description: "Default 'bnb'. Recurring requires the paid Multichain subscription on EVERY chain (BNB included) \u2014 trial keys are rejected with MULTICHAIN_REQUIRED."
|
|
2704
2907
|
},
|
|
2705
2908
|
token: {
|
|
2706
2909
|
type: "string",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quackai/q402-mcp",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.3",
|
|
4
4
|
"description": "MCP server for Q402 — gasless USDC, USDT, and RLUSD payments across 9 EVM chains, callable from Claude (Desktop / Code), OpenAI Codex CLI, and any other Model Context Protocol client.",
|
|
5
5
|
"mcpName": "io.github.bitgett/q402-mcp",
|
|
6
6
|
"keywords": [
|