agentic-wallet-mcp 0.9.1 → 0.10.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,65 @@ 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.10.0] — 14 September 2026
12
+
13
+ ### Added
14
+
15
+ - **`credential_preflight` — a free readiness check to call before asking the user for anything.**
16
+ Reports the live fee and which side pays gas, the balances that matter, whether the spending
17
+ limit permits it, and (for a template credential) the attributes the template requires. Every
18
+ reason it is not ready is listed *together*, so one round of fixes is enough rather than
19
+ discovering a low balance and a too-low spending cap one failed payment at a time. It also
20
+ reports what it could **not** check, so a clean result is not mistaken for a guarantee — notably,
21
+ it cannot tell whether an agent name is still free, because that is decided at issuance.
22
+ - **Quote-only mode for the Verified AI Birthcert**, so its price and gas model can be seen without
23
+ starting a session or paying.
24
+
25
+ ### Changed
26
+
27
+ - **A dry run can no longer issue a credential.** `subscribe_and_issue({ dryRun: true })` now prices
28
+ through an endpoint that cannot issue, and stops before the call that can. Previously, against a
29
+ credential the issuer grants for free, asking what something cost *created it* — a real,
30
+ permanently-registered credential, which also displaced whatever the wallet already held for that
31
+ template. A dry run now signs nothing, issues nothing, and never touches the local credential
32
+ store.
33
+ - **A credential that is currently free is no longer reported as unaffordable.** The issuer states
34
+ separately whether a quoted amount will actually be charged; both `credential_preflight` and the
35
+ dry run now read it. When issuance is free, the amount is reported as indicative rather than as a
36
+ charge, and neither the balance nor the spending cap blocks it. Network gas is unaffected and can
37
+ still block, because gas is not the credential fee.
38
+ - **Where that answer is unknown it is reported as unknown, never as free** — an older issuer
39
+ deployment does not state it, and treating silence as "free" would under-report a real cost.
40
+ - **The default spending cap now permits the credential fee on mainnet**, scoped to credential
41
+ issuance rather than widened generally.
42
+ - **Balance reads during a preflight run concurrently**, so the fee balance and the native-gas
43
+ balance arrive together instead of one after the other.
44
+
45
+ ### Fixed
46
+
47
+ - **The outbound `User-Agent` sent to payment facilitators no longer advertises a non-public
48
+ host.** It is transmitted to third parties on every prepare request; the underlying client
49
+ dependency has been updated to one that reports its public project URL.
50
+
51
+ ## [0.9.2] — 3 September 2026
52
+
53
+ ### Fixed
54
+
55
+ - **`request_ai_birthcert_verification`'s sponsored-payment path no longer surfaces a facilitator
56
+ insufficient-funds rejection as an opaque, unhandled MCP tool error.** A `461407`
57
+ (`X402_INSUFFICIENT_FUNDS`) rejection from the facilitator's `/prepare` endpoint is now reshaped
58
+ into a clean `{ error }` result naming the asset and amount, instead of falling through every
59
+ error branch and throwing raw.
60
+ - **That message now renders amounts in human units, not raw base units.** A raw base-unit count
61
+ next to a token symbol (e.g. "requires 1,000,000 of JMYR") reads as a million *whole* tokens —
62
+ for a 6-decimal asset the actual requirement was 1 JMYR, a 1,000,000x misreading that could lead
63
+ to a drastically oversized top-up. Amounts are now resolved through the same symbol/decimals
64
+ formatter the rest of the wallet already uses.
65
+ - **The reported "current balance" is read from the facilitator's structured response field when
66
+ available**, falling back to parsing it out of the free-text error message only for an
67
+ older/unfixed facilitator — the free-text format was never a stable contract between the two
68
+ services.
69
+
11
70
  ## [0.9.1] — 28 August 2026
12
71
 
13
72
  ### Fixed
package/README.md CHANGED
@@ -20,15 +20,16 @@ VC issuance → identity proof → pay-per-use).
20
20
 
21
21
  | Tool | Does | Input | Output (shape) |
22
22
  |---|---|---|---|
23
- | `wallet_status` | Report holder DID/address/network + held VCs (client-supplied, or the local cache); optionally a token balance | `{ heldCredentials?, token? }` | `{ holderDid, zetrixAddress, network, credentials, tokenBalance? }` |
23
+ | `wallet_status` | Report holder DID/address/network + held VCs (client-supplied, or the local cache); optionally one token balance (`token`) or several in one call (`tokens`). Each balance carries `balance` (raw base units), `decimals` and `display` — the same amount in whole tokens with its symbol. Quote `display`; a raw count beside a ticker is wrong by orders of magnitude. A token may be named by ticker **or** contract address | `{ heldCredentials?, token?, tokens? }` | `{ holderDid, zetrixAddress, network, credentials, balances?, tokenBalance?, tokenBalances? }` |
24
+ | `credential_preflight` | **Free.** The first call for any credential — composes the quote, the balances and the spending cap into one answer, before a single application field is collected. Spends nothing, starts nothing. `blockers` lists *every* reason the wallet is not ready, at once; `notChecked` lists what preflight cannot know — agent-name availability is decided by myid at issuance, after payment, so a `ready` result is never a reservation | `{ credential, templateId? }` | `{ credential, ready, fee?, balances, cap?, schema?, blockers, notChecked }` |
24
25
  | `prove_identity` | Answer an x401 `PROOF-REQUEST` → return the `PROOF-RESPONSE` header to replay | `{ proofRequest, vc?, revealAttribute?, issuerKeys? }` | `{ proofResponseHeader, verified, presentationId }` |
25
26
  | `pay_and_fetch` | Fetch a URL, auto-pay with x402 (self-pay via Wallet BE) on `402` | `{ url, method?, headers?, body? }` | `{ status, body, paymentMade, amountPaid, amountPaidHuman, asset }` |
26
27
  | `subscribe_and_issue` | Reuse a cached VC if still valid, else pay x402 → MBI issues → return the VC | `{ templateId, attributes, expirationDate?, dryRun?, forceReissue? }` | `{ issued, vcId, vc, txHash, fromCache?, schema?, originalPayment?, paymentAttempted?, recovery? }` |
27
28
  | `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
29
  | `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
30
  | `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. Optional `gasPayer` (`"sponsored"` \| `"self"`) overrides the gas payer for this call only — precedence is per-call `gasPayer` > `GAS_PREFERENCE` config > `sponsored` hardcoded default. If a sponsored quote is refused before any money moved, the wallet automatically falls back to paying gas itself; it never falls back once payment is merely pending/indeterminate | `{ agentName, agentPurpose?, evidenceAssuranceLevel?, ownerType?, ownerVerified?, gasPayer? }` | `{ 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? }` |
31
+ | `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. Optional `gasPayer` (`"sponsored"` \| `"self"`) overrides the gas payer for this call only — precedence is per-call `gasPayer` > `GAS_PREFERENCE` config > `sponsored` hardcoded default. If a sponsored quote is refused before any money moved, the wallet automatically falls back to paying gas itself; it never falls back once payment is merely pending/indeterminate. `dryRun: true` quotes instead of paying: it stops after the 402 challenge, pays nothing, creates no session, and returns `{ quote }` — the asset, the amount and which side pays gas. Quote and paid request send byte-identical signed bodies | `{ agentName, agentPurpose?, evidenceAssuranceLevel?, ownerType?, ownerVerified?, gasPayer?, dryRun? }` | `{ sessionId, verificationUrl, expiresAt }` or `{ quote: { asset, maxAmountRequired, payTo?, gasModel } }` |
32
+ | `check_ai_birthcert_verification` | **Free.** Poll the most recently requested Verified AI Birthcert session; on `status: "issued"`, also fetches, verifies, and caches the credential. While the session is still open it replays the stored `verificationUrl` alongside SSIVC's live `expiresAt` — SSIVC issues the link only once, at creation, so this store is the only place it survives. That makes *"where is my link?"* answerable without touching the paid tool. No link is returned once `status` is `"issued"`: the link is spent | (none) | `{ status: "pending" \| "issued" \| "no_session", verificationUrl?, expiresAt?, vcId?, vc?, cacheError? }` |
32
33
 
33
34
  > **VCs are cached locally**, keyed by `templateId`, under `~/.agentic-wallet-mcp/vc-cache/`
34
35
  > (scoped per network + holder — different identities or networks never share a cache).
@@ -176,7 +177,7 @@ Node ≥ 18 required (built-in `fetch`).
176
177
  | `OID4VP_BASE_URL` | no | OID4VP verifier base URL override — auto-derived from `ZETRIX_NETWORK` by the x401 SDK when not set |
177
178
  | `ZETRIX_NODE_HOST` / `ZETRIX_NODE_PORT` | no | RPC node override (auto-derived from network) |
178
179
  | `ZID_RESOLVER_BASE_URL` | no | ZID resolver override (auto-derived from network: sandbox for testnet, prod for mainnet) |
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
+ | `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. **Either the ticker or the contract address works** — the challenge identifies a ZTP20 token by its contract address, and the wallet resolves a known ticker (e.g. `JMYR`) to it. If both are written for the same asset, the contract address wins. An unrecognised ticker still matches nothing and falls through to `"*"`. e.g. `{"ZTX":"1000000000","ZTX3WeinXtt28YMyr4vUZ14ddTgEMGeuc1e6b":"5000000","*":"0"}`. **Default when unset:** both networks allow exactly the AI Birthcert fee (1 JMYR), keyed to that network's JMYR contract, and refuse everything else. **The cap is per call, not cumulative** — so an unconfigured wallet, mainnet included, can pay that fee once per call with no overall ceiling. Set this explicitly to lock a wallet down. |
180
181
  | `ZETRIX_WALLET_STATE_DIR` | no | Where the wallet keeps `account.json` and its VC cache. Defaults to `~/.agentic-wallet-mcp` |
181
182
  | `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
183
  | `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 |
@@ -12147,7 +12147,7 @@ var require_prepare_client = __commonJS({
12147
12147
  method: "POST",
12148
12148
  headers: {
12149
12149
  "Content-Type": "application/json",
12150
- "User-Agent": "x402-zetrix-js/1.0 (+https://git.myeg.com.my/genesis/x402/x402-zetrix-js)"
12150
+ "User-Agent": "x402-zetrix-js/1.0 (+https://github.com/Zetrix-Chain/x402-zetrix-js)"
12151
12151
  },
12152
12152
  body: JSON.stringify(params),
12153
12153
  signal: controller.signal
@@ -12538,6 +12538,7 @@ var require_dist2 = __commonJS({
12538
12538
  var index_exports = {};
12539
12539
  __export(index_exports, {
12540
12540
  asPayRequest: () => asPayRequest,
12541
+ buildPayers: () => buildPayers,
12541
12542
  buildToolList: () => buildToolList
12542
12543
  });
12543
12544
  module.exports = __toCommonJS(index_exports);
@@ -12549,7 +12550,7 @@ var import_node_path6 = require("node:path");
12549
12550
  // package.json
12550
12551
  var package_default = {
12551
12552
  name: "agentic-wallet-mcp",
12552
- version: "0.9.1",
12553
+ version: "0.10.0",
12553
12554
  description: "Agent-facing MCP wallet for Zetrix \u2014 orchestrates x401 identity proof, x402 payment, and MBI VC issuance",
12554
12555
  keywords: [
12555
12556
  "mcp",
@@ -12592,7 +12593,7 @@ var package_default = {
12592
12593
  dependencies: {
12593
12594
  "@modelcontextprotocol/sdk": "^1.0.0",
12594
12595
  "x401-zetrix-client": "^0.2.1",
12595
- "x402-zetrix-client": "^0.2.2",
12596
+ "x402-zetrix-client": "^0.2.4",
12596
12597
  "zetrix-sdk-nodejs": "^1.0.3"
12597
12598
  },
12598
12599
  devDependencies: {
@@ -21511,10 +21512,15 @@ var PaymentCapError = class extends Error {
21511
21512
  this.detail = detail;
21512
21513
  }
21513
21514
  };
21515
+ function formatCapRefusal(detail, renderRequired = detail.requiredRaw, renderCap = detail.capRaw) {
21516
+ const asset = detail.asset || "(unknown asset)";
21517
+ const base = `payment blocked: this call needs ${renderRequired} of asset "${asset}", but the limit that applies is ${renderCap}`;
21518
+ return detail.matchedKey === detail.asset ? `${base}, set for that asset.` : `${base}. No limit is set for this asset, so the "*" fallback applies \u2014 set a limit keyed by "${asset}" to allow it.`;
21519
+ }
21514
21520
  function isNonNegativeIntegerString(v) {
21515
21521
  return /^\d+$/.test(v);
21516
21522
  }
21517
- function parsePaymentCaps(json) {
21523
+ function parsePaymentCaps(json, opts = {}) {
21518
21524
  if (json === void 0) return void 0;
21519
21525
  let parsed;
21520
21526
  try {
@@ -21532,12 +21538,43 @@ function parsePaymentCaps(json) {
21532
21538
  }
21533
21539
  caps[asset] = raw;
21534
21540
  }
21535
- return caps;
21541
+ return opts.resolveSymbol ? expandSymbolKeys(caps, opts.resolveSymbol, opts.onWarn) : caps;
21542
+ }
21543
+ function expandSymbolKeys(caps, resolveSymbol, onWarn) {
21544
+ const out = {};
21545
+ const pendingSymbols = [];
21546
+ for (const [key, value] of Object.entries(caps)) {
21547
+ const address = key === "*" || key === "ZTX" ? void 0 : resolveSymbol(key);
21548
+ if (address === void 0) out[key] = value;
21549
+ else pendingSymbols.push([key, address, value]);
21550
+ }
21551
+ for (const [symbol, address, value] of pendingSymbols) {
21552
+ if (out[address] !== void 0) continue;
21553
+ out[address] = value;
21554
+ const previous = caps["*"] ?? "0";
21555
+ if (BigInt(value) > BigInt(previous)) {
21556
+ onWarn?.(
21557
+ `MAX_PAYMENT_AMOUNT: the limit written as "${symbol}" now applies to ${address} and permits ${value}. Until this version a ticker matched nothing, so that asset was limited to ${previous}. If you did not intend to allow ${value}, change or remove the "${symbol}" entry.`
21558
+ );
21559
+ }
21560
+ }
21561
+ return out;
21562
+ }
21563
+ function describePaymentCap(asset, requiredRaw, caps) {
21564
+ if (caps === void 0) return { asset, capRaw: null, matchedKey: null, wouldPass: true };
21565
+ const { capRaw, matchedKey } = resolveCap(asset, caps);
21566
+ if (capRaw === void 0) return { asset, capRaw: null, matchedKey: null, wouldPass: false };
21567
+ const wouldPass = isNonNegativeIntegerString(requiredRaw) && BigInt(requiredRaw) <= BigInt(capRaw);
21568
+ return { asset, capRaw, matchedKey, wouldPass };
21569
+ }
21570
+ function resolveCap(asset, caps) {
21571
+ const explicit = caps[asset];
21572
+ return explicit !== void 0 ? { capRaw: explicit, matchedKey: asset } : { capRaw: caps["*"], matchedKey: "*" };
21536
21573
  }
21537
21574
  function assertWithinPaymentCap(accept, caps) {
21538
21575
  if (caps === void 0) return;
21539
21576
  const asset = accept.asset ?? "";
21540
- const capRaw = caps[asset] ?? caps["*"];
21577
+ const { capRaw, matchedKey } = resolveCap(asset, caps);
21541
21578
  if (capRaw === void 0) {
21542
21579
  throw new PaymentCapError(`payment blocked: no MAX_PAYMENT_AMOUNT entry for asset "${asset}" and no "*" fallback configured`);
21543
21580
  }
@@ -21548,10 +21585,13 @@ function assertWithinPaymentCap(accept, caps) {
21548
21585
  const required2 = BigInt(requiredRaw);
21549
21586
  const cap = BigInt(capRaw);
21550
21587
  if (required2 > cap) {
21551
- throw new PaymentCapError(
21552
- `payment blocked: requested ${required2} ${asset || "(unknown asset)"} exceeds configured MAX_PAYMENT_AMOUNT ${cap}`,
21553
- { asset, requiredRaw: required2.toString(), capRaw: cap.toString() }
21554
- );
21588
+ const detail = {
21589
+ asset,
21590
+ requiredRaw: required2.toString(),
21591
+ capRaw: cap.toString(),
21592
+ matchedKey
21593
+ };
21594
+ throw new PaymentCapError(formatCapRefusal(detail), detail);
21555
21595
  }
21556
21596
  }
21557
21597
 
@@ -21583,10 +21623,22 @@ function deriveAiBirthcertVerifiedTemplateId(network) {
21583
21623
  var TOKEN_REGISTRY = {
21584
21624
  JMYR: { testnet: "ZTX3WeinXtt28YMyr4vUZ14ddTgEMGeuc1e6b", mainnet: "ZTX3NCkXBqbyJWjZZxciQez945Lu6tGAcjNJr" }
21585
21625
  };
21626
+ function defaultPaymentCaps(network) {
21627
+ const jmyr = resolveTokenAddress("JMYR", network);
21628
+ return isTestnet(network) && jmyr ? { [jmyr]: "1000000", "*": "0" } : { "*": "0" };
21629
+ }
21630
+ function defaultCredentialIssuanceCaps(network) {
21631
+ const jmyr = resolveTokenAddress("JMYR", network);
21632
+ return jmyr ? { [jmyr]: "1000000", "*": "0" } : { "*": "0" };
21633
+ }
21634
+ var KNOWN_NETWORKS = ["zetrix:testnet", "zetrix:mainnet"];
21635
+ function isTestnet(network) {
21636
+ return network.includes("testnet");
21637
+ }
21586
21638
  function resolveTokenAddress(symbol, network) {
21587
21639
  const entry = TOKEN_REGISTRY[symbol.toUpperCase()];
21588
21640
  if (!entry) return void 0;
21589
- return network.includes("testnet") ? entry.testnet : entry.mainnet;
21641
+ return isTestnet(network) ? entry.testnet : entry.mainnet;
21590
21642
  }
21591
21643
  function loadConfig(env) {
21592
21644
  const req = (key, hint) => {
@@ -21599,7 +21651,17 @@ function loadConfig(env) {
21599
21651
  return v && v.trim() ? v.trim() : void 0;
21600
21652
  };
21601
21653
  const network = opt("ZETRIX_NETWORK") ?? "zetrix:testnet";
21654
+ if (!KNOWN_NETWORKS.includes(network)) {
21655
+ throw new Error(
21656
+ `agentic-wallet-mcp: unrecognised ZETRIX_NETWORK "${network}" \u2014 expected one of ${KNOWN_NETWORKS.join(", ")}. Values are matched exactly and are case-sensitive; anything unrecognised would otherwise resolve to MAINNET addresses and grant a real spending allowance.`
21657
+ );
21658
+ }
21602
21659
  const oid4vpBaseUrlOverride = opt("OID4VP_BASE_URL");
21660
+ const explicitCaps = parsePaymentCaps(opt("MAX_PAYMENT_AMOUNT"), {
21661
+ resolveSymbol: (symbol) => resolveTokenAddress(symbol, network),
21662
+ onWarn: (message) => process.stderr.write(`agentic-wallet-mcp: ${message}
21663
+ `)
21664
+ });
21603
21665
  return {
21604
21666
  walletBeUrl: stripTrailingSlash(opt("WALLET_BE_URL") ?? deriveWalletBeUrl(network)),
21605
21667
  oid4vpBaseUrl: oid4vpBaseUrlOverride ? stripTrailingSlash(oid4vpBaseUrlOverride) : void 0,
@@ -21619,7 +21681,11 @@ function loadConfig(env) {
21619
21681
  // Fail closed: an unset cap means "spend nothing", not "spend anything". A wallet that starts
21620
21682
  // with no configuration at all must not be able to auto-pay a hostile x402 challenge. Raising
21621
21683
  // it is a deliberate act.
21622
- maxPaymentAmount: parsePaymentCaps(opt("MAX_PAYMENT_AMOUNT")) ?? { "*": "0" },
21684
+ //
21685
+ // An explicit MAX_PAYMENT_AMOUNT governs BOTH caps below — a user who sets a limit means it
21686
+ // everywhere. Only the defaults differ, and only because the two surfaces carry different risk.
21687
+ maxPaymentAmount: explicitCaps ?? defaultPaymentCaps(network),
21688
+ credentialIssuanceCaps: explicitCaps ?? defaultCredentialIssuanceCaps(network),
21623
21689
  ssivcBaseUrl: (() => {
21624
21690
  const v = opt("SSIVC_BASE_URL") ?? deriveSsivcBaseUrl(network);
21625
21691
  return v ? stripTrailingSlash(v) : void 0;
@@ -21738,27 +21804,42 @@ async function fetchZTP20BalanceStrict(contractAddress, address, query) {
21738
21804
  if (typeof balance !== "string") {
21739
21805
  throw new Error("balanceOf returned no balance field");
21740
21806
  }
21807
+ if (!/^\d+$/.test(balance)) {
21808
+ throw new Error(`balanceOf returned a non-numeric balance: ${JSON.stringify(balance)}`);
21809
+ }
21741
21810
  return balance;
21742
21811
  }
21812
+ function looksLikeContractAddress(token) {
21813
+ return /^ZTX[0-9A-Za-z]{30,}$/.test(token);
21814
+ }
21743
21815
  async function queryTokenBalance(deps, token) {
21744
21816
  const symbol = token.toUpperCase();
21745
21817
  if (symbol === "ZTX") {
21746
21818
  try {
21747
- return { token: symbol, balance: await deps.fetchNativeBalance(deps.address), decimals: ZTX_DECIMALS2 };
21819
+ const balance2 = await deps.fetchNativeBalance(deps.address);
21820
+ return { token: symbol, balance: balance2, decimals: ZTX_DECIMALS2, display: renderDisplay(balance2, ZTX_DECIMALS2, symbol) };
21748
21821
  } catch {
21749
21822
  return { token: symbol, error: "query_failed" };
21750
21823
  }
21751
21824
  }
21752
- const contractAddress = deps.resolveTokenAddress(symbol);
21825
+ const registered = deps.resolveTokenAddress(symbol);
21826
+ const byAddress = registered === null && looksLikeContractAddress(token);
21827
+ const contractAddress = registered ?? (byAddress ? token : null);
21753
21828
  if (!contractAddress) return { token: symbol, error: "unknown_token" };
21829
+ const asked = byAddress ? token : symbol;
21754
21830
  let balance;
21755
21831
  try {
21756
21832
  balance = await fetchZTP20BalanceStrict(contractAddress, deps.address, deps.query);
21757
21833
  } catch {
21758
- return { token: symbol, error: "query_failed" };
21834
+ return { token: asked, error: "query_failed" };
21759
21835
  }
21760
21836
  const info = await fetchTokenInfo(contractAddress, deps.query);
21761
- return { token: symbol, balance, decimals: info?.decimals ?? null };
21837
+ const label = byAddress ? info?.symbol ?? asked : symbol;
21838
+ const decimals = info?.decimals ?? null;
21839
+ return { token: label, balance, decimals, display: renderDisplay(balance, decimals, label) };
21840
+ }
21841
+ function renderDisplay(raw, decimals, label) {
21842
+ return `${decimals === null ? raw : formatHumanAmount(raw, decimals)} ${label}`;
21762
21843
  }
21763
21844
 
21764
21845
  // src/clients/template-info-client.ts
@@ -22032,6 +22113,24 @@ var MbiClient = class _MbiClient {
22032
22113
  if (!res.ok) throw await this.error(res, "apply (phase 2) failed");
22033
22114
  return this.unwrap(res);
22034
22115
  }
22116
+ /**
22117
+ * POST /v1/vc/pay/quote — the price for a template, with no issuance and no payment record.
22118
+ *
22119
+ * Unlike `applyChallenge`, this cannot mint anything: MBI resolves the template, builds the
22120
+ * canonical sign payload, and reads its configured price. Notably a FREE template does not
22121
+ * short-circuit into synchronous issuance here, which is what makes this safe to call purely to
22122
+ * find out a cost — `applyChallenge` is not (see its docstring).
22123
+ *
22124
+ * `data` is required to be non-empty by MBI's DTO but does not affect the price, which comes from
22125
+ * MBI's own asset config keyed by the resolved template. A caller pricing a credential before it
22126
+ * has collected any attribute therefore passes a throwaway value, and must not present it as
22127
+ * anything the user supplied.
22128
+ */
22129
+ async quote(templateId, data) {
22130
+ const res = await this.fetch("POST", "/v1/vc/pay/quote", { templateId, data });
22131
+ if (!res.ok) throw await this.error(res, "quote failed");
22132
+ return this.unwrap(res);
22133
+ }
22035
22134
  /** Idempotent recovery — GET /v1/vc/pay/status/{paymentId}. */
22036
22135
  async getStatus(paymentId) {
22037
22136
  const res = await this.fetch("GET", `/v1/vc/pay/status/${encodeURIComponent(paymentId)}`);
@@ -22169,6 +22268,43 @@ function payAndFetch(payer, req) {
22169
22268
  return payer(req);
22170
22269
  }
22171
22270
 
22271
+ // src/accept-selection.ts
22272
+ function extraOf(accept) {
22273
+ const extra = accept.extra;
22274
+ if (!extra || typeof extra !== "object" || Array.isArray(extra)) return {};
22275
+ return extra;
22276
+ }
22277
+ function assetOf(accept) {
22278
+ return String(accept.asset ?? "");
22279
+ }
22280
+ function declaresFacilitator(accept) {
22281
+ return extraOf(accept).gasModel === "facilitator";
22282
+ }
22283
+ function isSponsored(accept) {
22284
+ if (!declaresFacilitator(accept)) return false;
22285
+ const prepareEndpoint = extraOf(accept).prepareEndpoint;
22286
+ if (typeof prepareEndpoint !== "string" || prepareEndpoint === "") return false;
22287
+ const asset = assetOf(accept);
22288
+ return asset !== "" && asset !== "ZTX";
22289
+ }
22290
+ function isUnusable(accept) {
22291
+ return declaresFacilitator(accept) && !isSponsored(accept);
22292
+ }
22293
+ function orderAccepts(accepts, prefer) {
22294
+ const usable = accepts.filter((a) => !isUnusable(a));
22295
+ const sponsored = usable.filter(isSponsored);
22296
+ const selfPay = usable.filter((a) => !isSponsored(a));
22297
+ return prefer === "sponsored" ? [...sponsored, ...selfPay] : [...selfPay, ...sponsored];
22298
+ }
22299
+ function prepareBaseUrl(prepareEndpoint) {
22300
+ return prepareEndpoint.replace(/\/+$/, "").replace(/\/prepare$/, "");
22301
+ }
22302
+ function needsNativeGasCheck(accept) {
22303
+ const asset = String(accept.asset ?? "");
22304
+ if (asset === "" || asset === "ZTX") return false;
22305
+ return !isSponsored(accept);
22306
+ }
22307
+
22172
22308
  // src/zetrix-hex.ts
22173
22309
  function zetrixHexStringToBytes(s) {
22174
22310
  const up = s.toUpperCase();
@@ -22359,6 +22495,45 @@ async function subscribeAndIssue(deps, opts) {
22359
22495
  };
22360
22496
  }
22361
22497
  }
22498
+ if (opts.dryRun) {
22499
+ if (!deps.mbi.quote) {
22500
+ return {
22501
+ issued: false,
22502
+ reason: "dry run unavailable \u2014 this wallet has no MBI quote client, and pricing any other way risks issuing the credential",
22503
+ ...schema ? { schema } : {}
22504
+ };
22505
+ }
22506
+ let quoted;
22507
+ try {
22508
+ quoted = await deps.mbi.quote(opts.templateId, attributes);
22509
+ } catch (err) {
22510
+ if (err instanceof MbiError) {
22511
+ return { issued: false, reason: err.message, httpStatus: err.httpStatus, ...schema ? { schema } : {} };
22512
+ }
22513
+ throw err;
22514
+ }
22515
+ const quotedAccept = quoted.accepts?.[0];
22516
+ if (!quotedAccept) {
22517
+ return { issued: false, reason: "MBI quoted no payment options", ...schema ? { schema } : {} };
22518
+ }
22519
+ const quotedRaw = String(quotedAccept.asset ?? "");
22520
+ const quotedAsset = deps.resolveSymbol ? await deps.resolveSymbol(quotedRaw) : quotedRaw;
22521
+ const reason = quoted.paymentRequired === false ? "dry run \u2014 issuance is free right now, no payment required" : quoted.paymentRequired === true ? "dry run \u2014 quoted only, no payment made" : "dry run \u2014 quoted only, no payment made. This MBI does not report whether issuance is free, so the quoted amount may not actually be charged.";
22522
+ return {
22523
+ issued: false,
22524
+ reason,
22525
+ quote: {
22526
+ asset: quotedAsset,
22527
+ maxAmountRequired: quotedAccept.maxAmountRequired,
22528
+ payTo: quotedAccept.payTo,
22529
+ // credential_preflight reads a missing gasModel as self-pay (preflight.ts), so a
22530
+ // sponsored template credential must say so explicitly rather than fall through as absent.
22531
+ gasModel: isSponsored(quotedAccept) ? "sponsored" : "self",
22532
+ ...quoted.paymentRequired === void 0 ? {} : { paymentRequired: quoted.paymentRequired }
22533
+ },
22534
+ ...schema ? { schema } : {}
22535
+ };
22536
+ }
22362
22537
  const data = JSON.stringify([{ templateId: opts.templateId, metadata: attributes }]);
22363
22538
  const blob = zetrixHexStringToBytes(data).toString("hex");
22364
22539
  const { signBlob: signData, publicKey } = await deps.sign(blob);
@@ -22394,26 +22569,11 @@ async function subscribeAndIssue(deps, opts) {
22394
22569
  txHash: issued2.txHash,
22395
22570
  paidAsset: "none",
22396
22571
  amountPaid: "0",
22397
- ...schema ? { schema } : {},
22398
- ...opts.dryRun ? { reason: "this template requires no payment \u2014 MBI issues synchronously at phase 1, so dryRun could not prevent this issuance" } : {}
22572
+ ...schema ? { schema } : {}
22399
22573
  };
22400
22574
  }
22401
22575
  const accept = challenge.accepts[0];
22402
22576
  if (!accept) return { issued: false, reason: "MBI 402 returned no payment options", ...schema ? { schema } : {} };
22403
- if (opts.dryRun) {
22404
- const quotedRaw = String(accept.asset ?? "");
22405
- const quotedAsset = deps.resolveSymbol ? await deps.resolveSymbol(quotedRaw) : quotedRaw;
22406
- return {
22407
- issued: false,
22408
- reason: "dry run \u2014 quoted only, no payment made",
22409
- quote: {
22410
- asset: quotedAsset,
22411
- maxAmountRequired: accept.maxAmountRequired,
22412
- payTo: accept.payTo
22413
- },
22414
- ...schema ? { schema } : {}
22415
- };
22416
- }
22417
22577
  let xPayment;
22418
22578
  try {
22419
22579
  xPayment = await deps.pay(accept);
@@ -22874,6 +23034,102 @@ function validateTemplateAttributes(templateId, network, attributes) {
22874
23034
  return errors;
22875
23035
  }
22876
23036
 
23037
+ // src/orchestrator/preflight.ts
23038
+ var VERIFIED_AI_BIRTHCERT = "verified_ai_birthcert";
23039
+ var NATIVE = "ZTX";
23040
+ function placeholderAgentName() {
23041
+ return `preflight-quote-only-${Date.now()}`;
23042
+ }
23043
+ async function credentialPreflight(deps, input) {
23044
+ const blockers = [];
23045
+ const notChecked = [];
23046
+ const isVerified = input.credential === VERIFIED_AI_BIRTHCERT;
23047
+ const quoted = isVerified ? await deps.quoteVerified({ agentName: input.agentName?.trim() || placeholderAgentName(), dryRun: true }) : await deps.quoteTemplate(input.credential);
23048
+ const quote = quoted.quote;
23049
+ const schema = quoted.schema;
23050
+ if (!quote) {
23051
+ const why = String(quoted.error ?? quoted.reason ?? "the price could not be read");
23052
+ return {
23053
+ credential: input.credential,
23054
+ ready: false,
23055
+ balances: [],
23056
+ ...schema ? { schema } : {},
23057
+ blockers: [`Could not price this credential: ${why}`],
23058
+ notChecked: uncheckable(isVerified)
23059
+ };
23060
+ }
23061
+ const asset = String(quote.asset ?? "");
23062
+ const requiredRaw = String(quote.maxAmountRequired ?? "0");
23063
+ const gasModel = quote.gasModel === "sponsored" || quote.gasModel === "self" ? quote.gasModel : void 0;
23064
+ const selfPaysGas = gasModel !== "sponsored";
23065
+ const wanted = asset === NATIVE || !selfPaysGas ? [asset] : [asset, NATIVE];
23066
+ const balances = await Promise.all(wanted.map((token) => deps.queryTokenBalance(token)));
23067
+ const isFree = quote.paymentRequired === false;
23068
+ const feeBalance = balances[0];
23069
+ if ("error" in feeBalance) {
23070
+ blockers.push(`Could not read the ${asset} balance (${feeBalance.error}) \u2014 retry before paying.`);
23071
+ } else if (!isFree && BigInt(feeBalance.balance) < BigInt(requiredRaw)) {
23072
+ blockers.push(`Not enough ${feeBalance.token}: the fee is ${renderAmount(requiredRaw, feeBalance)}, the balance is ${feeBalance.display}.`);
23073
+ }
23074
+ if (selfPaysGas && asset !== NATIVE) {
23075
+ const gas = balances[1];
23076
+ if (gas && "error" in gas) {
23077
+ blockers.push(`Could not read the ZTX balance (${gas.error}) \u2014 ZTX pays network gas and is separate from the fee.`);
23078
+ } else if (gas && BigInt(gas.balance) === 0n) {
23079
+ blockers.push(`No ZTX for network gas. ZTX is separate from the ${feeBalance.token ?? asset} fee and is needed for every transaction.`);
23080
+ }
23081
+ }
23082
+ const cap = describePaymentCap(asset, requiredRaw, deps.caps);
23083
+ if (!cap.wouldPass && !isFree) blockers.push(renderCapBlocker(cap, requiredRaw));
23084
+ return {
23085
+ credential: input.credential,
23086
+ ready: blockers.length === 0,
23087
+ fee: {
23088
+ asset,
23089
+ maxAmountRequired: requiredRaw,
23090
+ ...quote.payTo ? { payTo: String(quote.payTo) } : {},
23091
+ ...gasModel ? { gasModel } : {},
23092
+ ..."error" in feeBalance ? {} : { display: renderAmount(requiredRaw, feeBalance) },
23093
+ ...quote.paymentRequired === void 0 ? {} : { paymentRequired: quote.paymentRequired }
23094
+ },
23095
+ balances,
23096
+ cap,
23097
+ ...schema ? { schema } : {},
23098
+ blockers,
23099
+ notChecked: uncheckable(isVerified, isFree)
23100
+ };
23101
+ }
23102
+ function renderAmount(raw, balance) {
23103
+ if ("error" in balance || balance.decimals === null) return raw;
23104
+ const d = BigInt(10) ** BigInt(balance.decimals);
23105
+ const whole = BigInt(raw) / d;
23106
+ const frac = (BigInt(raw) % d).toString().padStart(balance.decimals, "0").replace(/0+$/, "");
23107
+ return `${whole}${frac ? `.${frac}` : ""} ${balance.token}`;
23108
+ }
23109
+ function renderCapBlocker(cap, requiredRaw) {
23110
+ if (cap.capRaw === null) {
23111
+ return `No spending limit applies to ${cap.asset}, and limits are configured \u2014 so this payment would be refused. Set a limit keyed by "${cap.asset}".`;
23112
+ }
23113
+ const misKeyed = cap.matchedKey !== cap.asset;
23114
+ return misKeyed ? `The spending limit that applies is ${cap.capRaw} (from the "*" fallback \u2014 no limit is set for ${cap.asset}), and this needs ${requiredRaw}.` : `The spending limit for ${cap.asset} is ${cap.capRaw}, and this needs ${requiredRaw}.`;
23115
+ }
23116
+ function uncheckable(isVerified, isFree = false) {
23117
+ const items = [
23118
+ "Whether the wallet address is activated on chain \u2014 a never-funded address fails differently from a low balance."
23119
+ ];
23120
+ if (isVerified) {
23121
+ items.unshift(
23122
+ "Whether the agent name is still free. myid checks uniqueness at issuance, not now, so a name in use still prices normally and is only refused after payment."
23123
+ );
23124
+ }
23125
+ if (isFree) {
23126
+ items.unshift(
23127
+ "Whether issuance is still free at apply time \u2014 `paymentRequired` is a service-wide MBI setting and can change between this quote and issuance."
23128
+ );
23129
+ }
23130
+ return items;
23131
+ }
23132
+
22877
23133
  // src/mcp-tools.ts
22878
23134
  async function loadValidCachedCredentials(cache) {
22879
23135
  if (!cache) return [];
@@ -22882,10 +23138,15 @@ async function loadValidCachedCredentials(cache) {
22882
23138
  }
22883
23139
  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.";
22884
23140
  function createTools(deps) {
22885
- return {
23141
+ const tools = {
22886
23142
  async wallet_status(input = {}) {
22887
23143
  const balances = deps.getBalances ? await deps.getBalances() : void 0;
22888
23144
  const tokenBalance = input.token && deps.queryTokenBalance ? await deps.queryTokenBalance(input.token) : void 0;
23145
+ let tokenBalances;
23146
+ if (input.tokens?.length && deps.queryTokenBalance) {
23147
+ tokenBalances = [];
23148
+ for (const t of input.tokens) tokenBalances.push(await deps.queryTokenBalance(t));
23149
+ }
22889
23150
  const credentials = input.heldCredentials ?? (await loadValidCachedCredentials(deps.cache)).map((entry) => entry.vc);
22890
23151
  return {
22891
23152
  holderDid: deps.config.holderDid,
@@ -22893,7 +23154,8 @@ function createTools(deps) {
22893
23154
  network: deps.config.network,
22894
23155
  credentials,
22895
23156
  ...balances !== void 0 ? { balances } : {},
22896
- ...tokenBalance !== void 0 ? { tokenBalance } : {}
23157
+ ...tokenBalance !== void 0 ? { tokenBalance } : {},
23158
+ ...tokenBalances !== void 0 ? { tokenBalances } : {}
22897
23159
  };
22898
23160
  },
22899
23161
  async prove_identity(input) {
@@ -22990,6 +23252,38 @@ function createTools(deps) {
22990
23252
  }
22991
23253
  return deps.verifyAiBirthcert.request(input);
22992
23254
  },
23255
+ /**
23256
+ * One read-only answer to "can this wallet buy this, and what does it cost?", before any
23257
+ * application field is collected. Composes the free pieces that already exist — a phase-1 quote,
23258
+ * balances, the spending cap — into a single verdict, so the balance and the cap stop being two
23259
+ * sequential dead ends.
23260
+ */
23261
+ async credential_preflight(input) {
23262
+ if (input.credential === VERIFIED_AI_BIRTHCERT && !deps.verifyAiBirthcert) {
23263
+ return { credential: input.credential, ready: false, balances: [], blockers: [AI_BIRTHCERT_NOT_CONFIGURED_ERROR], notChecked: [] };
23264
+ }
23265
+ if (!deps.queryTokenBalance) {
23266
+ return {
23267
+ credential: input.credential,
23268
+ ready: false,
23269
+ balances: [],
23270
+ blockers: ["This wallet cannot read balances, so readiness cannot be confirmed."],
23271
+ notChecked: []
23272
+ };
23273
+ }
23274
+ return credentialPreflight(
23275
+ {
23276
+ quoteVerified: (q) => deps.verifyAiBirthcert.request(q),
23277
+ quoteTemplate: async (templateId) => {
23278
+ const resolved = resolveTemplateAlias(templateId, deps.config.network) ?? templateId;
23279
+ return priceTemplate(deps, tools, resolved);
23280
+ },
23281
+ queryTokenBalance: deps.queryTokenBalance,
23282
+ caps: deps.paymentCaps
23283
+ },
23284
+ input
23285
+ );
23286
+ },
22993
23287
  check_ai_birthcert_verification() {
22994
23288
  if (!deps.verifyAiBirthcert) {
22995
23289
  return { error: AI_BIRTHCERT_NOT_CONFIGURED_ERROR };
@@ -22997,6 +23291,42 @@ function createTools(deps) {
22997
23291
  return deps.verifyAiBirthcert.check();
22998
23292
  }
22999
23293
  };
23294
+ return tools;
23295
+ }
23296
+ async function priceTemplate(deps, tools, templateId) {
23297
+ const schemaResult = await tools.get_template_schema({ templateId });
23298
+ const schema = schemaResult.schema;
23299
+ if (!schema && schemaResult.error) {
23300
+ return { reason: schemaResult.error };
23301
+ }
23302
+ const quoteFn = deps.subscribeDeps.mbi.quote;
23303
+ if (!quoteFn) {
23304
+ return { reason: "this wallet cannot price template credentials (MBI quote unavailable)", ...schema ? { schema } : {} };
23305
+ }
23306
+ let accepts;
23307
+ let paymentRequired;
23308
+ try {
23309
+ ;
23310
+ ({ accepts, paymentRequired } = await quoteFn.call(deps.subscribeDeps.mbi, templateId, { preflight: "quote-only" }));
23311
+ } catch (err) {
23312
+ return { reason: err.message, ...schema ? { schema } : {} };
23313
+ }
23314
+ const accept = accepts?.[0];
23315
+ if (!accept) return { reason: "MBI quoted no payment options for this template", ...schema ? { schema } : {} };
23316
+ return {
23317
+ quote: {
23318
+ asset: accept.asset,
23319
+ maxAmountRequired: accept.maxAmountRequired,
23320
+ payTo: accept.payTo,
23321
+ // Stated either way: preflight reads a missing gasModel as self-pay, so silence here would be
23322
+ // indistinguishable from a deliberate "you pay the gas".
23323
+ gasModel: isSponsored(accept) ? "sponsored" : "self",
23324
+ // Carried through: this is the only thing in the response that
23325
+ // says whether `maxAmountRequired` will actually be charged, and preflight gates on it.
23326
+ ...paymentRequired === void 0 ? {} : { paymentRequired }
23327
+ },
23328
+ ...schema ? { schema } : {}
23329
+ };
23000
23330
  }
23001
23331
 
23002
23332
  // src/orchestrator/resolve-holder.ts
@@ -23338,43 +23668,6 @@ function canonicalizeJson(value) {
23338
23668
  return JSON.stringify(value);
23339
23669
  }
23340
23670
 
23341
- // src/accept-selection.ts
23342
- function extraOf(accept) {
23343
- const extra = accept.extra;
23344
- if (!extra || typeof extra !== "object" || Array.isArray(extra)) return {};
23345
- return extra;
23346
- }
23347
- function assetOf(accept) {
23348
- return String(accept.asset ?? "");
23349
- }
23350
- function declaresFacilitator(accept) {
23351
- return extraOf(accept).gasModel === "facilitator";
23352
- }
23353
- function isSponsored(accept) {
23354
- if (!declaresFacilitator(accept)) return false;
23355
- const prepareEndpoint = extraOf(accept).prepareEndpoint;
23356
- if (typeof prepareEndpoint !== "string" || prepareEndpoint === "") return false;
23357
- const asset = assetOf(accept);
23358
- return asset !== "" && asset !== "ZTX";
23359
- }
23360
- function isUnusable(accept) {
23361
- return declaresFacilitator(accept) && !isSponsored(accept);
23362
- }
23363
- function orderAccepts(accepts, prefer) {
23364
- const usable = accepts.filter((a) => !isUnusable(a));
23365
- const sponsored = usable.filter(isSponsored);
23366
- const selfPay = usable.filter((a) => !isSponsored(a));
23367
- return prefer === "sponsored" ? [...sponsored, ...selfPay] : [...selfPay, ...sponsored];
23368
- }
23369
- function prepareBaseUrl(prepareEndpoint) {
23370
- return prepareEndpoint.replace(/\/+$/, "").replace(/\/prepare$/, "");
23371
- }
23372
- function needsNativeGasCheck(accept) {
23373
- const asset = String(accept.asset ?? "");
23374
- if (asset === "" || asset === "ZTX") return false;
23375
- return !isSponsored(accept);
23376
- }
23377
-
23378
23671
  // src/orchestrator/verify-ai-birthcert.ts
23379
23672
  function subjectMatches(vc, holderDid) {
23380
23673
  if (typeof vc !== "object" || vc === null) return false;
@@ -23496,6 +23789,7 @@ var DEFINITIVE_PREPARE_REFUSALS = /* @__PURE__ */ new Set([
23496
23789
  461415
23497
23790
  // X402_UNSUPPORTED_ASSET — sponsorship is ZTP-20 only
23498
23791
  ]);
23792
+ var FACILITATOR_INSUFFICIENT_FUNDS_CODE = 461407;
23499
23793
  function facilitatorErrorCode(err) {
23500
23794
  const m = /"errorCode"\s*:\s*(\d{6})/.exec(err instanceof Error ? err.message : String(err));
23501
23795
  return m ? Number(m[1]) : void 0;
@@ -23515,6 +23809,41 @@ function isDefinitiveSponsorshipFailure(err) {
23515
23809
  if (code === void 0) return false;
23516
23810
  return DEFINITIVE_PREPARE_REFUSALS.has(code);
23517
23811
  }
23812
+ var FacilitatorInsufficientFundsError = class extends Error {
23813
+ constructor(requiredHuman, availableHuman, rawMessage) {
23814
+ super(
23815
+ `insufficient funds: the sponsored payment requires ${requiredHuman}` + (availableHuman !== void 0 ? ` \u2014 this wallet currently holds ${availableHuman}` : " and this wallet does not hold enough of it") + ` (facilitator: ${rawMessage})`
23816
+ );
23817
+ this.name = "FacilitatorInsufficientFundsError";
23818
+ }
23819
+ };
23820
+ function extractFacilitatorBalance(rawMessage) {
23821
+ const braceIdx = rawMessage.indexOf("{");
23822
+ if (braceIdx !== -1) {
23823
+ try {
23824
+ const body = JSON.parse(rawMessage.slice(braceIdx));
23825
+ const detail = body.messages?.[0]?.detail;
23826
+ if (Array.isArray(detail) && detail.length >= 3 && detail[2] !== null && detail[2] !== void 0) {
23827
+ return String(detail[2]);
23828
+ }
23829
+ } catch {
23830
+ }
23831
+ }
23832
+ const m = /insufficient_funds \[[^,]+,\s*[^,]+,\s*([^\]]+)\]/.exec(rawMessage);
23833
+ return m ? m[1].trim() : void 0;
23834
+ }
23835
+ async function toFacilitatorInsufficientFundsError(err, accept, formatAssetAmount) {
23836
+ if (!(err instanceof PrepareStageError)) return err;
23837
+ if (facilitatorErrorCode(err.cause) !== FACILITATOR_INSUFFICIENT_FUNDS_CODE) return err;
23838
+ const asset = accept.asset ?? "";
23839
+ const maxAmountRequired = accept.maxAmountRequired ?? "";
23840
+ if (!asset || !maxAmountRequired) return err;
23841
+ const rawMessage = err.cause instanceof Error ? err.cause.message : String(err.cause);
23842
+ const requiredHuman = formatAssetAmount ? await formatAssetAmount(asset, maxAmountRequired) : `${maxAmountRequired} of asset "${asset}"`;
23843
+ const availableRaw = extractFacilitatorBalance(rawMessage);
23844
+ const availableHuman = availableRaw !== void 0 && formatAssetAmount ? await formatAssetAmount(asset, availableRaw) : availableRaw;
23845
+ return new FacilitatorInsufficientFundsError(requiredHuman, availableHuman, rawMessage);
23846
+ }
23518
23847
  async function attemptCandidate(deps, accept, buildBody, onQueued) {
23519
23848
  let xPayment;
23520
23849
  try {
@@ -23534,8 +23863,9 @@ async function payAndCreateSession(deps, buildBody, onQueued) {
23534
23863
  return await attemptCandidate(deps, primary, buildBody, onQueued);
23535
23864
  } catch (err) {
23536
23865
  const fallback = candidates[1];
23537
- if (!fallback || !isSponsored(primary) || isSponsored(fallback)) throw err;
23538
- if (!isDefinitiveSponsorshipFailure(err)) throw err;
23866
+ if (!fallback || !isSponsored(primary) || isSponsored(fallback) || !isDefinitiveSponsorshipFailure(err)) {
23867
+ throw await toFacilitatorInsufficientFundsError(err, primary, deps.formatAssetAmount);
23868
+ }
23539
23869
  return await attemptCandidate(deps, fallback, buildBody, onQueued);
23540
23870
  }
23541
23871
  }
@@ -23544,29 +23874,48 @@ async function requestAiBirthcertVerification(deps, input) {
23544
23874
  throw new Error("requestAiBirthcertVerification: agentName is required");
23545
23875
  }
23546
23876
  const agentName = input.agentName.trim();
23877
+ if (input.dryRun) return quoteVerification(deps, agentName, input);
23547
23878
  return withRequestLock(() => requestAiBirthcertVerificationLocked(deps, agentName, input));
23548
23879
  }
23880
+ async function buildSessionBody(deps, agentName, input) {
23881
+ const fields = {
23882
+ publicKey: deps.publicKeyHex,
23883
+ address: deps.address,
23884
+ timestamp: isoSeconds(deps.now()),
23885
+ agentName,
23886
+ id: agentName,
23887
+ ownerReference: deps.holderDid
23888
+ };
23889
+ if (input.agentPurpose) fields.agentPurpose = input.agentPurpose;
23890
+ if (input.evidenceAssuranceLevel) fields.evidenceAssuranceLevel = input.evidenceAssuranceLevel;
23891
+ if (input.ownerType) fields.ownerType = input.ownerType;
23892
+ if (input.ownerVerified) fields.ownerVerified = input.ownerVerified;
23893
+ const digestHex = (0, import_node_crypto4.createHash)("sha256").update(canonicalizeJson(fields), "utf8").digest("hex");
23894
+ const { signBlob: signedData } = await deps.signHexBlob(digestHex);
23895
+ return { ...fields, signedData };
23896
+ }
23897
+ async function quoteVerification(deps, agentName, input) {
23898
+ const requestedGasPayer = input.gasPayer === "self" || input.gasPayer === "sponsored" ? input.gasPayer : void 0;
23899
+ const body = await buildSessionBody(deps, agentName, input);
23900
+ const challenge = await deps.ssivc.createSessionChallenge(body);
23901
+ const chosen = orderAccepts(challenge.accepts, requestedGasPayer ?? deps.gasPreference ?? "sponsored")[0];
23902
+ if (!chosen) return { error: "SSIVC 402 returned no usable payment options" };
23903
+ return {
23904
+ quote: {
23905
+ asset: String(chosen.asset ?? ""),
23906
+ maxAmountRequired: String(chosen.maxAmountRequired ?? ""),
23907
+ ...chosen.payTo ? { payTo: String(chosen.payTo) } : {},
23908
+ // Report the option that would actually be paid, not merely what was offered — the ranking
23909
+ // already discards a sponsored quote we could not act on.
23910
+ gasModel: isSponsored(chosen) ? "sponsored" : "self"
23911
+ }
23912
+ };
23913
+ }
23549
23914
  async function requestAiBirthcertVerificationLocked(deps, agentName, input) {
23550
23915
  const decision = await decidePriorSession(deps, agentName);
23551
23916
  if (decision.kind === "still_pending") return decision.result;
23552
23917
  if (decision.kind === "blocked") return { error: decision.message };
23553
- const buildBody = async () => {
23554
- const fields = {
23555
- publicKey: deps.publicKeyHex,
23556
- address: deps.address,
23557
- timestamp: isoSeconds(deps.now()),
23558
- agentName,
23559
- id: agentName,
23560
- ownerReference: deps.holderDid
23561
- };
23562
- if (input.agentPurpose) fields.agentPurpose = input.agentPurpose;
23563
- if (input.evidenceAssuranceLevel) fields.evidenceAssuranceLevel = input.evidenceAssuranceLevel;
23564
- if (input.ownerType) fields.ownerType = input.ownerType;
23565
- if (input.ownerVerified) fields.ownerVerified = input.ownerVerified;
23566
- const digestHex = (0, import_node_crypto4.createHash)("sha256").update(canonicalizeJson(fields), "utf8").digest("hex");
23567
- const { signBlob: signedData } = await deps.signHexBlob(digestHex);
23568
- return { ...fields, signedData };
23569
- };
23918
+ const buildBody = () => buildSessionBody(deps, agentName, input);
23570
23919
  const persistQueuedReceipt = async (receipt) => {
23571
23920
  try {
23572
23921
  await deps.sessionStore.set({
@@ -23602,9 +23951,10 @@ async function requestAiBirthcertVerificationLocked(deps, agentName, input) {
23602
23951
  }
23603
23952
  } catch (rawErr) {
23604
23953
  const err = rawErr instanceof PrepareStageError ? rawErr.cause : rawErr;
23605
- if (err instanceof PaymentReadinessError) return { error: `insufficient funds: ${err.message}` };
23606
- if (err instanceof PaymentCapError) return { error: err.message };
23954
+ if (err instanceof PaymentReadinessError) return { error: `insufficient funds: ${err.message}`, insufficientFunds: err.shortfall };
23955
+ if (err instanceof PaymentCapError) return { error: err.message, ...err.detail ? { paymentCap: err.detail } : {} };
23607
23956
  if (err instanceof NoPaymentOptionsError) return { error: err.message };
23957
+ if (err instanceof FacilitatorInsufficientFundsError) return { error: err.message };
23608
23958
  if (err instanceof SettlementStillQueuedError) {
23609
23959
  try {
23610
23960
  await deps.sessionStore.set({
@@ -23655,6 +24005,9 @@ async function checkAiBirthcertVerification(deps) {
23655
24005
  };
23656
24006
  }
23657
24007
  const status = await deps.ssivc.getSession(stored.sessionId);
24008
+ if (status.status !== "issued" && stored.verificationUrl) {
24009
+ return { ...status, verificationUrl: stored.verificationUrl };
24010
+ }
23658
24011
  if (status.status !== "issued" || !status.vcId) return status;
23659
24012
  if (deps.verifiedTemplateId && deps.cache) {
23660
24013
  const cached2 = await deps.cache.get(deps.verifiedTemplateId);
@@ -23724,7 +24077,8 @@ function buildToolList() {
23724
24077
  type: "object",
23725
24078
  properties: {
23726
24079
  heldCredentials: { type: "array", items: { type: "object" }, description: "VCs the client holds. Omit to report whatever the wallet has cached locally from prior subscribe_and_issue calls instead." },
23727
- token: { type: "string", description: `Optional token symbol (e.g. "ZTX", "JMYR") to check its balance for the active network, alongside the usual status fields. Returns { balance, decimals } where balance is in the asset's raw base units \u2014 divide by 10^decimals for the human amount (e.g. balance "473999900" with decimals 6 is 473.9999 JMYR). A failed lookup reports { error: "query_failed" } rather than a zero balance.` }
24080
+ token: { type: "string", description: 'Optional token symbol (e.g. "ZTX", "JMYR") OR a ZTP20 contract address, to check its balance for the active network alongside the usual status fields. Returns { balance, decimals, display } \u2014 `balance` is in the asset\'s raw base units and `display` is the same amount in whole tokens with its symbol (e.g. balance "473999900", decimals 6, display "473.9999 JMYR"). Quote a `display` value to the user, never a bare `balance`. A failed lookup reports { error: "query_failed" } rather than a zero balance; an unrecognised name reports { error: "unknown_token" }.' },
24081
+ tokens: { type: "array", items: { type: "string" }, description: 'Several tokens (symbols and/or ZTP20 contract addresses) in one call, returned as `tokenBalances` in the order asked. Prefer this over repeated single-token calls when checking affordability: a credential fee and the native ZTX needed for gas are separate balances, and asking one at a time is how "you hold the token but no gas" is discovered only after the first shortfall was already fixed. Each entry carries its own result or error, so one failure does not hide the rest.' }
23728
24082
  }
23729
24083
  }
23730
24084
  },
@@ -23807,7 +24161,7 @@ function buildToolList() {
23807
24161
  expirationDate: { type: "string" },
23808
24162
  dryRun: {
23809
24163
  type: "boolean",
23810
- description: "Stop after MBI's free phase-1 quote and return { quote: { asset, maxAmountRequired, payTo }, schema: { required, optional } } without paying or issuing \u2014 use this to check the payment requirement AND the template's full attribute schema (read from chain) before spending funds. Still validates required attributes locally first \u2014 a missing one blocks before any MBI call."
24164
+ description: "Price the credential without paying, signing, or issuing anything. Returns { quote: { asset, maxAmountRequired, payTo, gasModel, paymentRequired? }, schema: { required, optional } }. Priced via MBI's /quote endpoint, which cannot issue \u2014 so this is safe even on a template that issues for free (such a template mints synchronously on the real path, which is why pricing never goes through it). Never writes the VC cache, so it cannot displace a credential you already hold. `quote.paymentRequired` is the authoritative free-vs-paid answer: false means issuance is currently free and `maxAmountRequired` will not be charged. When the field is ABSENT the MBI predates it and the amount is unconfirmed \u2014 report it as a possible charge, never as certainly free. Still validates required attributes locally first \u2014 a missing one blocks before any MBI call."
23811
24165
  },
23812
24166
  forceReissue: {
23813
24167
  type: "boolean",
@@ -23819,7 +24173,7 @@ function buildToolList() {
23819
24173
  },
23820
24174
  {
23821
24175
  name: "request_ai_birthcert_verification",
23822
- 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.`,
24176
+ 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 same credential-issuance payment cap as subscribe_and_issue \u2014 a separate, narrower cap than pay_and_fetch's, which defaults to refusing everything on mainnet. Set MAX_PAYMENT_AMOUNT to override either. 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.`,
23823
24177
  inputSchema: {
23824
24178
  type: "object",
23825
24179
  properties: {
@@ -23835,14 +24189,36 @@ function buildToolList() {
23835
24189
  type: "string",
23836
24190
  enum: ["sponsored", "self"],
23837
24191
  description: `Who pays network gas. "sponsored" (default) asks the platform paymaster to cover gas, so this wallet needs no ZTX. "self" pays gas from this wallet's own ZTX balance. Omit to use the configured default.`
24192
+ },
24193
+ dryRun: {
24194
+ type: "boolean",
24195
+ description: "Ask the price WITHOUT paying. Returns { quote: { asset, maxAmountRequired, payTo, gasModel } } and spends nothing, creates no session, and starts no verification \u2014 so you can tell the user the cost before collecting anything. `maxAmountRequired` is in the asset's RAW base units; resolve decimals (wallet_status returns `display`) before quoting a figure to a human. A quote does NOT reserve the name and does NOT check whether it is already taken \u2014 myid checks uniqueness only at issuance, so a name already in use still quotes cleanly. `agentName` is still required because the server rejects a request without one, but the fee does not depend on it."
23838
24196
  }
23839
24197
  },
23840
24198
  required: ["agentName"]
23841
24199
  }
23842
24200
  },
24201
+ {
24202
+ name: "credential_preflight",
24203
+ description: "FREE readiness check \u2014 call this FIRST, before collecting ANY application detail from the user, whenever they ask for a credential. Spends nothing, signs no transaction, creates no session. Returns { ready, fee, balances, cap, schema?, blockers, notChecked }: the live fee and which side pays gas, the balances that matter, whether the spending limit permits it, and \u2014 for a template credential \u2014 the attributes it requires. `blockers` lists EVERY reason it is not ready at once (a low balance and a too-low spending limit are different problems and both appear together), so one round of fixes is enough rather than discovering them one failed payment at a time. ALWAYS relay `notChecked` too: a clean result is not a guarantee. In particular it does NOT check whether an agent name is free \u2014 myid decides that at issuance, after payment. Report `fee.display` and each balance's `display` to the user, never the raw base-unit numbers. `fee.paymentRequired: false` means issuance is currently FREE \u2014 `fee.display` is then only what it WOULD cost if payment were switched back on, so do not ask the user to fund it and do not present that amount as a charge. Gas is separate and can still block a free credential. When the field is ABSENT the cost is unknown (an older MBI), and it is treated as chargeable \u2014 never report absent as free.",
24204
+ inputSchema: {
24205
+ type: "object",
24206
+ properties: {
24207
+ credential: {
24208
+ type: "string",
24209
+ description: 'Which credential to price: "verified_ai_birthcert" for the MyDigital-ID-verified AI Birthcert, or a template id (did:zid:...) / known template name (e.g. "AI Birthcert") for a template-issued one.'
24210
+ },
24211
+ agentName: {
24212
+ type: "string",
24213
+ description: 'Optional, and only used for "verified_ai_birthcert". The fee does not depend on it, so omit it when pricing before the user has chosen a name \u2014 the wallet substitutes a placeholder purely to satisfy the server. Never present that placeholder as the name that will be used.'
24214
+ }
24215
+ },
24216
+ required: ["credential"]
24217
+ }
24218
+ },
23843
24219
  {
23844
24220
  name: "check_ai_birthcert_verification",
23845
- 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.',
24221
+ description: 'Check the status of the most recently requested Verified AI Birthcert session (see request_ai_birthcert_verification). FREE \u2014 spends nothing and starts nothing. Use this, not request_ai_birthcert_verification, whenever the user asks where their verification link is, what happened to their session, or whether their credential is ready. While the session is still open the result carries `verificationUrl` (the same link issued at creation) and `expiresAt` \u2014 give the user both, so they know how long it is good for. 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.',
23846
24222
  inputSchema: { type: "object", properties: {} }
23847
24223
  },
23848
24224
  {
@@ -23862,6 +24238,13 @@ function buildToolList() {
23862
24238
  }
23863
24239
  ];
23864
24240
  }
24241
+ function buildPayers(config2, makePay) {
24242
+ return {
24243
+ pay: makePay(config2.maxPaymentAmount),
24244
+ payForCredential: makePay(config2.credentialIssuanceCaps),
24245
+ preflightCaps: config2.credentialIssuanceCaps
24246
+ };
24247
+ }
23865
24248
  function asPayRequest(accept) {
23866
24249
  const extra = accept.extra ?? {};
23867
24250
  const prepareEndpoint = extra.prepareEndpoint;
@@ -23954,15 +24337,19 @@ async function main() {
23954
24337
  const human = formatHumanAmount(raw, decimals);
23955
24338
  return human === raw ? `${raw} ${label}` : `${raw} (${human} ${label})`;
23956
24339
  };
23957
- const pay = async (accept) => {
24340
+ const makePay = (caps) => async (accept) => {
23958
24341
  const rawAsset = String(accept.asset ?? "");
23959
24342
  try {
23960
- assertWithinPaymentCap(accept, config2.maxPaymentAmount);
24343
+ assertWithinPaymentCap(accept, caps);
23961
24344
  } catch (err) {
23962
24345
  if (err instanceof PaymentCapError && err.detail) {
23963
24346
  const { asset, requiredRaw, capRaw } = err.detail;
23964
24347
  throw new PaymentCapError(
23965
- `payment blocked: requested ${await formatAssetAmount(asset, requiredRaw)} exceeds configured MAX_PAYMENT_AMOUNT ${await formatAssetAmount(asset, capRaw)}`,
24348
+ formatCapRefusal(
24349
+ err.detail,
24350
+ await formatAssetAmount(asset, requiredRaw),
24351
+ await formatAssetAmount(asset, capRaw)
24352
+ ),
23966
24353
  err.detail
23967
24354
  );
23968
24355
  }
@@ -23996,6 +24383,7 @@ async function main() {
23996
24383
  throw err;
23997
24384
  }
23998
24385
  };
24386
+ const { pay, payForCredential, preflightCaps } = buildPayers(config2, makePay);
23999
24387
  const verifyAiBirthcert = config2.ssivcBaseUrl ? (() => {
24000
24388
  const ssivcSessionStore = createFsSsivcSessionStore((0, import_node_path6.join)(config2.stateDir, "ssivc-session.json"));
24001
24389
  const downloadQuarantine = createFsDownloadQuarantineStore((0, import_node_path6.join)(config2.stateDir, "ssivc-download-quarantine"));
@@ -24005,7 +24393,7 @@ async function main() {
24005
24393
  signHexBlob: walletBeSignerFn,
24006
24394
  messageSigner,
24007
24395
  mbi,
24008
- pay,
24396
+ pay: payForCredential,
24009
24397
  publicKeyHex,
24010
24398
  address: zetrixAddress,
24011
24399
  holderDid,
@@ -24015,7 +24403,8 @@ async function main() {
24015
24403
  cache: vcCache,
24016
24404
  quarantine: downloadQuarantine,
24017
24405
  gasPreference: config2.gasPreference,
24018
- maxSettlementAttempts: config2.maxSettlementAttempts
24406
+ maxSettlementAttempts: config2.maxSettlementAttempts,
24407
+ formatAssetAmount
24019
24408
  };
24020
24409
  return {
24021
24410
  request: (input) => requestAiBirthcertVerification(verifyAiBirthcertDeps, input),
@@ -24066,9 +24455,13 @@ async function main() {
24066
24455
  config: { holderDid, zetrixAddress, network: config2.network },
24067
24456
  makeWallet,
24068
24457
  payer,
24069
- subscribeDeps: { mbi, sign: subscribeSign, pay, resolveSymbol, holderDid, resolveTemplateFields, cache: vcCache },
24458
+ subscribeDeps: { mbi, sign: subscribeSign, pay: payForCredential, resolveSymbol, holderDid, resolveTemplateFields, cache: vcCache },
24070
24459
  queryContract: (input) => queryContract(input, contractQuery),
24071
24460
  queryTokenBalance: queryTokenBalance2,
24461
+ // Read-only, for credential_preflight's cap headroom. Preflight only ever prices credentials,
24462
+ // so it must report against the issuance cap — the same map payForCredential enforces, or
24463
+ // preflight and the guard would disagree about what is permitted.
24464
+ paymentCaps: preflightCaps,
24072
24465
  // The session password is bound here, in the wiring, so it never crosses into the tool
24073
24466
  // layer — create_holder_account has no password parameter for a model to be asked for.
24074
24467
  createAccount: (label, purpose) => be.createAccount(hsmPassword, label, purpose),
@@ -24111,5 +24504,6 @@ if (process.env.NODE_ENV !== "test") {
24111
24504
  // Annotate the CommonJS export names for ESM import in node:
24112
24505
  0 && (module.exports = {
24113
24506
  asPayRequest,
24507
+ buildPayers,
24114
24508
  buildToolList
24115
24509
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-wallet-mcp",
3
- "version": "0.9.1",
3
+ "version": "0.10.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",
@@ -43,7 +43,7 @@
43
43
  "dependencies": {
44
44
  "@modelcontextprotocol/sdk": "^1.0.0",
45
45
  "x401-zetrix-client": "^0.2.1",
46
- "x402-zetrix-client": "^0.2.2",
46
+ "x402-zetrix-client": "^0.2.4",
47
47
  "zetrix-sdk-nodejs": "^1.0.3"
48
48
  },
49
49
  "devDependencies": {