agentic-wallet-mcp 0.12.1 → 0.12.3

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,73 @@ 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.3] — 28 September 2026
12
+
13
+ ### Fixed
14
+
15
+ - **`request_ai_birthcert_verification` now checks for an existing Verified AI Birthcert VC before
16
+ paying for a new one.** Asking for a verified credential when the holder already had one used to
17
+ start a brand-new paid session unconditionally, surfacing as a confusing "name already in use"
18
+ error instead of reusing or asking about the existing VC. It now checks the local cache, and — if
19
+ the cache never saw a previously-issued session — resolves that session's VC the same way
20
+ `check_ai_birthcert_verification` does, rather than trusting an empty cache. A still-valid existing
21
+ VC blocks the request and returns `{ existingVerifiedVc: { vcId, validUntil }, message }`; replacing
22
+ it requires echoing its exact `vcId` back as `confirmReplaceExistingVc`. An expired existing VC is
23
+ replaced automatically, with the result carrying `replacedExpiredVc: { vcId, validUntil }`.
24
+ - **After a Verified AI Birthcert is issued, the agent now shows the full credential, including its
25
+ pass-design image when one is available.** The response used to summarise only a handful of fields
26
+ (credential id, agent name, owner, valid-until, evidence) and never mentioned the credential's
27
+ official pass-design image, even when the wallet already had it. Every claim on the credential is
28
+ now shown, and the pass-design image is surfaced when present, instead of being silently dropped
29
+ from the summary.
30
+ - **`policy_preflight` now validates against the real on-chain template vocabulary instead of an
31
+ invented one.** Its type checking and the "this policy denies everything" blocker were built from a
32
+ field guide written before any policy template existed on chain, so the real deployed types and
33
+ list-attribute names didn't match what was actually being checked for — a spending cap of
34
+ `"not-a-number"` could pass clean, and an empty allow-list wasn't flagged as blocking every payment.
35
+ Both now fire correctly against the real templates.
36
+ - **A deny-list is no longer described as if it were an allow-list.** `policy_preflight` used to guess
37
+ a list attribute's meaning from its data type alone, which says nothing about which way the list
38
+ points — a recipient deny-list came back described as "allows ONLY the entries listed" when it
39
+ actually does the opposite, and an empty deny-list (which blocks nobody) was wrongly flagged as
40
+ blocking everything. Each list attribute's true direction is now looked up individually; one the
41
+ wallet can't confirm is reported as "not checked" rather than given a guessed meaning.
42
+ - **`get_my_policy` no longer reports a policy as found when it isn't there.** A shape mismatch meant a
43
+ genuine "no policy deployed" answer from the credential service was read as a successful, empty
44
+ policy, so a real policy could look like it had no rules at all. It's now correctly reported as a
45
+ failed read instead.
46
+
47
+ ## [0.12.2] — 24 September 2026
48
+
49
+ ### Fixed
50
+
51
+ - **A taken `agentName` is no longer reported as an already-settled payment.** The credential service
52
+ answers both "this name is already taken" and "this exact payment was already settled" with the same
53
+ HTTP 409, and the wallet used to treat every 409 as the settled case — telling the user their fee was
54
+ most likely taken and warning that retrying would charge it again, when picking a different name was
55
+ actually free to try. The two are now told apart, and a taken name says only that: choose a different
56
+ `agentName`; it does not claim the fee was, or was not, taken.
57
+ - **A payment can no longer be discarded and paid again until it is genuinely stuck.** In a live run the
58
+ first payment had already settled, the assistant offered to "discard the stuck receipt and start
59
+ fresh" for a second 1 JMYR, and a bare "retry" from the user was taken as agreement, so the user paid
60
+ twice. The rule that a receipt only counts as stuck after 24 hours (`SETTLEMENT_STUCK_AFTER_MS`) was
61
+ only advice; the wallet now enforces it on both ways of discarding a receipt and pays nothing when the
62
+ receipt is younger. Set `SETTLEMENT_STUCK_AFTER_MS` lower on purpose if you need to start over sooner.
63
+ - **The agent can now tell the user how long the verification link really lasts.** The wallet used to
64
+ return the session's expiry untouched and leave the arithmetic to the agent, and in one run the agent
65
+ mixed up timezones and said the link was good for about 8 hours when it had about 15 minutes. Results
66
+ now carry `expiresIn` ("about 14 minutes") and `expiresInSeconds`, worked out by the wallet, and the
67
+ tool descriptions tell the agent to quote them.
68
+ - **A payment the credential service has not confirmed yet is now reported with what the service actually
69
+ said**, including its status code and message, plus a short sentence the agent can pass on to the user
70
+ as-is. Previously the agent had nothing real to quote and sometimes made up an explanation.
71
+
72
+ ### Added
73
+
74
+ - `SSIVC_TRACE=1` prints every request to and response from the credential service to stderr, for
75
+ diagnosing a settlement that will not resolve. Off by default; the trace contains the payment receipt,
76
+ so it is for reproductions only.
77
+
11
78
  ## [0.12.1] — 24 September 2026
12
79
 
13
80
  ### Fixed
package/README.md CHANGED
@@ -28,9 +28,13 @@ 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 — on a hostile draft with hundreds of faults the list is capped, and it says so on the last line), 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, validFromBlock?, validToBlock?, templateId?, publisher? }` | `{ policyKey, ready, declared?, blockers, interpretation, notChecked }` — `declared` is the template's own attribute names, present only when the template resolved; long results are bounded, and anything omitted is stated in `notChecked` rather than dropped silently |
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. **Checks for an existing Verified AI Birthcert VC before paying**: if this holder already has a still-valid one (found in the local cache, or resolved from a previously-issued session the cache never saw), nothing is paid and no session is started — you get `{ existingVerifiedVc: { vcId, validUntil }, message }` instead; call again with `confirmReplaceExistingVc` set to exactly that `vcId` to replace it anyway. An existing VC that has already expired is replaced automatically, no confirmation needed, and the result carries `replacedExpiredVc: { vcId, validUntil }` naming the one it replaced. It can also refuse with `{ error }` specifically because an already-issued credential exists but could not be confirmed as valid or expired — nothing is paid on that path either; relay `error` as given, which names the concrete next step | `{ agentName, agentPurpose?, evidenceAssuranceLevel?, ownerType?, ownerVerified?, gasPayer?, dryRun?, discardStuckReceiptAndPayFresh?, confirmReplaceExistingVc? }` | `{ sessionId, verificationUrl, expiresAt, expiresIn, expiresInSeconds, replacedExpiredVc? }` (`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 } }`, `{ existingVerifiedVc: { vcId, validUntil? }, message }` (nothing paid, see above), 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? }` |
37
+ | `transfer_token` | **Moves real funds, irreversible.** Send native ZTX or any ZTP20 token to an address. `token` takes `"ZTX"`, a registered symbol (no contract address needed) or a raw ZTP20 address; an unregistered symbol returns `needsTokenAddress: true` and signs nothing. Give `amountHuman` or `amount` — if both, they must agree, which is the cheapest way to catch a 1-vs-1000000 error. Nothing is signed without `confirm: true`, and the wallet never infers it. `outcomeUnknown: true` means the transaction may already be on chain — **do not retry**, check the reported nonce. `policyDenied: true` means the spending policy refused it: a decision, not a failure, so retrying will not help | `{ token, to, amountHuman?, amount?, confirm?, dryRun? }` | `{ sent, txHash?, nonce, token, asset, amount, amountHuman, decimals, fee, needsConfirmation?, needsTokenAddress?, outcomeUnknown?, policyDenied? }` |
34
38
 
35
39
  > **VCs are cached locally**, keyed by `templateId`, under `~/.agentic-wallet-mcp/vc-cache/`
36
40
  > (scoped per network + holder — different identities or networks never share a cache).
@@ -185,7 +189,8 @@ Node ≥ 18 required (built-in `fetch`).
185
189
  | `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
190
  | `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
191
  | `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`** |
192
+ | `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`** |
193
+ | `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
194
 
190
195
  \* sensitive — never logged, never returned in a tool result, and never a tool parameter.
191
196