agentic-wallet-mcp 0.9.1 → 0.10.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 +59 -0
- package/README.md +5 -4
- package/dist/server-bundle.cjs +496 -102
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,65 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
8
8
|
> Entries for 0.5.0 and earlier were reconstructed from commit history when this file was
|
|
9
9
|
> introduced in 0.6.0, so they summarise each release rather than being exhaustive.
|
|
10
10
|
|
|
11
|
+
## [0.10.0] — 14 September 2026
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **`credential_preflight` — a free readiness check to call before asking the user for anything.**
|
|
16
|
+
Reports the live fee and which side pays gas, the balances that matter, whether the spending
|
|
17
|
+
limit permits it, and (for a template credential) the attributes the template requires. Every
|
|
18
|
+
reason it is not ready is listed *together*, so one round of fixes is enough rather than
|
|
19
|
+
discovering a low balance and a too-low spending cap one failed payment at a time. It also
|
|
20
|
+
reports what it could **not** check, so a clean result is not mistaken for a guarantee — notably,
|
|
21
|
+
it cannot tell whether an agent name is still free, because that is decided at issuance.
|
|
22
|
+
- **Quote-only mode for the Verified AI Birthcert**, so its price and gas model can be seen without
|
|
23
|
+
starting a session or paying.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- **A dry run can no longer issue a credential.** `subscribe_and_issue({ dryRun: true })` now prices
|
|
28
|
+
through an endpoint that cannot issue, and stops before the call that can. Previously, against a
|
|
29
|
+
credential the issuer grants for free, asking what something cost *created it* — a real,
|
|
30
|
+
permanently-registered credential, which also displaced whatever the wallet already held for that
|
|
31
|
+
template. A dry run now signs nothing, issues nothing, and never touches the local credential
|
|
32
|
+
store.
|
|
33
|
+
- **A credential that is currently free is no longer reported as unaffordable.** The issuer states
|
|
34
|
+
separately whether a quoted amount will actually be charged; both `credential_preflight` and the
|
|
35
|
+
dry run now read it. When issuance is free, the amount is reported as indicative rather than as a
|
|
36
|
+
charge, and neither the balance nor the spending cap blocks it. Network gas is unaffected and can
|
|
37
|
+
still block, because gas is not the credential fee.
|
|
38
|
+
- **Where that answer is unknown it is reported as unknown, never as free** — an older issuer
|
|
39
|
+
deployment does not state it, and treating silence as "free" would under-report a real cost.
|
|
40
|
+
- **The default spending cap now permits the credential fee on mainnet**, scoped to credential
|
|
41
|
+
issuance rather than widened generally.
|
|
42
|
+
- **Balance reads during a preflight run concurrently**, so the fee balance and the native-gas
|
|
43
|
+
balance arrive together instead of one after the other.
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
|
|
47
|
+
- **The outbound `User-Agent` sent to payment facilitators no longer advertises a non-public
|
|
48
|
+
host.** It is transmitted to third parties on every prepare request; the underlying client
|
|
49
|
+
dependency has been updated to one that reports its public project URL.
|
|
50
|
+
|
|
51
|
+
## [0.9.2] — 3 September 2026
|
|
52
|
+
|
|
53
|
+
### Fixed
|
|
54
|
+
|
|
55
|
+
- **`request_ai_birthcert_verification`'s sponsored-payment path no longer surfaces a facilitator
|
|
56
|
+
insufficient-funds rejection as an opaque, unhandled MCP tool error.** A `461407`
|
|
57
|
+
(`X402_INSUFFICIENT_FUNDS`) rejection from the facilitator's `/prepare` endpoint is now reshaped
|
|
58
|
+
into a clean `{ error }` result naming the asset and amount, instead of falling through every
|
|
59
|
+
error branch and throwing raw.
|
|
60
|
+
- **That message now renders amounts in human units, not raw base units.** A raw base-unit count
|
|
61
|
+
next to a token symbol (e.g. "requires 1,000,000 of JMYR") reads as a million *whole* tokens —
|
|
62
|
+
for a 6-decimal asset the actual requirement was 1 JMYR, a 1,000,000x misreading that could lead
|
|
63
|
+
to a drastically oversized top-up. Amounts are now resolved through the same symbol/decimals
|
|
64
|
+
formatter the rest of the wallet already uses.
|
|
65
|
+
- **The reported "current balance" is read from the facilitator's structured response field when
|
|
66
|
+
available**, falling back to parsing it out of the free-text error message only for an
|
|
67
|
+
older/unfixed facilitator — the free-text format was never a stable contract between the two
|
|
68
|
+
services.
|
|
69
|
+
|
|
11
70
|
## [0.9.1] — 28 August 2026
|
|
12
71
|
|
|
13
72
|
### Fixed
|
package/README.md
CHANGED
|
@@ -20,15 +20,16 @@ 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
|
|
23
|
+
| `wallet_status` | Report holder DID/address/network + held VCs (client-supplied, or the local cache); optionally one token balance (`token`) or several in one call (`tokens`). Each balance carries `balance` (raw base units), `decimals` and `display` — the same amount in whole tokens with its symbol. Quote `display`; a raw count beside a ticker is wrong by orders of magnitude. A token may be named by ticker **or** contract address | `{ heldCredentials?, token?, tokens? }` | `{ holderDid, zetrixAddress, network, credentials, balances?, tokenBalance?, tokenBalances? }` |
|
|
24
|
+
| `credential_preflight` | **Free.** The first call for any credential — composes the quote, the balances and the spending cap into one answer, before a single application field is collected. Spends nothing, starts nothing. `blockers` lists *every* reason the wallet is not ready, at once; `notChecked` lists what preflight cannot know — agent-name availability is decided by myid at issuance, after payment, so a `ready` result is never a reservation | `{ credential, templateId? }` | `{ credential, ready, fee?, balances, cap?, schema?, blockers, notChecked }` |
|
|
24
25
|
| `prove_identity` | Answer an x401 `PROOF-REQUEST` → return the `PROOF-RESPONSE` header to replay | `{ proofRequest, vc?, revealAttribute?, issuerKeys? }` | `{ proofResponseHeader, verified, presentationId }` |
|
|
25
26
|
| `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 }` |
|
|
26
27
|
| `subscribe_and_issue` | Reuse a cached VC if still valid, else pay x402 → MBI issues → return the VC | `{ templateId, attributes, expirationDate?, dryRun?, forceReissue? }` | `{ issued, vcId, vc, txHash, fromCache?, schema?, originalPayment?, paymentAttempted?, recovery? }` |
|
|
27
28
|
| `create_holder_account` | Onboarding: mint an HSM account (the MCP already auto-creates one at startup if `ZETRIX_ADDRESS` is omitted — see Environment below). Always checks for an existing account first — if one is active for this session, returns `{ alreadyExists: true, existing }` without creating anything; pass `confirmNew: true` (after asking the user) to mint a new one anyway | `{ password, label?, purpose?, confirmNew? }` | `{ created, alreadyExists, existing?, zetrixAddress?, holderDid?, publicKeyHex?, message }` |
|
|
28
29
|
| `get_template_schema` | **Free** read of a VC template's declared attribute schema. Call before `subscribe_and_issue` to learn which attributes it requires | `{ templateId }` | `{ templateId, schema: { required, optional } }` or `{ templateId, error }` |
|
|
29
30
|
| `query_contract` | Read-only query against any Zetrix contract — call an arbitrary method and return its raw result. No signing, no state change | `{ contractAddress, method, params? }` | `{ ok: true, result }` or `{ ok: false, error }` |
|
|
30
|
-
| `request_ai_birthcert_verification` | Start a **Verified** AI Birthcert issuance session with myid (MyDigital ID owner verification) — distinct from `subscribe_and_issue`'s self-declared Basic AI Birthcert. Optional `gasPayer` (`"sponsored"` \| `"self"`) overrides the gas payer for this call only — precedence is per-call `gasPayer` > `GAS_PREFERENCE` config > `sponsored` hardcoded default. If a sponsored quote is refused before any money moved, the wallet automatically falls back to paying gas itself; it never falls back once payment is merely pending/indeterminate | `{ agentName, agentPurpose?, evidenceAssuranceLevel?, ownerType?, ownerVerified?, gasPayer? }` | `{ sessionId, verificationUrl, expiresAt }` |
|
|
31
|
-
| `check_ai_birthcert_verification` | Poll the most recently requested Verified AI Birthcert session; on `status: "issued"`, also fetches, verifies, and caches the credential | (none) | `{ status: "pending" \| "issued" \| "no_session", vcId?, vc?, cacheError? }` |
|
|
31
|
+
| `request_ai_birthcert_verification` | Start a **Verified** AI Birthcert issuance session with myid (MyDigital ID owner verification) — distinct from `subscribe_and_issue`'s self-declared Basic AI Birthcert. Optional `gasPayer` (`"sponsored"` \| `"self"`) overrides the gas payer for this call only — precedence is per-call `gasPayer` > `GAS_PREFERENCE` config > `sponsored` hardcoded default. If a sponsored quote is refused before any money moved, the wallet automatically falls back to paying gas itself; it never falls back once payment is merely pending/indeterminate. `dryRun: true` quotes instead of paying: it stops after the 402 challenge, pays nothing, creates no session, and returns `{ quote }` — the asset, the amount and which side pays gas. Quote and paid request send byte-identical signed bodies | `{ agentName, agentPurpose?, evidenceAssuranceLevel?, ownerType?, ownerVerified?, gasPayer?, dryRun? }` | `{ sessionId, verificationUrl, expiresAt }` or `{ quote: { asset, maxAmountRequired, payTo?, gasModel } }` |
|
|
32
|
+
| `check_ai_birthcert_verification` | **Free.** Poll the most recently requested Verified AI Birthcert session; on `status: "issued"`, also fetches, verifies, and caches the credential. While the session is still open it replays the stored `verificationUrl` alongside SSIVC's live `expiresAt` — SSIVC issues the link only once, at creation, so this store is the only place it survives. That makes *"where is my link?"* answerable without touching the paid tool. No link is returned once `status` is `"issued"`: the link is spent | (none) | `{ status: "pending" \| "issued" \| "no_session", verificationUrl?, expiresAt?, vcId?, vc?, cacheError? }` |
|
|
32
33
|
|
|
33
34
|
> **VCs are cached locally**, keyed by `templateId`, under `~/.agentic-wallet-mcp/vc-cache/`
|
|
34
35
|
> (scoped per network + holder — different identities or networks never share a cache).
|
|
@@ -176,7 +177,7 @@ Node ≥ 18 required (built-in `fetch`).
|
|
|
176
177
|
| `OID4VP_BASE_URL` | no | OID4VP verifier base URL override — auto-derived from `ZETRIX_NETWORK` by the x401 SDK when not set |
|
|
177
178
|
| `ZETRIX_NODE_HOST` / `ZETRIX_NODE_PORT` | no | RPC node override (auto-derived from network) |
|
|
178
179
|
| `ZID_RESOLVER_BASE_URL` | no | ZID resolver override (auto-derived from network: sandbox for testnet, prod for mainnet) |
|
|
179
|
-
| `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. **
|
|
180
|
+
| `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. |
|
|
180
181
|
| `ZETRIX_WALLET_STATE_DIR` | no | Where the wallet keeps `account.json` and its VC cache. Defaults to `~/.agentic-wallet-mcp` |
|
|
181
182
|
| `SSIVC_BASE_URL` | no | myid's SSIVC API base URL, for the Verified AI Birthcert flow (`request_ai_birthcert_verification`/`check_ai_birthcert_verification`). **Auto-derived per network** — testnet `https://ssivc-api-uat.myegdev.com/api`, mainnet `https://verifyid-api.zetrix.com/api`. Override only if either changes |
|
|
182
183
|
| `AI_BIRTHCERT_VERIFIED_TEMPLATE_ID` | no | The Verified AI Birthcert's on-chain `did:zid:...` template id. Auto-derived per network by default — **the mainnet default is unverified**, so override this explicitly once the mainnet template id is confirmed |
|
package/dist/server-bundle.cjs
CHANGED
|
@@ -12147,7 +12147,7 @@ var require_prepare_client = __commonJS({
|
|
|
12147
12147
|
method: "POST",
|
|
12148
12148
|
headers: {
|
|
12149
12149
|
"Content-Type": "application/json",
|
|
12150
|
-
"User-Agent": "x402-zetrix-js/1.0 (+https://
|
|
12150
|
+
"User-Agent": "x402-zetrix-js/1.0 (+https://github.com/Zetrix-Chain/x402-zetrix-js)"
|
|
12151
12151
|
},
|
|
12152
12152
|
body: JSON.stringify(params),
|
|
12153
12153
|
signal: controller.signal
|
|
@@ -12538,6 +12538,7 @@ var require_dist2 = __commonJS({
|
|
|
12538
12538
|
var index_exports = {};
|
|
12539
12539
|
__export(index_exports, {
|
|
12540
12540
|
asPayRequest: () => asPayRequest,
|
|
12541
|
+
buildPayers: () => buildPayers,
|
|
12541
12542
|
buildToolList: () => buildToolList
|
|
12542
12543
|
});
|
|
12543
12544
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -12549,7 +12550,7 @@ var import_node_path6 = require("node:path");
|
|
|
12549
12550
|
// package.json
|
|
12550
12551
|
var package_default = {
|
|
12551
12552
|
name: "agentic-wallet-mcp",
|
|
12552
|
-
version: "0.
|
|
12553
|
+
version: "0.10.0",
|
|
12553
12554
|
description: "Agent-facing MCP wallet for Zetrix \u2014 orchestrates x401 identity proof, x402 payment, and MBI VC issuance",
|
|
12554
12555
|
keywords: [
|
|
12555
12556
|
"mcp",
|
|
@@ -12592,7 +12593,7 @@ var package_default = {
|
|
|
12592
12593
|
dependencies: {
|
|
12593
12594
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
12594
12595
|
"x401-zetrix-client": "^0.2.1",
|
|
12595
|
-
"x402-zetrix-client": "^0.2.
|
|
12596
|
+
"x402-zetrix-client": "^0.2.4",
|
|
12596
12597
|
"zetrix-sdk-nodejs": "^1.0.3"
|
|
12597
12598
|
},
|
|
12598
12599
|
devDependencies: {
|
|
@@ -21511,10 +21512,15 @@ var PaymentCapError = class extends Error {
|
|
|
21511
21512
|
this.detail = detail;
|
|
21512
21513
|
}
|
|
21513
21514
|
};
|
|
21515
|
+
function formatCapRefusal(detail, renderRequired = detail.requiredRaw, renderCap = detail.capRaw) {
|
|
21516
|
+
const asset = detail.asset || "(unknown asset)";
|
|
21517
|
+
const base = `payment blocked: this call needs ${renderRequired} of asset "${asset}", but the limit that applies is ${renderCap}`;
|
|
21518
|
+
return detail.matchedKey === detail.asset ? `${base}, set for that asset.` : `${base}. No limit is set for this asset, so the "*" fallback applies \u2014 set a limit keyed by "${asset}" to allow it.`;
|
|
21519
|
+
}
|
|
21514
21520
|
function isNonNegativeIntegerString(v) {
|
|
21515
21521
|
return /^\d+$/.test(v);
|
|
21516
21522
|
}
|
|
21517
|
-
function parsePaymentCaps(json) {
|
|
21523
|
+
function parsePaymentCaps(json, opts = {}) {
|
|
21518
21524
|
if (json === void 0) return void 0;
|
|
21519
21525
|
let parsed;
|
|
21520
21526
|
try {
|
|
@@ -21532,12 +21538,43 @@ function parsePaymentCaps(json) {
|
|
|
21532
21538
|
}
|
|
21533
21539
|
caps[asset] = raw;
|
|
21534
21540
|
}
|
|
21535
|
-
return caps;
|
|
21541
|
+
return opts.resolveSymbol ? expandSymbolKeys(caps, opts.resolveSymbol, opts.onWarn) : caps;
|
|
21542
|
+
}
|
|
21543
|
+
function expandSymbolKeys(caps, resolveSymbol, onWarn) {
|
|
21544
|
+
const out = {};
|
|
21545
|
+
const pendingSymbols = [];
|
|
21546
|
+
for (const [key, value] of Object.entries(caps)) {
|
|
21547
|
+
const address = key === "*" || key === "ZTX" ? void 0 : resolveSymbol(key);
|
|
21548
|
+
if (address === void 0) out[key] = value;
|
|
21549
|
+
else pendingSymbols.push([key, address, value]);
|
|
21550
|
+
}
|
|
21551
|
+
for (const [symbol, address, value] of pendingSymbols) {
|
|
21552
|
+
if (out[address] !== void 0) continue;
|
|
21553
|
+
out[address] = value;
|
|
21554
|
+
const previous = caps["*"] ?? "0";
|
|
21555
|
+
if (BigInt(value) > BigInt(previous)) {
|
|
21556
|
+
onWarn?.(
|
|
21557
|
+
`MAX_PAYMENT_AMOUNT: the limit written as "${symbol}" now applies to ${address} and permits ${value}. Until this version a ticker matched nothing, so that asset was limited to ${previous}. If you did not intend to allow ${value}, change or remove the "${symbol}" entry.`
|
|
21558
|
+
);
|
|
21559
|
+
}
|
|
21560
|
+
}
|
|
21561
|
+
return out;
|
|
21562
|
+
}
|
|
21563
|
+
function describePaymentCap(asset, requiredRaw, caps) {
|
|
21564
|
+
if (caps === void 0) return { asset, capRaw: null, matchedKey: null, wouldPass: true };
|
|
21565
|
+
const { capRaw, matchedKey } = resolveCap(asset, caps);
|
|
21566
|
+
if (capRaw === void 0) return { asset, capRaw: null, matchedKey: null, wouldPass: false };
|
|
21567
|
+
const wouldPass = isNonNegativeIntegerString(requiredRaw) && BigInt(requiredRaw) <= BigInt(capRaw);
|
|
21568
|
+
return { asset, capRaw, matchedKey, wouldPass };
|
|
21569
|
+
}
|
|
21570
|
+
function resolveCap(asset, caps) {
|
|
21571
|
+
const explicit = caps[asset];
|
|
21572
|
+
return explicit !== void 0 ? { capRaw: explicit, matchedKey: asset } : { capRaw: caps["*"], matchedKey: "*" };
|
|
21536
21573
|
}
|
|
21537
21574
|
function assertWithinPaymentCap(accept, caps) {
|
|
21538
21575
|
if (caps === void 0) return;
|
|
21539
21576
|
const asset = accept.asset ?? "";
|
|
21540
|
-
const capRaw =
|
|
21577
|
+
const { capRaw, matchedKey } = resolveCap(asset, caps);
|
|
21541
21578
|
if (capRaw === void 0) {
|
|
21542
21579
|
throw new PaymentCapError(`payment blocked: no MAX_PAYMENT_AMOUNT entry for asset "${asset}" and no "*" fallback configured`);
|
|
21543
21580
|
}
|
|
@@ -21548,10 +21585,13 @@ function assertWithinPaymentCap(accept, caps) {
|
|
|
21548
21585
|
const required2 = BigInt(requiredRaw);
|
|
21549
21586
|
const cap = BigInt(capRaw);
|
|
21550
21587
|
if (required2 > cap) {
|
|
21551
|
-
|
|
21552
|
-
|
|
21553
|
-
|
|
21554
|
-
|
|
21588
|
+
const detail = {
|
|
21589
|
+
asset,
|
|
21590
|
+
requiredRaw: required2.toString(),
|
|
21591
|
+
capRaw: cap.toString(),
|
|
21592
|
+
matchedKey
|
|
21593
|
+
};
|
|
21594
|
+
throw new PaymentCapError(formatCapRefusal(detail), detail);
|
|
21555
21595
|
}
|
|
21556
21596
|
}
|
|
21557
21597
|
|
|
@@ -21583,10 +21623,22 @@ function deriveAiBirthcertVerifiedTemplateId(network) {
|
|
|
21583
21623
|
var TOKEN_REGISTRY = {
|
|
21584
21624
|
JMYR: { testnet: "ZTX3WeinXtt28YMyr4vUZ14ddTgEMGeuc1e6b", mainnet: "ZTX3NCkXBqbyJWjZZxciQez945Lu6tGAcjNJr" }
|
|
21585
21625
|
};
|
|
21626
|
+
function defaultPaymentCaps(network) {
|
|
21627
|
+
const jmyr = resolveTokenAddress("JMYR", network);
|
|
21628
|
+
return isTestnet(network) && jmyr ? { [jmyr]: "1000000", "*": "0" } : { "*": "0" };
|
|
21629
|
+
}
|
|
21630
|
+
function defaultCredentialIssuanceCaps(network) {
|
|
21631
|
+
const jmyr = resolveTokenAddress("JMYR", network);
|
|
21632
|
+
return jmyr ? { [jmyr]: "1000000", "*": "0" } : { "*": "0" };
|
|
21633
|
+
}
|
|
21634
|
+
var KNOWN_NETWORKS = ["zetrix:testnet", "zetrix:mainnet"];
|
|
21635
|
+
function isTestnet(network) {
|
|
21636
|
+
return network.includes("testnet");
|
|
21637
|
+
}
|
|
21586
21638
|
function resolveTokenAddress(symbol, network) {
|
|
21587
21639
|
const entry = TOKEN_REGISTRY[symbol.toUpperCase()];
|
|
21588
21640
|
if (!entry) return void 0;
|
|
21589
|
-
return network
|
|
21641
|
+
return isTestnet(network) ? entry.testnet : entry.mainnet;
|
|
21590
21642
|
}
|
|
21591
21643
|
function loadConfig(env) {
|
|
21592
21644
|
const req = (key, hint) => {
|
|
@@ -21599,7 +21651,17 @@ function loadConfig(env) {
|
|
|
21599
21651
|
return v && v.trim() ? v.trim() : void 0;
|
|
21600
21652
|
};
|
|
21601
21653
|
const network = opt("ZETRIX_NETWORK") ?? "zetrix:testnet";
|
|
21654
|
+
if (!KNOWN_NETWORKS.includes(network)) {
|
|
21655
|
+
throw new Error(
|
|
21656
|
+
`agentic-wallet-mcp: unrecognised ZETRIX_NETWORK "${network}" \u2014 expected one of ${KNOWN_NETWORKS.join(", ")}. Values are matched exactly and are case-sensitive; anything unrecognised would otherwise resolve to MAINNET addresses and grant a real spending allowance.`
|
|
21657
|
+
);
|
|
21658
|
+
}
|
|
21602
21659
|
const oid4vpBaseUrlOverride = opt("OID4VP_BASE_URL");
|
|
21660
|
+
const explicitCaps = parsePaymentCaps(opt("MAX_PAYMENT_AMOUNT"), {
|
|
21661
|
+
resolveSymbol: (symbol) => resolveTokenAddress(symbol, network),
|
|
21662
|
+
onWarn: (message) => process.stderr.write(`agentic-wallet-mcp: ${message}
|
|
21663
|
+
`)
|
|
21664
|
+
});
|
|
21603
21665
|
return {
|
|
21604
21666
|
walletBeUrl: stripTrailingSlash(opt("WALLET_BE_URL") ?? deriveWalletBeUrl(network)),
|
|
21605
21667
|
oid4vpBaseUrl: oid4vpBaseUrlOverride ? stripTrailingSlash(oid4vpBaseUrlOverride) : void 0,
|
|
@@ -21619,7 +21681,11 @@ function loadConfig(env) {
|
|
|
21619
21681
|
// Fail closed: an unset cap means "spend nothing", not "spend anything". A wallet that starts
|
|
21620
21682
|
// with no configuration at all must not be able to auto-pay a hostile x402 challenge. Raising
|
|
21621
21683
|
// it is a deliberate act.
|
|
21622
|
-
|
|
21684
|
+
//
|
|
21685
|
+
// An explicit MAX_PAYMENT_AMOUNT governs BOTH caps below — a user who sets a limit means it
|
|
21686
|
+
// everywhere. Only the defaults differ, and only because the two surfaces carry different risk.
|
|
21687
|
+
maxPaymentAmount: explicitCaps ?? defaultPaymentCaps(network),
|
|
21688
|
+
credentialIssuanceCaps: explicitCaps ?? defaultCredentialIssuanceCaps(network),
|
|
21623
21689
|
ssivcBaseUrl: (() => {
|
|
21624
21690
|
const v = opt("SSIVC_BASE_URL") ?? deriveSsivcBaseUrl(network);
|
|
21625
21691
|
return v ? stripTrailingSlash(v) : void 0;
|
|
@@ -21738,27 +21804,42 @@ async function fetchZTP20BalanceStrict(contractAddress, address, query) {
|
|
|
21738
21804
|
if (typeof balance !== "string") {
|
|
21739
21805
|
throw new Error("balanceOf returned no balance field");
|
|
21740
21806
|
}
|
|
21807
|
+
if (!/^\d+$/.test(balance)) {
|
|
21808
|
+
throw new Error(`balanceOf returned a non-numeric balance: ${JSON.stringify(balance)}`);
|
|
21809
|
+
}
|
|
21741
21810
|
return balance;
|
|
21742
21811
|
}
|
|
21812
|
+
function looksLikeContractAddress(token) {
|
|
21813
|
+
return /^ZTX[0-9A-Za-z]{30,}$/.test(token);
|
|
21814
|
+
}
|
|
21743
21815
|
async function queryTokenBalance(deps, token) {
|
|
21744
21816
|
const symbol = token.toUpperCase();
|
|
21745
21817
|
if (symbol === "ZTX") {
|
|
21746
21818
|
try {
|
|
21747
|
-
|
|
21819
|
+
const balance2 = await deps.fetchNativeBalance(deps.address);
|
|
21820
|
+
return { token: symbol, balance: balance2, decimals: ZTX_DECIMALS2, display: renderDisplay(balance2, ZTX_DECIMALS2, symbol) };
|
|
21748
21821
|
} catch {
|
|
21749
21822
|
return { token: symbol, error: "query_failed" };
|
|
21750
21823
|
}
|
|
21751
21824
|
}
|
|
21752
|
-
const
|
|
21825
|
+
const registered = deps.resolveTokenAddress(symbol);
|
|
21826
|
+
const byAddress = registered === null && looksLikeContractAddress(token);
|
|
21827
|
+
const contractAddress = registered ?? (byAddress ? token : null);
|
|
21753
21828
|
if (!contractAddress) return { token: symbol, error: "unknown_token" };
|
|
21829
|
+
const asked = byAddress ? token : symbol;
|
|
21754
21830
|
let balance;
|
|
21755
21831
|
try {
|
|
21756
21832
|
balance = await fetchZTP20BalanceStrict(contractAddress, deps.address, deps.query);
|
|
21757
21833
|
} catch {
|
|
21758
|
-
return { token:
|
|
21834
|
+
return { token: asked, error: "query_failed" };
|
|
21759
21835
|
}
|
|
21760
21836
|
const info = await fetchTokenInfo(contractAddress, deps.query);
|
|
21761
|
-
|
|
21837
|
+
const label = byAddress ? info?.symbol ?? asked : symbol;
|
|
21838
|
+
const decimals = info?.decimals ?? null;
|
|
21839
|
+
return { token: label, balance, decimals, display: renderDisplay(balance, decimals, label) };
|
|
21840
|
+
}
|
|
21841
|
+
function renderDisplay(raw, decimals, label) {
|
|
21842
|
+
return `${decimals === null ? raw : formatHumanAmount(raw, decimals)} ${label}`;
|
|
21762
21843
|
}
|
|
21763
21844
|
|
|
21764
21845
|
// src/clients/template-info-client.ts
|
|
@@ -22032,6 +22113,24 @@ var MbiClient = class _MbiClient {
|
|
|
22032
22113
|
if (!res.ok) throw await this.error(res, "apply (phase 2) failed");
|
|
22033
22114
|
return this.unwrap(res);
|
|
22034
22115
|
}
|
|
22116
|
+
/**
|
|
22117
|
+
* POST /v1/vc/pay/quote — the price for a template, with no issuance and no payment record.
|
|
22118
|
+
*
|
|
22119
|
+
* Unlike `applyChallenge`, this cannot mint anything: MBI resolves the template, builds the
|
|
22120
|
+
* canonical sign payload, and reads its configured price. Notably a FREE template does not
|
|
22121
|
+
* short-circuit into synchronous issuance here, which is what makes this safe to call purely to
|
|
22122
|
+
* find out a cost — `applyChallenge` is not (see its docstring).
|
|
22123
|
+
*
|
|
22124
|
+
* `data` is required to be non-empty by MBI's DTO but does not affect the price, which comes from
|
|
22125
|
+
* MBI's own asset config keyed by the resolved template. A caller pricing a credential before it
|
|
22126
|
+
* has collected any attribute therefore passes a throwaway value, and must not present it as
|
|
22127
|
+
* anything the user supplied.
|
|
22128
|
+
*/
|
|
22129
|
+
async quote(templateId, data) {
|
|
22130
|
+
const res = await this.fetch("POST", "/v1/vc/pay/quote", { templateId, data });
|
|
22131
|
+
if (!res.ok) throw await this.error(res, "quote failed");
|
|
22132
|
+
return this.unwrap(res);
|
|
22133
|
+
}
|
|
22035
22134
|
/** Idempotent recovery — GET /v1/vc/pay/status/{paymentId}. */
|
|
22036
22135
|
async getStatus(paymentId) {
|
|
22037
22136
|
const res = await this.fetch("GET", `/v1/vc/pay/status/${encodeURIComponent(paymentId)}`);
|
|
@@ -22169,6 +22268,43 @@ function payAndFetch(payer, req) {
|
|
|
22169
22268
|
return payer(req);
|
|
22170
22269
|
}
|
|
22171
22270
|
|
|
22271
|
+
// src/accept-selection.ts
|
|
22272
|
+
function extraOf(accept) {
|
|
22273
|
+
const extra = accept.extra;
|
|
22274
|
+
if (!extra || typeof extra !== "object" || Array.isArray(extra)) return {};
|
|
22275
|
+
return extra;
|
|
22276
|
+
}
|
|
22277
|
+
function assetOf(accept) {
|
|
22278
|
+
return String(accept.asset ?? "");
|
|
22279
|
+
}
|
|
22280
|
+
function declaresFacilitator(accept) {
|
|
22281
|
+
return extraOf(accept).gasModel === "facilitator";
|
|
22282
|
+
}
|
|
22283
|
+
function isSponsored(accept) {
|
|
22284
|
+
if (!declaresFacilitator(accept)) return false;
|
|
22285
|
+
const prepareEndpoint = extraOf(accept).prepareEndpoint;
|
|
22286
|
+
if (typeof prepareEndpoint !== "string" || prepareEndpoint === "") return false;
|
|
22287
|
+
const asset = assetOf(accept);
|
|
22288
|
+
return asset !== "" && asset !== "ZTX";
|
|
22289
|
+
}
|
|
22290
|
+
function isUnusable(accept) {
|
|
22291
|
+
return declaresFacilitator(accept) && !isSponsored(accept);
|
|
22292
|
+
}
|
|
22293
|
+
function orderAccepts(accepts, prefer) {
|
|
22294
|
+
const usable = accepts.filter((a) => !isUnusable(a));
|
|
22295
|
+
const sponsored = usable.filter(isSponsored);
|
|
22296
|
+
const selfPay = usable.filter((a) => !isSponsored(a));
|
|
22297
|
+
return prefer === "sponsored" ? [...sponsored, ...selfPay] : [...selfPay, ...sponsored];
|
|
22298
|
+
}
|
|
22299
|
+
function prepareBaseUrl(prepareEndpoint) {
|
|
22300
|
+
return prepareEndpoint.replace(/\/+$/, "").replace(/\/prepare$/, "");
|
|
22301
|
+
}
|
|
22302
|
+
function needsNativeGasCheck(accept) {
|
|
22303
|
+
const asset = String(accept.asset ?? "");
|
|
22304
|
+
if (asset === "" || asset === "ZTX") return false;
|
|
22305
|
+
return !isSponsored(accept);
|
|
22306
|
+
}
|
|
22307
|
+
|
|
22172
22308
|
// src/zetrix-hex.ts
|
|
22173
22309
|
function zetrixHexStringToBytes(s) {
|
|
22174
22310
|
const up = s.toUpperCase();
|
|
@@ -22359,6 +22495,45 @@ async function subscribeAndIssue(deps, opts) {
|
|
|
22359
22495
|
};
|
|
22360
22496
|
}
|
|
22361
22497
|
}
|
|
22498
|
+
if (opts.dryRun) {
|
|
22499
|
+
if (!deps.mbi.quote) {
|
|
22500
|
+
return {
|
|
22501
|
+
issued: false,
|
|
22502
|
+
reason: "dry run unavailable \u2014 this wallet has no MBI quote client, and pricing any other way risks issuing the credential",
|
|
22503
|
+
...schema ? { schema } : {}
|
|
22504
|
+
};
|
|
22505
|
+
}
|
|
22506
|
+
let quoted;
|
|
22507
|
+
try {
|
|
22508
|
+
quoted = await deps.mbi.quote(opts.templateId, attributes);
|
|
22509
|
+
} catch (err) {
|
|
22510
|
+
if (err instanceof MbiError) {
|
|
22511
|
+
return { issued: false, reason: err.message, httpStatus: err.httpStatus, ...schema ? { schema } : {} };
|
|
22512
|
+
}
|
|
22513
|
+
throw err;
|
|
22514
|
+
}
|
|
22515
|
+
const quotedAccept = quoted.accepts?.[0];
|
|
22516
|
+
if (!quotedAccept) {
|
|
22517
|
+
return { issued: false, reason: "MBI quoted no payment options", ...schema ? { schema } : {} };
|
|
22518
|
+
}
|
|
22519
|
+
const quotedRaw = String(quotedAccept.asset ?? "");
|
|
22520
|
+
const quotedAsset = deps.resolveSymbol ? await deps.resolveSymbol(quotedRaw) : quotedRaw;
|
|
22521
|
+
const reason = quoted.paymentRequired === false ? "dry run \u2014 issuance is free right now, no payment required" : quoted.paymentRequired === true ? "dry run \u2014 quoted only, no payment made" : "dry run \u2014 quoted only, no payment made. This MBI does not report whether issuance is free, so the quoted amount may not actually be charged.";
|
|
22522
|
+
return {
|
|
22523
|
+
issued: false,
|
|
22524
|
+
reason,
|
|
22525
|
+
quote: {
|
|
22526
|
+
asset: quotedAsset,
|
|
22527
|
+
maxAmountRequired: quotedAccept.maxAmountRequired,
|
|
22528
|
+
payTo: quotedAccept.payTo,
|
|
22529
|
+
// credential_preflight reads a missing gasModel as self-pay (preflight.ts), so a
|
|
22530
|
+
// sponsored template credential must say so explicitly rather than fall through as absent.
|
|
22531
|
+
gasModel: isSponsored(quotedAccept) ? "sponsored" : "self",
|
|
22532
|
+
...quoted.paymentRequired === void 0 ? {} : { paymentRequired: quoted.paymentRequired }
|
|
22533
|
+
},
|
|
22534
|
+
...schema ? { schema } : {}
|
|
22535
|
+
};
|
|
22536
|
+
}
|
|
22362
22537
|
const data = JSON.stringify([{ templateId: opts.templateId, metadata: attributes }]);
|
|
22363
22538
|
const blob = zetrixHexStringToBytes(data).toString("hex");
|
|
22364
22539
|
const { signBlob: signData, publicKey } = await deps.sign(blob);
|
|
@@ -22394,26 +22569,11 @@ async function subscribeAndIssue(deps, opts) {
|
|
|
22394
22569
|
txHash: issued2.txHash,
|
|
22395
22570
|
paidAsset: "none",
|
|
22396
22571
|
amountPaid: "0",
|
|
22397
|
-
...schema ? { schema } : {}
|
|
22398
|
-
...opts.dryRun ? { reason: "this template requires no payment \u2014 MBI issues synchronously at phase 1, so dryRun could not prevent this issuance" } : {}
|
|
22572
|
+
...schema ? { schema } : {}
|
|
22399
22573
|
};
|
|
22400
22574
|
}
|
|
22401
22575
|
const accept = challenge.accepts[0];
|
|
22402
22576
|
if (!accept) return { issued: false, reason: "MBI 402 returned no payment options", ...schema ? { schema } : {} };
|
|
22403
|
-
if (opts.dryRun) {
|
|
22404
|
-
const quotedRaw = String(accept.asset ?? "");
|
|
22405
|
-
const quotedAsset = deps.resolveSymbol ? await deps.resolveSymbol(quotedRaw) : quotedRaw;
|
|
22406
|
-
return {
|
|
22407
|
-
issued: false,
|
|
22408
|
-
reason: "dry run \u2014 quoted only, no payment made",
|
|
22409
|
-
quote: {
|
|
22410
|
-
asset: quotedAsset,
|
|
22411
|
-
maxAmountRequired: accept.maxAmountRequired,
|
|
22412
|
-
payTo: accept.payTo
|
|
22413
|
-
},
|
|
22414
|
-
...schema ? { schema } : {}
|
|
22415
|
-
};
|
|
22416
|
-
}
|
|
22417
22577
|
let xPayment;
|
|
22418
22578
|
try {
|
|
22419
22579
|
xPayment = await deps.pay(accept);
|
|
@@ -22874,6 +23034,102 @@ function validateTemplateAttributes(templateId, network, attributes) {
|
|
|
22874
23034
|
return errors;
|
|
22875
23035
|
}
|
|
22876
23036
|
|
|
23037
|
+
// src/orchestrator/preflight.ts
|
|
23038
|
+
var VERIFIED_AI_BIRTHCERT = "verified_ai_birthcert";
|
|
23039
|
+
var NATIVE = "ZTX";
|
|
23040
|
+
function placeholderAgentName() {
|
|
23041
|
+
return `preflight-quote-only-${Date.now()}`;
|
|
23042
|
+
}
|
|
23043
|
+
async function credentialPreflight(deps, input) {
|
|
23044
|
+
const blockers = [];
|
|
23045
|
+
const notChecked = [];
|
|
23046
|
+
const isVerified = input.credential === VERIFIED_AI_BIRTHCERT;
|
|
23047
|
+
const quoted = isVerified ? await deps.quoteVerified({ agentName: input.agentName?.trim() || placeholderAgentName(), dryRun: true }) : await deps.quoteTemplate(input.credential);
|
|
23048
|
+
const quote = quoted.quote;
|
|
23049
|
+
const schema = quoted.schema;
|
|
23050
|
+
if (!quote) {
|
|
23051
|
+
const why = String(quoted.error ?? quoted.reason ?? "the price could not be read");
|
|
23052
|
+
return {
|
|
23053
|
+
credential: input.credential,
|
|
23054
|
+
ready: false,
|
|
23055
|
+
balances: [],
|
|
23056
|
+
...schema ? { schema } : {},
|
|
23057
|
+
blockers: [`Could not price this credential: ${why}`],
|
|
23058
|
+
notChecked: uncheckable(isVerified)
|
|
23059
|
+
};
|
|
23060
|
+
}
|
|
23061
|
+
const asset = String(quote.asset ?? "");
|
|
23062
|
+
const requiredRaw = String(quote.maxAmountRequired ?? "0");
|
|
23063
|
+
const gasModel = quote.gasModel === "sponsored" || quote.gasModel === "self" ? quote.gasModel : void 0;
|
|
23064
|
+
const selfPaysGas = gasModel !== "sponsored";
|
|
23065
|
+
const wanted = asset === NATIVE || !selfPaysGas ? [asset] : [asset, NATIVE];
|
|
23066
|
+
const balances = await Promise.all(wanted.map((token) => deps.queryTokenBalance(token)));
|
|
23067
|
+
const isFree = quote.paymentRequired === false;
|
|
23068
|
+
const feeBalance = balances[0];
|
|
23069
|
+
if ("error" in feeBalance) {
|
|
23070
|
+
blockers.push(`Could not read the ${asset} balance (${feeBalance.error}) \u2014 retry before paying.`);
|
|
23071
|
+
} else if (!isFree && BigInt(feeBalance.balance) < BigInt(requiredRaw)) {
|
|
23072
|
+
blockers.push(`Not enough ${feeBalance.token}: the fee is ${renderAmount(requiredRaw, feeBalance)}, the balance is ${feeBalance.display}.`);
|
|
23073
|
+
}
|
|
23074
|
+
if (selfPaysGas && asset !== NATIVE) {
|
|
23075
|
+
const gas = balances[1];
|
|
23076
|
+
if (gas && "error" in gas) {
|
|
23077
|
+
blockers.push(`Could not read the ZTX balance (${gas.error}) \u2014 ZTX pays network gas and is separate from the fee.`);
|
|
23078
|
+
} else if (gas && BigInt(gas.balance) === 0n) {
|
|
23079
|
+
blockers.push(`No ZTX for network gas. ZTX is separate from the ${feeBalance.token ?? asset} fee and is needed for every transaction.`);
|
|
23080
|
+
}
|
|
23081
|
+
}
|
|
23082
|
+
const cap = describePaymentCap(asset, requiredRaw, deps.caps);
|
|
23083
|
+
if (!cap.wouldPass && !isFree) blockers.push(renderCapBlocker(cap, requiredRaw));
|
|
23084
|
+
return {
|
|
23085
|
+
credential: input.credential,
|
|
23086
|
+
ready: blockers.length === 0,
|
|
23087
|
+
fee: {
|
|
23088
|
+
asset,
|
|
23089
|
+
maxAmountRequired: requiredRaw,
|
|
23090
|
+
...quote.payTo ? { payTo: String(quote.payTo) } : {},
|
|
23091
|
+
...gasModel ? { gasModel } : {},
|
|
23092
|
+
..."error" in feeBalance ? {} : { display: renderAmount(requiredRaw, feeBalance) },
|
|
23093
|
+
...quote.paymentRequired === void 0 ? {} : { paymentRequired: quote.paymentRequired }
|
|
23094
|
+
},
|
|
23095
|
+
balances,
|
|
23096
|
+
cap,
|
|
23097
|
+
...schema ? { schema } : {},
|
|
23098
|
+
blockers,
|
|
23099
|
+
notChecked: uncheckable(isVerified, isFree)
|
|
23100
|
+
};
|
|
23101
|
+
}
|
|
23102
|
+
function renderAmount(raw, balance) {
|
|
23103
|
+
if ("error" in balance || balance.decimals === null) return raw;
|
|
23104
|
+
const d = BigInt(10) ** BigInt(balance.decimals);
|
|
23105
|
+
const whole = BigInt(raw) / d;
|
|
23106
|
+
const frac = (BigInt(raw) % d).toString().padStart(balance.decimals, "0").replace(/0+$/, "");
|
|
23107
|
+
return `${whole}${frac ? `.${frac}` : ""} ${balance.token}`;
|
|
23108
|
+
}
|
|
23109
|
+
function renderCapBlocker(cap, requiredRaw) {
|
|
23110
|
+
if (cap.capRaw === null) {
|
|
23111
|
+
return `No spending limit applies to ${cap.asset}, and limits are configured \u2014 so this payment would be refused. Set a limit keyed by "${cap.asset}".`;
|
|
23112
|
+
}
|
|
23113
|
+
const misKeyed = cap.matchedKey !== cap.asset;
|
|
23114
|
+
return misKeyed ? `The spending limit that applies is ${cap.capRaw} (from the "*" fallback \u2014 no limit is set for ${cap.asset}), and this needs ${requiredRaw}.` : `The spending limit for ${cap.asset} is ${cap.capRaw}, and this needs ${requiredRaw}.`;
|
|
23115
|
+
}
|
|
23116
|
+
function uncheckable(isVerified, isFree = false) {
|
|
23117
|
+
const items = [
|
|
23118
|
+
"Whether the wallet address is activated on chain \u2014 a never-funded address fails differently from a low balance."
|
|
23119
|
+
];
|
|
23120
|
+
if (isVerified) {
|
|
23121
|
+
items.unshift(
|
|
23122
|
+
"Whether the agent name is still free. myid checks uniqueness at issuance, not now, so a name in use still prices normally and is only refused after payment."
|
|
23123
|
+
);
|
|
23124
|
+
}
|
|
23125
|
+
if (isFree) {
|
|
23126
|
+
items.unshift(
|
|
23127
|
+
"Whether issuance is still free at apply time \u2014 `paymentRequired` is a service-wide MBI setting and can change between this quote and issuance."
|
|
23128
|
+
);
|
|
23129
|
+
}
|
|
23130
|
+
return items;
|
|
23131
|
+
}
|
|
23132
|
+
|
|
22877
23133
|
// src/mcp-tools.ts
|
|
22878
23134
|
async function loadValidCachedCredentials(cache) {
|
|
22879
23135
|
if (!cache) return [];
|
|
@@ -22882,10 +23138,15 @@ async function loadValidCachedCredentials(cache) {
|
|
|
22882
23138
|
}
|
|
22883
23139
|
var AI_BIRTHCERT_NOT_CONFIGURED_ERROR = "AI Birthcert verification is not configured on this wallet. On mainnet this is expected until SSIVC_BASE_URL is set explicitly (the mainnet host was never confirmed reachable \u2014 APP-M04); on testnet it means verifyAiBirthcert was not wired at all.";
|
|
22884
23140
|
function createTools(deps) {
|
|
22885
|
-
|
|
23141
|
+
const tools = {
|
|
22886
23142
|
async wallet_status(input = {}) {
|
|
22887
23143
|
const balances = deps.getBalances ? await deps.getBalances() : void 0;
|
|
22888
23144
|
const tokenBalance = input.token && deps.queryTokenBalance ? await deps.queryTokenBalance(input.token) : void 0;
|
|
23145
|
+
let tokenBalances;
|
|
23146
|
+
if (input.tokens?.length && deps.queryTokenBalance) {
|
|
23147
|
+
tokenBalances = [];
|
|
23148
|
+
for (const t of input.tokens) tokenBalances.push(await deps.queryTokenBalance(t));
|
|
23149
|
+
}
|
|
22889
23150
|
const credentials = input.heldCredentials ?? (await loadValidCachedCredentials(deps.cache)).map((entry) => entry.vc);
|
|
22890
23151
|
return {
|
|
22891
23152
|
holderDid: deps.config.holderDid,
|
|
@@ -22893,7 +23154,8 @@ function createTools(deps) {
|
|
|
22893
23154
|
network: deps.config.network,
|
|
22894
23155
|
credentials,
|
|
22895
23156
|
...balances !== void 0 ? { balances } : {},
|
|
22896
|
-
...tokenBalance !== void 0 ? { tokenBalance } : {}
|
|
23157
|
+
...tokenBalance !== void 0 ? { tokenBalance } : {},
|
|
23158
|
+
...tokenBalances !== void 0 ? { tokenBalances } : {}
|
|
22897
23159
|
};
|
|
22898
23160
|
},
|
|
22899
23161
|
async prove_identity(input) {
|
|
@@ -22990,6 +23252,38 @@ function createTools(deps) {
|
|
|
22990
23252
|
}
|
|
22991
23253
|
return deps.verifyAiBirthcert.request(input);
|
|
22992
23254
|
},
|
|
23255
|
+
/**
|
|
23256
|
+
* One read-only answer to "can this wallet buy this, and what does it cost?", before any
|
|
23257
|
+
* application field is collected. Composes the free pieces that already exist — a phase-1 quote,
|
|
23258
|
+
* balances, the spending cap — into a single verdict, so the balance and the cap stop being two
|
|
23259
|
+
* sequential dead ends.
|
|
23260
|
+
*/
|
|
23261
|
+
async credential_preflight(input) {
|
|
23262
|
+
if (input.credential === VERIFIED_AI_BIRTHCERT && !deps.verifyAiBirthcert) {
|
|
23263
|
+
return { credential: input.credential, ready: false, balances: [], blockers: [AI_BIRTHCERT_NOT_CONFIGURED_ERROR], notChecked: [] };
|
|
23264
|
+
}
|
|
23265
|
+
if (!deps.queryTokenBalance) {
|
|
23266
|
+
return {
|
|
23267
|
+
credential: input.credential,
|
|
23268
|
+
ready: false,
|
|
23269
|
+
balances: [],
|
|
23270
|
+
blockers: ["This wallet cannot read balances, so readiness cannot be confirmed."],
|
|
23271
|
+
notChecked: []
|
|
23272
|
+
};
|
|
23273
|
+
}
|
|
23274
|
+
return credentialPreflight(
|
|
23275
|
+
{
|
|
23276
|
+
quoteVerified: (q) => deps.verifyAiBirthcert.request(q),
|
|
23277
|
+
quoteTemplate: async (templateId) => {
|
|
23278
|
+
const resolved = resolveTemplateAlias(templateId, deps.config.network) ?? templateId;
|
|
23279
|
+
return priceTemplate(deps, tools, resolved);
|
|
23280
|
+
},
|
|
23281
|
+
queryTokenBalance: deps.queryTokenBalance,
|
|
23282
|
+
caps: deps.paymentCaps
|
|
23283
|
+
},
|
|
23284
|
+
input
|
|
23285
|
+
);
|
|
23286
|
+
},
|
|
22993
23287
|
check_ai_birthcert_verification() {
|
|
22994
23288
|
if (!deps.verifyAiBirthcert) {
|
|
22995
23289
|
return { error: AI_BIRTHCERT_NOT_CONFIGURED_ERROR };
|
|
@@ -22997,6 +23291,42 @@ function createTools(deps) {
|
|
|
22997
23291
|
return deps.verifyAiBirthcert.check();
|
|
22998
23292
|
}
|
|
22999
23293
|
};
|
|
23294
|
+
return tools;
|
|
23295
|
+
}
|
|
23296
|
+
async function priceTemplate(deps, tools, templateId) {
|
|
23297
|
+
const schemaResult = await tools.get_template_schema({ templateId });
|
|
23298
|
+
const schema = schemaResult.schema;
|
|
23299
|
+
if (!schema && schemaResult.error) {
|
|
23300
|
+
return { reason: schemaResult.error };
|
|
23301
|
+
}
|
|
23302
|
+
const quoteFn = deps.subscribeDeps.mbi.quote;
|
|
23303
|
+
if (!quoteFn) {
|
|
23304
|
+
return { reason: "this wallet cannot price template credentials (MBI quote unavailable)", ...schema ? { schema } : {} };
|
|
23305
|
+
}
|
|
23306
|
+
let accepts;
|
|
23307
|
+
let paymentRequired;
|
|
23308
|
+
try {
|
|
23309
|
+
;
|
|
23310
|
+
({ accepts, paymentRequired } = await quoteFn.call(deps.subscribeDeps.mbi, templateId, { preflight: "quote-only" }));
|
|
23311
|
+
} catch (err) {
|
|
23312
|
+
return { reason: err.message, ...schema ? { schema } : {} };
|
|
23313
|
+
}
|
|
23314
|
+
const accept = accepts?.[0];
|
|
23315
|
+
if (!accept) return { reason: "MBI quoted no payment options for this template", ...schema ? { schema } : {} };
|
|
23316
|
+
return {
|
|
23317
|
+
quote: {
|
|
23318
|
+
asset: accept.asset,
|
|
23319
|
+
maxAmountRequired: accept.maxAmountRequired,
|
|
23320
|
+
payTo: accept.payTo,
|
|
23321
|
+
// Stated either way: preflight reads a missing gasModel as self-pay, so silence here would be
|
|
23322
|
+
// indistinguishable from a deliberate "you pay the gas".
|
|
23323
|
+
gasModel: isSponsored(accept) ? "sponsored" : "self",
|
|
23324
|
+
// Carried through: this is the only thing in the response that
|
|
23325
|
+
// says whether `maxAmountRequired` will actually be charged, and preflight gates on it.
|
|
23326
|
+
...paymentRequired === void 0 ? {} : { paymentRequired }
|
|
23327
|
+
},
|
|
23328
|
+
...schema ? { schema } : {}
|
|
23329
|
+
};
|
|
23000
23330
|
}
|
|
23001
23331
|
|
|
23002
23332
|
// src/orchestrator/resolve-holder.ts
|
|
@@ -23338,43 +23668,6 @@ function canonicalizeJson(value) {
|
|
|
23338
23668
|
return JSON.stringify(value);
|
|
23339
23669
|
}
|
|
23340
23670
|
|
|
23341
|
-
// src/accept-selection.ts
|
|
23342
|
-
function extraOf(accept) {
|
|
23343
|
-
const extra = accept.extra;
|
|
23344
|
-
if (!extra || typeof extra !== "object" || Array.isArray(extra)) return {};
|
|
23345
|
-
return extra;
|
|
23346
|
-
}
|
|
23347
|
-
function assetOf(accept) {
|
|
23348
|
-
return String(accept.asset ?? "");
|
|
23349
|
-
}
|
|
23350
|
-
function declaresFacilitator(accept) {
|
|
23351
|
-
return extraOf(accept).gasModel === "facilitator";
|
|
23352
|
-
}
|
|
23353
|
-
function isSponsored(accept) {
|
|
23354
|
-
if (!declaresFacilitator(accept)) return false;
|
|
23355
|
-
const prepareEndpoint = extraOf(accept).prepareEndpoint;
|
|
23356
|
-
if (typeof prepareEndpoint !== "string" || prepareEndpoint === "") return false;
|
|
23357
|
-
const asset = assetOf(accept);
|
|
23358
|
-
return asset !== "" && asset !== "ZTX";
|
|
23359
|
-
}
|
|
23360
|
-
function isUnusable(accept) {
|
|
23361
|
-
return declaresFacilitator(accept) && !isSponsored(accept);
|
|
23362
|
-
}
|
|
23363
|
-
function orderAccepts(accepts, prefer) {
|
|
23364
|
-
const usable = accepts.filter((a) => !isUnusable(a));
|
|
23365
|
-
const sponsored = usable.filter(isSponsored);
|
|
23366
|
-
const selfPay = usable.filter((a) => !isSponsored(a));
|
|
23367
|
-
return prefer === "sponsored" ? [...sponsored, ...selfPay] : [...selfPay, ...sponsored];
|
|
23368
|
-
}
|
|
23369
|
-
function prepareBaseUrl(prepareEndpoint) {
|
|
23370
|
-
return prepareEndpoint.replace(/\/+$/, "").replace(/\/prepare$/, "");
|
|
23371
|
-
}
|
|
23372
|
-
function needsNativeGasCheck(accept) {
|
|
23373
|
-
const asset = String(accept.asset ?? "");
|
|
23374
|
-
if (asset === "" || asset === "ZTX") return false;
|
|
23375
|
-
return !isSponsored(accept);
|
|
23376
|
-
}
|
|
23377
|
-
|
|
23378
23671
|
// src/orchestrator/verify-ai-birthcert.ts
|
|
23379
23672
|
function subjectMatches(vc, holderDid) {
|
|
23380
23673
|
if (typeof vc !== "object" || vc === null) return false;
|
|
@@ -23496,6 +23789,7 @@ var DEFINITIVE_PREPARE_REFUSALS = /* @__PURE__ */ new Set([
|
|
|
23496
23789
|
461415
|
|
23497
23790
|
// X402_UNSUPPORTED_ASSET — sponsorship is ZTP-20 only
|
|
23498
23791
|
]);
|
|
23792
|
+
var FACILITATOR_INSUFFICIENT_FUNDS_CODE = 461407;
|
|
23499
23793
|
function facilitatorErrorCode(err) {
|
|
23500
23794
|
const m = /"errorCode"\s*:\s*(\d{6})/.exec(err instanceof Error ? err.message : String(err));
|
|
23501
23795
|
return m ? Number(m[1]) : void 0;
|
|
@@ -23515,6 +23809,41 @@ function isDefinitiveSponsorshipFailure(err) {
|
|
|
23515
23809
|
if (code === void 0) return false;
|
|
23516
23810
|
return DEFINITIVE_PREPARE_REFUSALS.has(code);
|
|
23517
23811
|
}
|
|
23812
|
+
var FacilitatorInsufficientFundsError = class extends Error {
|
|
23813
|
+
constructor(requiredHuman, availableHuman, rawMessage) {
|
|
23814
|
+
super(
|
|
23815
|
+
`insufficient funds: the sponsored payment requires ${requiredHuman}` + (availableHuman !== void 0 ? ` \u2014 this wallet currently holds ${availableHuman}` : " and this wallet does not hold enough of it") + ` (facilitator: ${rawMessage})`
|
|
23816
|
+
);
|
|
23817
|
+
this.name = "FacilitatorInsufficientFundsError";
|
|
23818
|
+
}
|
|
23819
|
+
};
|
|
23820
|
+
function extractFacilitatorBalance(rawMessage) {
|
|
23821
|
+
const braceIdx = rawMessage.indexOf("{");
|
|
23822
|
+
if (braceIdx !== -1) {
|
|
23823
|
+
try {
|
|
23824
|
+
const body = JSON.parse(rawMessage.slice(braceIdx));
|
|
23825
|
+
const detail = body.messages?.[0]?.detail;
|
|
23826
|
+
if (Array.isArray(detail) && detail.length >= 3 && detail[2] !== null && detail[2] !== void 0) {
|
|
23827
|
+
return String(detail[2]);
|
|
23828
|
+
}
|
|
23829
|
+
} catch {
|
|
23830
|
+
}
|
|
23831
|
+
}
|
|
23832
|
+
const m = /insufficient_funds \[[^,]+,\s*[^,]+,\s*([^\]]+)\]/.exec(rawMessage);
|
|
23833
|
+
return m ? m[1].trim() : void 0;
|
|
23834
|
+
}
|
|
23835
|
+
async function toFacilitatorInsufficientFundsError(err, accept, formatAssetAmount) {
|
|
23836
|
+
if (!(err instanceof PrepareStageError)) return err;
|
|
23837
|
+
if (facilitatorErrorCode(err.cause) !== FACILITATOR_INSUFFICIENT_FUNDS_CODE) return err;
|
|
23838
|
+
const asset = accept.asset ?? "";
|
|
23839
|
+
const maxAmountRequired = accept.maxAmountRequired ?? "";
|
|
23840
|
+
if (!asset || !maxAmountRequired) return err;
|
|
23841
|
+
const rawMessage = err.cause instanceof Error ? err.cause.message : String(err.cause);
|
|
23842
|
+
const requiredHuman = formatAssetAmount ? await formatAssetAmount(asset, maxAmountRequired) : `${maxAmountRequired} of asset "${asset}"`;
|
|
23843
|
+
const availableRaw = extractFacilitatorBalance(rawMessage);
|
|
23844
|
+
const availableHuman = availableRaw !== void 0 && formatAssetAmount ? await formatAssetAmount(asset, availableRaw) : availableRaw;
|
|
23845
|
+
return new FacilitatorInsufficientFundsError(requiredHuman, availableHuman, rawMessage);
|
|
23846
|
+
}
|
|
23518
23847
|
async function attemptCandidate(deps, accept, buildBody, onQueued) {
|
|
23519
23848
|
let xPayment;
|
|
23520
23849
|
try {
|
|
@@ -23534,8 +23863,9 @@ async function payAndCreateSession(deps, buildBody, onQueued) {
|
|
|
23534
23863
|
return await attemptCandidate(deps, primary, buildBody, onQueued);
|
|
23535
23864
|
} catch (err) {
|
|
23536
23865
|
const fallback = candidates[1];
|
|
23537
|
-
if (!fallback || !isSponsored(primary) || isSponsored(fallback)
|
|
23538
|
-
|
|
23866
|
+
if (!fallback || !isSponsored(primary) || isSponsored(fallback) || !isDefinitiveSponsorshipFailure(err)) {
|
|
23867
|
+
throw await toFacilitatorInsufficientFundsError(err, primary, deps.formatAssetAmount);
|
|
23868
|
+
}
|
|
23539
23869
|
return await attemptCandidate(deps, fallback, buildBody, onQueued);
|
|
23540
23870
|
}
|
|
23541
23871
|
}
|
|
@@ -23544,29 +23874,48 @@ async function requestAiBirthcertVerification(deps, input) {
|
|
|
23544
23874
|
throw new Error("requestAiBirthcertVerification: agentName is required");
|
|
23545
23875
|
}
|
|
23546
23876
|
const agentName = input.agentName.trim();
|
|
23877
|
+
if (input.dryRun) return quoteVerification(deps, agentName, input);
|
|
23547
23878
|
return withRequestLock(() => requestAiBirthcertVerificationLocked(deps, agentName, input));
|
|
23548
23879
|
}
|
|
23880
|
+
async function buildSessionBody(deps, agentName, input) {
|
|
23881
|
+
const fields = {
|
|
23882
|
+
publicKey: deps.publicKeyHex,
|
|
23883
|
+
address: deps.address,
|
|
23884
|
+
timestamp: isoSeconds(deps.now()),
|
|
23885
|
+
agentName,
|
|
23886
|
+
id: agentName,
|
|
23887
|
+
ownerReference: deps.holderDid
|
|
23888
|
+
};
|
|
23889
|
+
if (input.agentPurpose) fields.agentPurpose = input.agentPurpose;
|
|
23890
|
+
if (input.evidenceAssuranceLevel) fields.evidenceAssuranceLevel = input.evidenceAssuranceLevel;
|
|
23891
|
+
if (input.ownerType) fields.ownerType = input.ownerType;
|
|
23892
|
+
if (input.ownerVerified) fields.ownerVerified = input.ownerVerified;
|
|
23893
|
+
const digestHex = (0, import_node_crypto4.createHash)("sha256").update(canonicalizeJson(fields), "utf8").digest("hex");
|
|
23894
|
+
const { signBlob: signedData } = await deps.signHexBlob(digestHex);
|
|
23895
|
+
return { ...fields, signedData };
|
|
23896
|
+
}
|
|
23897
|
+
async function quoteVerification(deps, agentName, input) {
|
|
23898
|
+
const requestedGasPayer = input.gasPayer === "self" || input.gasPayer === "sponsored" ? input.gasPayer : void 0;
|
|
23899
|
+
const body = await buildSessionBody(deps, agentName, input);
|
|
23900
|
+
const challenge = await deps.ssivc.createSessionChallenge(body);
|
|
23901
|
+
const chosen = orderAccepts(challenge.accepts, requestedGasPayer ?? deps.gasPreference ?? "sponsored")[0];
|
|
23902
|
+
if (!chosen) return { error: "SSIVC 402 returned no usable payment options" };
|
|
23903
|
+
return {
|
|
23904
|
+
quote: {
|
|
23905
|
+
asset: String(chosen.asset ?? ""),
|
|
23906
|
+
maxAmountRequired: String(chosen.maxAmountRequired ?? ""),
|
|
23907
|
+
...chosen.payTo ? { payTo: String(chosen.payTo) } : {},
|
|
23908
|
+
// Report the option that would actually be paid, not merely what was offered — the ranking
|
|
23909
|
+
// already discards a sponsored quote we could not act on.
|
|
23910
|
+
gasModel: isSponsored(chosen) ? "sponsored" : "self"
|
|
23911
|
+
}
|
|
23912
|
+
};
|
|
23913
|
+
}
|
|
23549
23914
|
async function requestAiBirthcertVerificationLocked(deps, agentName, input) {
|
|
23550
23915
|
const decision = await decidePriorSession(deps, agentName);
|
|
23551
23916
|
if (decision.kind === "still_pending") return decision.result;
|
|
23552
23917
|
if (decision.kind === "blocked") return { error: decision.message };
|
|
23553
|
-
const buildBody =
|
|
23554
|
-
const fields = {
|
|
23555
|
-
publicKey: deps.publicKeyHex,
|
|
23556
|
-
address: deps.address,
|
|
23557
|
-
timestamp: isoSeconds(deps.now()),
|
|
23558
|
-
agentName,
|
|
23559
|
-
id: agentName,
|
|
23560
|
-
ownerReference: deps.holderDid
|
|
23561
|
-
};
|
|
23562
|
-
if (input.agentPurpose) fields.agentPurpose = input.agentPurpose;
|
|
23563
|
-
if (input.evidenceAssuranceLevel) fields.evidenceAssuranceLevel = input.evidenceAssuranceLevel;
|
|
23564
|
-
if (input.ownerType) fields.ownerType = input.ownerType;
|
|
23565
|
-
if (input.ownerVerified) fields.ownerVerified = input.ownerVerified;
|
|
23566
|
-
const digestHex = (0, import_node_crypto4.createHash)("sha256").update(canonicalizeJson(fields), "utf8").digest("hex");
|
|
23567
|
-
const { signBlob: signedData } = await deps.signHexBlob(digestHex);
|
|
23568
|
-
return { ...fields, signedData };
|
|
23569
|
-
};
|
|
23918
|
+
const buildBody = () => buildSessionBody(deps, agentName, input);
|
|
23570
23919
|
const persistQueuedReceipt = async (receipt) => {
|
|
23571
23920
|
try {
|
|
23572
23921
|
await deps.sessionStore.set({
|
|
@@ -23602,9 +23951,10 @@ async function requestAiBirthcertVerificationLocked(deps, agentName, input) {
|
|
|
23602
23951
|
}
|
|
23603
23952
|
} catch (rawErr) {
|
|
23604
23953
|
const err = rawErr instanceof PrepareStageError ? rawErr.cause : rawErr;
|
|
23605
|
-
if (err instanceof PaymentReadinessError) return { error: `insufficient funds: ${err.message}
|
|
23606
|
-
if (err instanceof PaymentCapError) return { error: err.message };
|
|
23954
|
+
if (err instanceof PaymentReadinessError) return { error: `insufficient funds: ${err.message}`, insufficientFunds: err.shortfall };
|
|
23955
|
+
if (err instanceof PaymentCapError) return { error: err.message, ...err.detail ? { paymentCap: err.detail } : {} };
|
|
23607
23956
|
if (err instanceof NoPaymentOptionsError) return { error: err.message };
|
|
23957
|
+
if (err instanceof FacilitatorInsufficientFundsError) return { error: err.message };
|
|
23608
23958
|
if (err instanceof SettlementStillQueuedError) {
|
|
23609
23959
|
try {
|
|
23610
23960
|
await deps.sessionStore.set({
|
|
@@ -23655,6 +24005,9 @@ async function checkAiBirthcertVerification(deps) {
|
|
|
23655
24005
|
};
|
|
23656
24006
|
}
|
|
23657
24007
|
const status = await deps.ssivc.getSession(stored.sessionId);
|
|
24008
|
+
if (status.status !== "issued" && stored.verificationUrl) {
|
|
24009
|
+
return { ...status, verificationUrl: stored.verificationUrl };
|
|
24010
|
+
}
|
|
23658
24011
|
if (status.status !== "issued" || !status.vcId) return status;
|
|
23659
24012
|
if (deps.verifiedTemplateId && deps.cache) {
|
|
23660
24013
|
const cached2 = await deps.cache.get(deps.verifiedTemplateId);
|
|
@@ -23724,7 +24077,8 @@ function buildToolList() {
|
|
|
23724
24077
|
type: "object",
|
|
23725
24078
|
properties: {
|
|
23726
24079
|
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." },
|
|
23727
|
-
token: { type: "string", description:
|
|
24080
|
+
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 a `display` value to the user, never a bare `balance`. A failed lookup reports { error: "query_failed" } rather than a zero balance; an unrecognised name reports { error: "unknown_token" }.' },
|
|
24081
|
+
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.' }
|
|
23728
24082
|
}
|
|
23729
24083
|
}
|
|
23730
24084
|
},
|
|
@@ -23807,7 +24161,7 @@ function buildToolList() {
|
|
|
23807
24161
|
expirationDate: { type: "string" },
|
|
23808
24162
|
dryRun: {
|
|
23809
24163
|
type: "boolean",
|
|
23810
|
-
description: "
|
|
24164
|
+
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."
|
|
23811
24165
|
},
|
|
23812
24166
|
forceReissue: {
|
|
23813
24167
|
type: "boolean",
|
|
@@ -23819,7 +24173,7 @@ function buildToolList() {
|
|
|
23819
24173
|
},
|
|
23820
24174
|
{
|
|
23821
24175
|
name: "request_ai_birthcert_verification",
|
|
23822
|
-
description: `Start a Verified AI Birthcert issuance session with myid (MyDigital ID owner verification). Returns { sessionId, verificationUrl, expiresAt } \u2014 show verificationUrl to the human owner and ask them to open it and complete MyDigital ID verification (typically finishes in seconds). Once they confirm they are done, call check_ai_birthcert_verification to see whether the credential was issued. IMPORTANT: agentName must be unique \u2014 if this exact name has already been used to request a Verified AI Birthcert, issuance will fail. Before calling, ask the human owner whether they want to supply any of the optional fields \u2014 agentPurpose, evidenceAssuranceLevel, ownerType, ownerVerified \u2014 do not silently omit them; they only need to say no. Calling this again with the SAME agentName while a prior session is still pending returns that same session unchanged \u2014 no new session is started and nothing is paid again. This tool spends real funds: it self-pays an x402 challenge, subject to the
|
|
24176
|
+
description: `Start a Verified AI Birthcert issuance session with myid (MyDigital ID owner verification). Returns { sessionId, verificationUrl, expiresAt } \u2014 show verificationUrl to the human owner and ask them to open it and complete MyDigital ID verification (typically finishes in seconds). Once they confirm they are done, call check_ai_birthcert_verification to see whether the credential was issued. IMPORTANT: agentName must be unique \u2014 if this exact name has already been used to request a Verified AI Birthcert, issuance will fail. Before calling, ask the human owner whether they want to supply any of the optional fields \u2014 agentPurpose, evidenceAssuranceLevel, ownerType, ownerVerified \u2014 do not silently omit them; they only need to say no. Calling this again with the SAME agentName while a prior session is still pending returns that same session unchanged \u2014 no new session is started and nothing is paid again. This tool spends real funds: it self-pays an x402 challenge, subject to the same credential-issuance payment cap as subscribe_and_issue \u2014 a separate, narrower cap than pay_and_fetch's, which defaults to refusing everything on mainnet. Set MAX_PAYMENT_AMOUNT to override either. It can return { error: "..." } instead of a session if that payment fails (insufficient funds, or the payment cap blocked it) \u2014 nothing is created in that case. If the user did NOT ask for a "verified" credential specifically, they most likely want the self-declared, non-verified Basic AI Birthcert instead \u2014 use subscribe_and_issue for that.`,
|
|
23823
24177
|
inputSchema: {
|
|
23824
24178
|
type: "object",
|
|
23825
24179
|
properties: {
|
|
@@ -23835,14 +24189,36 @@ function buildToolList() {
|
|
|
23835
24189
|
type: "string",
|
|
23836
24190
|
enum: ["sponsored", "self"],
|
|
23837
24191
|
description: `Who pays network gas. "sponsored" (default) asks the platform paymaster to cover gas, so this wallet needs no ZTX. "self" pays gas from this wallet's own ZTX balance. Omit to use the configured default.`
|
|
24192
|
+
},
|
|
24193
|
+
dryRun: {
|
|
24194
|
+
type: "boolean",
|
|
24195
|
+
description: "Ask the price WITHOUT paying. Returns { quote: { asset, maxAmountRequired, payTo, gasModel } } and spends nothing, creates no session, and starts no verification \u2014 so you can tell the user the cost before collecting anything. `maxAmountRequired` is in the asset's RAW base units; resolve decimals (wallet_status returns `display`) before quoting a figure to a human. A quote does NOT reserve the name and does NOT check whether it is already taken \u2014 myid checks uniqueness only at issuance, so a name already in use still quotes cleanly. `agentName` is still required because the server rejects a request without one, but the fee does not depend on it."
|
|
23838
24196
|
}
|
|
23839
24197
|
},
|
|
23840
24198
|
required: ["agentName"]
|
|
23841
24199
|
}
|
|
23842
24200
|
},
|
|
24201
|
+
{
|
|
24202
|
+
name: "credential_preflight",
|
|
24203
|
+
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?, 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. Gas is separate and can still block a free credential. When the field is ABSENT the cost is unknown (an older MBI), and it is treated as chargeable \u2014 never report absent as free.",
|
|
24204
|
+
inputSchema: {
|
|
24205
|
+
type: "object",
|
|
24206
|
+
properties: {
|
|
24207
|
+
credential: {
|
|
24208
|
+
type: "string",
|
|
24209
|
+
description: 'Which credential to price: "verified_ai_birthcert" for the MyDigital-ID-verified AI Birthcert, or a template id (did:zid:...) / known template name (e.g. "AI Birthcert") for a template-issued one.'
|
|
24210
|
+
},
|
|
24211
|
+
agentName: {
|
|
24212
|
+
type: "string",
|
|
24213
|
+
description: 'Optional, and only used for "verified_ai_birthcert". The fee does not depend on it, so omit it when pricing before the user has chosen a name \u2014 the wallet substitutes a placeholder purely to satisfy the server. Never present that placeholder as the name that will be used.'
|
|
24214
|
+
}
|
|
24215
|
+
},
|
|
24216
|
+
required: ["credential"]
|
|
24217
|
+
}
|
|
24218
|
+
},
|
|
23843
24219
|
{
|
|
23844
24220
|
name: "check_ai_birthcert_verification",
|
|
23845
|
-
description: 'Check the status of the most recently requested Verified AI Birthcert session (see request_ai_birthcert_verification). Returns { status: "pending" } while the owner has not yet completed MyDigital ID verification, or { status: "issued", vcId } once myid has minted the credential \u2014 myid returns vcId ONLY when status is "issued", never otherwise. On { status: "issued" }, the wallet also fetches the credential from MBI, verifies it, and caches it locally, returning it as `vc` \u2014 it is then also visible via wallet_status and usable by prove_identity without any further call. If `cacheError` is present instead of `vc`, the credential WAS issued successfully but could not be fetched/verified/cached yet (e.g. a transient MBI error) \u2014 this is NOT the same as issuance failing, so do not retry request_ai_birthcert_verification; call check_ai_birthcert_verification again instead. Returns { status: "no_session" } if request_ai_birthcert_verification has never been called.',
|
|
24221
|
+
description: 'Check the status of the most recently requested Verified AI Birthcert session (see request_ai_birthcert_verification). FREE \u2014 spends nothing and starts nothing. Use this, not request_ai_birthcert_verification, whenever the user asks where their verification link is, what happened to their session, or whether their credential is ready. While the session is still open the result carries `verificationUrl` (the same link issued at creation) and `expiresAt` \u2014 give the user both, so they know how long it is good for. Returns { status: "pending" } while the owner has not yet completed MyDigital ID verification, or { status: "issued", vcId } once myid has minted the credential \u2014 myid returns vcId ONLY when status is "issued", never otherwise. On { status: "issued" }, the wallet also fetches the credential from MBI, verifies it, and caches it locally, returning it as `vc` \u2014 it is then also visible via wallet_status and usable by prove_identity without any further call. If `cacheError` is present instead of `vc`, the credential WAS issued successfully but could not be fetched/verified/cached yet (e.g. a transient MBI error) \u2014 this is NOT the same as issuance failing, so do not retry request_ai_birthcert_verification; call check_ai_birthcert_verification again instead. Returns { status: "no_session" } if request_ai_birthcert_verification has never been called.',
|
|
23846
24222
|
inputSchema: { type: "object", properties: {} }
|
|
23847
24223
|
},
|
|
23848
24224
|
{
|
|
@@ -23862,6 +24238,13 @@ function buildToolList() {
|
|
|
23862
24238
|
}
|
|
23863
24239
|
];
|
|
23864
24240
|
}
|
|
24241
|
+
function buildPayers(config2, makePay) {
|
|
24242
|
+
return {
|
|
24243
|
+
pay: makePay(config2.maxPaymentAmount),
|
|
24244
|
+
payForCredential: makePay(config2.credentialIssuanceCaps),
|
|
24245
|
+
preflightCaps: config2.credentialIssuanceCaps
|
|
24246
|
+
};
|
|
24247
|
+
}
|
|
23865
24248
|
function asPayRequest(accept) {
|
|
23866
24249
|
const extra = accept.extra ?? {};
|
|
23867
24250
|
const prepareEndpoint = extra.prepareEndpoint;
|
|
@@ -23954,15 +24337,19 @@ async function main() {
|
|
|
23954
24337
|
const human = formatHumanAmount(raw, decimals);
|
|
23955
24338
|
return human === raw ? `${raw} ${label}` : `${raw} (${human} ${label})`;
|
|
23956
24339
|
};
|
|
23957
|
-
const
|
|
24340
|
+
const makePay = (caps) => async (accept) => {
|
|
23958
24341
|
const rawAsset = String(accept.asset ?? "");
|
|
23959
24342
|
try {
|
|
23960
|
-
assertWithinPaymentCap(accept,
|
|
24343
|
+
assertWithinPaymentCap(accept, caps);
|
|
23961
24344
|
} catch (err) {
|
|
23962
24345
|
if (err instanceof PaymentCapError && err.detail) {
|
|
23963
24346
|
const { asset, requiredRaw, capRaw } = err.detail;
|
|
23964
24347
|
throw new PaymentCapError(
|
|
23965
|
-
|
|
24348
|
+
formatCapRefusal(
|
|
24349
|
+
err.detail,
|
|
24350
|
+
await formatAssetAmount(asset, requiredRaw),
|
|
24351
|
+
await formatAssetAmount(asset, capRaw)
|
|
24352
|
+
),
|
|
23966
24353
|
err.detail
|
|
23967
24354
|
);
|
|
23968
24355
|
}
|
|
@@ -23996,6 +24383,7 @@ async function main() {
|
|
|
23996
24383
|
throw err;
|
|
23997
24384
|
}
|
|
23998
24385
|
};
|
|
24386
|
+
const { pay, payForCredential, preflightCaps } = buildPayers(config2, makePay);
|
|
23999
24387
|
const verifyAiBirthcert = config2.ssivcBaseUrl ? (() => {
|
|
24000
24388
|
const ssivcSessionStore = createFsSsivcSessionStore((0, import_node_path6.join)(config2.stateDir, "ssivc-session.json"));
|
|
24001
24389
|
const downloadQuarantine = createFsDownloadQuarantineStore((0, import_node_path6.join)(config2.stateDir, "ssivc-download-quarantine"));
|
|
@@ -24005,7 +24393,7 @@ async function main() {
|
|
|
24005
24393
|
signHexBlob: walletBeSignerFn,
|
|
24006
24394
|
messageSigner,
|
|
24007
24395
|
mbi,
|
|
24008
|
-
pay,
|
|
24396
|
+
pay: payForCredential,
|
|
24009
24397
|
publicKeyHex,
|
|
24010
24398
|
address: zetrixAddress,
|
|
24011
24399
|
holderDid,
|
|
@@ -24015,7 +24403,8 @@ async function main() {
|
|
|
24015
24403
|
cache: vcCache,
|
|
24016
24404
|
quarantine: downloadQuarantine,
|
|
24017
24405
|
gasPreference: config2.gasPreference,
|
|
24018
|
-
maxSettlementAttempts: config2.maxSettlementAttempts
|
|
24406
|
+
maxSettlementAttempts: config2.maxSettlementAttempts,
|
|
24407
|
+
formatAssetAmount
|
|
24019
24408
|
};
|
|
24020
24409
|
return {
|
|
24021
24410
|
request: (input) => requestAiBirthcertVerification(verifyAiBirthcertDeps, input),
|
|
@@ -24066,9 +24455,13 @@ async function main() {
|
|
|
24066
24455
|
config: { holderDid, zetrixAddress, network: config2.network },
|
|
24067
24456
|
makeWallet,
|
|
24068
24457
|
payer,
|
|
24069
|
-
subscribeDeps: { mbi, sign: subscribeSign, pay, resolveSymbol, holderDid, resolveTemplateFields, cache: vcCache },
|
|
24458
|
+
subscribeDeps: { mbi, sign: subscribeSign, pay: payForCredential, resolveSymbol, holderDid, resolveTemplateFields, cache: vcCache },
|
|
24070
24459
|
queryContract: (input) => queryContract(input, contractQuery),
|
|
24071
24460
|
queryTokenBalance: queryTokenBalance2,
|
|
24461
|
+
// Read-only, for credential_preflight's cap headroom. Preflight only ever prices credentials,
|
|
24462
|
+
// so it must report against the issuance cap — the same map payForCredential enforces, or
|
|
24463
|
+
// preflight and the guard would disagree about what is permitted.
|
|
24464
|
+
paymentCaps: preflightCaps,
|
|
24072
24465
|
// The session password is bound here, in the wiring, so it never crosses into the tool
|
|
24073
24466
|
// layer — create_holder_account has no password parameter for a model to be asked for.
|
|
24074
24467
|
createAccount: (label, purpose) => be.createAccount(hsmPassword, label, purpose),
|
|
@@ -24111,5 +24504,6 @@ if (process.env.NODE_ENV !== "test") {
|
|
|
24111
24504
|
// Annotate the CommonJS export names for ESM import in node:
|
|
24112
24505
|
0 && (module.exports = {
|
|
24113
24506
|
asPayRequest,
|
|
24507
|
+
buildPayers,
|
|
24114
24508
|
buildToolList
|
|
24115
24509
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentic-wallet-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Agent-facing MCP wallet for Zetrix — orchestrates x401 identity proof, x402 payment, and MBI VC issuance",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
45
45
|
"x401-zetrix-client": "^0.2.1",
|
|
46
|
-
"x402-zetrix-client": "^0.2.
|
|
46
|
+
"x402-zetrix-client": "^0.2.4",
|
|
47
47
|
"zetrix-sdk-nodejs": "^1.0.3"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|