@aifinpay/mcp 1.5.1 → 2.0.0-rc.12

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 (68) hide show
  1. package/README.md +116 -69
  2. package/bin/aifinpay-mcp.js +231 -4
  3. package/dist/api.d.ts +2 -0
  4. package/dist/api.d.ts.map +1 -0
  5. package/dist/api.js +6 -0
  6. package/dist/api.js.map +1 -0
  7. package/dist/config.d.ts +35 -2
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/config.js +63 -0
  10. package/dist/config.js.map +1 -1
  11. package/dist/identity.d.ts +9 -0
  12. package/dist/identity.d.ts.map +1 -0
  13. package/dist/identity.js +75 -0
  14. package/dist/identity.js.map +1 -0
  15. package/dist/index.d.ts +3 -6
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +3 -6
  18. package/dist/index.js.map +1 -1
  19. package/dist/safe-fetch.d.ts +2 -0
  20. package/dist/safe-fetch.d.ts.map +1 -1
  21. package/dist/safe-fetch.js +76 -17
  22. package/dist/safe-fetch.js.map +1 -1
  23. package/dist/server.d.ts +11 -8
  24. package/dist/server.d.ts.map +1 -1
  25. package/dist/server.js +106 -102
  26. package/dist/server.js.map +1 -1
  27. package/dist/tools/agent-address.d.ts.map +1 -1
  28. package/dist/tools/agent-address.js +6 -27
  29. package/dist/tools/agent-address.js.map +1 -1
  30. package/dist/tools/{payable-fetch.d.ts → agent-history.d.ts} +21 -23
  31. package/dist/tools/agent-history.d.ts.map +1 -0
  32. package/dist/tools/agent-history.js +73 -0
  33. package/dist/tools/agent-history.js.map +1 -0
  34. package/dist/tools/{agent-quote.d.ts → agent-quota.d.ts} +7 -8
  35. package/dist/tools/agent-quota.d.ts.map +1 -0
  36. package/dist/tools/agent-quota.js +95 -0
  37. package/dist/tools/agent-quota.js.map +1 -0
  38. package/dist/tools/dev-payment-quote.d.ts +39 -0
  39. package/dist/tools/dev-payment-quote.d.ts.map +1 -0
  40. package/dist/tools/dev-payment-quote.js +52 -0
  41. package/dist/tools/dev-payment-quote.js.map +1 -0
  42. package/dist/tools/{pay-with-split.d.ts → production-control.d.ts} +49 -34
  43. package/dist/tools/production-control.d.ts.map +1 -0
  44. package/dist/tools/production-control.js +132 -0
  45. package/dist/tools/production-control.js.map +1 -0
  46. package/package.json +21 -10
  47. package/skills/SKILL.md +143 -0
  48. package/dist/directory.d.ts +0 -55
  49. package/dist/directory.d.ts.map +0 -1
  50. package/dist/directory.js +0 -56
  51. package/dist/directory.js.map +0 -1
  52. package/dist/tools/agent-call.d.ts +0 -68
  53. package/dist/tools/agent-call.d.ts.map +0 -1
  54. package/dist/tools/agent-call.js +0 -129
  55. package/dist/tools/agent-call.js.map +0 -1
  56. package/dist/tools/agent-claim-self.d.ts +0 -66
  57. package/dist/tools/agent-claim-self.d.ts.map +0 -1
  58. package/dist/tools/agent-claim-self.js +0 -299
  59. package/dist/tools/agent-claim-self.js.map +0 -1
  60. package/dist/tools/agent-quote.d.ts.map +0 -1
  61. package/dist/tools/agent-quote.js +0 -81
  62. package/dist/tools/agent-quote.js.map +0 -1
  63. package/dist/tools/pay-with-split.d.ts.map +0 -1
  64. package/dist/tools/pay-with-split.js +0 -138
  65. package/dist/tools/pay-with-split.js.map +0 -1
  66. package/dist/tools/payable-fetch.d.ts.map +0 -1
  67. package/dist/tools/payable-fetch.js +0 -107
  68. package/dist/tools/payable-fetch.js.map +0 -1
package/README.md CHANGED
@@ -1,109 +1,156 @@
1
1
  # @aifinpay/mcp
2
2
 
3
- MCP server exposing AiFinPay's autonomous x402 payment loop as
4
- agent-callable tools. Drop it into Claude Desktop, MCP Inspector, or any
5
- MCP-aware agent runtime — your agent can now buy services autonomously.
6
-
7
- Canonical domain: **aifinpay.io** (the legacy `aifinpay.company` host is
8
- retired — ignore any docs pointing there). SDK settlement runs on
9
- **Polygon (default) and Solana**; the underlying Node SDK (≥ 1.3.0) also
10
- supports direct splitter settlement on Base, Optimism, Unichain, BOT Chain
11
- and XRPL EVM (native-token path), while the MCP `pay_with_split` /
12
- `quote_split` tools stay Polygon + Solana (backend invoice flow). The
13
- protocol itself is live across 13 networks — see
14
- [aifinpay.io/llms.txt](https://aifinpay.io/llms.txt).
3
+ AiFinPay MCP server for persistent agent identity, Agent Passport resolution,
4
+ route discovery and non-signing settlement invoices. Canonical domain:
5
+ **aifinpay.io**.
6
+
7
+ This 2.0 release candidate does not expose payment-signing tools. Returning a
8
+ wallet address or preparing an invoice does not authorize or execute a payment.
9
+ Signing remains gated on the verified SDK v2 executor and its release evidence.
10
+ Local MCP tools for wallet discovery, payment history, quotas and non-signing
11
+ payment preparation. Read the bundled [skill](skills/SKILL.md), also exposed
12
+ as the MCP resource `aifinpay://skill`.
15
13
 
16
14
  ## Tools
17
15
 
18
- | Tool | What it does |
16
+ | Tool | Purpose |
19
17
  |---|---|
20
- | `payable_fetch(url, opts?)` | Fetch any URL. On 402, auto-detect facilitator, sign, retry. |
21
- | `agent_address()` | Return the agent's funding addresses on **both** chains — Polygon `0x…` (default settlement: io.net, Exa, Venice bridges) and Solana base58 (Seat PDA / leaderboard). One seed, two chains — fund either. |
22
- | `agent_quote(url)` | Inspect a 402 challenge without paying. Shows the merchant's quoted amount + facilitator flavor. |
23
- | `agent_call(provider, …)` | Call a live provider from the AiFinPay directory (io.net, Exa, Venice, …) with automatic payment. |
24
- | `pay_with_split(chain, merchant, amount, …)` | Direct fee-on-top payment to any merchant wallet — merchant receives 100% of the amount, AiFinPay adds 1% on top. |
25
- | `quote_split(chain, amount)` | Preview the exact on-chain amounts of a `pay_with_split` before paying. |
26
- | `agent_claim_self(magic_link_url)` | Link this agent to your dashboard account via a one-shot magic link from `aifinpay.io/login` — spend history shows up in the dashboard. |
27
-
28
- ## Install
29
-
18
+ | `agent_address` | Read the current Solana, EVM and Casper addresses. |
19
+ | `agent_reload` | Reload local wallet files in the current MCP connection. |
20
+ | `agent_quota` | Read the agent's quota. |
21
+ | `agent_passport_resolve` | Resolve the global Agent Passport identity. |
22
+ | `settlement_routes` | Read the available verified settlement routes. |
23
+ | `settlement_invoice` | Prepare a non-signing settlement invoice. |
24
+
25
+ Legacy `payable_fetch`, `agent_call`, `agent_quote`, `pay_with_split`,
26
+ `quote_split` and `agent_claim_self` tools are not registered by this RC.
27
+
28
+ ## Persistent wallet selection
29
+
30
+ The CLI (`init` and stdio), programmatic server and `agent_reload` use the same
31
+ priority:
32
+
33
+ 1. `SEED_HASH`: an existing 32-byte seed encoded as 64 hex characters, optionally
34
+ prefixed with `0x`. It is passed directly to `AiFinPayAgent.fromSeed`; do not
35
+ hash it again or supply a mnemonic.
36
+ 2. `./aifinpay/agents.json`, relative to the MCP process's working directory.
37
+ Set an absolute `AIFINPAY_AGENTS_FILE` when a desktop client's working
38
+ directory differs from the project directory. The file contains an `agents`
39
+ array of records with `id` and private `seed_hash` fields. If there is more
40
+ than one record, select exactly one with `AIFINPAY_AGENT_ID`.
41
+ 3. `AIFINPAY_AGENT_SECRET`: a legacy base58 Solana secret.
42
+ 4. `~/.aifinpay/agent.json`, or `AIFINPAY_HOME/agent.json`: the legacy CLI
43
+ keystore. Encrypted files require `AIFINPAY_WALLET_PASSPHRASE`.
44
+
45
+ Keep seeds and wallet files private and out of chat, source control and logs.
46
+ Invalid or ambiguous configured inputs fail; they never generate a replacement
47
+ wallet. With no configured wallet at all the server has an ephemeral identity:
48
+ **do not fund it**.
49
+
50
+ ## Initialize and connect
51
+ This RC does not register payment-signing tools. Its legacy SDK dependency
52
+ supplies wallet derivation only; installing this MCP does not enable v2
53
+ settlement. An invoice or quote is not a completed payment.
54
+
55
+ ## Local configuration
30
56
  ```bash
31
- # Globally — usable as `npx @aifinpay/mcp` from any client config
32
- # (installs the latest stable — the old @alpha tag is retired, don't use it)
33
- npm install -g @aifinpay/mcp
57
+ npx @aifinpay/mcp@next init
34
58
  ```
35
59
 
36
- ## Use with Claude Desktop
60
+ Select the wallet in this order: `SEED_HASH` → `./aifinpay/agents.json` →
61
+ legacy `AIFINPAY_AGENT_SECRET` → `~/.aifinpay/agent.json`.
62
+ SEED_HASH means a 32-byte hex seed, passed directly to SDK `fromSeed`.
63
+ See the skill for the exact project-file schema and multi-agent selection.
64
+ `init` prints the selected wallet's public addresses. If a seed or project wallet
65
+ is already configured, it does not create a second legacy wallet. With no
66
+ wallet, it creates the legacy keystore with mode `600`; existing keystores are
67
+ retained. Back up that file privately. The `@next` tag selects the 2.0 RC lane;
68
+ use the release containing these changes, or build this source checkout.
37
69
 
38
- Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
70
+ Use an absolute `AIFINPAY_AGENTS_FILE` path when the host's working directory
71
+ is not your project directory. Do not put the seed itself in a shared config.
72
+ A client configuration can use the keystore without embedding its secret:
39
73
 
40
74
  ```json
41
75
  {
42
76
  "mcpServers": {
43
77
  "aifinpay": {
44
78
  "command": "npx",
45
- "args": ["@aifinpay/mcp"],
79
+ "args": ["-y", "@aifinpay/mcp@2.0.0-rc.12"],
46
80
  "env": {
47
- "AIFINPAY_AGENT_SECRET": "<base58 secret — see below>",
48
- "AIFINPAY_MAX_USD": "0.50"
81
+ "AIFINPAY_AGENTS_FILE": "/absolute/project/aifinpay/agents.json",
82
+ "AIFINPAY_AGENT_ID": "research-agent"
49
83
  }
50
84
  }
51
85
  }
52
86
  }
53
87
  ```
54
88
 
55
- Restart Claude Desktop. Now Claude can call `payable_fetch`, `agent_address`,
56
- and `agent_quote` like any other tool.
89
+ The release above must be published before this npx command can install it.
90
+ For a source checkout, run `npm ci && npm run build` in mcp/ and configure
91
+ `node /absolute/path/to/mcp/bin/aifinpay-mcp.js` instead.
57
92
 
58
- ## First run — generating an agent
93
+ If using the legacy keystore, run `npx @aifinpay/mcp init` once and omit the
94
+ project-file variables. Optional `AIFINPAY_WALLET_PASSPHRASE` encrypts that
95
+ legacy file at creation. An already connected server can load it with
96
+ `agent_reload`; it does not need a new conversation. A changed package or
97
+ launch environment requires a server reconnect, subject to host support.
59
98
 
60
- If `AIFINPAY_AGENT_SECRET` is not set, the server generates an ephemeral
61
- keypair and **prints it to stderr** at startup:
62
-
63
- ```
64
- [warn] no AIFINPAY_AGENT_SECRET set — generated EPHEMERAL agent.
65
- address: 9xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
66
- secret: 4xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
67
- >> Save this secret to AIFINPAY_AGENT_SECRET to keep the agent across restarts.
68
- ```
99
+ ## History
100
+ Connect the MCP server once. After `init` or a wallet file update, call
101
+ `agent_reload` in that same connection; no new conversation is required. A
102
+ failed reload keeps the previous wallet. Changed shell environment variables
103
+ require reconnecting the MCP process, since its environment is a startup
104
+ snapshot. Verify `agent_address` against the wallet you intend to use.
69
105
 
70
- Save the secret to `AIFINPAY_AGENT_SECRET` in your client config so the
71
- agent identity (and any funded Seat) persists across restarts.
106
+ ## Other environment variables
72
107
 
73
- ## Environment variables
74
-
75
- | Var | Default | Purpose |
108
+ | Variable | Default | Purpose |
76
109
  |---|---|---|
77
- | `AIFINPAY_AGENT_SECRET` | — | Base58 secret. If absent → ephemeral agent printed to stderr. |
78
- | `AIFINPAY_BASE_URL` | `https://aifinpay.io` | Backend URL for nonce + funding probes. |
110
+ | `AIFINPAY_BASE_URL` | `https://aifinpay.io` | Backend URL. |
79
111
  | `AIFINPAY_TIMEOUT_MS` | `30000` | Request timeout. |
80
- | `AIFINPAY_MAX_USD` | — | Hard cap per single payment. Strongly recommended. |
112
+ | `AIFINPAY_MAX_USD` | — | Configures the underlying agent's per-call cap; it does not enable signing. |
113
+ | `AIFINPAY_GATEWAY_ORIGINS` | SDK default | Comma-separated exact HTTPS origins trusted for AIFP-1 settlement. |
114
+ | `AIFINPAY_GATEWAY_PATH_MODE` | `gateway` | Use `gateway` for merchant-slug identity or `direct` for full request-path identity. |
81
115
 
82
- ## Programmatic use
116
+ `agent_history({address, source:"transactions"})` reads indexed Polygon
117
+ settlements; `source:"receipts"` reads retained batches, including test-mode
118
+ payments. Pass `passport` instead of, or alongside, `address` to resolve a
119
+ public passport first. A backend without the passport resolver cannot satisfy
120
+ passport-only queries. No secret API key is needed for public metadata.
83
121
 
84
- ```ts
85
- import { createServer, loadConfigFromEnv } from "@aifinpay/mcp";
86
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
122
+ Responses do not include bearer receipts. Limits are 1..100; follow
123
+ `next_offset`. History source/coverage is explicit: it is not a full wallet
124
+ explorer. Dev networks currently use receipt history, not the Polygon ledger.
87
125
 
88
- const { server } = await createServer({
89
- ...loadConfigFromEnv(),
90
- agentSecretB58: "your-secret-here",
91
- maxAmountUsd: 0.10,
92
- });
126
+ ```
127
+ const { server } = await createServer(loadConfigFromEnv());
93
128
  await server.connect(new StdioServerTransport());
94
129
  ```
95
130
 
96
- ## How `payable_fetch` works
131
+ The programmatic equivalents of identity environment options are `seedHash`,
132
+ `agentsFile`, `agentId`, `agentSecretB58`, `walletHome` and `walletPassphrase`.
133
+
134
+
135
+ - AIFINPAY_BASE_URL: backend origin; default https://aifinpay.io.
136
+ - AIFINPAY_HOME: directory of the legacy agent.json keystore.
137
+ - AIFINPAY_MODE=dev: expose dev_payment_quote; requires a separate dev base URL.
138
+
139
+ - AIFINPAY_TIMEOUT_MS: SDK request timeout.
140
+ - AIFINPAY_MAX_USD: legacy per-call budget; does not enable signing.
141
+ - AIFINPAY_TRUSTED_HOSTS: exact hosts allowed to bypass the DNS pre-check.
142
+ - AIFINPAY_ALLOW_PRIVATE_FETCH=1: explicit local-development network access.
143
+ Never enable this on the public hosted MCP.
97
144
 
98
- 1. Sends the request unauthenticated.
99
- 2. On `402`, the underlying [`@aifinpay/agent`](../node) SDK detects the
100
- facilitator flavor (AiFinPay native, Coinbase x402, …).
101
- 3. Signs a payment payload and retries.
102
- 4. Returns `{ status, ok, headers, body }` to the agent.
145
+ For a self-hosted AIFP-1 gateway, set `AIFINPAY_GATEWAY_ORIGINS` to exact HTTPS
146
+ origins. `AIFINPAY_GATEWAY_PATH_MODE=direct` makes the process use each request's
147
+ full pathname as its AIFP-1 resource identity; `gateway` uses the merchant slug.
148
+ The mode is process-wide, does not broaden the trusted-origin allowlist, and
149
+ does not enable the retired signing tools in the production RC.
103
150
 
104
- The flow is identical to calling `agent.pay(url)` directly — this
105
- package just wraps it as an MCP tool surface so LLM agents can call it
106
- without writing payment code.
151
+ Dev quoting does not bypass wallet signatures, issuer verification or
152
+ receipt metering. See the bundled skill for backend prerequisites and the
153
+ remaining Amoy executor requirement.
107
154
 
108
155
  ## License
109
156
 
@@ -4,14 +4,241 @@
4
4
  * in stdio mode. Compatible with Claude Desktop, MCP Inspector, and any
5
5
  * MCP-aware agent runtime.
6
6
  *
7
- * Configure via env:
8
- * AIFINPAY_AGENT_SECRET base58 secret (load existing identity)
7
+ * Two commands, and the first one only has to be run once:
8
+ *
9
+ * npx @aifinpay/mcp init create a persistent wallet, print the config
10
+ * npx @aifinpay/mcp start the server
11
+ *
12
+ * WHY `init` EXISTS
13
+ *
14
+ * Running the server with nothing configured produced an EPHEMERAL wallet and
15
+ * a "DO NOT FUND" warning — correct, and a dead end. The warning pointed at
16
+ * `aifinpay init`, which is not published to npm: `@aifinpay/cli` and
17
+ * `aifinpay` both 404. So the documented way out of the dead end did not exist,
18
+ * and the only real path was to write a Node script against the SDK, which is
19
+ * not something you ask of someone whose goal is "give my agent a wallet".
20
+ *
21
+ * The gap was exactly one subcommand. This is it.
22
+ *
23
+ * Configure via env (all optional; the secret is read from the keystore below
24
+ * when the variable is unset):
25
+ * SEED_HASH 32-byte hex seed — highest priority
26
+ * AIFINPAY_AGENTS_FILE project agents file — second priority
27
+ * AIFINPAY_AGENT_SECRET legacy base58 secret — overrides the legacy keystore
9
28
  * AIFINPAY_BASE_URL default https://aifinpay.io
10
29
  * AIFINPAY_TIMEOUT_MS default 30000
11
30
  * AIFINPAY_MAX_USD hard cap per single payment (no default)
12
31
  */
13
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
14
- import { createServer, loadConfigFromEnv } from "../dist/index.js";
32
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, openSync, closeSync, fchmodSync, fsyncSync, linkSync, unlinkSync } from "node:fs";
33
+ import { join } from "node:path";
34
+ import { homedir } from "node:os";
35
+ import { createRequire } from "node:module";
36
+ import { scryptSync, randomBytes, createCipheriv, createDecipheriv } from "node:crypto";
37
+
38
+ const HOME = process.env.AIFINPAY_HOME || join(homedir(), ".aifinpay");
39
+ const KEYSTORE = join(HOME, "agent.json");
40
+
41
+ const require_ = createRequire(import.meta.url);
42
+ const VERSION = (() => {
43
+ try {
44
+ return require_("../package.json").version;
45
+ } catch {
46
+ return "unknown";
47
+ }
48
+ })();
49
+
50
+ const arg = (process.argv[2] || "").toLowerCase();
51
+
52
+ // ── help / version ────────────────────────────────────────────────────────
53
+ // These used to start the server. `--help` launching a stdio server is not a
54
+ // harmless quirk: the process appears to hang, because it is waiting for MCP
55
+ // framing on stdin that a human is never going to type.
56
+
57
+ if (arg === "--help" || arg === "-h" || arg === "help") {
58
+ process.stdout.write(`aifinpay-mcp ${VERSION}
59
+
60
+ npx @aifinpay/mcp init create a persistent wallet and print the config
61
+ npx @aifinpay/mcp start the MCP server (stdio)
62
+
63
+ Run init once. It writes ${KEYSTORE} with mode 600 and the server picks
64
+ it up automatically — you do not have to put the secret in a config file.
65
+
66
+ Env (all optional):
67
+ SEED_HASH 32-byte hex seed; highest priority\n AIFINPAY_AGENTS_FILE default ./aifinpay/agents.json; second priority\n AIFINPAY_AGENT_ID select one record when the file has multiple agents\n AIFINPAY_AGENT_SECRET legacy base58 secret; after project wallet sources
68
+ AIFINPAY_MAX_USD hard cap per single payment — set this
69
+ AIFINPAY_BASE_URL default https://aifinpay.io
70
+ AIFINPAY_TIMEOUT_MS default 30000
71
+ AIFINPAY_HOME default ~/.aifinpay
72
+ `);
73
+ process.exit(0);
74
+ }
75
+
76
+ if (arg === "--version" || arg === "-v" || arg === "version") {
77
+ process.stdout.write(`${VERSION}\n`);
78
+ process.exit(0);
79
+ }
80
+
81
+ // ── init ──────────────────────────────────────────────────────────────────
82
+
83
+ const PASSPHRASE = process.env.AIFINPAY_WALLET_PASSPHRASE || null;
84
+
85
+ // Optional at-rest encryption. Mode 600 keeps the keystore from OTHER users; it
86
+ // does nothing against malware running as YOU — the threat the OSINT write-up of
87
+ // a compromised dev machine described. A passphrase makes the on-disk file
88
+ // ciphertext; the secret is only in memory while the passphrase is supplied.
89
+ // Opt-in and env-supplied so the non-interactive `npx @aifinpay/mcp` launch path
90
+ // is unaffected: no passphrase => plaintext, exactly as before. scrypt to
91
+ // stretch, AES-256-GCM so tampering is detected rather than decrypting to junk.
92
+ function encryptSecret(secretB58) {
93
+ const salt = randomBytes(16), iv = randomBytes(12);
94
+ const key = scryptSync(PASSPHRASE, salt, 32, { N: 1 << 15, r: 8, p: 1, maxmem: 64 * 1024 * 1024 });
95
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
96
+ const ct = Buffer.concat([cipher.update(secretB58, "utf8"), cipher.final()]);
97
+ return { enc: "scrypt-aes-256-gcm", salt: salt.toString("base64"), iv: iv.toString("base64"),
98
+ tag: cipher.getAuthTag().toString("base64"), ct: ct.toString("base64"),
99
+ created: new Date().toISOString() };
100
+ }
101
+ function decryptSecret(store) {
102
+ if (!PASSPHRASE) throw new Error(`${KEYSTORE} is encrypted but AIFINPAY_WALLET_PASSPHRASE is not set.`);
103
+ const key = scryptSync(PASSPHRASE, Buffer.from(store.salt, "base64"), 32, { N: 1 << 15, r: 8, p: 1, maxmem: 64 * 1024 * 1024 });
104
+ const dc = createDecipheriv("aes-256-gcm", key, Buffer.from(store.iv, "base64"));
105
+ dc.setAuthTag(Buffer.from(store.tag, "base64"));
106
+ try { return Buffer.concat([dc.update(Buffer.from(store.ct, "base64")), dc.final()]).toString("utf8"); }
107
+ catch { throw new Error(`could not decrypt ${KEYSTORE}: wrong AIFINPAY_WALLET_PASSPHRASE or the file was modified.`); }
108
+ }
109
+
110
+ function readKeystore() {
111
+ if (!existsSync(KEYSTORE)) return null;
112
+ let parsed;
113
+ try { parsed = JSON.parse(readFileSync(KEYSTORE, "utf8")); } catch { return null; }
114
+ // A decrypt failure THROWS rather than returning null — null reads as "no
115
+ // wallet" and mints a new one, which is how a mistyped passphrase loses a key.
116
+ if (parsed?.enc) return { secretB58: decryptSecret(parsed), created: parsed.created, encrypted: true };
117
+ return typeof parsed?.secretB58 === "string" ? parsed : null;
118
+ }
119
+
120
+ if (arg === "init") {
121
+ const { Agent, AiFinPayAgent } = await import("@aifinpay/agent");
122
+
123
+ const { loadConfigFromEnv } = await import("../dist/config.js");
124
+ const { loadWalletIdentity } = await import("../dist/identity.js");
125
+ const selected = loadWalletIdentity(loadConfigFromEnv());
126
+ if (selected && selected.source !== "legacy-keystore") {
127
+ const agent = selected.seedHash
128
+ ? await AiFinPayAgent.fromSeed(selected.seedHash)
129
+ : await AiFinPayAgent.fromSolanaSecret(selected.secretB58);
130
+ process.stdout.write(`Using ${selected.source}; no replacement wallet created.\n` +
131
+ `EVM ${agent.evmAddress}\nSolana ${agent.solanaAddress}\nCasper ${agent.casperAddress}\n` +
132
+ `Keep your configured seed backed up privately. Call agent_reload in an already connected MCP server.\n`);
133
+ process.exit(0);
134
+ }
135
+
136
+ let store = readKeystore();
137
+ let freshlyCreated = false;
138
+ if (!store) {
139
+ mkdirSync(HOME, { recursive: true, mode: 0o700 });
140
+ const secretB58 = Agent.new().secretB58;
141
+ const candidate = { secretB58, created: new Date().toISOString() };
142
+ const onDisk = PASSPHRASE ? encryptSecret(secretB58) : candidate;
143
+ const temporary = join(HOME, `.agent-init-${process.pid}-${randomBytes(12).toString("hex")}.tmp`);
144
+ // Publish a complete file without replacing an existing wallet. Two init
145
+ // processes may both see no keystore; only one exclusive link can win.
146
+ // A direct exclusive write would expose a partially written file to readers.
147
+ let fd;
148
+ let ownsTemporary = false;
149
+ try {
150
+ fd = openSync(temporary, "wx", 0o600);
151
+ ownsTemporary = true;
152
+ fchmodSync(fd, 0o600);
153
+ writeFileSync(fd, JSON.stringify(onDisk, null, 2) + "\n");
154
+ fsyncSync(fd);
155
+ closeSync(fd);
156
+ fd = undefined;
157
+ try {
158
+ linkSync(temporary, KEYSTORE);
159
+ store = candidate;
160
+ freshlyCreated = true;
161
+ } catch (error) {
162
+ if (error.code !== "EEXIST") throw error;
163
+ store = readKeystore();
164
+ if (!store) throw new Error(`Existing ${KEYSTORE} is unreadable; refusing to replace it.`);
165
+ }
166
+ } finally {
167
+ if (fd !== undefined) closeSync(fd);
168
+ if (ownsTemporary) unlinkSync(temporary);
169
+ }
170
+ }
171
+ if (freshlyCreated) {
172
+ process.stdout.write(
173
+ PASSPHRASE
174
+ ? `Created ${KEYSTORE} (mode 600, ENCRYPTED). Keep AIFINPAY_WALLET_PASSPHRASE — the wallet is unrecoverable without it.\n\n`
175
+ : `Created ${KEYSTORE} (mode 600, plaintext). For at-rest encryption, set AIFINPAY_WALLET_PASSPHRASE before init.\n\n`,
176
+ );
177
+ } else {
178
+ process.stdout.write(`Existing wallet found at ${KEYSTORE} — keeping it.\n\n`);
179
+ }
180
+
181
+ // The one-time recovery output.
182
+ //
183
+ // Shown only to the successful creator of a plaintext wallet on a TTY.
184
+ // Piped output (including agent subprocess logs) contains public data only.
185
+ //
186
+ // Encrypted keystores print nothing here: recovery there IS the keystore file
187
+ // plus the passphrase, and re-printing the plaintext secret would defeat the
188
+ // encryption the user just asked for.
189
+ if (freshlyCreated && !PASSPHRASE && process.stdout.isTTY) {
190
+ process.stdout.write(
191
+ `\n RECOVERY KEY (shown once, never again):\n\n` +
192
+ ` ${store.secretB58}\n\n` +
193
+ ` Save this off this machine NOW. Anyone with it controls the wallet and\n` +
194
+ ` its funds. Do NOT paste it into a chat, an issue, or a config file — the\n` +
195
+ ` keystore at ${KEYSTORE} already holds it (encrypt it with\n` +
196
+ ` AIFINPAY_WALLET_PASSPHRASE). This line is your OFF-machine backup.\n\n`,
197
+ );
198
+ }
199
+
200
+ const agent = await AiFinPayAgent.fromSolanaSecret(store.secretB58);
201
+
202
+ process.stdout.write(
203
+ `Your agent's addresses — the EVM one is the same on every EVM chain:\n\n` +
204
+ ` EVM ${agent.evmAddress}\n` +
205
+ ` Solana ${agent.solanaAddress}\n` +
206
+ ` Casper ${agent.casperAddress}\n\n` +
207
+ `Add this to your MCP client config and connect/reconnect this MCP server:\n\n` +
208
+ JSON.stringify(
209
+ {
210
+ mcpServers: {
211
+ aifinpay: {
212
+ command: "npx",
213
+ args: ["-y", `@aifinpay/mcp@${VERSION}`],
214
+ env: { AIFINPAY_MAX_USD: "0.10" },
215
+ },
216
+ },
217
+ },
218
+ null,
219
+ 2,
220
+ ) +
221
+ `\n\nThe secret is NOT in that block on purpose — the server reads the\n` +
222
+ `keystore. If this server is already connected, call agent_reload after init.\nA new conversation is not required by this server.\n\n` +
223
+ `Back up ${KEYSTORE}. It is the only copy. The derivation is not\n` +
224
+ `BIP-39, so no standard wallet can recover this from a phrase.\n\n` +
225
+ `The EVM address is used for Polygon payments. Check its balance before\n` +
226
+ `funding it or paying for calls.\n`,
227
+ );
228
+ process.exit(0);
229
+ }
230
+
231
+ if (arg && !arg.startsWith("-")) {
232
+ process.stderr.write(`aifinpay-mcp: unknown command "${arg}". Try --help.\n`);
233
+ process.exit(2);
234
+ }
235
+
236
+ // ── server ────────────────────────────────────────────────────────────────
237
+
238
+ // Identity selection is shared with the programmatic server and agent_reload.
239
+ // Do not copy a file secret into process.env: it would mask later file updates.
240
+ const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
241
+ const { createServer, loadConfigFromEnv } = await import("../dist/index.js");
15
242
 
16
243
  const { server } = await createServer(loadConfigFromEnv());
17
244
  const transport = new StdioServerTransport();
package/dist/api.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export declare function apiUrl(base: string, path: string): string;
2
+ //# sourceMappingURL=api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAGzD"}
package/dist/api.js ADDED
@@ -0,0 +1,6 @@
1
+ export function apiUrl(base, path) {
2
+ if (new URL(base).hostname === "api.aifinpay.io" && path.startsWith("/api/"))
3
+ path = path.slice(4);
4
+ return `${base.replace(/\/+$/, "")}${path}`;
5
+ }
6
+ //# sourceMappingURL=api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,MAAM,CAAC,IAAY,EAAE,IAAY;IAC/C,IAAI,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,QAAQ,KAAK,iBAAiB,IAAI,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC;QAAE,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACnG,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC;AAC9C,CAAC"}
package/dist/config.d.ts CHANGED
@@ -1,14 +1,47 @@
1
1
  /** Runtime configuration loaded from env. */
2
2
  export interface McpConfig {
3
- /** Base58 secret to load the agent identity. If absent, a fresh keypair is
4
- * generated AND printed to stderr at startup with a "save this!" warning. */
3
+ /** Legacy base58 secret, after SEED_HASH and the project agents file. */
5
4
  agentSecretB58?: string;
5
+ /** 32-byte hex seed, consumed exactly as AiFinPayAgent.fromSeed does. */
6
+ seedHash?: string;
7
+ agentsFile?: string;
8
+ agentId?: string;
9
+ walletHome?: string;
10
+ walletPassphrase?: string;
11
+ /** Enables dev discovery/quoting only; never enables settlement signing. */
12
+ devMode?: boolean;
6
13
  /** Custom AiFinPay backend URL. Defaults to production. */
7
14
  baseUrl?: string;
8
15
  /** Request timeout in ms. */
9
16
  timeoutMs?: number;
10
17
  /** Hard cap on a single payment to prevent runaway agents. */
11
18
  maxAmountUsd?: number;
19
+ /** Gateway origins this agent may settle against.
20
+ *
21
+ * The SDK has always supported this (parseGatewayUrl in @aifinpay/agent) and
22
+ * this wrapper never exposed it, so a self-hosted merchant was unreachable:
23
+ * payable_fetch reached the 402 and then refused with "dev.ratersapp.com is
24
+ * not a known AiFinPay gateway; allowed: https://gateway.aifinpay.io".
25
+ * Observed in an external E2E run on 2026-08-27.
26
+ *
27
+ * Set by the OPERATOR, exact origins only, never a wildcard. The refusal it
28
+ * relaxes is not paranoia: a 402 is unauthenticated, so paying an
29
+ * unrecognised host means paying whoever answered. Naming a host here is a
30
+ * statement that you know who that is. */
31
+ gatewayOrigins?: string[];
32
+ /** AIFP-1 resource identity: merchant slug or full direct request path. */
33
+ gatewayPathMode?: "gateway" | "direct";
34
+ /** Hosts whose DNS pre-check is skipped, exact names only.
35
+ *
36
+ * safe-fetch resolves a hostname and refuses if any answer is a private
37
+ * address — an SSRF guard. Behind an HTTP proxy the client cannot resolve at
38
+ * all (getaddrinfo EAI_AGAIN), so every host is refused for the wrong reason.
39
+ *
40
+ * This is deliberately NOT a proxy-detection switch that disables the check
41
+ * globally: that would turn one environment quirk into a blanket SSRF
42
+ * bypass. An operator names the hosts they vouch for, one at a time, and the
43
+ * guard stays on for everything else. */
44
+ trustedHosts?: string[];
12
45
  /** Optional log destination (defaults to stderr). */
13
46
  logFn?: (level: "info" | "warn" | "error", msg: string) => void;
14
47
  }
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,6CAA6C;AAC7C,MAAM,WAAW,SAAS;IACxB;kFAC8E;IAC9E,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB,2DAA2D;IAC3D,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,6BAA6B;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,8DAA8D;IAC9D,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB,qDAAqD;IACrD,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CACjE;AAED,wBAAgB,iBAAiB,IAAI,SAAS,CAW7C"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,6CAA6C;AAC7C,MAAM,WAAW,SAAS;IACxB,yEAAyE;IACzE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,yEAAyE;IACzE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,4EAA4E;IAC5E,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB,2DAA2D;IAC3D,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,6BAA6B;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,8DAA8D;IAC9D,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;;;;;;;;;;+CAW2C;IAC3C,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAE1B,2EAA2E;IAC3E,eAAe,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;IAEvC;;;;;;;;;8CAS0C;IAC1C,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IAExB,qDAAqD;IACrD,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CACjE;AAED,wBAAgB,iBAAiB,IAAI,SAAS,CAuB7C"}
package/dist/config.js CHANGED
@@ -1,5 +1,14 @@
1
1
  export function loadConfigFromEnv() {
2
+ if (process.env.AIFINPAY_MODE && !['live', 'dev'].includes(process.env.AIFINPAY_MODE)) {
3
+ throw new Error('AIFINPAY_MODE must be live or dev');
4
+ }
2
5
  return {
6
+ devMode: process.env.AIFINPAY_MODE === 'dev',
7
+ seedHash: process.env.SEED_HASH,
8
+ agentsFile: process.env.AIFINPAY_AGENTS_FILE || undefined,
9
+ agentId: process.env.AIFINPAY_AGENT_ID || undefined,
10
+ walletHome: process.env.AIFINPAY_HOME || undefined,
11
+ walletPassphrase: process.env.AIFINPAY_WALLET_PASSPHRASE || undefined,
3
12
  agentSecretB58: process.env.AIFINPAY_AGENT_SECRET || undefined,
4
13
  baseUrl: process.env.AIFINPAY_BASE_URL || undefined,
5
14
  timeoutMs: process.env.AIFINPAY_TIMEOUT_MS
@@ -8,6 +17,60 @@ export function loadConfigFromEnv() {
8
17
  maxAmountUsd: process.env.AIFINPAY_MAX_USD
9
18
  ? Number(process.env.AIFINPAY_MAX_USD)
10
19
  : undefined,
20
+ gatewayOrigins: splitOrigins(process.env.AIFINPAY_GATEWAY_ORIGINS),
21
+ gatewayPathMode: parseGatewayPathMode(process.env.AIFINPAY_GATEWAY_PATH_MODE),
22
+ trustedHosts: splitList(process.env.AIFINPAY_TRUSTED_HOSTS),
11
23
  };
12
24
  }
25
+ function parseGatewayPathMode(raw) {
26
+ if (raw === undefined || raw.trim() === "")
27
+ return "gateway";
28
+ if (raw === "gateway" || raw === "direct")
29
+ return raw;
30
+ throw new Error("AIFINPAY_GATEWAY_PATH_MODE must be either gateway or direct");
31
+ }
32
+ /** Comma-separated list → trimmed entries, or undefined when unset/empty.
33
+ * Undefined and [] mean different things downstream: undefined keeps the SDK
34
+ * default, [] would mean "no origin is payable". An operator who sets the
35
+ * variable to an empty string means the former. */
36
+ function splitList(raw) {
37
+ if (!raw)
38
+ return undefined;
39
+ const out = raw.split(",").map((s) => s.trim()).filter(Boolean);
40
+ return out.length ? out : undefined;
41
+ }
42
+ /** Same, but each entry must be a bare https origin.
43
+ * A path or a wildcard here would read as allowed and match nothing, which is
44
+ * the failure that looks like the feature is broken rather than misconfigured. */
45
+ function splitOrigins(raw) {
46
+ const list = splitList(raw);
47
+ if (!list)
48
+ return undefined;
49
+ for (const entry of list) {
50
+ let u;
51
+ try {
52
+ u = new URL(entry);
53
+ }
54
+ catch {
55
+ throw new Error(`AIFINPAY_GATEWAY_ORIGINS: "${entry}" is not a URL`);
56
+ }
57
+ if (u.origin !== entry.replace(/\/+$/, "")) {
58
+ throw new Error(`AIFINPAY_GATEWAY_ORIGINS: "${entry}" must be a bare origin like https://dev.example.com `
59
+ + `(got path/query "${u.pathname}${u.search}")`);
60
+ }
61
+ // WHATWG URL accepts "*" in a hostname, so new URL("https://*.example.com")
62
+ // parses and its origin round-trips — the entry would be stored, matched
63
+ // against nothing, and read as "I allowed this host". Rejecting it here is
64
+ // the difference between a misconfiguration that says so and one that looks
65
+ // like the feature is broken.
66
+ if (!/^[a-z0-9.-]+$/i.test(u.hostname) || u.hostname.startsWith("-") || u.hostname.includes("..")) {
67
+ throw new Error(`AIFINPAY_GATEWAY_ORIGINS: "${entry}" is not a plain hostname. `
68
+ + `Wildcards are not supported — name each origin you settle against.`);
69
+ }
70
+ if (u.protocol !== "https:" && u.hostname !== "localhost" && u.hostname !== "127.0.0.1") {
71
+ throw new Error(`AIFINPAY_GATEWAY_ORIGINS: "${entry}" is not https — refusing to settle over plaintext`);
72
+ }
73
+ }
74
+ return list;
75
+ }
13
76
  //# sourceMappingURL=config.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAmBA,MAAM,UAAU,iBAAiB;IAC/B,OAAO;QACL,cAAc,EAAE,OAAO,CAAC,GAAG,CAAC,qBAAqB,IAAI,SAAS;QAC9D,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,iBAAiB,IAAI,SAAS;QACnD,SAAS,EAAE,OAAO,CAAC,GAAG,CAAC,mBAAmB;YACxC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC;YACzC,CAAC,CAAC,SAAS;QACb,YAAY,EAAE,OAAO,CAAC,GAAG,CAAC,gBAAgB;YACxC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC;YACtC,CAAC,CAAC,SAAS;KACd,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}