agentic-wallet-mcp 0.14.1 → 0.15.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 +27 -0
- package/README.md +11 -9
- package/dist/server-bundle.cjs +437 -45
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
+
## [0.15.0] — 15 October 2026
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- **Paid issuance asks before it pays, and the yes is tied to the price.** `request_ai_birthcert_verification` and a chargeable `subscribe_and_issue` used to pay on the first call, so an agent could spend before the user said yes. Called without a confirmed price, both now return `{ needsConfirmation: true, quote, askUser }` and pay nothing (`subscribe_and_issue` asks MBI's `/quote`, so nothing is even signed). `confirm` is no longer a boolean: it is the `quote` object, passed back unchanged (`asset`, `maxAmountRequired`, `payTo`, `gasModel`), and is declared that way in the tool schema; `true` is refused. Just before paying, the wallet compares the payment request with the confirmed quote and, if the token, amount, payee or gas model differ, pays nothing and returns the new price to ask about. The confirmed gas model picks the payment option: when a sponsored option is refused, the Verified AI Birthcert no longer switches to paying gas itself, it offers the self-pay price instead. A free issuance, a credential already held, the same open verification link and a new link that reuses an earlier payment need no confirmation. The price question names the credential ("Issuing this Basic AI Birthcert (self-declared, not identity-verified) costs …") and says when ZTX network gas is paid on top. **Agents and scripts that called these two tools once and expected a payment now get the price back first.**
|
|
18
|
+
- **Results say what to do next.** `request_ai_birthcert_verification`, `subscribe_and_issue` and `credential_preflight` results carry `nextStep` (for example `ask_user_to_confirm_payment`, `show_link_and_wait`, `confirm_kind_then_collect_details`, `follow_message`) and, when there is a question to put to the user, `askUser`, so a model does not have to find the next step in the prose.
|
|
19
|
+
- **A Verified AI Birthcert name is never treated as the Basic one.** "Verified AI Birthcert" contains "birthcert", so it resolved to the Basic template. Names that say verified, verify, verification, MyDigital ID or MyID (with or without spaces) are now the Verified one; "not verified", "self-declared" and "basic" mean Basic; a name with both kinds of wording is asked about. `subscribe_and_issue` refuses a Verified or unclear name with nothing paid.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- **The agent no longer says it paid again for a new verification link.** A new link after an expired one reuses the earlier, unused payment, but every result looked the same and the agent told the user it had paid once per link. Every `request_ai_birthcert_verification` session result now says `paidThisCall`, its message opens with "No new payment" when it did not pay, and `credential_preflight` reports an earlier payment the next link will reuse (`unusedPayment`) instead of quoting the fee again.
|
|
24
|
+
- **A credential already held is no longer reported as issued.** Asked for a Basic AI Birthcert under a new username, `subscribe_and_issue` returned the cached one, with the old username, as `issued: true`. A cache hit is now `{ issued: false, alreadyHeld: true }`, and `differsFromRequest` names any requested value the held credential does not carry, so the agent can ask whether to keep or replace it.
|
|
25
|
+
- **Amounts are reported in whole tokens only, and never wrongly.** `wallet_status` told the agent to quote `display` but not to stop there, and QA saw "1 JMYR (1,000,000 base units, 6 decimals)": the agent now quotes only the whole-token amount. Where a token's decimals cannot be read, the amount is no longer shown as whole tokens (it read "473999900 JMYR" for 473.9999 JMYR): `wallet_status`, `credential_preflight`, the policy price and cap messages say the amount could not be converted, and a token whose decimals cannot be read reports `decimals: null`, not 0.
|
|
26
|
+
|
|
27
|
+
## [0.14.2] — 13 October 2026
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- **An AI Birthcert request that does not say Basic or Verified is put to the user.** "Get me AI Birth VC" was answered with the Basic credential in one chat and the Verified one in another: the wallet does not read the request, the agent chooses by the tool and credential it names first, and nothing told it which or asked the user. Every AI Birthcert `credential_preflight` result now carries a `choice` note, and the tool text and the plugin skill say the same: check what the wallet already holds, explain the two kinds in plain words, offer only what it does not hold, ask which the user prefers, and, if they want both, issue them one after the other, Basic first. The older sentence in `request_ai_birthcert_verification` that said the user "most likely wants" the Basic one, and to use `subscribe_and_issue`, is replaced by the same rule: if the request does not say which kind, do not start it, check both and ask. Each kind has its own price and needs its own yes, so "both" is not a yes to either payment. Where the Verified kind is not configured (mainnet, by default), the note says to offer Basic only. `prove_identity` now says what to do when several credentials are held: pass one, prefer the Verified one when the request names none, and say which was presented.
|
|
32
|
+
|
|
33
|
+
- **The Basic AI Birthcert's testnet pass design was updated.** The Basic AI Birthcert is now issued on testnet with the new pass-design image id, so the card image that comes back with the credential is the current one. Mainnet and other templates are unchanged.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- **The agent no longer asks again for a name the user has already given.** Asked for `agentUsername`, a user answered "QA Basic VC" twice and the agent kept asking, because the rule "never invent a name" said nothing about a short answer that repeats the question. An answer is now the name: the agent uses it as given and does not ask for it again (unless it was refused), tidying it is optional and the agent waits for a yes before using a tidied form, it never goes ahead on a name or a payment the user has not agreed to, and it treats any example it gives as only an example of the format.
|
|
38
|
+
- **The agent no longer shows the raw `paymentRequired` field name to the user.** QA saw "issuance is currently free (paymentRequired: false)": the wallet's own agent-facing text used the raw MBI field name as vocabulary, and a preflight `notChecked` item that the agent is told to relay contained it. `notChecked` no longer names the field, and the preflight and dry-run descriptions tell the agent to say free or chargeable in plain words and never to quote JSON field names.
|
|
39
|
+
|
|
13
40
|
## [0.14.1] — 12 October 2026
|
|
14
41
|
|
|
15
42
|
### Fixed
|
package/README.md
CHANGED
|
@@ -20,12 +20,12 @@ 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, 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 }` |
|
|
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. When `decimals` is `null`, `display` is the raw base-unit count, not whole tokens: say the amount could not be converted. 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". When a user asks for an AI Birthcert without saying Basic or Verified, every result carries a `choice` note: the agent checks both kinds, explains the difference, and asks which they prefer; if they want both, it issues them one after the other, Basic first. | `{ credential, agentName?, replacing? }` | `{ credential, ready, fee?, balances, cap?, schema?, alreadyHeld?, unusedPayment?, blockers, notChecked, choice? }` — `unusedPayment: { agentName, linkOpen, note }` (Verified only) means an earlier payment has not been used yet and the next link reuses it, so balance and spending limit do not gate and no fee is asked for |
|
|
25
25
|
| `prove_identity` | Answer an x401 `PROOF-REQUEST` → return the `PROOF-RESPONSE` header to replay | `{ proofRequest, vc?, revealAttribute?, issuerKeys? }` | `{ proofResponseHeader, verified, presentationId }` |
|
|
26
26
|
| `create_verification_qr` | Create a link and a QR code that open this wallet's credential in the MyID app, for a human to verify the agent. Presents the credential to MBI — with no `vc` given, the Verified AI Birthcert if the wallet holds one, otherwise the Basic AI Birthcert, and if it holds neither it answers `created: false` and says to create one first. The link holds only the reference id, but that id grants access to whatever is revealed until it expires (5 minutes by default, up to 60), so by default it reveals only a standard minimal set (Verified: `agentName`, `evidenceProvider`, `ownerVerified`; Basic: `agentUsername`) — name other attributes with `revealAttribute`, and pass `revealAll: true` only when the user explicitly asks to reveal everything; show the link only to the human user. The QR code comes back as an image and as a PNG file (`qrCodePath`) that a chat channel such as WhatsApp can attach; a file is removed when the wallet starts and the next time a QR is made, once its link has expired. The file is on the wallet's own machine, so a client that runs the wallet remotely cannot read the path and gets the image block and the link instead. The agent is told never to send another image as the QR code, and to give the link and say so if it cannot attach one. When the credential has a saved pass design, the result also carries its path (`vcPassImagePaths`), so the agent can show both pictures, each captioned for what it is. A Basic credential does not mean the owner was verified, and the result says so. Needs `MYID_VERIFY_LINK_TEMPLATE`; without it the tool answers `created: false` before anything is created | `{ vc?, revealAttribute?, revealAll?, expiryMinutes? }` | `{ created, link, referenceId, expiresAt, expiresInMinutes, revealed (the attribute paths, or `"all"`), credentialUsed, revealedByDefault, qrCodePngBase64 (sent as an image; absent with a `qrError` if it could not be drawn), message }` or `{ created: false, reason }` |
|
|
27
27
|
| `pay_and_fetch` | Fetch a URL, auto-pay with x402 (self-pay via Wallet BE) on `402` | `{ url, method?, headers?, body? }` | `{ status, body, paymentMade, amountPaid, amountPaidHuman, asset }` |
|
|
28
|
-
| `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
|
+
| `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?, confirm? }` (`confirm` is the `quote` from a `needsConfirmation` result, passed back unchanged; anything else, including `true`, returns `{ issued: false, needsConfirmation: true, quote, askUser, nextStep }` and signs, pays and issues nothing; a confirmed call whose payment request differs from the quote in token, amount, payee or gas model is not paid either) | `{ issued, vcId, vc, txHash, fromCache?, alreadyHeld?, differsFromRequest?, schema?, originalPayment?, paymentAttempted?, recovery?, vcPassImagePaths? }` — `issued: false` with `fromCache: true` means an existing valid VC is being shown, not an error: a plain cache hit adds `alreadyHeld: true` (nothing paid, nothing new issued) and, when the held VC carries different values from the ones asked for, `differsFromRequest: [{ attribute, held, requested }]` |
|
|
29
29
|
| `create_holder_account` | Onboarding: mint an HSM account (the MCP already auto-creates one at startup if `ZETRIX_ADDRESS` is omitted — see Environment below). Always checks for an existing account first — if one is active for this session, returns `{ alreadyExists: true, existing }` without creating anything; pass `confirmNew: true` (after asking the user) to mint a new one anyway | `{ password, label?, purpose?, confirmNew? }` | `{ created, alreadyExists, existing?, zetrixAddress?, holderDid?, publicKeyHex?, message }` |
|
|
30
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 }` |
|
|
31
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 }` |
|
|
@@ -37,7 +37,7 @@ VC issuance → identity proof → pay-per-use).
|
|
|
37
37
|
| `update_policy` | **Pays a real fee, and only with `confirm: true`.** Replace a deployed policy's attributes and validity window over x402. `attributes` is the **full replacement set**, not a patch. An amount may be given as `valueHuman` (whole tokens, `"100"` for 100 JMYR) instead of `value`: the wallet converts it with the token's own decimals, shows both forms, and never sends `valueHuman` on. It is only for the amount attributes (`perTransactionMax`, `cumulativeMax`, `velocityCap`, and any attribute the service lists as an amount in the smallest unit): `maxTransactionCount` is a count and a window is a duration, and neither is ever scaled. Each amount attribute may carry its own `valueHuman`. Never put `convertedAmounts` in `valueHuman`: those are raw base units. A number is accepted only as a safe whole number; write anything else, such as `"0.5"`, as text. An attribute needs a `value` or a `valueHuman`, whatever its type. If `value` and `valueHuman` are both given they must agree, and `amountUnit` does not apply to a `valueHuman`. Read the policy first with `get_my_policy` and pass its `forUpdate.expectedUpdatedAtBlock` (a string): if the policy has changed since, the update is refused for free (`modified`). The policy keeps the template it already references — pass none — and its validity window is kept unless `validFromBlock` / `validToBlock` are given. The same three steps, states and `dryRun` as `write_policy`; `check_policy_write` finishes a paid update from its receipt. Free refusals, each saying nothing was paid: `not_found`, `modified`, `template_unavailable`, `refused`. `written` is the only state that means the policy was updated; `submitted` is **not** (the block has not confirmed it); On the sponsored path `write_failed` means the update was **not** applied, the policy is unchanged and a payment may have been taken. The fee goes through the same payment cap as every other paying tool. **Who pays the gas is the user's choice** (`gasPayer`): `"sponsored"` pays the fee in JMYR and the gas is covered (no ZTX needed), `"self"` costs no fee and only the network gas in ZTX. The wallet has no default for it: when the wallet can pay the gas itself, a call without `gasPayer` stops at `needs_choice` (nothing paid, whatever `confirm` says) with both options, what each costs and whether the wallet can pay it right now, so the user can decide; a value that is neither is refused before the service is contacted. Self-pay is on offer where the policy contracts are deployed (testnet today); elsewhere nothing changes and `"self"` is refused. A self-paid write is the owner's own `createOrUpdatePolicy` call on the Policy Registry, built here and signed through Wallet BE, and the policy service is never contacted. The wallet reads the chain first (a policy that already exists, or one that has changed since you read it, is refused for free), checks the ZTX balance covers the estimated gas plus a reserve (a balance or estimate it cannot read is not a yes), and reports `written` only when the chain then holds exactly what was sent: the same attributes, template and validity window, and for an update `updatedAtBlock` moved. Otherwise it reports `submitted` with the `txHash`: do not send it again, and `get_my_policy` shows when it lands. Wallet BE refusing to sign is a decision (`policyDenied`); its check failing is transient (`policyCheckUnavailable`). Nothing is sent when the node's own simulation rejects the call. Without `confirm: true` it quotes the gas and sends nothing. **A self-paid send also needs `maxGasRaw`**, the `requiredRaw` of the self option the user was shown (the most gas they agreed to): without it, or when the gas now costs more than it, nothing is signed and the refusal gives the current figure to show and what to call with, so the fee limit signed can never exceed what the user saw. **A self-paid send is remembered until its outcome is settled:** the transaction carries an expiry (60 ledgers past the current one), a small note (policy key, nonce, transaction hash, expiry) is written before it is submitted, and a later write or update for the same key is refused, with nothing sent, while the transaction could still land, and so is a sponsored write, the collect of an already-paid write and `remove_policy` (the self-paid transaction would replace the whole policy when it lands, so nothing else may write or remove in the meantime; nothing is paid or removed). The note holds the owner, policy key, operation, nonce, transaction hash, expiry and send time (no secret), is kept per network, and is removed only by its own transaction. Every confirmed write, collect or removal for a key in a process waits for the one in front of it for up to 15 seconds, and then refuses with nothing sent, paid, collected or removed, so a call never acts after its client could have given up. The note is cleared when the transaction is found on chain (landed or failed) or the ledger has passed its expiry, so an outcome that never arrived is settled in bounded time. A send is refused if the ledger number cannot be read or the note cannot be written, and a note that exists but cannot be read (unreadable, empty or damaged, as a crash can leave it) is never read as "no note": the call stops and names the file, which a person can delete once 60 ledgers (about 10 minutes) have passed since it was last changed, or once `get_my_policy` shows the write. A note is deleted only after at least 15 minutes and with the chain producing ledgers. **With `gasPayer "self"` there is no `paymentReceipt` and `check_policy_write` does not apply:** `submitted` means the network gas is spent (do not send it again; `get_my_policy` shows when it lands, and quote the `txHash` if it never does), `unknown` means it may be on chain (do not send it again; quote the nonce), and `write_failed` means the transaction was included and rejected (the gas is spent and nothing was written). The gas is spent even when the write fails on chain. The wallet signs with the fee ceiling it checked the balance against, and refuses (nothing signed) if the node's fresh estimate is above it. A pending paid write for the same `policyKey` blocks a self-paid one (finish it with `check_policy_write` first), and so does an attribute vocabulary that could not be read or that no source is configured for (the vocabulary comes from the policy service, so self-pay needs it), because a self-paid write has no service to catch what it would have. A receipt the service said failed does not hold a self-paid write back; one with an unresolved outcome does. When neither way of paying is available the answer is `unavailable` and names both reasons. A list attribute is capped at 500 entries and a policy at 64 attributes. A wallet that already has a policy can self-pay too: Wallet BE (a current version) signs the owner's own Registry call. A Wallet BE older than that refuses it (`UNSUPPORTED_OPERATION`, a decision made before any policy is consulted), which is reported as final, not about the policy's limits, with no invitation to retry. A self-paid update is a whole-policy replacement that the contract does not compare with what was there, so another write by the same owner landing in the few seconds between the wallet's last chain read and the block that includes the transaction is overwritten (the wallet still reports `written` because its own content is there and `updatedAtBlock` moved). A self-paid write has not yet been run end to end against a live chain. | `{ policyKey, attributes, expectedUpdatedAtBlock, validFromBlock?, validToBlock?, amountUnit?, confirm?, dryRun?, gasPayer?, maxGasRaw?, pollBudgetMs? }` | `{ state, message, paymentReceipt?, policyKey?, txHash?, paid?, payFresh?, quote?, affordability?, interpretation?, gasOptions?, needsConfirmation?, policyDenied?, policyCheckUnavailable?, upstream? }` |
|
|
38
38
|
| `remove_policy` | **Free, but it lifts every limit the policy set** — Wallet BE then signs spends of that asset without any limit — so nothing is sent without `confirm: true`. Without it you get `needs_confirmation` with what the policy currently limits. `removed` is the only state that means the policy is gone, and it is reported only when a chain read no longer holds it; `submitted` is a removal on its way, **not** a removed policy. Repeating the call never submits a second removal. `not_found` means there was nothing to remove (or it is already gone); `in_progress` means a paid update has not been collected yet (`check_policy_write` first); `refused` naming a self-paid transaction means an earlier self-paid update may still land and would bring the old policy back, so nothing was removed (wait until the ledger it names, or read the policy with `get_my_policy`) | `{ policyKey, confirm?, pollBudgetMs? }` | `{ state, message, policyKey?, txHash?, currentAttributes? }` |
|
|
39
39
|
| `check_policy_write` | **Free — never pays, on any path.** Finish a policy write or update that has already been paid for, from its `paymentReceipt` (or the most recent pending one when omitted). This is the right answer to “did my policy get created?”; calling `write_policy` again is not. A receipt this wallet does not recognise does **not** mean the write failed — the service completes a paid write on its own, and asking to write the same `policyKey` again reports the truth for free before any payment. When a collect ends with an error that says nothing about the write (a gateway timeout, a 5xx), the wallet reads the chain itself (up to 45 seconds, less when the poll has already used part of its budget; each read is cut off at 10 seconds) and reports `written` only when the chain holds exactly the attributes that were submitted, under the same template (for an update, with `updatedAtBlock` moved). That covers every answer the wallet cannot read: a 5xx, a gateway page, the service's own "unknown", and any other status it does not recognise (a 4xx included); the service's own "the chain rejected it" is never second-guessed. `check_policy_write` looks at the chain first for a receipt that still carries what to verify, so a write that already landed costs no collect. A receipt stops being verified against the chain once the service has said its write failed, or once a newer paid write for the same `policyKey` has been made, and a receipt bought by another owner is never checked against this owner's policy. An error answer is returned apart in `upstream: { status, detail }` (control characters replaced, at most 200 characters) | `{ paymentReceipt?, pollBudgetMs? }` | `{ state, message, paymentReceipt?, policyKey?, txHash?, paid?, payFresh?, upstream? }` |
|
|
40
|
-
| `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
|
|
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 option is refused before any money moved, the wallet does not switch to paying gas itself on its own: it returns `needsConfirmation` with the self-pay price (its own token and amount, plus ZTX gas) to ask the user about, and pays it only once that quote is confirmed; it never offers a switch 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 needs no `confirmReplaceExistingVc` (the payment still stops at the price like any other), and a result that went ahead 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?, confirm? }` (`confirm` is the `quote` from a `needsConfirmation` result, passed back unchanged; anything else, including `true`, returns `{ needsConfirmation: true, paidThisCall: false, quote, askUser, nextStep, message }` and pays nothing; a confirmed call whose payment request differs from the quote in token, amount, payee or gas model is not paid either, and the quote's `gasModel` decides which option is paid; a call that pays nothing needs no confirm). Results carry `nextStep` (one of `ask_user_to_confirm_payment`, `show_link_and_wait`, `wait_then_check`, `ask_user_whether_to_replace`, `tell_user_already_held`, `show_credential`, `collect_details`, `confirm_kind_then_collect_details`, `resolve_blockers`, `follow_message`) and, when there is a question to ask, `askUser` | `{ sessionId, verificationUrl, expiresAt, expiresIn, expiresInSeconds, paidThisCall, message, replacedExpiredVc? }` (`paidThisCall` is `true` only when this call sent a payment: the same still-pending link handed back, or a new link after an expired one, reuses the payment already made, which is only used up once the credential is issued, and `message` then opens with "No new payment"; `expiresIn` / `expiresInSeconds` are worked out by the wallet from its own clock, so an agent quotes `expiresIn` instead of doing timezone arithmetic itself; `message` carries a "Tell the user:" sentence about following up, worded to match how much time is actually left on the link), `{ quote: { asset, maxAmountRequired, payTo?, gasModel } }`, `{ existingVerifiedVc: { vcId, validUntil? }, message }` (nothing paid, see above), or — instead of a session — `{ settlementPending: true, paymentReceipt, message }` after ~90s, whose meaning depends on which flag (if any) is set below: never assume the payment succeeded from this shape alone. While a merely-stuck receipt (outcome still unresolved) is held this tool can only REPLAY it — it never buys a new credential, which is what stops a second charge. A credential-service refusal is different again: `issuerRejected: true` means the service was reached and refused the request (the settlement is NOT what failed); `paymentInvalid: true` means the service specifically ruled the payment or receipt invalid (this one IS about the payment). Neither tells you whether the fee was taken, in either direction. A **void** receipt (SSIVC status_code 67/68) is different again: the wallet discards it itself, with no confirmation and no flag, so the very next call is an ordinary fresh purchase — see `{ error }` below. `discardStuckReceiptAndPayFresh: "<exact receipt id>"` is the way to start over on a merely-stuck receipt instead: it throws that payment away and pays the fee again, refuses a mismatched id (discarding and paying nothing), refuses a receipt that belongs to a live session, **refuses a receipt that is not stuck yet** (younger than `SETTLEMENT_STUCK_AFTER_MS`, 24h by default — enforced by the wallet, so an agent that mistakes a bare "retry" for consent still cannot cause a second payment; nothing is discarded and nothing is paid), and returns `discardedPaymentReceipt` so the lost payment stays traceable. `{ error }` (`RequestVerificationFailure`) also carries `discardedPaymentReceipt` when this call itself discarded a dead receipt — that confirmed stuck-receipt discard, or an unprompted void one, or **both on one call**; it also rides on `settlementPending` results, not only successes. When both happen (the confirmed discard, then the fresh payment ruled void) the field holds the **void** receipt and the id the user confirmed is named at the end of the `error`/`message` text ("ALSO DISCARDED earlier on this same call") — quote both ids to support, not just the field. Retrying is fine only for an insufficient-funds or payment-cap `{ error }` (nothing was paid); any other `{ error }` (already-settled, receipt not saved, receipt void, outcome unknown) may mean money moved, so get the user's explicit agreement before calling again |
|
|
41
41
|
| `check_ai_birthcert_verification` | **Free** (never pays). Poll the most recently requested Verified AI Birthcert session; on `status: "issued"`, also fetches, verifies, and caches the credential. While the session is still open it replays the stored `verificationUrl` alongside SSIVC's live `expiresAt` and the wallet's own `expiresIn` / `expiresInSeconds` (time left, worked out from the wallet clock, never from the agent's) — SSIVC issues the link only once, at creation, so this store is the only place it survives. That makes *"where is my link?"* answerable without touching the paid tool. No link is returned once `status` is `"issued"`: the link is spent. If a prior payment is still clearing this tool **advances** it — it replays the saved receipt (never a new payment) and returns the live session once it settles, so "check back in a few minutes" genuinely progresses the flow | (none) | `{ status: "pending" | "issued" | "no_session" | "settlement_pending" | "receipt_void", verificationUrl?, expiresAt?, vcId?, vc?, cacheError?, paymentReceipt?, outcomeUnknown?, issuerRejected?, paymentInvalid?, message? }` — `settlement_pending` means a payment was made and is being followed: without `outcomeUnknown`, `issuerRejected` or `paymentInvalid` the message leads `PAYMENT SENT` and covers **two** states that the following clause tells apart — *"still being processed"* is a confirmed queued settlement (check again shortly), while *"has not been confirmed yet"* means the outcome could not be determined at all yet, so it must not be reported as progressing or as succeeded; either way the receipt is saved, nothing is paid again, and checking again later is the next step; with `outcomeUnknown: true` the outcome could not be determined at all and has been unresolved longer than `SETTLEMENT_STUCK_AFTER_MS` (24h default), so it is not coming back on its own — `stuckFor` says how long, the fee was most likely already taken, and quoting `paymentReceipt` to support is the way forward. Starting over via `clear_stuck_payment_receipt` costs a second fee. `issuerRejected: true` is a distinct case: the credential service was reached and refused the request (a `4xx` that is not a settlement verdict — measured live as `status_code 99` "Error retrieving ZVG access token"), so the settlement is NOT what failed; `message` quotes what it said, the receipt is kept, no new payment was made, and it can clear once the service recovers. `paymentInvalid: true` is different again: the service has specifically ruled the payment or receipt invalid — this one IS about the payment. Both say nothing about whether the fee was taken, in either direction. `receipt_void` is terminal and different: SSIVC has ruled the receipt finished (status_code 67 expired / 68 failed), so checking again cannot help. The wallet **discards the dead receipt itself** here — no confirmation needed, because a receipt the server will never honour protects nothing and only blocks the next purchase — and returns it as `discardedPaymentReceipt`. It does NOT mean the fee was refunded, and buying the credential means calling `request_ai_birthcert_verification` again, which pays the fee again |
|
|
42
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? }` |
|
|
43
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? }` |
|
|
@@ -138,11 +138,13 @@ VC issuance → identity proof → pay-per-use).
|
|
|
138
138
|
> | `REQUIRED` / `SETTLED` | Still unresolved when the poll budget ran out — check again later with the `paymentId`. |
|
|
139
139
|
> | `UNKNOWN` | `/status` itself was unreachable. The charge still stands; retry the lookup, not the payment. |
|
|
140
140
|
|
|
141
|
-
> `wallet_status({ token })` returns `tokenBalance: { token, balance, decimals }` for `ZTX` or any
|
|
141
|
+
> `wallet_status({ token })` returns `tokenBalance: { token, balance, decimals, display }` for `ZTX` or any
|
|
142
142
|
> registered ZTP20 symbol (e.g. `JMYR`). `balance` is in the asset's **raw base units** — the same
|
|
143
143
|
> unit x402 quotes `maxAmountRequired` in, so cap checks and quote comparisons stay integer-only.
|
|
144
|
-
>
|
|
145
|
-
> `decimals` is `null` if the token's `contractInfo` can't be read
|
|
144
|
+
> Quote `display`, the same amount in whole tokens with its symbol: `"473999900"` with `decimals: 6` is
|
|
145
|
+
> `473.9999 JMYR`. `decimals` is `null` if the token's `contractInfo` can't be read, or reads a symbol but has missing or invalid
|
|
146
|
+
> decimals (the balance is still returned); `display` then carries the raw base-unit count, not whole tokens, so report that the amount
|
|
147
|
+
> could not be converted instead of quoting it as a balance.
|
|
146
148
|
> A failed lookup reports `{ token, error: "query_failed" }` and an unregistered symbol
|
|
147
149
|
> `{ token, error: "unknown_token" }` — **never a zero balance**, so "0" always means you really
|
|
148
150
|
> hold nothing rather than that the node was unreachable.
|
|
@@ -204,7 +206,7 @@ Node ≥ 18 required (built-in `fetch`).
|
|
|
204
206
|
| `ZID_RESOLVER_BASE_URL` | no | ZID resolver override (auto-derived from network: sandbox for testnet, prod for mainnet) |
|
|
205
207
|
| `MAX_PAYMENT_AMOUNT` | no** | Per-asset x402 auto-pay cap — JSON `{ "<asset>": "<maxRawUnits>", "*": "<fallback>" }`. `pay_and_fetch`/`subscribe_and_issue` are asset-agnostic: the resource server's 402 challenge may quote the native ZETRIX token (asset code `ZTX`) **or** a ZTP20 token (e.g. `JMYR`) — cap whichever assets you expect. **Either the ticker or the contract address works** — the challenge identifies a ZTP20 token by its contract address, and the wallet resolves a known ticker (e.g. `JMYR`) to it. If both are written for the same asset, the contract address wins. An unrecognised ticker still matches nothing and falls through to `"*"`. e.g. `{"ZTX":"1000000000","ZTX3WeinXtt28YMyr4vUZ14ddTgEMGeuc1e6b":"5000000","*":"0"}`. **Default when unset:** both networks allow exactly the AI Birthcert fee (1 JMYR), keyed to that network's JMYR contract, and refuse everything else. **The cap is per call, not cumulative** — so an unconfigured wallet, mainnet included, can pay that fee once per call with no overall ceiling. Set this explicitly to lock a wallet down. **A spending policy stands the DEFAULT cap aside for an asset it governs** (an `assetScope` for that asset, a `perTransactionMax`, and no block range), so the owner's own limit applies instead of a 1 JMYR default underneath it; an explicit `MAX_PAYMENT_AMOUNT` is never bypassed, and a failed read of the policy list leaves the cap in force. The policy is only as strong as Wallet BE's enforcement of it, which is off in prod as shipped. **The agent could write such a policy itself**, so `write_policy` pays and writes only with `confirm: true`, which the user must have given after seeing the interpretation and the price; the wallet cannot verify a person said yes, it is the same instruction-level gate `transfer_token` uses. Collecting a write that was paid for elsewhere needs the same confirmation, and `pay_and_fetch` refuses the policy-write service's paid paths. |
|
|
206
208
|
| `ZETRIX_WALLET_STATE_DIR` | no | Where the wallet keeps `account.json` and its VC cache. Defaults to `~/.agentic-wallet-mcp` |
|
|
207
|
-
| `SSIVC_BASE_URL` | no | myid's SSIVC API base URL, for the Verified AI Birthcert flow (`request_ai_birthcert_verification`/`check_ai_birthcert_verification`). **
|
|
209
|
+
| `SSIVC_BASE_URL` | no | myid's SSIVC API base URL, for the Verified AI Birthcert flow (`request_ai_birthcert_verification`/`check_ai_birthcert_verification`). **Testnet: derived** (`https://ssivc-api-uat.myegdev2.com/api`). **There is no default on mainnet**: its candidate host (`https://verifyid-api.zetrix.com/api`) was never confirmed, so it is kept in code but not used and the Verified AI Birthcert check is off there until `SSIVC_BASE_URL` is set. Override on either network if the host changes |
|
|
208
210
|
| `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
211
|
| `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
212
|
| `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 |
|
|
@@ -332,7 +334,7 @@ the `<...>` placeholders (don't commit a filled copy; `mcp.local.json` is gitign
|
|
|
332
334
|
## Example prompts
|
|
333
335
|
|
|
334
336
|
- *"Check my wallet status."* → `wallet_status`
|
|
335
|
-
- *"What's my JMYR balance?"* → `wallet_status({ token: "JMYR" })` (
|
|
337
|
+
- *"What's my JMYR balance?"* → `wallet_status({ token: "JMYR" })` (quote `display`, the amount in whole tokens)
|
|
336
338
|
- *"I got a 401 with this PROOF-REQUEST header — prove my identity and give me the PROOF-RESPONSE to replay."* → `prove_identity`
|
|
337
339
|
- *"Fetch `https://api.example/data` and pay automatically if it asks."* → `pay_and_fetch`
|
|
338
340
|
- *"What fields does the AI Birthcert template need?"* → `get_template_schema` (free — do this before issuing)
|
package/dist/server-bundle.cjs
CHANGED
|
@@ -31391,7 +31391,7 @@ var import_node_path10 = require("node:path");
|
|
|
31391
31391
|
// package.json
|
|
31392
31392
|
var package_default = {
|
|
31393
31393
|
name: "agentic-wallet-mcp",
|
|
31394
|
-
version: "0.
|
|
31394
|
+
version: "0.15.0",
|
|
31395
31395
|
description: "Agent-facing MCP wallet for Zetrix \u2014 orchestrates x401 identity proof, x402 payment, and MBI VC issuance",
|
|
31396
31396
|
keywords: [
|
|
31397
31397
|
"mcp",
|
|
@@ -40819,7 +40819,7 @@ async function queryTokenBalance(deps, token) {
|
|
|
40819
40819
|
}
|
|
40820
40820
|
const info = await fetchTokenInfo(contractAddress, deps.query);
|
|
40821
40821
|
const label = byAddress ? info?.symbol ?? asked : symbol;
|
|
40822
|
-
const decimals = info?.decimals
|
|
40822
|
+
const decimals = info?.decimalsReadable ? info.decimals : null;
|
|
40823
40823
|
return { token: label, balance, decimals, display: renderDisplay(balance, decimals, label), tokenAddress: contractAddress };
|
|
40824
40824
|
}
|
|
40825
40825
|
function renderDisplay(raw, decimals, label) {
|
|
@@ -41305,6 +41305,12 @@ async function resolveIssuerProofKeys(vc, resolver) {
|
|
|
41305
41305
|
return { bbsPublicKey, ed25519PublicKey };
|
|
41306
41306
|
}
|
|
41307
41307
|
|
|
41308
|
+
// src/orchestrator/birthcert-choice.ts
|
|
41309
|
+
function aiBirthcertChoice(kind, opts = {}) {
|
|
41310
|
+
const note = `AI Birthcert comes in two kinds. BASIC: self-declared by the agent, with no check of the owner's identity; it is quick, shows only the agent's username, and does not mean the owner was verified. VERIFIED: the owner's identity is confirmed through MyDigital ID; the fee is paid up front, the owner completes a check in the MyID app, and the agent name must be unique (the name is only checked after payment, so a name that turns out to be taken is refused late, and whether the fee was spent cannot be confirmed). Each kind has its own price, read from the check for that kind: never assume Basic is free. This result is for the ${kind === "basic" ? "BASIC" : "VERIFIED"} one. If the user has not said which kind they want (they did not say "verified" or "basic", and did not mention MyDigital ID or owner verification), do not start either: run this check for both kinds (it is free) to see what the wallet already holds, tell the user the difference in plain words, offer only the kind or kinds they do not already hold, and ask which they prefer. If they want both, issue them one after the other, Basic first, and finish and show the first before starting the second; each has its own price and needs its own yes, so "both" is not a yes to either payment. Holding one kind never blocks the other, and replacing one that is held is its own explicit choice.`;
|
|
41311
|
+
return opts.unavailable ? note + " The Verified kind is not available on this wallet or network: say so, and offer the Basic one only." : note;
|
|
41312
|
+
}
|
|
41313
|
+
|
|
41308
41314
|
// src/mcp-tools.ts
|
|
41309
41315
|
var import_promises4 = require("node:fs/promises");
|
|
41310
41316
|
|
|
@@ -41493,6 +41499,47 @@ function needsNativeGasCheck(accept) {
|
|
|
41493
41499
|
return !isSponsored(accept);
|
|
41494
41500
|
}
|
|
41495
41501
|
|
|
41502
|
+
// src/price-confirmation.ts
|
|
41503
|
+
function asPriceConfirmation(value) {
|
|
41504
|
+
if (typeof value === "string") {
|
|
41505
|
+
try {
|
|
41506
|
+
value = JSON.parse(value);
|
|
41507
|
+
} catch {
|
|
41508
|
+
return void 0;
|
|
41509
|
+
}
|
|
41510
|
+
}
|
|
41511
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
41512
|
+
const v = value;
|
|
41513
|
+
if (typeof v.asset !== "string" || v.asset === "") return void 0;
|
|
41514
|
+
if (typeof v.maxAmountRequired !== "string" || v.maxAmountRequired === "") return void 0;
|
|
41515
|
+
if (v.gasModel !== "sponsored" && v.gasModel !== "self") return void 0;
|
|
41516
|
+
if (v.payTo !== void 0 && typeof v.payTo !== "string") return void 0;
|
|
41517
|
+
return { asset: v.asset, maxAmountRequired: v.maxAmountRequired, payTo: v.payTo ?? "", gasModel: v.gasModel };
|
|
41518
|
+
}
|
|
41519
|
+
function quoteOf(accept) {
|
|
41520
|
+
return {
|
|
41521
|
+
asset: String(accept.asset ?? ""),
|
|
41522
|
+
maxAmountRequired: String(accept.maxAmountRequired ?? ""),
|
|
41523
|
+
payTo: String(accept.payTo ?? ""),
|
|
41524
|
+
gasModel: isSponsored(accept) ? "sponsored" : "self"
|
|
41525
|
+
};
|
|
41526
|
+
}
|
|
41527
|
+
function priceMismatch(confirmed, actual) {
|
|
41528
|
+
const differences = [];
|
|
41529
|
+
if (confirmed.asset !== actual.asset) {
|
|
41530
|
+
differences.push(`the token is ${actual.asset}, not ${confirmed.asset}`);
|
|
41531
|
+
}
|
|
41532
|
+
if (confirmed.maxAmountRequired !== actual.maxAmountRequired) {
|
|
41533
|
+
differences.push(`the amount is ${actual.maxAmountRequired}, not ${confirmed.maxAmountRequired} (raw base units)`);
|
|
41534
|
+
}
|
|
41535
|
+
if (confirmed.payTo !== actual.payTo) differences.push(`the payee is ${actual.payTo || "(none)"}, not ${confirmed.payTo || "(none)"}`);
|
|
41536
|
+
if (confirmed.gasModel !== actual.gasModel) differences.push(`network gas is ${actual.gasModel}, not ${confirmed.gasModel}`);
|
|
41537
|
+
return differences.length > 0 ? differences.join("; ") : void 0;
|
|
41538
|
+
}
|
|
41539
|
+
function gasNote(quote) {
|
|
41540
|
+
return quote.gasModel === "self" && quote.asset !== "ZTX" ? ", plus network gas paid in ZTX" : "";
|
|
41541
|
+
}
|
|
41542
|
+
|
|
41496
41543
|
// src/zetrix-hex.ts
|
|
41497
41544
|
function zetrixHexStringToBytes(s) {
|
|
41498
41545
|
const up = s.toUpperCase();
|
|
@@ -41665,7 +41712,59 @@ async function resolveVcPassImagePaths(deps, vcId) {
|
|
|
41665
41712
|
return void 0;
|
|
41666
41713
|
}
|
|
41667
41714
|
}
|
|
41715
|
+
function differsFromRequest(vc, requested) {
|
|
41716
|
+
const subject = vc?.credentialSubject;
|
|
41717
|
+
if (!requested || typeof subject !== "object" || subject === null) return void 0;
|
|
41718
|
+
const held = subject;
|
|
41719
|
+
const out = [];
|
|
41720
|
+
for (const [attribute, value] of Object.entries(requested)) {
|
|
41721
|
+
if (attribute === "id" || attribute === "agentDid") continue;
|
|
41722
|
+
if (value === void 0 || value === null || value === "" || typeof value === "object") continue;
|
|
41723
|
+
const current = held[attribute];
|
|
41724
|
+
if (current === void 0 || current === null || String(current) !== String(value)) {
|
|
41725
|
+
out.push({ attribute, held: current ?? void 0, requested: value });
|
|
41726
|
+
}
|
|
41727
|
+
}
|
|
41728
|
+
return out.length > 0 ? out : void 0;
|
|
41729
|
+
}
|
|
41730
|
+
async function renderPrice(deps, asset, raw, symbol) {
|
|
41731
|
+
if (raw === "") return `an amount the issuer did not state (${symbol})`;
|
|
41732
|
+
if (deps.formatAssetAmount) {
|
|
41733
|
+
try {
|
|
41734
|
+
return await deps.formatAssetAmount(asset, raw);
|
|
41735
|
+
} catch {
|
|
41736
|
+
}
|
|
41737
|
+
}
|
|
41738
|
+
return `${raw} base units of ${symbol}`;
|
|
41739
|
+
}
|
|
41740
|
+
async function askToConfirmPayment(deps, accepted, schema, options = {}) {
|
|
41741
|
+
const raw = String(accepted.asset ?? "");
|
|
41742
|
+
const symbol = deps.resolveSymbol ? await deps.resolveSymbol(raw) : raw;
|
|
41743
|
+
const quote = { ...quoteOf(accepted), assetSymbol: symbol };
|
|
41744
|
+
const display = await renderPrice(deps, raw, quote.maxAmountRequired, symbol) + gasNote(quote);
|
|
41745
|
+
const price = options.chargeUnknown ? `may cost up to ${display} (this issuer does not say whether it charges)` : `costs ${display}`;
|
|
41746
|
+
return {
|
|
41747
|
+
issued: false,
|
|
41748
|
+
needsConfirmation: true,
|
|
41749
|
+
quote,
|
|
41750
|
+
nextStep: "ask_user_to_confirm_payment",
|
|
41751
|
+
askUser: `Issuing this ${deps.credentialLabel ?? "credential"} ${price}. Shall I go ahead?`,
|
|
41752
|
+
reason: `NOT PAID YET: ${options.lead ?? ""}issuing this ${deps.credentialLabel ?? "credential"} ${price}, and nothing has been paid or issued. Tell the user the price and ask. Only after their explicit yes to this payment, call again with confirm set to this exact quote object (asset, maxAmountRequired, payTo, gasModel, unchanged), keeping every other argument the same (forceReissue and confirmReplaceExistingVc too, if you sent them). confirm: true is not accepted.`,
|
|
41753
|
+
...schema ? { schema } : {}
|
|
41754
|
+
};
|
|
41755
|
+
}
|
|
41756
|
+
function describeHeld(d) {
|
|
41757
|
+
return d.held === void 0 || d.held === null ? `${d.attribute} not set` : `${d.attribute} "${String(d.held)}"`;
|
|
41758
|
+
}
|
|
41759
|
+
function describeDifference(d) {
|
|
41760
|
+
return `${describeHeld(d)}, not the "${String(d.requested)}" asked for`;
|
|
41761
|
+
}
|
|
41668
41762
|
async function subscribeAndIssue(deps, opts) {
|
|
41763
|
+
const result = await subscribeAndIssueInner(deps, opts);
|
|
41764
|
+
if (result.nextStep || opts.dryRun && result.quote) return result;
|
|
41765
|
+
return { ...result, nextStep: result.issued ? "show_credential" : "follow_message" };
|
|
41766
|
+
}
|
|
41767
|
+
async function subscribeAndIssueInner(deps, opts) {
|
|
41669
41768
|
if (!/^did:zid:/.test(opts.templateId)) {
|
|
41670
41769
|
return { issued: false, reason: `templateId must be a did:zid:... credential-definition id, got "${opts.templateId}"` };
|
|
41671
41770
|
}
|
|
@@ -41701,6 +41800,8 @@ async function subscribeAndIssue(deps, opts) {
|
|
|
41701
41800
|
return {
|
|
41702
41801
|
issued: false,
|
|
41703
41802
|
reason: `A credential for this template already exists and is still valid` + (cached2.validUntil ? ` until ${cached2.validUntil}` : "") + ` (credential id ${confirmableVcId}). Nothing was paid and no new credential was issued. Show this to the user before doing anything else. Call subscribe_and_issue again with forceReissue set to true AND confirmReplaceExistingVc set to exactly "${confirmableVcId}" only if the user explicitly wants to replace it.`,
|
|
41803
|
+
nextStep: "ask_user_whether_to_replace",
|
|
41804
|
+
askUser: "You already have this credential, and it is still valid. Do you want to replace it?",
|
|
41704
41805
|
...cacheHitFields,
|
|
41705
41806
|
// Overrides cacheHitFields.vcId (which may be absent/non-string on the fallback path
|
|
41706
41807
|
// above) with the exact id the message just told the caller to echo back.
|
|
@@ -41708,7 +41809,20 @@ async function subscribeAndIssue(deps, opts) {
|
|
|
41708
41809
|
};
|
|
41709
41810
|
}
|
|
41710
41811
|
} else {
|
|
41711
|
-
|
|
41812
|
+
const differs = differsFromRequest(cached2.vc, opts.attributes);
|
|
41813
|
+
const until = cached2.validUntil ? ` until ${cached2.validUntil}` : "";
|
|
41814
|
+
const id = typeof cached2.vcId === "string" && cached2.vcId ? ` (credential id ${cached2.vcId})` : "";
|
|
41815
|
+
return {
|
|
41816
|
+
issued: false,
|
|
41817
|
+
alreadyHeld: true,
|
|
41818
|
+
reason: `Already held: this wallet already has a valid credential for this template${until}${id}. Nothing was paid and no new credential was issued. Tell the user they already have it; do not tell the user it was just issued.` + (differs ? ` It differs from what was asked for: it has ${differs.map(describeDifference).join("; ")}. Ask the user whether to keep it or replace it. Replacing it is a new issuance at the current price (credential_preflight with replacing: true shows it): call again with forceReissue set to true to see what would be replaced, and confirm only on the user's explicit yes.` : ""),
|
|
41819
|
+
...differs ? { differsFromRequest: differs } : {},
|
|
41820
|
+
...differs ? {
|
|
41821
|
+
nextStep: "ask_user_whether_to_replace",
|
|
41822
|
+
askUser: `You already have this credential, with ${differs.map(describeHeld).join(", ")}. Do you want to keep it, or replace it?`
|
|
41823
|
+
} : { nextStep: "tell_user_already_held" },
|
|
41824
|
+
...cacheHitFields
|
|
41825
|
+
};
|
|
41712
41826
|
}
|
|
41713
41827
|
}
|
|
41714
41828
|
}
|
|
@@ -41765,6 +41879,23 @@ async function subscribeAndIssue(deps, opts) {
|
|
|
41765
41879
|
...schema ? { schema } : {}
|
|
41766
41880
|
};
|
|
41767
41881
|
}
|
|
41882
|
+
const confirmed = asPriceConfirmation(opts.confirm);
|
|
41883
|
+
if (!confirmed && deps.mbi.quote) {
|
|
41884
|
+
let quoted2;
|
|
41885
|
+
try {
|
|
41886
|
+
quoted2 = await deps.mbi.quote(opts.templateId, attributes);
|
|
41887
|
+
} catch (err) {
|
|
41888
|
+
if (err instanceof MbiError) {
|
|
41889
|
+
return { issued: false, reason: err.message, httpStatus: err.httpStatus, ...schema ? { schema } : {} };
|
|
41890
|
+
}
|
|
41891
|
+
throw err;
|
|
41892
|
+
}
|
|
41893
|
+
if (quoted2.paymentRequired !== false) {
|
|
41894
|
+
const quotedAccept = quoted2.accepts?.[0];
|
|
41895
|
+
if (!quotedAccept) return { issued: false, reason: "MBI quoted no payment options", ...schema ? { schema } : {} };
|
|
41896
|
+
return askToConfirmPayment(deps, quotedAccept, schema, { chargeUnknown: quoted2.paymentRequired === void 0 });
|
|
41897
|
+
}
|
|
41898
|
+
}
|
|
41768
41899
|
const data = JSON.stringify([
|
|
41769
41900
|
deps.passDesignId ? { templateId: opts.templateId, passDesignId: deps.passDesignId, metadata: attributes } : { templateId: opts.templateId, metadata: attributes }
|
|
41770
41901
|
]);
|
|
@@ -41810,6 +41941,13 @@ async function subscribeAndIssue(deps, opts) {
|
|
|
41810
41941
|
}
|
|
41811
41942
|
const accept = challenge.accepts[0];
|
|
41812
41943
|
if (!accept) return { issued: false, reason: "MBI 402 returned no payment options", ...schema ? { schema } : {} };
|
|
41944
|
+
if (!confirmed) return askToConfirmPayment(deps, accept, schema);
|
|
41945
|
+
const difference = priceMismatch(confirmed, quoteOf(accept));
|
|
41946
|
+
if (difference) {
|
|
41947
|
+
return askToConfirmPayment(deps, accept, schema, {
|
|
41948
|
+
lead: `the issuer's payment request is not what the user agreed to (${difference}), so nothing was paid. The new price: `
|
|
41949
|
+
});
|
|
41950
|
+
}
|
|
41813
41951
|
let xPayment;
|
|
41814
41952
|
try {
|
|
41815
41953
|
xPayment = await deps.pay(accept);
|
|
@@ -42239,7 +42377,7 @@ var TEMPLATE_ALIASES = [
|
|
|
42239
42377
|
deriveAttributes: { id: "agentUsername" },
|
|
42240
42378
|
validateAttributes: { dob: validateDob, countryOfOrigin: validateCountryOfOrigin },
|
|
42241
42379
|
passDesignId: {
|
|
42242
|
-
testnet: "did:zid:
|
|
42380
|
+
testnet: "did:zid:bb7202482ef6053be6d46ad8e2190b0ba1e3b2024d24ce7f37bc4c3747e2378f",
|
|
42243
42381
|
mainnet: "did:zid:915955cb71c6fd2a256d04344f57381084903cb6a85a1c9ddb71c746f08932ab"
|
|
42244
42382
|
}
|
|
42245
42383
|
}
|
|
@@ -42247,11 +42385,51 @@ var TEMPLATE_ALIASES = [
|
|
|
42247
42385
|
function normalize2(s) {
|
|
42248
42386
|
return s.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
42249
42387
|
}
|
|
42388
|
+
function saysVerified(word) {
|
|
42389
|
+
if (/^(un|non|not)verif/.test(word)) return false;
|
|
42390
|
+
return word.includes("verif") && !word.includes("verifiable") || word.includes("mydigital") || word === "myid";
|
|
42391
|
+
}
|
|
42392
|
+
function saysBasic(word) {
|
|
42393
|
+
return ["basic", "self", "selfdeclared", "declared"].includes(word) || /^(un|non|not)verif/.test(word);
|
|
42394
|
+
}
|
|
42395
|
+
var NEGATORS = /* @__PURE__ */ new Set(["not", "non", "no", "un", "without"]);
|
|
42396
|
+
var VERIFIED_NEGATORS = /* @__PURE__ */ new Set([...NEGATORS, "self"]);
|
|
42397
|
+
var CLAUSE_BREAKS = /* @__PURE__ */ new Set(["but", "and", "or"]);
|
|
42398
|
+
function negated(words, i, negators) {
|
|
42399
|
+
for (let j = i - 1; j >= 0 && j >= i - 3; j--) {
|
|
42400
|
+
if (CLAUSE_BREAKS.has(words[j])) return false;
|
|
42401
|
+
if (negators.has(words[j])) return true;
|
|
42402
|
+
}
|
|
42403
|
+
return false;
|
|
42404
|
+
}
|
|
42405
|
+
function aiBirthcertKindOfName(input) {
|
|
42406
|
+
if (/^did:zid:/.test(input)) return void 0;
|
|
42407
|
+
if (!normalize2(input).includes("birthcert")) return void 0;
|
|
42408
|
+
const words = input.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
|
|
42409
|
+
let verified = false;
|
|
42410
|
+
let basic = false;
|
|
42411
|
+
words.forEach((w, i) => {
|
|
42412
|
+
if (saysVerified(w)) {
|
|
42413
|
+
if (negated(words, i, VERIFIED_NEGATORS)) basic = true;
|
|
42414
|
+
else verified = true;
|
|
42415
|
+
} else if (saysBasic(w)) {
|
|
42416
|
+
if (negated(words, i, NEGATORS)) verified = true;
|
|
42417
|
+
else basic = true;
|
|
42418
|
+
}
|
|
42419
|
+
});
|
|
42420
|
+
if (verified && basic) return "unclear";
|
|
42421
|
+
return verified ? "verified" : "basic";
|
|
42422
|
+
}
|
|
42423
|
+
function isVerifiedAiBirthcertName(input) {
|
|
42424
|
+
return aiBirthcertKindOfName(input) === "verified";
|
|
42425
|
+
}
|
|
42250
42426
|
function findEntry(templateId, network) {
|
|
42251
42427
|
if (/^did:zid:/.test(templateId)) {
|
|
42252
42428
|
const key = network.includes("testnet") ? "testnet" : "mainnet";
|
|
42253
42429
|
return TEMPLATE_ALIASES.find((e) => e[key] === templateId);
|
|
42254
42430
|
}
|
|
42431
|
+
const kind = aiBirthcertKindOfName(templateId);
|
|
42432
|
+
if (kind === "verified" || kind === "unclear") return void 0;
|
|
42255
42433
|
const normalized = normalize2(templateId);
|
|
42256
42434
|
return TEMPLATE_ALIASES.find((e) => normalized.includes(e.match));
|
|
42257
42435
|
}
|
|
@@ -42304,6 +42482,8 @@ async function credentialPreflight(deps, input) {
|
|
|
42304
42482
|
const blockers = [];
|
|
42305
42483
|
const notChecked = [];
|
|
42306
42484
|
const isVerified = input.credential === VERIFIED_AI_BIRTHCERT;
|
|
42485
|
+
const kind = isVerified ? "verified" : deps.isBasicAiBirthcert?.(input.credential) ? "basic" : void 0;
|
|
42486
|
+
const choice = kind ? { choice: aiBirthcertChoice(kind) } : {};
|
|
42307
42487
|
let alreadyHeld;
|
|
42308
42488
|
if (deps.findHeld) {
|
|
42309
42489
|
try {
|
|
@@ -42321,7 +42501,9 @@ async function credentialPreflight(deps, input) {
|
|
|
42321
42501
|
blockers: [
|
|
42322
42502
|
`This wallet already holds a valid ${found.label}${until}${found.vcId ? `, ${found.vcId}` : ""}. Tell the user, and do not start another: a new one would REPLACE it (the wallet keeps one per template) and costs the fee. Only if the user explicitly says they want a replacement, run this check again with replacing: true.`
|
|
42323
42503
|
],
|
|
42324
|
-
notChecked: []
|
|
42504
|
+
notChecked: [],
|
|
42505
|
+
nextStep: "tell_user_already_held",
|
|
42506
|
+
...choice
|
|
42325
42507
|
};
|
|
42326
42508
|
}
|
|
42327
42509
|
} else {
|
|
@@ -42335,11 +42517,43 @@ async function credentialPreflight(deps, input) {
|
|
|
42335
42517
|
);
|
|
42336
42518
|
}
|
|
42337
42519
|
}
|
|
42520
|
+
let unusedPayment;
|
|
42521
|
+
if (isVerified && deps.findUnusedPayment) {
|
|
42522
|
+
try {
|
|
42523
|
+
const found = await deps.findUnusedPayment();
|
|
42524
|
+
if (found) {
|
|
42525
|
+
unusedPayment = { ...found, note: unusedPaymentNote(found) };
|
|
42526
|
+
notChecked.push(
|
|
42527
|
+
"Whether the service still accepts the earlier payment. If it refuses the earlier payment, request_ai_birthcert_verification says so and pays nothing; a new link would then cost the fee again, and the user must agree to that first."
|
|
42528
|
+
);
|
|
42529
|
+
}
|
|
42530
|
+
} catch (e) {
|
|
42531
|
+
notChecked.push(
|
|
42532
|
+
`Whether an earlier payment could be reused could not be checked (${e instanceof Error ? e.message : String(e)}), so the fee below is what a new link would cost if there is none.`
|
|
42533
|
+
);
|
|
42534
|
+
}
|
|
42535
|
+
}
|
|
42338
42536
|
const quoted2 = isVerified ? await deps.quoteVerified({ agentName: input.agentName?.trim() || placeholderAgentName(), dryRun: true }) : await deps.quoteTemplate(input.credential);
|
|
42339
42537
|
const quote = quoted2.quote;
|
|
42340
42538
|
const schema = quoted2.schema;
|
|
42341
42539
|
if (!quote) {
|
|
42342
42540
|
const why = String(quoted2.error ?? quoted2.reason ?? "the price could not be read");
|
|
42541
|
+
if (unusedPayment) {
|
|
42542
|
+
return {
|
|
42543
|
+
credential: input.credential,
|
|
42544
|
+
ready: true,
|
|
42545
|
+
balances: [],
|
|
42546
|
+
unusedPayment,
|
|
42547
|
+
nextStep: "confirm_kind_then_collect_details",
|
|
42548
|
+
blockers: [],
|
|
42549
|
+
notChecked: [
|
|
42550
|
+
...notChecked,
|
|
42551
|
+
`The price could not be read (${why}). It only matters if the earlier payment is refused.`,
|
|
42552
|
+
...uncheckable(isVerified)
|
|
42553
|
+
],
|
|
42554
|
+
...choice
|
|
42555
|
+
};
|
|
42556
|
+
}
|
|
42343
42557
|
return {
|
|
42344
42558
|
credential: input.credential,
|
|
42345
42559
|
ready: false,
|
|
@@ -42347,7 +42561,9 @@ async function credentialPreflight(deps, input) {
|
|
|
42347
42561
|
...alreadyHeld ? { alreadyHeld } : {},
|
|
42348
42562
|
...schema ? { schema } : {},
|
|
42349
42563
|
blockers: [`Could not price this credential: ${why}`],
|
|
42350
|
-
notChecked: [...notChecked, ...uncheckable(isVerified)]
|
|
42564
|
+
notChecked: [...notChecked, ...uncheckable(isVerified)],
|
|
42565
|
+
nextStep: "resolve_blockers",
|
|
42566
|
+
...choice
|
|
42351
42567
|
};
|
|
42352
42568
|
}
|
|
42353
42569
|
const asset = String(quote.asset ?? "");
|
|
@@ -42357,11 +42573,12 @@ async function credentialPreflight(deps, input) {
|
|
|
42357
42573
|
const wanted = asset === NATIVE || !selfPaysGas ? [asset] : [asset, NATIVE];
|
|
42358
42574
|
const balances = await Promise.all(wanted.map((token) => deps.queryTokenBalance(token)));
|
|
42359
42575
|
const isFree = quote.paymentRequired === false;
|
|
42576
|
+
const noPaymentDue = isFree || unusedPayment !== void 0;
|
|
42360
42577
|
const feeBalance = balances[0];
|
|
42361
42578
|
if ("error" in feeBalance) {
|
|
42362
42579
|
blockers.push(`Could not read the ${asset} balance (${feeBalance.error}) \u2014 retry before paying.`);
|
|
42363
|
-
} else if (!
|
|
42364
|
-
blockers.push(`Not enough ${feeBalance.token}: the fee is ${renderAmount(requiredRaw, feeBalance)}, the balance is ${feeBalance.
|
|
42580
|
+
} else if (!noPaymentDue && BigInt(feeBalance.balance) < BigInt(requiredRaw)) {
|
|
42581
|
+
blockers.push(`Not enough ${feeBalance.token}: the fee is ${renderAmount(requiredRaw, feeBalance)}, the balance is ${renderAmount(feeBalance.balance, feeBalance)}.`);
|
|
42365
42582
|
}
|
|
42366
42583
|
const gasNotes = [];
|
|
42367
42584
|
if (selfPaysGas && asset !== NATIVE) {
|
|
@@ -42369,19 +42586,22 @@ async function credentialPreflight(deps, input) {
|
|
|
42369
42586
|
if (gas && "error" in gas) {
|
|
42370
42587
|
const msg = `Could not read the ZTX balance (${gas.error}) \u2014 ZTX pays network gas and is separate from the fee.`;
|
|
42371
42588
|
if (isFree) gasNotes.push(`${msg} Issuance is free right now, so none is needed unless it becomes chargeable.`);
|
|
42589
|
+
else if (unusedPayment) gasNotes.push(`${msg} The next link reuses an earlier payment, so none is needed for it.`);
|
|
42372
42590
|
else blockers.push(msg);
|
|
42373
42591
|
} else if (gas && BigInt(gas.balance) === 0n) {
|
|
42374
42592
|
if (isFree) {
|
|
42375
42593
|
gasNotes.push(
|
|
42376
42594
|
"This wallet holds no ZTX. Issuance is free right now, so no payment is made and no gas is needed; if it becomes chargeable and this wallet pays its own gas, it will need ZTX."
|
|
42377
42595
|
);
|
|
42596
|
+
} else if (unusedPayment) {
|
|
42597
|
+
gasNotes.push("This wallet holds no ZTX. The next link reuses an earlier payment, so no transaction is sent and no gas is needed for it.");
|
|
42378
42598
|
} else {
|
|
42379
42599
|
blockers.push(`No ZTX for network gas. ZTX is separate from the ${feeBalance.token ?? asset} fee and is needed for every transaction.`);
|
|
42380
42600
|
}
|
|
42381
42601
|
}
|
|
42382
42602
|
}
|
|
42383
42603
|
const cap = deps.policyGoverns && await deps.policyGoverns(asset) ? describePolicyGovernedCap(asset) : describePaymentCap(asset, requiredRaw, deps.caps);
|
|
42384
|
-
if (!cap.wouldPass && !
|
|
42604
|
+
if (!cap.wouldPass && !noPaymentDue) blockers.push(renderCapBlocker(cap, requiredRaw, feeBalance));
|
|
42385
42605
|
return {
|
|
42386
42606
|
credential: input.credential,
|
|
42387
42607
|
ready: blockers.length === 0,
|
|
@@ -42396,13 +42616,25 @@ async function credentialPreflight(deps, input) {
|
|
|
42396
42616
|
balances,
|
|
42397
42617
|
cap,
|
|
42398
42618
|
...alreadyHeld ? { alreadyHeld } : {},
|
|
42619
|
+
...unusedPayment ? { unusedPayment } : {},
|
|
42399
42620
|
...schema ? { schema } : {},
|
|
42400
42621
|
blockers,
|
|
42401
|
-
notChecked: [...notChecked, ...gasNotes, ...uncheckable(isVerified, isFree)]
|
|
42622
|
+
notChecked: [...notChecked, ...gasNotes, ...uncheckable(isVerified, isFree)],
|
|
42623
|
+
// An AI Birthcert carries the `choice` question; a bare collect_details made weaker models skip it (review R1).
|
|
42624
|
+
nextStep: blockers.length > 0 ? "resolve_blockers" : kind ? "confirm_kind_then_collect_details" : "collect_details",
|
|
42625
|
+
...choice
|
|
42402
42626
|
};
|
|
42403
42627
|
}
|
|
42628
|
+
function unusedPaymentNote(found) {
|
|
42629
|
+
if (found.settling) {
|
|
42630
|
+
return `No new payment is needed for the next verification link: an earlier payment is still being settled, and request_ai_birthcert_verification follows that payment rather than paying again. Do not ask the user to agree to a fee for it. Its outcome is not known yet, so do not tell the user it succeeded; call check_ai_birthcert_verification to follow it (it was for the name "${found.agentName}").`;
|
|
42631
|
+
}
|
|
42632
|
+
const head = "No new payment is needed for the next verification link: an earlier payment has not been used yet (it is only used up once the credential is issued), and request_ai_birthcert_verification reuses it. Do not ask the user to agree to a fee for it, and do not count it as another payment. The fee below is what a link costs when there is no earlier payment.";
|
|
42633
|
+
return found.linkOpen ? `${head} Its link is still open: calling with the same name, "${found.agentName}", hands back that same link, and a different name is refused until it ends.` : `${head} Its link has ended, so the next link can use any name the user chooses.`;
|
|
42634
|
+
}
|
|
42404
42635
|
function renderAmount(raw, balance) {
|
|
42405
|
-
if ("error" in balance
|
|
42636
|
+
if ("error" in balance) return raw;
|
|
42637
|
+
if (balance.decimals === null) return `${raw} base units of ${balance.token}`;
|
|
42406
42638
|
const d = BigInt(10) ** BigInt(balance.decimals);
|
|
42407
42639
|
const whole = BigInt(raw) / d;
|
|
42408
42640
|
const frac = (BigInt(raw) % d).toString().padStart(balance.decimals, "0").replace(/0+$/, "");
|
|
@@ -42428,7 +42660,7 @@ function uncheckable(isVerified, isFree = false) {
|
|
|
42428
42660
|
}
|
|
42429
42661
|
if (isFree) {
|
|
42430
42662
|
items.unshift(
|
|
42431
|
-
"Whether issuance is still free at apply time \u2014
|
|
42663
|
+
"Whether issuance is still free at apply time \u2014 whether the service charges is a service-wide setting and can change between this quote and issuance."
|
|
42432
42664
|
);
|
|
42433
42665
|
}
|
|
42434
42666
|
return items;
|
|
@@ -46169,6 +46401,15 @@ function defaultRevealFor(kind) {
|
|
|
46169
46401
|
}
|
|
46170
46402
|
|
|
46171
46403
|
// src/mcp-tools.ts
|
|
46404
|
+
function unclearBirthcertKind(name) {
|
|
46405
|
+
return `"${name}" does not say clearly which AI Birthcert it means: it has both Basic and Verified wording. Ask the user whether they want the Basic (self-declared) one or the Verified (MyDigital ID) one, then use "AI Birthcert" or "verified_ai_birthcert".`;
|
|
46406
|
+
}
|
|
46407
|
+
function unclearBirthcertIssue(name) {
|
|
46408
|
+
return { issued: false, reason: `${unclearBirthcertKind(name)} Nothing was paid.`, nextStep: "follow_message" };
|
|
46409
|
+
}
|
|
46410
|
+
function unclearBirthcertPreflight(name) {
|
|
46411
|
+
return { credential: name, ready: false, balances: [], blockers: [unclearBirthcertKind(name)], notChecked: [], nextStep: "resolve_blockers" };
|
|
46412
|
+
}
|
|
46172
46413
|
async function loadValidCachedCredentials(cache) {
|
|
46173
46414
|
if (!cache) return [];
|
|
46174
46415
|
const all = await cache.list();
|
|
@@ -46302,6 +46543,13 @@ function createTools(deps) {
|
|
|
46302
46543
|
return payAndFetch(deps.payer, input);
|
|
46303
46544
|
},
|
|
46304
46545
|
async subscribe_and_issue(input) {
|
|
46546
|
+
if (aiBirthcertKindOfName(input.templateId) === "unclear") return unclearBirthcertIssue(input.templateId);
|
|
46547
|
+
if (isVerifiedAiBirthcertName(input.templateId)) {
|
|
46548
|
+
return {
|
|
46549
|
+
issued: false,
|
|
46550
|
+
reason: `"${input.templateId}" is the Verified AI Birthcert, which this tool cannot issue: nothing was paid. Use request_ai_birthcert_verification for it. For the self-declared Basic AI Birthcert, use the name "AI Birthcert".`
|
|
46551
|
+
};
|
|
46552
|
+
}
|
|
46305
46553
|
const resolved = resolveTemplateAlias(input.templateId, deps.config.network);
|
|
46306
46554
|
const templateId = resolved ?? input.templateId;
|
|
46307
46555
|
const attributes = deriveTemplateAttributes(templateId, deps.config.network, input.attributes ?? {});
|
|
@@ -46310,8 +46558,10 @@ function createTools(deps) {
|
|
|
46310
46558
|
return { issued: false, reason: errors.join("; ") };
|
|
46311
46559
|
}
|
|
46312
46560
|
const passDesignId = resolvePassDesignId(templateId, deps.config.network);
|
|
46561
|
+
const basicId = resolveTemplateAlias("ai birthcert", deps.config.network);
|
|
46562
|
+
const credentialLabel = basicId !== void 0 && templateId === basicId ? "Basic AI Birthcert (self-declared, not identity-verified)" : void 0;
|
|
46313
46563
|
const result = await subscribeAndIssue(
|
|
46314
|
-
{ ...deps.subscribeDeps, passDesignId },
|
|
46564
|
+
{ ...deps.subscribeDeps, passDesignId, ...credentialLabel ? { credentialLabel } : {} },
|
|
46315
46565
|
{ ...input, templateId, attributes }
|
|
46316
46566
|
);
|
|
46317
46567
|
if (!result.schema) return result;
|
|
@@ -46382,9 +46632,18 @@ function createTools(deps) {
|
|
|
46382
46632
|
* balances, the spending cap — into a single verdict, so the balance and the cap stop being two
|
|
46383
46633
|
* sequential dead ends.
|
|
46384
46634
|
*/
|
|
46385
|
-
async credential_preflight(
|
|
46635
|
+
async credential_preflight(rawInput) {
|
|
46636
|
+
if (aiBirthcertKindOfName(rawInput.credential) === "unclear") return unclearBirthcertPreflight(rawInput.credential);
|
|
46637
|
+
const input = isVerifiedAiBirthcertName(rawInput.credential) ? { ...rawInput, credential: VERIFIED_AI_BIRTHCERT } : rawInput;
|
|
46386
46638
|
if (input.credential === VERIFIED_AI_BIRTHCERT && !deps.verifyAiBirthcert) {
|
|
46387
|
-
return {
|
|
46639
|
+
return {
|
|
46640
|
+
credential: input.credential,
|
|
46641
|
+
ready: false,
|
|
46642
|
+
balances: [],
|
|
46643
|
+
blockers: [AI_BIRTHCERT_NOT_CONFIGURED_ERROR],
|
|
46644
|
+
notChecked: [],
|
|
46645
|
+
choice: aiBirthcertChoice("verified", { unavailable: true })
|
|
46646
|
+
};
|
|
46388
46647
|
}
|
|
46389
46648
|
if (!deps.queryTokenBalance) {
|
|
46390
46649
|
return {
|
|
@@ -46395,8 +46654,11 @@ function createTools(deps) {
|
|
|
46395
46654
|
notChecked: []
|
|
46396
46655
|
};
|
|
46397
46656
|
}
|
|
46657
|
+
const basicId = resolveTemplateAlias("ai birthcert", deps.config.network);
|
|
46658
|
+
const isBasicAiBirthcert = (credential) => basicId !== void 0 && (credential === basicId || resolveTemplateAlias(credential, deps.config.network) === basicId);
|
|
46398
46659
|
return credentialPreflight(
|
|
46399
46660
|
{
|
|
46661
|
+
isBasicAiBirthcert,
|
|
46400
46662
|
quoteVerified: (q) => deps.verifyAiBirthcert.request(q),
|
|
46401
46663
|
quoteTemplate: async (templateId) => {
|
|
46402
46664
|
const resolved = resolveTemplateAlias(templateId, deps.config.network) ?? templateId;
|
|
@@ -46405,7 +46667,8 @@ function createTools(deps) {
|
|
|
46405
46667
|
queryTokenBalance: deps.queryTokenBalance,
|
|
46406
46668
|
caps: deps.paymentCaps,
|
|
46407
46669
|
policyGoverns: deps.policyGoverns,
|
|
46408
|
-
...deps.findHeldCredential ? { findHeld: deps.findHeldCredential } : {}
|
|
46670
|
+
...deps.findHeldCredential ? { findHeld: deps.findHeldCredential } : {},
|
|
46671
|
+
...deps.verifyAiBirthcert?.findUnusedPayment ? { findUnusedPayment: deps.verifyAiBirthcert.findUnusedPayment } : {}
|
|
46409
46672
|
},
|
|
46410
46673
|
input
|
|
46411
46674
|
);
|
|
@@ -48058,6 +48321,14 @@ async function decidePriorSession(deps, agentName) {
|
|
|
48058
48321
|
if (status.status === "issued") return { kind: "pay_fresh" };
|
|
48059
48322
|
return { kind: "replay_receipt", receipt: stored.paymentReceipt, stored };
|
|
48060
48323
|
}
|
|
48324
|
+
async function findUnusedVerificationPayment(deps) {
|
|
48325
|
+
const stored = await deps.sessionStore.get();
|
|
48326
|
+
if (!stored) return void 0;
|
|
48327
|
+
if (stored.sessionId === "") return { agentName: stored.agentName, linkOpen: false, settling: true };
|
|
48328
|
+
const status = await getConfirmedStatus(deps, stored.sessionId);
|
|
48329
|
+
if (status !== "gone" && status.status === "issued") return void 0;
|
|
48330
|
+
return { agentName: stored.agentName, linkOpen: status !== "gone" && status.status === "pending" };
|
|
48331
|
+
}
|
|
48061
48332
|
var requestQueue = Promise.resolve();
|
|
48062
48333
|
function withRequestLock(fn) {
|
|
48063
48334
|
const run = requestQueue.then(fn, fn);
|
|
@@ -48177,6 +48448,8 @@ function ssivcAnswerNote(cause) {
|
|
|
48177
48448
|
var TELL_USER_UNCONFIRMED = ` Tell the user: "Your payment was sent, but the credential service has not confirmed it yet, so I cannot say whether it went through. The receipt is saved \u2014 please do not pay again. Please message me again in a few minutes and I will check."`;
|
|
48178
48449
|
var TELL_USER_QUEUED = ` Tell the user: "Your payment was sent and the settlement is still being processed. The receipt is saved \u2014 please do not pay again. Please message me again in a few minutes and I will check."`;
|
|
48179
48450
|
var RECHECK_MESSAGE_URGENT_THRESHOLD_SECONDS = 300;
|
|
48451
|
+
var NO_NEW_PAYMENT_REPLAYED = "No new payment was made on this call: this new link uses a payment already made, which is only used up once the credential is issued. Do not tell the user they paid again, and do not count this link as another fee. ";
|
|
48452
|
+
var NO_NEW_PAYMENT_SAME_LINK = "No new payment was made on this call: this is the same link as before, still open, and already paid for. Do not tell the user they paid again. ";
|
|
48180
48453
|
function noAutoRecheckMessage(expiresAt, now) {
|
|
48181
48454
|
const expiry = sessionExpiryFields(expiresAt, now);
|
|
48182
48455
|
if (expiry && expiry.expiresInSeconds === 0) {
|
|
@@ -48308,11 +48581,13 @@ async function attemptCandidate(deps, accept, buildBody, onQueued) {
|
|
|
48308
48581
|
const initialOutcome = await deps.ssivc.createSessionSettle(await buildBody(), xPayment);
|
|
48309
48582
|
return resolveSettlement(deps, buildBody, initialOutcome, onQueued);
|
|
48310
48583
|
}
|
|
48311
|
-
async function payAndCreateSession(deps, buildBody, onQueued) {
|
|
48584
|
+
async function payAndCreateSession(deps, buildBody, onQueued, confirmed) {
|
|
48312
48585
|
const challenge = await deps.ssivc.createSessionChallenge(await buildBody());
|
|
48313
|
-
const candidates = orderAccepts(challenge.accepts,
|
|
48586
|
+
const candidates = orderAccepts(challenge.accepts, confirmed.gasModel);
|
|
48314
48587
|
const primary = candidates[0];
|
|
48315
48588
|
if (!primary) throw new NoPaymentOptionsError("SSIVC 402 returned no usable payment options");
|
|
48589
|
+
const difference = priceMismatch(confirmed, quoteOf(primary));
|
|
48590
|
+
if (difference) throw new PriceChangedError(verificationQuoteOf(primary), difference);
|
|
48316
48591
|
try {
|
|
48317
48592
|
return await attemptCandidate(deps, primary, buildBody, onQueued);
|
|
48318
48593
|
} catch (err) {
|
|
@@ -48320,11 +48595,70 @@ async function payAndCreateSession(deps, buildBody, onQueued) {
|
|
|
48320
48595
|
if (!fallback || !isSponsored(primary) || isSponsored(fallback) || !isDefinitiveSponsorshipFailure(err)) {
|
|
48321
48596
|
throw await toFacilitatorInsufficientFundsError(err, primary, deps.formatAssetAmount);
|
|
48322
48597
|
}
|
|
48323
|
-
|
|
48598
|
+
throw new PriceChangedError(
|
|
48599
|
+
verificationQuoteOf(fallback),
|
|
48600
|
+
"the sponsored option was refused before anything was paid, and the only option left is paying the network gas yourself, at its own price"
|
|
48601
|
+
);
|
|
48324
48602
|
}
|
|
48325
48603
|
}
|
|
48604
|
+
function verificationQuoteOf(accept) {
|
|
48605
|
+
const q = quoteOf(accept);
|
|
48606
|
+
return { asset: q.asset, maxAmountRequired: q.maxAmountRequired, ...q.payTo ? { payTo: q.payTo } : {}, gasModel: q.gasModel };
|
|
48607
|
+
}
|
|
48608
|
+
var PriceChangedError = class extends Error {
|
|
48609
|
+
constructor(quote, difference) {
|
|
48610
|
+
super(`price changed: ${difference}`);
|
|
48611
|
+
this.quote = quote;
|
|
48612
|
+
this.difference = difference;
|
|
48613
|
+
this.name = "PriceChangedError";
|
|
48614
|
+
}
|
|
48615
|
+
};
|
|
48326
48616
|
async function requestAiBirthcertVerification(deps, input) {
|
|
48327
|
-
return withSessionExpiry(await requestAiBirthcertVerificationInner(deps, input), deps.now());
|
|
48617
|
+
return withNextStep(withSessionExpiry(await requestAiBirthcertVerificationInner(deps, input), deps.now()));
|
|
48618
|
+
}
|
|
48619
|
+
function withNextStep(result) {
|
|
48620
|
+
if ("needsConfirmation" in result) return result;
|
|
48621
|
+
if ("error" in result) return { ...result, nextStep: "follow_message" };
|
|
48622
|
+
if ("existingVerifiedVc" in result) {
|
|
48623
|
+
const until = result.existingVerifiedVc.validUntil ? `, valid until ${result.existingVerifiedVc.validUntil.slice(0, 10)}` : "";
|
|
48624
|
+
return {
|
|
48625
|
+
...result,
|
|
48626
|
+
nextStep: "ask_user_whether_to_replace",
|
|
48627
|
+
askUser: `You already have a Verified AI Birthcert${until}. Do you want to replace it?`
|
|
48628
|
+
};
|
|
48629
|
+
}
|
|
48630
|
+
if ("settlementPending" in result) {
|
|
48631
|
+
return { ...result, nextStep: result.issuerRejected || result.paymentInvalid ? "follow_message" : "wait_then_check" };
|
|
48632
|
+
}
|
|
48633
|
+
if ("sessionId" in result) return { ...result, nextStep: "show_link_and_wait" };
|
|
48634
|
+
return result;
|
|
48635
|
+
}
|
|
48636
|
+
async function askToConfirmPayment2(deps, agentName, input, lead = "") {
|
|
48637
|
+
const quoted2 = await quoteVerification(deps, agentName, input);
|
|
48638
|
+
if (!("quote" in quoted2)) return quoted2;
|
|
48639
|
+
return needsConfirmationResult(deps, quoted2.quote, lead);
|
|
48640
|
+
}
|
|
48641
|
+
async function needsConfirmationResult(deps, quote, lead, afterDiscard = false) {
|
|
48642
|
+
const display = await renderPrice2(deps.formatAssetAmount, quote.asset, quote.maxAmountRequired) + gasNote(quote);
|
|
48643
|
+
const keep = afterDiscard ? `The stuck receipt was already discarded on this call, so call again WITHOUT discardStuckReceiptAndPayFresh, keeping every other argument the same.` : `Keep every other argument the same (confirmReplaceExistingVc and discardStuckReceiptAndPayFresh too, if you sent them).`;
|
|
48644
|
+
return {
|
|
48645
|
+
needsConfirmation: true,
|
|
48646
|
+
paidThisCall: false,
|
|
48647
|
+
quote,
|
|
48648
|
+
nextStep: "ask_user_to_confirm_payment",
|
|
48649
|
+
askUser: `Starting the Verified AI Birthcert verification costs ${display}. Shall I go ahead?`,
|
|
48650
|
+
message: `NOT PAID YET: ${lead}starting this verification costs ${display}, and nothing has been paid. Tell the user the price and ask. Only after their explicit yes to this payment, call again with confirm set to this exact quote object (asset, maxAmountRequired, payTo, gasModel, unchanged). ${keep} A yes to something else, such as the name, is not a yes to the payment; confirm: true is not accepted.`
|
|
48651
|
+
};
|
|
48652
|
+
}
|
|
48653
|
+
async function renderPrice2(formatAssetAmount, asset, raw) {
|
|
48654
|
+
if (raw === "") return `an amount the service did not state (asset "${asset}")`;
|
|
48655
|
+
if (formatAssetAmount) {
|
|
48656
|
+
try {
|
|
48657
|
+
return await formatAssetAmount(asset, raw);
|
|
48658
|
+
} catch {
|
|
48659
|
+
}
|
|
48660
|
+
}
|
|
48661
|
+
return `${raw} base units of asset "${asset}"`;
|
|
48328
48662
|
}
|
|
48329
48663
|
async function requestAiBirthcertVerificationInner(deps, input) {
|
|
48330
48664
|
if (!input.agentName || !input.agentName.trim()) {
|
|
@@ -48378,7 +48712,7 @@ async function quoteVerification(deps, agentName, input) {
|
|
|
48378
48712
|
}
|
|
48379
48713
|
};
|
|
48380
48714
|
}
|
|
48381
|
-
async function discardStuckReceiptBeforePaying(deps, confirmReceiptId) {
|
|
48715
|
+
async function discardStuckReceiptBeforePaying(deps, confirmReceiptId, options = {}) {
|
|
48382
48716
|
const stored = await deps.sessionStore.get();
|
|
48383
48717
|
if (!stored) {
|
|
48384
48718
|
return {
|
|
@@ -48413,6 +48747,7 @@ async function discardStuckReceiptBeforePaying(deps, confirmReceiptId) {
|
|
|
48413
48747
|
}
|
|
48414
48748
|
};
|
|
48415
48749
|
}
|
|
48750
|
+
if (options.checkOnly) return { discarded: stored.paymentReceipt };
|
|
48416
48751
|
await deps.sessionStore.clear();
|
|
48417
48752
|
return { discarded: stored.paymentReceipt };
|
|
48418
48753
|
}
|
|
@@ -48474,7 +48809,7 @@ async function checkExistingVerifiedVc(deps, input) {
|
|
|
48474
48809
|
return { kind: "none" };
|
|
48475
48810
|
}
|
|
48476
48811
|
function canCarryReplacedExpiredVc(result) {
|
|
48477
|
-
return typeof result === "object" && result !== null && !("error" in result);
|
|
48812
|
+
return typeof result === "object" && result !== null && !("error" in result) && !("needsConfirmation" in result);
|
|
48478
48813
|
}
|
|
48479
48814
|
async function requestAiBirthcertVerificationLocked(deps, agentName, input) {
|
|
48480
48815
|
const existingVc = await checkExistingVerifiedVc(deps, input);
|
|
@@ -48489,6 +48824,16 @@ async function requestAiBirthcertVerificationLockedInner(deps, agentName, input)
|
|
|
48489
48824
|
if (input.discardStuckReceiptAndPayFresh === void 0) {
|
|
48490
48825
|
return payOrReplayLocked(deps, agentName, input);
|
|
48491
48826
|
}
|
|
48827
|
+
if (!asPriceConfirmation(input.confirm)) {
|
|
48828
|
+
const check = await discardStuckReceiptBeforePaying(deps, input.discardStuckReceiptAndPayFresh, { checkOnly: true });
|
|
48829
|
+
if ("refusal" in check) return check.refusal;
|
|
48830
|
+
return askToConfirmPayment2(
|
|
48831
|
+
deps,
|
|
48832
|
+
agentName,
|
|
48833
|
+
input,
|
|
48834
|
+
`receipt ${input.discardStuckReceiptAndPayFresh} has NOT been discarded yet. Starting over forfeits it and `
|
|
48835
|
+
);
|
|
48836
|
+
}
|
|
48492
48837
|
const outcome = await discardStuckReceiptBeforePaying(deps, input.discardStuckReceiptAndPayFresh);
|
|
48493
48838
|
if ("refusal" in outcome) return outcome.refusal;
|
|
48494
48839
|
let result;
|
|
@@ -48512,8 +48857,14 @@ async function requestAiBirthcertVerificationLockedInner(deps, agentName, input)
|
|
|
48512
48857
|
async function payOrReplayLocked(deps, agentName, input) {
|
|
48513
48858
|
const decision = await decidePriorSession(deps, agentName);
|
|
48514
48859
|
if (decision.kind === "still_pending")
|
|
48515
|
-
return {
|
|
48860
|
+
return {
|
|
48861
|
+
...decision.result,
|
|
48862
|
+
paidThisCall: false,
|
|
48863
|
+
message: NO_NEW_PAYMENT_SAME_LINK + noAutoRecheckMessage(decision.result.expiresAt, deps.now())
|
|
48864
|
+
};
|
|
48516
48865
|
if (decision.kind === "blocked") return { error: decision.message };
|
|
48866
|
+
const confirmed = asPriceConfirmation(input.confirm);
|
|
48867
|
+
if (decision.kind === "pay_fresh" && !confirmed) return askToConfirmPayment2(deps, agentName, input);
|
|
48517
48868
|
const effectiveOptionalFields = {
|
|
48518
48869
|
...decision.kind === "replay_receipt" ? optionalRequestFields(decision.stored) : {},
|
|
48519
48870
|
...optionalRequestFields(input)
|
|
@@ -48558,7 +48909,9 @@ async function payOrReplayLocked(deps, agentName, input) {
|
|
|
48558
48909
|
paid = await payAndCreateSession(
|
|
48559
48910
|
{ ...deps, gasPreference: requestedGasPayer ?? deps.gasPreference },
|
|
48560
48911
|
buildBody,
|
|
48561
|
-
persistQueuedReceipt
|
|
48912
|
+
persistQueuedReceipt,
|
|
48913
|
+
// Set on this path: the gate above returned for an unconfirmed fresh payment.
|
|
48914
|
+
confirmed
|
|
48562
48915
|
);
|
|
48563
48916
|
}
|
|
48564
48917
|
} catch (rawErr) {
|
|
@@ -48573,6 +48926,15 @@ async function payOrReplayLocked(deps, agentName, input) {
|
|
|
48573
48926
|
};
|
|
48574
48927
|
}
|
|
48575
48928
|
if (err instanceof NoPaymentOptionsError) return { error: err.message };
|
|
48929
|
+
if (err instanceof PriceChangedError) {
|
|
48930
|
+
return needsConfirmationResult(
|
|
48931
|
+
deps,
|
|
48932
|
+
err.quote,
|
|
48933
|
+
`the payment request is not what the user agreed to (${err.difference}), so nothing was paid. The new price: `,
|
|
48934
|
+
// payOrReplayLocked runs with this set only after the receipt has been discarded (requestAiBirthcertVerificationLockedInner).
|
|
48935
|
+
input.discardStuckReceiptAndPayFresh !== void 0
|
|
48936
|
+
);
|
|
48937
|
+
}
|
|
48576
48938
|
if (err instanceof FacilitatorInsufficientFundsError) return { error: err.message };
|
|
48577
48939
|
if (err instanceof SettlementStillQueuedError) {
|
|
48578
48940
|
try {
|
|
@@ -48591,7 +48953,7 @@ async function payOrReplayLocked(deps, agentName, input) {
|
|
|
48591
48953
|
error: `PAYMENT SENT \u2014 the sponsored settlement is still processing, but the receipt could NOT be saved on this machine. Keep this value safe, it is the only record of the payment: ${err.paymentReceipt}. Do not pay again. Checking the status will not find it \u2014 quote this receipt id to support to resume manually.`
|
|
48592
48954
|
};
|
|
48593
48955
|
}
|
|
48594
|
-
return { settlementPending: true, paymentReceipt: err.paymentReceipt, message: err.message };
|
|
48956
|
+
return { settlementPending: true, paymentReceipt: err.paymentReceipt, paidThisCall, message: err.message };
|
|
48595
48957
|
}
|
|
48596
48958
|
if (isAgentNameInUse(err)) {
|
|
48597
48959
|
return { error: agentNameInUseMessage(err, agentName, paidThisCall) };
|
|
@@ -48623,6 +48985,7 @@ async function payOrReplayLocked(deps, agentName, input) {
|
|
|
48623
48985
|
paymentReceipt: err.paymentReceipt,
|
|
48624
48986
|
settlementPending: true,
|
|
48625
48987
|
issuerRejected: true,
|
|
48988
|
+
paidThisCall,
|
|
48626
48989
|
message: agentNameInUseMessage(err.cause, agentName, paidThisCall, err.paymentReceipt)
|
|
48627
48990
|
};
|
|
48628
48991
|
}
|
|
@@ -48631,6 +48994,7 @@ async function payOrReplayLocked(deps, agentName, input) {
|
|
|
48631
48994
|
paymentReceipt: err.paymentReceipt,
|
|
48632
48995
|
settlementPending: true,
|
|
48633
48996
|
paymentInvalid: true,
|
|
48997
|
+
paidThisCall,
|
|
48634
48998
|
// paidThisCall, because on the fresh-pay route deps.pay already ran on THIS call
|
|
48635
48999
|
// before SSIVC ruled on the receipt. The builder's default wording denies that.
|
|
48636
49000
|
message: paymentInvalidMessage(err.cause, err.paymentReceipt, age.humanAge, paidThisCall)
|
|
@@ -48641,6 +49005,7 @@ async function payOrReplayLocked(deps, agentName, input) {
|
|
|
48641
49005
|
paymentReceipt: err.paymentReceipt,
|
|
48642
49006
|
settlementPending: true,
|
|
48643
49007
|
issuerRejected: true,
|
|
49008
|
+
paidThisCall,
|
|
48644
49009
|
// See the paymentInvalidMessage call just above.
|
|
48645
49010
|
message: issuerRejectionMessage(err.cause, err.paymentReceipt, age.humanAge, paidThisCall)
|
|
48646
49011
|
};
|
|
@@ -48649,6 +49014,7 @@ async function payOrReplayLocked(deps, agentName, input) {
|
|
|
48649
49014
|
return {
|
|
48650
49015
|
paymentReceipt: err.paymentReceipt,
|
|
48651
49016
|
settlementPending: true,
|
|
49017
|
+
paidThisCall,
|
|
48652
49018
|
// The paidThisCall half does not open with "Nothing is lost". A fee was spent on
|
|
48653
49019
|
// this very call with an outcome nobody knows yet, so "nothing is lost" sitting against that
|
|
48654
49020
|
// admission reads as reassurance the wallet cannot give. The replay half keeps it: there
|
|
@@ -48677,7 +49043,11 @@ async function payOrReplayLocked(deps, agentName, input) {
|
|
|
48677
49043
|
// queued writes, so it carries the fields for consistency.
|
|
48678
49044
|
...effectiveOptionalFields
|
|
48679
49045
|
});
|
|
48680
|
-
return {
|
|
49046
|
+
return {
|
|
49047
|
+
...paid.session,
|
|
49048
|
+
paidThisCall,
|
|
49049
|
+
message: (paidThisCall ? "" : NO_NEW_PAYMENT_REPLAYED) + noAutoRecheckMessage(paid.session.expiresAt, deps.now())
|
|
49050
|
+
};
|
|
48681
49051
|
}
|
|
48682
49052
|
async function advanceQueuedSettlement(deps, stored) {
|
|
48683
49053
|
const buildBody = () => buildSessionBody(deps, stored.agentName, {
|
|
@@ -49442,17 +49812,17 @@ async function checkAffordability(deps, accept) {
|
|
|
49442
49812
|
fee = { status: "unknown", required: required2 };
|
|
49443
49813
|
problems.push(`Could not read the ${asset} balance (${clip4(String(feeBalance.error), 60)}) \u2014 the fee cannot be checked.`);
|
|
49444
49814
|
} else if (BigInt(feeBalance.balance) < BigInt(required2)) {
|
|
49445
|
-
fee = { status: "short", required: required2, balance: feeBalance.balance, display: feeBalance.
|
|
49815
|
+
fee = { status: "short", required: required2, balance: feeBalance.balance, display: renderAmount(feeBalance.balance, feeBalance) };
|
|
49446
49816
|
problems.push(
|
|
49447
|
-
`Not enough ${clip4(feeBalance.token, 40)}: the quote is ${renderAmount(required2, feeBalance)}, the balance is ${clip4(feeBalance.
|
|
49817
|
+
`Not enough ${clip4(feeBalance.token, 40)}: the quote is ${renderAmount(required2, feeBalance)}, the balance is ${clip4(renderAmount(feeBalance.balance, feeBalance), 60)}.` + (asset === NATIVE3 ? " The payer needs the amount plus the network fee from this one balance." : "")
|
|
49448
49818
|
);
|
|
49449
49819
|
} else if (asset === NATIVE3 && BigInt(feeBalance.balance) === BigInt(required2)) {
|
|
49450
|
-
fee = { status: "short", required: required2, balance: feeBalance.balance, display: feeBalance.
|
|
49820
|
+
fee = { status: "short", required: required2, balance: feeBalance.balance, display: renderAmount(feeBalance.balance, feeBalance) };
|
|
49451
49821
|
problems.push(
|
|
49452
49822
|
`Not enough ZTX: the quote is ${renderAmount(required2, feeBalance)} and the balance is exactly that, leaving nothing for the network fee, which the payer needs on top of the amount.`
|
|
49453
49823
|
);
|
|
49454
49824
|
} else {
|
|
49455
|
-
fee = { status: "enough", required: required2, balance: feeBalance.balance, display: feeBalance.
|
|
49825
|
+
fee = { status: "enough", required: required2, balance: feeBalance.balance, display: renderAmount(feeBalance.balance, feeBalance) };
|
|
49456
49826
|
}
|
|
49457
49827
|
let gas;
|
|
49458
49828
|
if (!gasBalance) {
|
|
@@ -49490,28 +49860,35 @@ async function checkAffordability(deps, accept) {
|
|
|
49490
49860
|
// src/index.ts
|
|
49491
49861
|
var packageVersion = package_default.version;
|
|
49492
49862
|
var POLICY_HTTP_TIMEOUT_MS = 6e4;
|
|
49863
|
+
var CONFIRM_QUOTE_PROPERTIES = {
|
|
49864
|
+
asset: { type: "string", description: "Exactly as the quote gave it." },
|
|
49865
|
+
maxAmountRequired: { type: "string", description: "Exactly as the quote gave it, in raw base units." },
|
|
49866
|
+
payTo: { type: "string", description: "Exactly as the quote gave it; leave it out only if the quote had none." },
|
|
49867
|
+
gasModel: { type: "string", enum: ["sponsored", "self"], description: "Exactly as the quote gave it." },
|
|
49868
|
+
assetSymbol: { type: "string", description: "For display only, when the quote has it; ignored when the price is checked." }
|
|
49869
|
+
};
|
|
49493
49870
|
function buildToolList() {
|
|
49494
49871
|
return [
|
|
49495
49872
|
{
|
|
49496
49873
|
name: "wallet_status",
|
|
49497
|
-
description: "Report the holder DID/address/network and the client-supplied held credentials. With `token`, also returns that token's balance and its contract address (`tokenAddress`) \u2014 the value a policy's tokenAddress needs, so the user never has to be asked for it.",
|
|
49874
|
+
description: "Report the holder DID/address/network and the client-supplied held credentials. With `token`, also returns that token's balance and its contract address (`tokenAddress`) \u2014 the value a policy's tokenAddress needs, so the user never has to be asked for it. Report a balance to the user as its `display` amount only.",
|
|
49498
49875
|
inputSchema: {
|
|
49499
49876
|
type: "object",
|
|
49500
49877
|
properties: {
|
|
49501
49878
|
heldCredentials: { type: "array", items: { type: "object" }, description: "VCs the client holds. Omit to report whatever the wallet has cached locally from prior subscribe_and_issue calls instead." },
|
|
49502
|
-
token: { type: "string", description: 'Optional token symbol (e.g. "ZTX", "JMYR") OR a ZTP20 contract address, to check its balance for the active network alongside the usual status fields. Returns { balance, decimals, display } \u2014 `balance` is in the asset\'s raw base units and `display` is the same amount in whole tokens with its symbol (e.g. balance "473999900", decimals 6, display "473.9999 JMYR"). Quote
|
|
49503
|
-
tokens: { type: "array", items: { type: "string" }, description: 'Several tokens (symbols and/or ZTP20 contract addresses) in one call, returned as `tokenBalances` in the order asked. Prefer this over repeated single-token calls when checking affordability: a credential fee and the native ZTX needed for gas are separate balances, and asking one at a time is how "you hold the token but no gas" is discovered only after the first shortfall was already fixed. Each entry carries its own result or error, so one failure does not hide the rest.' }
|
|
49879
|
+
token: { type: "string", description: 'Optional token symbol (e.g. "ZTX", "JMYR") OR a ZTP20 contract address, to check its balance for the active network alongside the usual status fields. Returns { balance, decimals, display } \u2014 `balance` is in the asset\'s raw base units and `display` is the same amount in whole tokens with its symbol (e.g. balance "473999900", decimals 6, display "473.9999 JMYR"). Quote only the `display` value to the user, never a bare `balance`, and never add the base units or the decimals unless the user asks. When `decimals` is null, `display` is still in raw base units, so say the amount could not be converted to whole tokens rather than quoting it as a balance. A failed lookup reports { error: "query_failed" } rather than a zero balance; an unrecognised name reports { error: "unknown_token" }.' },
|
|
49880
|
+
tokens: { type: "array", items: { type: "string" }, description: 'Several tokens (symbols and/or ZTP20 contract addresses) in one call, returned as `tokenBalances` in the order asked. Prefer this over repeated single-token calls when checking affordability: a credential fee and the native ZTX needed for gas are separate balances, and asking one at a time is how "you hold the token but no gas" is discovered only after the first shortfall was already fixed. Each entry carries its own result or error, so one failure does not hide the rest. Report each entry as for `token`: quote only its `display`, and when its `decimals` is null say the amount could not be converted to whole tokens.' }
|
|
49504
49881
|
}
|
|
49505
49882
|
}
|
|
49506
49883
|
},
|
|
49507
49884
|
{
|
|
49508
49885
|
name: "prove_identity",
|
|
49509
|
-
description: "Answer an x401 PROOF-REQUEST and return the PROOF-RESPONSE header to replay to the resource server.",
|
|
49886
|
+
description: "Answer an x401 PROOF-REQUEST and return the PROOF-RESPONSE header to replay to the resource server. If the wallet holds several credentials, pass vc: use the one the request or the verifier's challenge names, and if it names none, prefer the Verified AI Birthcert over the Basic one and say which one you presented.",
|
|
49510
49887
|
inputSchema: {
|
|
49511
49888
|
type: "object",
|
|
49512
49889
|
properties: {
|
|
49513
49890
|
proofRequest: { type: "string", description: "The PROOF-REQUEST header value from the 401 challenge." },
|
|
49514
|
-
vc: { type: "object", description: "The VerifiableCredential to present. Omit to use the wallet's single locally-cached credential, if there is exactly one \u2014 the call fails with a clear error if none or several are cached." },
|
|
49891
|
+
vc: { type: "object", description: "The VerifiableCredential to present. Omit to use the wallet's single locally-cached credential, if there is exactly one \u2014 the call fails with a clear error if none or several are cached, so with several, pass it." },
|
|
49515
49892
|
revealAttribute: { type: "array", items: { type: "string" }, description: "Dotted disclosure paths to reveal. Omit to reveal exactly the claims the challenge (DCQL) requests; a challenge naming no claims reveals all." },
|
|
49516
49893
|
issuerKeys: {
|
|
49517
49894
|
type: "object",
|
|
@@ -49877,7 +50254,7 @@ function buildToolList() {
|
|
|
49877
50254
|
},
|
|
49878
50255
|
{
|
|
49879
50256
|
name: "subscribe_and_issue",
|
|
49880
|
-
description: "Obtain a VC from MBI: build the signed payload, pay x402, and return the issued credential. If a spending policy refuses the fee, the result has `policyDenied: true` (a decision: do not retry) or `policyCheckUnavailable: true` (transient: trying again shortly is right); either way nothing was paid and no credential was issued. If a still-valid credential for this templateId is already cached locally, it is returned
|
|
50257
|
+
description: "Obtain a VC from MBI: build the signed payload, pay x402, and return the issued credential. If a spending policy refuses the fee, the result has `policyDenied: true` (a decision: do not retry) or `policyCheckUnavailable: true` (transient: trying again shortly is right); either way nothing was paid and no credential was issued. If a still-valid credential for this templateId is already cached locally, it is returned with no payment as { issued: false, alreadyHeld: true, fromCache: true, reason }: the user already has it, so never tell the user it was just issued. If it carries different values from the ones asked for (a different username, say), `differsFromRequest` lists them: tell the user and ask whether to keep it or replace it. Setting forceReissue:true alone does NOT pay again \u2014 it only asks to be shown that existing credential first: you get back { issued: false, vcId, vc, fromCache: true, reason } instead, and nothing is paid. Show that existing credential to the user and ask whether they actually want to replace it; only call this tool again, with BOTH forceReissue:true and confirmReplaceExistingVc set to exactly the vcId you were shown, if they explicitly say yes. Never pass confirmReplaceExistingVc on your own judgement. When nothing valid is cached, forceReissue has no effect either way \u2014 that case has always paid and issued fresh. When a call DOES render a full credential to the user (a fresh issuance, a plain cache hit, or a confirmed replace), show every claim under vc.credentialSubject as a table \u2014 not a cherry-picked subset \u2014 labelling each key by title-casing it (the exact claim set varies by template; use schema.required/optional to know what to expect), plus vcId and validUntil if present. If vcPassImagePaths is also present, the credential's own pass-design image(s) came back attached to this result \u2014 tell the user their credential's official pass design is shown below and display the image(s); do not silently drop them from your summary just because they are not text. Payment is asset-agnostic \u2014 MBI's 402 challenge may quote the native ZETRIX token or a ZTP20 token (e.g. JMYR); pass dryRun:true first to see the quoted asset/amount for free before committing to pay. What a call actually cost is reported precisely: paidAsset/amountPaid are set ONLY when this call paid, a cache hit reports the earlier charge under originalPayment instead (never as amountPaid, so summing spend cannot double-count), and any failure after the payment has settled on chain reports paymentAttempted: { asset, amount, paymentId }. Two such failures exist and mean different things: MBI 4006 is a definitive facilitator rejection, while 4012 (HTTP 502) means the outcome is INDETERMINATE \u2014 the payment may well have landed. On 4012 the wallet automatically polls MBI's recovery endpoint and reports recovery: { status, txHash?, vcId?, polls }, where status is ISSUED (the credential exists after all \u2014 fetch it by vcId, since recovery returns no VC body), FAILED, or REQUIRED/SETTLED (still unresolved) / UNKNOWN (recovery itself unreachable). NEVER retry a payment after either failure: the funds may already be gone, and a retry charges the full amount again \u2014 look the paymentId up instead. Every response also includes { schema: { required, optional } } whenever the template schema could be read from chain \u2014 the template's full declared attribute list, so you see the complete field list, not just what went wrong. For the AI Birthcert specifically: this issues the BASIC one \u2014 self-declared by the agent, agent-paid via x402, owner identity NOT identity-verified. If the user asked for a \"verified\" AI birthcert (owner identity confirmed via MyDigital ID), use request_ai_birthcert_verification instead \u2014 this tool cannot produce that credential. If the user did not say which kind, ask first: see the `choice` note in credential_preflight. Without a confirmed price a chargeable issuance stops at the price and signs, pays and issues nothing: you get { issued: false, needsConfirmation: true, quote, askUser, reason }. Put the askUser question to the user, and only after their explicit yes call again with confirm set to that quote object, unchanged (confirm: true is not accepted). If the issuer's payment request then differs from it in token, amount, payee or gas model, nothing is paid and you get needsConfirmation again with the new price. A free issuance, and a credential already held, need no confirm. Results may carry nextStep (what to do next) and askUser (the question to put to the user).",
|
|
49881
50258
|
inputSchema: {
|
|
49882
50259
|
type: "object",
|
|
49883
50260
|
properties: {
|
|
@@ -49887,20 +50264,26 @@ function buildToolList() {
|
|
|
49887
50264
|
},
|
|
49888
50265
|
attributes: {
|
|
49889
50266
|
type: "object",
|
|
49890
|
-
description: `Claim values for the credential (schema varies by template \u2014 check what the issuer requires before guessing). ALWAYS ask the human owner directly for any name/identifier-type value (e.g. an agentUsername or similar field) \u2014 NEVER invent, guess, or silently reuse one of your own choosing; if the template requires one, get it from the user first. "agentDid" is the one exception: it does not need to be supplied, since it is auto-filled with this wallet's own holder DID (the credential's self-referential subject) unless you explicitly override it.`
|
|
50267
|
+
description: `Claim values for the credential (schema varies by template \u2014 check what the issuer requires before guessing). ALWAYS ask the human owner directly for any name/identifier-type value (e.g. an agentUsername or similar field) \u2014 NEVER invent, guess, or silently reuse one of your own choosing; if the template requires one, get it from the user first. When the user answers with a name \u2014 even a short phrase, even one that repeats words from your question \u2014 that IS the name: use it as given and do not ask for it again, unless it was refused (for example already in use). Tidying it (spaces or capitals, say) is optional and never needed; if you do want to suggest a tidier form, ask and WAIT for their answer, and use the tidied form only after a yes. Never go ahead on a name, or on a payment, that the user has not agreed to. Any example you give is only an example of the format, not a suggestion to use. "agentDid" is the one exception: it does not need to be supplied, since it is auto-filled with this wallet's own holder DID (the credential's self-referential subject) unless you explicitly override it.`
|
|
49891
50268
|
},
|
|
49892
50269
|
expirationDate: { type: "string" },
|
|
49893
50270
|
dryRun: {
|
|
49894
50271
|
type: "boolean",
|
|
49895
|
-
description: "Price the credential without paying, signing, or issuing anything. Returns { quote: { asset, maxAmountRequired, payTo, gasModel, paymentRequired? }, schema: { required, optional } }. Priced via MBI's /quote endpoint, which cannot issue \u2014 so this is safe even on a template that issues for free (such a template mints synchronously on the real path, which is why pricing never goes through it). Never writes the VC cache, so it cannot displace a credential you already hold. `quote.paymentRequired` is the authoritative free-vs-paid answer: false means issuance is currently free and `maxAmountRequired` will not be charged. When the field is ABSENT the MBI predates it and the amount is unconfirmed \u2014 report it as a possible charge, never as certainly free. Still validates required attributes locally first \u2014 a missing one blocks before any MBI call."
|
|
50272
|
+
description: "Price the credential without paying, signing, or issuing anything. Returns { quote: { asset, maxAmountRequired, payTo, gasModel, paymentRequired? }, schema: { required, optional } }. Priced via MBI's /quote endpoint, which cannot issue \u2014 so this is safe even on a template that issues for free (such a template mints synchronously on the real path, which is why pricing never goes through it). Never writes the VC cache, so it cannot displace a credential you already hold. `quote.paymentRequired` is the authoritative free-vs-paid answer: false means issuance is currently free and `maxAmountRequired` will not be charged. When the field is ABSENT the MBI predates it and the amount is unconfirmed \u2014 report it as a possible charge, never as certainly free. Tell the user in plain words that it is free or chargeable; never quote JSON field names such as paymentRequired to them. Still validates required attributes locally first \u2014 a missing one blocks before any MBI call."
|
|
49896
50273
|
},
|
|
49897
50274
|
forceReissue: {
|
|
49898
50275
|
type: "boolean",
|
|
49899
50276
|
description: "Alone, this does NOT pay again \u2014 if a valid credential is already cached it only returns that credential (issued: false, fromCache: true, reason) so it can be shown to the user first. Pair it with confirmReplaceExistingVc to actually pay and reissue over a still-valid one. Has no effect when nothing valid is cached \u2014 that case always pays and issues fresh regardless."
|
|
49900
50277
|
},
|
|
50278
|
+
confirm: {
|
|
50279
|
+
type: "object",
|
|
50280
|
+
properties: CONFIRM_QUOTE_PROPERTIES,
|
|
50281
|
+
required: ["asset", "maxAmountRequired", "gasModel"],
|
|
50282
|
+
description: "The user's explicit yes to the price: the quote object from a needsConfirmation result, passed back unchanged. Anything else, including true, stops at the price (needsConfirmation: true) and nothing is signed, paid or issued. Pass it only after the user has seen that price and said yes to it, never on your own judgement: a yes to something else, such as the name, is not a yes to the payment."
|
|
50283
|
+
},
|
|
49901
50284
|
confirmReplaceExistingVc: {
|
|
49902
50285
|
type: "string",
|
|
49903
|
-
description: "Only for replacing a cached credential that is STILL VALID (not expired). Pass the exact vcId from a prior forceReissue response \u2014 never a guess, never made up. Only set this after the user has SEEN that existing credential and explicitly asked to replace it; omit it otherwise, and never set it without forceReissue also being true. A mismatched value is ignored: nothing is paid and you get the same { issued: false, reason } response again, naming the id to confirm. Setting it without forceReissue does nothing at all either way \u2014 that case is a plain cache hit ({ issued: true }) regardless of what this field is set to."
|
|
50286
|
+
description: "Only for replacing a cached credential that is STILL VALID (not expired). Pass the exact vcId from a prior forceReissue response \u2014 never a guess, never made up. Only set this after the user has SEEN that existing credential and explicitly asked to replace it; omit it otherwise, and never set it without forceReissue also being true. A mismatched value is ignored: nothing is paid and you get the same { issued: false, reason } response again, naming the id to confirm. Setting it without forceReissue does nothing at all either way \u2014 that case is a plain cache hit ({ issued: false, alreadyHeld: true }) regardless of what this field is set to."
|
|
49904
50287
|
}
|
|
49905
50288
|
},
|
|
49906
50289
|
required: ["templateId", "attributes"]
|
|
@@ -49908,13 +50291,13 @@ function buildToolList() {
|
|
|
49908
50291
|
},
|
|
49909
50292
|
{
|
|
49910
50293
|
name: "request_ai_birthcert_verification",
|
|
49911
|
-
description: 'Start a Verified AI Birthcert issuance session with myid (MyDigital ID owner verification). ALWAYS run credential_preflight for "verified_ai_birthcert" immediately before calling this, even if you checked earlier in the conversation \u2014 preflight is free, this tool spends real funds, and a balance the user topped up a minute ago is not the balance you read before that. If a spending policy refuses the session fee, the result is { error } with `policyDenied: true` (a decision: do not retry) or `policyCheckUnavailable: true` (transient: trying again shortly is right); either way nothing was paid and no session was created. Returns { sessionId, verificationUrl, expiresAt, expiresIn, expiresInSeconds, message } \u2014 show verificationUrl to the human owner and ask them to open it and complete MyDigital ID verification (typically finishes in seconds). Tell them how long the link is good for by quoting `expiresIn` exactly as given: the wallet works it out from its own clock, so never calculate the time remaining yourself from `expiresAt` \u2014 your clock and timezone may differ from the server\'s. `message` here contains a "Tell the user:" sentence about following up \u2014 relay it rather than composing your own reassurance. Once they confirm they are done, call check_ai_birthcert_verification to see whether the credential was issued. Nothing keeps running after this call returns \u2014 there is no background polling, webhook, or timer, so you can only check status when the user sends you a new message. NEVER tell the user you have "set up an automation" or that you will "alert them" when it completes \u2014 you cannot act on your own between messages, and promising that leaves them waiting for a notification that will never come. Instead, tell them how long the link is good for and ask them to message you back once done (or after a few minutes) so you can check_ai_birthcert_verification again. ALWAYS ask the human owner directly what agentName they want to use for a NEW session \u2014 NEVER invent, guess, or silently choose one yourself. (Calling again with the SAME name to resume an already-pending session, as described below, is not "inventing" one \u2014 that is reusing the exact name the owner already gave.) IMPORTANT: agentName must be unique \u2014 if this exact name has already been used to request a Verified AI Birthcert, issuance will fail. Before calling, ask the human owner whether they want to supply any of the optional fields \u2014 agentPurpose, evidenceAssuranceLevel, ownerType, ownerVerified \u2014 do not silently omit them; they only need to say no. Calling this again with the SAME agentName while a prior session is still pending returns that same session unchanged \u2014 no new session is started and nothing is paid again. This tool spends real funds: it self-pays an x402 challenge, subject to the same credential-issuance payment cap as subscribe_and_issue, which by default allows
|
|
50294
|
+
description: 'Start a Verified AI Birthcert issuance session with myid (MyDigital ID owner verification). ALWAYS run credential_preflight for "verified_ai_birthcert" immediately before calling this, even if you checked earlier in the conversation \u2014 preflight is free, this tool spends real funds, and a balance the user topped up a minute ago is not the balance you read before that. If a spending policy refuses the session fee, the result is { error } with `policyDenied: true` (a decision: do not retry) or `policyCheckUnavailable: true` (transient: trying again shortly is right); either way nothing was paid and no session was created. Returns { sessionId, verificationUrl, expiresAt, expiresIn, expiresInSeconds, paidThisCall, message } \u2014 show verificationUrl to the human owner and ask them to open it and complete MyDigital ID verification (typically finishes in seconds). Tell them how long the link is good for by quoting `expiresIn` exactly as given: the wallet works it out from its own clock, so never calculate the time remaining yourself from `expiresAt` \u2014 your clock and timezone may differ from the server\'s. `message` here contains a "Tell the user:" sentence about following up \u2014 relay it rather than composing your own reassurance. Once they confirm they are done, call check_ai_birthcert_verification to see whether the credential was issued. Nothing keeps running after this call returns \u2014 there is no background polling, webhook, or timer, so you can only check status when the user sends you a new message. NEVER tell the user you have "set up an automation" or that you will "alert them" when it completes \u2014 you cannot act on your own between messages, and promising that leaves them waiting for a notification that will never come. Instead, tell them how long the link is good for and ask them to message you back once done (or after a few minutes) so you can check_ai_birthcert_verification again. ALWAYS ask the human owner directly what agentName they want to use for a NEW session \u2014 NEVER invent, guess, or silently choose one yourself. (Calling again with the SAME name to resume an already-pending session, as described below, is not "inventing" one \u2014 that is reusing the exact name the owner already gave.) IMPORTANT: agentName must be unique \u2014 if this exact name has already been used to request a Verified AI Birthcert, issuance will fail. Before calling, ask the human owner whether they want to supply any of the optional fields \u2014 agentPurpose, evidenceAssuranceLevel, ownerType, ownerVerified \u2014 do not silently omit them; they only need to say no. Calling this again with the SAME agentName while a prior session is still pending returns that same session unchanged \u2014 no new session is started and nothing is paid again. A new link after an expired one does not pay again either: it reuses the payment already made, which is only used up once the credential is issued. Every session result says so in `paidThisCall` (true only when THIS call sent a payment), and `message` opens with "No new payment" when it did not. Count a fee only when paidThisCall is true: never tell the user they paid once per link. This tool spends real funds: it self-pays an x402 challenge, subject to the same credential-issuance payment cap as subscribe_and_issue, which by default allows at most 1 JMYR for one payment. Set MAX_PAYMENT_AMOUNT to override it; a spending policy that governs the asset replaces the default cap. It can return { error: "..." } instead of a session. Read `message` before deciding what to tell the user or whether retrying is safe \u2014 it does NOT always mean nothing was paid. For insufficient funds or a payment-cap block specifically, nothing is created and nothing was paid, so once the underlying problem is fixed (e.g. the user tops up), calling this tool again is the right next step. Several OTHER { error } shapes mean the opposite: a payment may already have been sent on a prior or even this call (blob_already_settled, a receipt that could not be saved locally, RECEIPT VOID, OUTCOME UNKNOWN) \u2014 for any of those, calling this tool again pays the fee AGAIN, so do NOT retry without explicit agreement from the user, exactly as `message` itself will say. It can also return { settlementPending: true, paymentReceipt, message } instead \u2014 a payment WAS SENT and no session exists yet. That is NOT a failure and NOT an error, but do NOT pay again and do NOT call this tool again to retry it \u2014 call check_ai_birthcert_verification to follow it through instead. It covers three different situations, told apart by the `issuerRejected` and `paymentInvalid` fields. With neither set, do NOT assume the payment is known to have succeeded \u2014 this shape covers TWO different states that look identical from these fields alone: usually the settlement is genuinely still clearing (the message says "still being processed"), but sometimes the outcome could not be determined at all yet (the message says "has not been confirmed yet" and does not say "still being processed") \u2014 an indeterminate state, not a confirmed one, even though neither flag is set. Either way the receipt is saved and you must not pay again, so report it as "payment sent, still settling" without promising the user it definitely succeeded. When the message contains a "Tell the user:" sentence, relay that sentence rather than composing your own reassurance, and if asked what the service said, quote only what the message says it answered. With issuerRejected: true the credential service was reached and refused the request \u2014 the settlement is NOT what failed, but do NOT describe this as the payment having succeeded either: `message` quotes its reason, and you must relay that reason rather than describing it as still settling or as a success. With paymentInvalid: true the service has specifically ruled the payment or receipt invalid \u2014 do NOT say "the settlement is not what failed" for this one, since this verdict IS about the payment; relay what it said instead, and do NOT describe it as a success either. Neither this case nor issuerRejected tells you whether the fee was taken, in either direction \u2014 never tell the user they were not charged, and just as much, never tell them they WERE charged either. That no-claim rule covers issuerRejected and paymentInvalid specifically. The plain no-flag case above is different again, and NOT simply "known to have succeeded" either \u2014 see the note on it above: it is two states, only one of which is confirmed, so read `message` there too rather than assuming success from the shape alone. If the request does not say which kind of AI Birthcert the user wants, do not start this: run credential_preflight for both kinds and ask which they prefer (see its `choice` note), and use this tool only for the Verified kind; if they want the self-declared, non-verified Basic one, use subscribe_and_issue for that. Without a confirmed price a call that would pay stops at the price and pays nothing: you get { needsConfirmation: true, paidThisCall: false, quote, askUser, message }. Put the askUser question to the user, and only after their explicit yes call again with confirm set to that quote object, unchanged (confirm: true is not accepted). If the payment request then differs from it in token, amount, payee or gas model, nothing is paid and you get needsConfirmation again with the new price; the quote\'s gasModel decides which option is paid, so if the sponsored option is refused you are given the self-pay price to ask about, never a silent switch. A call that pays nothing (the same open link, or a new link after an expired one) needs no confirm. Results may carry nextStep (what to do next) and askUser (the question to put to the user). BEFORE this pays or starts a session, it checks whether this holder already has a Verified AI Birthcert \u2014 found in the local cache, or (if the cache has not seen it yet) resolved from a previously-issued session. If one exists and is STILL VALID, nothing is paid and no session is started \u2014 you get back { existingVerifiedVc: { vcId, validUntil }, message } instead. Show that existing credential to the user and ask whether they actually want to replace it; only call this tool again, with confirmReplaceExistingVc set to exactly existingVerifiedVc.vcId, if they explicitly say yes. Never pass confirmReplaceExistingVc on your own judgement. If the existing VC has already EXPIRED, it needs no confirmReplaceExistingVc, but the payment still stops at the price like any other, and a result that went ahead carries replacedExpiredVc: { vcId, validUntil } naming the one it replaced. That field can appear on a settled session, on a payment still settling, or on an EARLIER call\'s still-pending session simply being returned unchanged \u2014 so treat it only as "the holder\'s old VC had expired", never as proof that THIS call itself just spent money; read the rest of the result (an error, a settled session, or settlementPending) to know what this call actually did. It can also return { error } specifically because an already-issued VC exists but could not be confirmed as valid or expired (a resolution problem, not a payment problem) \u2014 nothing is paid on that path either; relay `error` as given, since it names the concrete next step (retry check_ai_birthcert_verification, or as a last resort clear_stuck_payment_receipt with the user\'s explicit agreement).',
|
|
49912
50295
|
inputSchema: {
|
|
49913
50296
|
type: "object",
|
|
49914
50297
|
properties: {
|
|
49915
50298
|
agentName: {
|
|
49916
50299
|
type: "string",
|
|
49917
|
-
description: "A unique, human-readable name for this agent. Must not already be in use for a Verified AI Birthcert, or issuance will fail. ALWAYS ask the human owner directly which name to use for a new session \u2014 NEVER invent, guess, or silently choose one yourself."
|
|
50300
|
+
description: "A unique, human-readable name for this agent. Must not already be in use for a Verified AI Birthcert, or issuance will fail. ALWAYS ask the human owner directly which name to use for a new session \u2014 NEVER invent, guess, or silently choose one yourself. When the user answers with a name \u2014 even a short phrase, even one that repeats words from your question \u2014 that IS the name: use it as given and do not ask for it again, unless it was refused (for example already in use). Tidying it (spaces or capitals, say) is optional and never needed; if you do want to suggest a tidier form, ask and WAIT for their answer, and use the tidied form only after a yes. Never go ahead on a name, or on a payment, that the user has not agreed to. Any example you give is only an example of the format, not a suggestion to use."
|
|
49918
50301
|
},
|
|
49919
50302
|
agentPurpose: { type: "string", description: 'Optional \u2014 what this agent does, e.g. "Negotiate and settle supplier invoices".' },
|
|
49920
50303
|
evidenceAssuranceLevel: { type: "string", description: 'Optional \u2014 assurance level of the identity evidence, e.g. "high".' },
|
|
@@ -49933,6 +50316,12 @@ function buildToolList() {
|
|
|
49933
50316
|
type: "string",
|
|
49934
50317
|
description: 'DESTRUCTIVE, and it SPENDS. Only for a payment that is genuinely stuck (outcome still unresolved). While the wallet holds a stuck receipt this tool can only replay it \u2014 it will never buy a new credential \u2014 so this is the way to start over: it throws that payment away and pays a SECOND fee. Not needed for a VOID receipt (settlement ruled expired/failed): the wallet discards that one itself, with no flag and no confirmation, so the next call is already an ordinary fresh purchase. Pass the stuck receipt id EXACTLY as check_ai_birthcert_verification or clear_stuck_payment_receipt reported it \u2014 never a guess, never true. A mismatched id discards nothing and pays nothing. Before using it, show the user the receipt id, tell them the first payment is forfeit and that this costs the fee again, and get their explicit agreement. If the receipt turns out to belong to a live session this is refused: that session is already paid for, so call check_ai_birthcert_verification instead. Normally the result carries the id you confirmed as discardedPaymentReceipt \u2014 keep it, support needs it to trace the lost payment. But if this call ALSO threw away a void receipt of its own (the fresh payment it just made was then ruled void), discardedPaymentReceipt holds THAT receipt instead, and the id the user confirmed is named at the end of the error text ("ALSO DISCARDED earlier on this same call") \u2014 so on that path quote BOTH ids to support, not just the field. A receipt only counts as stuck once it is older than SETTLEMENT_STUCK_AFTER_MS (24h by default, e.g. SETTLEMENT_STUCK_AFTER_MS=3600000 for one hour); before that the wallet REFUSES this parameter outright \u2014 nothing is discarded and nothing is paid \u2014 so do not offer the user this option for a receipt that is not stuck yet. A bare "retry" or "yes" from the user is not agreement to pay again.'
|
|
49935
50318
|
},
|
|
50319
|
+
confirm: {
|
|
50320
|
+
type: "object",
|
|
50321
|
+
properties: CONFIRM_QUOTE_PROPERTIES,
|
|
50322
|
+
required: ["asset", "maxAmountRequired", "gasModel"],
|
|
50323
|
+
description: "The user's explicit yes to the price: the quote object from a needsConfirmation result, passed back unchanged. Anything else, including true, stops at the price (needsConfirmation: true) and nothing is paid. Pass it only after the user has seen that price and said yes to it, never on your own judgement: a yes to something else, such as the name, is not a yes to the payment."
|
|
50324
|
+
},
|
|
49936
50325
|
confirmReplaceExistingVc: {
|
|
49937
50326
|
type: "string",
|
|
49938
50327
|
description: "Only for replacing a Verified AI Birthcert VC that is STILL VALID (not expired). Pass the exact vcId from a prior existingVerifiedVc response \u2014 never a guess, never made up. Only set this after the user has SEEN that existing credential and explicitly asked to replace it; omit it otherwise. A mismatched or unconfirmed value is ignored: nothing is paid and no session is started, and you get the same existingVerifiedVc block again. Not needed at all when the existing VC has already expired \u2014 that case replaces itself automatically."
|
|
@@ -49943,13 +50332,13 @@ function buildToolList() {
|
|
|
49943
50332
|
},
|
|
49944
50333
|
{
|
|
49945
50334
|
name: "credential_preflight",
|
|
49946
|
-
description: "FREE readiness check \u2014 call this FIRST, before collecting ANY application detail from the user, whenever they ask for a credential. Spends nothing, signs no transaction, creates no session. Returns { ready, fee, balances, cap, schema?, alreadyHeld?, blockers, notChecked }: the live fee and which side pays gas, the balances that matter, whether the spending limit permits it, and \u2014 for a template credential \u2014 the attributes it requires. `blockers` lists EVERY reason it is not ready at once (a low balance and a too-low spending limit are different problems and both appear together), so one round of fixes is enough rather than discovering them one failed payment at a time. ALWAYS relay `notChecked` too: a clean result is not a guarantee. In particular it does NOT check whether an agent name is free \u2014 myid decides that at issuance, after payment. Report `fee.display` and each balance's `display` to the user, never the raw base-unit numbers. `fee.paymentRequired: false` means issuance is currently FREE \u2014 `fee.display` is then only what it WOULD cost if payment were switched back on, so do not ask the user to fund it and do not present that amount as a charge. A free credential sends no transaction, so no gas is spent and a missing ZTX balance is not a blocker on it: never ask the user to fund ZTX for a free credential, and if notChecked mentions ZTX, pass it on as a note only. When the field is ABSENT the cost is unknown (an older MBI), and it is treated as chargeable \u2014 never report absent as free. If the result has `alreadyHeld`, this wallet already holds a valid copy of that credential: tell the user so, name it, and stop \u2014 do not ask for an agent name or any other detail, and do not begin an issuance. A new one would replace it. Only when the user has explicitly said they want a replacement, run this check with replacing: true.",
|
|
50335
|
+
description: "FREE readiness check \u2014 call this FIRST, before collecting ANY application detail from the user, whenever they ask for a credential. Spends nothing, signs no transaction, creates no session. Returns { ready, fee, balances, cap, schema?, alreadyHeld?, unusedPayment?, blockers, notChecked }: the live fee and which side pays gas, the balances that matter, whether the spending limit permits it, and \u2014 for a template credential \u2014 the attributes it requires. `blockers` lists EVERY reason it is not ready at once (a low balance and a too-low spending limit are different problems and both appear together), so one round of fixes is enough rather than discovering them one failed payment at a time. ALWAYS relay `notChecked` too: a clean result is not a guarantee. In particular it does NOT check whether an agent name is free \u2014 myid decides that at issuance, after payment. Report `fee.display` and each balance's `display` to the user, never the raw base-unit numbers. If a balance's `decimals` is null, its `display` is still in raw base units, so say the amount could not be converted to whole tokens rather than quoting it. `fee.paymentRequired: false` means issuance is currently FREE \u2014 `fee.display` is then only what it WOULD cost if payment were switched back on, so do not ask the user to fund it and do not present that amount as a charge. Tell the user in plain words that it is free or chargeable; never quote JSON field names such as paymentRequired to them. A free credential sends no transaction, so no gas is spent and a missing ZTX balance is not a blocker on it: never ask the user to fund ZTX for a free credential, and if notChecked mentions ZTX, pass it on as a note only. When the field is ABSENT the cost is unknown (an older MBI), and it is treated as chargeable \u2014 never report absent as free. If the result has `alreadyHeld`, this wallet already holds a valid copy of that credential: tell the user so, name it, and stop \u2014 do not ask for an agent name or any other detail, and do not begin an issuance. A new one would replace it. That stop applies to that credential only: the `choice` note still applies to the other kind. Only when the user has explicitly said they want a replacement, run this check with replacing: true. If a Verified result has `unusedPayment`, an earlier payment has not been used yet and the next link reuses it: no new payment is needed, so do not ask the user to agree to a fee for it, and follow its `note`. When a user asks for an AI Birthcert and the request does not say which kind they want (Basic, which is self-declared, or Verified, through MyDigital ID), do not pick one: follow the `choice` note in the result, which is to run this check for both kinds, tell the user the difference in plain words, and ask which they prefer. The result also says what to do next in nextStep: confirm_kind_then_collect_details (an AI Birthcert: ask Basic or Verified unless the user already said, then collect the details), collect_details, resolve_blockers or tell_user_already_held.",
|
|
49947
50336
|
inputSchema: {
|
|
49948
50337
|
type: "object",
|
|
49949
50338
|
properties: {
|
|
49950
50339
|
credential: {
|
|
49951
50340
|
type: "string",
|
|
49952
|
-
description: 'Which credential to price: "verified_ai_birthcert" for the MyDigital-ID-verified AI Birthcert, or a template id (did:zid:...) / known template name
|
|
50341
|
+
description: 'Which credential to price: "verified_ai_birthcert" for the VERIFIED (MyDigital-ID-verified) AI Birthcert, or a template id (did:zid:...) / known template name for a template-issued one \u2014 "AI Birthcert" is the BASIC, self-declared one. For the Verified one use exactly "verified_ai_birthcert". A name that says verified (such as "Verified AI Birthcert") is also treated as the Verified one; any other name containing "birthcert" is priced as the Basic one.'
|
|
49953
50342
|
},
|
|
49954
50343
|
agentName: {
|
|
49955
50344
|
type: "string",
|
|
@@ -50077,8 +50466,9 @@ function buildPolicyWriteDeps(input) {
|
|
|
50077
50466
|
};
|
|
50078
50467
|
}
|
|
50079
50468
|
async function describeAssetAmount(asset, raw, query) {
|
|
50080
|
-
const { symbol, decimals } = await resolveAssetInfo(asset, query);
|
|
50469
|
+
const { symbol, decimals, decimalsReadable } = await resolveAssetInfo(asset, query);
|
|
50081
50470
|
const label = symbol || "(unknown asset)";
|
|
50471
|
+
if (!decimalsReadable) return `${raw} base units of ${label}`;
|
|
50082
50472
|
const human2 = formatHumanAmount(raw, decimals);
|
|
50083
50473
|
return human2 === raw ? `${raw} ${label}` : `${raw} (${human2} ${label})`;
|
|
50084
50474
|
}
|
|
@@ -50330,7 +50720,8 @@ async function main() {
|
|
|
50330
50720
|
return {
|
|
50331
50721
|
request: (input) => requestAiBirthcertVerification(verifyAiBirthcertDeps, input),
|
|
50332
50722
|
check: () => checkAiBirthcertVerification(verifyAiBirthcertDeps),
|
|
50333
|
-
clearStuckReceipt: (input) => clearStuckPaymentReceipt(verifyAiBirthcertDeps, input)
|
|
50723
|
+
clearStuckReceipt: (input) => clearStuckPaymentReceipt(verifyAiBirthcertDeps, input),
|
|
50724
|
+
findUnusedPayment: () => findUnusedVerificationPayment(verifyAiBirthcertDeps)
|
|
50334
50725
|
};
|
|
50335
50726
|
})() : void 0;
|
|
50336
50727
|
const payer = createPayer({
|
|
@@ -50394,6 +50785,7 @@ async function main() {
|
|
|
50394
50785
|
sign: subscribeSign,
|
|
50395
50786
|
pay: payForCredential,
|
|
50396
50787
|
resolveSymbol,
|
|
50788
|
+
formatAssetAmount,
|
|
50397
50789
|
holderDid,
|
|
50398
50790
|
resolveTemplateFields,
|
|
50399
50791
|
cache: vcCache,
|