agentic-wallet-mcp 0.14.0 → 0.14.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 +26 -0
- package/README.md +6 -6
- package/dist/server-bundle.cjs +618 -121
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
+
## [0.14.2] — 13 October 2026
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- **An AI Birthcert request that does not say Basic or Verified is put to the user.** "Get me AI Birth VC" was answered with the Basic credential in one chat and the Verified one in another: the wallet does not read the request, the agent chooses by the tool and credential it names first, and nothing told it which or asked the user. Every AI Birthcert `credential_preflight` result now carries a `choice` note, and the tool text and the plugin skill say the same: check what the wallet already holds, explain the two kinds in plain words, offer only what it does not hold, ask which the user prefers, and, if they want both, issue them one after the other, Basic first. The older sentence in `request_ai_birthcert_verification` that said the user "most likely wants" the Basic one, and to use `subscribe_and_issue`, is replaced by the same rule: if the request does not say which kind, do not start it, check both and ask. Each kind has its own price and needs its own yes, so "both" is not a yes to either payment. Where the Verified kind is not configured (mainnet, by default), the note says to offer Basic only. `prove_identity` now says what to do when several credentials are held: pass one, prefer the Verified one when the request names none, and say which was presented.
|
|
18
|
+
|
|
19
|
+
- **The Basic AI Birthcert's testnet pass design was updated.** The Basic AI Birthcert is now issued on testnet with the new pass-design image id, so the card image that comes back with the credential is the current one. Mainnet and other templates are unchanged.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- **The agent no longer asks again for a name the user has already given.** Asked for `agentUsername`, a user answered "QA Basic VC" twice and the agent kept asking, because the rule "never invent a name" said nothing about a short answer that repeats the question. An answer is now the name: the agent uses it as given and does not ask for it again (unless it was refused), tidying it is optional and the agent waits for a yes before using a tidied form, it never goes ahead on a name or a payment the user has not agreed to, and it treats any example it gives as only an example of the format.
|
|
24
|
+
- **The agent no longer shows the raw `paymentRequired` field name to the user.** QA saw "issuance is currently free (paymentRequired: false)": the wallet's own agent-facing text used the raw MBI field name as vocabulary, and a preflight `notChecked` item that the agent is told to relay contained it. `notChecked` no longer names the field, and the preflight and dry-run descriptions tell the agent to say free or chargeable in plain words and never to quote JSON field names.
|
|
25
|
+
|
|
26
|
+
## [0.14.1] — 12 October 2026
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- **`create_verification_qr` gives a chat channel a QR file to attach.** Asked for the QR code over WhatsApp, the agent sent the credential's pass-design image captioned as the QR code and said "QR code sent!": the QR existed only as an MCP image block, which a channel that sends files cannot forward, so the agent attached the one image file it did have. The wallet now also writes the QR as a PNG file and returns its path (`qrCodePath`); a file is removed when the wallet starts and the next time a QR is made, once its link has expired, and a file that cannot be written is reported (`qrFileError`) without losing the link. The tool text and result message tell the agent to attach that file, never to send another image as the QR code, and to give the link and say plainly that no QR was sent when it cannot attach one. The result also carries the credential's saved pass-design image(s) (`vcPassImagePaths`, only files that still exist) when the wallet chose or recognised the credential, so the agent can show the QR code and the pass design together, each captioned for what it is.
|
|
31
|
+
|
|
32
|
+
- **`credential_preflight` no longer blocks a free credential on a missing ZTX balance.** A user asked for an AI Birthcert, was told issuance was free, and was in the same answer told "No ZTX for network gas" was a blocker and asked to fund the wallet. The fee and spending-limit checks already stood aside when MBI reports `paymentRequired: false`; the gas check did not, and it also treated an absent gas model as self-pay. A free issuance makes no payment, so no transaction is sent and no gas is spent. On a free credential a wallet with no ZTX (or an unreadable ZTX balance) is now `ready: true`, with a note in `notChecked` that ZTX would be needed if issuance becomes chargeable and the wallet pays its own gas. A chargeable credential on a self-pay gas model still blocks on missing ZTX, as before. The tool text no longer says gas can block a free credential, and tells the agent never to ask for ZTX for one.
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
|
|
36
|
+
- **A self-paid send whose outcome is not settled is remembered.** The transaction now carries an expiry (`ceilLedgerSeq`, 60 ledgers past the current one), so after that ledger the node can never accept it. A small note (owner, policy key, operation, nonce, transaction hash, expiry, send time; no secret) is written, fsynced and renamed into place owner-only, kept per network, after signing and before the submit, and kept until the outcome is settled: a transaction found on chain (landed or failed) or a ledger past its expiry clears it. While it could still land, a later write or update for the same key is refused with nothing sent, saying when it settles, and so are a sponsored write, the collect of an already-paid write and `remove_policy` (the self-paid transaction replaces the whole policy when it lands); only its own transaction removes a note, and every confirmed write, collect or removal for a key in a process waits for the one in front of it for up to 15 seconds, then refuses with nothing done, so a call never acts after its client could have given up. A request to the policy write service times out after 60 seconds, so a hung one cannot hold a key. Only a missing file means "no note": one that cannot be read, is empty or is damaged (what a crash can leave) stops the call, names the file and sends nothing; the note is deleted by hand only after at least 15 minutes and with the chain producing ledgers, or once `get_my_policy` shows the write. A note whose write failed part-way (a directory fsync error) is removed again, since nothing was sent. A send is refused, with nothing signed, if the ledger number cannot be read, and with nothing sent if the note cannot be written. Previously a repeat call could only look at the chain, leaving a narrow window in which a transaction that landed just afterwards could be followed by a second one.
|
|
37
|
+
- **A self-paid write is bounded by the gas figure the user was shown.** `write_policy` and `update_policy` take `maxGasRaw` for `gasPayer "self"`: the `requiredRaw` of the self option (the estimate plus a fifth) that the quote or the choice showed. A self-paid send is refused, free and with nothing signed, without it or when the wallet's own figure at the time of signing is above it; the refusal gives the current figure to show and what to call with. The fee limit signed is the wallet's own ceiling, which is checked against the balance and can never be above what the user was shown. A `maxGasRaw` that is not a positive whole number (digits, or a safe-integer number) is refused before the chain is read. The quote, a dry run and the choice say what to pass; the sponsored path does not use it. An operator-configured ceiling was left out: its value is a decision for the team.
|
|
38
|
+
|
|
13
39
|
## [0.14.0] — 12 October 2026
|
|
14
40
|
|
|
15
41
|
### Fixed
|
package/README.md
CHANGED
|
@@ -21,9 +21,9 @@ VC issuance → identity proof → pay-per-use).
|
|
|
21
21
|
| Tool | Does | Input | Output (shape) |
|
|
22
22
|
|---|---|---|---|
|
|
23
23
|
| `wallet_status` | Report holder DID/address/network + held VCs (client-supplied, or the local cache); optionally one token balance (`token`) or several in one call (`tokens`). Each balance carries `balance` (raw base units), `decimals` and `display` — the same amount in whole tokens with its symbol. Quote `display`; a raw count beside a ticker is wrong by orders of magnitude. A token may be named by ticker **or** contract address, and a ZTP20 result also carries its contract address as `tokenAddress` — the value a policy needs, so it never has to be asked for | `{ heldCredentials?, token?, tokens? }` | `{ holderDid, zetrixAddress, network, credentials, balances?, tokenBalance?, tokenBalances? }` |
|
|
24
|
-
| `credential_preflight` | **Free.** The first call for any credential — composes the quote, the balances and the spending cap into one answer, before a single application field is collected. Spends nothing, starts nothing. `blockers` lists *every* reason the wallet is not ready, at once; `notChecked` lists what preflight cannot know — agent-name availability is decided by myid at issuance, after payment, so a `ready` result is never a reservation. **It also says when the wallet already holds a valid copy of the credential** (`alreadyHeld`, with `ready: false`): it looks at the credentials the wallet has saved, locally and for free, before pricing anything, so the agent stops with "you already have one" instead of asking for an agent name. Buying another would replace it (one credential per template) and costs the fee; only when the user explicitly wants a replacement is it run again with `replacing: true`. An expired credential does not count, and a lookup that fails is reported in `notChecked`, never as "nothing is held" | `{ credential, agentName?, replacing? }` | `{ credential, ready, fee?, balances, cap?, schema?, alreadyHeld?, blockers, notChecked }` |
|
|
24
|
+
| `credential_preflight` | **Free.** The first call for any credential — composes the quote, the balances and the spending cap into one answer, before a single application field is collected. Spends nothing, starts nothing. `blockers` lists *every* reason the wallet is not ready, at once; `notChecked` lists what preflight cannot know — agent-name availability is decided by myid at issuance, after payment, so a `ready` result is never a reservation. **It also says when the wallet already holds a valid copy of the credential** (`alreadyHeld`, with `ready: false`): it looks at the credentials the wallet has saved, locally and for free, before pricing anything, so the agent stops with "you already have one" instead of asking for an agent name. Buying another would replace it (one credential per template) and costs the fee; only when the user explicitly wants a replacement is it run again with `replacing: true`. An expired credential does not count, and a lookup that fails is reported in `notChecked`, never as "nothing is held". When a user asks for an AI Birthcert without saying Basic or Verified, every result carries a `choice` note: the agent checks both kinds, explains the difference, and asks which they prefer; if they want both, it issues them one after the other, Basic first. | `{ credential, agentName?, replacing? }` | `{ credential, ready, fee?, balances, cap?, schema?, alreadyHeld?, blockers, notChecked, choice? }` |
|
|
25
25
|
| `prove_identity` | Answer an x401 `PROOF-REQUEST` → return the `PROOF-RESPONSE` header to replay | `{ proofRequest, vc?, revealAttribute?, issuerKeys? }` | `{ proofResponseHeader, verified, presentationId }` |
|
|
26
|
-
| `create_verification_qr` | Create a link and a QR code that open this wallet's credential in the MyID app, for a human to verify the agent. Presents the credential to MBI — with no `vc` given, the Verified AI Birthcert if the wallet holds one, otherwise the Basic AI Birthcert, and if it holds neither it answers `created: false` and says to create one first. The link holds only the reference id, but that id grants access to whatever is revealed until it expires (5 minutes by default, up to 60), so by default it reveals only a standard minimal set (Verified: `agentName`, `evidenceProvider`, `ownerVerified`; Basic: `agentUsername`) — name other attributes with `revealAttribute`, and pass `revealAll: true` only when the user explicitly asks to reveal everything; show the link only to the human user. A Basic credential does not mean the owner was verified, and the result says so. Needs `MYID_VERIFY_LINK_TEMPLATE`; without it the tool answers `created: false` before anything is created | `{ vc?, revealAttribute?, revealAll?, expiryMinutes? }` | `{ created, link, referenceId, expiresAt, expiresInMinutes, revealed (the attribute paths, or `"all"`), credentialUsed, revealedByDefault, qrCodePngBase64 (sent as an image; absent with a `qrError` if it could not be drawn), message }` or `{ created: false, reason }` |
|
|
26
|
+
| `create_verification_qr` | Create a link and a QR code that open this wallet's credential in the MyID app, for a human to verify the agent. Presents the credential to MBI — with no `vc` given, the Verified AI Birthcert if the wallet holds one, otherwise the Basic AI Birthcert, and if it holds neither it answers `created: false` and says to create one first. The link holds only the reference id, but that id grants access to whatever is revealed until it expires (5 minutes by default, up to 60), so by default it reveals only a standard minimal set (Verified: `agentName`, `evidenceProvider`, `ownerVerified`; Basic: `agentUsername`) — name other attributes with `revealAttribute`, and pass `revealAll: true` only when the user explicitly asks to reveal everything; show the link only to the human user. The QR code comes back as an image and as a PNG file (`qrCodePath`) that a chat channel such as WhatsApp can attach; a file is removed when the wallet starts and the next time a QR is made, once its link has expired. The file is on the wallet's own machine, so a client that runs the wallet remotely cannot read the path and gets the image block and the link instead. The agent is told never to send another image as the QR code, and to give the link and say so if it cannot attach one. When the credential has a saved pass design, the result also carries its path (`vcPassImagePaths`), so the agent can show both pictures, each captioned for what it is. A Basic credential does not mean the owner was verified, and the result says so. Needs `MYID_VERIFY_LINK_TEMPLATE`; without it the tool answers `created: false` before anything is created | `{ vc?, revealAttribute?, revealAll?, expiryMinutes? }` | `{ created, link, referenceId, expiresAt, expiresInMinutes, revealed (the attribute paths, or `"all"`), credentialUsed, revealedByDefault, qrCodePngBase64 (sent as an image; absent with a `qrError` if it could not be drawn), message }` or `{ created: false, reason }` |
|
|
27
27
|
| `pay_and_fetch` | Fetch a URL, auto-pay with x402 (self-pay via Wallet BE) on `402` | `{ url, method?, headers?, body? }` | `{ status, body, paymentMade, amountPaid, amountPaidHuman, asset }` |
|
|
28
28
|
| `subscribe_and_issue` | Reuse a cached VC if still valid, else pay x402 → MBI issues → return the VC. Replacing a still-valid cached VC needs `forceReissue: true` **and** `confirmReplaceExistingVc` set to the exact `vcId` you were shown first — `forceReissue` alone only shows you the existing VC (`issued: false`), it never pays | `{ templateId, attributes, expirationDate?, dryRun?, forceReissue?, confirmReplaceExistingVc? }` | `{ issued, vcId, vc, txHash, fromCache?, schema?, originalPayment?, paymentAttempted?, recovery?, vcPassImagePaths? }` — `issued: false` with `fromCache: true` means an existing valid VC is being shown, not an error |
|
|
29
29
|
| `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 }` |
|
|
@@ -33,9 +33,9 @@ VC issuance → identity proof → pay-per-use).
|
|
|
33
33
|
| `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". Each policy that was read carries `forUpdate.expectedUpdatedAtBlock`, the value `update_policy` needs, already as a string | `{ owner? }` | `{ owner, policies?, warning?, error? }` |
|
|
34
34
|
| `policy_preflight` | **Free** check of a draft policy *before* it is deployed. An amount may be given as `valueHuman` (whole tokens, `"100"` for 100 JMYR) instead of `value`: the wallet converts it with the token's own decimals, shows both forms, and never sends `valueHuman` on. It is only for the amount attributes (`perTransactionMax`, `cumulativeMax`, `velocityCap`, and any attribute the service lists as an amount in the smallest unit): `maxTransactionCount` is a count and a window is a duration, and neither is ever scaled. Each amount attribute may carry its own `valueHuman`. Never put `convertedAmounts` in `valueHuman`: those are raw base units. A number is accepted only as a safe whole number; write anything else, such as `"0.5"`, as text. An attribute needs a `value` or a `valueHuman`, whatever its type. If `value` and `valueHuman` are both given they must agree, and `amountUnit` does not apply to a `valueHuman`. 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. It also applies the write service’s scope rules, so a draft the service would refuse is refused here for free: `assetScope` must be exactly `native` or `ztp20` (never a token symbol such as JMYR), a `ztp20` policy needs a `tokenAddress`, and any amount or count cap needs an `assetScope`. A token symbol written where `tokenAddress` belongs is refused with the registered address to use, and a `ztp20` draft with no `tokenAddress` names the registered ones. Every `*Window` must be a duration such as `7d`, `12h` or `30m` (or ISO `P7D`) — `week`, a bare number or a value with spaces around it is refused, and `interpretation` states each window's period in words (`1M` is one minute, not one month) — and `interpretation` states what each amount means in whole tokens, because amounts are raw base units. A non-zero amount cap (`perTransactionMax`, `cumulativeMax`, `velocityCap`) under one whole token is refused unless `amountUnit` says what is meant: `"whole"` gives whole-token amounts (`"1"`, `"0.5"`) that the wallet converts by the token's decimals and returns in `convertedAmounts`, and `"base"` confirms a tiny raw value is intended. `convertedAmounts` is for showing the user — never send it back as the values; a `"whole"` draft whose every amount already looks raw is refused with both readings and the exact raw value for each. `notChecked` lists what could not be verified, including whether the policy would be enforced at all | `{ policyKey, attributes, validFromBlock?, validToBlock?, templateId?, publisher?, amountUnit? }` | `{ policyKey, ready, declared?, blockers, interpretation, notChecked, convertedAmounts? }` — `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 |
|
|
35
35
|
| `check_policy_decision` | **Free** — but NOT free of consequence. Ask whether a specific spend is permitted RIGHT NOW: the one policy question no chain read can answer, because a cap is measured against cumulative spend held off-chain. Three outcomes — `permitted`, `refused`, `undetermined`. **`undetermined` is neither a refusal nor permission**: it means nothing was evaluated (service unreachable, ledger stale, or none configured), and it is what every failure produces — no transport error, bad envelope or unknown verdict string can ever yield `permitted`. There is **no step-up verdict**; the decision service answers only allow or deny, so the wallet never reports that a human must approve a spend. **A permitted answer reserves the owner’s budget for 15 minutes**, with no way to release it early — so do not poll it, do not probe amounts (every allow along the way reserves again, and the owner’s next real payment can be refused by their own agent), and **never retry automatically after a timeout**: the call may have succeeded and reserved already. To find an amount that fits, read `remaining` from one answer and compute locally. `requestKey` is **correlation only, not idempotency**: repeating it does not deduplicate and each permitted answer reserves again. While the spend-ledger crawl is off, every decision answers `undetermined` / `EVALUATION_UNAVAILABLE`, which is correct behaviour rather than an outage. `ignored` is surfaced on a permitted answer too — it lists constraints the policy carries that the service could not enforce | `{ ownerAddress, asset, amount, policyKey?, recipientAddress?, method?, payTo?, paymentNonce?, requestKey? }` | `{ outcome, summary, reasonCode?, policyKey?, enforced?, ignored?, remaining?, capacityReturnsAt?, reservationId?, notChecked }` |
|
|
36
|
-
| `write_policy` | **Pays a real fee, and only with `confirm: true`** (the user must have seen the interpretation and the price; without it the call returns the price and pays nothing). Deploy a spending policy on chain over x402. An amount may be given as `valueHuman` (whole tokens, `"100"` for 100 JMYR) instead of `value`: the wallet converts it with the token's own decimals, shows both forms, and never sends `valueHuman` on. It is only for the amount attributes (`perTransactionMax`, `cumulativeMax`, `velocityCap`, and any attribute the service lists as an amount in the smallest unit): `maxTransactionCount` is a count and a window is a duration, and neither is ever scaled. Each amount attribute may carry its own `valueHuman`. Never put `convertedAmounts` in `valueHuman`: those are raw base units. A number is accepted only as a safe whole number; write anything else, such as `"0.5"`, as text. An attribute needs a `value` or a `valueHuman`, whatever its type. If `value` and `valueHuman` are both given they must agree, and `amountUnit` does not apply to a `valueHuman`. Three steps: a free pre-check, a payment that writes **nothing**, and a collect that finishes the write once the payment settles. Between the payment and the settlement a payment has been made and no policy exists — **that window is normal, not a failure**. On the sponsored path `settling` and `submitted` both mean a payment has been made: pass `paymentReceipt` to `check_policy_write`, never call this again. `submitted` carries a `txHash` and still is **not** a written policy — the block has not confirmed it. `written` is the only state where the policy exists. `already_exists` / `refused` come from the free pre-check, so nothing was paid; `payment_refused` says nothing either way about whether the fee was taken. `receipt_void` is the one state where paying again is right (`payFresh`). The fee goes through the same payment cap as every other paying tool, and the wallet never builds a blob, permit digest or canonical JSON — ms-zetrix signs with the owner’s own key. `templateContractAddress` is optional and best left out: the wallet uses the one Template contract it is configured for and refuses any other before any payment, so there is nothing for a user to supply. **`dryRun: true` returns the price without paying** — it runs every free check and stops at the quote (`quoted`): nothing is paid, collected or written to chain, and it never collects an already-paid write, because collecting signs and writes (it only keeps a local bookmark so `check_policy_write` can finish it). The quote also carries `affordability` — whether the wallet holds the quoted amount of the fee asset (plus some ZTX for gas, when the payer needs it) and whether the payment cap would refuse it, each shortfall named separately and a failed balance read reported as `unknown`, never as a yes. It does not estimate the network fee: a payment made in ZTX needs the amount plus that fee from one balance, so `feeNotEstimated` says when "affordable" means only "holds the amount". A quote is what the service asked for just now, not a promise the payment will be allowed: balances move and the cap is enforced again when paying. **Who pays the gas is the user's choice** (`gasPayer`): `"sponsored"` pays the fee in JMYR and the gas is covered (no ZTX needed), `"self"` costs no fee and only the network gas in ZTX. The wallet has no default for it: when the wallet can pay the gas itself, a call without `gasPayer` stops at `needs_choice` (nothing paid, whatever `confirm` says) with both options, what each costs and whether the wallet can pay it right now, so the user can decide; a value that is neither is refused before the service is contacted. Self-pay is on offer where the policy contracts are deployed (testnet today); elsewhere nothing changes and `"self"` is refused. A self-paid write is the owner's own `createOrUpdatePolicy` call on the Policy Registry, built here and signed through Wallet BE, and the policy service is never contacted. The wallet reads the chain first (a policy that already exists, or one that has changed since you read it, is refused for free), checks the ZTX balance covers the estimated gas plus a reserve (a balance or estimate it cannot read is not a yes), and reports `written` only when the chain then holds exactly what was sent: the same attributes, template and validity window, and for an update `updatedAtBlock` moved. Otherwise it reports `submitted` with the `txHash`: do not send it again, and `get_my_policy` shows when it lands. Wallet BE refusing to sign is a decision (`policyDenied`); its check failing is transient (`policyCheckUnavailable`). Nothing is sent when the node's own simulation rejects the call. Without `confirm: true` it quotes the gas and sends nothing. **With `gasPayer "self"` there is no `paymentReceipt` and `check_policy_write` does not apply:** `submitted` means the network gas is spent (do not send it again; `get_my_policy` shows when it lands, and quote the `txHash` if it never does), `unknown` means it may be on chain (do not send it again; quote the nonce), and `write_failed` means the transaction was included and rejected (the gas is spent and nothing was written). The gas is spent even when the write fails on chain. The wallet signs with the fee ceiling it checked the balance against, and refuses (nothing signed) if the node's fresh estimate is above it. A pending paid write for the same `policyKey` blocks a self-paid one (finish it with `check_policy_write` first), and so does an attribute vocabulary that could not be read or that no source is configured for (the vocabulary comes from the policy service, so self-pay needs it), because a self-paid write has no service to catch what it would have. A receipt the service said failed does not hold a self-paid write back; one with an unresolved outcome does. When neither way of paying is available the answer is `unavailable` and names both reasons. A list attribute is capped at 500 entries and a policy at 64 attributes. A wallet that already has a policy can self-pay too: Wallet BE (a current version) signs the owner's own Registry call. A Wallet BE older than that refuses it (`UNSUPPORTED_OPERATION`, a decision made before any policy is consulted), which is reported as final, not about the policy's limits, with no invitation to retry. A self-paid update is a whole-policy replacement that the contract does not compare with what was there, so another write by the same owner landing in the few seconds between the wallet's last chain read and the block that includes the transaction is overwritten (the wallet still reports `written` because its own content is there and `updatedAtBlock` moved). A self-paid write has not yet been run end to end against a live chain. | `{ policyKey, attributes, templateId, templateContractAddress?, validFromBlock?, validToBlock?, requestKey?, pollBudgetMs?, amountUnit?, dryRun?, confirm?, gasPayer? }` | `{ state, message, paymentReceipt?, policyKey?, txHash?, paid?, payFresh?, quote?, affordability?, gasOptions?, needsConfirmation?, policyDenied?, policyCheckUnavailable?, upstream? }` |
|
|
37
|
-
| `update_policy` | **Pays a real fee, and only with `confirm: true`.** Replace a deployed policy's attributes and validity window over x402. `attributes` is the **full replacement set**, not a patch. An amount may be given as `valueHuman` (whole tokens, `"100"` for 100 JMYR) instead of `value`: the wallet converts it with the token's own decimals, shows both forms, and never sends `valueHuman` on. It is only for the amount attributes (`perTransactionMax`, `cumulativeMax`, `velocityCap`, and any attribute the service lists as an amount in the smallest unit): `maxTransactionCount` is a count and a window is a duration, and neither is ever scaled. Each amount attribute may carry its own `valueHuman`. Never put `convertedAmounts` in `valueHuman`: those are raw base units. A number is accepted only as a safe whole number; write anything else, such as `"0.5"`, as text. An attribute needs a `value` or a `valueHuman`, whatever its type. If `value` and `valueHuman` are both given they must agree, and `amountUnit` does not apply to a `valueHuman`. Read the policy first with `get_my_policy` and pass its `forUpdate.expectedUpdatedAtBlock` (a string): if the policy has changed since, the update is refused for free (`modified`). The policy keeps the template it already references — pass none — and its validity window is kept unless `validFromBlock` / `validToBlock` are given. The same three steps, states and `dryRun` as `write_policy`; `check_policy_write` finishes a paid update from its receipt. Free refusals, each saying nothing was paid: `not_found`, `modified`, `template_unavailable`, `refused`. `written` is the only state that means the policy was updated; `submitted` is **not** (the block has not confirmed it); On the sponsored path `write_failed` means the update was **not** applied, the policy is unchanged and a payment may have been taken. The fee goes through the same payment cap as every other paying tool. **Who pays the gas is the user's choice** (`gasPayer`): `"sponsored"` pays the fee in JMYR and the gas is covered (no ZTX needed), `"self"` costs no fee and only the network gas in ZTX. The wallet has no default for it: when the wallet can pay the gas itself, a call without `gasPayer` stops at `needs_choice` (nothing paid, whatever `confirm` says) with both options, what each costs and whether the wallet can pay it right now, so the user can decide; a value that is neither is refused before the service is contacted. Self-pay is on offer where the policy contracts are deployed (testnet today); elsewhere nothing changes and `"self"` is refused. A self-paid write is the owner's own `createOrUpdatePolicy` call on the Policy Registry, built here and signed through Wallet BE, and the policy service is never contacted. The wallet reads the chain first (a policy that already exists, or one that has changed since you read it, is refused for free), checks the ZTX balance covers the estimated gas plus a reserve (a balance or estimate it cannot read is not a yes), and reports `written` only when the chain then holds exactly what was sent: the same attributes, template and validity window, and for an update `updatedAtBlock` moved. Otherwise it reports `submitted` with the `txHash`: do not send it again, and `get_my_policy` shows when it lands. Wallet BE refusing to sign is a decision (`policyDenied`); its check failing is transient (`policyCheckUnavailable`). Nothing is sent when the node's own simulation rejects the call. Without `confirm: true` it quotes the gas and sends nothing. **With `gasPayer "self"` there is no `paymentReceipt` and `check_policy_write` does not apply:** `submitted` means the network gas is spent (do not send it again; `get_my_policy` shows when it lands, and quote the `txHash` if it never does), `unknown` means it may be on chain (do not send it again; quote the nonce), and `write_failed` means the transaction was included and rejected (the gas is spent and nothing was written). The gas is spent even when the write fails on chain. The wallet signs with the fee ceiling it checked the balance against, and refuses (nothing signed) if the node's fresh estimate is above it. A pending paid write for the same `policyKey` blocks a self-paid one (finish it with `check_policy_write` first), and so does an attribute vocabulary that could not be read or that no source is configured for (the vocabulary comes from the policy service, so self-pay needs it), because a self-paid write has no service to catch what it would have. A receipt the service said failed does not hold a self-paid write back; one with an unresolved outcome does. When neither way of paying is available the answer is `unavailable` and names both reasons. A list attribute is capped at 500 entries and a policy at 64 attributes. A wallet that already has a policy can self-pay too: Wallet BE (a current version) signs the owner's own Registry call. A Wallet BE older than that refuses it (`UNSUPPORTED_OPERATION`, a decision made before any policy is consulted), which is reported as final, not about the policy's limits, with no invitation to retry. A self-paid update is a whole-policy replacement that the contract does not compare with what was there, so another write by the same owner landing in the few seconds between the wallet's last chain read and the block that includes the transaction is overwritten (the wallet still reports `written` because its own content is there and `updatedAtBlock` moved). A self-paid write has not yet been run end to end against a live chain. | `{ policyKey, attributes, expectedUpdatedAtBlock, validFromBlock?, validToBlock?, amountUnit?, confirm?, dryRun?, gasPayer?, pollBudgetMs? }` | `{ state, message, paymentReceipt?, policyKey?, txHash?, paid?, payFresh?, quote?, affordability?, interpretation?, gasOptions?, needsConfirmation?, policyDenied?, policyCheckUnavailable?, upstream? }` |
|
|
38
|
-
| `remove_policy` | **Free, but it lifts every limit the policy set** — Wallet BE then signs spends of that asset without any limit — so nothing is sent without `confirm: true`. Without it you get `needs_confirmation` with what the policy currently limits. `removed` is the only state that means the policy is gone, and it is reported only when a chain read no longer holds it; `submitted` is a removal on its way, **not** a removed policy. Repeating the call never submits a second removal. `not_found` means there was nothing to remove (or it is already gone); `in_progress` means a paid update has not been collected yet (`check_policy_write` first) | `{ policyKey, confirm?, pollBudgetMs? }` | `{ state, message, policyKey?, txHash?, currentAttributes? }` |
|
|
36
|
+
| `write_policy` | **Pays a real fee, and only with `confirm: true`** (the user must have seen the interpretation and the price; without it the call returns the price and pays nothing). Deploy a spending policy on chain over x402. An amount may be given as `valueHuman` (whole tokens, `"100"` for 100 JMYR) instead of `value`: the wallet converts it with the token's own decimals, shows both forms, and never sends `valueHuman` on. It is only for the amount attributes (`perTransactionMax`, `cumulativeMax`, `velocityCap`, and any attribute the service lists as an amount in the smallest unit): `maxTransactionCount` is a count and a window is a duration, and neither is ever scaled. Each amount attribute may carry its own `valueHuman`. Never put `convertedAmounts` in `valueHuman`: those are raw base units. A number is accepted only as a safe whole number; write anything else, such as `"0.5"`, as text. An attribute needs a `value` or a `valueHuman`, whatever its type. If `value` and `valueHuman` are both given they must agree, and `amountUnit` does not apply to a `valueHuman`. Three steps: a free pre-check, a payment that writes **nothing**, and a collect that finishes the write once the payment settles. Between the payment and the settlement a payment has been made and no policy exists — **that window is normal, not a failure**. On the sponsored path `settling` and `submitted` both mean a payment has been made: pass `paymentReceipt` to `check_policy_write`, never call this again. `submitted` carries a `txHash` and still is **not** a written policy — the block has not confirmed it. `written` is the only state where the policy exists. `already_exists` / `refused` come from the free pre-check, so nothing was paid; `payment_refused` says nothing either way about whether the fee was taken. `receipt_void` is the one state where paying again is right (`payFresh`). The fee goes through the same payment cap as every other paying tool, and the wallet never builds a blob, permit digest or canonical JSON — ms-zetrix signs with the owner’s own key. `templateContractAddress` is optional and best left out: the wallet uses the one Template contract it is configured for and refuses any other before any payment, so there is nothing for a user to supply. **`dryRun: true` returns the price without paying** — it runs every free check and stops at the quote (`quoted`): nothing is paid, collected or written to chain, and it never collects an already-paid write, because collecting signs and writes (it only keeps a local bookmark so `check_policy_write` can finish it). The quote also carries `affordability` — whether the wallet holds the quoted amount of the fee asset (plus some ZTX for gas, when the payer needs it) and whether the payment cap would refuse it, each shortfall named separately and a failed balance read reported as `unknown`, never as a yes. It does not estimate the network fee: a payment made in ZTX needs the amount plus that fee from one balance, so `feeNotEstimated` says when "affordable" means only "holds the amount". A quote is what the service asked for just now, not a promise the payment will be allowed: balances move and the cap is enforced again when paying. **Who pays the gas is the user's choice** (`gasPayer`): `"sponsored"` pays the fee in JMYR and the gas is covered (no ZTX needed), `"self"` costs no fee and only the network gas in ZTX. The wallet has no default for it: when the wallet can pay the gas itself, a call without `gasPayer` stops at `needs_choice` (nothing paid, whatever `confirm` says) with both options, what each costs and whether the wallet can pay it right now, so the user can decide; a value that is neither is refused before the service is contacted. Self-pay is on offer where the policy contracts are deployed (testnet today); elsewhere nothing changes and `"self"` is refused. A self-paid write is the owner's own `createOrUpdatePolicy` call on the Policy Registry, built here and signed through Wallet BE, and the policy service is never contacted. The wallet reads the chain first (a policy that already exists, or one that has changed since you read it, is refused for free), checks the ZTX balance covers the estimated gas plus a reserve (a balance or estimate it cannot read is not a yes), and reports `written` only when the chain then holds exactly what was sent: the same attributes, template and validity window, and for an update `updatedAtBlock` moved. Otherwise it reports `submitted` with the `txHash`: do not send it again, and `get_my_policy` shows when it lands. Wallet BE refusing to sign is a decision (`policyDenied`); its check failing is transient (`policyCheckUnavailable`). Nothing is sent when the node's own simulation rejects the call. Without `confirm: true` it quotes the gas and sends nothing. **A self-paid send also needs `maxGasRaw`**, the `requiredRaw` of the self option the user was shown (the most gas they agreed to): without it, or when the gas now costs more than it, nothing is signed and the refusal gives the current figure to show and what to call with, so the fee limit signed can never exceed what the user saw. **A self-paid send is remembered until its outcome is settled:** the transaction carries an expiry (60 ledgers past the current one), a small note (policy key, nonce, transaction hash, expiry) is written before it is submitted, and a later write or update for the same key is refused, with nothing sent, while the transaction could still land, and so is a sponsored write, the collect of an already-paid write and `remove_policy` (the self-paid transaction would replace the whole policy when it lands, so nothing else may write or remove in the meantime; nothing is paid or removed). The note holds the owner, policy key, operation, nonce, transaction hash, expiry and send time (no secret), is kept per network, and is removed only by its own transaction. Every confirmed write, collect or removal for a key in a process waits for the one in front of it for up to 15 seconds, and then refuses with nothing sent, paid, collected or removed, so a call never acts after its client could have given up. The note is cleared when the transaction is found on chain (landed or failed) or the ledger has passed its expiry, so an outcome that never arrived is settled in bounded time. A send is refused if the ledger number cannot be read or the note cannot be written, and a note that exists but cannot be read (unreadable, empty or damaged, as a crash can leave it) is never read as "no note": the call stops and names the file, which a person can delete once 60 ledgers (about 10 minutes) have passed since it was last changed, or once `get_my_policy` shows the write. A note is deleted only after at least 15 minutes and with the chain producing ledgers. **With `gasPayer "self"` there is no `paymentReceipt` and `check_policy_write` does not apply:** `submitted` means the network gas is spent (do not send it again; `get_my_policy` shows when it lands, and quote the `txHash` if it never does), `unknown` means it may be on chain (do not send it again; quote the nonce), and `write_failed` means the transaction was included and rejected (the gas is spent and nothing was written). The gas is spent even when the write fails on chain. The wallet signs with the fee ceiling it checked the balance against, and refuses (nothing signed) if the node's fresh estimate is above it. A pending paid write for the same `policyKey` blocks a self-paid one (finish it with `check_policy_write` first), and so does an attribute vocabulary that could not be read or that no source is configured for (the vocabulary comes from the policy service, so self-pay needs it), because a self-paid write has no service to catch what it would have. A receipt the service said failed does not hold a self-paid write back; one with an unresolved outcome does. When neither way of paying is available the answer is `unavailable` and names both reasons. A list attribute is capped at 500 entries and a policy at 64 attributes. A wallet that already has a policy can self-pay too: Wallet BE (a current version) signs the owner's own Registry call. A Wallet BE older than that refuses it (`UNSUPPORTED_OPERATION`, a decision made before any policy is consulted), which is reported as final, not about the policy's limits, with no invitation to retry. A self-paid update is a whole-policy replacement that the contract does not compare with what was there, so another write by the same owner landing in the few seconds between the wallet's last chain read and the block that includes the transaction is overwritten (the wallet still reports `written` because its own content is there and `updatedAtBlock` moved). A self-paid write has not yet been run end to end against a live chain. | `{ policyKey, attributes, templateId, templateContractAddress?, validFromBlock?, validToBlock?, requestKey?, pollBudgetMs?, amountUnit?, dryRun?, confirm?, gasPayer?, maxGasRaw? }` | `{ state, message, paymentReceipt?, policyKey?, txHash?, paid?, payFresh?, quote?, affordability?, gasOptions?, needsConfirmation?, policyDenied?, policyCheckUnavailable?, upstream? }` |
|
|
37
|
+
| `update_policy` | **Pays a real fee, and only with `confirm: true`.** Replace a deployed policy's attributes and validity window over x402. `attributes` is the **full replacement set**, not a patch. An amount may be given as `valueHuman` (whole tokens, `"100"` for 100 JMYR) instead of `value`: the wallet converts it with the token's own decimals, shows both forms, and never sends `valueHuman` on. It is only for the amount attributes (`perTransactionMax`, `cumulativeMax`, `velocityCap`, and any attribute the service lists as an amount in the smallest unit): `maxTransactionCount` is a count and a window is a duration, and neither is ever scaled. Each amount attribute may carry its own `valueHuman`. Never put `convertedAmounts` in `valueHuman`: those are raw base units. A number is accepted only as a safe whole number; write anything else, such as `"0.5"`, as text. An attribute needs a `value` or a `valueHuman`, whatever its type. If `value` and `valueHuman` are both given they must agree, and `amountUnit` does not apply to a `valueHuman`. Read the policy first with `get_my_policy` and pass its `forUpdate.expectedUpdatedAtBlock` (a string): if the policy has changed since, the update is refused for free (`modified`). The policy keeps the template it already references — pass none — and its validity window is kept unless `validFromBlock` / `validToBlock` are given. The same three steps, states and `dryRun` as `write_policy`; `check_policy_write` finishes a paid update from its receipt. Free refusals, each saying nothing was paid: `not_found`, `modified`, `template_unavailable`, `refused`. `written` is the only state that means the policy was updated; `submitted` is **not** (the block has not confirmed it); On the sponsored path `write_failed` means the update was **not** applied, the policy is unchanged and a payment may have been taken. The fee goes through the same payment cap as every other paying tool. **Who pays the gas is the user's choice** (`gasPayer`): `"sponsored"` pays the fee in JMYR and the gas is covered (no ZTX needed), `"self"` costs no fee and only the network gas in ZTX. The wallet has no default for it: when the wallet can pay the gas itself, a call without `gasPayer` stops at `needs_choice` (nothing paid, whatever `confirm` says) with both options, what each costs and whether the wallet can pay it right now, so the user can decide; a value that is neither is refused before the service is contacted. Self-pay is on offer where the policy contracts are deployed (testnet today); elsewhere nothing changes and `"self"` is refused. A self-paid write is the owner's own `createOrUpdatePolicy` call on the Policy Registry, built here and signed through Wallet BE, and the policy service is never contacted. The wallet reads the chain first (a policy that already exists, or one that has changed since you read it, is refused for free), checks the ZTX balance covers the estimated gas plus a reserve (a balance or estimate it cannot read is not a yes), and reports `written` only when the chain then holds exactly what was sent: the same attributes, template and validity window, and for an update `updatedAtBlock` moved. Otherwise it reports `submitted` with the `txHash`: do not send it again, and `get_my_policy` shows when it lands. Wallet BE refusing to sign is a decision (`policyDenied`); its check failing is transient (`policyCheckUnavailable`). Nothing is sent when the node's own simulation rejects the call. Without `confirm: true` it quotes the gas and sends nothing. **A self-paid send also needs `maxGasRaw`**, the `requiredRaw` of the self option the user was shown (the most gas they agreed to): without it, or when the gas now costs more than it, nothing is signed and the refusal gives the current figure to show and what to call with, so the fee limit signed can never exceed what the user saw. **A self-paid send is remembered until its outcome is settled:** the transaction carries an expiry (60 ledgers past the current one), a small note (policy key, nonce, transaction hash, expiry) is written before it is submitted, and a later write or update for the same key is refused, with nothing sent, while the transaction could still land, and so is a sponsored write, the collect of an already-paid write and `remove_policy` (the self-paid transaction would replace the whole policy when it lands, so nothing else may write or remove in the meantime; nothing is paid or removed). The note holds the owner, policy key, operation, nonce, transaction hash, expiry and send time (no secret), is kept per network, and is removed only by its own transaction. Every confirmed write, collect or removal for a key in a process waits for the one in front of it for up to 15 seconds, and then refuses with nothing sent, paid, collected or removed, so a call never acts after its client could have given up. The note is cleared when the transaction is found on chain (landed or failed) or the ledger has passed its expiry, so an outcome that never arrived is settled in bounded time. A send is refused if the ledger number cannot be read or the note cannot be written, and a note that exists but cannot be read (unreadable, empty or damaged, as a crash can leave it) is never read as "no note": the call stops and names the file, which a person can delete once 60 ledgers (about 10 minutes) have passed since it was last changed, or once `get_my_policy` shows the write. A note is deleted only after at least 15 minutes and with the chain producing ledgers. **With `gasPayer "self"` there is no `paymentReceipt` and `check_policy_write` does not apply:** `submitted` means the network gas is spent (do not send it again; `get_my_policy` shows when it lands, and quote the `txHash` if it never does), `unknown` means it may be on chain (do not send it again; quote the nonce), and `write_failed` means the transaction was included and rejected (the gas is spent and nothing was written). The gas is spent even when the write fails on chain. The wallet signs with the fee ceiling it checked the balance against, and refuses (nothing signed) if the node's fresh estimate is above it. A pending paid write for the same `policyKey` blocks a self-paid one (finish it with `check_policy_write` first), and so does an attribute vocabulary that could not be read or that no source is configured for (the vocabulary comes from the policy service, so self-pay needs it), because a self-paid write has no service to catch what it would have. A receipt the service said failed does not hold a self-paid write back; one with an unresolved outcome does. When neither way of paying is available the answer is `unavailable` and names both reasons. A list attribute is capped at 500 entries and a policy at 64 attributes. A wallet that already has a policy can self-pay too: Wallet BE (a current version) signs the owner's own Registry call. A Wallet BE older than that refuses it (`UNSUPPORTED_OPERATION`, a decision made before any policy is consulted), which is reported as final, not about the policy's limits, with no invitation to retry. A self-paid update is a whole-policy replacement that the contract does not compare with what was there, so another write by the same owner landing in the few seconds between the wallet's last chain read and the block that includes the transaction is overwritten (the wallet still reports `written` because its own content is there and `updatedAtBlock` moved). A self-paid write has not yet been run end to end against a live chain. | `{ policyKey, attributes, expectedUpdatedAtBlock, validFromBlock?, validToBlock?, amountUnit?, confirm?, dryRun?, gasPayer?, maxGasRaw?, pollBudgetMs? }` | `{ state, message, paymentReceipt?, policyKey?, txHash?, paid?, payFresh?, quote?, affordability?, interpretation?, gasOptions?, needsConfirmation?, policyDenied?, policyCheckUnavailable?, upstream? }` |
|
|
38
|
+
| `remove_policy` | **Free, but it lifts every limit the policy set** — Wallet BE then signs spends of that asset without any limit — so nothing is sent without `confirm: true`. Without it you get `needs_confirmation` with what the policy currently limits. `removed` is the only state that means the policy is gone, and it is reported only when a chain read no longer holds it; `submitted` is a removal on its way, **not** a removed policy. Repeating the call never submits a second removal. `not_found` means there was nothing to remove (or it is already gone); `in_progress` means a paid update has not been collected yet (`check_policy_write` first); `refused` naming a self-paid transaction means an earlier self-paid update may still land and would bring the old policy back, so nothing was removed (wait until the ledger it names, or read the policy with `get_my_policy`) | `{ policyKey, confirm?, pollBudgetMs? }` | `{ state, message, policyKey?, txHash?, currentAttributes? }` |
|
|
39
39
|
| `check_policy_write` | **Free — never pays, on any path.** Finish a policy write or update that has already been paid for, from its `paymentReceipt` (or the most recent pending one when omitted). This is the right answer to “did my policy get created?”; calling `write_policy` again is not. A receipt this wallet does not recognise does **not** mean the write failed — the service completes a paid write on its own, and asking to write the same `policyKey` again reports the truth for free before any payment. When a collect ends with an error that says nothing about the write (a gateway timeout, a 5xx), the wallet reads the chain itself (up to 45 seconds, less when the poll has already used part of its budget; each read is cut off at 10 seconds) and reports `written` only when the chain holds exactly the attributes that were submitted, under the same template (for an update, with `updatedAtBlock` moved). That covers every answer the wallet cannot read: a 5xx, a gateway page, the service's own "unknown", and any other status it does not recognise (a 4xx included); the service's own "the chain rejected it" is never second-guessed. `check_policy_write` looks at the chain first for a receipt that still carries what to verify, so a write that already landed costs no collect. A receipt stops being verified against the chain once the service has said its write failed, or once a newer paid write for the same `policyKey` has been made, and a receipt bought by another owner is never checked against this owner's policy. An error answer is returned apart in `upstream: { status, detail }` (control characters replaced, at most 200 characters) | `{ paymentReceipt?, pollBudgetMs? }` | `{ state, message, paymentReceipt?, policyKey?, txHash?, paid?, payFresh?, upstream? }` |
|
|
40
40
|
| `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, message, replacedExpiredVc? }` (`expiresIn` / `expiresInSeconds` are worked out by the wallet from its own clock, so an agent quotes `expiresIn` instead of doing timezone arithmetic itself; `message` carries a "Tell the user:" sentence about following up, worded to match how much time is actually left on the link), `{ 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 |
|
|
41
41
|
| `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 |
|
|
@@ -204,7 +204,7 @@ Node ≥ 18 required (built-in `fetch`).
|
|
|
204
204
|
| `ZID_RESOLVER_BASE_URL` | no | ZID resolver override (auto-derived from network: sandbox for testnet, prod for mainnet) |
|
|
205
205
|
| `MAX_PAYMENT_AMOUNT` | no** | Per-asset x402 auto-pay cap — JSON `{ "<asset>": "<maxRawUnits>", "*": "<fallback>" }`. `pay_and_fetch`/`subscribe_and_issue` are asset-agnostic: the resource server's 402 challenge may quote the native ZETRIX token (asset code `ZTX`) **or** a ZTP20 token (e.g. `JMYR`) — cap whichever assets you expect. **Either the ticker or the contract address works** — the challenge identifies a ZTP20 token by its contract address, and the wallet resolves a known ticker (e.g. `JMYR`) to it. If both are written for the same asset, the contract address wins. An unrecognised ticker still matches nothing and falls through to `"*"`. e.g. `{"ZTX":"1000000000","ZTX3WeinXtt28YMyr4vUZ14ddTgEMGeuc1e6b":"5000000","*":"0"}`. **Default when unset:** both networks allow exactly the AI Birthcert fee (1 JMYR), keyed to that network's JMYR contract, and refuse everything else. **The cap is per call, not cumulative** — so an unconfigured wallet, mainnet included, can pay that fee once per call with no overall ceiling. Set this explicitly to lock a wallet down. **A spending policy stands the DEFAULT cap aside for an asset it governs** (an `assetScope` for that asset, a `perTransactionMax`, and no block range), so the owner's own limit applies instead of a 1 JMYR default underneath it; an explicit `MAX_PAYMENT_AMOUNT` is never bypassed, and a failed read of the policy list leaves the cap in force. The policy is only as strong as Wallet BE's enforcement of it, which is off in prod as shipped. **The agent could write such a policy itself**, so `write_policy` pays and writes only with `confirm: true`, which the user must have given after seeing the interpretation and the price; the wallet cannot verify a person said yes, it is the same instruction-level gate `transfer_token` uses. Collecting a write that was paid for elsewhere needs the same confirmation, and `pay_and_fetch` refuses the policy-write service's paid paths. |
|
|
206
206
|
| `ZETRIX_WALLET_STATE_DIR` | no | Where the wallet keeps `account.json` and its VC cache. Defaults to `~/.agentic-wallet-mcp` |
|
|
207
|
-
| `SSIVC_BASE_URL` | no | myid's SSIVC API base URL, for the Verified AI Birthcert flow (`request_ai_birthcert_verification`/`check_ai_birthcert_verification`). **
|
|
207
|
+
| `SSIVC_BASE_URL` | no | myid's SSIVC API base URL, for the Verified AI Birthcert flow (`request_ai_birthcert_verification`/`check_ai_birthcert_verification`). **Testnet: derived** (`https://ssivc-api-uat.myegdev2.com/api`). **There is no default on mainnet**: its candidate host (`https://verifyid-api.zetrix.com/api`) was never confirmed, so it is kept in code but not used and the Verified AI Birthcert check is off there until `SSIVC_BASE_URL` is set. Override on either network if the host changes |
|
|
208
208
|
| `MYID_VERIFY_LINK_TEMPLATE` | no | The link `create_verification_qr` gives the user, with `{referenceId}` where the MBI reference id goes — MyID's universal link, e.g. `https://<MyID link domain>/<path>?referenceId={referenceId}` — must be `https`, with the placeholder in the path or query (not the host); a template that breaks this is warned about at startup and refused by the tool. **Defaults on testnet** to the UAT link the MyID app is registered for, `https://ssivc-api-uat.myegdev2.com/api/agentic-verify?referenceId={referenceId}` (read from that host's `apple-app-site-association`, which claims exactly that path and query). **No default on mainnet** until MyID's production side is verified: MyID has stated `https://myid-verifier.zetrix.com/api/agentic-verify?referenceId={referenceId}`, but it was not deployed when this was written, so the tool refuses before it creates anything rather than hand out a link that may not open. Set this variable to use it (or any other link) there; it also overrides the testnet default |
|
|
209
209
|
| `POLICY_DECISION_URL` | no | Base URL of the policy DECISION service, for `check_policy_decision`. **No default**: the endpoint sits behind an authenticated path this wallet has no credential for, so guessing a host would turn "nobody has wired this up" into a connection error. With none configured the tool answers `undetermined` and says why |
|
|
210
210
|
| `POLICY_DECISION_AUTH` | no* | `Authorization` header value for the policy decision service (e.g. `Bearer …`). Without it the service answers 401 and the tool reports that the wallet is not authorised — not that the request was wrong |
|