@aifinpay/mcp 2.7.1 → 2.11.0

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 (41) hide show
  1. package/README.md +174 -17
  2. package/bin/aifinpay-mcp.js +142 -41
  3. package/dist/client-framework.d.ts +3 -0
  4. package/dist/client-framework.d.ts.map +1 -0
  5. package/dist/client-framework.js +22 -0
  6. package/dist/client-framework.js.map +1 -0
  7. package/dist/config.d.ts +5 -0
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/config.js +13 -0
  10. package/dist/config.js.map +1 -1
  11. package/dist/identity.d.ts +1 -0
  12. package/dist/identity.d.ts.map +1 -1
  13. package/dist/identity.js +55 -5
  14. package/dist/identity.js.map +1 -1
  15. package/dist/payment-state.d.ts.map +1 -1
  16. package/dist/safe-fetch.js.map +1 -1
  17. package/dist/server.d.ts +5 -1
  18. package/dist/server.d.ts.map +1 -1
  19. package/dist/server.js +38 -3
  20. package/dist/server.js.map +1 -1
  21. package/dist/tools/agent-address.d.ts.map +1 -1
  22. package/dist/tools/agent-claim-self.d.ts +1 -1
  23. package/dist/tools/agent-claim-self.d.ts.map +1 -1
  24. package/dist/tools/agent-history.d.ts.map +1 -1
  25. package/dist/tools/agent-quota.d.ts +1 -1
  26. package/dist/tools/agent-quota.d.ts.map +1 -1
  27. package/dist/tools/dev-payment-quote.d.ts.map +1 -1
  28. package/dist/tools/payable-fetch.d.ts +5 -0
  29. package/dist/tools/payable-fetch.d.ts.map +1 -1
  30. package/dist/tools/payable-fetch.js +41 -7
  31. package/dist/tools/payable-fetch.js.map +1 -1
  32. package/dist/tools/payment-setup-status.d.ts +84 -0
  33. package/dist/tools/payment-setup-status.d.ts.map +1 -0
  34. package/dist/tools/payment-setup-status.js +193 -0
  35. package/dist/tools/payment-setup-status.js.map +1 -0
  36. package/dist/tools/production-control.d.ts.map +1 -1
  37. package/dist/tools/production-control.js +106 -6
  38. package/dist/tools/production-control.js.map +1 -1
  39. package/dist/tools/supported-chains.d.ts.map +1 -1
  40. package/package.json +5 -6
  41. package/skills/SKILL.md +63 -20
package/README.md CHANGED
@@ -4,13 +4,60 @@ AiFinPay MCP server for persistent agent identity, Agent Passport resolution,
4
4
  route discovery and non-signing settlement invoices. Canonical domain:
5
5
  **aifinpay.io**.
6
6
 
7
- Version **2.7.1** is prepared in this checkout, requiring agent2.5.1 and canonical skill2.8.0.
7
+ Version **2.11.0** is prepared in this checkout, on the published agent 2.8.0
8
+ and canonical skill 2.10.0 (AiFinPay/skill 49c2e2a, published 2026-10-19). The separate canonical producer's final immutable
9
+ source pin is a required handoff, not a moving-branch substitution.
8
10
  The published baseline checked2026-10-04 was MCP2.5.0. Package publication and
9
11
  real registry dependency/lock refresh remain release gates; a source-cohort
10
12
  build does not prove those steps. Owner-enabled v1.4 purchases with `payable_fetch` are
11
13
  released: on Polygon since 2.2.0, on Base (ETH or USDC) since 2.5.0; site-wide
12
- batches with `scope: "merchant"` since 2.4.1. Without payment configuration the server keeps
13
- its inspection-only tool inventory.
14
+ batches with `scope: "merchant"` since 2.4.1. Without payment configuration the server
15
+ lists every tool, including `payable_fetch`, but signs nothing: `payable_fetch`
16
+ and `payment_setup_status` answer with the setup checklist instead.
17
+
18
+ ## First payment in five steps
19
+
20
+ Each step is one command or one tool call. Signing happens only in step 5,
21
+ and only once the owner has set every limit in step 4.
22
+
23
+ 1. **Read the price.** `GET` the resource. The `402` names the merchant
24
+ (`merchant_id`), the price per request and the smallest batch (`$0.10` for
25
+ 200 standard requests).
26
+ 2. **Check the setup.** Call `payment_setup_status`. It returns the wallet
27
+ (persistent or ephemeral), whether payments are enabled, which owner limits
28
+ are set, the balance when the RPC answers, and `next_step`: the first thing
29
+ still missing.
30
+ 3. **Persistent wallet.** If `wallet.persistent` is false, run
31
+ `npx @aifinpay/mcp init`. Without a terminal it prints one JSON document;
32
+ copy its `env` (the passphrase is only there) into the `aifinpay` server
33
+ entry of the MCP client config, reconnect the server, then call
34
+ `agent_reload`. Never fund an ephemeral address.
35
+ 4. **Fund and enable.** Tell the owner `addresses.evm` and the amount: the
36
+ batch price in POL plus the gas cap, so about `0.5 POL` for one `$0.10`
37
+ batch at `$0.20`/POL plus up to `0.3 POL` gas (the SDK refuses before
38
+ signing if the worst-case fee exceeds the cap). When the owner confirms
39
+ funding, they add the payment settings to the same `env` and reconnect:
40
+
41
+ ```json
42
+ {
43
+ "AIFINPAY_PAYMENTS_ENABLED": "1",
44
+ "AIFINPAY_GATEWAY_ORIGINS": "https://dev.ratersapp.com",
45
+ "AIFINPAY_GATEWAY_PATH_MODE": "direct",
46
+ "AIFINPAY_MAX_USD": "0.15",
47
+ "AIFINPAY_DAILY_USD": "1.00",
48
+ "AIFINPAY_MAX_GAS_POL": "0.3"
49
+ }
50
+ ```
51
+
52
+ `payment_setup_status` then reports `ready: true`.
53
+
54
+ 5. **Pay.** `payable_fetch({"url": "https://dev.ratersapp.com/api/agent/genres/list"})`
55
+ buys the batch on Polygon (splitter v1.4) and returns the content and the
56
+ receipt summary. Later requests reuse the batch.
57
+
58
+ Polygon and Base are served on v1.4 today; `settlement_routes` lists them
59
+ first, with `live: true`. Its `legacy_v13` section describes the older
60
+ splitter v1.3 routes, which `payable_fetch` does not use.
14
61
 
15
62
  ## Enable native paid GET requests
16
63
 
@@ -35,6 +82,23 @@ owner. `direct` uses the full path for self-hosted sites; `gateway` uses the
35
82
  merchant slug on the hosted gateway. Restart/reconnect the MCP process after
36
83
  changing its environment.
37
84
 
85
+ ## Indexed wallet derivation
86
+
87
+ The wallet CLI and MCP can use deterministic child wallets from one recovery
88
+ seed. Create an MCP-compatible wallet with
89
+ `npx @aifinpay/wallet new --index 0`; the keystore retains the
90
+ root seed and adds the index as optional metadata without changing existing
91
+ fields. MCP uses that stored index automatically. To select another child in a different MCP
92
+ process, set `AIFINPAY_WALLET_INDEX` to its decimal index; the same variable
93
+ also applies when `SEED_HASH` or an `agents.json` record supplies the seed.
94
+
95
+ Each chain derives its own indexed key; for example,
96
+ `EVM = SHA-256("aifinpay:evm:v1\0" || decimalIndex || recoverySeed)` and
97
+ `Solana = Ed25519(SHA-256("aifinpay:solana:v1\0" || decimalIndex || recoverySeed))`.
98
+ The decimal UTF-8 index follows that chain's domain NUL separator. Index 0 is
99
+ the first wallet. Indices must be between 0 and 4294967295. With no selected
100
+ index, existing identities retain their current derivation.
101
+
38
102
  `AIFINPAY_MAX_GAS_POL` caps the worst case, not the fee you expect to pay.
39
103
  Before signing, the client prices the estimated gas plus 20% at the maximum fee
40
104
  per gas the Polygon RPC quotes, and refuses with `V14_GAS_BUDGET_EXCEEDED` if
@@ -104,6 +168,24 @@ then `api.coingecko.com`; other chains use those HTTP price sources.
104
168
  If none provides a valid fresh rate, `payable_fetch` stops before paying and
105
169
  the error names each host it tried.
106
170
 
171
+ ### Which AI client paid
172
+
173
+ `payable_fetch` tells the merchant which AI client is paying: the server maps the
174
+ `clientInfo.name` your MCP client sent at initialize to an `AIFP-Agent-Framework`
175
+ header on the payment requests (and on recovery of a pending payment).
176
+
177
+ | `clientInfo.name` | `AIFP-Agent-Framework` |
178
+ | -------------------------- | ---------------------- |
179
+ | `claude-ai`, `claude-code` | `claude` |
180
+ | `cursor`, `cursor-vscode` | `cursor` |
181
+ | `windsurf` | `windsurf` |
182
+ | `chatgpt`, `openai` | `chatgpt` |
183
+ | `gemini` | `gemini` |
184
+ | `perplexity` | `perplexity` |
185
+
186
+ Names are matched exactly. Any other client sends no header; the server never
187
+ guesses. Nothing in the payment, limits or receipts depends on it.
188
+
107
189
  ## Link the agent to its owner's dashboard
108
190
 
109
191
  The owner sees the agent's balance, payments and receipts at
@@ -133,18 +215,31 @@ state file in chat. Unconfirmed payments consume the budget conservatively.
133
215
  | `agent_reload` | Reload local wallet files in the current MCP connection. |
134
216
  | `agent_quota` | Read the agent's quota. |
135
217
  | `agent_passport_resolve` | Resolve the global Agent Passport identity. |
136
- | `settlement_routes` | Read the available verified settlement routes. |
218
+ | `payment_setup_status` | Read-only setup checklist with the one `next_step`. |
219
+ | `settlement_routes` | Splitter v1.4 routes (live now) first, then legacy v1.3. |
137
220
  | `settlement_invoice` | Prepare a non-signing settlement invoice. |
138
221
  | `agent_history` | Read indexed settlements or retained receipt batches. |
139
222
  | `settlement_solana` | Build a non-signing Solana settlement invoice. |
140
223
  | `settlement_casper` | Build a non-signing Casper settlement invoice. |
141
224
  | `deployment_info` | Read deployments: addresses, assets, settlement status. |
142
225
 
143
- `payable_fetch` is registered when the owner sets `AIFINPAY_PAYMENTS_ENABLED=1`
144
- with a persistent wallet. With payments enabled and any owner limit missing or
145
- invalid, the server refuses to start rather than hiding the tool. It accepts
146
- only `url`, `max_amount_usd` and `scope`; `max_amount_usd` can lower the owner's
147
- per-payment cap for one call, never raise it. `dev_payment_quote` is registered
226
+ `payable_fetch` is always listed. It signs only with a persistent wallet,
227
+ `AIFINPAY_PAYMENTS_ENABLED=1` and every owner limit; before that it returns the
228
+ `payment_setup_status` checklist as a tool error and contacts nothing. With
229
+ payments enabled and any owner limit missing or invalid, the server refuses to
230
+ start rather than hiding the tool. It accepts
231
+ only `url`, `max_amount_usd`, `scope` and `expected_merchant_id`; `max_amount_usd`
232
+ can lower the owner's per-payment cap for one call, never raise it.
233
+
234
+ Each gateway site (origin + slug) is pinned to the merchant this wallet first
235
+ paid it as. The agent SDK keeps these pins under `~/.aifinpay/merchant-pins`, or
236
+ `AIFINPAY_STATE_DIR/merchant-pins`; `AIFINPAY_HOME` does not move them. When a
237
+ site answers as another merchant, `payable_fetch` stops before any quote and
238
+ names both merchant ids. Nothing is paid, and the agent cannot approve the change
239
+ itself. Once the owner confirms the new merchant, a call with
240
+ `expected_merchant_id` set to it pays and re-pins the site. There is no blanket
241
+ "allow any merchant change" argument. A `410 gateway_retired` site is terminal:
242
+ the tool says not to retry. `dev_payment_quote` is registered
148
243
  only with `AIFINPAY_MODE=dev`. Legacy `agent_call`, `agent_quote`,
149
244
  `pay_with_split` and `quote_split` remain unregistered.
150
245
 
@@ -170,6 +265,41 @@ Invalid or ambiguous configured inputs fail; they never generate a replacement
170
265
  wallet. With no configured wallet at all the server has an ephemeral identity:
171
266
  **do not fund it**.
172
267
 
268
+ ### Embed an external EVM wallet
269
+
270
+ An embedding host can pass a viem `WalletClient` directly to `createServer`.
271
+ This is programmatic-only; `npx @aifinpay/mcp` and environment variables cannot
272
+ carry a wallet client. Keep the existing local identity configured for Solana
273
+ and other identity surfaces. When supplied, the client account becomes the EVM
274
+ payment identity and the payment journal is keyed to that address. Its chain
275
+ must match the owner-selected `AIFINPAY_PAY_CHAIN` (Polygon by default); the
276
+ SDK refuses a mismatch rather than switching providers or falling back to the
277
+ local EVM key.
278
+
279
+ ```ts
280
+ import { createServer, loadConfigFromEnv } from "@aifinpay/mcp";
281
+ import type { EvmWalletClient } from "@aifinpay/agent";
282
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
283
+
284
+ // Constructed by the host from its chosen external signer/provider.
285
+ declare const walletClient: EvmWalletClient;
286
+
287
+ const { server } = await createServer({
288
+ ...loadConfigFromEnv(),
289
+ evmWalletClient: walletClient,
290
+ });
291
+ await server.connect(new StdioServerTransport());
292
+ ```
293
+
294
+ The host must provide a viem `LocalAccount` with `signTransaction`, bound to
295
+ the selected chain. v1.4 persists exact signed transaction bytes before the
296
+ SDK broadcasts them through its verified RPC; JSON-RPC/send-only and smart
297
+ account clients are refused rather than bypassing durable recovery. MCP still
298
+ applies its configured spending limits, runtime and route checks, receipt
299
+ verification, and recovery. Changing the injected account while payment
300
+ recovery is pending requires reconciliation and reconnecting; it is not a
301
+ tool-level wallet switch.
302
+
173
303
  ## Initialize and connect
174
304
 
175
305
  A quote or invoice is not a completed payment. `payable_fetch` pays only with the
@@ -184,7 +314,33 @@ AIFINPAY_WALLET_PASSPHRASE='<a long passphrase>' npx @aifinpay/mcp init
184
314
  The keystore is encrypted at rest and the MCP server needs the same
185
315
  `AIFINPAY_WALLET_PASSPHRASE` to load it. For a disposable test wallet only,
186
316
  `npx @aifinpay/mcp init --plaintext` writes it unencrypted (mode 600); without
187
- either, `init` refuses rather than create an unencrypted wallet you might fund.
317
+ either, `init` on a terminal refuses rather than create an unencrypted wallet
318
+ you might fund.
319
+
320
+ Without a terminal on stdin (an agent's subprocess), or with
321
+ `AIFINPAY_NONINTERACTIVE=1`, `init` never prompts. It generates a 192-bit
322
+ passphrase, encrypts the keystore and prints one JSON document:
323
+
324
+ ```json
325
+ {
326
+ "keystore": "/home/me/.aifinpay/agent.json",
327
+ "created": true,
328
+ "encrypted": true,
329
+ "source": "legacy-keystore",
330
+ "addresses": { "evm": "0x…", "solana": "…", "casper": "account-hash-…" },
331
+ "env": { "AIFINPAY_WALLET_PASSPHRASE": "<generated>" },
332
+ "passphrase_note": "…",
333
+ "payment_env_example": { "AIFINPAY_PAYMENTS_ENABLED": "1", "…": "…" },
334
+ "next_step": "Put \"env\" into the aifinpay server entry of the MCP client config …"
335
+ }
336
+ ```
337
+
338
+ The passphrase appears only inside `env`, because the caller must put it in
339
+ the client config and there is no other channel; a process that captures that
340
+ output captures the passphrase, so treat the output as a secret. A second run
341
+ keeps the wallet and prints a placeholder instead of the passphrase. If the
342
+ keystore exists and `AIFINPAY_WALLET_PASSPHRASE` is unset, `init` prints one
343
+ line naming the variable and exits with status 2.
188
344
 
189
345
  Select the wallet in this order: `SEED_HASH` → `./aifinpay/agents.json` →
190
346
  legacy `AIFINPAY_AGENT_SECRET` → `~/.aifinpay/agent.json`.
@@ -247,7 +403,7 @@ snapshot. Verify `agent_address` against the wallet you intend to use.
247
403
  | `AIFINPAY_MAX_USD` | — | Per-payment USD cap for `payable_fetch`; required when payments are enabled. |
248
404
  | `AIFINPAY_GATEWAY_ORIGINS` | — | Comma-separated exact HTTPS origins the agent may pay; required with payments. |
249
405
  | `AIFINPAY_GATEWAY_PATH_MODE` | `gateway` | Use `gateway` for merchant-slug identity or `direct` for full request-path identity. |
250
- | `AIFINPAY_PAY_CHAIN` | `polygon` | Owner-selected payment chain; see the2.7.1 source capability list. |
406
+ | `AIFINPAY_PAY_CHAIN` | `polygon` | Owner-selected payment chain; see the2.8.1 source capability list. |
251
407
  | `AIFINPAY_MAX_GAS` | — | Gas cap per payment in the pay chain's native currency (POL, ETH, AVAX, BNB, XRP). |
252
408
  | `AIFINPAY_RPC_URL` | chain's public RPC | HTTPS RPC for the pay chain. |
253
409
  | `AIFINPAY_CLAIM_ORIGINS` | AiFinPay dashboards | Exact origins `agent_claim_self` may link the agent to. |
@@ -297,14 +453,14 @@ executor; this MCP does not expose Amoy paid receipt purchases.
297
453
 
298
454
  MIT.
299
455
 
300
- ## Additional EVM networks in the 2.7.1 source candidate
456
+ ## Additional EVM networks in the 2.8.1 source candidate
301
457
 
302
458
  Owner `AIFINPAY_PAY_CHAIN` accepts polygon, base, optimism, arbitrum, avalanche,
303
459
  bnb, unichain, xrplevm and robinhood. `AIFINPAY_MAX_GAS` is in the selected native
304
460
  POL/ETH/AVAX/BNB/XRP currency; the legacy POL cap applies only on Polygon. Assets
305
461
  must be pinned for that chain, including BNB 18-decimal USDC/USDT and Robinhood
306
462
  18-decimal USDe/6-decimal USDG. XRPL EVM has native XRP only. Metadata comes from
307
- agent 2.5.1; the backend must still authorize and serve that merchant/network.
463
+ agent 2.6.1; the backend must still authorize and serve that merchant/network.
308
464
  Source support does not activate networks in production. Pending journals retain
309
465
  the original chain and are recovered without another settlement. A legacy
310
466
  missing-chain journal is adopted only when the owner-configured chain and the
@@ -312,13 +468,14 @@ actual signed transaction's chain ID, payer, pinned target and exact call agree.
312
468
  Insufficient evidence requires manual receipt reconciliation. A later quote
313
469
  expiry does not invalidate receipt recovery for an existing payment.
314
470
 
315
- Release order: publish reviewed canonical skill2.8.0 and agent2.5.1, refresh MCP
471
+ Release order after final release gates: publish reviewed canonical skill2.9.0 and agent2.6.1, refresh MCP
316
472
  dependencies and lock from those real registry releases, pass standalone MCP
317
- CI, then publish MCP2.7.1. Source-cohort validation requires fresh source-packed
318
- agent2.5.1 and skill2.8.0. The existing registry entries remain until publication;
473
+ CI, then publish MCP2.8.0. This candidate requires fresh source-packed
474
+ agent2.6.0 and skill2.9.0, plus the exact Python2.5.1 release target. The existing
475
+ registry agent2.5.0 / skill2.8.0 entries remain until publication;
319
476
  no npm integrity/resolution is fabricated. An old2.3.x agent cannot build the
320
477
  new descriptor/helper imports, and old skill2.6.0 cannot satisfy the exact
321
- release-target guard. Hosted source CI uses `scripts/check-mcp-source-cohort.mjs`: a byte-preserving fixed registry bootstrap and genuine same-commit Node plus fully pinned canonical skill packages in disposable directories. Exact package bytes, versions, bundled skill and full tests must pass; the registry publication guard must still refuse this source cohort. Standalone release CI requires the refreshed real registry lock. Never publish the candidate with the old registry dependency lock.
478
+ release-target guard. Hosted source CI uses `scripts/check-mcp-source-cohort.mjs`: a byte-preserving patched registry bootstrap and genuine same-commit Node plus fully pinned canonical skill packages in disposable directories. The Node producer's authoritative source lock seeds the genuine runtime; it is separate resolution evidence, since npm excludes it from the tarball. Exact package bytes, versions, bundled skill, high/critical audits and full tests must pass; the registry publication guard must still refuse a source cohort lacking a matching real registry lock. Standalone release CI requires the refreshed real registry lock. Never publish the candidate with the old registry dependency lock.
322
479
 
323
480
  Use one shared local filesystem for a wallet's ledger/journal and reconcile
324
481
  pending transactions before downgrading. Node 2.4 reservations never expire for
@@ -75,16 +75,26 @@ if (arg === "--help" || arg === "-h" || arg === "help") {
75
75
  Run init once. It writes ${KEYSTORE} with mode 600 and the server picks
76
76
  it up automatically — you do not have to put the secret in a config file.
77
77
 
78
+ Without a terminal on stdin (an agent's subprocess), or with
79
+ AIFINPAY_NONINTERACTIVE=1, init never prompts: it generates the passphrase
80
+ itself, encrypts the keystore and prints one JSON document with the keystore
81
+ path, the public addresses and the exact "env" block for the MCP client config.
82
+
78
83
  Env (all optional):
79
- 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
84
+ SEED_HASH 32-byte hex seed; highest priority
85
+ AIFINPAY_AGENTS_FILE project agents file (default ./aifinpay/agents.json)
86
+ AIFINPAY_AGENT_ID select one record when the file has multiple agents
87
+ AIFINPAY_AGENT_SECRET legacy base58 secret; after project wallet sources
88
+ AIFINPAY_WALLET_INDEX child index derived from the selected seed
80
89
  AIFINPAY_WALLET_PASSPHRASE opens an encrypted keystore; the server needs it too
81
90
  AIFINPAY_BASE_URL default https://aifinpay.io
82
91
  AIFINPAY_TIMEOUT_MS default 30000
83
92
  AIFINPAY_HOME default ~/.aifinpay
84
93
 
85
- Payments (payable_fetch) are off until the owner sets all of
94
+ Payments (payable_fetch) sign nothing until the owner sets all of
86
95
  AIFINPAY_PAYMENTS_ENABLED=1, AIFINPAY_GATEWAY_ORIGINS, AIFINPAY_MAX_USD,
87
- AIFINPAY_DAILY_USD and AIFINPAY_MAX_GAS_POL. init prints an example block.
96
+ AIFINPAY_DAILY_USD and AIFINPAY_MAX_GAS_POL. init prints an example block;
97
+ the payment_setup_status tool says which of them is still missing.
88
98
  `);
89
99
  process.exit(0);
90
100
  }
@@ -96,7 +106,14 @@ if (arg === "--version" || arg === "-v" || arg === "version") {
96
106
 
97
107
  // ── init ──────────────────────────────────────────────────────────────────
98
108
 
99
- const PASSPHRASE = process.env.AIFINPAY_WALLET_PASSPHRASE || null;
109
+ const PASSPHRASE_ENV = "AIFINPAY_WALLET_PASSPHRASE";
110
+ let PASSPHRASE = process.env[PASSPHRASE_ENV] || null;
111
+ // An agent's subprocess has no terminal on stdin. Nothing here may wait for a
112
+ // human then: a 2026-10-19 agent run hung 167 s on a passphrase it could not
113
+ // type and invented its own key handling instead. In this mode init generates
114
+ // the passphrase itself and prints JSON the agent can act on.
115
+ const NONINTERACTIVE = process.env.AIFINPAY_NONINTERACTIVE === "1" || !process.stdin.isTTY;
116
+ let GENERATED_PASSPHRASE = false;
100
117
 
101
118
  // Optional at-rest encryption. Mode 600 keeps the keystore from OTHER users; it
102
119
  // does nothing against malware running as YOU — the threat the OSINT write-up of
@@ -126,7 +143,7 @@ function encryptSecret(secretB58) {
126
143
  };
127
144
  }
128
145
  function decryptSecret(store) {
129
- if (!PASSPHRASE) throw new Error(`${KEYSTORE} is encrypted but AIFINPAY_WALLET_PASSPHRASE is not set.`);
146
+ if (!PASSPHRASE) throw new Error(`${KEYSTORE} is encrypted but ${PASSPHRASE_ENV} is not set.`);
130
147
  const key = scryptSync(PASSPHRASE, Buffer.from(store.salt, "base64"), 32, {
131
148
  N: 1 << 15,
132
149
  r: 8,
@@ -156,52 +173,97 @@ function readKeystore() {
156
173
  return typeof parsed?.secretB58 === "string" ? parsed : null;
157
174
  }
158
175
 
159
- // The owner settings that register payable_fetch, as init prints them. README
160
- // "Enable native paid GET requests" shows the same block; a test keeps the two
161
- // equal and starts a server with it. The gas cap is a worst case at the current
162
- // Polygon gas price, not the fee: 0.05 POL refused every payment at ~280 gwei.
163
- const PAYMENT_ENV = {
164
- AIFINPAY_PAYMENTS_ENABLED: "1",
165
- AIFINPAY_GATEWAY_ORIGINS: "https://merchant.example",
166
- AIFINPAY_GATEWAY_PATH_MODE: "direct",
167
- AIFINPAY_MAX_USD: "0.15",
168
- AIFINPAY_DAILY_USD: "1.00",
169
- AIFINPAY_MAX_GAS_POL: "0.3",
170
- };
171
-
172
176
  if (arg === "init") {
173
177
  const { Agent, AiFinPayAgent } = await import("@aifinpay/agent");
174
178
 
175
179
  const { loadConfigFromEnv } = await import("../dist/config.js");
176
180
  const { loadWalletIdentity } = await import("../dist/identity.js");
177
- const selected = loadWalletIdentity(loadConfigFromEnv());
181
+ const { PAYMENT_ENV } = await import("../dist/tools/payment-setup-status.js");
182
+ const config = loadConfigFromEnv();
183
+ // A keystore that cannot be opened is reported in one line that names the
184
+ // variable, never as a stack trace — and never replaced.
185
+ const failKeystore = (error) => {
186
+ const line =
187
+ !PASSPHRASE && existsSync(KEYSTORE)
188
+ ? `${KEYSTORE} exists and is encrypted; set ${PASSPHRASE_ENV} to the passphrase used at init (it is not set in this environment).`
189
+ : error.message;
190
+ if (NONINTERACTIVE)
191
+ process.stdout.write(
192
+ JSON.stringify({ error: line, keystore: KEYSTORE, ...(PASSPHRASE ? {} : { missing_env: PASSPHRASE_ENV }) }) +
193
+ "\n"
194
+ );
195
+ else process.stderr.write(line + "\n");
196
+ process.exit(2);
197
+ };
198
+ let selected;
199
+ try {
200
+ selected = loadWalletIdentity(config);
201
+ } catch (error) {
202
+ failKeystore(error);
203
+ }
204
+ const plaintext = process.argv.includes("--plaintext");
205
+ const addressesOf = (agent) => ({ evm: agent.evmAddress, solana: agent.solanaAddress, casper: agent.casperAddress });
206
+ const indexEnv =
207
+ selected?.derivationIndex === undefined ? {} : { AIFINPAY_WALLET_INDEX: String(selected.derivationIndex) };
208
+ // Non-interactive stdout is one JSON document and nothing else, so the
209
+ // caller can parse it. The same facts, as prose, go to a terminal.
210
+ const emit = (json, text) => process.stdout.write(NONINTERACTIVE ? JSON.stringify(json, null, 2) + "\n" : text);
211
+ const nextStep =
212
+ 'Put "env" into the aifinpay server entry of the MCP client config and reconnect the server (or call agent_reload in an already connected one), then call payment_setup_status: it names the next step up to the first payment.';
213
+
178
214
  if (selected && selected.source !== "legacy-keystore") {
179
215
  const agent = selected.seedHash
180
- ? await AiFinPayAgent.fromSeed(selected.seedHash)
216
+ ? await AiFinPayAgent.fromSeed(selected.seedHash, { derivationIndex: selected.derivationIndex })
181
217
  : await AiFinPayAgent.fromSolanaSecret(selected.secretB58);
182
- process.stdout.write(
183
- `Using ${selected.source}; no replacement wallet created.\n` +
218
+ emit(
219
+ {
220
+ keystore: null,
221
+ created: false,
222
+ encrypted: null,
223
+ source: selected.source,
224
+ addresses: addressesOf(agent),
225
+ env: indexEnv,
226
+ payment_env_example: PAYMENT_ENV,
227
+ next_step: nextStep,
228
+ },
229
+ `Using ${selected.source}${selected.derivationIndex === undefined ? "" : ` (index ${selected.derivationIndex})`}; no replacement wallet created.\n` +
184
230
  `EVM ${agent.evmAddress}\nSolana ${agent.solanaAddress}\nCasper ${agent.casperAddress}\n` +
185
231
  `Keep your configured seed backed up privately. Call agent_reload in an already connected MCP server.\n`
186
232
  );
187
233
  process.exit(0);
188
234
  }
189
235
 
190
- let store = readKeystore();
236
+ const openExistingKeystore = () => {
237
+ try {
238
+ return readKeystore();
239
+ } catch (error) {
240
+ failKeystore(error);
241
+ }
242
+ };
243
+ let store = openExistingKeystore();
191
244
  let freshlyCreated = false;
192
245
  // A new wallet is encrypted at rest unless the owner asks otherwise, by name.
193
246
  // It used to be written in plaintext whenever AIFINPAY_WALLET_PASSPHRASE was
194
247
  // unset — which is the default — so the documented one-liner produced an
195
248
  // unencrypted key for a wallet people then funded. Existing keystores are
196
249
  // untouched; only creating a new plaintext one needs the flag.
197
- if (!store && !PASSPHRASE && !process.argv.includes("--plaintext")) {
198
- process.stderr.write(
199
- `No wallet at ${KEYSTORE}, and none will be created unencrypted by default.\n\n` +
200
- ` Encrypted (recommended): AIFINPAY_WALLET_PASSPHRASE='<a long passphrase>' npx @aifinpay/mcp init\n` +
201
- ` Disposable test wallet: npx @aifinpay/mcp init --plaintext\n\n` +
202
- `Keep the passphrase: an encrypted wallet cannot be recovered without it.\n`
203
- );
204
- process.exit(2);
250
+ if (!store && !PASSPHRASE && !plaintext) {
251
+ if (NONINTERACTIVE) {
252
+ // 192 random bits. The passphrase is printed once, inside the env block
253
+ // below, because the caller has to put it in the client config and there
254
+ // is no other channel; this is the trade-off of a wallet created by a
255
+ // process that cannot be asked anything.
256
+ PASSPHRASE = randomBytes(24).toString("base64url");
257
+ GENERATED_PASSPHRASE = true;
258
+ } else {
259
+ process.stderr.write(
260
+ `No wallet at ${KEYSTORE}, and none will be created unencrypted by default.\n\n` +
261
+ ` Encrypted (recommended): AIFINPAY_WALLET_PASSPHRASE='<a long passphrase>' npx @aifinpay/mcp init\n` +
262
+ ` Disposable test wallet: npx @aifinpay/mcp init --plaintext\n\n` +
263
+ `Keep the passphrase: an encrypted wallet cannot be recovered without it.\n`
264
+ );
265
+ process.exit(2);
266
+ }
205
267
  }
206
268
  if (!store) {
207
269
  mkdirSync(HOME, { recursive: true, mode: 0o700 });
@@ -228,7 +290,9 @@ if (arg === "init") {
228
290
  freshlyCreated = true;
229
291
  } catch (error) {
230
292
  if (error.code !== "EEXIST") throw error;
231
- store = readKeystore();
293
+ // The other process won; a passphrase generated here opens nothing.
294
+ if (GENERATED_PASSPHRASE) PASSPHRASE = null;
295
+ store = openExistingKeystore();
232
296
  if (!store) throw new Error(`Existing ${KEYSTORE} is unreadable; refusing to replace it.`);
233
297
  }
234
298
  } finally {
@@ -236,6 +300,44 @@ if (arg === "init") {
236
300
  if (ownsTemporary) unlinkSync(temporary);
237
301
  }
238
302
  }
303
+ const agent = selected?.seedHash
304
+ ? await AiFinPayAgent.fromSeed(selected.seedHash, { derivationIndex: selected.derivationIndex })
305
+ : await AiFinPayAgent.fromSolanaSecret(store.secretB58);
306
+ // The server cannot open an encrypted keystore without the passphrase, so a
307
+ // config block without it does not start.
308
+ const encrypted = freshlyCreated ? Boolean(PASSPHRASE) : Boolean(store.encrypted);
309
+
310
+ if (NONINTERACTIVE) {
311
+ emit(
312
+ {
313
+ keystore: KEYSTORE,
314
+ created: freshlyCreated,
315
+ encrypted,
316
+ source: "legacy-keystore",
317
+ addresses: addressesOf(agent),
318
+ env: {
319
+ ...(encrypted
320
+ ? {
321
+ [PASSPHRASE_ENV]: GENERATED_PASSPHRASE
322
+ ? PASSPHRASE
323
+ : `<the ${PASSPHRASE_ENV} already set in this environment>`,
324
+ }
325
+ : {}),
326
+ ...indexEnv,
327
+ },
328
+ ...(GENERATED_PASSPHRASE
329
+ ? {
330
+ passphrase_note: `${PASSPHRASE_ENV} was generated for this wallet and appears only here, inside env. The wallet cannot be opened or recovered without it.`,
331
+ }
332
+ : {}),
333
+ payment_env_example: PAYMENT_ENV,
334
+ next_step: nextStep,
335
+ },
336
+ ""
337
+ );
338
+ process.exit(0);
339
+ }
340
+
239
341
  if (freshlyCreated) {
240
342
  process.stdout.write(
241
343
  PASSPHRASE
@@ -265,26 +367,25 @@ if (arg === "init") {
265
367
  );
266
368
  }
267
369
 
268
- const agent = await AiFinPayAgent.fromSolanaSecret(store.secretB58);
269
- // The server cannot open an encrypted keystore without the passphrase, so a
270
- // config block without it does not start.
271
- const encrypted = freshlyCreated ? Boolean(PASSPHRASE) : Boolean(store.encrypted);
272
-
273
370
  process.stdout.write(
274
- `Your agent's addresses — the EVM one is the same on every EVM chain:\n\n` +
371
+ `Your agent's addresses${selected?.derivationIndex === undefined ? "" : ` (index ${selected.derivationIndex})`} — the EVM one is the same on every EVM chain:\n\n` +
275
372
  ` EVM ${agent.evmAddress}\n` +
276
373
  ` Solana ${agent.solanaAddress}\n` +
277
374
  ` Casper ${agent.casperAddress}\n\n` +
278
375
  `Add this to your MCP client config and connect/reconnect this MCP server.\n` +
279
- `As printed it cannot pay; payable_fetch appears once you add the payment\n` +
280
- `settings further down:\n\n` +
376
+ `As printed it cannot pay: payable_fetch is listed but signs nothing until\n` +
377
+ `you add the payment settings further down (payment_setup_status says what\n` +
378
+ `is still missing):\n\n` +
281
379
  JSON.stringify(
282
380
  {
283
381
  mcpServers: {
284
382
  aifinpay: {
285
383
  command: "npx",
286
384
  args: ["-y", `@aifinpay/mcp@${VERSION}`],
287
- env: encrypted ? { AIFINPAY_WALLET_PASSPHRASE: "<the passphrase you used for init>" } : {},
385
+ env: {
386
+ ...(encrypted ? { AIFINPAY_WALLET_PASSPHRASE: "<the passphrase you used for init>" } : {}),
387
+ ...indexEnv,
388
+ },
288
389
  },
289
390
  },
290
391
  },
@@ -0,0 +1,3 @@
1
+ import type { AgentFramework } from "@aifinpay/agent";
2
+ export declare function frameworkForClient(name: unknown): AgentFramework | undefined;
3
+ //# sourceMappingURL=client-framework.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-framework.d.ts","sourceRoot":"","sources":["../src/client-framework.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAqBtD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,OAAO,GAAG,cAAc,GAAG,SAAS,CAE5E"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * MCP `clientInfo.name`, as the client sent it at initialize, → the
3
+ * AIFP-Agent-Framework the backend records. Exact names only: a client not
4
+ * listed here sends no header rather than a guess. Entries are the names the
5
+ * product owner listed plus `cursor-vscode`, which Cursor is reported to send;
6
+ * confirm new ones against real initialize payloads before adding them.
7
+ */
8
+ const CLIENT_FRAMEWORKS = new Map([
9
+ ["claude-ai", "claude"],
10
+ ["claude-code", "claude"],
11
+ ["cursor", "cursor"],
12
+ ["cursor-vscode", "cursor"],
13
+ ["windsurf", "windsurf"],
14
+ ["chatgpt", "chatgpt"],
15
+ ["openai", "chatgpt"],
16
+ ["gemini", "gemini"],
17
+ ["perplexity", "perplexity"],
18
+ ]);
19
+ export function frameworkForClient(name) {
20
+ return typeof name === "string" ? CLIENT_FRAMEWORKS.get(name) : undefined;
21
+ }
22
+ //# sourceMappingURL=client-framework.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-framework.js","sourceRoot":"","sources":["../src/client-framework.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,MAAM,iBAAiB,GAAG,IAAI,GAAG,CAAyB;IACxD,CAAC,WAAW,EAAE,QAAQ,CAAC;IACvB,CAAC,aAAa,EAAE,QAAQ,CAAC;IACzB,CAAC,QAAQ,EAAE,QAAQ,CAAC;IACpB,CAAC,eAAe,EAAE,QAAQ,CAAC;IAC3B,CAAC,UAAU,EAAE,UAAU,CAAC;IACxB,CAAC,SAAS,EAAE,SAAS,CAAC;IACtB,CAAC,QAAQ,EAAE,SAAS,CAAC;IACrB,CAAC,QAAQ,EAAE,QAAQ,CAAC;IACpB,CAAC,YAAY,EAAE,YAAY,CAAC;CAC7B,CAAC,CAAC;AAEH,MAAM,UAAU,kBAAkB,CAAC,IAAa;IAC9C,OAAO,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,iBAAiB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AAC5E,CAAC"}
package/dist/config.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { EvmWalletClient } from "@aifinpay/agent";
1
2
  /** Runtime configuration loaded from env. */
2
3
  export interface McpConfig {
3
4
  /** Legacy base58 secret, after SEED_HASH and the project agents file. */
@@ -8,6 +9,10 @@ export interface McpConfig {
8
9
  agentId?: string;
9
10
  walletHome?: string;
10
11
  walletPassphrase?: string;
12
+ /** Child wallet number from a seed or indexed wallet keystore. */
13
+ walletIndex?: number;
14
+ /** Programmatic external EVM signer supplied by an embedding MCP host. */
15
+ evmWalletClient?: EvmWalletClient;
11
16
  /** Dev environment. EVM payments stay live-only; Solana requires explicit devnet. */
12
17
  devMode?: boolean;
13
18
  /** Custom AiFinPay backend URL. Defaults to production. */
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAGA,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,qFAAqF;IACrF,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB,2DAA2D;IAC3D,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,6BAA6B;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,wDAAwD;IACxD,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;0EACsE;IACtE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iEAAiE;IACjE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0EAA0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,aAAa,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;IACrC,uEAAuE;IACvE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,kEAAkE;IAClE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;yFACqF;IACrF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB,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,CA+B7C;AAgED,oFAAoF;AACpF,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,CAkF/D"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAGvD,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,kEAAkE;IAClE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0EAA0E;IAC1E,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,qFAAqF;IACrF,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB,2DAA2D;IAC3D,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,6BAA6B;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,wDAAwD;IACxD,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;0EACsE;IACtE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iEAAiE;IACjE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0EAA0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,aAAa,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;IACrC,uEAAuE;IACvE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,kEAAkE;IAClE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;yFACqF;IACrF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB,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,CAgC7C;AA4ED,oFAAoF;AACpF,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,CAkF/D"}
package/dist/config.js CHANGED
@@ -23,6 +23,7 @@ export function loadConfigFromEnv() {
23
23
  agentId: process.env.AIFINPAY_AGENT_ID || undefined,
24
24
  walletHome: process.env.AIFINPAY_HOME || undefined,
25
25
  walletPassphrase: process.env.AIFINPAY_WALLET_PASSPHRASE || undefined,
26
+ walletIndex: parseWalletIndex(process.env.AIFINPAY_WALLET_INDEX),
26
27
  agentSecretB58: process.env.AIFINPAY_AGENT_SECRET || undefined,
27
28
  baseUrl: process.env.AIFINPAY_BASE_URL || undefined,
28
29
  timeoutMs: process.env.AIFINPAY_TIMEOUT_MS ? Number(process.env.AIFINPAY_TIMEOUT_MS) : undefined,
@@ -32,6 +33,18 @@ export function loadConfigFromEnv() {
32
33
  trustedHosts: splitList(process.env.AIFINPAY_TRUSTED_HOSTS),
33
34
  };
34
35
  }
36
+ function parseWalletIndex(raw) {
37
+ if (raw === undefined)
38
+ return undefined;
39
+ if (!/^(0|[1-9][0-9]*)$/.test(raw)) {
40
+ throw new Error("AIFINPAY_WALLET_INDEX must be a non-negative integer");
41
+ }
42
+ const index = Number(raw);
43
+ if (!Number.isSafeInteger(index) || index > 0xffffffff) {
44
+ throw new Error("AIFINPAY_WALLET_INDEX must be between 0 and 4294967295");
45
+ }
46
+ return index;
47
+ }
35
48
  function parseSolanaNetwork(raw) {
36
49
  if (raw === undefined)
37
50
  return undefined;