@quackai/q402-mcp 0.5.4 → 0.5.6

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.
Files changed (3) hide show
  1. package/README.md +61 -66
  2. package/dist/index.js +372 -27
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -9,101 +9,95 @@
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
- Claude can now reason about stablecoin payments end to end quote a transfer across 9 chains, pick the cheapest route, and (optionally) settle the transaction over [Q402](https://q402.quackai.ai)'s EIP-7702 relayer infrastructure. The recipient receives the full amount; the sender pays $0 in gas.
12
+ AI agents — Claude (Desktop / Code), OpenAI Codex CLI, Cursor, Cline, and any other MCP-compatible client — can now reason about stablecoin payments end to end: quote a transfer across 9 chains, pick the cheapest route, and (optionally) settle the transaction over [Q402](https://q402.quackai.ai)'s EIP-7702 relayer infrastructure. The recipient receives the full amount; the sender pays $0 in gas.
13
13
 
14
14
  ---
15
15
 
16
16
  ## Quick start
17
17
 
18
- The server speaks stdio MCP, so any MCP-compatible client can use it. The two paths verified end-to-end today are **Claude (Desktop / Code)** and **OpenAI Codex CLI**.
18
+ Two steps:
19
19
 
20
- ### Claude Desktop / Claude Code
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.
21
22
 
22
- ```bash
23
- claude mcp add q402 -- npx -y @quackai/q402-mcp
24
- ```
23
+ ### 1. Register the server
25
24
 
26
- Or edit `claude_desktop_config.json` directly:
27
-
28
- ```json
29
- {
30
- "mcpServers": {
31
- "q402": {
32
- "command": "npx",
33
- "args": ["-y", "@quackai/q402-mcp"]
34
- }
35
- }
36
- }
37
- ```
25
+ | Client | Command / config |
26
+ |---|---|
27
+ | **Claude Desktop / Claude Code** | `claude mcp add q402 -- npx -y @quackai/q402-mcp` |
28
+ | **OpenAI Codex CLI** | `codex mcp add q402 -- npx -y @quackai/q402-mcp` |
29
+ | **Cursor** | Add to `~/.cursor/mcp.json`: `{ "mcpServers": { "q402": { "command": "npx", "args": ["-y", "@quackai/q402-mcp"] } } }` |
30
+ | **Cline** | Cline → Settings → MCP Servers → Edit JSON. Same shape as Cursor. |
31
+ | **Any other stdio MCP client** | Point it at `npx -y @quackai/q402-mcp`. No client-specific code. |
38
32
 
39
- Restart Claude Desktop and ask:
33
+ That's it secrets are NOT configured here. The MCP server reads them from `~/.q402/mcp.env` at startup (same pattern as AWS CLI / Stripe CLI / gh CLI), so every client uses the same file with no per-client wiring.
40
34
 
41
- > *"Compare gas costs to send 50 USDC to vitalik.eth across all 9 Q402 chains."*
35
+ ### 2. First-time setup
42
36
 
43
- ### OpenAI Codex CLI
37
+ Restart your client, then ask your agent:
44
38
 
45
- Three install paths — pick the one that matches your workflow.
39
+ > *"Set up Q402"*
46
40
 
47
- **(a) Codex plugin marketplace** (recommended bundles the MCP config so users don't write TOML):
41
+ The agent calls `q402_doctor`. On first install, the tool tells the agent to:
48
42
 
49
- ```bash
50
- codex plugin marketplace add bitgett/q402-mcp
51
- codex /plugins # browse and install "q402"
52
- ```
43
+ 1. Offer to create `~/.q402/mcp.env` (placeholder values only)
44
+ 2. Open the file in your editor (`code` / `open` / `start` / `xdg-open`)
45
+ 3. Walk you through pasting in (a) your API key from <https://q402.quackai.ai/event> (free Trial) or <https://q402.quackai.ai/payment> (paid Multichain), and (b) your wallet private key — **into the file, not into chat**
46
+ 4. Restart the client and run `q402_doctor` again to verify
53
47
 
54
- This repo carries a Codex plugin manifest at [`.codex-plugin/plugin.json`](./.codex-plugin/plugin.json) and a marketplace catalog at [`.agents/plugins/marketplace.json`](./.agents/plugins/marketplace.json), so any signed-in Codex user can register it as a marketplace source and install with one click.
48
+ 🔒 **Q402 never asks you to paste your private key into chat.** The MCP server signs payments LOCALLY on your machine your key never leaves your device.
55
49
 
56
- **(b) Single MCP server via `codex mcp add`** (no plugin wrapper — just register the stdio server):
50
+ ### Manual setup (no AI)
51
+
52
+ Create `~/.q402/mcp.env` yourself:
57
53
 
58
54
  ```bash
59
- codex mcp add q402 -- npx -y @quackai/q402-mcp
55
+ # ~/.q402/mcp.env
56
+ # Pick ONE of these:
57
+ Q402_TRIAL_API_KEY=q402_live_...
58
+ # Q402_MULTICHAIN_API_KEY=q402_live_...
59
+
60
+ Q402_PRIVATE_KEY=0x...
61
+ Q402_ENABLE_REAL_PAYMENTS=1
62
+ Q402_RELAY_BASE_URL=https://q402.quackai.ai/api
63
+
64
+ # Optional safety guards:
65
+ # Q402_MAX_AMOUNT_PER_CALL=5
66
+ # Q402_ALLOWED_RECIPIENTS=0xabc...,0xdef...
60
67
  ```
61
68
 
62
- **(c) Direct `~/.codex/config.toml` edit** (`.codex/config.toml` for per-project scope):
69
+ Then `chmod 600 ~/.q402/mcp.env` (Unix) and restart your client. That's the full configuration.
63
70
 
64
- ```toml
65
- [mcp_servers.q402]
66
- command = "npx"
67
- args = ["-y", "@quackai/q402-mcp"]
68
- startup_timeout_sec = 20.0
69
- ```
71
+ ### Advanced — explicit env injection
72
+
73
+ If you'd rather skip the file and inject env vars yourself (e.g. via Codex `env_vars` allow-list, a secrets manager, or shell exports), the server falls through to `process.env` — and `process.env` wins over file values on conflicts. So existing shell-export setups keep working unchanged.
70
74
 
71
- To enable real on-chain payments, pass the three live-mode env vars **explicitly** under `env` — Codex does **not** forward host env vars by default:
75
+ <details>
76
+ <summary>Codex <code>env_vars</code> allow-list example</summary>
72
77
 
73
78
  ```toml
74
79
  [mcp_servers.q402]
75
80
  command = "npx"
76
81
  args = ["-y", "@quackai/q402-mcp"]
77
82
  startup_timeout_sec = 20.0
78
- env = {
79
- # Two-key model: set whichever applies — both is best.
80
- # Auto-routing rule (same for q402_pay AND q402_batch_pay):
81
- # chain="bnb" + Q402_TRIAL_API_KEY set → Trial (free sponsored)
82
- # anything else → Multichain (paid 9-chain)
83
- # Batch ambiguity: when auto would land on Trial AND recipients.length > 5,
84
- # q402_batch_pay returns status="ambiguous" WITHOUT executing so the agent
85
- # can ask the user — pass keyScope="trial" (first 5), "multichain" (all
86
- # paid), or call twice (5 free + remainder paid).
87
- # Both keys use the same q402_live_ prefix — the env var name is what
88
- # carries the scope, not the key string. Get the values from the
89
- # dashboard (each key has its own copy button per view).
90
- Q402_TRIAL_API_KEY = "q402_live_...", # BNB-only sponsored (from /event)
91
- Q402_MULTICHAIN_API_KEY = "q402_live_...", # paid 9-chain (from /payment)
92
- # Legacy fallback — used if neither scoped key above is set.
93
- Q402_API_KEY = "q402_live_...",
94
- Q402_PRIVATE_KEY = "0xabc...",
95
- Q402_ENABLE_REAL_PAYMENTS = "1",
96
- Q402_MAX_AMOUNT_PER_CALL = "5",
97
- }
83
+ env_vars = [
84
+ "Q402_TRIAL_API_KEY",
85
+ "Q402_MULTICHAIN_API_KEY",
86
+ "Q402_PRIVATE_KEY",
87
+ "Q402_ENABLE_REAL_PAYMENTS",
88
+ "Q402_RELAY_BASE_URL",
89
+ ]
98
90
  ```
99
91
 
100
- > If you'd rather not inline secrets in `config.toml`, use the `env_vars` allow-list form to forward specific names from your shell environment instead — see the [Codex config reference](https://developers.openai.com/codex/config-reference) for the full schema.
92
+ Then export the values in `~/.zshrc` / `~/.bashrc`. See the [Codex config reference](https://developers.openai.com/codex/config-reference) for the full schema.
101
93
 
102
- Then run `codex` and ask the same kind of question. The first call may take a few seconds while `npx` warms its cache; subsequent calls are instant.
94
+ </details>
103
95
 
104
- ### Any other MCP client
96
+ ### Try it without any setup
105
97
 
106
- The server has no client-specific code. If your client speaks stdio MCP, point it at `npx -y @quackai/q402-mcp` and the seven tools listed below will appear.
98
+ `q402_quote` works with zero configuration no API key, no private key, no env file. Ask:
99
+
100
+ > *"Compare gas costs to send 50 USDC to vitalik.eth across all 9 Q402 chains."*
107
101
 
108
102
  ---
109
103
 
@@ -115,6 +109,7 @@ The server has no client-specific code. If your client speaks stdio MCP, point i
115
109
 
116
110
  | Tool | Auth | Purpose |
117
111
  |---|---|---|
112
+ | `q402_doctor` | none | Health check covering BOTH first-install onboarding AND ongoing operational diagnostics. AI calls this when the user says "set up Q402" / "verify Q402" / "why isn't Q402 working". On first install, returns a `recommendedActions[]` payload telling the client to create `~/.q402/mcp.env` and open it in the user's editor. Later phases verify per-scope quota, EIP-7702 delegation state per chain, relay reachability, and surface slot-mismatch warnings (e.g. Trial-tier key sitting in `Q402_MULTICHAIN_API_KEY` would silently burn paid quota). Read-only — no signing, no on-chain action. |
118
113
  | `q402_quote` | none | Compare gas cost and supported tokens across chains. Read-only. |
119
114
  | `q402_balance` | API key | Verify the API key and report its plan tier + remaining quota credits (live vs sandbox). |
120
115
  | `q402_pay` | API key + private key + flag | Send a gasless payment to a single recipient. **Sandbox by default** — see [Sandbox vs live mode](#sandbox-vs-live-mode). |
@@ -136,7 +131,7 @@ The server has no client-specific code. If your client speaks stdio MCP, point i
136
131
 
137
132
  ## Sandbox vs live mode
138
133
 
139
- By default the MCP server operates in **sandbox mode**: `q402_pay` returns a random fake transaction hash (32-byte hex, no on-chain broadcast), no funds move, no gas-tank credit is consumed. That makes it safe to plug into any MCP client without worrying about an LLM hallucinating a payment.
134
+ By default the MCP server operates in **sandbox mode**: `q402_pay` returns a random fake transaction hash (32-byte hex, no on-chain broadcast), no funds move, no gas-tank credit is consumed. That makes it safe to plug into any MCP client without worrying about an accidental payment if the agent misreads the conversation and fires `q402_pay` before you intended, nothing moves.
140
135
 
141
136
  To enable real on-chain transactions, the resolved API key must be live (`q402_live_*`), `Q402_PRIVATE_KEY` must be set, and `Q402_ENABLE_REAL_PAYMENTS=1`:
142
137
 
@@ -211,11 +206,11 @@ Combined with the `confirm: true` argument the tool requires, this means the mod
211
206
 
212
207
  ## Why this exists
213
208
 
214
- x402 standardised "402 Payment Required" semantics for AI agents but the official Coinbase facilitator only covers a few chains and assumes ERC-3009 token support which excludes BNB USDT, Mantle USDT0, Injective USDT, and the chains where most stablecoin volume actually lives.
209
+ AI agents are becoming the default interface for software, but the moment they need to move money the stack breaks: holding gas tokens, signing every transaction, managing wallets across many chains. None of that scales when the agent is supposed to act on its own.
215
210
 
216
- Q402 implements the same payer experience (single signature, $0 gas, instant settlement) on all 9 of those chains using EIP-7702 delegated execution, which works with any ERC-20. This MCP server makes that infrastructure addressable from Claude itself.
211
+ Q402 is the payment layer for that gap. A single signing primitive (EIP-712 + EIP-7702) settles gasless stablecoin payments across 9 EVM chains, with an ECDSA-signed Trust Receipt for every transaction. The MCP package exposes that surface inside Claude, Codex, Cursor, and Cline — your agent can quote, send, batch, and audit payments from a natural-language prompt.
217
212
 
218
- If you want to dig into how the wire protocol differs from x402, see [Q402 docs](https://q402.quackai.ai/docs).
213
+ Single transfers and multi-recipient batches ship today. The next layer — recurring payouts, conditional execution, and policy-gated treasury automation — is the same primitive composed differently. We're building toward agents that operate real budgets, settle among themselves, and move value through workflows no human triggers manually.
219
214
 
220
215
  ---
221
216
 
package/dist/index.js CHANGED
@@ -9,7 +9,63 @@ import {
9
9
  } from "@modelcontextprotocol/sdk/types.js";
10
10
 
11
11
  // src/config.ts
12
+ import { existsSync, readFileSync, statSync } from "fs";
13
+ import { homedir } from "os";
14
+ import { join } from "path";
12
15
  import { isAddress } from "ethers";
16
+ var Q402_ENV_FILE = join(homedir(), ".q402", "mcp.env");
17
+ function loadQ402EnvFileFromPath(path) {
18
+ if (!existsSync(path)) return {};
19
+ if (process.platform !== "win32") {
20
+ try {
21
+ const mode = statSync(path).mode & 511;
22
+ if (mode & 63) {
23
+ process.stderr.write(
24
+ `[q402-mcp] warning: ${path} is readable by group/other (mode ${mode.toString(8)}). Run: chmod 600 ${path}
25
+ `
26
+ );
27
+ }
28
+ } catch {
29
+ }
30
+ }
31
+ const out = {};
32
+ let raw;
33
+ try {
34
+ raw = readFileSync(path, "utf-8");
35
+ } catch (e) {
36
+ process.stderr.write(
37
+ `[q402-mcp] warning: could not read ${path}: ${e instanceof Error ? e.message : String(e)}
38
+ `
39
+ );
40
+ return {};
41
+ }
42
+ for (const line of raw.split(/\r?\n/)) {
43
+ const t = line.trim();
44
+ if (!t || t.startsWith("#")) continue;
45
+ const eq = t.indexOf("=");
46
+ if (eq < 0) continue;
47
+ const k = t.slice(0, eq).trim();
48
+ if (!k.startsWith("Q402_")) continue;
49
+ const v = t.slice(eq + 1).trim().replace(/^['"](.*)['"]$/, "$1");
50
+ out[k] = v;
51
+ }
52
+ return out;
53
+ }
54
+ function loadQ402EnvFile() {
55
+ return loadQ402EnvFileFromPath(Q402_ENV_FILE);
56
+ }
57
+ var FILE_ENV = loadQ402EnvFile();
58
+ var ENV = Object.freeze({
59
+ ...FILE_ENV,
60
+ ...process.env
61
+ });
62
+ var Q402_ENV_FILE_PATH = Q402_ENV_FILE;
63
+ var Q402_ENV_FILE_PRESENT = existsSync(Q402_ENV_FILE);
64
+ var Q402_ENV_FILE_KEYS = Object.freeze(
65
+ new Set(
66
+ Object.keys(FILE_ENV).filter((k) => process.env[k] === void 0)
67
+ )
68
+ );
13
69
  var DEFAULT_RELAY_BASE = "https://q402.quackai.ai/api";
14
70
  var DEFAULT_MAX_AMOUNT = 5;
15
71
  function classifyApiKey(k) {
@@ -29,13 +85,13 @@ function parseMaxAmount(raw) {
29
85
  return n;
30
86
  }
31
87
  function loadConfig() {
32
- const trialApiKey = process.env.Q402_TRIAL_API_KEY ?? null;
33
- const multichainApiKey = process.env.Q402_MULTICHAIN_API_KEY ?? null;
34
- const legacyApiKey = process.env.Q402_API_KEY ?? null;
88
+ const trialApiKey = ENV.Q402_TRIAL_API_KEY ?? null;
89
+ const multichainApiKey = ENV.Q402_MULTICHAIN_API_KEY ?? null;
90
+ const legacyApiKey = ENV.Q402_API_KEY ?? null;
35
91
  const apiKey = multichainApiKey ?? trialApiKey ?? legacyApiKey;
36
92
  const apiKeyKind = classifyApiKey(apiKey);
37
- const privateKey = process.env.Q402_PRIVATE_KEY ?? null;
38
- const realPaymentsRequested = process.env.Q402_ENABLE_REAL_PAYMENTS === "1";
93
+ const privateKey = ENV.Q402_PRIVATE_KEY ?? null;
94
+ const realPaymentsRequested = ENV.Q402_ENABLE_REAL_PAYMENTS === "1";
39
95
  const live = realPaymentsRequested && apiKeyKind === "live" && typeof privateKey === "string" && privateKey.length > 0;
40
96
  return {
41
97
  trialApiKey,
@@ -46,9 +102,9 @@ function loadConfig() {
46
102
  privateKey,
47
103
  realPaymentsRequested,
48
104
  mode: live ? "live" : "sandbox",
49
- relayBaseUrl: (process.env.Q402_RELAY_BASE_URL ?? DEFAULT_RELAY_BASE).replace(/\/$/, ""),
50
- maxAmountPerCallUsd: parseMaxAmount(process.env.Q402_MAX_AMOUNT_PER_CALL),
51
- allowedRecipients: parseAllowedRecipients(process.env.Q402_ALLOWED_RECIPIENTS)
105
+ relayBaseUrl: (ENV.Q402_RELAY_BASE_URL ?? DEFAULT_RELAY_BASE).replace(/\/$/, ""),
106
+ maxAmountPerCallUsd: parseMaxAmount(ENV.Q402_MAX_AMOUNT_PER_CALL),
107
+ allowedRecipients: parseAllowedRecipients(ENV.Q402_ALLOWED_RECIPIENTS)
52
108
  };
53
109
  }
54
110
  var CONFIG = loadConfig();
@@ -97,6 +153,10 @@ function isLiveModeFor(resolved) {
97
153
  return resolved.apiKey.startsWith("q402_live_");
98
154
  }
99
155
 
156
+ // src/version.ts
157
+ var PACKAGE_NAME = "@quackai/q402-mcp";
158
+ var PACKAGE_VERSION = "0.5.6";
159
+
100
160
  // src/tools/quote.ts
101
161
  import { z } from "zod";
102
162
 
@@ -748,7 +808,7 @@ async function runPay(input) {
748
808
  token: input.token
749
809
  });
750
810
  guardsApplied.push("mode=sandbox");
751
- const setupHint = resolved.sandboxReason ?? describeSandboxReason(resolved.apiKey ?? "");
811
+ const setupHint = resolved.sandboxReason ?? describeSandboxReason(resolved.apiKey ?? "", resolved.scope);
752
812
  return { result: result2, guardsApplied, setupHint };
753
813
  }
754
814
  const client = new Q402NodeClient({
@@ -765,17 +825,19 @@ async function runPay(input) {
765
825
  guardsApplied.push("mode=live");
766
826
  return { result, guardsApplied };
767
827
  }
768
- function describeSandboxReason(resolvedKey) {
828
+ function describeSandboxReason(resolvedKey, scope) {
769
829
  const missing = [];
770
830
  if (!resolvedKey.startsWith("q402_live_")) missing.push("a live API key (must start with q402_live_)");
771
831
  if (!CONFIG.privateKey) missing.push("Q402_PRIVATE_KEY");
772
832
  if (!CONFIG.realPaymentsRequested) missing.push("Q402_ENABLE_REAL_PAYMENTS=1");
773
833
  if (missing.length === 0) return "Sandbox mode active (no env state change needed).";
774
- return "Sandbox mode is active because the following env vars are missing or not yet set: " + missing.join(", ") + ". Get a live API key at https://q402.quackai.ai/dashboard.";
834
+ const tier = scope === "trial" ? "Free Trial" : "Multichain";
835
+ const url = scope === "trial" ? "https://q402.quackai.ai/event" : "https://q402.quackai.ai/payment";
836
+ return "Sandbox mode is active because the following env vars are missing or not yet set: " + missing.join(", ") + `. Get a live ${tier} key at ${url}, then call q402_doctor \u2014 it will walk the user through creating ~/.q402/mcp.env and pasting the key into the right slot.`;
775
837
  }
776
838
  var PAY_TOOL = {
777
839
  name: "q402_pay",
778
- description: "Send a gasless USDC, USDT, or RLUSD payment via Q402. Auto-routing: chain='bnb' + Q402_TRIAL_API_KEY set \u2192 Trial (free sponsored); anything else \u2192 Multichain (paid 9-chain). Same rule for q402_batch_pay. Set keyScope='trial' or 'multichain' to force one explicitly. Trial keys reject any non-BNB chain server-side with TRIAL_BNB_ONLY. Multichain keys cover avax, bnb, eth, xlayer, stable, mantle, injective, monad, scroll \u2014 USDC/USDT on most chains, RLUSD on Ethereum only, Injective USDT-only. SANDBOX BY DEFAULT \u2014 no funds move unless the resolved key is a live key (q402_live_*), Q402_PRIVATE_KEY is set, and Q402_ENABLE_REAL_PAYMENTS=1. The recipient receives the full amount; the sender pays $0 in gas. Note: the first q402_pay on a chain creates a persistent EIP-7702 delegation on the sender's EOA (set-code TX, Pectra). Subsequent payments on the same chain reuse it (gas-efficient). To remove the delegation later, call q402_clear_delegation. ALWAYS get explicit user confirmation of the exact recipient address, amount, chain, and token in conversation immediately before calling this tool.",
840
+ description: "Send a gasless USDC, USDT, or RLUSD payment via Q402. Auto-routing: chain='bnb' + Q402_TRIAL_API_KEY set \u2192 Trial (free sponsored); anything else \u2192 Multichain (paid 9-chain). Same rule for q402_batch_pay. Set keyScope='trial' or 'multichain' to force one explicitly. Trial keys reject any non-BNB chain server-side with TRIAL_BNB_ONLY. Multichain keys cover avax, bnb, eth, xlayer, stable, mantle, injective, monad, scroll \u2014 USDC/USDT on most chains, RLUSD on Ethereum only, Injective USDT-only. SANDBOX BY DEFAULT \u2014 no funds move unless the resolved key is a live key (q402_live_*), Q402_PRIVATE_KEY is set, and Q402_ENABLE_REAL_PAYMENTS=1. The recipient receives the full amount; the sender pays $0 in gas. After the first payment on a chain, follow-up payments on the same chain are faster and cheaper (Q402 reuses the wallet's setup); q402_clear_delegation resets it if the user ever asks. ALWAYS get explicit user confirmation of the exact recipient address, amount, chain, and token in conversation immediately before calling this tool.",
779
841
  inputSchema: {
780
842
  type: "object",
781
843
  properties: {
@@ -901,7 +963,7 @@ async function runBatchPay(input) {
901
963
  (r) => sandboxPay(chain, { to: r.to, amount: r.amount, token: input.token })
902
964
  );
903
965
  guardsApplied.push("mode=sandbox");
904
- const reason = resolved.sandboxReason ?? describeSandboxReason2(resolved.apiKey ?? "");
966
+ const reason = resolved.sandboxReason ?? describeSandboxReason2(resolved.apiKey ?? "", resolved.scope);
905
967
  return {
906
968
  mode: "sandbox",
907
969
  status: "sandbox",
@@ -950,17 +1012,19 @@ async function runBatchPay(input) {
950
1012
  throw err;
951
1013
  }
952
1014
  }
953
- function describeSandboxReason2(resolvedKey) {
1015
+ function describeSandboxReason2(resolvedKey, scope) {
954
1016
  const missing = [];
955
1017
  if (!resolvedKey.startsWith("q402_live_")) missing.push("a live API key (must start with q402_live_)");
956
1018
  if (!CONFIG.privateKey) missing.push("Q402_PRIVATE_KEY");
957
1019
  if (!CONFIG.realPaymentsRequested) missing.push("Q402_ENABLE_REAL_PAYMENTS=1");
958
1020
  if (missing.length === 0) return "Sandbox mode active (no env state change needed).";
959
- return "Sandbox mode is active because the following env vars are missing or not yet set: " + missing.join(", ") + ". Get a live API key at https://q402.quackai.ai/dashboard.";
1021
+ const tier = scope === "trial" ? "Free Trial" : "Multichain";
1022
+ const url = scope === "trial" ? "https://q402.quackai.ai/event" : "https://q402.quackai.ai/payment";
1023
+ return "Sandbox mode is active because the following env vars are missing or not yet set: " + missing.join(", ") + `. Get a live ${tier} key at ${url}, then call q402_doctor \u2014 it will walk the user through creating ~/.q402/mcp.env and pasting the key into the right slot.`;
960
1024
  }
961
1025
  var BATCH_PAY_TOOL = {
962
1026
  name: "q402_batch_pay",
963
- description: `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 EIP-7702 default 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. Note: same EIP-7702 delegation behaviour as q402_pay \u2014 the first call on a chain creates a persistent set-code delegation on the sender's EOA, reused by subsequent calls. Use q402_clear_delegation to remove. 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.`,
1027
+ description: `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. 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.`,
964
1028
  inputSchema: {
965
1029
  type: "object",
966
1030
  properties: {
@@ -1052,7 +1116,7 @@ async function runBalance() {
1052
1116
  apiKeyMasked: null,
1053
1117
  scopes: [],
1054
1118
  dashboardUrl: "https://q402.quackai.ai/dashboard",
1055
- setupHint: "Set Q402_TRIAL_API_KEY (BNB-only sponsored, free at /event) and/or Q402_MULTICHAIN_API_KEY (paid 9-chain from /dashboard). Single-env legacy: Q402_API_KEY also works."
1119
+ setupHint: "No API key configured. Call q402_doctor for guided setup \u2014 it will offer to create ~/.q402/mcp.env with placeholders that the user can fill in. (Manual path: set Q402_TRIAL_API_KEY for BNB-only sponsored (free at https://q402.quackai.ai/event) and/or Q402_MULTICHAIN_API_KEY for paid 9-chain (https://q402.quackai.ai/payment). Q402_API_KEY is the legacy single-env fallback.)"
1056
1120
  };
1057
1121
  }
1058
1122
  const scopes = await Promise.all(
@@ -1271,19 +1335,24 @@ async function runWalletStatus() {
1271
1335
  }
1272
1336
  const url = `${CONFIG.relayBaseUrl.replace(/\/$/, "")}/wallet/delegation-status?address=${address}`;
1273
1337
  let body;
1338
+ let res;
1274
1339
  try {
1275
- const res = await fetch(url);
1340
+ res = await fetch(url);
1276
1341
  body = await res.json();
1277
- if (!res.ok) {
1278
- return {
1279
- address,
1280
- error: typeof body === "object" && body && "error" in body ? String(body.error) : `HTTP ${res.status}`
1281
- };
1282
- }
1283
1342
  } catch (e) {
1284
1343
  return {
1285
1344
  address,
1286
- error: e instanceof Error ? e.message : String(e)
1345
+ error: "RELAY_UNREACHABLE",
1346
+ hint: `Could not reach Q402 at ${url}: ${e instanceof Error ? e.message : String(e)}`
1347
+ };
1348
+ }
1349
+ if (!res.ok) {
1350
+ const errBody = body;
1351
+ return {
1352
+ address,
1353
+ error: errBody.error ?? `HTTP ${res.status}`,
1354
+ hint: errBody.hint ?? errBody.reason,
1355
+ retryAfterSec: errBody.retryAfterSec
1287
1356
  };
1288
1357
  }
1289
1358
  const parsed = body;
@@ -1444,9 +1513,278 @@ var CLEAR_DELEGATION_TOOL = {
1444
1513
  }
1445
1514
  };
1446
1515
 
1516
+ // src/tools/doctor.ts
1517
+ import { z as z8 } from "zod";
1518
+ import { Wallet as Wallet4 } from "ethers";
1519
+ var DoctorInputSchema = z8.object({});
1520
+ var ENV_FILE_TEMPLATE = `# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1521
+ # Q402 MCP \u2014 secrets
1522
+ # Read automatically by @quackai/q402-mcp on startup.
1523
+ # Edit this file in your editor. NEVER paste your private key into chat.
1524
+ # After editing, restart your MCP client (Codex / Claude / Cursor / Cline).
1525
+ # \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1526
+
1527
+ # \u2500\u2500\u2500 API key \u2014 pick ONE (uncomment the one you have) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1528
+ # Free Trial: BNB Chain only, 2,000 sponsored TX
1529
+ # Get one at: https://q402.quackai.ai/event
1530
+ Q402_TRIAL_API_KEY=q402_live_...
1531
+
1532
+ # Paid Multichain: all 9 chains, per-chain Gas Tank
1533
+ # Get one at: https://q402.quackai.ai/payment
1534
+ # Q402_MULTICHAIN_API_KEY=q402_live_...
1535
+
1536
+ # \u2500\u2500\u2500 Your wallet \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1537
+ # Hex EVM private key. Signs payments LOCALLY on your machine.
1538
+ # Never leaves your device, never sent to any server.
1539
+ Q402_PRIVATE_KEY=0x...
1540
+
1541
+ # \u2500\u2500\u2500 Live mode flag \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1542
+ # Must be exactly "1" to allow real on-chain transactions.
1543
+ # Anything else = test response (fake hash, no funds move).
1544
+ Q402_ENABLE_REAL_PAYMENTS=1
1545
+
1546
+ # \u2500\u2500\u2500 Q402 relay endpoint \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1547
+ # Default canonical Q402 deployment. Only change for self-hosted.
1548
+ Q402_RELAY_BASE_URL=https://q402.quackai.ai/api
1549
+
1550
+ # \u2500\u2500\u2500 Optional safety guards \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1551
+ # Max USD per single q402_pay call (default: 5)
1552
+ # Q402_MAX_AMOUNT_PER_CALL=5
1553
+ #
1554
+ # Comma-separated lowercase recipient allowlist (unset = any address OK)
1555
+ # Q402_ALLOWED_RECIPIENTS=0xabc...,0xdef...
1556
+ `;
1557
+ var SECURITY_NOTICE = "Q402 never asks you to paste your private key into chat. The MCP server signs payments LOCALLY on your machine \u2014 your key never leaves your device, never goes to a remote server. If a key was already pasted in chat by mistake, treat the wallet as exposed: move funds to a fresh wallet and use that new key in ~/.q402/mcp.env going forward.";
1558
+ function envSource(name) {
1559
+ if (process.env[name] !== void 0) return "process";
1560
+ if (Q402_ENV_FILE_KEYS.has(name)) return "file";
1561
+ if (ENV[name] !== void 0) return "file";
1562
+ return "unset";
1563
+ }
1564
+ function envSlot(name, purpose) {
1565
+ const source = envSource(name);
1566
+ return { set: source !== "unset", source, purpose };
1567
+ }
1568
+ function mask2(key) {
1569
+ if (!key || key.length < 12) return key ?? "";
1570
+ return `${key.slice(0, 12)}\u2026${key.slice(-4)}`;
1571
+ }
1572
+ function detectPhase() {
1573
+ const anyKey = !!(CONFIG.trialApiKey || CONFIG.multichainApiKey || CONFIG.legacyApiKey);
1574
+ const allEssentials = anyKey && !!CONFIG.privateKey && CONFIG.realPaymentsRequested && CONFIG.apiKeyKind === "live";
1575
+ if (allEssentials) return "live-check";
1576
+ if (anyKey || CONFIG.privateKey || CONFIG.realPaymentsRequested) return "needs-completion";
1577
+ return "first-install";
1578
+ }
1579
+ async function verifyOneKey(scope, envVar, apiKey) {
1580
+ const url = `${CONFIG.relayBaseUrl}/keys/verify`;
1581
+ try {
1582
+ const resp = await fetch(url, {
1583
+ method: "POST",
1584
+ headers: { "Content-Type": "application/json" },
1585
+ body: JSON.stringify({ apiKey })
1586
+ });
1587
+ if (!resp.ok) {
1588
+ return { scope, envVar, apiKeyMasked: mask2(apiKey), valid: false, error: `HTTP ${resp.status}` };
1589
+ }
1590
+ const body = await resp.json();
1591
+ const result = {
1592
+ scope,
1593
+ envVar,
1594
+ apiKeyMasked: mask2(apiKey),
1595
+ valid: body.valid ?? false,
1596
+ plan: body.plan,
1597
+ remainingCredits: body.remainingCredits,
1598
+ isTrial: body.isTrial,
1599
+ trialExpiresAt: body.trialExpiresAt,
1600
+ trialDaysLeft: body.trialDaysLeft
1601
+ };
1602
+ if (scope === "multichain" && body.isTrial) {
1603
+ result.slotWarning = "Trial-tier key found in Q402_MULTICHAIN_API_KEY slot. This works but you'll lose the auto-routing benefit (BNB free via Trial). Move the key to Q402_TRIAL_API_KEY in ~/.q402/mcp.env.";
1604
+ } else if (scope === "trial" && body.isTrial === false && body.plan && body.plan !== "trial") {
1605
+ result.slotWarning = "Paid Multichain-tier key found in Q402_TRIAL_API_KEY slot. BNB payments will burn your paid quota instead of using free Trial sponsorship. Move the key to Q402_MULTICHAIN_API_KEY in ~/.q402/mcp.env.";
1606
+ }
1607
+ return result;
1608
+ } catch (e) {
1609
+ return {
1610
+ scope,
1611
+ envVar,
1612
+ apiKeyMasked: mask2(apiKey),
1613
+ valid: false,
1614
+ error: e instanceof Error ? e.message : String(e)
1615
+ };
1616
+ }
1617
+ }
1618
+ async function pingRelay() {
1619
+ const url = `${CONFIG.relayBaseUrl}/health`;
1620
+ const t0 = Date.now();
1621
+ try {
1622
+ const resp = await fetch(url, { method: "GET" });
1623
+ const reachable = resp.status < 500;
1624
+ return { url, reachable, latencyMs: Date.now() - t0 };
1625
+ } catch (e) {
1626
+ return {
1627
+ url,
1628
+ reachable: false,
1629
+ latencyMs: Date.now() - t0,
1630
+ error: e instanceof Error ? e.message : String(e)
1631
+ };
1632
+ }
1633
+ }
1634
+ async function fetchDelegation(address) {
1635
+ const url = `${CONFIG.relayBaseUrl}/wallet/delegation-status?address=${address}`;
1636
+ try {
1637
+ const resp = await fetch(url);
1638
+ if (!resp.ok) {
1639
+ return CHAIN_KEYS.map((chain) => ({ chain, delegated: false, error: `HTTP ${resp.status}` }));
1640
+ }
1641
+ const body = await resp.json();
1642
+ return CHAIN_KEYS.map((chain) => {
1643
+ const s = body.chains?.[chain];
1644
+ if (!s) return { chain, delegated: false, error: "missing from response" };
1645
+ return { chain, delegated: s.delegated, impl: s.impl, error: s.error };
1646
+ });
1647
+ } catch (e) {
1648
+ const error = e instanceof Error ? e.message : String(e);
1649
+ return CHAIN_KEYS.map((chain) => ({ chain, delegated: false, error }));
1650
+ }
1651
+ }
1652
+ async function runDoctor() {
1653
+ const phase = detectPhase();
1654
+ const envFile = {
1655
+ path: Q402_ENV_FILE_PATH,
1656
+ exists: Q402_ENV_FILE_PRESENT
1657
+ };
1658
+ const envState = {
1659
+ Q402_TRIAL_API_KEY: envSlot(
1660
+ "Q402_TRIAL_API_KEY",
1661
+ "Free Trial \u2014 BNB only, 2,000 sponsored TX. Get at https://q402.quackai.ai/event"
1662
+ ),
1663
+ Q402_MULTICHAIN_API_KEY: envSlot(
1664
+ "Q402_MULTICHAIN_API_KEY",
1665
+ "Paid Multichain \u2014 all 9 chains, per-chain Gas Tank. Get at https://q402.quackai.ai/payment"
1666
+ ),
1667
+ Q402_PRIVATE_KEY: envSlot(
1668
+ "Q402_PRIVATE_KEY",
1669
+ "Hex EVM private key. Signs LOCALLY on your machine \u2014 never leaves your device."
1670
+ ),
1671
+ Q402_ENABLE_REAL_PAYMENTS: envSlot(
1672
+ "Q402_ENABLE_REAL_PAYMENTS",
1673
+ "Must be '1' to allow real TX. Anything else = test response (fake hash)."
1674
+ )
1675
+ };
1676
+ let legacyDetected;
1677
+ if (CONFIG.legacyApiKey) {
1678
+ legacyDetected = "Q402_API_KEY is set \u2014 works as a fallback for both scopes, but the newer two-key model (Q402_TRIAL_API_KEY + Q402_MULTICHAIN_API_KEY) gives you auto-routing between free Trial (BNB) and paid Multichain. Rename in ~/.q402/mcp.env when convenient.";
1679
+ }
1680
+ const missing = [];
1681
+ if (!CONFIG.trialApiKey && !CONFIG.multichainApiKey && !CONFIG.legacyApiKey) {
1682
+ missing.push(
1683
+ "An API key (Q402_TRIAL_API_KEY for free BNB OR Q402_MULTICHAIN_API_KEY for paid 9-chain)"
1684
+ );
1685
+ }
1686
+ if (!CONFIG.privateKey) missing.push("Q402_PRIVATE_KEY");
1687
+ if (!CONFIG.realPaymentsRequested) missing.push("Q402_ENABLE_REAL_PAYMENTS=1");
1688
+ const recommendedActions = [];
1689
+ if (!envFile.exists) {
1690
+ recommendedActions.push({
1691
+ id: "create-env-file",
1692
+ type: "write_file",
1693
+ path: Q402_ENV_FILE_PATH,
1694
+ createParentDirs: true,
1695
+ content: ENV_FILE_TEMPLATE,
1696
+ requiresUserConfirm: true,
1697
+ description: "Create ~/.q402/mcp.env with placeholder values, then open it in the user's editor.",
1698
+ ifExists: "skip"
1699
+ });
1700
+ }
1701
+ const warnings = [];
1702
+ if (phase !== "live-check") {
1703
+ return {
1704
+ package: PACKAGE_NAME,
1705
+ version: PACKAGE_VERSION,
1706
+ phase,
1707
+ ready: false,
1708
+ envFile,
1709
+ envState,
1710
+ missing,
1711
+ legacyDetected,
1712
+ warnings,
1713
+ recommendedActions,
1714
+ greeting: phase === "first-install" ? `Q402 MCP is installed (v${PACKAGE_VERSION}).` : `Q402 MCP is installed (v${PACKAGE_VERSION}) \u2014 partially configured.`,
1715
+ nextStep: phase === "first-install" ? "Offer to create ~/.q402/mcp.env. After yes, run the recommendedActions[].write_file action, then open the file in the user's editor (e.g. via `code` / `open` / `start` / `xdg-open`). Then walk through filling in the API key and private key, one at a time. Do NOT accept key values via chat \u2014 direct the user to edit the file in their editor." : `Tell the user which env vars are still missing (from the 'missing' list) and how to add them to ~/.q402/mcp.env. Restart needed after editing.`,
1716
+ securityNotice: SECURITY_NOTICE
1717
+ };
1718
+ }
1719
+ let walletAddress;
1720
+ try {
1721
+ walletAddress = new Wallet4(CONFIG.privateKey).address;
1722
+ } catch {
1723
+ warnings.push("Q402_PRIVATE_KEY is set but does not parse as a 32-byte hex key. Live calls will fail.");
1724
+ }
1725
+ const verifyTargets = [];
1726
+ if (CONFIG.trialApiKey) verifyTargets.push({ scope: "trial", envVar: "Q402_TRIAL_API_KEY", key: CONFIG.trialApiKey });
1727
+ if (CONFIG.multichainApiKey) verifyTargets.push({ scope: "multichain", envVar: "Q402_MULTICHAIN_API_KEY", key: CONFIG.multichainApiKey });
1728
+ if (verifyTargets.length === 0 && CONFIG.legacyApiKey) {
1729
+ verifyTargets.push({ scope: "legacy", envVar: "Q402_API_KEY", key: CONFIG.legacyApiKey });
1730
+ }
1731
+ const [keys, delegation, relay] = await Promise.all([
1732
+ Promise.all(verifyTargets.map((t) => verifyOneKey(t.scope, t.envVar, t.key))),
1733
+ walletAddress ? fetchDelegation(walletAddress) : Promise.resolve([]),
1734
+ pingRelay()
1735
+ ]);
1736
+ for (const k of keys) if (k.slotWarning) warnings.push(k.slotWarning);
1737
+ for (const k of keys) {
1738
+ if (typeof k.remainingCredits === "number" && k.remainingCredits === 0) {
1739
+ warnings.push(
1740
+ `${k.envVar} has 0 credits remaining. ` + (k.isTrial ? "Trial allotment exhausted \u2014 upgrade to a Multichain plan at https://q402.quackai.ai/payment." : "Paid plan quota exhausted \u2014 top up at https://q402.quackai.ai/dashboard?tab=billing.")
1741
+ );
1742
+ } else if (typeof k.remainingCredits === "number" && k.remainingCredits > 0 && k.remainingCredits < 50) {
1743
+ warnings.push(
1744
+ `${k.envVar} has only ${k.remainingCredits} credits left \u2014 top up before you run out.`
1745
+ );
1746
+ }
1747
+ if (!k.valid && !k.error) {
1748
+ warnings.push(`${k.envVar} verified as invalid by the relay \u2014 check the key value in ~/.q402/mcp.env.`);
1749
+ }
1750
+ }
1751
+ if (relay && !relay.reachable) {
1752
+ warnings.push(
1753
+ `Q402 relay at ${relay.url} is unreachable. Check your network or override with Q402_RELAY_BASE_URL if you self-host.`
1754
+ );
1755
+ }
1756
+ const ready = warnings.length === 0 && keys.some((k) => k.valid);
1757
+ return {
1758
+ package: PACKAGE_NAME,
1759
+ version: PACKAGE_VERSION,
1760
+ phase,
1761
+ ready,
1762
+ envFile,
1763
+ envState,
1764
+ missing,
1765
+ legacyDetected,
1766
+ wallet: walletAddress ? { address: walletAddress } : void 0,
1767
+ keys,
1768
+ delegation,
1769
+ relay,
1770
+ warnings,
1771
+ recommendedActions,
1772
+ greeting: ready ? `Q402 MCP is ready (v${PACKAGE_VERSION}).` : `Q402 MCP is installed but has ${warnings.length} issue${warnings.length === 1 ? "" : "s"} to address.`,
1773
+ nextStep: ready ? "Summarize the wallet address, plan tier(s), remaining quota, and any non-zero delegation counts to the user as a checklist. Then offer to make a test payment via q402_quote." : "Walk the user through each warning in order. For slot-mismatch warnings, the fix is editing ~/.q402/mcp.env and restarting the client.",
1774
+ securityNotice: SECURITY_NOTICE
1775
+ };
1776
+ }
1777
+ var DOCTOR_TOOL = {
1778
+ name: "q402_doctor",
1779
+ description: 'Run a Q402 health check \u2014 covers first-install onboarding AND ongoing diagnostics in one tool. Read-only, no API key required. Detects the current phase (first-install / needs-completion / live-check) and tailors output to it. \n\nUse when the user says any of: "set up Q402", "verify Q402", "why isn\'t Q402 working", "Q402 status", "check Q402". This is the FIRST tool to call after install, BEFORE q402_pay or q402_balance \u2014 it tells the agent what state the user is in. \n\nMulti-turn pattern the AI should follow when phase = first-install: (1) Tell user MCP is installed. (2) Ask one yes/no question: \'Want me to create your secrets file at ~/.q402/mcp.env?\' (3) On yes, execute the recommendedActions[].write_file action using the client\'s own filesystem tool, then open the file in the user\'s editor (e.g. `code ~/.q402/mcp.env`, `open` on macOS, `start` on Windows, `xdg-open` on Linux). (4) Guide the user through getting an API key (free Trial at https://q402.quackai.ai/event OR paid Multichain at /payment) and pasting it into the file (in their editor \u2014 NEVER in chat). (5) Same for the private key. (6) Tell them to save + restart the MCP client. (7) Call q402_doctor again to verify. \n\nSecurity policy carried in the response: AI MUST surface the securityNotice when first walking through setup. If the user pastes a private key directly in chat, DO NOT refuse \u2014 the exposure already happened. Help them by directing them to put it in the file themselves (via their editor), and inform them the chat history now contains the key (most clients store this locally, some sync to cloud) so they should treat the wallet as exposed if it holds valuables. \n\nLive-check phase additionally returns per-scope quota, EIP-7702 delegation state per chain, relay reachability, and slot-mismatch warnings (e.g. Trial key in Multichain slot silently burns paid quota \u2014 surface this to the user).',
1780
+ inputSchema: {
1781
+ type: "object",
1782
+ properties: {},
1783
+ additionalProperties: false
1784
+ }
1785
+ };
1786
+
1447
1787
  // src/index.ts
1448
- var PACKAGE_NAME = "@quackai/q402-mcp";
1449
- var PACKAGE_VERSION = "0.5.4";
1450
1788
  function jsonText(value) {
1451
1789
  return { type: "text", text: JSON.stringify(value, null, 2) };
1452
1790
  }
@@ -1457,6 +1795,9 @@ async function main() {
1457
1795
  );
1458
1796
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
1459
1797
  tools: [
1798
+ // doctor first — it's the bootstrap tool: any "set up Q402" / "is Q402
1799
+ // working" prompt should land here before quote/balance/pay are tried.
1800
+ DOCTOR_TOOL,
1460
1801
  QUOTE_TOOL,
1461
1802
  BALANCE_TOOL,
1462
1803
  PAY_TOOL,
@@ -1470,6 +1811,10 @@ async function main() {
1470
1811
  const { name, arguments: args } = req.params;
1471
1812
  try {
1472
1813
  switch (name) {
1814
+ case "q402_doctor": {
1815
+ DoctorInputSchema.parse(args ?? {});
1816
+ return { content: [jsonText(await runDoctor())] };
1817
+ }
1473
1818
  case "q402_quote": {
1474
1819
  const parsed = QuoteInputSchema.parse(args ?? {});
1475
1820
  return { content: [jsonText(runQuote(parsed))] };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quackai/q402-mcp",
3
- "version": "0.5.4",
3
+ "version": "0.5.6",
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": [