agentic-wallet-mcp 0.12.1 → 0.12.2

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,37 @@ 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.12.2] — 24 September 2026
12
+
13
+ ### Fixed
14
+
15
+ - **A taken `agentName` is no longer reported as an already-settled payment.** The credential service
16
+ answers both "this name is already taken" and "this exact payment was already settled" with the same
17
+ HTTP 409, and the wallet used to treat every 409 as the settled case — telling the user their fee was
18
+ most likely taken and warning that retrying would charge it again, when picking a different name was
19
+ actually free to try. The two are now told apart, and a taken name says only that: choose a different
20
+ `agentName`; it does not claim the fee was, or was not, taken.
21
+ - **A payment can no longer be discarded and paid again until it is genuinely stuck.** In a live run the
22
+ first payment had already settled, the assistant offered to "discard the stuck receipt and start
23
+ fresh" for a second 1 JMYR, and a bare "retry" from the user was taken as agreement, so the user paid
24
+ twice. The rule that a receipt only counts as stuck after 24 hours (`SETTLEMENT_STUCK_AFTER_MS`) was
25
+ only advice; the wallet now enforces it on both ways of discarding a receipt and pays nothing when the
26
+ receipt is younger. Set `SETTLEMENT_STUCK_AFTER_MS` lower on purpose if you need to start over sooner.
27
+ - **The agent can now tell the user how long the verification link really lasts.** The wallet used to
28
+ return the session's expiry untouched and leave the arithmetic to the agent, and in one run the agent
29
+ mixed up timezones and said the link was good for about 8 hours when it had about 15 minutes. Results
30
+ now carry `expiresIn` ("about 14 minutes") and `expiresInSeconds`, worked out by the wallet, and the
31
+ tool descriptions tell the agent to quote them.
32
+ - **A payment the credential service has not confirmed yet is now reported with what the service actually
33
+ said**, including its status code and message, plus a short sentence the agent can pass on to the user
34
+ as-is. Previously the agent had nothing real to quote and sometimes made up an explanation.
35
+
36
+ ### Added
37
+
38
+ - `SSIVC_TRACE=1` prints every request to and response from the credential service to stderr, for
39
+ diagnosing a settlement that will not resolve. Off by default; the trace contains the payment receipt,
40
+ so it is for reproductions only.
41
+
11
42
  ## [0.12.1] — 24 September 2026
12
43
 
13
44
  ### Fixed
package/README.md CHANGED
@@ -28,9 +28,12 @@ VC issuance → identity proof → pay-per-use).
28
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 }` |
29
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 }` |
30
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 }` |
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?, discardStuckReceiptAndPayFresh? }` | `{ sessionId, verificationUrl, expiresAt }`, `{ quote: { asset, maxAmountRequired, payTo?, gasModel } }`, or — instead of a session — `{ settlementPending: true, paymentReceipt, message }` after ~90s, whose meaning depends on which flag (if any) is set below: never assume the payment succeeded from this shape alone. While a merely-stuck receipt (outcome still unresolved) is held this tool can only REPLAY it — it never buys a new credential, which is what stops a second charge. A credential-service refusal is different again: `issuerRejected: true` means the service was reached and refused the request (the settlement is NOT what failed); `paymentInvalid: true` means the service specifically ruled the payment or receipt invalid (this one IS about the payment). Neither tells you whether the fee was taken, in either direction. A **void** receipt (SSIVC status_code 67/68) is different again: the wallet discards it itself, with no confirmation and no flag, so the very next call is an ordinary fresh purchase — see `{ error }` below. `discardStuckReceiptAndPayFresh: "<exact receipt id>"` is the way to start over on a merely-stuck receipt instead: it throws that payment away and pays the fee again, refuses a mismatched id (discarding and paying nothing), refuses a receipt that belongs to a live session, and returns `discardedPaymentReceipt` so the lost payment stays traceable. `{ error }` (`RequestVerificationFailure`) also carries `discardedPaymentReceipt` when this call itself discarded a dead receipt — that confirmed stuck-receipt discard, or an unprompted void one, or **both on one call**; it also rides on `settlementPending` results, not only successes. When both happen (the confirmed discard, then the fresh payment ruled void) the field holds the **void** receipt and the id the user confirmed is named at the end of the `error`/`message` text ("ALSO DISCARDED earlier on this same call") — quote both ids to support, not just the field. Retrying is fine only for an insufficient-funds or payment-cap `{ error }` (nothing was paid); any other `{ error }` (already-settled, receipt not saved, receipt void, outcome unknown) may mean money moved, so get the user's explicit agreement before calling again |
32
- | `check_ai_birthcert_verification` | **Free** (never pays). 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. If a prior payment is still clearing this tool **advances** it — it replays the saved receipt (never a new payment) and returns the live session once it settles, so "check back in a few minutes" genuinely progresses the flow | (none) | `{ status: "pending" | "issued" | "no_session" | "settlement_pending" | "receipt_void", verificationUrl?, expiresAt?, vcId?, vc?, cacheError?, paymentReceipt?, outcomeUnknown?, issuerRejected?, paymentInvalid?, message? }` — `settlement_pending` means a payment was made and is being followed: without `outcomeUnknown`, `issuerRejected` or `paymentInvalid` the message leads `PAYMENT SENT` and covers **two** states that the following clause tells apart — *"still being processed"* is a confirmed queued settlement (check again shortly), while *"has not been confirmed yet"* means the outcome could not be determined at all yet, so it must not be reported as progressing or as succeeded; either way the receipt is saved, nothing is paid again, and checking again later is the next step; with `outcomeUnknown: true` the outcome could not be determined at all and has been unresolved longer than `SETTLEMENT_STUCK_AFTER_MS` (24h default), so it is not coming back on its own — `stuckFor` says how long, the fee was most likely already taken, and quoting `paymentReceipt` to support is the way forward. Starting over via `clear_stuck_payment_receipt` costs a second fee. `issuerRejected: true` is a distinct case: the credential service was reached and refused the request (a `4xx` that is not a settlement verdict — measured live as `status_code 99` "Error retrieving ZVG access token"), so the settlement is NOT what failed; `message` quotes what it said, the receipt is kept, no new payment was made, and it can clear once the service recovers. `paymentInvalid: true` is different again: the service has specifically ruled the payment or receipt invalid — this one IS about the payment. Both say nothing about whether the fee was taken, in either direction. `receipt_void` is terminal and different: SSIVC has ruled the receipt finished (status_code 67 expired / 68 failed), so checking again cannot help. The wallet **discards the dead receipt itself** here — no confirmation needed, because a receipt the server will never honour protects nothing and only blocks the next purchase — and returns it as `discardedPaymentReceipt`. It does NOT mean the fee was refunded, and buying the credential means calling `request_ai_birthcert_verification` again, which pays the fee again |
33
- | `clear_stuck_payment_receipt` | **Free, last resort, destructive.** Discard a stuck Verified AI Birthcert payment receipt the wallet is holding and refusing to pay past — the supported replacement for deleting `<stateDir>/ssivc-session.json` by hand, which a hosted subscriber cannot do. Two steps by construction: with no argument it clears nothing and returns the receipt id plus a warning; clearing requires echoing that exact id back as `confirmReceiptId`, and a mismatch clears nothing. If the settlement completed in between — which is what happens when you follow the advice to call `check_ai_birthcert_verification` first — the receipt now belongs to a **live, paid-for session**, and the id alone no longer clears it: the tool hands back `sessionId` and `verificationUrl` instead, and only `confirmDiscardLiveSession: true` will discard them. The discarded payment is unrecoverable | `{ confirmReceiptId?, confirmDiscardLiveSession? }` | `{ cleared, paymentReceipt?, requiresConfirmation?, sessionId?, verificationUrl?, message?, error? }` |
31
+ | `get_policy_template_schema` | **Free** read of a POLICY template's declared attribute vocabulary. This is the only vocabulary that means anything on chain — the policy contract validates nothing, so an attribute outside this list deploys cleanly and then enforces nothing at all. Accepts `{ publisher, policyKey }` or `{ templateId }`. A template that cannot be read reports `{ error }` rather than `{ found: false }`, so "no such template" is never confused with "could not look it up" | `{ templateId?, publisher?, policyKey? }` | `{ found, attributes?, error? }` |
32
+ | `get_my_policy` | **Free** read of the spending policies this owner has deployed on chain; defaults to this wallet's own address. Costs 2 + N chain calls and warns above 50 keys. An owner who has never deployed one is a normal absence, **not** an error — the policy contract is created lazily on first write — and a failed lookup keeps its own error state, so "we could not list your policies" is never presented as "you have none" | `{ owner? }` | `{ owner, policies?, warning?, error? }` |
33
+ | `policy_preflight` | **Free** check of a draft policy *before* it is deployed. Two questions: is it well-formed (`blockers` lists every problem at once, so one round of fixes is enough), and — more important — does it MEAN what the user thinks (`interpretation` states in plain words what it actually does). Always show `interpretation`, **including when `ready` is true**: a valid policy can still mean something unintended — a spending cap with no window is a LIFETIME cap, not a monthly one. `notChecked` lists what could not be verified, including whether the policy would be enforced at all | `{ policyKey, attributes }` | `{ ready, interpretation, blockers, notChecked }` |
34
+ | `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?, discardStuckReceiptAndPayFresh? }` | `{ sessionId, verificationUrl, expiresAt, expiresIn, expiresInSeconds }` (`expiresIn` / `expiresInSeconds` are worked out by the wallet from its own clock, so an agent quotes `expiresIn` instead of doing timezone arithmetic itself), `{ quote: { asset, maxAmountRequired, payTo?, gasModel } }`, or — instead of a session — `{ settlementPending: true, paymentReceipt, message }` after ~90s, whose meaning depends on which flag (if any) is set below: never assume the payment succeeded from this shape alone. While a merely-stuck receipt (outcome still unresolved) is held this tool can only REPLAY it — it never buys a new credential, which is what stops a second charge. A credential-service refusal is different again: `issuerRejected: true` means the service was reached and refused the request (the settlement is NOT what failed); `paymentInvalid: true` means the service specifically ruled the payment or receipt invalid (this one IS about the payment). Neither tells you whether the fee was taken, in either direction. A **void** receipt (SSIVC status_code 67/68) is different again: the wallet discards it itself, with no confirmation and no flag, so the very next call is an ordinary fresh purchase — see `{ error }` below. `discardStuckReceiptAndPayFresh: "<exact receipt id>"` is the way to start over on a merely-stuck receipt instead: it throws that payment away and pays the fee again, refuses a mismatched id (discarding and paying nothing), refuses a receipt that belongs to a live session, **refuses a receipt that is not stuck yet** (younger than `SETTLEMENT_STUCK_AFTER_MS`, 24h by default — enforced by the wallet, so an agent that mistakes a bare "retry" for consent still cannot cause a second payment; nothing is discarded and nothing is paid), and returns `discardedPaymentReceipt` so the lost payment stays traceable. `{ error }` (`RequestVerificationFailure`) also carries `discardedPaymentReceipt` when this call itself discarded a dead receipt — that confirmed stuck-receipt discard, or an unprompted void one, or **both on one call**; it also rides on `settlementPending` results, not only successes. When both happen (the confirmed discard, then the fresh payment ruled void) the field holds the **void** receipt and the id the user confirmed is named at the end of the `error`/`message` text ("ALSO DISCARDED earlier on this same call") — quote both ids to support, not just the field. Retrying is fine only for an insufficient-funds or payment-cap `{ error }` (nothing was paid); any other `{ error }` (already-settled, receipt not saved, receipt void, outcome unknown) may mean money moved, so get the user's explicit agreement before calling again |
35
+ | `check_ai_birthcert_verification` | **Free** (never pays). 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` and the wallet's own `expiresIn` / `expiresInSeconds` (time left, worked out from the wallet clock, never from the agent's) — 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. If a prior payment is still clearing this tool **advances** it — it replays the saved receipt (never a new payment) and returns the live session once it settles, so "check back in a few minutes" genuinely progresses the flow | (none) | `{ status: "pending" | "issued" | "no_session" | "settlement_pending" | "receipt_void", verificationUrl?, expiresAt?, vcId?, vc?, cacheError?, paymentReceipt?, outcomeUnknown?, issuerRejected?, paymentInvalid?, message? }` — `settlement_pending` means a payment was made and is being followed: without `outcomeUnknown`, `issuerRejected` or `paymentInvalid` the message leads `PAYMENT SENT` and covers **two** states that the following clause tells apart — *"still being processed"* is a confirmed queued settlement (check again shortly), while *"has not been confirmed yet"* means the outcome could not be determined at all yet, so it must not be reported as progressing or as succeeded; either way the receipt is saved, nothing is paid again, and checking again later is the next step; with `outcomeUnknown: true` the outcome could not be determined at all and has been unresolved longer than `SETTLEMENT_STUCK_AFTER_MS` (24h default), so it is not coming back on its own — `stuckFor` says how long, the fee was most likely already taken, and quoting `paymentReceipt` to support is the way forward. Starting over via `clear_stuck_payment_receipt` costs a second fee. `issuerRejected: true` is a distinct case: the credential service was reached and refused the request (a `4xx` that is not a settlement verdict — measured live as `status_code 99` "Error retrieving ZVG access token"), so the settlement is NOT what failed; `message` quotes what it said, the receipt is kept, no new payment was made, and it can clear once the service recovers. `paymentInvalid: true` is different again: the service has specifically ruled the payment or receipt invalid — this one IS about the payment. Both say nothing about whether the fee was taken, in either direction. `receipt_void` is terminal and different: SSIVC has ruled the receipt finished (status_code 67 expired / 68 failed), so checking again cannot help. The wallet **discards the dead receipt itself** here — no confirmation needed, because a receipt the server will never honour protects nothing and only blocks the next purchase — and returns it as `discardedPaymentReceipt`. It does NOT mean the fee was refunded, and buying the credential means calling `request_ai_birthcert_verification` again, which pays the fee again |
36
+ | `clear_stuck_payment_receipt` | **Free, last resort, destructive.** Discard a stuck Verified AI Birthcert payment receipt the wallet is holding and refusing to pay past — the supported replacement for deleting `<stateDir>/ssivc-session.json` by hand, which a hosted subscriber cannot do. Two steps by construction: with no argument it clears nothing and returns the receipt id plus a warning; clearing requires echoing that exact id back as `confirmReceiptId`, and a mismatch clears nothing. A receipt younger than `SETTLEMENT_STUCK_AFTER_MS` (24h by default) is refused on both steps. If the settlement completed in between — which is what happens when you follow the advice to call `check_ai_birthcert_verification` first — the receipt now belongs to a **live, paid-for session**, and the id alone no longer clears it: the tool hands back `sessionId` and `verificationUrl` instead, and only `confirmDiscardLiveSession: true` will discard them. The discarded payment is unrecoverable | `{ confirmReceiptId?, confirmDiscardLiveSession? }` | `{ cleared, paymentReceipt?, requiresConfirmation?, sessionId?, verificationUrl?, message?, error? }` |
34
37
 
35
38
  > **VCs are cached locally**, keyed by `templateId`, under `~/.agentic-wallet-mcp/vc-cache/`
36
39
  > (scoped per network + holder — different identities or networks never share a cache).
@@ -185,7 +188,8 @@ Node ≥ 18 required (built-in `fetch`).
185
188
  | `GAS_PREFERENCE` | no | `sponsored` or `self` — the default gas payer for the Verified AI Birthcert flow (`request_ai_birthcert_verification`). **Defaults to `sponsored`; any unrecognised value also falls back to `sponsored`** rather than throwing. Sponsorship only actually engages when the resource server's x402 quote offers a sponsored option (`extra.gasModel: "facilitator"`) — a server that only quotes self-pay behaves unchanged. **Sponsorship is testnet-only today**; override per-call with the tool's `gasPayer` parameter |
186
189
  | `MAX_SETTLEMENT_ATTEMPTS` | no | Cap on retry attempts while polling a sponsored payment for settlement after SSIVC returns `202 Accepted` (queued). **Defaults to `20`; a non-positive or unparseable value also falls back to `20`** |
187
190
  | `SETTLEMENT_WAIT_BUDGET_MS` | no | Total time `request_ai_birthcert_verification` **and** `check_ai_birthcert_verification` will each wait for a queued settlement before returning `{ settlementPending: true }` / `{ status: "settlement_pending" }` — `check_` advances the settlement too, so it can block for this long. The attempt cap alone cannot bound this, because the delay between attempts is server-supplied. At least one poll always happens, even if the budget is shorter than the first retry delay. Not capped at the top end — raising it *and* `MAX_SETTLEMENT_ATTEMPTS` together can reinstate the long blocking call this budget exists to prevent, so raise it deliberately and temporarily. **Defaults to `90000` (90s); a non-positive or unparseable value also falls back to `90000`** |
188
- | `SETTLEMENT_STUCK_AFTER_MS` | no | How long a settlement SSIVC cannot confirm may stay unresolved before `check_ai_birthcert_verification` stops calling it "still settling" and calls it permanently stuck. SSIVC returns the same `status_code 69` for a settlement two minutes old and one three weeks old, so the receipt's own age is what separates *"payment sent, check back shortly"* from *"this is not coming back, the fee was most likely already taken, and starting over costs a second fee"*. This changes **only the wording**: either side of the line the wallet never pays again and never discards the receipt by itself. **Defaults to `86400000` (24h); a non-positive or unparseable value also falls back to `86400000`** |
191
+ | `SETTLEMENT_STUCK_AFTER_MS` | no | How long a settlement SSIVC cannot confirm may stay unresolved before `check_ai_birthcert_verification` stops calling it "still settling" and calls it permanently stuck. SSIVC returns the same `status_code 69` for a settlement two minutes old and one three weeks old, so the receipt's own age is what separates *"payment sent, check back shortly"* from *"this is not coming back, the fee was most likely already taken, and starting over costs a second fee"*. It also decides when starting over is allowed: a receipt younger than this is **refused** by both discard routes (`discardStuckReceiptAndPayFresh` and `clear_stuck_payment_receipt`), with nothing discarded and nothing paid, so a very low value makes the second-fee path available after a shorter wait for a settlement that may still resolve. Lower it only deliberately, for an operator or a test. Either side of the line the wallet never pays again or discards the receipt by itself. **Defaults to `86400000` (24h); a non-positive or unparseable value also falls back to `86400000`** |
192
+ | `SSIVC_TRACE` | no | Set to exactly `1` to print every request to and response from SSIVC (headers, body, status) to **stderr**, for diagnosing a settlement that will not resolve. Off by default. **Reproduction only:** the trace contains the payment receipt, which is a bearer handle on a real payment, plus the request signature — never leave it on, never ship it to a log pipeline. Nothing is ever written to stdout. |
189
193
 
190
194
  \* sensitive — never logged, never returned in a tool result, and never a tool parameter.
191
195
 
@@ -12552,7 +12552,7 @@ var import_node_path7 = require("node:path");
12552
12552
  // package.json
12553
12553
  var package_default = {
12554
12554
  name: "agentic-wallet-mcp",
12555
- version: "0.12.1",
12555
+ version: "0.12.2",
12556
12556
  description: "Agent-facing MCP wallet for Zetrix \u2014 orchestrates x401 identity proof, x402 payment, and MBI VC issuance",
12557
12557
  keywords: [
12558
12558
  "mcp",
@@ -21729,10 +21729,14 @@ function loadConfig(env) {
21729
21729
  }
21730
21730
  return budget;
21731
21731
  })(),
21732
- // Not clamped and not warned about: unlike the wait budget, nothing blocks on this —
21733
- // it only decides which of two true statements the user is told about a receipt SSIVC will not
21734
- // resolve. Setting it very low makes the wallet give up on the wording early (it still never
21735
- // pays again or discards anything on its own); very high just means it keeps saying "check back".
21732
+ // Not clamped and not warned about: unlike the wait budget, nothing blocks on this. But it is NOT
21733
+ // just wording any more. It decides when a receipt counts as stuck, and a receipt that is not stuck
21734
+ // yet is REFUSED by both discard routes (discardStuckReceiptAndPayFresh and
21735
+ // clear_stuck_payment_receipt) — nothing discarded, nothing paid. So a very LOW value makes the
21736
+ // discard-and-pay-a-second-fee path available sooner, after a shorter wait for a settlement that may
21737
+ // still resolve. The default (24h) is the safe setting; lower it only deliberately, for an operator
21738
+ // or a test. It is not agent-reachable. Very high just means the wallet keeps saying "check back"
21739
+ // and never offers a fresh start. The wallet never pays again or discards on its own either way.
21736
21740
  settlementStuckAfterMs: (() => {
21737
21741
  const ms = Number(env.SETTLEMENT_STUCK_AFTER_MS);
21738
21742
  return Number.isInteger(ms) && ms > 0 ? ms : 864e5;
@@ -24013,6 +24017,7 @@ async function buildToolContent(result, readFile6) {
24013
24017
  var SETTLEMENT_UNCONFIRMED_STATUS_CODE = "69";
24014
24018
  var SETTLEMENT_EXPIRED_STATUS_CODE = "67";
24015
24019
  var SETTLEMENT_FAILED_STATUS_CODE = "68";
24020
+ var AGENT_NAME_IN_USE_STATUS_CODE = "26";
24016
24021
  var SsivcError = class extends Error {
24017
24022
  httpStatus;
24018
24023
  /** SSIVC's own `status_code` string (distinct from the HTTP status) — e.g. "50" signature invalid, "55" expired timestamp, "23" not found. */
@@ -24029,6 +24034,31 @@ var SsivcError = class extends Error {
24029
24034
  };
24030
24035
  var DEFAULT_RETRY_AFTER_SECONDS = 15;
24031
24036
  var SESSIONS_PATH = "/v2/verify/ai-birthcert/sessions";
24037
+ var traceEnabled = () => process.env.SSIVC_TRACE === "1";
24038
+ function traceRequest(method, url, headers, body) {
24039
+ if (!traceEnabled()) return;
24040
+ try {
24041
+ const lines = [`[ssivc-trace] --> ${method} ${url}`];
24042
+ for (const [k, v] of Object.entries(headers)) lines.push(`[ssivc-trace] --> ${k}: ${v}`);
24043
+ if (body !== void 0) lines.push(`[ssivc-trace] --> body: ${JSON.stringify(body)}`);
24044
+ process.stderr.write(lines.join("\n") + "\n");
24045
+ } catch {
24046
+ }
24047
+ }
24048
+ async function traceResponse(method, url, res) {
24049
+ if (!traceEnabled()) return;
24050
+ try {
24051
+ const text = await res.clone().text();
24052
+ const receipt = res.headers.get("x-payment-response");
24053
+ process.stderr.write(
24054
+ `[ssivc-trace] <-- ${res.status} ${method} ${url}
24055
+ ` + (receipt ? `[ssivc-trace] <-- X-Payment-Response: ${receipt}
24056
+ ` : "") + `[ssivc-trace] <-- body: ${text}
24057
+ `
24058
+ );
24059
+ } catch {
24060
+ }
24061
+ }
24032
24062
  var SsivcClient = class {
24033
24063
  baseUrl;
24034
24064
  constructor(baseUrl) {
@@ -24085,10 +24115,15 @@ var SsivcClient = class {
24085
24115
  fetch(method, path, body, extraHeaders) {
24086
24116
  const headers = { Accept: "application/json", ...extraHeaders ?? {} };
24087
24117
  if (body !== void 0) headers["Content-Type"] = "application/json";
24088
- return fetch(`${this.baseUrl}${path}`, {
24118
+ const url = `${this.baseUrl}${path}`;
24119
+ traceRequest(method, url, headers, body);
24120
+ return fetch(url, {
24089
24121
  method,
24090
24122
  headers,
24091
24123
  ...body !== void 0 ? { body: JSON.stringify(body) } : {}
24124
+ }).then(async (res) => {
24125
+ await traceResponse(method, url, res);
24126
+ return res;
24092
24127
  }).catch((e) => {
24093
24128
  throw new SsivcError(`SSIVC ${path} request failed: ${e.message}`);
24094
24129
  });
@@ -24104,7 +24139,8 @@ var SsivcClient = class {
24104
24139
  const j = JSON.parse(text);
24105
24140
  statusCode = j.status_code;
24106
24141
  msg = j.errors?.length ? j.errors.join("; ") : j.message ?? j.error ?? text;
24107
- if (res.status === 402 && j.error === "payment_invalid") kind = "payment_invalid";
24142
+ if (res.status === 409 && statusCode === AGENT_NAME_IN_USE_STATUS_CODE) kind = "agent_name_in_use";
24143
+ else if (res.status === 402 && j.error === "payment_invalid") kind = "payment_invalid";
24108
24144
  else if (res.status === 503 && j.error === "facilitator_unavailable") kind = "facilitator_unavailable";
24109
24145
  else if (res.status === 400 && statusCode === SETTLEMENT_UNCONFIRMED_STATUS_CODE) kind = "settlement_unconfirmed";
24110
24146
  else if (res.status === 400 && (statusCode === SETTLEMENT_EXPIRED_STATUS_CODE || statusCode === SETTLEMENT_FAILED_STATUS_CODE)) {
@@ -24211,6 +24247,33 @@ function canonicalizeJson(value) {
24211
24247
  return JSON.stringify(value);
24212
24248
  }
24213
24249
 
24250
+ // src/orchestrator/session-expiry.ts
24251
+ function humanRemaining(seconds) {
24252
+ if (seconds <= 0) return "already expired";
24253
+ if (seconds < 60) return "less than a minute";
24254
+ const minutes = Math.round(seconds / 60);
24255
+ if (minutes < 60) return `about ${minutes} minute${minutes === 1 ? "" : "s"}`;
24256
+ const hours = Math.round(seconds / 3600);
24257
+ if (hours < 48) return `about ${hours} hour${hours === 1 ? "" : "s"}`;
24258
+ const days = Math.round(seconds / 86400);
24259
+ return `about ${days} days`;
24260
+ }
24261
+ function sessionExpiryFields(expiresAt, now) {
24262
+ if (typeof expiresAt !== "string") return null;
24263
+ const at = Date.parse(expiresAt);
24264
+ if (Number.isNaN(at)) return null;
24265
+ const seconds = Math.max(Math.floor((at - now.getTime()) / 1e3), 0);
24266
+ return { expiresInSeconds: seconds, expiresIn: humanRemaining(seconds) };
24267
+ }
24268
+ function withSessionExpiry(result, now) {
24269
+ if (typeof result !== "object" || result === null) return result;
24270
+ const r = result;
24271
+ if (typeof r.expiresAt !== "string") return result;
24272
+ if (typeof r.verificationUrl !== "string" && typeof r.status !== "string") return result;
24273
+ const fields = sessionExpiryFields(r.expiresAt, now);
24274
+ return fields ? { ...r, ...fields } : result;
24275
+ }
24276
+
24214
24277
  // src/orchestrator/verify-ai-birthcert.ts
24215
24278
  function subjectMatches(vc, holderDid) {
24216
24279
  if (typeof vc !== "object" || vc === null) return false;
@@ -24280,9 +24343,14 @@ var DEFAULT_SETTLEMENT_STUCK_AFTER_MS = 864e5;
24280
24343
  function settlementAge(stored, now, stuckAfterMs) {
24281
24344
  const created = stored?.createdAt ? new Date(stored.createdAt).getTime() : NaN;
24282
24345
  const ageMs = Number.isNaN(created) ? 0 : Math.max(0, now.getTime() - created);
24283
- const hours = ageMs / 36e5;
24284
- const humanAge = hours < 1 ? `${Math.round(ageMs / 6e4)} minutes` : hours < 48 ? `${Math.round(hours)} hours` : `${Math.round(hours / 24)} days`;
24285
- return { ageMs, stuck: ageMs > stuckAfterMs, humanAge };
24346
+ return { ageMs, stuck: ageMs > stuckAfterMs, humanAge: humanDuration(ageMs) };
24347
+ }
24348
+ function humanDuration(ms) {
24349
+ const hours = ms / 36e5;
24350
+ return hours < 1 ? `${Math.round(ms / 6e4)} minutes` : hours < 48 ? `${Math.round(hours)} hours` : `${Math.round(hours / 24)} days`;
24351
+ }
24352
+ function notStuckYetMessage(receipt, humanAge, stuckAfterMs) {
24353
+ return `NOT STUCK YET \u2014 nothing was discarded and NOTHING WAS PAID. Receipt ${receipt} is only ${humanAge} old, and a settlement can still resolve after that long: the credential service may simply not have re-checked it. Discarding it now would forfeit a payment that may already have gone through and charge a SECOND fee. Do not offer that option to the user. Call check_ai_birthcert_verification to follow the receipt instead; starting over only becomes available once it has been unresolved for more than ${humanDuration(stuckAfterMs)} (SETTLEMENT_STUCK_AFTER_MS). A bare "retry" or "yes" from the user is not agreement to pay again.`;
24286
24354
  }
24287
24355
  var FALLBACK_RETRY_DELAY_MS = 15e3;
24288
24356
  function retryDelayMs(retryAfterSeconds) {
@@ -24340,19 +24408,41 @@ function isIssuerRejection(err) {
24340
24408
  "settlement_unconfirmed",
24341
24409
  "settlement_void",
24342
24410
  "blob_already_settled",
24411
+ "agent_name_in_use",
24343
24412
  "payment_invalid"
24344
24413
  ];
24345
24414
  return !notARejection.includes(err.kind);
24346
24415
  }
24416
+ function isAgentNameInUse(err) {
24417
+ return err instanceof SsivcError && err.kind === "agent_name_in_use";
24418
+ }
24419
+ function agentNameInUseMessage(err, agentName, paidThisCall, receipt) {
24420
+ const code = err.statusCode ? ` (status_code ${err.statusCode})` : "";
24421
+ return `AGENT NAME ALREADY IN USE${code}: "${quotedServiceReason(err)}". The credential service refused agentName "${agentName}" because it is already taken, so this request will never succeed with that name \u2014 do NOT retry it with the same name. Ask the user for a different agentName. This is a verdict about the NAME only: it does NOT establish whether a fee was taken, in either direction, so never tell the user they were charged or that they were not, and never describe it as a settled payment. ` + (paidThisCall ? `THIS CALL ITSELF SENT A PAYMENT before the refusal, so a fee may have been spent on this attempt` : `No new payment was made by this call`) + (receipt ? ` (receipt ${receipt} has been KEPT)` : ``) + `. ` + (paidThisCall ? `A new request with a different agentName pays its own fee, so get the user's agreement first. ` : `Do NOT pay again for this name. `) + `If the user asks about a charge, quote ${receipt ? "the receipt" : "the agentName"} and this message to support.`;
24422
+ }
24347
24423
  function isPaymentInvalidRejection(err) {
24348
24424
  return err instanceof SsivcError && err.kind === "payment_invalid";
24349
24425
  }
24350
24426
  function quotedServiceReason(err) {
24351
- const stripped = err.message.replace(/^.*?HTTP \d+:\s*/, "") || err.message;
24352
- const oneLine = stripped.replace(/[\u0000-\u001F\u007F]+/g, " ").replace(/["'`]/g, "").replace(/\s+/g, " ").trim();
24427
+ return boundReason(err.message.replace(/^.*?HTTP \d+:\s*/, "") || err.message);
24428
+ }
24429
+ function boundReason(raw) {
24430
+ const oneLine = raw.replace(/[\u0000-\u001F\u007F]+/g, " ").replace(/["'`]/g, "").replace(/\s+/g, " ").trim();
24353
24431
  if (!oneLine) return "no reason given";
24354
24432
  return oneLine.length > 300 ? `${oneLine.slice(0, 300)}\u2026` : oneLine;
24355
24433
  }
24434
+ function ssivcAnswerNote(cause) {
24435
+ if (cause instanceof SsivcError && cause.httpStatus !== void 0) {
24436
+ const code = cause.statusCode ? `, status_code ${cause.statusCode}` : "";
24437
+ return ` SSIVC's answer was HTTP ${cause.httpStatus}${code}: "${quotedServiceReason(cause)}".`;
24438
+ }
24439
+ if (cause instanceof Error) {
24440
+ return ` The call to SSIVC itself failed before any answer: "${boundReason(cause.message)}".`;
24441
+ }
24442
+ return "";
24443
+ }
24444
+ var TELL_USER_UNCONFIRMED = ` Tell the user: "Your payment was sent, but the credential service has not confirmed it yet, so I cannot say whether it went through. The receipt is saved \u2014 please do not pay again. I will check again shortly."`;
24445
+ var TELL_USER_QUEUED = ` Tell the user: "Your payment was sent and the settlement is still being processed. The receipt is saved \u2014 please do not pay again. I will check again shortly."`;
24356
24446
  function issuerRejectionMessage(err, receipt, humanAge, paidThisCall) {
24357
24447
  const code = err.statusCode ? ` (status_code ${err.statusCode})` : "";
24358
24448
  return `PAYMENT SENT, BUT THE CREDENTIAL SERVICE REFUSED THE REQUEST${code}: "${quotedServiceReason(err)}". That refusal is about the request, not about the settlement \u2014 do not tell the user the payment is still processing, because processing is not what failed. ${humanAge} in, receipt ${receipt} has been KEPT` + (paidThisCall ? `, and THIS CALL ITSELF SENT A PAYMENT before the refusal \u2014 so a fee may have been spent on this attempt. It is still ` : ` and no new payment was made, so this is still `) + `recoverable and a refusal can clear once the service recovers \u2014 call check_ai_birthcert_verification again later. It does NOT establish whether the fee was taken: never tell the user they were not charged. Do NOT pay again. If it keeps repeating, quote the receipt and this message to support.`;
@@ -24395,7 +24485,7 @@ async function resolveSettlement(deps, buildBody, initialOutcome, onQueued) {
24395
24485
  }
24396
24486
  if (outcome.kind === "queued") {
24397
24487
  throw new SettlementStillQueuedError(
24398
- "PAYMENT SENT \u2014 the sponsored settlement is still being processed. Nothing has gone wrong and no funds are lost: the receipt has been saved. Call check_ai_birthcert_verification in a few minutes to follow it through. Do not pay again.",
24488
+ "PAYMENT SENT \u2014 the sponsored settlement is still being processed. Nothing has gone wrong and no funds are lost: the receipt has been saved. Call check_ai_birthcert_verification in a few minutes to follow it through. Do not pay again." + TELL_USER_QUEUED,
24399
24489
  outcome.paymentReceipt
24400
24490
  );
24401
24491
  }
@@ -24490,6 +24580,9 @@ async function payAndCreateSession(deps, buildBody, onQueued) {
24490
24580
  }
24491
24581
  }
24492
24582
  async function requestAiBirthcertVerification(deps, input) {
24583
+ return withSessionExpiry(await requestAiBirthcertVerificationInner(deps, input), deps.now());
24584
+ }
24585
+ async function requestAiBirthcertVerificationInner(deps, input) {
24493
24586
  if (!input.agentName || !input.agentName.trim()) {
24494
24587
  throw new Error("requestAiBirthcertVerification: agentName is required");
24495
24588
  }
@@ -24566,6 +24659,16 @@ async function discardStuckReceiptBeforePaying(deps, confirmReceiptId) {
24566
24659
  }
24567
24660
  };
24568
24661
  }
24662
+ const stuckAfterMs = deps.settlementStuckAfterMs ?? DEFAULT_SETTLEMENT_STUCK_AFTER_MS;
24663
+ const age = settlementAge(stored, deps.now(), stuckAfterMs);
24664
+ if (!age.stuck) {
24665
+ return {
24666
+ refusal: {
24667
+ paymentReceipt: stored.paymentReceipt,
24668
+ error: notStuckYetMessage(stored.paymentReceipt, age.humanAge, stuckAfterMs)
24669
+ }
24670
+ };
24671
+ }
24569
24672
  await deps.sessionStore.clear();
24570
24673
  return { discarded: stored.paymentReceipt };
24571
24674
  }
@@ -24667,6 +24770,9 @@ async function payOrReplayLocked(deps, agentName, input) {
24667
24770
  }
24668
24771
  return { settlementPending: true, paymentReceipt: err.paymentReceipt, message: err.message };
24669
24772
  }
24773
+ if (isAgentNameInUse(err)) {
24774
+ return { error: agentNameInUseMessage(err, agentName, paidThisCall) };
24775
+ }
24670
24776
  if (err instanceof SsivcError && err.kind === "blob_already_settled") {
24671
24777
  return {
24672
24778
  error: `PAYMENT ALREADY SETTLED FOR THIS ATTEMPT \u2014 do not retry, do not assume it failed. SSIVC says this exact payment attempt was already settled once before (${err.message}), most likely because a prior call's success response was lost before we could save its receipt. The fee was most likely already taken for agentName "${agentName}". Calling this tool again pays the fee AGAIN, so do NOT retry without the user's explicit agreement \u2014 a human may need to check on the SSIVC side whether this agentName already has a session before anyone pays again.`
@@ -24689,6 +24795,14 @@ async function payOrReplayLocked(deps, agentName, input) {
24689
24795
  deps.now(),
24690
24796
  deps.settlementStuckAfterMs ?? DEFAULT_SETTLEMENT_STUCK_AFTER_MS
24691
24797
  );
24798
+ if (isAgentNameInUse(err.cause)) {
24799
+ return {
24800
+ paymentReceipt: err.paymentReceipt,
24801
+ settlementPending: true,
24802
+ issuerRejected: true,
24803
+ message: agentNameInUseMessage(err.cause, agentName, paidThisCall, err.paymentReceipt)
24804
+ };
24805
+ }
24692
24806
  if (isPaymentInvalidRejection(err.cause)) {
24693
24807
  return {
24694
24808
  paymentReceipt: err.paymentReceipt,
@@ -24716,7 +24830,7 @@ async function payOrReplayLocked(deps, agentName, input) {
24716
24830
  // this very call with an outcome nobody knows yet, so "nothing is lost" sitting against that
24717
24831
  // admission reads as reassurance the wallet cannot give. The replay half keeps it: there
24718
24832
  // nothing WAS spent on this call, which is what makes the phrase true.
24719
- message: paidThisCall ? `PAYMENT SENT \u2014 the settlement has not been confirmed yet, ${age.humanAge} in. This call itself sent the payment; whether it settled is not yet known. Receipt ${err.paymentReceipt} is saved. Do NOT pay again. Call check_ai_birthcert_verification in a few minutes to follow it through.` : `PAYMENT SENT \u2014 the settlement has not been confirmed yet, ${age.humanAge} in. Nothing is lost: receipt ${err.paymentReceipt} is saved and no new payment was attempted. Do NOT pay again. Call check_ai_birthcert_verification in a few minutes to follow it through.`
24833
+ message: paidThisCall ? `PAYMENT SENT \u2014 the settlement has not been confirmed yet, ${age.humanAge} in. This call itself sent the payment; whether it settled is not yet known. Receipt ${err.paymentReceipt} is saved. Do NOT pay again. Call check_ai_birthcert_verification in a few minutes to follow it through.` + ssivcAnswerNote(err.cause) + TELL_USER_UNCONFIRMED : `PAYMENT SENT \u2014 the settlement has not been confirmed yet, ${age.humanAge} in. The outcome is not known yet, so do not tell the user the payment succeeded or that it failed. Receipt ${err.paymentReceipt} is saved and no new payment was attempted. Do NOT pay again. Call check_ai_birthcert_verification in a few minutes to follow it through.` + ssivcAnswerNote(err.cause) + TELL_USER_UNCONFIRMED
24720
24834
  };
24721
24835
  }
24722
24836
  return {
@@ -24754,7 +24868,7 @@ async function advanceQueuedSettlement(deps, stored) {
24754
24868
  await discardVoidReceipt(deps);
24755
24869
  return voidReceiptOutcome(stored.paymentReceipt, err.message);
24756
24870
  }
24757
- return unknownSettlementOutcome(stored.paymentReceipt, err, void 0, settlementAge(stored, deps.now(), deps.settlementStuckAfterMs ?? DEFAULT_SETTLEMENT_STUCK_AFTER_MS));
24871
+ return unknownSettlementOutcome(stored.paymentReceipt, err, void 0, settlementAge(stored, deps.now(), deps.settlementStuckAfterMs ?? DEFAULT_SETTLEMENT_STUCK_AFTER_MS), void 0, stored.agentName);
24758
24872
  }
24759
24873
  let settled;
24760
24874
  try {
@@ -24774,7 +24888,7 @@ async function advanceQueuedSettlement(deps, stored) {
24774
24888
  return voidReceiptOutcome(err.paymentReceipt, err.message);
24775
24889
  }
24776
24890
  if (err instanceof SettlementOutcomeUnknownError) {
24777
- return unknownSettlementOutcome(err.paymentReceipt, void 0, err.message, settlementAge(stored, deps.now(), deps.settlementStuckAfterMs ?? DEFAULT_SETTLEMENT_STUCK_AFTER_MS), err.cause);
24891
+ return unknownSettlementOutcome(err.paymentReceipt, void 0, err.message, settlementAge(stored, deps.now(), deps.settlementStuckAfterMs ?? DEFAULT_SETTLEMENT_STUCK_AFTER_MS), err.cause, stored.agentName);
24778
24892
  }
24779
24893
  throw err;
24780
24894
  }
@@ -24808,8 +24922,19 @@ function voidReceiptOutcome(paymentReceipt, detail) {
24808
24922
  message: `RECEIPT VOID \u2014 this payment can no longer be used and no credential was issued. The payment service has ruled on it: ${detail}. Checking again will not change this. The dead receipt has been discarded, so nothing is blocking a fresh purchase. Whether the fee was actually taken is NOT settled by this \u2014 a settlement can expire after the money has moved \u2014 so do not tell the user they were not charged; give them receipt ${paymentReceipt} to quote to support. If they want the credential, call request_ai_birthcert_verification again and it will pay normally \u2014 that is THE FEE AGAIN, so ask them first rather than calling it on their behalf.`
24809
24923
  };
24810
24924
  }
24811
- function unknownSettlementOutcome(paymentReceipt, err, message, age, cause) {
24925
+ function unknownSettlementOutcome(paymentReceipt, err, message, age, cause, agentName) {
24812
24926
  const detail = message ?? unresolvedCause(err);
24927
+ const nameInUse = [cause, err].find(isAgentNameInUse);
24928
+ if (nameInUse) {
24929
+ return {
24930
+ status: "settlement_pending",
24931
+ paymentReceipt,
24932
+ issuerRejected: true,
24933
+ ...age?.stuck ? { stuckFor: age.humanAge } : {},
24934
+ // `false`: this helper only serves check_, whose deps have no `pay` (R10-M01).
24935
+ message: agentNameInUseMessage(nameInUse, agentName ?? "the requested name", false, paymentReceipt)
24936
+ };
24937
+ }
24813
24938
  const rejection = [cause, err].find(isIssuerRejection);
24814
24939
  const paymentInvalid = [cause, err].find(isPaymentInvalidRejection);
24815
24940
  if (paymentInvalid) {
@@ -24837,7 +24962,7 @@ function unknownSettlementOutcome(paymentReceipt, err, message, age, cause) {
24837
24962
  return {
24838
24963
  status: "settlement_pending",
24839
24964
  paymentReceipt,
24840
- message: `PAYMENT SENT \u2014 the settlement has not been confirmed yet, ${age.humanAge} in. Nothing has gone wrong and no funds are lost: receipt ${paymentReceipt} is saved. Do NOT pay again. Call check_ai_birthcert_verification again in a few minutes to follow it through.`
24965
+ message: `PAYMENT SENT \u2014 the settlement has not been confirmed yet, ${age.humanAge} in. The outcome is not known yet, so do not tell the user the payment succeeded or that it failed. Receipt ${paymentReceipt} is saved. Do NOT pay again. Call check_ai_birthcert_verification again in a few minutes to follow it through.` + ssivcAnswerNote(cause ?? err) + TELL_USER_UNCONFIRMED
24841
24966
  };
24842
24967
  }
24843
24968
  return {
@@ -24859,6 +24984,17 @@ async function clearStuckPaymentReceiptLocked(deps, input) {
24859
24984
  return { cleared: false, message: "Nothing to clear \u2014 this wallet has no stored verification session or payment receipt." };
24860
24985
  }
24861
24986
  const isLiveSession = stored.sessionId !== "";
24987
+ if (!isLiveSession) {
24988
+ const stuckAfterMs = deps.settlementStuckAfterMs ?? DEFAULT_SETTLEMENT_STUCK_AFTER_MS;
24989
+ const age = settlementAge(stored, deps.now(), stuckAfterMs);
24990
+ if (!age.stuck) {
24991
+ return {
24992
+ cleared: false,
24993
+ paymentReceipt: stored.paymentReceipt,
24994
+ error: notStuckYetMessage(stored.paymentReceipt, age.humanAge, stuckAfterMs)
24995
+ };
24996
+ }
24997
+ }
24862
24998
  if (input.confirmReceiptId === void 0) {
24863
24999
  return {
24864
25000
  cleared: false,
@@ -24894,6 +25030,9 @@ async function clearStuckPaymentReceiptLocked(deps, input) {
24894
25030
  };
24895
25031
  }
24896
25032
  async function checkAiBirthcertVerification(deps) {
25033
+ return withSessionExpiry(await checkAiBirthcertVerificationInner(deps), deps.now());
25034
+ }
25035
+ async function checkAiBirthcertVerificationInner(deps) {
24897
25036
  const stored = await deps.sessionStore.get();
24898
25037
  if (!stored) return noStoredSession();
24899
25038
  if (stored.sessionId === "") {
@@ -25138,7 +25277,7 @@ function buildToolList() {
25138
25277
  },
25139
25278
  {
25140
25279
  name: "request_ai_birthcert_verification",
25141
- description: 'Start a Verified AI Birthcert issuance session with myid (MyDigital ID owner verification). ALWAYS run credential_preflight for "verified_ai_birthcert" immediately before calling this, even if you checked earlier in the conversation \u2014 preflight is free, this tool spends real funds, and a balance the user topped up a minute ago is not the balance you read before that. 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. Read `message` before deciding what to tell the user or whether retrying is safe \u2014 it does NOT always mean nothing was paid. For insufficient funds or a payment-cap block specifically, nothing is created and nothing was paid, so once the underlying problem is fixed (e.g. the user tops up), calling this tool again is the right next step. Several OTHER { error } shapes mean the opposite: a payment may already have been sent on a prior or even this call (blob_already_settled, a receipt that could not be saved locally, RECEIPT VOID, OUTCOME UNKNOWN) \u2014 for any of those, calling this tool again pays the fee AGAIN, so do NOT retry without explicit agreement from the user, exactly as `message` itself will say. It can also return { settlementPending: true, paymentReceipt, message } instead \u2014 a payment WAS SENT and no session exists yet. That is NOT a failure and NOT an error, but do NOT pay again and do NOT call this tool again to retry it \u2014 call check_ai_birthcert_verification to follow it through instead. It covers three different situations, told apart by the `issuerRejected` and `paymentInvalid` fields. With neither set, do NOT assume the payment is known to have succeeded \u2014 this shape covers TWO different states that look identical from these fields alone: usually the settlement is genuinely still clearing (the message says "still being processed"), but sometimes the outcome could not be determined at all yet (the message says "has not been confirmed yet" and does not say "still being processed") \u2014 an indeterminate state, not a confirmed one, even though neither flag is set. Either way the receipt is saved and you must not pay again, so report it as "payment sent, still settling" without promising the user it definitely succeeded. With issuerRejected: true the credential service was reached and refused the request \u2014 the settlement is NOT what failed, but do NOT describe this as the payment having succeeded either: `message` quotes its reason, and you must relay that reason rather than describing it as still settling or as a success. With paymentInvalid: true the service has specifically ruled the payment or receipt invalid \u2014 do NOT say "the settlement is not what failed" for this one, since this verdict IS about the payment; relay what it said instead, and do NOT describe it as a success either. Neither this case nor issuerRejected tells you whether the fee was taken, in either direction \u2014 never tell the user they were not charged, and just as much, never tell them they WERE charged either. That no-claim rule covers issuerRejected and paymentInvalid specifically. The plain no-flag case above is different again, and NOT simply "known to have succeeded" either \u2014 see the note on it above: it is two states, only one of which is confirmed, so read `message` there too rather than assuming success from the shape alone. 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.',
25280
+ description: 'Start a Verified AI Birthcert issuance session with myid (MyDigital ID owner verification). ALWAYS run credential_preflight for "verified_ai_birthcert" immediately before calling this, even if you checked earlier in the conversation \u2014 preflight is free, this tool spends real funds, and a balance the user topped up a minute ago is not the balance you read before that. Returns { sessionId, verificationUrl, expiresAt, expiresIn, expiresInSeconds } \u2014 show verificationUrl to the human owner and ask them to open it and complete MyDigital ID verification (typically finishes in seconds). Tell them how long the link is good for by quoting `expiresIn` exactly as given: the wallet works it out from its own clock, so never calculate the time remaining yourself from `expiresAt` \u2014 your clock and timezone may differ from the server\'s. 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. Read `message` before deciding what to tell the user or whether retrying is safe \u2014 it does NOT always mean nothing was paid. For insufficient funds or a payment-cap block specifically, nothing is created and nothing was paid, so once the underlying problem is fixed (e.g. the user tops up), calling this tool again is the right next step. Several OTHER { error } shapes mean the opposite: a payment may already have been sent on a prior or even this call (blob_already_settled, a receipt that could not be saved locally, RECEIPT VOID, OUTCOME UNKNOWN) \u2014 for any of those, calling this tool again pays the fee AGAIN, so do NOT retry without explicit agreement from the user, exactly as `message` itself will say. It can also return { settlementPending: true, paymentReceipt, message } instead \u2014 a payment WAS SENT and no session exists yet. That is NOT a failure and NOT an error, but do NOT pay again and do NOT call this tool again to retry it \u2014 call check_ai_birthcert_verification to follow it through instead. It covers three different situations, told apart by the `issuerRejected` and `paymentInvalid` fields. With neither set, do NOT assume the payment is known to have succeeded \u2014 this shape covers TWO different states that look identical from these fields alone: usually the settlement is genuinely still clearing (the message says "still being processed"), but sometimes the outcome could not be determined at all yet (the message says "has not been confirmed yet" and does not say "still being processed") \u2014 an indeterminate state, not a confirmed one, even though neither flag is set. Either way the receipt is saved and you must not pay again, so report it as "payment sent, still settling" without promising the user it definitely succeeded. When the message contains a "Tell the user:" sentence, relay that sentence rather than composing your own reassurance, and if asked what the service said, quote only what the message says it answered. With issuerRejected: true the credential service was reached and refused the request \u2014 the settlement is NOT what failed, but do NOT describe this as the payment having succeeded either: `message` quotes its reason, and you must relay that reason rather than describing it as still settling or as a success. With paymentInvalid: true the service has specifically ruled the payment or receipt invalid \u2014 do NOT say "the settlement is not what failed" for this one, since this verdict IS about the payment; relay what it said instead, and do NOT describe it as a success either. Neither this case nor issuerRejected tells you whether the fee was taken, in either direction \u2014 never tell the user they were not charged, and just as much, never tell them they WERE charged either. That no-claim rule covers issuerRejected and paymentInvalid specifically. The plain no-flag case above is different again, and NOT simply "known to have succeeded" either \u2014 see the note on it above: it is two states, only one of which is confirmed, so read `message` there too rather than assuming success from the shape alone. 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.',
25142
25281
  inputSchema: {
25143
25282
  type: "object",
25144
25283
  properties: {
@@ -25161,7 +25300,7 @@ function buildToolList() {
25161
25300
  },
25162
25301
  discardStuckReceiptAndPayFresh: {
25163
25302
  type: "string",
25164
- description: 'DESTRUCTIVE, and it SPENDS. Only for a payment that is genuinely stuck (outcome still unresolved). While the wallet holds a stuck receipt this tool can only replay it \u2014 it will never buy a new credential \u2014 so this is the way to start over: it throws that payment away and pays a SECOND fee. Not needed for a VOID receipt (settlement ruled expired/failed): the wallet discards that one itself, with no flag and no confirmation, so the next call is already an ordinary fresh purchase. Pass the stuck receipt id EXACTLY as check_ai_birthcert_verification or clear_stuck_payment_receipt reported it \u2014 never a guess, never true. A mismatched id discards nothing and pays nothing. Before using it, show the user the receipt id, tell them the first payment is forfeit and that this costs the fee again, and get their explicit agreement. If the receipt turns out to belong to a live session this is refused: that session is already paid for, so call check_ai_birthcert_verification instead. Normally the result carries the id you confirmed as discardedPaymentReceipt \u2014 keep it, support needs it to trace the lost payment. But if this call ALSO threw away a void receipt of its own (the fresh payment it just made was then ruled void), discardedPaymentReceipt holds THAT receipt instead, and the id the user confirmed is named at the end of the error text ("ALSO DISCARDED earlier on this same call") \u2014 so on that path quote BOTH ids to support, not just the field. A receipt only counts as stuck once it is older than SETTLEMENT_STUCK_AFTER_MS (24h by default, e.g. SETTLEMENT_STUCK_AFTER_MS=3600000 for one hour); before that the wallet will say the settlement may still be in flight, and starting over is the user decision, not yours.'
25303
+ description: 'DESTRUCTIVE, and it SPENDS. Only for a payment that is genuinely stuck (outcome still unresolved). While the wallet holds a stuck receipt this tool can only replay it \u2014 it will never buy a new credential \u2014 so this is the way to start over: it throws that payment away and pays a SECOND fee. Not needed for a VOID receipt (settlement ruled expired/failed): the wallet discards that one itself, with no flag and no confirmation, so the next call is already an ordinary fresh purchase. Pass the stuck receipt id EXACTLY as check_ai_birthcert_verification or clear_stuck_payment_receipt reported it \u2014 never a guess, never true. A mismatched id discards nothing and pays nothing. Before using it, show the user the receipt id, tell them the first payment is forfeit and that this costs the fee again, and get their explicit agreement. If the receipt turns out to belong to a live session this is refused: that session is already paid for, so call check_ai_birthcert_verification instead. Normally the result carries the id you confirmed as discardedPaymentReceipt \u2014 keep it, support needs it to trace the lost payment. But if this call ALSO threw away a void receipt of its own (the fresh payment it just made was then ruled void), discardedPaymentReceipt holds THAT receipt instead, and the id the user confirmed is named at the end of the error text ("ALSO DISCARDED earlier on this same call") \u2014 so on that path quote BOTH ids to support, not just the field. A receipt only counts as stuck once it is older than SETTLEMENT_STUCK_AFTER_MS (24h by default, e.g. SETTLEMENT_STUCK_AFTER_MS=3600000 for one hour); before that the wallet REFUSES this parameter outright \u2014 nothing is discarded and nothing is paid \u2014 so do not offer the user this option for a receipt that is not stuck yet. A bare "retry" or "yes" from the user is not agreement to pay again.'
25165
25304
  }
25166
25305
  },
25167
25306
  required: ["agentName"]
@@ -25187,12 +25326,12 @@ function buildToolList() {
25187
25326
  },
25188
25327
  {
25189
25328
  name: "check_ai_birthcert_verification",
25190
- description: 'Check the status of the most recently requested Verified AI Birthcert session (see request_ai_birthcert_verification). FREE \u2014 it never pays for anything. 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. If a previous payment is still clearing, this tool ACTIVELY ADVANCES it \u2014 so in that one case it can take up to ~90s to return (it is waiting on the settlement, not hung; every other case returns immediately). It replays the saved receipt (never a new payment) and returns the live session once it settles, so telling the user to check back here genuinely moves things forward. While it is still clearing you get { status: "settlement_pending", paymentReceipt, message }: a payment HAS been made, so never call request_ai_birthcert_verification and never tell the user it failed. With issuerRejected: true (message leads "PAYMENT SENT, BUT THE CREDENTIAL SERVICE REFUSED THE REQUEST") the credential service was reached and refused the request, and message quotes what it said \u2014 relay that reason to the user and tell them the verification service is not completing requests right now. Do NOT describe it as a settlement still processing: the settlement is not what failed. The receipt is kept and no new payment was made, so it is worth checking again later \u2014 an outage on their side can clear. This says NOTHING about whether the fee was taken, so never tell the user they were not charged. With paymentInvalid: true (message leads "PAYMENT SENT, BUT THE CREDENTIAL SERVICE SAYS THIS PAYMENT DID NOT VALIDATE") the service has specifically ruled the payment or receipt invalid \u2014 different from issuerRejected: do NOT say "the settlement is not what failed" here, because this verdict IS about the payment. Relay what the service said, but this one says NOTHING about whether the fee was taken in EITHER direction \u2014 never tell the user they were not charged, and just as much, never tell them they WERE charged either. Do not pay again either way. stuckFor may appear alongside EITHER issuerRejected or paymentInvalid (the receipt happens to also be old) \u2014 it is just context on how long this has been retried, not a reason to change any of the advice above for either case. Otherwise, outcomeUnknown tells the remaining cases apart, and without it the message splits again. Without outcomeUnknown the message leads "PAYMENT SENT", and that shape is TWO different states you tell apart by the clause that follows. If it says "still being processed" the settlement is confirmed queued and progressing \u2014 check again in a few minutes. If it says "has not been confirmed yet" the outcome could not be determined at all yet: do NOT describe that one as progressing and do NOT describe it as succeeded, because it is indeterminate, not confirmed. Either way the receipt is saved, so do not pay again, and check again later. With outcomeUnknown: true (message leads "OUTCOME UNKNOWN") the settlement outcome could not be determined at all and has been unresolved long enough that it is not coming back (stuckFor says how long). The fee was most likely ALREADY TAKEN and no credential was issued \u2014 say that plainly rather than implying it may still land. do not just tell the user to wait; give them the paymentReceipt and tell them to quote it to support. Quote paymentReceipt to support in either case if they ask. Separately, { status: "receipt_void" } is TERMINAL: the payment service has ruled that this receipt is finished (it expired, or the settlement failed), so checking again cannot help and no credential will come from it. That does NOT mean the fee was refunded \u2014 never tell the user they were not charged; give them paymentReceipt for support. The wallet has already DISCARDED the dead receipt, so nothing is blocking a new purchase: if the user wants the credential, call request_ai_birthcert_verification again and it pays normally. That is the fee AGAIN, so ask them first rather than calling it on their behalf.',
25329
+ description: 'Check the status of the most recently requested Verified AI Birthcert session (see request_ai_birthcert_verification). FREE \u2014 it never pays for anything. 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` and `expiresIn` \u2014 give the user the link and `expiresIn` exactly as given, so they know how long it is good for; never work out the time remaining yourself from `expiresAt`, because your clock and timezone may differ from the server\'s. 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. If a previous payment is still clearing, this tool ACTIVELY ADVANCES it \u2014 so in that one case it can take up to ~90s to return (it is waiting on the settlement, not hung; every other case returns immediately). It replays the saved receipt (never a new payment) and returns the live session once it settles, so telling the user to check back here genuinely moves things forward. While it is still clearing you get { status: "settlement_pending", paymentReceipt, message }: a payment HAS been made, so never call request_ai_birthcert_verification and never tell the user it failed. With issuerRejected: true (message leads "PAYMENT SENT, BUT THE CREDENTIAL SERVICE REFUSED THE REQUEST") the credential service was reached and refused the request, and message quotes what it said \u2014 relay that reason to the user and tell them the verification service is not completing requests right now. Do NOT describe it as a settlement still processing: the settlement is not what failed. The receipt is kept and no new payment was made, so it is worth checking again later \u2014 an outage on their side can clear. This says NOTHING about whether the fee was taken, so never tell the user they were not charged. With paymentInvalid: true (message leads "PAYMENT SENT, BUT THE CREDENTIAL SERVICE SAYS THIS PAYMENT DID NOT VALIDATE") the service has specifically ruled the payment or receipt invalid \u2014 different from issuerRejected: do NOT say "the settlement is not what failed" here, because this verdict IS about the payment. Relay what the service said, but this one says NOTHING about whether the fee was taken in EITHER direction \u2014 never tell the user they were not charged, and just as much, never tell them they WERE charged either. Do not pay again either way. stuckFor may appear alongside EITHER issuerRejected or paymentInvalid (the receipt happens to also be old) \u2014 it is just context on how long this has been retried, not a reason to change any of the advice above for either case. Otherwise, outcomeUnknown tells the remaining cases apart, and without it the message splits again. Without outcomeUnknown the message leads "PAYMENT SENT", and that shape is TWO different states you tell apart by the clause that follows. If it says "still being processed" the settlement is confirmed queued and progressing \u2014 check again in a few minutes. If it says "has not been confirmed yet" the outcome could not be determined at all yet: do NOT describe that one as progressing and do NOT describe it as succeeded, because it is indeterminate, not confirmed. Either way the receipt is saved, so do not pay again, and check again later. When the message contains a "Tell the user:" sentence, relay it rather than composing your own reassurance. With outcomeUnknown: true (message leads "OUTCOME UNKNOWN") the settlement outcome could not be determined at all and has been unresolved long enough that it is not coming back (stuckFor says how long). The fee was most likely ALREADY TAKEN and no credential was issued \u2014 say that plainly rather than implying it may still land. do not just tell the user to wait; give them the paymentReceipt and tell them to quote it to support. Quote paymentReceipt to support in either case if they ask. Separately, { status: "receipt_void" } is TERMINAL: the payment service has ruled that this receipt is finished (it expired, or the settlement failed), so checking again cannot help and no credential will come from it. That does NOT mean the fee was refunded \u2014 never tell the user they were not charged; give them paymentReceipt for support. The wallet has already DISCARDED the dead receipt, so nothing is blocking a new purchase: if the user wants the credential, call request_ai_birthcert_verification again and it pays normally. That is the fee AGAIN, so ask them first rather than calling it on their behalf.',
25191
25330
  inputSchema: { type: "object", properties: {} }
25192
25331
  },
25193
25332
  {
25194
25333
  name: "clear_stuck_payment_receipt",
25195
- description: 'LAST RESORT. Discard a stuck Verified AI Birthcert payment receipt that the wallet is holding and refusing to pay past. DESTRUCTIVE and CANNOT BE UNDONE: the payment it represents becomes unrecoverable \u2014 if that settlement ever completes, the funds are forfeit and no credential is issued. Do NOT use this as a retry. If the wallet reports { status: "settlement_pending" }, a payment has already been made and the settlement may still complete by itself \u2014 but that same shape also covers an indeterminate outcome and a service refusal, so do not promise the user it will resolve on its own; call check_ai_birthcert_verification again instead; it actively advances a queued settlement. Only reach for this tool when the outcome has been stuck with no change for a long time and the user accepts losing the payment. Two steps, deliberately: call it with no arguments first and it clears NOTHING \u2014 it returns the receipt id and a warning. Show that id to the user, get their explicit agreement, then call again with confirmReceiptId set to exactly that id. A mismatched id clears nothing. If the settlement completed in between \u2014 which is exactly what happens when you follow the advice above and call check_ai_birthcert_verification first \u2014 the receipt now belongs to a LIVE, paid-for session and this tool REFUSES to clear on the id alone: it hands back the session id and verification link instead. Give that link to the user; only if they truly want to abandon a session they already paid for, call again with confirmDiscardLiveSession set to true as well.',
25334
+ description: 'LAST RESORT. Discard a stuck Verified AI Birthcert payment receipt that the wallet is holding and refusing to pay past. DESTRUCTIVE and CANNOT BE UNDONE: the payment it represents becomes unrecoverable \u2014 if that settlement ever completes, the funds are forfeit and no credential is issued. Do NOT use this as a retry. If the wallet reports { status: "settlement_pending" }, a payment has already been made and the settlement may still complete by itself \u2014 but that same shape also covers an indeterminate outcome and a service refusal, so do not promise the user it will resolve on its own; call check_ai_birthcert_verification again instead; it actively advances a queued settlement. Only reach for this tool when the outcome has been stuck with no change for a long time and the user accepts losing the payment. The wallet enforces that: a receipt younger than SETTLEMENT_STUCK_AFTER_MS (24h by default) is REFUSED on both steps, so do not offer this option for one. Two steps, deliberately: call it with no arguments first and it clears NOTHING \u2014 it returns the receipt id and a warning. Show that id to the user, get their explicit agreement, then call again with confirmReceiptId set to exactly that id. A mismatched id clears nothing. If the settlement completed in between \u2014 which is exactly what happens when you follow the advice above and call check_ai_birthcert_verification first \u2014 the receipt now belongs to a LIVE, paid-for session and this tool REFUSES to clear on the id alone: it hands back the session id and verification link instead. Give that link to the user; only if they truly want to abandon a session they already paid for, call again with confirmDiscardLiveSession set to true as well.',
25196
25335
  inputSchema: {
25197
25336
  type: "object",
25198
25337
  properties: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-wallet-mcp",
3
- "version": "0.12.1",
3
+ "version": "0.12.2",
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",