agentic-wallet-mcp 0.6.1 → 0.8.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,97 @@ 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.8.0] — 17 August 2026
12
+
13
+ ### Added
14
+
15
+ - **`request_ai_birthcert_verification` / `check_ai_birthcert_verification` tools** — a
16
+ **Verified** AI Birthcert flow via myid's SSIVC API, distinct from `subscribe_and_issue`'s
17
+ self-declared **Basic** AI Birthcert. `request_ai_birthcert_verification` starts a session and
18
+ returns a `verificationUrl` for the human owner to complete MyDigital ID verification;
19
+ `check_ai_birthcert_verification` polls that session and, once `status: "issued"`, fetches the
20
+ credential from MBI, verifies its subject against this wallet's `holderDid`, and caches it
21
+ locally (returned as `vc`, and from then on also visible via `wallet_status` and usable by
22
+ `prove_identity`) — a `cacheError` instead of `vc` means the credential was issued but could not
23
+ yet be fetched/verified/cached, which is not the same as issuance failing. Both tools are wired
24
+ whenever `SSIVC_BASE_URL` resolves — always on testnet; on mainnet only once set explicitly,
25
+ since the mainnet host was never actually confirmed reachable. `request_ai_birthcert_verification`'s
26
+ session creation is x402-payment-gated: the tool self-pays myid's 402 challenge, subject to the
27
+ wallet's `MAX_PAYMENT_AMOUNT` cap, the same as `pay_and_fetch`/`subscribe_and_issue`. A session
28
+ that goes terminal without minting a credential (owner never verifies, verification fails, etc.)
29
+ can be retried without paying again, since the payment stays valid until actually consumed —
30
+ concurrent requests are serialized so this can't double-pay, and switching to a different agent
31
+ name while a session is still pending is refused rather than silently losing track of it. New env
32
+ vars: `SSIVC_BASE_URL` (auto-derived on testnet only — `ssivc-api-uat.myegdev.com/api`; unset on
33
+ mainnet unless overridden) and `AI_BIRTHCERT_VERIFIED_TEMPLATE_ID` (same pattern — testnet only,
34
+ since the mainnet template id was never confirmed on-chain).
35
+
36
+ ### Fixed
37
+
38
+ - **A malformed or unexpectedly-shaped response from MBI's credential-download endpoint no longer
39
+ crashes `check_ai_birthcert_verification` with a raw, undiagnosable error.** Found via live
40
+ testing against a real verification flow: MBI's response envelope turned out to be nested one
41
+ level deeper than expected. Both the crash and the actual envelope shape are now handled
42
+ correctly, confirmed end-to-end against a live credential issuance.
43
+ - **A downloaded-but-not-yet-cached credential is no longer lost if it fails validation or the
44
+ process crashes before caching.** MBI's download is one-shot — a second attempt just fails — so
45
+ the raw response is now persisted immediately on arrival and re-validated from that copy on any
46
+ retry, instead of being discarded the moment a check (subject match, expiry) rejects it.
47
+ - Fixed several smaller gaps found in the same review: an already-settled payment blob and an empty
48
+ payment-options response now return a clean error instead of an unhandled exception; a 409 with a
49
+ non-JSON body still reports the right error kind; an unrecognized session status is no longer
50
+ assumed safe to retry against; and switching agents no longer risks silently overwriting another
51
+ agent's still-unresolved payment.
52
+
53
+ ## [0.7.0] — 4 August 2026
54
+
55
+ **Read the breaking change before upgrading if you make x402 payments.** This release lets the wallet
56
+ provision itself, so it can start with no configuration at all — which is what makes an OpenClaw plugin
57
+ install possible, and also why the payment cap now has to default to zero.
58
+
59
+ ### Changed (breaking)
60
+
61
+ - **`MAX_PAYMENT_AMOUNT` now defaults to `{"*":"0"}` instead of being unset.** An unconfigured wallet
62
+ refuses every x402 payment rather than allowing any amount. Previously an unset cap disabled the
63
+ ceiling entirely, which was defensible only while every install required hand-written environment
64
+ variables; a wallet that can now start with no configuration must not be able to auto-pay a hostile
65
+ challenge. To keep paying, set the cap explicitly, e.g.
66
+ `MAX_PAYMENT_AMOUNT={"ZTX":"1000000000","*":"0"}`.
67
+
68
+ - **`create_holder_account` no longer takes a `password` parameter.** The wallet uses the password
69
+ already active for the session, so a model can neither be asked for one nor supply one. A
70
+ `password` still passed by an old caller is **silently ignored** rather than rejected, because the
71
+ tool schema permits extra properties. Consequence: a newly minted account now inherits the session
72
+ password instead of taking a different one.
73
+
74
+ ### Added
75
+
76
+ - **The wallet provisions itself.** `HSM_PASSWORD` and `ZETRIX_NETWORK` are both optional now.
77
+ With neither set, the wallet defaults to testnet, generates a random HSM password, creates a
78
+ holder account, and stores address, DID and password in its own state directory — so it starts
79
+ with no configuration at all. An explicit env var still wins over every other source, so an
80
+ existing `.mcp.json` behaves exactly as before.
81
+ - **`npx agentic-wallet-mcp export-credentials`** prints the address, DID and HSM password for
82
+ backup. A generated password is the only thing that can authorize signing for the account, so
83
+ losing the state directory means losing the account. Interactive terminals only, and deliberately
84
+ not an MCP tool, so an agent can never read it.
85
+ - **`--config <path>`** reads settings from a JSON file instead of the environment, for hosts that
86
+ cannot set env vars. It carries no secret: a `hsmPassword` key is a hard error, and so is any
87
+ unknown key, so a typo cannot silently start the wallet on the wrong network.
88
+ - **`ZETRIX_WALLET_STATE_DIR`** moves `account.json` and the VC cache out of the home directory.
89
+ The default is unchanged.
90
+ - `pay_and_fetch` and `subscribe_and_issue` now report a `not_activated` shortfall when the holder
91
+ address is not yet on chain, instead of reporting it as a low balance — the remedy is to send it
92
+ gas, not to top up a token.
93
+
94
+ ### Fixed
95
+
96
+ - **A zero ZTX balance no longer reports `query_failed`.** The node omits the `balance` field
97
+ entirely when it is zero, and `wallet_status({ token: "ZTX" })` treated its absence as a failed
98
+ read — so every account holding no ZTX looked like a broken lookup. A successful RPC that omits a
99
+ zero-valued field is now read as `0`. A non-zero `errorCode`, or a response with no `result` at
100
+ all, still fails loudly rather than reporting a fabricated zero.
101
+
11
102
  ## [0.6.1] — 2026-07-29
12
103
 
13
104
  **Upgrade if you use testnet.** The testnet Wallet BE and MBI endpoints have moved to the Zetrix
package/README.md CHANGED
@@ -27,6 +27,8 @@ VC issuance → identity proof → pay-per-use).
27
27
  | `create_holder_account` | Onboarding: mint an HSM account (the MCP already auto-creates one at startup if `ZETRIX_ADDRESS` is omitted — see Environment below). Always checks for an existing account first — if one is active for this session, returns `{ alreadyExists: true, existing }` without creating anything; pass `confirmNew: true` (after asking the user) to mint a new one anyway | `{ password, label?, purpose?, confirmNew? }` | `{ created, alreadyExists, existing?, zetrixAddress?, holderDid?, publicKeyHex?, message }` |
28
28
  | `get_template_schema` | **Free** read of a VC template's declared attribute schema. Call before `subscribe_and_issue` to learn which attributes it requires | `{ templateId }` | `{ templateId, schema: { required, optional } }` or `{ templateId, error }` |
29
29
  | `query_contract` | Read-only query against any Zetrix contract — call an arbitrary method and return its raw result. No signing, no state change | `{ contractAddress, method, params? }` | `{ ok: true, result }` or `{ ok: false, error }` |
30
+ | `request_ai_birthcert_verification` | Start a **Verified** AI Birthcert issuance session with myid (MyDigital ID owner verification) — distinct from `subscribe_and_issue`'s self-declared Basic AI Birthcert | `{ agentName, agentPurpose?, evidenceAssuranceLevel?, ownerType?, ownerVerified? }` | `{ sessionId, verificationUrl, expiresAt }` |
31
+ | `check_ai_birthcert_verification` | Poll the most recently requested Verified AI Birthcert session; on `status: "issued"`, also fetches, verifies, and caches the credential | (none) | `{ status: "pending" \| "issued" \| "no_session", vcId?, vc?, cacheError? }` |
30
32
 
31
33
  > **VCs are cached locally**, keyed by `templateId`, under `~/.agentic-wallet-mcp/vc-cache/`
32
34
  > (scoped per network + holder — different identities or networks never share a cache).
@@ -165,8 +167,8 @@ Node ≥ 18 required (built-in `fetch`).
165
167
 
166
168
  | Variable | Required | Description |
167
169
  |---|---|---|
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 |
170
+ | `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 |
171
+ | `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
172
  | `ZETRIX_ADDRESS` | no | Holder Zetrix address (the HSM account). Omit on first run — see "Onboarding" below |
171
173
  | `HOLDER_DID` | no | Holder DID. Omit and the MCP derives it automatically — see "Onboarding" below |
172
174
  | `WALLET_BE_URL` | no | Wallet BE base URL override (HSM `/wallet/hsm/sign-blob`) — auto-derived from `ZETRIX_NETWORK` when not set |
@@ -174,22 +176,27 @@ Node ≥ 18 required (built-in `fetch`).
174
176
  | `OID4VP_BASE_URL` | no | OID4VP verifier base URL override — auto-derived from `ZETRIX_NETWORK` by the x401 SDK when not set |
175
177
  | `ZETRIX_NODE_HOST` / `ZETRIX_NODE_PORT` | no | RPC node override (auto-derived from network) |
176
178
  | `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. |
179
+ | `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. **Key by contract address, not symbol** — the challenge identifies a ZTP20 token by its contract address, so `{"JMYR":...}` never matches and falls through to `"*"`. e.g. `{"ZTX":"1000000000","ZTX3WeinXtt28YMyr4vUZ14ddTgEMGeuc1e6b":"5000000","*":"0"}`. **Defaults to `{"*":"0"}` — every payment is refused until you set this.** |
180
+ | `ZETRIX_WALLET_STATE_DIR` | no | Where the wallet keeps `account.json` and its VC cache. Defaults to `~/.agentic-wallet-mcp` |
181
+ | `SSIVC_BASE_URL` | no | myid's SSIVC API base URL, for the Verified AI Birthcert flow (`request_ai_birthcert_verification`/`check_ai_birthcert_verification`). **Auto-derived per network** — testnet `https://ssivc-api-uat.myegdev.com/api`, mainnet `https://verifyid-api.zetrix.com/api`. Override only if either changes |
182
+ | `AI_BIRTHCERT_VERIFIED_TEMPLATE_ID` | no | The Verified AI Birthcert's on-chain `did:zid:...` template id. Auto-derived per network by default — **the mainnet default is unverified**, so override this explicitly once the mainnet template id is confirmed |
178
183
 
179
- \* sensitive — never logged.
184
+ \* sensitive — never logged, never returned in a tool result, and never a tool parameter.
180
185
 
181
186
  ### Onboarding: two ways to set up your holder identity
182
187
 
183
188
  `ZETRIX_ADDRESS` and `HOLDER_DID` are both optional — the MCP resolves your holder identity at
184
189
  startup, in one of two ways:
185
190
 
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.
191
+ 1. **First-time user — nothing set at all, or only `HSM_PASSWORD` set.** The MCP creates a
192
+ brand-new HSM account on Wallet BE (`POST /wallet/hsm/account/create`) and derives the DID from
193
+ the returned public key. If you did not set `HSM_PASSWORD`, it generates one for you first. It
194
+ logs the new `ZETRIX_ADDRESS` (and `HOLDER_DID`) to stderr on startup, and saves the address,
195
+ DID, and password to a local account store (`~/.agentic-wallet-mcp/account.json`, owner-only) —
196
+ it's reused automatically next run, no config edit required. An explicit
197
+ `ZETRIX_ADDRESS`/`HSM_PASSWORD` set later in your MCP config still overrides the saved account.
198
+
199
+ **If the wallet generated your password, back it up — see below.**
193
200
  2. **Existing user — `ZETRIX_ADDRESS` + `HSM_PASSWORD` set, `HOLDER_DID` optional.** The MCP
194
201
  always self-signs the address via the existing `POST /wallet/hsm/sign-message` call and
195
202
  derives the DID from the `publicKey` the response carries — no separate lookup endpoint
@@ -201,13 +208,28 @@ startup, in one of two ways:
201
208
  Either way, `wallet_status` always reports the resolved `zetrixAddress`/`holderDid` for the
202
209
  running session, so you can confirm what the MCP resolved to at any time.
203
210
 
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.
211
+ ### Backing up a self-provisioned wallet
212
+
213
+ If you never set `HSM_PASSWORD`, the wallet generated one and stored it in
214
+ `~/.agentic-wallet-mcp/account.json`. You never have to type it — but it is the only thing that
215
+ can authorize signing for your account. Wallet BE holds the key and will not use it without this
216
+ password, so if you lose the file the account cannot be recovered and any funds in it are gone.
217
+ Back it up with:
218
+
219
+ ```bash
220
+ npx agentic-wallet-mcp export-credentials
221
+ ```
222
+
223
+ This only runs in an interactive terminal, so it can't be piped into a file or a log, and it is
224
+ deliberately **not** available as an MCP tool — an AI agent can never read your password.
225
+
226
+ \*\* **You must set this before the wallet will pay for anything.** It defaults to `{"*":"0"}`,
227
+ which refuses every payment. `pay_and_fetch` and `subscribe_and_issue` auto-pay whatever
228
+ `maxAmountRequired` a remote server's 402 challenge demands, so without a ceiling a prompt-injected
229
+ or misled agent calling either tool against a hostile endpoint would pay whatever that endpoint
230
+ asks for, bounded only by the account balance. `MAX_PAYMENT_AMOUNT` is a hard, code-enforced cap
231
+ that holds regardless of what the calling agent decides. It is also an allowlist: an asset with no
232
+ entry and no `"*"` fallback is **denied**, not passed through uncapped.
211
233
 
212
234
  You don't need to look up or fill in `WALLET_BE_URL`/`MBI_BASE_URL`/`OID4VP_BASE_URL`/
213
235
  `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_crypto5 = require("node:crypto");
12544
+ var import_node_fs = require("node:fs");
12545
+ var import_node_os2 = require("node:os");
12546
+ var import_node_path6 = 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.8.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) {
@@ -21556,6 +21561,12 @@ function deriveMbiBaseUrl(network) {
21556
21561
  function deriveTemplateRegistryAddress(network) {
21557
21562
  return network.includes("testnet") ? "ZTX3JszqPgRUx743SAp7q7zURfjvkWuH2FMEz" : "ZTX3GqJM1U6ifMPonwD4fGvrgoTKJua7b2cKX";
21558
21563
  }
21564
+ function deriveSsivcBaseUrl(network) {
21565
+ return network.includes("testnet") ? "https://ssivc-api-uat.myegdev.com/api" : void 0;
21566
+ }
21567
+ function deriveAiBirthcertVerifiedTemplateId(network) {
21568
+ return network.includes("testnet") ? "did:zid:9641ee92552e9bcec672f300b071ff86d340ac78c83c225e95971cab8108fb80" : void 0;
21569
+ }
21559
21570
  var TOKEN_REGISTRY = {
21560
21571
  JMYR: { testnet: "ZTX3WeinXtt28YMyr4vUZ14ddTgEMGeuc1e6b", mainnet: "ZTX3NCkXBqbyJWjZZxciQez945Lu6tGAcjNJr" }
21561
21572
  };
@@ -21574,24 +21585,33 @@ function loadConfig(env) {
21574
21585
  const v = env[key];
21575
21586
  return v && v.trim() ? v.trim() : void 0;
21576
21587
  };
21577
- const network = req("ZETRIX_NETWORK");
21588
+ const network = opt("ZETRIX_NETWORK") ?? "zetrix:testnet";
21578
21589
  const oid4vpBaseUrlOverride = opt("OID4VP_BASE_URL");
21579
21590
  return {
21580
21591
  walletBeUrl: stripTrailingSlash(opt("WALLET_BE_URL") ?? deriveWalletBeUrl(network)),
21581
21592
  oid4vpBaseUrl: oid4vpBaseUrlOverride ? stripTrailingSlash(oid4vpBaseUrlOverride) : void 0,
21582
21593
  mbiBaseUrl: stripTrailingSlash(opt("MBI_BASE_URL") ?? deriveMbiBaseUrl(network)),
21583
21594
  network,
21595
+ stateDir: stripTrailingSlash(opt("ZETRIX_WALLET_STATE_DIR") ?? (0, import_node_path.join)((0, import_node_os.homedir)(), ".agentic-wallet-mcp")),
21584
21596
  zetrixAddress: opt("ZETRIX_ADDRESS"),
21585
21597
  holderDid: opt("HOLDER_DID"),
21586
21598
  hsmPassword: req(
21587
21599
  "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"
21600
+ "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
21601
  ),
21590
21602
  nodeHost: opt("ZETRIX_NODE_HOST") ?? deriveNodeHost(network),
21591
21603
  nodePort: opt("ZETRIX_NODE_PORT") ?? "",
21592
21604
  templateRegistryAddress: opt("ZETRIX_TEMPLATE_REGISTRY_ADDRESS") ?? deriveTemplateRegistryAddress(network),
21593
21605
  zidResolverBaseUrl: stripTrailingSlash(opt("ZID_RESOLVER_BASE_URL") ?? deriveZidResolverBaseUrl(network)),
21594
- maxPaymentAmount: parsePaymentCaps(opt("MAX_PAYMENT_AMOUNT"))
21606
+ // Fail closed: an unset cap means "spend nothing", not "spend anything". A wallet that starts
21607
+ // with no configuration at all must not be able to auto-pay a hostile x402 challenge. Raising
21608
+ // it is a deliberate act.
21609
+ maxPaymentAmount: parsePaymentCaps(opt("MAX_PAYMENT_AMOUNT")) ?? { "*": "0" },
21610
+ ssivcBaseUrl: (() => {
21611
+ const v = opt("SSIVC_BASE_URL") ?? deriveSsivcBaseUrl(network);
21612
+ return v ? stripTrailingSlash(v) : void 0;
21613
+ })(),
21614
+ aiBirthcertVerifiedTemplateId: opt("AI_BIRTHCERT_VERIFIED_TEMPLATE_ID") ?? deriveAiBirthcertVerifiedTemplateId(network)
21595
21615
  };
21596
21616
  }
21597
21617
 
@@ -21654,6 +21674,15 @@ async function queryContract(input, query) {
21654
21674
 
21655
21675
  // src/clients/token-balance-client.ts
21656
21676
  var ZTX_DECIMALS = 6;
21677
+ function parseNativeBalance(res) {
21678
+ if (res?.errorCode !== 0) throw new Error(`getInfo failed with errorCode ${res?.errorCode}`);
21679
+ if (res.result === void 0 || res.result === null) throw new Error("getInfo returned no result");
21680
+ const balance = res.result.balance;
21681
+ if (typeof balance === "string") return balance;
21682
+ if (typeof balance === "number" && Number.isFinite(balance)) return String(balance);
21683
+ if (balance === void 0 || balance === null) return "0";
21684
+ throw new Error(`getInfo returned an unusable balance of type ${typeof balance}`);
21685
+ }
21657
21686
  async function fetchZTP20BalanceStrict(contractAddress, address, query) {
21658
21687
  const result = await query({
21659
21688
  contractAddress,
@@ -21984,6 +22013,18 @@ var MbiClient = class _MbiClient {
21984
22013
  if (!res.ok) throw await this.error(res, "vp/ext/submit failed");
21985
22014
  return this.unwrap(res);
21986
22015
  }
22016
+ /** POST /v1/vc/ext/download — holder-authenticated; returns EVERY VC for the address, not one. */
22017
+ async downloadVcs(body, auth) {
22018
+ const res = await this.fetch("POST", "/v1/vc/ext/download", body, { signedData: auth.signedData, publicKey: auth.publicKey });
22019
+ if (!res.ok) throw await this.error(res, "vc/ext/download failed");
22020
+ const raw = await this.unwrap(res);
22021
+ const nested = raw !== null && typeof raw === "object" ? raw.data : void 0;
22022
+ const data = Array.isArray(raw) ? raw : Array.isArray(nested) ? nested : void 0;
22023
+ if (!data) {
22024
+ throw new MbiError(`MBI vc/ext/download succeeded (2xx) but returned a non-array data envelope: ${JSON.stringify(raw)}`, res.status);
22025
+ }
22026
+ return data;
22027
+ }
21987
22028
  fetch(method, path, body, extraHeaders) {
21988
22029
  const headers = { Accept: "application/json", ...extraHeaders ?? {} };
21989
22030
  if (body !== void 0) headers["Content-Type"] = "application/json";
@@ -22109,7 +22150,7 @@ function zetrixHexStringToBytes(s) {
22109
22150
  // src/clients/vc-cache.ts
22110
22151
  var import_node_crypto = require("node:crypto");
22111
22152
  var import_promises = require("node:fs/promises");
22112
- var import_node_path = require("node:path");
22153
+ var import_node_path2 = require("node:path");
22113
22154
  function cacheFileName(templateId) {
22114
22155
  return `${(0, import_node_crypto.createHash)("sha256").update(templateId).digest("hex")}.json`;
22115
22156
  }
@@ -22120,7 +22161,7 @@ function createFsVcCache(baseDir) {
22120
22161
  return {
22121
22162
  async get(templateId) {
22122
22163
  try {
22123
- const raw = await (0, import_promises.readFile)((0, import_node_path.join)(baseDir, cacheFileName(templateId)), "utf8");
22164
+ const raw = await (0, import_promises.readFile)((0, import_node_path2.join)(baseDir, cacheFileName(templateId)), "utf8");
22124
22165
  const parsed = JSON.parse(raw);
22125
22166
  return isCachedVcShape(parsed) ? parsed : null;
22126
22167
  } catch {
@@ -22129,7 +22170,7 @@ function createFsVcCache(baseDir) {
22129
22170
  },
22130
22171
  async set(templateId, entry) {
22131
22172
  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 });
22173
+ await (0, import_promises.writeFile)((0, import_node_path2.join)(baseDir, cacheFileName(templateId)), JSON.stringify(entry), { encoding: "utf8", mode: 384 });
22133
22174
  },
22134
22175
  async list() {
22135
22176
  let files;
@@ -22141,7 +22182,7 @@ function createFsVcCache(baseDir) {
22141
22182
  const entries = await Promise.all(
22142
22183
  files.filter((f) => f.endsWith(".json")).map(async (f) => {
22143
22184
  try {
22144
- const parsed = JSON.parse(await (0, import_promises.readFile)((0, import_node_path.join)(baseDir, f), "utf8"));
22185
+ const parsed = JSON.parse(await (0, import_promises.readFile)((0, import_node_path2.join)(baseDir, f), "utf8"));
22145
22186
  return isCachedVcShape(parsed) ? parsed : null;
22146
22187
  } catch {
22147
22188
  return null;
@@ -22189,8 +22230,14 @@ var PaymentReadinessError = class extends Error {
22189
22230
  this.shortfall = shortfall;
22190
22231
  }
22191
22232
  };
22192
- function toPaymentReadinessError(err, requestedAsset) {
22233
+ function toPaymentReadinessError(err, requestedAsset, activated) {
22193
22234
  if (!(err instanceof import_x402_zetrix_client.InsufficientBalanceError)) return null;
22235
+ if (activated === false) {
22236
+ return new PaymentReadinessError(
22237
+ `payment blocked: this wallet's address has not been activated on chain yet \u2014 send ZTX to it from a funded account, then retry`,
22238
+ { asset: err.asset, required: err.required, available: err.available, reason: "not_activated" }
22239
+ );
22240
+ }
22194
22241
  const reason = err.asset === "ZTX" && requestedAsset !== "ZTX" ? "gas" : "resource_payment";
22195
22242
  return new PaymentReadinessError(err.message, {
22196
22243
  asset: err.asset,
@@ -22199,11 +22246,11 @@ function toPaymentReadinessError(err, requestedAsset) {
22199
22246
  reason
22200
22247
  });
22201
22248
  }
22202
- async function payWithReadinessCheck(requestedAsset, rawPay) {
22249
+ async function payWithReadinessCheck(requestedAsset, rawPay, activated) {
22203
22250
  try {
22204
22251
  return await rawPay();
22205
22252
  } catch (err) {
22206
- const readinessError = toPaymentReadinessError(err, requestedAsset);
22253
+ const readinessError = toPaymentReadinessError(err, requestedAsset, activated);
22207
22254
  if (readinessError) throw readinessError;
22208
22255
  throw err;
22209
22256
  }
@@ -22412,9 +22459,9 @@ async function createHolderAccount(deps, input) {
22412
22459
  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
22460
  };
22414
22461
  }
22415
- const { zetrixAddress, publicKeyHex, activated } = await deps.create(input.password, input.label, input.purpose);
22462
+ const { zetrixAddress, publicKeyHex, activated } = await deps.create(input.label, input.purpose);
22416
22463
  const holderDid = deriveHolderDid(publicKeyHex);
22417
- await deps.saveAccount({ zetrixAddress, holderDid, hsmPassword: input.password, label: input.label, purpose: input.purpose });
22464
+ await deps.saveAccount({ zetrixAddress, holderDid, label: input.label, purpose: input.purpose });
22418
22465
  const finalActivated = activated || await waitForActivation(deps.checkActivationStatus, zetrixAddress, deps.sleep);
22419
22466
  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
22467
  return { created: true, alreadyExists: Boolean(existing), zetrixAddress, holderDid, publicKeyHex, activated: finalActivated, message };
@@ -22796,6 +22843,7 @@ async function loadValidCachedCredentials(cache) {
22796
22843
  const all = await cache.list();
22797
22844
  return all.filter((entry) => isVcValid(entry));
22798
22845
  }
22846
+ var AI_BIRTHCERT_NOT_CONFIGURED_ERROR = "AI Birthcert verification is not configured on this wallet. On mainnet this is expected until SSIVC_BASE_URL is set explicitly (the mainnet host was never confirmed reachable \u2014 APP-M04); on testnet it means verifyAiBirthcert was not wired at all.";
22799
22847
  function createTools(deps) {
22800
22848
  return {
22801
22849
  async wallet_status(input = {}) {
@@ -22898,6 +22946,18 @@ function createTools(deps) {
22898
22946
  },
22899
22947
  input
22900
22948
  );
22949
+ },
22950
+ request_ai_birthcert_verification(input) {
22951
+ if (!deps.verifyAiBirthcert) {
22952
+ return { error: AI_BIRTHCERT_NOT_CONFIGURED_ERROR };
22953
+ }
22954
+ return deps.verifyAiBirthcert.request(input);
22955
+ },
22956
+ check_ai_birthcert_verification() {
22957
+ if (!deps.verifyAiBirthcert) {
22958
+ return { error: AI_BIRTHCERT_NOT_CONFIGURED_ERROR };
22959
+ }
22960
+ return deps.verifyAiBirthcert.check();
22901
22961
  }
22902
22962
  };
22903
22963
  }
@@ -22914,17 +22974,131 @@ async function resolveHolder(deps, input) {
22914
22974
  finalActivated = false;
22915
22975
  }
22916
22976
  }
22917
- return { zetrixAddress, holderDid: deriveHolderDid(publicKeyHex), created: true, didMismatch: false, activated: finalActivated };
22977
+ return { zetrixAddress, holderDid: deriveHolderDid(publicKeyHex), publicKeyHex, created: true, didMismatch: false, activated: finalActivated };
22918
22978
  }
22919
22979
  const { publicKey } = await deps.signMessage(input.zetrixAddress, input.zetrixAddress, input.hsmPassword);
22920
22980
  const derivedDid = deriveHolderDid(publicKey);
22921
22981
  const didMismatch = input.holderDid !== void 0 && input.holderDid !== derivedDid;
22922
- return { zetrixAddress: input.zetrixAddress, holderDid: derivedDid, created: false, didMismatch };
22982
+ return { zetrixAddress: input.zetrixAddress, holderDid: derivedDid, publicKeyHex: publicKey, created: false, didMismatch };
22983
+ }
22984
+
22985
+ // src/startup-env.ts
22986
+ function resolveStartupEnv(input) {
22987
+ const env = { ...input.fileEnv, ...input.processEnv };
22988
+ const stored = input.storedAccount;
22989
+ if (!env.ZETRIX_ADDRESS && stored) env.ZETRIX_ADDRESS = stored.zetrixAddress;
22990
+ if (!env.HOLDER_DID && stored) env.HOLDER_DID = stored.holderDid;
22991
+ if (!env.HSM_PASSWORD && stored) env.HSM_PASSWORD = stored.hsmPassword;
22992
+ let passwordGenerated = false;
22993
+ if (!env.HSM_PASSWORD) {
22994
+ env.HSM_PASSWORD = input.generatePassword();
22995
+ passwordGenerated = true;
22996
+ }
22997
+ return { env, passwordGenerated };
22998
+ }
22999
+
23000
+ // src/config-file.ts
23001
+ var KEY_TO_ENV = {
23002
+ network: "ZETRIX_NETWORK",
23003
+ zetrixAddress: "ZETRIX_ADDRESS",
23004
+ holderDid: "HOLDER_DID",
23005
+ maxPaymentAmount: "MAX_PAYMENT_AMOUNT",
23006
+ stateDir: "ZETRIX_WALLET_STATE_DIR",
23007
+ walletBeUrl: "WALLET_BE_URL",
23008
+ mbiBaseUrl: "MBI_BASE_URL"
23009
+ };
23010
+ var FORBIDDEN_KEYS = ["hsmPassword", "password", "HSM_PASSWORD"];
23011
+ function configPathFrom(argv) {
23012
+ const inline = argv.find((a) => a.startsWith("--config="));
23013
+ if (inline) {
23014
+ const path2 = inline.slice("--config=".length);
23015
+ if (!path2) throw new Error("agentic-wallet-mcp: --config requires a file path");
23016
+ return path2;
23017
+ }
23018
+ const i = argv.indexOf("--config");
23019
+ if (i === -1) return null;
23020
+ const path = argv[i + 1];
23021
+ if (!path || path.startsWith("--")) throw new Error("agentic-wallet-mcp: --config requires a file path");
23022
+ return path;
23023
+ }
23024
+ function loadConfigFileEnv(argv, readFile5) {
23025
+ const path = configPathFrom(argv);
23026
+ if (path === null) return {};
23027
+ let raw;
23028
+ try {
23029
+ raw = readFile5(path);
23030
+ } catch (e) {
23031
+ throw new Error(`agentic-wallet-mcp: cannot read config file ${path}: ${e.message}`);
23032
+ }
23033
+ let parsed;
23034
+ try {
23035
+ parsed = JSON.parse(raw);
23036
+ } catch (e) {
23037
+ throw new Error(`agentic-wallet-mcp: config file ${path} is not valid JSON: ${e.message}`);
23038
+ }
23039
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
23040
+ throw new Error(`agentic-wallet-mcp: config file ${path} must contain a JSON object`);
23041
+ }
23042
+ const env = {};
23043
+ for (const [key, value] of Object.entries(parsed)) {
23044
+ if (FORBIDDEN_KEYS.includes(key)) {
23045
+ throw new Error(
23046
+ `agentic-wallet-mcp: config file ${path} must not contain a secret ("${key}") \u2014 the wallet generates and stores its own HSM password, so no credential belongs in this file.`
23047
+ );
23048
+ }
23049
+ const envKey = KEY_TO_ENV[key];
23050
+ if (!envKey) {
23051
+ throw new Error(
23052
+ `agentic-wallet-mcp: unknown config key "${key}" in ${path} \u2014 allowed: ${Object.keys(KEY_TO_ENV).join(", ")}`
23053
+ );
23054
+ }
23055
+ env[envKey] = typeof value === "string" ? value : JSON.stringify(value);
23056
+ }
23057
+ return env;
23058
+ }
23059
+
23060
+ // src/hsm-password.ts
23061
+ var import_node_crypto2 = require("node:crypto");
23062
+ function generateHsmPassword() {
23063
+ return (0, import_node_crypto2.randomBytes)(24).toString("base64url");
23064
+ }
23065
+
23066
+ // src/export-credentials.ts
23067
+ async function exportCredentials(deps) {
23068
+ if (!deps.isTty) {
23069
+ deps.writeErr(
23070
+ "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"
23071
+ );
23072
+ return { exitCode: 1 };
23073
+ }
23074
+ const account = await deps.getAccount();
23075
+ if (!account) {
23076
+ deps.writeErr(
23077
+ "agentic-wallet-mcp: no account has been created yet \u2014 start the wallet once to provision one, then run this again.\n"
23078
+ );
23079
+ return { exitCode: 1 };
23080
+ }
23081
+ deps.write(
23082
+ `
23083
+ Zetrix Agentic Wallet \u2014 credentials for backup
23084
+
23085
+ Address: ${account.zetrixAddress}
23086
+ Holder DID: ${account.holderDid}
23087
+ HSM password: ${account.hsmPassword}
23088
+
23089
+ Store these somewhere safe and private, such as a password manager.
23090
+ The HSM password is the only thing that can authorize signing for this wallet. If you lose it
23091
+ and lose this machine's wallet state, the account cannot be recovered and any funds in it are
23092
+ permanently inaccessible.
23093
+
23094
+ `
23095
+ );
23096
+ return { exitCode: 0 };
22923
23097
  }
22924
23098
 
22925
23099
  // src/clients/account-store.ts
22926
23100
  var import_promises2 = require("node:fs/promises");
22927
- var import_node_path2 = require("node:path");
23101
+ var import_node_path3 = require("node:path");
22928
23102
  function isStoredAccountShape(value) {
22929
23103
  return typeof value === "object" && value !== null && typeof value.zetrixAddress === "string" && typeof value.holderDid === "string" && typeof value.hsmPassword === "string";
22930
23104
  }
@@ -22940,12 +23114,369 @@ function createFsAccountStore(filePath) {
22940
23114
  }
22941
23115
  },
22942
23116
  async set(account) {
22943
- await (0, import_promises2.mkdir)((0, import_node_path2.dirname)(filePath), { recursive: true, mode: 448 });
23117
+ await (0, import_promises2.mkdir)((0, import_node_path3.dirname)(filePath), { recursive: true, mode: 448 });
22944
23118
  await (0, import_promises2.writeFile)(filePath, JSON.stringify(account, null, 2), { encoding: "utf8", mode: 384 });
22945
23119
  }
22946
23120
  };
22947
23121
  }
22948
23122
 
23123
+ // src/clients/ssivc-client.ts
23124
+ var SsivcError = class extends Error {
23125
+ httpStatus;
23126
+ /** SSIVC's own `status_code` string (distinct from the HTTP status) — e.g. "50" signature invalid, "55" expired timestamp, "23" not found. */
23127
+ statusCode;
23128
+ /** Classifies the x402-era error responses — see ADDENDUM_X402_SESSION_GATING.md §6. Undefined for the older 400/404/malformed-envelope cases. */
23129
+ kind;
23130
+ constructor(message, httpStatus, statusCode, kind) {
23131
+ super(message);
23132
+ this.name = "SsivcError";
23133
+ this.httpStatus = httpStatus;
23134
+ this.statusCode = statusCode;
23135
+ this.kind = kind;
23136
+ }
23137
+ };
23138
+ var SESSIONS_PATH = "/v2/verify/ai-birthcert/sessions";
23139
+ var SsivcClient = class {
23140
+ baseUrl;
23141
+ constructor(baseUrl) {
23142
+ this.baseUrl = baseUrl.replace(/\/+$/, "");
23143
+ }
23144
+ /** Phase 1 — POST with no payment headers. SSIVC always responds 402 with the raw x402 envelope. */
23145
+ async createSessionChallenge(body) {
23146
+ const res = await this.fetch("POST", SESSIONS_PATH, body);
23147
+ if (res.status !== 402) throw await this.error(res, "createSessionChallenge: expected 402 payment_required");
23148
+ const raw = await res.json();
23149
+ return { x402Version: raw.x402Version, accepts: raw.accepts ?? [] };
23150
+ }
23151
+ /** Fresh pay — retry with `X-Payment`. Returns the session plus the settlement receipt to persist. */
23152
+ createSessionSettle(body, xPayment) {
23153
+ return this.createSessionPaid(body, { "X-Payment": xPayment });
23154
+ }
23155
+ /** Retry after a failed-issuance terminal session — replays a still-unconsumed settlement receipt. Never sent alongside `X-Payment`. */
23156
+ createSessionWithReceipt(body, paymentReceipt) {
23157
+ return this.createSessionPaid(body, { "X-Payment-Response": paymentReceipt });
23158
+ }
23159
+ async createSessionPaid(body, paymentHeader) {
23160
+ const res = await this.fetch("POST", SESSIONS_PATH, body, paymentHeader);
23161
+ if (!res.ok) throw await this.error(res, "createSession (paid) failed");
23162
+ const env = await res.json();
23163
+ const data = env.data;
23164
+ if (!data || typeof data !== "object" || !data.sessionId || !data.verification_url || !data.expiresAt) {
23165
+ throw new SsivcError("SSIVC createSession succeeded (2xx) but returned a malformed data envelope", res.status);
23166
+ }
23167
+ const paymentReceipt = res.headers.get("x-payment-response");
23168
+ if (!paymentReceipt) {
23169
+ throw new SsivcError("SSIVC createSession succeeded (2xx) but returned no X-Payment-Response settlement receipt header", res.status);
23170
+ }
23171
+ return {
23172
+ session: { sessionId: data.sessionId, verificationUrl: data.verification_url, expiresAt: data.expiresAt },
23173
+ paymentReceipt
23174
+ };
23175
+ }
23176
+ async getSession(sessionId) {
23177
+ const res = await this.fetch("GET", `${SESSIONS_PATH}/${encodeURIComponent(sessionId)}`);
23178
+ if (!res.ok) throw await this.error(res);
23179
+ const env = await res.json();
23180
+ const data = env.data;
23181
+ if (!data || typeof data !== "object" || typeof data.sessionId !== "string" || typeof data.status !== "string") {
23182
+ throw new SsivcError("SSIVC getSession succeeded (2xx) but returned a malformed data envelope", res.status);
23183
+ }
23184
+ return data;
23185
+ }
23186
+ fetch(method, path, body, extraHeaders) {
23187
+ const headers = { Accept: "application/json", ...extraHeaders ?? {} };
23188
+ if (body !== void 0) headers["Content-Type"] = "application/json";
23189
+ return fetch(`${this.baseUrl}${path}`, {
23190
+ method,
23191
+ headers,
23192
+ ...body !== void 0 ? { body: JSON.stringify(body) } : {}
23193
+ }).catch((e) => {
23194
+ throw new SsivcError(`SSIVC ${path} request failed: ${e.message}`);
23195
+ });
23196
+ }
23197
+ async error(res, context) {
23198
+ const text = await res.text().catch(() => "");
23199
+ let msg = text;
23200
+ let statusCode;
23201
+ let kind;
23202
+ if (res.status === 409) kind = "blob_already_settled";
23203
+ else if (res.status === 400 || res.status === 422) kind = "validation";
23204
+ try {
23205
+ const j = JSON.parse(text);
23206
+ statusCode = j.status_code;
23207
+ msg = j.errors?.length ? j.errors.join("; ") : j.message ?? j.error ?? text;
23208
+ if (res.status === 402 && j.error === "payment_invalid") kind = "payment_invalid";
23209
+ else if (res.status === 503 && j.error === "facilitator_unavailable") kind = "facilitator_unavailable";
23210
+ } catch {
23211
+ }
23212
+ const prefix = context ? `${context} \u2014 ` : "";
23213
+ return new SsivcError(`${prefix}SSIVC request failed \u2014 HTTP ${res.status}: ${msg}`, res.status, statusCode, kind);
23214
+ }
23215
+ };
23216
+
23217
+ // src/clients/ssivc-session-store.ts
23218
+ var import_promises3 = require("node:fs/promises");
23219
+ var import_node_path4 = require("node:path");
23220
+ function isStoredSessionShape(value) {
23221
+ if (typeof value !== "object" || value === null) return false;
23222
+ const v = value;
23223
+ return typeof v.sessionId === "string" && typeof v.agentName === "string" && typeof v.createdAt === "string" && typeof v.verificationUrl === "string" && typeof v.paymentReceipt === "string";
23224
+ }
23225
+ function createFsSsivcSessionStore(filePath) {
23226
+ return {
23227
+ async get() {
23228
+ try {
23229
+ const raw = await (0, import_promises3.readFile)(filePath, "utf8");
23230
+ const parsed = JSON.parse(raw);
23231
+ return isStoredSessionShape(parsed) ? parsed : null;
23232
+ } catch {
23233
+ return null;
23234
+ }
23235
+ },
23236
+ async set(session) {
23237
+ await (0, import_promises3.mkdir)((0, import_node_path4.dirname)(filePath), { recursive: true, mode: 448 });
23238
+ const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
23239
+ await (0, import_promises3.writeFile)(tmpPath, JSON.stringify(session, null, 2), { encoding: "utf8", mode: 384 });
23240
+ await (0, import_promises3.rename)(tmpPath, filePath);
23241
+ }
23242
+ };
23243
+ }
23244
+
23245
+ // src/clients/ssivc-download-quarantine-store.ts
23246
+ var import_node_crypto3 = require("node:crypto");
23247
+ var import_promises4 = require("node:fs/promises");
23248
+ var import_node_path5 = require("node:path");
23249
+ function quarantineFileName(vcId) {
23250
+ return `${(0, import_node_crypto3.createHash)("sha256").update(vcId).digest("hex")}.json`;
23251
+ }
23252
+ function isQuarantinedDownloadShape(value) {
23253
+ if (typeof value !== "object" || value === null) return false;
23254
+ const v = value;
23255
+ return typeof v.vcId === "string" && "entries" in v && typeof v.downloadedAt === "string";
23256
+ }
23257
+ function createFsDownloadQuarantineStore(baseDir) {
23258
+ const pathFor = (vcId) => (0, import_node_path5.join)(baseDir, quarantineFileName(vcId));
23259
+ return {
23260
+ filePathFor: pathFor,
23261
+ async get(vcId) {
23262
+ try {
23263
+ const raw = await (0, import_promises4.readFile)(pathFor(vcId), "utf8");
23264
+ const parsed = JSON.parse(raw);
23265
+ return isQuarantinedDownloadShape(parsed) ? parsed : null;
23266
+ } catch {
23267
+ return null;
23268
+ }
23269
+ },
23270
+ async set(entry) {
23271
+ await (0, import_promises4.mkdir)(baseDir, { recursive: true, mode: 448 });
23272
+ const filePath = pathFor(entry.vcId);
23273
+ const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
23274
+ await (0, import_promises4.writeFile)(tmpPath, JSON.stringify(entry, null, 2), { encoding: "utf8", mode: 384 });
23275
+ await (0, import_promises4.rename)(tmpPath, filePath);
23276
+ }
23277
+ };
23278
+ }
23279
+
23280
+ // src/orchestrator/verify-ai-birthcert.ts
23281
+ var import_node_crypto4 = require("node:crypto");
23282
+
23283
+ // src/canonical-json.ts
23284
+ function canonicalizeJson(value) {
23285
+ if (Array.isArray(value)) {
23286
+ return `[${value.map((v) => canonicalizeJson(v)).join(",")}]`;
23287
+ }
23288
+ if (value !== null && typeof value === "object") {
23289
+ const record2 = value;
23290
+ const keys = Object.keys(record2).filter((k) => record2[k] !== void 0).sort();
23291
+ const parts = keys.map((k) => `${JSON.stringify(k)}:${canonicalizeJson(record2[k])}`);
23292
+ return `{${parts.join(",")}}`;
23293
+ }
23294
+ return JSON.stringify(value);
23295
+ }
23296
+
23297
+ // src/orchestrator/verify-ai-birthcert.ts
23298
+ function subjectMatches(vc, holderDid) {
23299
+ if (typeof vc !== "object" || vc === null) return false;
23300
+ const subject = vc.credentialSubject;
23301
+ const subjectId = typeof subject === "object" && subject !== null ? subject.id : void 0;
23302
+ return subjectId === holderDid;
23303
+ }
23304
+ function observedSubjectId(vc) {
23305
+ if (typeof vc !== "object" || vc === null) return "missing";
23306
+ const subject = vc.credentialSubject;
23307
+ const subjectId = typeof subject === "object" && subject !== null ? subject.id : void 0;
23308
+ return typeof subjectId === "string" && subjectId ? subjectId : "missing";
23309
+ }
23310
+ function isoSeconds(date3) {
23311
+ return date3.toISOString().replace(/\.\d{3}Z$/, "Z");
23312
+ }
23313
+ async function getConfirmedStatus(deps, sessionId) {
23314
+ try {
23315
+ return await deps.ssivc.getSession(sessionId);
23316
+ } catch (err) {
23317
+ if (!(err instanceof SsivcError) || err.httpStatus !== 404) throw err;
23318
+ return "gone";
23319
+ }
23320
+ }
23321
+ async function decidePriorSession(deps, agentName) {
23322
+ const stored = await deps.sessionStore.get();
23323
+ if (!stored) return { kind: "pay_fresh" };
23324
+ if (stored.agentName !== agentName) {
23325
+ const otherStatus = await getConfirmedStatus(deps, stored.sessionId);
23326
+ if (otherStatus === "gone") return { kind: "replay_receipt", receipt: stored.paymentReceipt };
23327
+ if (otherStatus.status !== "issued") {
23328
+ return {
23329
+ kind: "blocked",
23330
+ message: `a verification session for a different agent ("${stored.agentName}") is still in progress (status: "${otherStatus.status}") and its payment has not been consumed yet \u2014 starting a new session for "${agentName}" would lose track of it. Call check_ai_birthcert_verification to resolve or confirm it first.`
23331
+ };
23332
+ }
23333
+ return { kind: "pay_fresh" };
23334
+ }
23335
+ const status = await getConfirmedStatus(deps, stored.sessionId);
23336
+ if (status === "gone") return { kind: "replay_receipt", receipt: stored.paymentReceipt };
23337
+ if (status.status === "pending") {
23338
+ return {
23339
+ kind: "still_pending",
23340
+ result: { sessionId: stored.sessionId, verificationUrl: stored.verificationUrl, expiresAt: status.expiresAt }
23341
+ };
23342
+ }
23343
+ if (status.status === "issued") return { kind: "pay_fresh" };
23344
+ throw new Error(
23345
+ `requestAiBirthcertVerification: unrecognized prior session status "${status.status}" for session ${stored.sessionId} \u2014 refusing to guess whether its settlement receipt is safe to replay (see SPEC.md D8)`
23346
+ );
23347
+ }
23348
+ var requestQueue = Promise.resolve();
23349
+ function withRequestLock(fn) {
23350
+ const run = requestQueue.then(fn, fn);
23351
+ requestQueue = run.then(
23352
+ () => void 0,
23353
+ () => void 0
23354
+ );
23355
+ return run;
23356
+ }
23357
+ var NoPaymentOptionsError = class extends Error {
23358
+ };
23359
+ async function payAndCreateSession(deps, body) {
23360
+ const challenge = await deps.ssivc.createSessionChallenge(body);
23361
+ const accept = challenge.accepts[0];
23362
+ if (!accept) throw new NoPaymentOptionsError("SSIVC 402 returned no payment options");
23363
+ const xPayment = await deps.pay(accept);
23364
+ return deps.ssivc.createSessionSettle(body, xPayment);
23365
+ }
23366
+ async function requestAiBirthcertVerification(deps, input) {
23367
+ if (!input.agentName || !input.agentName.trim()) {
23368
+ throw new Error("requestAiBirthcertVerification: agentName is required");
23369
+ }
23370
+ const agentName = input.agentName.trim();
23371
+ return withRequestLock(() => requestAiBirthcertVerificationLocked(deps, agentName, input));
23372
+ }
23373
+ async function requestAiBirthcertVerificationLocked(deps, agentName, input) {
23374
+ const decision = await decidePriorSession(deps, agentName);
23375
+ if (decision.kind === "still_pending") return decision.result;
23376
+ if (decision.kind === "blocked") return { error: decision.message };
23377
+ const fields = {
23378
+ publicKey: deps.publicKeyHex,
23379
+ address: deps.address,
23380
+ timestamp: isoSeconds(deps.now()),
23381
+ agentName,
23382
+ id: agentName,
23383
+ ownerReference: deps.holderDid
23384
+ };
23385
+ if (input.agentPurpose) fields.agentPurpose = input.agentPurpose;
23386
+ if (input.evidenceAssuranceLevel) fields.evidenceAssuranceLevel = input.evidenceAssuranceLevel;
23387
+ if (input.ownerType) fields.ownerType = input.ownerType;
23388
+ if (input.ownerVerified) fields.ownerVerified = input.ownerVerified;
23389
+ const digestHex = (0, import_node_crypto4.createHash)("sha256").update(canonicalizeJson(fields), "utf8").digest("hex");
23390
+ const { signBlob: signedData } = await deps.signHexBlob(digestHex);
23391
+ const body = { ...fields, signedData };
23392
+ let paid;
23393
+ try {
23394
+ paid = decision.kind === "replay_receipt" ? await deps.ssivc.createSessionWithReceipt(body, decision.receipt) : await payAndCreateSession(deps, body);
23395
+ } catch (err) {
23396
+ if (err instanceof PaymentReadinessError) return { error: `insufficient funds: ${err.message}` };
23397
+ if (err instanceof PaymentCapError) return { error: err.message };
23398
+ if (err instanceof NoPaymentOptionsError) return { error: err.message };
23399
+ if (err instanceof SsivcError && err.kind === "blob_already_settled") {
23400
+ return { error: `payment already settled for this attempt: ${err.message}` };
23401
+ }
23402
+ throw err;
23403
+ }
23404
+ await deps.sessionStore.set({
23405
+ sessionId: paid.session.sessionId,
23406
+ agentName,
23407
+ createdAt: deps.now().toISOString(),
23408
+ verificationUrl: paid.session.verificationUrl,
23409
+ paymentReceipt: paid.paymentReceipt
23410
+ });
23411
+ return paid.session;
23412
+ }
23413
+ async function checkAiBirthcertVerification(deps) {
23414
+ const stored = await deps.sessionStore.get();
23415
+ if (!stored) {
23416
+ return {
23417
+ status: "no_session",
23418
+ message: "No verification session found for this wallet \u2014 call request_ai_birthcert_verification first."
23419
+ };
23420
+ }
23421
+ const status = await deps.ssivc.getSession(stored.sessionId);
23422
+ if (status.status !== "issued" || !status.vcId) return status;
23423
+ if (deps.verifiedTemplateId && deps.cache) {
23424
+ const cached2 = await deps.cache.get(deps.verifiedTemplateId);
23425
+ if (cached2 && cached2.vcId === status.vcId && isVcValid(cached2)) return { ...status, vc: cached2.vc };
23426
+ }
23427
+ if (!deps.verifiedTemplateId || !deps.cache) {
23428
+ return { ...status, cacheError: "AI Birthcert verified-template id is not configured \u2014 cannot fetch or cache the credential yet." };
23429
+ }
23430
+ const quarantined = await deps.quarantine.get(status.vcId);
23431
+ const quarantineFilePath = deps.quarantine.filePathFor(status.vcId);
23432
+ let entries;
23433
+ if (quarantined) {
23434
+ entries = quarantined.entries;
23435
+ } else {
23436
+ const auth = await deps.messageSigner(deps.address).then((r) => ({ signedData: r.signBlob, publicKey: r.publicKey }));
23437
+ try {
23438
+ entries = await deps.mbi.downloadVcs({ address: deps.address }, auth);
23439
+ } catch (err) {
23440
+ return { ...status, cacheError: `failed to fetch credential from MBI: ${err instanceof Error ? err.message : String(err)}` };
23441
+ }
23442
+ await deps.quarantine.set({ vcId: status.vcId, entries, downloadedAt: (/* @__PURE__ */ new Date()).toISOString() });
23443
+ }
23444
+ const match = entries.find((e) => typeof e.vc === "object" && e.vc !== null && e.vc.id === status.vcId);
23445
+ if (!match) {
23446
+ return {
23447
+ ...status,
23448
+ cacheError: `no matching credential found in MBI's download for vcId ${status.vcId} \u2014 raw response preserved for recovery at ${quarantineFilePath}`
23449
+ };
23450
+ }
23451
+ if (!subjectMatches(match.vc, deps.holderDid)) {
23452
+ return {
23453
+ ...status,
23454
+ cacheError: `downloaded credential subject (${observedSubjectId(match.vc)}) does not match this wallet's holderDid (${deps.holderDid}) \u2014 refusing to cache; raw response preserved for recovery at ${quarantineFilePath}`
23455
+ };
23456
+ }
23457
+ const validUntil = extractValidUntil(match.vc);
23458
+ if (!validUntil) {
23459
+ return {
23460
+ ...status,
23461
+ cacheError: `downloaded credential has no validUntil \u2014 refusing to cache indefinitely; raw response preserved for recovery at ${quarantineFilePath}`
23462
+ };
23463
+ }
23464
+ if (!isVcValid({ validUntil })) {
23465
+ return {
23466
+ ...status,
23467
+ cacheError: `downloaded credential expired at ${validUntil} \u2014 refusing to cache or return; raw response preserved for recovery at ${quarantineFilePath}`
23468
+ };
23469
+ }
23470
+ await deps.cache.set(deps.verifiedTemplateId, {
23471
+ templateId: deps.verifiedTemplateId,
23472
+ vc: match.vc,
23473
+ vcId: status.vcId,
23474
+ issuedAt: (/* @__PURE__ */ new Date()).toISOString(),
23475
+ validUntil
23476
+ });
23477
+ return { ...status, vc: match.vc };
23478
+ }
23479
+
22949
23480
  // src/index.ts
22950
23481
  var packageVersion = package_default.version;
22951
23482
  function buildToolList() {
@@ -23025,13 +23556,13 @@ function buildToolList() {
23025
23556
  },
23026
23557
  {
23027
23558
  name: "subscribe_and_issue",
23028
- description: "Obtain a VC from MBI: build the signed payload, pay x402, and return the issued credential. If a still-valid credential for this templateId is already cached locally, it is returned directly with no payment (fromCache: true) \u2014 pass forceReissue:true to pay and issue fresh regardless. Payment is asset-agnostic \u2014 MBI's 402 challenge may quote the native ZETRIX token or a ZTP20 token (e.g. JMYR); pass dryRun:true first to see the quoted asset/amount for free before committing to pay. What a call actually cost is reported precisely: paidAsset/amountPaid are set ONLY when this call paid, a cache hit reports the earlier charge under originalPayment instead (never as amountPaid, so summing spend cannot double-count), and any failure after the payment has settled on chain reports paymentAttempted: { asset, amount, paymentId }. Two such failures exist and mean different things: MBI 4006 is a definitive facilitator rejection, while 4012 (HTTP 502) means the outcome is INDETERMINATE \u2014 the payment may well have landed. On 4012 the wallet automatically polls MBI's recovery endpoint and reports recovery: { status, txHash?, vcId?, polls }, where status is ISSUED (the credential exists after all \u2014 fetch it by vcId, since recovery returns no VC body), FAILED, or REQUIRED/SETTLED (still unresolved) / UNKNOWN (recovery itself unreachable). NEVER retry a payment after either failure: the funds may already be gone, and a retry charges the full amount again \u2014 look the paymentId up instead. Every response except a cache hit also includes { schema: { required, optional } } \u2014 the template's full declared attribute schema read from chain \u2014 so you see the complete field list, not just what went wrong; a cache hit skips the chain lookup and omits it.",
23559
+ description: `Obtain a VC from MBI: build the signed payload, pay x402, and return the issued credential. If a still-valid credential for this templateId is already cached locally, it is returned directly with no payment (fromCache: true) \u2014 pass forceReissue:true to pay and issue fresh regardless. Payment is asset-agnostic \u2014 MBI's 402 challenge may quote the native ZETRIX token or a ZTP20 token (e.g. JMYR); pass dryRun:true first to see the quoted asset/amount for free before committing to pay. What a call actually cost is reported precisely: paidAsset/amountPaid are set ONLY when this call paid, a cache hit reports the earlier charge under originalPayment instead (never as amountPaid, so summing spend cannot double-count), and any failure after the payment has settled on chain reports paymentAttempted: { asset, amount, paymentId }. Two such failures exist and mean different things: MBI 4006 is a definitive facilitator rejection, while 4012 (HTTP 502) means the outcome is INDETERMINATE \u2014 the payment may well have landed. On 4012 the wallet automatically polls MBI's recovery endpoint and reports recovery: { status, txHash?, vcId?, polls }, where status is ISSUED (the credential exists after all \u2014 fetch it by vcId, since recovery returns no VC body), FAILED, or REQUIRED/SETTLED (still unresolved) / UNKNOWN (recovery itself unreachable). NEVER retry a payment after either failure: the funds may already be gone, and a retry charges the full amount again \u2014 look the paymentId up instead. Every response except a cache hit also includes { schema: { required, optional } } \u2014 the template's full declared attribute schema read from chain \u2014 so you see the complete field list, not just what went wrong; a cache hit skips the chain lookup and omits it. For the AI Birthcert specifically: this issues the BASIC one \u2014 self-declared by the agent, agent-paid via x402, owner identity NOT identity-verified. If the user asked for a "verified" AI birthcert (owner identity confirmed via MyDigital ID), use request_ai_birthcert_verification instead \u2014 this tool cannot produce that credential.`,
23029
23560
  inputSchema: {
23030
23561
  type: "object",
23031
23562
  properties: {
23032
23563
  templateId: {
23033
23564
  type: "string",
23034
- description: `The MBI credential-definition id to issue, e.g. "did:zid:...". Take this from the x401 challenge's credential_requirements.query.credentials[].id \u2014 NOT from requirementsId (that's just a label for the requirement set, e.g. "agent-identity"). A known template's natural-language name (e.g. "AI Birthcert") is also accepted and resolved to the right did:zid:... for the configured network.`
23565
+ description: `The MBI credential-definition id to issue, e.g. "did:zid:...". Take this from the x401 challenge's credential_requirements.query.credentials[].id \u2014 NOT from requirementsId (that's just a label for the requirement set, e.g. "agent-identity"). A known template's natural-language name (e.g. "AI Birthcert") is also accepted and resolved to the right did:zid:... for the configured network. This resolves to the BASIC (self-declared, non-verified) template \u2014 for the Verified AI Birthcert, use request_ai_birthcert_verification, not this tool.`
23035
23566
  },
23036
23567
  attributes: {
23037
23568
  type: "object",
@@ -23050,21 +23581,42 @@ function buildToolList() {
23050
23581
  required: ["templateId", "attributes"]
23051
23582
  }
23052
23583
  },
23584
+ {
23585
+ name: "request_ai_birthcert_verification",
23586
+ description: `Start a Verified AI Birthcert issuance session with myid (MyDigital ID owner verification). Returns { sessionId, verificationUrl, expiresAt } \u2014 show verificationUrl to the human owner and ask them to open it and complete MyDigital ID verification (typically finishes in seconds). Once they confirm they are done, call check_ai_birthcert_verification to see whether the credential was issued. IMPORTANT: agentName must be unique \u2014 if this exact name has already been used to request a Verified AI Birthcert, issuance will fail. Before calling, ask the human owner whether they want to supply any of the optional fields \u2014 agentPurpose, evidenceAssuranceLevel, ownerType, ownerVerified \u2014 do not silently omit them; they only need to say no. Calling this again with the SAME agentName while a prior session is still pending returns that same session unchanged \u2014 no new session is started and nothing is paid again. This tool spends real funds: it self-pays an x402 challenge, subject to the wallet's configured MAX_PAYMENT_AMOUNT cap, the same as pay_and_fetch/subscribe_and_issue. It can return { error: "..." } instead of a session if that payment fails (insufficient funds, or the payment cap blocked it) \u2014 nothing is created in that case. If the user did NOT ask for a "verified" credential specifically, they most likely want the self-declared, non-verified Basic AI Birthcert instead \u2014 use subscribe_and_issue for that.`,
23587
+ inputSchema: {
23588
+ type: "object",
23589
+ properties: {
23590
+ agentName: {
23591
+ type: "string",
23592
+ description: "A unique, human-readable name for this agent. Must not already be in use for a Verified AI Birthcert, or issuance will fail."
23593
+ },
23594
+ agentPurpose: { type: "string", description: 'Optional \u2014 what this agent does, e.g. "Negotiate and settle supplier invoices".' },
23595
+ evidenceAssuranceLevel: { type: "string", description: 'Optional \u2014 assurance level of the identity evidence, e.g. "high".' },
23596
+ ownerType: { type: "string", description: `Optional \u2014 the owner's type, e.g. "Individual".` },
23597
+ ownerVerified: { type: "string", description: 'Optional \u2014 whether the owner is already verified, as the string "true" or "false".' }
23598
+ },
23599
+ required: ["agentName"]
23600
+ }
23601
+ },
23602
+ {
23603
+ name: "check_ai_birthcert_verification",
23604
+ description: 'Check the status of the most recently requested Verified AI Birthcert session (see request_ai_birthcert_verification). Returns { status: "pending" } while the owner has not yet completed MyDigital ID verification, or { status: "issued", vcId } once myid has minted the credential \u2014 myid returns vcId ONLY when status is "issued", never otherwise. On { status: "issued" }, the wallet also fetches the credential from MBI, verifies it, and caches it locally, returning it as `vc` \u2014 it is then also visible via wallet_status and usable by prove_identity without any further call. If `cacheError` is present instead of `vc`, the credential WAS issued successfully but could not be fetched/verified/cached yet (e.g. a transient MBI error) \u2014 this is NOT the same as issuance failing, so do not retry request_ai_birthcert_verification; call check_ai_birthcert_verification again instead. Returns { status: "no_session" } if request_ai_birthcert_verification has never been called.',
23605
+ inputSchema: { type: "object", properties: {} }
23606
+ },
23053
23607
  {
23054
23608
  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.",
23609
+ 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
23610
  inputSchema: {
23057
23611
  type: "object",
23058
23612
  properties: {
23059
- password: { type: "string", description: "HSM password to protect the new account. Must come from the user." },
23060
23613
  label: { type: "string" },
23061
23614
  purpose: { type: "string" },
23062
23615
  confirmNew: {
23063
23616
  type: "boolean",
23064
23617
  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
23618
  }
23066
- },
23067
- required: ["password"]
23619
+ }
23068
23620
  }
23069
23621
  }
23070
23622
  ];
@@ -23074,17 +23626,30 @@ function asPayRequest(accept) {
23074
23626
  return { ...accept, extra: { gasModel: "client", ...extra } };
23075
23627
  }
23076
23628
  async function main() {
23077
- const accountStore = createFsAccountStore((0, import_node_path3.join)((0, import_node_os.homedir)(), ".agentic-wallet-mcp", "account.json"));
23629
+ const fileEnv = loadConfigFileEnv(process.argv, (p) => (0, import_node_fs.readFileSync)(p, "utf8"));
23630
+ const stateDir = (process.env.ZETRIX_WALLET_STATE_DIR ?? fileEnv.ZETRIX_WALLET_STATE_DIR ?? (0, import_node_path6.join)((0, import_node_os2.homedir)(), ".agentic-wallet-mcp")).replace(/\/+$/, "");
23631
+ const accountStore = createFsAccountStore((0, import_node_path6.join)(stateDir, "account.json"));
23632
+ if (process.argv[2] === "export-credentials") {
23633
+ const { exitCode } = await exportCredentials({
23634
+ getAccount: () => accountStore.get(),
23635
+ isTty: Boolean(process.stdout.isTTY),
23636
+ write: (s) => process.stdout.write(s),
23637
+ writeErr: (s) => process.stderr.write(s)
23638
+ });
23639
+ process.exit(exitCode);
23640
+ }
23078
23641
  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;
23642
+ const { env, passwordGenerated } = resolveStartupEnv({
23643
+ processEnv: process.env,
23644
+ storedAccount,
23645
+ fileEnv,
23646
+ generatePassword: generateHsmPassword
23647
+ });
23083
23648
  const config2 = loadConfig(env);
23084
23649
  const hsmPassword = config2.hsmPassword;
23085
23650
  const be = new WalletBeClient(config2.walletBeUrl);
23086
23651
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
23087
- const { zetrixAddress, holderDid, created, didMismatch, activated } = await resolveHolder(
23652
+ const { zetrixAddress, holderDid, publicKeyHex, created, didMismatch, activated } = await resolveHolder(
23088
23653
  {
23089
23654
  createAccount: (password) => be.createAccount(password),
23090
23655
  signMessage: (message, address, password) => be.signMessage(message, address, password),
@@ -23096,8 +23661,9 @@ async function main() {
23096
23661
  if (created) {
23097
23662
  await accountStore.set({ zetrixAddress, holderDid, hsmPassword, createdAt: (/* @__PURE__ */ new Date()).toISOString() });
23098
23663
  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
- `
23664
+ `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
23665
+ ` : `
23666
+ `)
23101
23667
  );
23102
23668
  } else if (storedAccount && config2.zetrixAddress === storedAccount.zetrixAddress) {
23103
23669
  process.stderr.write(
@@ -23124,13 +23690,7 @@ async function main() {
23124
23690
  const sdk = new import_zetrix_sdk_nodejs.default({ host: config2.nodeHost, port: config2.nodePort });
23125
23691
  const contractQuery = (a) => sdk.contract.call(a);
23126
23692
  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
- };
23693
+ const fetchNativeBalance = async (address) => parseNativeBalance(await sdk.account.getInfo(address));
23134
23694
  const tokenBalanceDeps = {
23135
23695
  address: zetrixAddress,
23136
23696
  fetchNativeBalance,
@@ -23141,15 +23701,42 @@ async function main() {
23141
23701
  const nodeBaseUrl = `https://${config2.nodeHost}${config2.nodePort ? `:${config2.nodePort}` : ""}`;
23142
23702
  const nodeMetaQuery = (url) => fetch(url, { headers: { Accept: "application/json" } }).then((r) => r.json());
23143
23703
  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));
23704
+ const cacheScope = (0, import_node_crypto5.createHash)("sha256").update(`${config2.network}:${zetrixAddress}`).digest("hex");
23705
+ const vcCache = createFsVcCache((0, import_node_path6.join)(config2.stateDir, "vc-cache", cacheScope));
23706
+ const mbi = new MbiClient(config2.mbiBaseUrl);
23707
+ const messageSigner = (message) => be.signMessage(message, zetrixAddress, hsmPassword);
23146
23708
  const pay = (accept) => {
23147
23709
  assertWithinPaymentCap(accept, config2.maxPaymentAmount);
23148
23710
  return payWithReadinessCheck(
23149
23711
  String(accept.asset ?? ""),
23150
- () => import_x402_zetrix_client2.PaymentEngine.pay(asPayRequest(accept), walletCfg, node, {}, walletBeSignerFn)
23712
+ () => import_x402_zetrix_client2.PaymentEngine.pay(asPayRequest(accept), walletCfg, node, {}, walletBeSignerFn),
23713
+ activated
23151
23714
  );
23152
23715
  };
23716
+ const verifyAiBirthcert = config2.ssivcBaseUrl ? (() => {
23717
+ const ssivcSessionStore = createFsSsivcSessionStore((0, import_node_path6.join)(config2.stateDir, "ssivc-session.json"));
23718
+ const downloadQuarantine = createFsDownloadQuarantineStore((0, import_node_path6.join)(config2.stateDir, "ssivc-download-quarantine"));
23719
+ const ssivc = new SsivcClient(config2.ssivcBaseUrl);
23720
+ const verifyAiBirthcertDeps = {
23721
+ ssivc,
23722
+ signHexBlob: walletBeSignerFn,
23723
+ messageSigner,
23724
+ mbi,
23725
+ pay,
23726
+ publicKeyHex,
23727
+ address: zetrixAddress,
23728
+ holderDid,
23729
+ now: () => /* @__PURE__ */ new Date(),
23730
+ sessionStore: ssivcSessionStore,
23731
+ verifiedTemplateId: config2.aiBirthcertVerifiedTemplateId,
23732
+ cache: vcCache,
23733
+ quarantine: downloadQuarantine
23734
+ };
23735
+ return {
23736
+ request: (input) => requestAiBirthcertVerification(verifyAiBirthcertDeps, input),
23737
+ check: () => checkAiBirthcertVerification(verifyAiBirthcertDeps)
23738
+ };
23739
+ })() : void 0;
23153
23740
  const payer = async (req) => {
23154
23741
  const init = { method: req.method ?? "GET", headers: req.headers, body: req.body };
23155
23742
  const res = await fetch(req.url, init);
@@ -23180,8 +23767,6 @@ async function main() {
23180
23767
  };
23181
23768
  };
23182
23769
  const subscribeSign = (blob) => be.signBlob(blob, zetrixAddress, hsmPassword);
23183
- const mbi = new MbiClient(config2.mbiBaseUrl);
23184
- const messageSigner = (message) => be.signMessage(message, zetrixAddress, hsmPassword);
23185
23770
  const zidResolver = new ZidResolverClient(config2.zidResolverBaseUrl);
23186
23771
  const resolveIssuerKeys = (vc) => resolveIssuerProofKeys(vc, zidResolver);
23187
23772
  const submitAuth = async () => {
@@ -23199,11 +23784,14 @@ async function main() {
23199
23784
  subscribeDeps: { mbi, sign: subscribeSign, pay, resolveSymbol, holderDid, resolveTemplateFields, cache: vcCache },
23200
23785
  queryContract: (input) => queryContract(input, contractQuery),
23201
23786
  queryTokenBalance: queryTokenBalance2,
23202
- createAccount: (password, label, purpose) => be.createAccount(password, label, purpose),
23203
- saveAccount: (account) => accountStore.set({ ...account, createdAt: (/* @__PURE__ */ new Date()).toISOString() }),
23787
+ // The session password is bound here, in the wiring, so it never crosses into the tool
23788
+ // layer — create_holder_account has no password parameter for a model to be asked for.
23789
+ createAccount: (label, purpose) => be.createAccount(hsmPassword, label, purpose),
23790
+ saveAccount: (account) => accountStore.set({ ...account, hsmPassword, createdAt: (/* @__PURE__ */ new Date()).toISOString() }),
23204
23791
  checkActivationStatus: (address) => be.checkActivationStatus(address),
23205
23792
  sleep,
23206
- cache: vcCache
23793
+ cache: vcCache,
23794
+ verifyAiBirthcert
23207
23795
  };
23208
23796
  const tools = createTools(deps);
23209
23797
  const server = new Server({ name: "agentic-wallet-mcp", version: packageVersion }, { capabilities: { tools: {} } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-wallet-mcp",
3
- "version": "0.6.1",
3
+ "version": "0.8.0",
4
4
  "description": "Agent-facing MCP wallet for Zetrix — orchestrates x401 identity proof, x402 payment, and MBI VC issuance",
5
5
  "keywords": [
6
6
  "mcp",