agentic-wallet-mcp 0.12.3 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +332 -0
- package/README.md +37 -13
- package/dist/server-bundle.cjs +5296 -311
- package/package.json +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,338 @@ 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
|
+
## [Unreleased]
|
|
12
|
+
|
|
13
|
+
## [0.14.0] — 12 October 2026
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- **`credential_preflight` now says when the wallet already holds the credential, before the agent asks the user for anything.** A user with a
|
|
18
|
+
valid Verified AI Birthcert asked for one, was told "no blockers, 1 JMYR", and was asked to choose an agent name: the only check for an existing
|
|
19
|
+
credential was in the paid `request_ai_birthcert_verification` call (the free quote path skips it on purpose, so a price can be read while a
|
|
20
|
+
session is in flight), and preflight never looked at what the wallet holds. Preflight now looks, locally and for free, at the credentials the
|
|
21
|
+
wallet has saved; if a valid copy is held it stops with `ready: false` and `alreadyHeld` (what it is, its id, its expiry), prices nothing, and
|
|
22
|
+
tells the agent not to ask for a name or start an issuance. A new one would replace it and cost the fee, so only when the user explicitly wants a
|
|
23
|
+
replacement does the agent run it again with `replacing: true`. Works for the Basic AI Birthcert and any template credential too. An expired
|
|
24
|
+
credential does not count; a lookup that fails is reported in `notChecked`, never as "nothing is held". It does not see a credential that was
|
|
25
|
+
paid for and issued but never collected with `check_ai_birthcert_verification` — the paid call's own guard still covers that.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- **The UAT host moved to `ssivc-api-uat.myegdev2.com`.** Both testnet defaults that used `ssivc-api-uat.myegdev.com` now use the new host: `SSIVC_BASE_URL` (the Verified AI Birthcert flow) and `MYID_VERIFY_LINK_TEMPLATE` (the `create_verification_qr` universal link, `https://ssivc-api-uat.myegdev2.com/api/agentic-verify?referenceId={referenceId}`). The new host serves the same `apple-app-site-association` (`/api/agentic-verify` with a `referenceId` query), the same `assetlinks.json` (`com.zetrix.myid.uat`) and the same "open on your phone" page. Anyone who set either variable to the old host explicitly should update it; the old host is no longer the default.
|
|
30
|
+
|
|
31
|
+
- **`create_verification_qr` has a built-in link on testnet (UAT), and it is the one the MyID app is registered for.** `MYID_VERIFY_LINK_TEMPLATE`
|
|
32
|
+
now defaults to `https://ssivc-api-uat.myegdev2.com/api/agentic-verify?referenceId={referenceId}` on testnet, so nobody has to set it. It was read from
|
|
33
|
+
the host's own `apple-app-site-association`, which claims exactly `/api/agentic-verify` with a `referenceId` query (plus an `assetlinks.json` for
|
|
34
|
+
`com.zetrix.myid.uat`), and that address serves an "open this on your phone" page. The first link the wallet was given,
|
|
35
|
+
`/v1/agent-verification/{referenceId}`, is claimed by neither: a phone would not hand it to the app, and the server answers 404. **Mainnet has
|
|
36
|
+
no default** until MyID's production side is verified (MyID stated `https://myid-verifier.zetrix.com/api/agentic-verify?referenceId={referenceId}`, but it was not deployed and its host sits behind a Cloudflare browser
|
|
37
|
+
check, so the association files could not be read): the tool refuses before it creates anything on MBI, as for any unverified mainnet value, and the stated link is
|
|
38
|
+
kept as `UNVERIFIED_MAINNET_MYID_VERIFY_LINK_TEMPLATE`. Set `MYID_VERIFY_LINK_TEMPLATE` to opt in. The variable still overrides the default on either
|
|
39
|
+
network, and only a value someone set is warned about at startup.
|
|
40
|
+
|
|
41
|
+
### Added
|
|
42
|
+
|
|
43
|
+
- **`write_policy` and `update_policy` take a `gasPayer` (`"sponsored"` or `"self"`), and the choice is the user's (phase 1).** `"sponsored"` is what both tools do today: the fee in JMYR, the gas covered. `"self"` is for a wallet that builds and signs the contract call itself: no fee, only the network gas in ZTX. The wallet never picks and has no default: when self-pay is on offer and a call names no `gasPayer`, it stops at the new state `needs_choice` (nothing paid or written, even with `confirm: true`) and lays out both options in `gasOptions`: the JMYR fee and whether the wallet holds it, and the estimated ZTX gas (the estimate plus a fifth is what the wallet asks to hold) and whether the ZTX balance covers it, each unknown rather than a yes when it cannot be read. A `gasPayer` that is neither value is refused before the service is contacted. **Self-pay (phase 2).** Where the policy contracts are deployed (testnet today) the wallet makes the owner's own `createOrUpdatePolicy` call on the Policy Registry itself: it builds the call, signs it through Wallet BE and submits it, paying only network gas in ZTX, with no service fee and no call to the policy service. Before sending it reads the chain (an existing policy, or one that changed since it was read, is refused for free), asks the node to simulate the exact call (a contract rejection costs nothing), and refuses unless the ZTX balance covers the estimated gas plus a reserve. It reports `written` only when the chain then holds exactly what was sent (same attributes, template and validity window; for an update `updatedAtBlock` moved), and `submitted` with the `txHash` otherwise. A list attribute (written as a JSON-array string) goes on chain as an array of strings, an attribute with no `attributeType` is refused rather than guessed, and nothing is retried after a transaction may have been sent. Wallet BE refusing to sign is reported as a decision (`policyDenied`), its check failing as transient (`policyCheckUnavailable`). Where the policy contracts are not deployed nothing changes. **Not yet run against a live chain:** whether the fee is charged on the gas used rather than on the fee limit (the transaction is signed with the ceiling the balance was checked against), how a failed transaction is charged, and whether a base reserve applies on top of the fee limit are confirmed only by a testnet run. **What the testnet run settled:** an older Wallet BE refuses any contract call that is not a ZTP20 `transfer` from an owner who already has a policy contract (`UNSUPPORTED_OPERATION`, decided before any policy is consulted); a later Wallet BE removes that refusal and was verified on the sandbox (the owner's `createOrUpdatePolicy` blob is signed). **The wallet no longer holds self-pay back for a wallet that already has a policy** (the check that asked the Registry whether the owner has a policy contract is removed, so an update can be self-paid again); a refusal from an older Wallet BE is still reported as final, matched on the refusal kind as well as its text, not about the policy's limits, with no invitation to retry. **After review (round 2):** the transaction lookup now reads the SDK as it really answers (every nested number is a string, so a code of "151" is a rejection and "0" a success, and a transaction the node names differently is not the one asked about), which is what makes the on-chain `write_failed` reachable at all; a receipt the service said failed no longer holds a self-paid write back; a wallet with no vocabulary source does not self-pay; when neither way of paying is available the answer is `unavailable` with both reasons; the lookup is bounded like the reads; and the wording for a transaction that succeeded on chain no longer says it may have failed. After review: the transaction is signed with the fee ceiling the balance was checked against (a fresh estimate above it, or a malformed one, is refused with nothing signed); `gasPayer "sponsored"` only ever uses a quote that really covers the gas; a transaction that was included and rejected by the contract is reported as `write_failed` (the gas is spent and nothing was written) instead of "not a failure", by looking it up by its hash; a pending paid write for the same key, or an attribute vocabulary that could not be read, blocks a self-paid write; an estimate of zero, a balance that is not ZTX in 6 decimals, or a null from either is `unknown`; the refusal reason is carried through; a list attribute is capped at 500 entries and a policy at 64 attributes; self-pay needs the attribute vocabulary, which comes from the policy service, so with no service URL the tools are not built at all (nothing there could write), and with a service but no vocabulary reader self-pay is refused with its own reason; and the agent-facing text now says that a self-paid result has no `paymentReceipt`, that `check_policy_write` does not apply to it, that its gas is spent even when the write fails.
|
|
44
|
+
|
|
45
|
+
- **`valueHuman`: give one amount in whole tokens, and the wallet does the multiplication.** On any attribute of `policy_preflight`,
|
|
46
|
+
`write_policy` and `update_policy`, `valueHuman: "100"` stands in for `value` and means 100 whole tokens. The wallet converts it with
|
|
47
|
+
the asset's own on-chain decimals (native ZTX, or the token named by `tokenAddress`), returns the raw value in `convertedAmounts`, shows
|
|
48
|
+
both forms in `interpretation` ("perTransactionMax: 100 JMYR is written as 100000000"), and never sends `valueHuman` to the service.
|
|
49
|
+
What counts as an amount comes from the service's served vocabulary (`unit: SMALLEST_UNIT`) when it can be read, and from the built-in
|
|
50
|
+
list when it cannot; an attribute the service calls a COUNT or a DURATION is never scaled, whatever the built-in list believes
|
|
51
|
+
(`maxTransactionCount` and the windows are refused). Anything it cannot convert exactly is refused with nothing converted: unknown
|
|
52
|
+
asset, unreadable decimals, more decimal places than the token has, an exponent, a sign, a separator. A `value` given alongside must agree.
|
|
53
|
+
It is applied BEFORE `amountUnit`, which then leaves it alone, so a human amount is never converted twice. The tool descriptions now
|
|
54
|
+
also say that `maxTransactionCount` is a count, not an amount.
|
|
55
|
+
|
|
56
|
+
- **`update_policy` and `remove_policy`: users manage a policy through the agent after it is deployed.** `write_policy` is create-only
|
|
57
|
+
(a second call with the same key answers `already_exists`); these add the other two on top of ms-zetrix's x402 routes.
|
|
58
|
+
- **`update_policy`** replaces a policy's attributes and validity window and **pays the update fee** (through the same payment cap and
|
|
59
|
+
`confirm: true` gate as `write_policy`, with the same `dryRun`). It takes the policy's `expectedUpdatedAtBlock` from
|
|
60
|
+
`get_my_policy` (new `forUpdate.expectedUpdatedAtBlock`, already a string — the chain returns a number and the service wants a string),
|
|
61
|
+
carries the policy's template forward rather than taking one, and keeps its validity window unless a new one is given (the service does
|
|
62
|
+
not default an omitted bound, so sending nothing would have stripped an existing expiry). It reads the policy first and checks the draft
|
|
63
|
+
against the policy's own template. Every refusal that can be known first is free and says nothing was paid: `not_found`, `modified`
|
|
64
|
+
(the policy changed since it was read), `template_unavailable`, `refused`. A paid update that the chain then rejects is `write_failed`
|
|
65
|
+
and says the policy is **unchanged** and a payment may have been taken.
|
|
66
|
+
- **`remove_policy`** is free but lifts every limit the policy set, so nothing is sent without `confirm: true`; the unconfirmed call
|
|
67
|
+
returns what the policy currently limits. It is reported as `removed` **only when a chain read shows the policy gone** — a submitted
|
|
68
|
+
transaction is `submitted`, never "removed" — and repeating it never submits a second removal.
|
|
69
|
+
- **`check_policy_write`** now collects an update receipt on the update route: the receipt bookmark records which write it pays for
|
|
70
|
+
(`operation`), and one saved before this change reads as a create.
|
|
71
|
+
- **`write_policy`'s default `requestKey`** is now a sha256 hex string. The old `owner-policyKey-timestamp` overflowed the service's
|
|
72
|
+
128-character column for a policy key longer than about 65 characters.
|
|
73
|
+
- A write the service says is already paid for is collected on the route **its own operation** names, which may not be the write just
|
|
74
|
+
asked for (previously it was always collected as a create).
|
|
75
|
+
- **The service's own description of every policy attribute.** ms-zetrix now publishes the vocabulary at
|
|
76
|
+
`GET /policy/vocabulary` (version `v1`, sixteen attributes, each with a description, unit, role, what it
|
|
77
|
+
pairs with and what an empty list or a use outside its scope means). `get_policy_template_schema` returns
|
|
78
|
+
it beside `declared` as `attributeMeanings`, so an agent can say what a rule means instead of guessing, and
|
|
79
|
+
`policy_preflight` (and the check `write_policy` runs before paying) uses it to refuse what the built-in
|
|
80
|
+
rules cannot know: an attribute the service does not recognise (`unknownAttributePolicy` defaults to
|
|
81
|
+
deny, so it refuses every transfer) and an attribute used outside the scope it applies to
|
|
82
|
+
(`allowedMethods` or `approvalPolicy` on a native policy). When the service and the built-in rules
|
|
83
|
+
disagree about a role or about what a missing window means, that is reported in `notChecked` and the
|
|
84
|
+
STRICTER of the two answers is used, so a service label can tighten the "enforceable" refusal but never loosen
|
|
85
|
+
it. The read is strict (one malformed attribute fails the whole read, and an unknown version is refused),
|
|
86
|
+
bounded (descriptions are flattened to one line, stripped of invisible formatting characters such as bidi
|
|
87
|
+
overrides, cut at 600 characters, and shown as quoted text from ms-zetrix that describes and does not
|
|
88
|
+
instruct; "bounded, not scrubbed"), never follows a redirect, refuses a declared oversize body before reading
|
|
89
|
+
it, cached (ten minutes for a good answer, one for a failure) and never throws. **If it
|
|
90
|
+
cannot be read, nothing else changes**: every existing preflight result is as before plus one `notChecked`
|
|
91
|
+
line. Known limit: the route is behind a Cloudflare bot challenge that, as observed on 2026-10-06, some
|
|
92
|
+
non-browser clients (Node `fetch`, Git's OpenSSL curl) receive as an HTML page with HTTP 403 and
|
|
93
|
+
`cf-mitigated: challenge`; that is reported as `challenged` and the built-in rules are used until it is
|
|
94
|
+
exempted.
|
|
95
|
+
|
|
96
|
+
- `create_verification_qr`: gives a human a link and a QR code that open the agent's credential in the MyID app. The wallet presents the credential to MBI, and the link holds only the reference id — which grants access to the revealed attributes until it expires, so by default only a standard minimal set is revealed — Verified AI Birthcert: `agentName`, `evidenceProvider`, `ownerVerified`; Basic AI Birthcert: `agentUsername` — never the owner's name, id or date of birth; other attributes need `revealAttribute`, and everything needs `revealAll: true`, which the agent uses only when the user explicitly asks. A Basic credential's result says it does not mean the owner was verified. The link is built from `MYID_VERIFY_LINK_TEMPLATE` (MyID's `https` universal link, with `{referenceId}` in the path or query); with none set, or one that breaks those rules, the tool refuses before anything is created on MBI, and a bad template is warned about at startup. A credential whose subject is not this wallet is refused locally. The link stays openable for 5 minutes by default, up to 60. With no `vc` given it presents the Verified AI Birthcert if the wallet holds one, otherwise the Basic AI Birthcert (the result says which, in `credentialUsed`), and if it holds neither it answers `created: false` and says to create one first; no other credential is picked for the caller. A refusal for a path the credential does not have lists the attribute names it does have. The OpenClaw plugin gains a `myidVerifyLinkTemplate` setting that is forwarded to the wallet. The named paths must each be a single attribute of the credential: a typo or a parent path is refused, and so is any path the wallet cannot check — a credential that is not an object, has no plain `credentialSubject`, or has an attribute name containing a dot — in which case the caller passes the credential object or uses `revealAll`. They are sent de-duplicated and in the credential's own order, and the result states what was revealed (`revealed`). The template is judged as a browser would read it: only printable ASCII, no backslash, no `@` before the host, and not containing the text `refidmarker`.
|
|
97
|
+
|
|
98
|
+
- **A unit guard, and `amountUnit`, for policy amounts.** A real policy went on chain with
|
|
99
|
+
`perTransactionMax: 1` and `cumulativeMax: 100` for a 6-decimal token (JMYR) — 0.000001 and 0.0001 of
|
|
100
|
+
it, a million times tighter than the "1 JMYR" and "100 JMYR" the user meant. Amounts are raw base units,
|
|
101
|
+
and nothing made that visible before the payment. Now a non-zero `perTransactionMax`, `cumulativeMax` or
|
|
102
|
+
`velocityCap` under one whole token is refused by `policy_preflight` and `write_policy` unless
|
|
103
|
+
`amountUnit` says what is meant: `"whole"` gives whole-token amounts (`"1"`, `"0.5"`) which the wallet
|
|
104
|
+
converts by the token's decimals, returns in `convertedAmounts` and states in `interpretation` — and
|
|
105
|
+
`write_policy` then writes the converted raw values, so what is paid for is what the user saw; `"base"`
|
|
106
|
+
confirms a tiny raw value is intended. Silence is not an acknowledgement. Conversion refuses to guess:
|
|
107
|
+
unreadable decimals, a missing scale, more decimal places than the token has, or anything that is not a
|
|
108
|
+
plain number all refuse the draft. Counts (`maxTransactionCount`) are not amounts and are untouched.
|
|
109
|
+
A `"whole"` draft in which every amount is already 10^decimals tokens or more is refused too, because
|
|
110
|
+
that is what an already-converted value looks like and converting it again would write a cap a million
|
|
111
|
+
times looser; `convertedAmounts` is for showing the user and must never be sent back as the values.
|
|
112
|
+
Because a genuine cap that large looks identical, the refusal names BOTH readings with the exact raw value
|
|
113
|
+
for each (resend a copied raw value unchanged with `"base"`; send a real large cap as the value times
|
|
114
|
+
10^decimals with `"base"`). It needs every amount to look raw, so a copied draft that also contains a zero
|
|
115
|
+
or sub-token amount is not caught. Raw amounts are also bounded to 77 digits, the most a 256-bit number has.
|
|
116
|
+
Token decimals are now typed strictly and bounded (0–36) at the read: `null`, an empty string, `false`
|
|
117
|
+
or an array used to read as a genuine 0-decimal token, and an absurd value made preflight throw or
|
|
118
|
+
stall. The guard cannot catch a raw value that is already one whole token or more (`interpretation` states
|
|
119
|
+
what such an amount means), and does nothing when the decimals cannot be read (`notChecked` says so).
|
|
120
|
+
- **`policy_preflight` no longer leaves a token policy to guesswork.** A real transcript ("limit 1 JMYR
|
|
121
|
+
per transaction, 100 a week") exposed three gaps. (1) The agent asked the user for the JMYR contract
|
|
122
|
+
address, which the wallet already holds: `wallet_status({ token })` now returns it as `tokenAddress`,
|
|
123
|
+
a `ztp20` draft with no `tokenAddress` names the registered ones, and a token symbol written where
|
|
124
|
+
the address belongs is refused with the address to use (the generic "ask the user, do not correct it"
|
|
125
|
+
advice is suppressed for that case, since correcting a registered symbol is the right move). (2) The
|
|
126
|
+
amounts were never stated in whole tokens, so `1` of a 6-decimal token — 0.000001 of it — read as
|
|
127
|
+
"1 JMYR"; `interpretation` now says what each amount means, and says so plainly when the decimals
|
|
128
|
+
cannot be read instead of assuming a scale. (3) Preflight accepted `cumulativeWindow: "week"`, which
|
|
129
|
+
the write service refuses: every `*Window` must be a duration such as `7d`, `12h` or `30m` (or ISO
|
|
130
|
+
`P7D`) — not a word, not a bare number (which would be read as milliseconds), and not padded with spaces,
|
|
131
|
+
and `interpretation` states each window's period in words, because `1M` is one minute, not one month. The window grammar is
|
|
132
|
+
restated from the service, so it is a tripwire rather than a guarantee; a window over the default 30d
|
|
133
|
+
retention is noted, not refused, because that limit is environment-specific.
|
|
134
|
+
|
|
135
|
+
- **The wallet can now find a policy template on its own.** Asking it to "show me the policy
|
|
136
|
+
template" on a wallet with no deployed policy used to go nowhere: the tool described a template id
|
|
137
|
+
as something found "inside a deployed policy", a first-time user has none, and the only way to get
|
|
138
|
+
one is to write a policy — which needs the id. The agent ended up asking the user for a publisher
|
|
139
|
+
and a template contract address, neither of which a user can be expected to know.
|
|
140
|
+
|
|
141
|
+
`get_policy_template_schema` with no arguments now lists the default publisher's templates, each
|
|
142
|
+
with its declared attributes and the `templateId` that `write_policy` needs. A single template can
|
|
143
|
+
be read by key alone, and another publisher's listed by naming it. An id is only ever returned
|
|
144
|
+
once the chain has confirmed it — it is derived from the publisher and key, and one that does not
|
|
145
|
+
resolve is withheld. The listing is bounded and says what it left out.
|
|
146
|
+
|
|
147
|
+
New optional setting `POLICY_TEMPLATE_PUBLISHER`. The default is derived per network, and there is
|
|
148
|
+
none where no policy module is deployed.
|
|
149
|
+
|
|
150
|
+
- **`policy_preflight` no longer approves drafts the write service refuses.** A real transcript
|
|
151
|
+
drafted "a 10 JMYR per-transaction max" as `assetScope: "JMYR"` on the native template and got
|
|
152
|
+
`ready: true` and "limited to JMYR" — for a draft the service rejects twice over. Preflight now
|
|
153
|
+
applies the service's scope rules: `assetScope` must be exactly `native` or `ztp20` (never a token
|
|
154
|
+
symbol), a `ztp20` policy needs a `tokenAddress` and says plainly when the chosen template cannot
|
|
155
|
+
express one, and an amount or count cap needs an `assetScope`. No meaning is offered for a scope it
|
|
156
|
+
is refusing. These are restated from the service, so they are a tripwire rather than a guarantee.
|
|
157
|
+
|
|
158
|
+
- **`policy_preflight` refuses a repeated attribute name.** The scope and token rules read the first
|
|
159
|
+
attribute with a given name, so `assetScope: "native"` followed by `assetScope: "JMYR"` was read as
|
|
160
|
+
ready. Which value the write service would take is not knowable from the wallet, so a repeat is refused
|
|
161
|
+
rather than guessed at.
|
|
162
|
+
|
|
163
|
+
- **`write_policy` can quote its price first.** `dryRun` runs every free check and returns what the
|
|
164
|
+
service asked for, paying, collecting and writing no policy — including never collecting a write that
|
|
165
|
+
has already been paid for (it only keeps a local bookmark so `check_policy_write` can finish it). Previously the price could only be learned by paying it.
|
|
166
|
+
|
|
167
|
+
- **`write_policy` `dryRun` now says whether the wallet can afford the quote.** (The network fee is not
|
|
168
|
+
estimated, and a payment made in ZTX needs the amount plus that fee from one balance, so a ZTX balance
|
|
169
|
+
equal to the quote is short and `feeNotEstimated` says when "affordable" means only "holds the amount".) It reads the fee-asset
|
|
170
|
+
balance, ZTX for gas (only when the payer would check it) and the payment cap the policy payer
|
|
171
|
+
carries, and reports each shortfall separately under `affordability`. A balance that cannot be read
|
|
172
|
+
is `unknown`, never `affordable`. Nothing is paid, collected or written, and the check is not run on
|
|
173
|
+
a real deploy.
|
|
174
|
+
|
|
175
|
+
### Fixed
|
|
176
|
+
|
|
177
|
+
- **A collect that answers with an error no longer leaves a landed write as "unknown".** On staging an `update_policy` was paid and the service finished the collect, but it took 12.7 seconds and something in front of it answered HTTP 500 at about 10; the wallet could only say "unknown, ask again later", and asking again spends one of the service's few collect retries, while the chain already held the update. Now, when the collect step ends with an error that says nothing about the write (a 5xx, a gateway page, a status this wallet does not recognise, or the service's own "unknown"), the wallet reads the chain for up to 45 seconds, which is free and spends no retry, and reports `written` when the chain holds **exactly** the attributes that were submitted (for an update, with `updatedAtBlock` moved from the value it had before). A policy that holds the same attributes but did not move, one with an extra or missing or repeated attribute, an unreadable chain, and the service's own "the chain rejected it" are all left as they were: the wallet says `written` from a chain read only on exactly that (and, for a create, under the same template). The answers that start the read include every status the wallet does not recognise, a 4xx too. `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 once the service has said its write failed, or once a newer paid write for the same `policyKey` is made (so a later, separately paid write of the same values is never credited to an older receipt), and a receipt bought by another owner is never checked against this owner's policy. The read gets what is left of the poll budget (at least 10 s, at most 45 s) and each read is cut off at 10 s. The bookmark now records the submitted attributes and the prior `updatedAtBlock` (public policy rules, no credential; a malformed record is dropped on read and the receipt stays usable). The result also carries `upstream: { status, detail }`, the service's own answer kept apart from the wallet's wording (control characters and line breaks replaced, at most 200 characters; the service's own "unknown" carries it too), and a `written` inferred from the chain carries `paymentReceipt`. **Not changed here, and not the wallet's to change:** the 500 itself. It is a gateway or ingress timeout on a collect that takes longer than it allows; raising that timeout for `/collect` (or making collect return before it finishes) is a server-side fix.
|
|
178
|
+
|
|
179
|
+
- **`valueHuman` review round 1.** A native policy that also names a `tokenAddress` is refused (the service ignores it for native payments, so it would not limit ZTX) and is never read as an amount of ZTX: before, a `valueHuman` on it was converted with ZTX decimals while the cap applied to the token, up to a million times off. An attribute with neither `value` nor `valueHuman` is refused for every type: before, a STRING attribute could reach the wire with no value. An attribute entry that is not an object is refused cleanly instead of with a raw error. An attribute the service lists with a unit other than the smallest unit is never scaled. A numeric `valueHuman` is accepted only as a safe whole number (write `"0.5"` as text), and a `value` that is not text beside a `valueHuman` is refused. The descriptions say never to put `convertedAmounts` into `valueHuman`.
|
|
180
|
+
|
|
181
|
+
- **`update_policy` / `remove_policy` review round 1.** Recovering a write that was already paid for now collects on the
|
|
182
|
+
receipt's OWN route: a receipt the wallet holds decides it, the server's `operation` must agree or nothing is collected, and an
|
|
183
|
+
update whose operation neither side names is refused instead of being collected as a create (which could lose the receipt and
|
|
184
|
+
invite a second charge). `validFromBlock: null` / `validToBlock: null` are treated as "not named" and the current bound is carried
|
|
185
|
+
forward, and a malformed bound is refused for free, so an agent can no longer strip an expiry by sending `null`. A malformed error
|
|
186
|
+
envelope (`messages` that is not an array) can no longer throw out of a payment refusal. A paid create that is still being
|
|
187
|
+
completed (409 with `errorCode` 461529) is reported as in progress, not as "the policy already exists, nothing was paid", and
|
|
188
|
+
the messages no longer promise that asking again will hand back a receipt. An unexpected 2xx from remove, and a 5xx on the update
|
|
189
|
+
pre-check, are reported as unknown or unavailable instead of "nothing was removed" / "refused"; a `getPolicy` reply with no
|
|
190
|
+
boolean `found` is a failed read, not "removed". The idempotence of `remove_policy` is described as the server's, not the
|
|
191
|
+
wallet's. Not done here, deliberately: saving the receipt before the phase-2 payment, which the server flag for owner-password
|
|
192
|
+
collection depends on; that flag must stay off until it ships.
|
|
193
|
+
|
|
194
|
+
- **Follow-ups from the vocabulary review.** The quoting around a service description can no longer be closed by a
|
|
195
|
+
double quote in the description (it is JSON-escaped, so it stays one quoted span). A pairing the service reports with a window the built-in rules do not name (a renamed
|
|
196
|
+
window) is always reported in `notChecked`, and refused when the service says that without it the cap is unenforceable or not
|
|
197
|
+
a limit at all, so a rename is never silent.
|
|
198
|
+
The `maxTransactionCount` blocker states its reading as ms-zetrix's published vocabulary as read on 2026-10-07 rather
|
|
199
|
+
than a hard-coded claim, `get_policy_template_schema` says the meanings are data from ms-zetrix and not instructions, and
|
|
200
|
+
`attributeMeaningsSource` is listed in the README.
|
|
201
|
+
|
|
202
|
+
- **A `maxTransactionCount` with no `countWindow` is refused.** It was a quiet note on a `ready: true` result, so the
|
|
203
|
+
owner paid for a policy with no count limit: the wallet's own rule says a count with no period is not requested at
|
|
204
|
+
all, and ms-zetrix describes it as refusing every transfer, and neither gives the limit the owner wrote. The fix
|
|
205
|
+
(add `countWindow`) is free, so it is now a blocker, as a `velocityCap` without its window already was. A pairing
|
|
206
|
+
the service calls unenforceable and the built-in rules call something looser is refused too. Which reading is
|
|
207
|
+
right still needs confirming with ms-zetrix.
|
|
208
|
+
- **An attribute the service does not know restricts nothing when `unknownAttributePolicy` is `"ignore"`**, so it no
|
|
209
|
+
longer counts as the policy's enforceable constraint.
|
|
210
|
+
|
|
211
|
+
- **`tokenAddress` counted as an enforceable constraint.** The service's vocabulary calls it a QUALIFIER (it
|
|
212
|
+
says which token a policy governs and caps nothing), and the wallet's built-in set did not, so a policy of
|
|
213
|
+
`assetScope` plus `tokenAddress` alone passed preflight although the service would answer
|
|
214
|
+
`NO_ENFORCEABLE_CONSTRAINTS` and refuse every transfer. It is now a qualifier.
|
|
215
|
+
|
|
216
|
+
- **The paying tools report a Wallet BE spending-policy refusal as what it is.** `transfer_token` told a policy denial
|
|
217
|
+
(`1000033`, a decision) from a check that could not complete (`1000034`, transient) from a signing failure; the four x402
|
|
218
|
+
paths did not, and Wallet BE signs the same way for all of them. `pay_and_fetch`, `subscribe_and_issue`, the Verified AI
|
|
219
|
+
Birthcert session fee and `write_policy`'s fee surfaced the raw `Wallet BE /wallet/hsm/sign-blob errorCode 1000033: …`
|
|
220
|
+
as a thrown error, with nothing to tell an agent that one is final and the other worth retrying. They now return
|
|
221
|
+
`policyDenied: true` or `policyCheckUnavailable: true` with a message that says plainly what was and was not done: the
|
|
222
|
+
refusal arrives at the signature, before the X-PAYMENT header exists, so nothing was paid, and each path adds what it
|
|
223
|
+
had already done (a quote, the free pre-check, the issuer's payment request — none moves money). Matched on the numeric
|
|
224
|
+
`errorCode` only, never on message text, and the failure text is bounded like `transfer_token`'s. A refusal is never
|
|
225
|
+
retried on the other gas option. `write_policy` reports a denial as state `refused` and an unavailable check as
|
|
226
|
+
`unavailable`, with `paid: false`. Every other failure still throws, as before. The matching moved to a shared module
|
|
227
|
+
(`policy-refusal.ts`) and `pay_and_fetch`'s payer to `orchestrator/pay.ts`, where it can be tested.
|
|
228
|
+
|
|
229
|
+
- **`transfer_token` now tells a policy refusal from a signing failure.** Wallet BE enforces the user's
|
|
230
|
+
policy at signing and answers a refusal as HTTP 200 with a numeric `errorCode`: `1000033` when
|
|
231
|
+
the policy decision service said DENY (a decision, with the reason code in the message) and `1000034`
|
|
232
|
+
when the policy check could not complete (it fails closed; transient). The wallet only recognised an HTTP
|
|
233
|
+
403 or a `policyCode` field, neither of which Wallet BE sends, so both surfaced as "signing failed" — the
|
|
234
|
+
same words as the signer being down, with nothing to tell an agent that one is final and the other is
|
|
235
|
+
worth retrying. `1000033` now yields `policyDenied: true` ("a decision, retrying will not help", with the
|
|
236
|
+
reason shown) and `1000034` yields `policyCheckUnavailable: true` ("transient, trying again shortly is
|
|
237
|
+
right"). They are matched on the numeric field only, never on message text, so a lookalike message or a
|
|
238
|
+
string code does not classify, and every other Wallet BE error is still "signing failed". The failure text
|
|
239
|
+
is bounded on every path, including an ordinary "signing failed" — bounded and flattened to one line (whitespace and
|
|
240
|
+
control characters collapse, so a remote message cannot lay out fake lines), cut by character so a surrogate pair is never
|
|
241
|
+
split, and total (it never throws), but NOT scrubbed: markup in a remote message is still shown. A bare HTTP 403 no longer counts as a policy
|
|
242
|
+
denial: it can come from a proxy or WAF, and reporting that as a final decision would be the inverse of this
|
|
243
|
+
bug; a structured `policyCode` and `errorCode` 1000033 still do. Nothing changes for an environment where
|
|
244
|
+
Wallet BE enforcement is off (the shipped default
|
|
245
|
+
for test, UAT and prod). Only `transfer_token` is changed; the x402 payment paths surface the Wallet BE
|
|
246
|
+
message as before.
|
|
247
|
+
|
|
248
|
+
- Existing tests used `cumulativeWindow: "43200"` — a bare number the service refuses — as a valid
|
|
249
|
+
window, encoding the same false assurance. They now use `12h`.
|
|
250
|
+
|
|
251
|
+
- **A gateway's 5xx on `collect` is no longer reported as the chain rejecting the write.** A real run got a
|
|
252
|
+
Spring `500` from `/collect` and then Cloudflare's "origin returned an invalid response" page as a `502`;
|
|
253
|
+
the wallet read the second as the service's own `WRITE_FAILED` and told the user the chain had rejected a
|
|
254
|
+
paid write, when nothing was known about it. The service's own 502 and 504 always carry JSON with
|
|
255
|
+
`state: "WRITE_FAILED"` / `"UNKNOWN"`, so a 502/504 now counts as the service's only when its body says so.
|
|
256
|
+
Any other 5xx is a new `server_error` outcome: state `unknown`, receipt kept, never retried (a failed
|
|
257
|
+
collect uses one of a small number of retries), no claim about the chain, and one later `check_policy_write`.
|
|
258
|
+
A gateway page is quoted as bounded plain text, and a 5xx carrying a state the wallet does not know stays on
|
|
259
|
+
the unrecognised path. The `500` itself is an unhandled exception in ms-zetrix and needs fixing there.
|
|
260
|
+
|
|
261
|
+
### Changed
|
|
262
|
+
|
|
263
|
+
- **A spending policy for an asset replaces the default wallet cap for that asset; mainnet gets the same default cap as testnet.**
|
|
264
|
+
A payment had to pass the wallet's own per-payment cap and then Wallet BE's policy, so a policy allowing 2 JMYR did not
|
|
265
|
+
raise the default 1 JMYR cap: a 1.5 JMYR payment was refused by the wallet before the policy was consulted. Now, when the
|
|
266
|
+
owner has a policy that governs the asset being paid (`assetScope` native for ZTX, or ztp20 with that `tokenAddress`, a
|
|
267
|
+
`perTransactionMax`, and no block range), the DEFAULT cap is not applied on any paying path (`pay_and_fetch`,
|
|
268
|
+
`subscribe_and_issue`, the AI Birthcert fee, `write_policy`'s fee, `transfer_token`) nor in the cap shown by
|
|
269
|
+
`credential_preflight` and the `write_policy` quote, which then say the policy governs. It fails closed: no policy
|
|
270
|
+
registry (mainnet today), a failed or timed-out read of the policy list, a policy for another asset, one with no
|
|
271
|
+
`perTransactionMax`, one that repeats `assetScope`, `tokenAddress` or `perTransactionMax`, a native policy that also names a
|
|
272
|
+
token (the service ignores it for native payments), or a policy bounded in time all leave the cap in force, and an explicit `MAX_PAYMENT_AMOUNT` is never bypassed. A good reading is cached for 30
|
|
273
|
+
seconds, so a removed policy can still stand the cap aside for up to that long. Only the cap stands aside: a malformed amount in a payment challenge is still refused. **`write_policy` now needs
|
|
274
|
+
`confirm: true` to pay and write** (without it the call returns the price, `needsConfirmation: true`, and nothing is paid),
|
|
275
|
+
because the agent could otherwise write a high `perTransactionMax` and lift its own cap in two calls; the wallet cannot verify
|
|
276
|
+
that a person said yes, it is an instruction to the agent like `transfer_token` `confirm`. The gate also covers collecting a write that was paid for elsewhere (a "already in flight" answer is neither saved nor collected without `confirm: true`, so `check_policy_write` has nothing to collect), and `pay_and_fetch` refuses the policy-write service's paid paths. A read of the policy list that times out is never cached when it finally answers, and no further read starts while it runs. **Two things this accepts, agreed with the
|
|
277
|
+
reviewer:** the general default cap on mainnet is now 1 JMYR per call (it was refuse-all, deliberately, so `pay_and_fetch`
|
|
278
|
+
could not auto-pay an arbitrary URL), with no running total; and the bypass trusts Wallet BE to enforce the policy, which it
|
|
279
|
+
does not in prod as shipped.
|
|
280
|
+
|
|
281
|
+
- **Corrected the `notChecked` text on preflight results.** It said the deploy path "is not built
|
|
282
|
+
yet", which stopped being true when `write_policy` shipped, and stated as fact that nothing consults
|
|
283
|
+
the decision service — something this wallet cannot see.
|
|
284
|
+
|
|
285
|
+
- **`write_policy` no longer asks for `templateContractAddress`.** The wallet already knows the one
|
|
286
|
+
Template contract it trusts and refuses any other before any payment, so requiring the caller to
|
|
287
|
+
supply it could only invite a wrong value. Leave it out; one that is supplied and differs is still
|
|
288
|
+
refused.
|
|
289
|
+
|
|
290
|
+
- **The wallet now signs each request it makes to MBI's `/ext` endpoints, instead of signing its own
|
|
291
|
+
address once.** Creating and submitting a presentation, and downloading the Verified AI Birthcert,
|
|
292
|
+
used to log in with a signature over the wallet's own address. That value never changes, so one
|
|
293
|
+
captured set of headers stayed valid for that address indefinitely, and MBI has marked the scheme
|
|
294
|
+
deprecated. Each call now signs its method, path, a hash of the exact body sent, and a fresh
|
|
295
|
+
timestamp, and the signature is good for that one request. MBI has accepted this since 31 July 2026.
|
|
296
|
+
If a Verified AI Birthcert download fails at the signing step, `check_ai_birthcert_verification`
|
|
297
|
+
now reports it as a retryable fetch error instead of failing outright.
|
|
298
|
+
|
|
299
|
+
## [0.13.0] — 29 September 2026
|
|
300
|
+
|
|
301
|
+
### Added
|
|
302
|
+
|
|
303
|
+
- **`check_policy_decision` — ask whether a specific spend is permitted right now.** The one policy
|
|
304
|
+
question no chain read can answer, because a cap is measured against cumulative spend held
|
|
305
|
+
off-chain. Three outcomes: `permitted`, `refused` and `undetermined`, where undetermined is
|
|
306
|
+
neither a refusal nor permission — it means nothing was evaluated, and every failure produces it,
|
|
307
|
+
so no transport error, bad envelope or unknown verdict can ever yield a permitted answer. There is
|
|
308
|
+
no step-up verdict, because the decision service has none.
|
|
309
|
+
|
|
310
|
+
**A permitted answer reserves the owner's budget for fifteen minutes**, with no way to release it
|
|
311
|
+
early, so the tool tells the agent not to poll it and not to probe amounts — every permitted
|
|
312
|
+
answer along the way reserves again, and the owner's next real payment can be refused by their own
|
|
313
|
+
agent. New optional settings: `POLICY_DECISION_URL` and `POLICY_DECISION_AUTH`. Without a URL the
|
|
314
|
+
tool answers `undetermined` and says so, rather than failing against a guessed host.
|
|
315
|
+
- **`write_policy` / `check_policy_write` — deploy a spending policy on chain, pay-gated over x402.**
|
|
316
|
+
Three phases: a free pre-check, a payment that writes nothing, and a collect that finishes the
|
|
317
|
+
write once the settlement confirms. Two tools rather than one, because the settlement queue can
|
|
318
|
+
outlast a single call. A `202` here is not a failure — between the payment and a confirmed
|
|
319
|
+
settlement, a payment has been made and no policy exists yet, and that window is the normal case;
|
|
320
|
+
`settling` and `submitted` both mean a payment has been made, so the agent is told to call
|
|
321
|
+
`check_policy_write` rather than retry `write_policy`. `submitted` carries a `txHash` and is still
|
|
322
|
+
not a written policy — the block hasn't confirmed it. A `receipt_void` result is the one state
|
|
323
|
+
where paying again is correct.
|
|
324
|
+
|
|
325
|
+
### Fixed
|
|
326
|
+
|
|
327
|
+
- **The wallet's own follow-up guidance no longer promises the agent will "check again shortly" on
|
|
328
|
+
its own.** QA saw an agent claim it had "set up an automation to check every 60 seconds" after a
|
|
329
|
+
Verified AI Birthcert session came back pending — no such capability exists. Investigation found
|
|
330
|
+
the wallet's own ready-made "Tell the user:" sentences ended the same way, and the new hardening
|
|
331
|
+
text in the tool descriptions had nowhere near the reach of a sentence the model is told to relay
|
|
332
|
+
verbatim. Every such sentence now asks the user to message back, never claims the agent will act
|
|
333
|
+
between messages, and — since a link that has already expired (or is about to) makes "message back
|
|
334
|
+
in a few minutes" actively wrong — the wording now depends on how much time is actually left on the
|
|
335
|
+
verification link.
|
|
336
|
+
- **`request_ai_birthcert_verification` and `subscribe_and_issue` now explicitly instruct the agent
|
|
337
|
+
to ask the human owner for an agent name, rather than inventing one.** The same QA session showed
|
|
338
|
+
an agent silently picking its own agent name in both the Verified and Basic AI Birthcert flows.
|
|
339
|
+
The "ask the user, never invent" rule previously lived only in a skill-layer document that not
|
|
340
|
+
every MCP client loads; it is now in the MCP tool schemas themselves, including at the specific
|
|
341
|
+
field the model fills in.
|
|
342
|
+
|
|
11
343
|
## [0.12.3] — 28 September 2026
|
|
12
344
|
|
|
13
345
|
### Fixed
|
package/README.md
CHANGED
|
@@ -20,27 +20,47 @@ VC issuance → identity proof → pay-per-use).
|
|
|
20
20
|
|
|
21
21
|
| Tool | Does | Input | Output (shape) |
|
|
22
22
|
|---|---|---|---|
|
|
23
|
-
| `wallet_status` | Report holder DID/address/network + held VCs (client-supplied, or the local cache); optionally one token balance (`token`) or several in one call (`tokens`). Each balance carries `balance` (raw base units), `decimals` and `display` — the same amount in whole tokens with its symbol. Quote `display`; a raw count beside a ticker is wrong by orders of magnitude. A token may be named by ticker **or** contract address | `{ heldCredentials?, token?, tokens? }` | `{ holderDid, zetrixAddress, network, credentials, balances?, tokenBalance?, tokenBalances? }` |
|
|
24
|
-
| `credential_preflight` | **Free.** The first call for any credential — composes the quote, the balances and the spending cap into one answer, before a single application field is collected. Spends nothing, starts nothing. `blockers` lists *every* reason the wallet is not ready, at once; `notChecked` lists what preflight cannot know — agent-name availability is decided by myid at issuance, after payment, so a `ready` result is never a reservation | `{ credential,
|
|
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 }` |
|
|
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
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 }` |
|
|
27
|
-
| `subscribe_and_issue` | Reuse a cached VC if still valid, else pay x402 → MBI issues → return the VC | `{ templateId, attributes, expirationDate?, dryRun?, forceReissue? }` | `{ issued, vcId, vc, txHash, fromCache?, schema?, originalPayment?, paymentAttempted?, recovery? }` |
|
|
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 |
|
|
28
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 }` |
|
|
29
30
|
| `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
31
|
| `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
|
-
| `get_policy_template_schema` | **Free** read of a
|
|
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
|
-
| `
|
|
32
|
+
| `get_policy_template_schema` | **Free** read of POLICY templates — and the place to START when writing a policy. With **no arguments** it lists the templates on offer (on testnet `native-v1` and `ztp20-v1`), each with its declared attributes and the `templateId` that `write_policy` needs, so nobody has to supply a publisher or a template contract address. `{ policyKey }` reads one template of the default publisher, `{ publisher }` lists a different publisher’s, and `{ publisher, policyKey }` and `{ templateId }` still work. An id is only ever returned once the chain has confirmed it: it is derived from the publisher and key, and an id that does not resolve is withheld rather than handed over. The listing is bounded (20 templates, 40 attributes each) and says what it left out. This is the only vocabulary that means anything on chain — the policy contract validates nothing, so an attribute outside it deploys cleanly and then enforces nothing at all. 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". Beside `declared` it returns `attributeMeanings` — the description each declared attribute has in the service (`GET /policy/vocabulary`), including what an empty list means and what an attribute applies to or needs, labelled by `attributeMeaningsSource` as text from ms-zetrix that describes and does not instruct — and `attributesUnknownToService` for a declared name the service does not recognise (which refuses every transfer). If the vocabulary cannot be read the result carries `attributeMeaningsNote` saying so and `declared` is unchanged | `{ templateId?, publisher?, policyKey? }` | `{ found, publisher?, publisherSource?, templates?, templateId?, templateIdConfirmed?, declared?, attributeMeanings?, attributeMeaningsSource?, attributeMeaningsNote?, attributesUnknownToService?, error? }` |
|
|
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
|
+
| `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
|
+
| `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? }` |
|
|
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
|
+
| `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 |
|
|
35
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 |
|
|
36
42
|
| `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? }` |
|
|
43
|
+
| `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 (Wallet BE errorCode 1000033): a decision, not a failure, so retrying will not help. `policyCheckUnavailable: true` means Wallet BE could not complete the policy check (errorCode 1000034): nothing was signed or sent, it is transient, and trying again shortly is right | `{ token, to, amountHuman?, amount?, confirm?, dryRun? }` | `{ sent, txHash?, nonce, token, asset, amount, amountHuman, decimals, fee, needsConfirmation?, needsTokenAddress?, outcomeUnknown?, policyDenied?, policyCheckUnavailable? }` |
|
|
44
|
+
|
|
45
|
+
> **A spending-policy refusal on a paying tool is a result, not an error.** Wallet BE checks the owner's spending policy
|
|
46
|
+
> before it signs any transaction that spends the wallet's assets (a message, a nonce or a presentation is not checked, and in current versions neither is a contract call that moves no asset, such as the self-paid policy write), so
|
|
47
|
+
> `pay_and_fetch`, `subscribe_and_issue`, `request_ai_birthcert_verification`, `write_policy` and `update_policy` can each be refused at the
|
|
48
|
+
> signature. The result then carries `policyDenied: true` (the policy said no: a decision, retrying will not help) or
|
|
49
|
+
> `policyCheckUnavailable: true` (the check could not complete and Wallet BE failed closed: transient, trying again shortly is
|
|
50
|
+
> right), never both. The signature is the last step before the X-PAYMENT header exists, so in both cases **nothing was
|
|
51
|
+
> paid**, and the message says what the call had already done (a quote, the free pre-check, the issuer's payment request),
|
|
52
|
+
> none of which moves money. `pay_and_fetch` returns it as `{ status: 402, paymentMade: false, policyDenied | policyCheckUnavailable,
|
|
53
|
+
> reason }`; `subscribe_and_issue` as `{ issued: false, … }`; `request_ai_birthcert_verification` as `{ error, … }`;
|
|
54
|
+
> `write_policy` and `update_policy` as state `refused` (a decision) or `unavailable` (transient), with `paid: false`. A refusal is never retried on
|
|
55
|
+
> the other gas option. Every other failure is unchanged. `transfer_token` reports the same two outcomes.
|
|
38
56
|
|
|
39
57
|
> **VCs are cached locally**, keyed by `templateId`, under `~/.agentic-wallet-mcp/vc-cache/`
|
|
40
58
|
> (scoped per network + holder — different identities or networks never share a cache).
|
|
41
59
|
> `subscribe_and_issue` checks the cache before paying: a still-valid cached VC is returned
|
|
42
|
-
> immediately with `fromCache: true` and **no payment made
|
|
43
|
-
>
|
|
60
|
+
> immediately with `fromCache: true` and **no payment made**. `forceReissue: true` on its own does
|
|
61
|
+
> NOT pay — it returns that same existing VC as `{ issued: false, fromCache: true, reason }` instead,
|
|
62
|
+
> so it can be shown to the user first; only with `confirmReplaceExistingVc` also set to the exact
|
|
63
|
+
> `vcId` shown does it actually pay and reissue. `wallet_status`/`prove_identity` fall back to the cache
|
|
44
64
|
> automatically when you don't pass `heldCredentials`/`vc` explicitly — `prove_identity` only
|
|
45
65
|
> auto-selects when there's exactly one valid cached VC; with zero or several, it errors and
|
|
46
66
|
> asks you to pass `vc` explicitly. Explicitly passing `vc`/`heldCredentials` (including `[]`)
|
|
@@ -178,13 +198,17 @@ Node ≥ 18 required (built-in `fetch`).
|
|
|
178
198
|
| `ZETRIX_ADDRESS` | no | Holder Zetrix address (the HSM account). Omit on first run — see "Onboarding" below |
|
|
179
199
|
| `HOLDER_DID` | no | Holder DID. Omit and the MCP derives it automatically — see "Onboarding" below |
|
|
180
200
|
| `WALLET_BE_URL` | no | Wallet BE base URL override (HSM `/wallet/hsm/sign-blob`) — auto-derived from `ZETRIX_NETWORK` when not set |
|
|
181
|
-
| `MBI_BASE_URL` | no | MBI RS base URL override (`/v1/vc/pay/apply
|
|
201
|
+
| `MBI_BASE_URL` | no | MBI RS base URL override (`/v1/vc/pay/apply`; `/v1/*/ext/*` calls are signed over the API path `/v1/...` only; a path prefix here is sent but not signed, so a gateway must strip it before MBI verifies the request) — auto-derived from `ZETRIX_NETWORK` when not set |
|
|
182
202
|
| `OID4VP_BASE_URL` | no | OID4VP verifier base URL override — auto-derived from `ZETRIX_NETWORK` by the x401 SDK when not set |
|
|
183
203
|
| `ZETRIX_NODE_HOST` / `ZETRIX_NODE_PORT` | no | RPC node override (auto-derived from network) |
|
|
184
204
|
| `ZID_RESOLVER_BASE_URL` | no | ZID resolver override (auto-derived from network: sandbox for testnet, prod for mainnet) |
|
|
185
|
-
| `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. |
|
|
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. |
|
|
186
206
|
| `ZETRIX_WALLET_STATE_DIR` | no | Where the wallet keeps `account.json` and its VC cache. Defaults to `~/.agentic-wallet-mcp` |
|
|
187
|
-
| `SSIVC_BASE_URL` | no | myid's SSIVC API base URL, for the Verified AI Birthcert flow (`request_ai_birthcert_verification`/`check_ai_birthcert_verification`). **Auto-derived per network** — testnet `https://ssivc-api-uat.
|
|
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`). **Auto-derived per network** — testnet `https://ssivc-api-uat.myegdev2.com/api`, mainnet `https://verifyid-api.zetrix.com/api`. Override only if either changes |
|
|
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
|
+
| `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
|
+
| `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 |
|
|
211
|
+
| `POLICY_TEMPLATE_PUBLISHER` | no | The publisher whose policy templates the wallet offers when the caller names none. **Auto-derived per network** — on testnet `ZTX3QFo5oc3Ep8rdJZKgfPDFNN29qjxn5ofED`, and none elsewhere: no policy module is deployed on mainnet, so a guessed publisher would read as "this publisher has no templates" instead of "there is no policy system here". Override only to point at a different publisher |
|
|
188
212
|
| `AI_BIRTHCERT_VERIFIED_TEMPLATE_ID` | no | The Verified AI Birthcert's on-chain `did:zid:...` template id. Auto-derived per network by default — **the mainnet default is unverified**, so override this explicitly once the mainnet template id is confirmed |
|
|
189
213
|
| `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 |
|
|
190
214
|
| `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`** |
|