agentic-wallet-mcp 0.6.1 → 0.7.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.
package/CHANGELOG.md CHANGED
@@ -8,6 +8,55 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
8
8
  > Entries for 0.5.0 and earlier were reconstructed from commit history when this file was
9
9
  > introduced in 0.6.0, so they summarise each release rather than being exhaustive.
10
10
 
11
+ ## [0.7.0] — 4 August 2026
12
+
13
+ **Read the breaking change before upgrading if you make x402 payments.** This release lets the wallet
14
+ provision itself, so it can start with no configuration at all — which is what makes an OpenClaw plugin
15
+ install possible, and also why the payment cap now has to default to zero.
16
+
17
+ ### Changed (breaking)
18
+
19
+ - **`MAX_PAYMENT_AMOUNT` now defaults to `{"*":"0"}` instead of being unset.** An unconfigured wallet
20
+ refuses every x402 payment rather than allowing any amount. Previously an unset cap disabled the
21
+ ceiling entirely, which was defensible only while every install required hand-written environment
22
+ variables; a wallet that can now start with no configuration must not be able to auto-pay a hostile
23
+ challenge. To keep paying, set the cap explicitly, e.g.
24
+ `MAX_PAYMENT_AMOUNT={"ZTX":"1000000000","*":"0"}`.
25
+
26
+ - **`create_holder_account` no longer takes a `password` parameter.** The wallet uses the password
27
+ already active for the session, so a model can neither be asked for one nor supply one. A
28
+ `password` still passed by an old caller is **silently ignored** rather than rejected, because the
29
+ tool schema permits extra properties. Consequence: a newly minted account now inherits the session
30
+ password instead of taking a different one.
31
+
32
+ ### Added
33
+
34
+ - **The wallet provisions itself.** `HSM_PASSWORD` and `ZETRIX_NETWORK` are both optional now.
35
+ With neither set, the wallet defaults to testnet, generates a random HSM password, creates a
36
+ holder account, and stores address, DID and password in its own state directory — so it starts
37
+ with no configuration at all. An explicit env var still wins over every other source, so an
38
+ existing `.mcp.json` behaves exactly as before.
39
+ - **`npx agentic-wallet-mcp export-credentials`** prints the address, DID and HSM password for
40
+ backup. A generated password is the only thing that can authorize signing for the account, so
41
+ losing the state directory means losing the account. Interactive terminals only, and deliberately
42
+ not an MCP tool, so an agent can never read it.
43
+ - **`--config <path>`** reads settings from a JSON file instead of the environment, for hosts that
44
+ cannot set env vars. It carries no secret: a `hsmPassword` key is a hard error, and so is any
45
+ unknown key, so a typo cannot silently start the wallet on the wrong network.
46
+ - **`ZETRIX_WALLET_STATE_DIR`** moves `account.json` and the VC cache out of the home directory.
47
+ The default is unchanged.
48
+ - `pay_and_fetch` and `subscribe_and_issue` now report a `not_activated` shortfall when the holder
49
+ address is not yet on chain, instead of reporting it as a low balance — the remedy is to send it
50
+ gas, not to top up a token.
51
+
52
+ ### Fixed
53
+
54
+ - **A zero ZTX balance no longer reports `query_failed`.** The node omits the `balance` field
55
+ entirely when it is zero, and `wallet_status({ token: "ZTX" })` treated its absence as a failed
56
+ read — so every account holding no ZTX looked like a broken lookup. A successful RPC that omits a
57
+ zero-valued field is now read as `0`. A non-zero `errorCode`, or a response with no `result` at
58
+ all, still fails loudly rather than reporting a fabricated zero.
59
+
11
60
  ## [0.6.1] — 2026-07-29
12
61
 
13
62
  **Upgrade if you use testnet.** The testnet Wallet BE and MBI endpoints have moved to the Zetrix
package/README.md CHANGED
@@ -165,8 +165,8 @@ Node ≥ 18 required (built-in `fetch`).
165
165
 
166
166
  | Variable | Required | Description |
167
167
  |---|---|---|
168
- | `ZETRIX_NETWORK` | yes | `zetrix:testnet` or `zetrix:mainnet` — also selects the default `WALLET_BE_URL`/`MBI_BASE_URL`/`OID4VP_BASE_URL`/`ZID_RESOLVER_BASE_URL` below |
169
- | `HSM_PASSWORD` | yes* | HSM password |
168
+ | `ZETRIX_NETWORK` | no | `zetrix:testnet` or `zetrix:mainnet` — also selects the default `WALLET_BE_URL`/`MBI_BASE_URL`/`OID4VP_BASE_URL`/`ZID_RESOLVER_BASE_URL` below. **Defaults to `zetrix:testnet`**; mainnet is always a deliberate choice |
169
+ | `HSM_PASSWORD` | no* | HSM password. Omit it and the wallet generates one on first run and stores it in its own state directory — see "Onboarding" below |
170
170
  | `ZETRIX_ADDRESS` | no | Holder Zetrix address (the HSM account). Omit on first run — see "Onboarding" below |
171
171
  | `HOLDER_DID` | no | Holder DID. Omit and the MCP derives it automatically — see "Onboarding" below |
172
172
  | `WALLET_BE_URL` | no | Wallet BE base URL override (HSM `/wallet/hsm/sign-blob`) — auto-derived from `ZETRIX_NETWORK` when not set |
@@ -174,22 +174,25 @@ Node ≥ 18 required (built-in `fetch`).
174
174
  | `OID4VP_BASE_URL` | no | OID4VP verifier base URL override — auto-derived from `ZETRIX_NETWORK` by the x401 SDK when not set |
175
175
  | `ZETRIX_NODE_HOST` / `ZETRIX_NODE_PORT` | no | RPC node override (auto-derived from network) |
176
176
  | `ZID_RESOLVER_BASE_URL` | no | ZID resolver override (auto-derived from network: sandbox for testnet, prod for mainnet) |
177
- | `MAX_PAYMENT_AMOUNT` | no** | Per-asset x402 auto-pay cap — JSON `{ "<asset>": "<maxRawUnits>", "*": "<fallback>" }`. `pay_and_fetch`/`subscribe_and_issue` are asset-agnostic: the resource server's 402 challenge may quote the native ZETRIX token (asset code `ZTX`) **or** a ZTP20 token (e.g. `JMYR`) — cap whichever assets you expect, e.g. `{"ZTX":"1000000000","JMYR":"5000000","*":"0"}`. Unset = no cap enforced. |
177
+ | `MAX_PAYMENT_AMOUNT` | no** | Per-asset x402 auto-pay cap — JSON `{ "<asset>": "<maxRawUnits>", "*": "<fallback>" }`. `pay_and_fetch`/`subscribe_and_issue` are asset-agnostic: the resource server's 402 challenge may quote the native ZETRIX token (asset code `ZTX`) **or** a ZTP20 token (e.g. `JMYR`) — cap whichever assets you expect, e.g. `{"ZTX":"1000000000","JMYR":"5000000","*":"0"}`. **Defaults to `{"*":"0"}` every payment is refused until you set this.** |
178
+ | `ZETRIX_WALLET_STATE_DIR` | no | Where the wallet keeps `account.json` and its VC cache. Defaults to `~/.agentic-wallet-mcp` |
178
179
 
179
- \* sensitive — never logged.
180
+ \* sensitive — never logged, never returned in a tool result, and never a tool parameter.
180
181
 
181
182
  ### Onboarding: two ways to set up your holder identity
182
183
 
183
184
  `ZETRIX_ADDRESS` and `HOLDER_DID` are both optional — the MCP resolves your holder identity at
184
185
  startup, in one of two ways:
185
186
 
186
- 1. **First-time user — only `HSM_PASSWORD` set.** The MCP creates a brand-new HSM account on
187
- Wallet BE (`POST /wallet/hsm/account/create`) and derives the DID from the returned public
188
- key. It logs the new `ZETRIX_ADDRESS` (and `HOLDER_DID`) to stderr on startup, and saves the
189
- address, DID, and password to a local account store
190
- (`~/.agentic-wallet-mcp/account.json`, owner-only) — it's reused automatically next run, no
191
- config edit required. An explicit `ZETRIX_ADDRESS`/`HSM_PASSWORD` set later in your MCP
192
- config still overrides the saved account.
187
+ 1. **First-time user — nothing set at all, or only `HSM_PASSWORD` set.** The MCP creates a
188
+ brand-new HSM account on Wallet BE (`POST /wallet/hsm/account/create`) and derives the DID from
189
+ the returned public key. If you did not set `HSM_PASSWORD`, it generates one for you first. It
190
+ logs the new `ZETRIX_ADDRESS` (and `HOLDER_DID`) to stderr on startup, and saves the address,
191
+ DID, and password to a local account store (`~/.agentic-wallet-mcp/account.json`, owner-only) —
192
+ it's reused automatically next run, no config edit required. An explicit
193
+ `ZETRIX_ADDRESS`/`HSM_PASSWORD` set later in your MCP config still overrides the saved account.
194
+
195
+ **If the wallet generated your password, back it up — see below.**
193
196
  2. **Existing user — `ZETRIX_ADDRESS` + `HSM_PASSWORD` set, `HOLDER_DID` optional.** The MCP
194
197
  always self-signs the address via the existing `POST /wallet/hsm/sign-message` call and
195
198
  derives the DID from the `publicKey` the response carries — no separate lookup endpoint
@@ -201,13 +204,28 @@ startup, in one of two ways:
201
204
  Either way, `wallet_status` always reports the resolved `zetrixAddress`/`holderDid` for the
202
205
  running session, so you can confirm what the MCP resolved to at any time.
203
206
 
204
- \*\* **strongly recommended before pointing this wallet at mainnet/real funds.** `pay_and_fetch`
205
- and `subscribe_and_issue` auto-pay whatever `maxAmountRequired` a remote server's 402 challenge
206
- demands, with no built-in ceiling a prompt-injected or misled agent calling either tool against
207
- a hostile endpoint would pay whatever that endpoint asks for, bounded only by the HSM account
208
- balance. `MAX_PAYMENT_AMOUNT` is a hard, code-enforced cap that holds regardless of what the
209
- calling agent decides. Once set, it becomes an allowlist: an asset with no entry and no `"*"`
210
- fallback is **denied**, not passed through uncapped.
207
+ ### Backing up a self-provisioned wallet
208
+
209
+ If you never set `HSM_PASSWORD`, the wallet generated one and stored it in
210
+ `~/.agentic-wallet-mcp/account.json`. You never have to type it but it is the only thing that
211
+ can authorize signing for your account. Wallet BE holds the key and will not use it without this
212
+ password, so if you lose the file the account cannot be recovered and any funds in it are gone.
213
+ Back it up with:
214
+
215
+ ```bash
216
+ npx agentic-wallet-mcp export-credentials
217
+ ```
218
+
219
+ This only runs in an interactive terminal, so it can't be piped into a file or a log, and it is
220
+ deliberately **not** available as an MCP tool — an AI agent can never read your password.
221
+
222
+ \*\* **You must set this before the wallet will pay for anything.** It defaults to `{"*":"0"}`,
223
+ which refuses every payment. `pay_and_fetch` and `subscribe_and_issue` auto-pay whatever
224
+ `maxAmountRequired` a remote server's 402 challenge demands, so without a ceiling a prompt-injected
225
+ or misled agent calling either tool against a hostile endpoint would pay whatever that endpoint
226
+ asks for, bounded only by the account balance. `MAX_PAYMENT_AMOUNT` is a hard, code-enforced cap
227
+ that holds regardless of what the calling agent decides. It is also an allowlist: an asset with no
228
+ entry and no `"*"` fallback is **denied**, not passed through uncapped.
211
229
 
212
230
  You don't need to look up or fill in `WALLET_BE_URL`/`MBI_BASE_URL`/`OID4VP_BASE_URL`/
213
231
  `ZID_RESOLVER_BASE_URL` yourself — just pick `zetrix:testnet` or `zetrix:mainnet` for
@@ -12540,14 +12540,15 @@ __export(index_exports, {
12540
12540
  buildToolList: () => buildToolList
12541
12541
  });
12542
12542
  module.exports = __toCommonJS(index_exports);
12543
- var import_node_crypto2 = require("node:crypto");
12544
- var import_node_os = require("node:os");
12545
- var import_node_path3 = require("node:path");
12543
+ var import_node_crypto3 = require("node:crypto");
12544
+ var import_node_fs = require("node:fs");
12545
+ var import_node_os2 = require("node:os");
12546
+ var import_node_path4 = require("node:path");
12546
12547
 
12547
12548
  // package.json
12548
12549
  var package_default = {
12549
12550
  name: "agentic-wallet-mcp",
12550
- version: "0.6.1",
12551
+ version: "0.7.0",
12551
12552
  description: "Agent-facing MCP wallet for Zetrix \u2014 orchestrates x401 identity proof, x402 payment, and MBI VC issuance",
12552
12553
  keywords: [
12553
12554
  "mcp",
@@ -21489,6 +21490,10 @@ var X401Wallet = class {
21489
21490
  // src/index.ts
21490
21491
  var import_zetrix_sdk_nodejs = __toESM(require("zetrix-sdk-nodejs"), 1);
21491
21492
 
21493
+ // src/config.ts
21494
+ var import_node_os = require("node:os");
21495
+ var import_node_path = require("node:path");
21496
+
21492
21497
  // src/payment-guard.ts
21493
21498
  var PaymentCapError = class extends Error {
21494
21499
  constructor(message) {
@@ -21574,24 +21579,28 @@ function loadConfig(env) {
21574
21579
  const v = env[key];
21575
21580
  return v && v.trim() ? v.trim() : void 0;
21576
21581
  };
21577
- const network = req("ZETRIX_NETWORK");
21582
+ const network = opt("ZETRIX_NETWORK") ?? "zetrix:testnet";
21578
21583
  const oid4vpBaseUrlOverride = opt("OID4VP_BASE_URL");
21579
21584
  return {
21580
21585
  walletBeUrl: stripTrailingSlash(opt("WALLET_BE_URL") ?? deriveWalletBeUrl(network)),
21581
21586
  oid4vpBaseUrl: oid4vpBaseUrlOverride ? stripTrailingSlash(oid4vpBaseUrlOverride) : void 0,
21582
21587
  mbiBaseUrl: stripTrailingSlash(opt("MBI_BASE_URL") ?? deriveMbiBaseUrl(network)),
21583
21588
  network,
21589
+ stateDir: stripTrailingSlash(opt("ZETRIX_WALLET_STATE_DIR") ?? (0, import_node_path.join)((0, import_node_os.homedir)(), ".agentic-wallet-mcp")),
21584
21590
  zetrixAddress: opt("ZETRIX_ADDRESS"),
21585
21591
  holderDid: opt("HOLDER_DID"),
21586
21592
  hsmPassword: req(
21587
21593
  "HSM_PASSWORD",
21588
- "set your own HSM password in the MCP config before starting the server \u2014 this MCP never generates, invents, or infers a password on your behalf"
21594
+ "the server is normally started through main(), which generates a password when none exists \u2014 reaching this error means loadConfig was called directly without one"
21589
21595
  ),
21590
21596
  nodeHost: opt("ZETRIX_NODE_HOST") ?? deriveNodeHost(network),
21591
21597
  nodePort: opt("ZETRIX_NODE_PORT") ?? "",
21592
21598
  templateRegistryAddress: opt("ZETRIX_TEMPLATE_REGISTRY_ADDRESS") ?? deriveTemplateRegistryAddress(network),
21593
21599
  zidResolverBaseUrl: stripTrailingSlash(opt("ZID_RESOLVER_BASE_URL") ?? deriveZidResolverBaseUrl(network)),
21594
- maxPaymentAmount: parsePaymentCaps(opt("MAX_PAYMENT_AMOUNT"))
21600
+ // Fail closed: an unset cap means "spend nothing", not "spend anything". A wallet that
21601
+ // starts with no configuration at all (plugin install, see docs/openclaw) must not be
21602
+ // able to auto-pay a hostile x402 challenge. Raising it is a deliberate act.
21603
+ maxPaymentAmount: parsePaymentCaps(opt("MAX_PAYMENT_AMOUNT")) ?? { "*": "0" }
21595
21604
  };
21596
21605
  }
21597
21606
 
@@ -21654,6 +21663,15 @@ async function queryContract(input, query) {
21654
21663
 
21655
21664
  // src/clients/token-balance-client.ts
21656
21665
  var ZTX_DECIMALS = 6;
21666
+ function parseNativeBalance(res) {
21667
+ if (res?.errorCode !== 0) throw new Error(`getInfo failed with errorCode ${res?.errorCode}`);
21668
+ if (res.result === void 0 || res.result === null) throw new Error("getInfo returned no result");
21669
+ const balance = res.result.balance;
21670
+ if (typeof balance === "string") return balance;
21671
+ if (typeof balance === "number" && Number.isFinite(balance)) return String(balance);
21672
+ if (balance === void 0 || balance === null) return "0";
21673
+ throw new Error(`getInfo returned an unusable balance of type ${typeof balance}`);
21674
+ }
21657
21675
  async function fetchZTP20BalanceStrict(contractAddress, address, query) {
21658
21676
  const result = await query({
21659
21677
  contractAddress,
@@ -22109,7 +22127,7 @@ function zetrixHexStringToBytes(s) {
22109
22127
  // src/clients/vc-cache.ts
22110
22128
  var import_node_crypto = require("node:crypto");
22111
22129
  var import_promises = require("node:fs/promises");
22112
- var import_node_path = require("node:path");
22130
+ var import_node_path2 = require("node:path");
22113
22131
  function cacheFileName(templateId) {
22114
22132
  return `${(0, import_node_crypto.createHash)("sha256").update(templateId).digest("hex")}.json`;
22115
22133
  }
@@ -22120,7 +22138,7 @@ function createFsVcCache(baseDir) {
22120
22138
  return {
22121
22139
  async get(templateId) {
22122
22140
  try {
22123
- const raw = await (0, import_promises.readFile)((0, import_node_path.join)(baseDir, cacheFileName(templateId)), "utf8");
22141
+ const raw = await (0, import_promises.readFile)((0, import_node_path2.join)(baseDir, cacheFileName(templateId)), "utf8");
22124
22142
  const parsed = JSON.parse(raw);
22125
22143
  return isCachedVcShape(parsed) ? parsed : null;
22126
22144
  } catch {
@@ -22129,7 +22147,7 @@ function createFsVcCache(baseDir) {
22129
22147
  },
22130
22148
  async set(templateId, entry) {
22131
22149
  await (0, import_promises.mkdir)(baseDir, { recursive: true, mode: 448 });
22132
- await (0, import_promises.writeFile)((0, import_node_path.join)(baseDir, cacheFileName(templateId)), JSON.stringify(entry), { encoding: "utf8", mode: 384 });
22150
+ await (0, import_promises.writeFile)((0, import_node_path2.join)(baseDir, cacheFileName(templateId)), JSON.stringify(entry), { encoding: "utf8", mode: 384 });
22133
22151
  },
22134
22152
  async list() {
22135
22153
  let files;
@@ -22141,7 +22159,7 @@ function createFsVcCache(baseDir) {
22141
22159
  const entries = await Promise.all(
22142
22160
  files.filter((f) => f.endsWith(".json")).map(async (f) => {
22143
22161
  try {
22144
- const parsed = JSON.parse(await (0, import_promises.readFile)((0, import_node_path.join)(baseDir, f), "utf8"));
22162
+ const parsed = JSON.parse(await (0, import_promises.readFile)((0, import_node_path2.join)(baseDir, f), "utf8"));
22145
22163
  return isCachedVcShape(parsed) ? parsed : null;
22146
22164
  } catch {
22147
22165
  return null;
@@ -22189,8 +22207,14 @@ var PaymentReadinessError = class extends Error {
22189
22207
  this.shortfall = shortfall;
22190
22208
  }
22191
22209
  };
22192
- function toPaymentReadinessError(err, requestedAsset) {
22210
+ function toPaymentReadinessError(err, requestedAsset, activated) {
22193
22211
  if (!(err instanceof import_x402_zetrix_client.InsufficientBalanceError)) return null;
22212
+ if (activated === false) {
22213
+ return new PaymentReadinessError(
22214
+ `payment blocked: this wallet's address has not been activated on chain yet \u2014 send ZTX to it from a funded account, then retry`,
22215
+ { asset: err.asset, required: err.required, available: err.available, reason: "not_activated" }
22216
+ );
22217
+ }
22194
22218
  const reason = err.asset === "ZTX" && requestedAsset !== "ZTX" ? "gas" : "resource_payment";
22195
22219
  return new PaymentReadinessError(err.message, {
22196
22220
  asset: err.asset,
@@ -22199,11 +22223,11 @@ function toPaymentReadinessError(err, requestedAsset) {
22199
22223
  reason
22200
22224
  });
22201
22225
  }
22202
- async function payWithReadinessCheck(requestedAsset, rawPay) {
22226
+ async function payWithReadinessCheck(requestedAsset, rawPay, activated) {
22203
22227
  try {
22204
22228
  return await rawPay();
22205
22229
  } catch (err) {
22206
- const readinessError = toPaymentReadinessError(err, requestedAsset);
22230
+ const readinessError = toPaymentReadinessError(err, requestedAsset, activated);
22207
22231
  if (readinessError) throw readinessError;
22208
22232
  throw err;
22209
22233
  }
@@ -22412,9 +22436,9 @@ async function createHolderAccount(deps, input) {
22412
22436
  message: `An account already exists for this wallet (zetrixAddress=${existing.zetrixAddress}, holderDid=${existing.holderDid}). Ask the user whether to keep using it or create a brand-new one \u2014 call create_holder_account again with confirmNew:true to mint a new account.`
22413
22437
  };
22414
22438
  }
22415
- const { zetrixAddress, publicKeyHex, activated } = await deps.create(input.password, input.label, input.purpose);
22439
+ const { zetrixAddress, publicKeyHex, activated } = await deps.create(input.label, input.purpose);
22416
22440
  const holderDid = deriveHolderDid(publicKeyHex);
22417
- await deps.saveAccount({ zetrixAddress, holderDid, hsmPassword: input.password, label: input.label, purpose: input.purpose });
22441
+ await deps.saveAccount({ zetrixAddress, holderDid, label: input.label, purpose: input.purpose });
22418
22442
  const finalActivated = activated || await waitForActivation(deps.checkActivationStatus, zetrixAddress, deps.sleep);
22419
22443
  const message = `New holder HSM account created \u2014 address, DID, and password saved to the wallet's local account store. Both will be used automatically on the next server restart; no manual config edit needed. If your MCP config also sets ZETRIX_ADDRESS/HSM_PASSWORD via environment variables, update or remove those too: an explicit env ZETRIX_ADDRESS/HSM_PASSWORD always takes precedence over the saved account. ZETRIX_ADDRESS=${zetrixAddress} (HOLDER_DID=${holderDid} is optional \u2014 it re-derives automatically).` + (finalActivated ? "" : ` Note: on-chain activation has not completed yet \u2014 balance/on-chain calls for this address may fail until it does; check again later.`);
22420
22444
  return { created: true, alreadyExists: Boolean(existing), zetrixAddress, holderDid, publicKeyHex, activated: finalActivated, message };
@@ -22922,9 +22946,123 @@ async function resolveHolder(deps, input) {
22922
22946
  return { zetrixAddress: input.zetrixAddress, holderDid: derivedDid, created: false, didMismatch };
22923
22947
  }
22924
22948
 
22949
+ // src/startup-env.ts
22950
+ function resolveStartupEnv(input) {
22951
+ const env = { ...input.fileEnv, ...input.processEnv };
22952
+ const stored = input.storedAccount;
22953
+ if (!env.ZETRIX_ADDRESS && stored) env.ZETRIX_ADDRESS = stored.zetrixAddress;
22954
+ if (!env.HOLDER_DID && stored) env.HOLDER_DID = stored.holderDid;
22955
+ if (!env.HSM_PASSWORD && stored) env.HSM_PASSWORD = stored.hsmPassword;
22956
+ let passwordGenerated = false;
22957
+ if (!env.HSM_PASSWORD) {
22958
+ env.HSM_PASSWORD = input.generatePassword();
22959
+ passwordGenerated = true;
22960
+ }
22961
+ return { env, passwordGenerated };
22962
+ }
22963
+
22964
+ // src/config-file.ts
22965
+ var KEY_TO_ENV = {
22966
+ network: "ZETRIX_NETWORK",
22967
+ zetrixAddress: "ZETRIX_ADDRESS",
22968
+ holderDid: "HOLDER_DID",
22969
+ maxPaymentAmount: "MAX_PAYMENT_AMOUNT",
22970
+ stateDir: "ZETRIX_WALLET_STATE_DIR",
22971
+ walletBeUrl: "WALLET_BE_URL",
22972
+ mbiBaseUrl: "MBI_BASE_URL"
22973
+ };
22974
+ var FORBIDDEN_KEYS = ["hsmPassword", "password", "HSM_PASSWORD"];
22975
+ function configPathFrom(argv) {
22976
+ const inline = argv.find((a) => a.startsWith("--config="));
22977
+ if (inline) {
22978
+ const path2 = inline.slice("--config=".length);
22979
+ if (!path2) throw new Error("agentic-wallet-mcp: --config requires a file path");
22980
+ return path2;
22981
+ }
22982
+ const i = argv.indexOf("--config");
22983
+ if (i === -1) return null;
22984
+ const path = argv[i + 1];
22985
+ if (!path || path.startsWith("--")) throw new Error("agentic-wallet-mcp: --config requires a file path");
22986
+ return path;
22987
+ }
22988
+ function loadConfigFileEnv(argv, readFile3) {
22989
+ const path = configPathFrom(argv);
22990
+ if (path === null) return {};
22991
+ let raw;
22992
+ try {
22993
+ raw = readFile3(path);
22994
+ } catch (e) {
22995
+ throw new Error(`agentic-wallet-mcp: cannot read config file ${path}: ${e.message}`);
22996
+ }
22997
+ let parsed;
22998
+ try {
22999
+ parsed = JSON.parse(raw);
23000
+ } catch (e) {
23001
+ throw new Error(`agentic-wallet-mcp: config file ${path} is not valid JSON: ${e.message}`);
23002
+ }
23003
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
23004
+ throw new Error(`agentic-wallet-mcp: config file ${path} must contain a JSON object`);
23005
+ }
23006
+ const env = {};
23007
+ for (const [key, value] of Object.entries(parsed)) {
23008
+ if (FORBIDDEN_KEYS.includes(key)) {
23009
+ throw new Error(
23010
+ `agentic-wallet-mcp: config file ${path} must not contain a secret ("${key}") \u2014 the wallet generates and stores its own HSM password; see docs/openclaw/DEPLOYMENT_DESIGN.md`
23011
+ );
23012
+ }
23013
+ const envKey = KEY_TO_ENV[key];
23014
+ if (!envKey) {
23015
+ throw new Error(
23016
+ `agentic-wallet-mcp: unknown config key "${key}" in ${path} \u2014 allowed: ${Object.keys(KEY_TO_ENV).join(", ")}`
23017
+ );
23018
+ }
23019
+ env[envKey] = typeof value === "string" ? value : JSON.stringify(value);
23020
+ }
23021
+ return env;
23022
+ }
23023
+
23024
+ // src/hsm-password.ts
23025
+ var import_node_crypto2 = require("node:crypto");
23026
+ function generateHsmPassword() {
23027
+ return (0, import_node_crypto2.randomBytes)(24).toString("base64url");
23028
+ }
23029
+
23030
+ // src/export-credentials.ts
23031
+ async function exportCredentials(deps) {
23032
+ if (!deps.isTty) {
23033
+ deps.writeErr(
23034
+ "agentic-wallet-mcp: export-credentials only runs in an interactive terminal, so the password cannot be piped into a file or a log. Run it directly in your shell.\n"
23035
+ );
23036
+ return { exitCode: 1 };
23037
+ }
23038
+ const account = await deps.getAccount();
23039
+ if (!account) {
23040
+ deps.writeErr(
23041
+ "agentic-wallet-mcp: no account has been created yet \u2014 start the wallet once to provision one, then run this again.\n"
23042
+ );
23043
+ return { exitCode: 1 };
23044
+ }
23045
+ deps.write(
23046
+ `
23047
+ Zetrix Agentic Wallet \u2014 credentials for backup
23048
+
23049
+ Address: ${account.zetrixAddress}
23050
+ Holder DID: ${account.holderDid}
23051
+ HSM password: ${account.hsmPassword}
23052
+
23053
+ Store these somewhere safe and private, such as a password manager.
23054
+ The HSM password is the only thing that can authorize signing for this wallet. If you lose it
23055
+ and lose this machine's wallet state, the account cannot be recovered and any funds in it are
23056
+ permanently inaccessible.
23057
+
23058
+ `
23059
+ );
23060
+ return { exitCode: 0 };
23061
+ }
23062
+
22925
23063
  // src/clients/account-store.ts
22926
23064
  var import_promises2 = require("node:fs/promises");
22927
- var import_node_path2 = require("node:path");
23065
+ var import_node_path3 = require("node:path");
22928
23066
  function isStoredAccountShape(value) {
22929
23067
  return typeof value === "object" && value !== null && typeof value.zetrixAddress === "string" && typeof value.holderDid === "string" && typeof value.hsmPassword === "string";
22930
23068
  }
@@ -22940,7 +23078,7 @@ function createFsAccountStore(filePath) {
22940
23078
  }
22941
23079
  },
22942
23080
  async set(account) {
22943
- await (0, import_promises2.mkdir)((0, import_node_path2.dirname)(filePath), { recursive: true, mode: 448 });
23081
+ await (0, import_promises2.mkdir)((0, import_node_path3.dirname)(filePath), { recursive: true, mode: 448 });
22944
23082
  await (0, import_promises2.writeFile)(filePath, JSON.stringify(account, null, 2), { encoding: "utf8", mode: 384 });
22945
23083
  }
22946
23084
  };
@@ -23052,19 +23190,17 @@ function buildToolList() {
23052
23190
  },
23053
23191
  {
23054
23192
  name: "create_holder_account",
23055
- description: "Create a new holder HSM account on Wallet BE (onboarding). Ask the user for a password first \u2014 never invent one. ALWAYS check first: if an account already exists for this session, this returns { alreadyExists: true, existing: {...} } WITHOUT creating anything \u2014 ask the user whether to keep using the existing account or create a new one, then call again with confirmNew:true only if they choose new. A freshly created account is saved to this MCP's local account store and reused automatically on the next restart; an explicit ZETRIX_ADDRESS in the MCP config still overrides it.",
23193
+ description: "Create a new holder HSM account on Wallet BE (onboarding). ALWAYS check first: if an account already exists for this session, this returns { alreadyExists: true, existing: {...} } WITHOUT creating anything \u2014 ask the user whether to keep using the existing account or create a new one, then call again with confirmNew:true only if they choose new. The wallet manages its own credentials; you neither need nor can supply any. A freshly created account is saved to this MCP's local account store and reused automatically on the next restart; an explicit ZETRIX_ADDRESS in the MCP config still overrides it.",
23056
23194
  inputSchema: {
23057
23195
  type: "object",
23058
23196
  properties: {
23059
- password: { type: "string", description: "HSM password to protect the new account. Must come from the user." },
23060
23197
  label: { type: "string" },
23061
23198
  purpose: { type: "string" },
23062
23199
  confirmNew: {
23063
23200
  type: "boolean",
23064
23201
  description: "Set true to mint a new account even though one already exists for this session \u2014 only after the user has confirmed they want a new one."
23065
23202
  }
23066
- },
23067
- required: ["password"]
23203
+ }
23068
23204
  }
23069
23205
  }
23070
23206
  ];
@@ -23074,12 +23210,25 @@ function asPayRequest(accept) {
23074
23210
  return { ...accept, extra: { gasModel: "client", ...extra } };
23075
23211
  }
23076
23212
  async function main() {
23077
- const accountStore = createFsAccountStore((0, import_node_path3.join)((0, import_node_os.homedir)(), ".agentic-wallet-mcp", "account.json"));
23213
+ const fileEnv = loadConfigFileEnv(process.argv, (p) => (0, import_node_fs.readFileSync)(p, "utf8"));
23214
+ const stateDir = (process.env.ZETRIX_WALLET_STATE_DIR ?? fileEnv.ZETRIX_WALLET_STATE_DIR ?? (0, import_node_path4.join)((0, import_node_os2.homedir)(), ".agentic-wallet-mcp")).replace(/\/+$/, "");
23215
+ const accountStore = createFsAccountStore((0, import_node_path4.join)(stateDir, "account.json"));
23216
+ if (process.argv[2] === "export-credentials") {
23217
+ const { exitCode } = await exportCredentials({
23218
+ getAccount: () => accountStore.get(),
23219
+ isTty: Boolean(process.stdout.isTTY),
23220
+ write: (s) => process.stdout.write(s),
23221
+ writeErr: (s) => process.stderr.write(s)
23222
+ });
23223
+ process.exit(exitCode);
23224
+ }
23078
23225
  const storedAccount = process.env.ZETRIX_ADDRESS ? null : await accountStore.get();
23079
- const env = { ...process.env };
23080
- if (!env.ZETRIX_ADDRESS && storedAccount) env.ZETRIX_ADDRESS = storedAccount.zetrixAddress;
23081
- if (!env.HOLDER_DID && storedAccount) env.HOLDER_DID = storedAccount.holderDid;
23082
- if (!env.HSM_PASSWORD && storedAccount) env.HSM_PASSWORD = storedAccount.hsmPassword;
23226
+ const { env, passwordGenerated } = resolveStartupEnv({
23227
+ processEnv: process.env,
23228
+ storedAccount,
23229
+ fileEnv,
23230
+ generatePassword: generateHsmPassword
23231
+ });
23083
23232
  const config2 = loadConfig(env);
23084
23233
  const hsmPassword = config2.hsmPassword;
23085
23234
  const be = new WalletBeClient(config2.walletBeUrl);
@@ -23096,8 +23245,9 @@ async function main() {
23096
23245
  if (created) {
23097
23246
  await accountStore.set({ zetrixAddress, holderDid, hsmPassword, createdAt: (/* @__PURE__ */ new Date()).toISOString() });
23098
23247
  process.stderr.write(
23099
- `agentic-wallet-mcp: no ZETRIX_ADDRESS was set \u2014 created a new HSM account and saved it (address, DID, and password) to ~/.agentic-wallet-mcp/account.json; it will be reused automatically next run. ZETRIX_ADDRESS=${zetrixAddress} (HOLDER_DID=${holderDid} is optional; it re-derives automatically).
23100
- `
23248
+ `agentic-wallet-mcp: no ZETRIX_ADDRESS was set \u2014 created a new HSM account and saved it to ~/.agentic-wallet-mcp/account.json; it will be reused automatically next run. ZETRIX_ADDRESS=${zetrixAddress} (HOLDER_DID=${holderDid} is optional; it re-derives automatically).` + (passwordGenerated ? ` An HSM password was generated for this account \u2014 you never need to enter it, but it is the ONLY thing that can authorize signing for this wallet. If this file is lost the account cannot be recovered. Back it up with: npx agentic-wallet-mcp export-credentials
23249
+ ` : `
23250
+ `)
23101
23251
  );
23102
23252
  } else if (storedAccount && config2.zetrixAddress === storedAccount.zetrixAddress) {
23103
23253
  process.stderr.write(
@@ -23124,13 +23274,7 @@ async function main() {
23124
23274
  const sdk = new import_zetrix_sdk_nodejs.default({ host: config2.nodeHost, port: config2.nodePort });
23125
23275
  const contractQuery = (a) => sdk.contract.call(a);
23126
23276
  const resolveSymbol = (asset) => resolveAssetSymbol(asset, contractQuery);
23127
- const fetchNativeBalance = async (address) => {
23128
- const res = await sdk.account.getInfo(address);
23129
- if (res.errorCode !== 0) throw new Error(`getInfo failed with errorCode ${res.errorCode}`);
23130
- const balance = res.result?.balance;
23131
- if (typeof balance !== "string") throw new Error("getInfo returned no balance");
23132
- return balance;
23133
- };
23277
+ const fetchNativeBalance = async (address) => parseNativeBalance(await sdk.account.getInfo(address));
23134
23278
  const tokenBalanceDeps = {
23135
23279
  address: zetrixAddress,
23136
23280
  fetchNativeBalance,
@@ -23141,13 +23285,14 @@ async function main() {
23141
23285
  const nodeBaseUrl = `https://${config2.nodeHost}${config2.nodePort ? `:${config2.nodePort}` : ""}`;
23142
23286
  const nodeMetaQuery = (url) => fetch(url, { headers: { Accept: "application/json" } }).then((r) => r.json());
23143
23287
  const resolveTemplateFields = (templateId) => fetchTemplateFields(templateId, config2.templateRegistryAddress, nodeBaseUrl, nodeMetaQuery);
23144
- const cacheScope = (0, import_node_crypto2.createHash)("sha256").update(`${config2.network}:${zetrixAddress}`).digest("hex");
23145
- const vcCache = createFsVcCache((0, import_node_path3.join)((0, import_node_os.homedir)(), ".agentic-wallet-mcp", "vc-cache", cacheScope));
23288
+ const cacheScope = (0, import_node_crypto3.createHash)("sha256").update(`${config2.network}:${zetrixAddress}`).digest("hex");
23289
+ const vcCache = createFsVcCache((0, import_node_path4.join)(config2.stateDir, "vc-cache", cacheScope));
23146
23290
  const pay = (accept) => {
23147
23291
  assertWithinPaymentCap(accept, config2.maxPaymentAmount);
23148
23292
  return payWithReadinessCheck(
23149
23293
  String(accept.asset ?? ""),
23150
- () => import_x402_zetrix_client2.PaymentEngine.pay(asPayRequest(accept), walletCfg, node, {}, walletBeSignerFn)
23294
+ () => import_x402_zetrix_client2.PaymentEngine.pay(asPayRequest(accept), walletCfg, node, {}, walletBeSignerFn),
23295
+ activated
23151
23296
  );
23152
23297
  };
23153
23298
  const payer = async (req) => {
@@ -23199,8 +23344,10 @@ async function main() {
23199
23344
  subscribeDeps: { mbi, sign: subscribeSign, pay, resolveSymbol, holderDid, resolveTemplateFields, cache: vcCache },
23200
23345
  queryContract: (input) => queryContract(input, contractQuery),
23201
23346
  queryTokenBalance: queryTokenBalance2,
23202
- createAccount: (password, label, purpose) => be.createAccount(password, label, purpose),
23203
- saveAccount: (account) => accountStore.set({ ...account, createdAt: (/* @__PURE__ */ new Date()).toISOString() }),
23347
+ // The session password is bound here, in the wiring, so it never crosses into the tool
23348
+ // layer create_holder_account has no password parameter for a model to be asked for.
23349
+ createAccount: (label, purpose) => be.createAccount(hsmPassword, label, purpose),
23350
+ saveAccount: (account) => accountStore.set({ ...account, hsmPassword, createdAt: (/* @__PURE__ */ new Date()).toISOString() }),
23204
23351
  checkActivationStatus: (address) => be.checkActivationStatus(address),
23205
23352
  sleep,
23206
23353
  cache: vcCache
package/package.json CHANGED
@@ -1,55 +1,55 @@
1
- {
2
- "name": "agentic-wallet-mcp",
3
- "version": "0.6.1",
4
- "description": "Agent-facing MCP wallet for Zetrix — orchestrates x401 identity proof, x402 payment, and MBI VC issuance",
5
- "keywords": [
6
- "mcp",
7
- "model-context-protocol",
8
- "zetrix",
9
- "wallet",
10
- "x401",
11
- "x402",
12
- "blockchain"
13
- ],
14
- "license": "MIT",
15
- "author": "MyEG Services Berhad",
16
- "homepage": "https://github.com/Zetrix-Chain/zetrix-agentic-wallet",
17
- "repository": {
18
- "type": "git",
19
- "url": "https://github.com/Zetrix-Chain/zetrix-agentic-wallet.git"
20
- },
21
- "bugs": {
22
- "url": "https://github.com/Zetrix-Chain/zetrix-agentic-wallet/issues"
23
- },
24
- "type": "module",
25
- "engines": {
26
- "node": ">=18"
27
- },
28
- "bin": {
29
- "agentic-wallet-mcp": "dist/server-bundle.cjs"
30
- },
31
- "files": [
32
- "dist/server-bundle.cjs",
33
- "README.md",
34
- "CHANGELOG.md",
35
- "LICENSE"
36
- ],
37
- "scripts": {
38
- "build": "tsc --noEmit && esbuild src/index.ts --bundle --platform=node --target=node18 --format=cjs --external:zetrix-sdk-nodejs --outfile=dist/server-bundle.cjs",
39
- "test": "vitest run",
40
- "test:watch": "vitest",
41
- "typecheck": "tsc --noEmit"
42
- },
43
- "dependencies": {
44
- "@modelcontextprotocol/sdk": "^1.0.0",
45
- "x401-zetrix-client": "^0.2.1",
46
- "x402-zetrix-client": "^0.2.2",
47
- "zetrix-sdk-nodejs": "^1.0.3"
48
- },
49
- "devDependencies": {
50
- "@types/node": "^20.0.0",
51
- "esbuild": "^0.24.0",
52
- "typescript": "^5.9.0",
53
- "vitest": "^1.6.0"
54
- }
55
- }
1
+ {
2
+ "name": "agentic-wallet-mcp",
3
+ "version": "0.7.0",
4
+ "description": "Agent-facing MCP wallet for Zetrix — orchestrates x401 identity proof, x402 payment, and MBI VC issuance",
5
+ "keywords": [
6
+ "mcp",
7
+ "model-context-protocol",
8
+ "zetrix",
9
+ "wallet",
10
+ "x401",
11
+ "x402",
12
+ "blockchain"
13
+ ],
14
+ "license": "MIT",
15
+ "author": "MyEG Services Berhad",
16
+ "homepage": "https://github.com/Zetrix-Chain/zetrix-agentic-wallet",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/Zetrix-Chain/zetrix-agentic-wallet.git"
20
+ },
21
+ "bugs": {
22
+ "url": "https://github.com/Zetrix-Chain/zetrix-agentic-wallet/issues"
23
+ },
24
+ "type": "module",
25
+ "engines": {
26
+ "node": ">=18"
27
+ },
28
+ "bin": {
29
+ "agentic-wallet-mcp": "dist/server-bundle.cjs"
30
+ },
31
+ "files": [
32
+ "dist/server-bundle.cjs",
33
+ "README.md",
34
+ "CHANGELOG.md",
35
+ "LICENSE"
36
+ ],
37
+ "scripts": {
38
+ "build": "tsc --noEmit && esbuild src/index.ts --bundle --platform=node --target=node18 --format=cjs --external:zetrix-sdk-nodejs --outfile=dist/server-bundle.cjs",
39
+ "test": "vitest run",
40
+ "test:watch": "vitest",
41
+ "typecheck": "tsc --noEmit"
42
+ },
43
+ "dependencies": {
44
+ "@modelcontextprotocol/sdk": "^1.0.0",
45
+ "x401-zetrix-client": "^0.2.1",
46
+ "x402-zetrix-client": "^0.2.2",
47
+ "zetrix-sdk-nodejs": "^1.0.3"
48
+ },
49
+ "devDependencies": {
50
+ "@types/node": "^20.0.0",
51
+ "esbuild": "^0.24.0",
52
+ "typescript": "^5.9.0",
53
+ "vitest": "^1.6.0"
54
+ }
55
+ }