agentic-wallet-mcp 0.8.1 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -8,6 +8,46 @@ 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.9.1] — 28 August 2026
12
+
13
+ ### Fixed
14
+
15
+ - **`request_ai_birthcert_verification` no longer blocks retrying an expired session.** If the
16
+ owner never completed MyDigital ID verification before a Verified AI Birthcert session's TTL
17
+ elapsed (`status: "expired"`), retrying previously threw an "unrecognized prior session status"
18
+ error and left the flow stuck — even though the settlement receipt was already safely persisted
19
+ locally and unconsumed. Any confirmed terminal session status other than `"issued"` (the only
20
+ status that consumes the receipt) is now treated the same as the existing 404/"gone" handling:
21
+ the receipt is replayed and a fresh verification session is opened, with no new payment made.
22
+
23
+ ## [0.9.0] — 24 August 2026
24
+
25
+ ### Added
26
+
27
+ - **Paymaster-sponsored gas for `request_ai_birthcert_verification`.** When the resource server
28
+ offers a sponsored payment option (`extra.gasModel: "facilitator"`), the wallet now prefers it by
29
+ default — the ms-zetrix paymaster covers network gas, so a wallet holding the payment token but
30
+ zero ZTX can still complete the flow. Handles the server's asynchronous `202 Accepted` settlement
31
+ response: the wallet retries with the returned payment receipt (never re-sending the original
32
+ payment header) until the settlement confirms or the retry budget is exhausted, without ever
33
+ losing the receipt to a mid-retry crash or transient error.
34
+ - **Automatic self-pay fallback.** If sponsorship is refused before any money moved (the
35
+ paymaster pool is exhausted, rate-limited, or the network/asset isn't sponsorable), the wallet
36
+ falls back to self-pay automatically. It never falls back on an indeterminate outcome — a
37
+ payment that may still be settling is never retried as a fresh payment, which would risk paying
38
+ twice.
39
+ - **`gasPayer` parameter** on `request_ai_birthcert_verification` (`"sponsored"` | `"self"`) —
40
+ overrides the deployment default for a single call. An unrecognised value is treated as absent
41
+ rather than silently forcing self-pay.
42
+ - **`GAS_PREFERENCE`** env var (default `sponsored`) — deployment-wide default gas payer.
43
+ - **`MAX_SETTLEMENT_ATTEMPTS`** env var (default `20`) — cap on retry attempts while polling a
44
+ queued sponsored settlement.
45
+
46
+ ### Changed
47
+
48
+ - Existing self-pay behaviour is unchanged — a resource server that only quotes self-pay (no
49
+ sponsored option) behaves identically to before this release.
50
+
11
51
  ## [0.8.1] — 19 August 2026
12
52
 
13
53
  ### Fixed
package/README.md CHANGED
@@ -27,7 +27,7 @@ VC issuance → identity proof → pay-per-use).
27
27
  | `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
28
  | `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
29
  | `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 | `{ agentName, agentPurpose?, evidenceAssuranceLevel?, ownerType?, ownerVerified? }` | `{ sessionId, verificationUrl, expiresAt }` |
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
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? }` |
32
32
 
33
33
  > **VCs are cached locally**, keyed by `templateId`, under `~/.agentic-wallet-mcp/vc-cache/`
@@ -180,6 +180,8 @@ Node ≥ 18 required (built-in `fetch`).
180
180
  | `ZETRIX_WALLET_STATE_DIR` | no | Where the wallet keeps `account.json` and its VC cache. Defaults to `~/.agentic-wallet-mcp` |
181
181
  | `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
182
  | `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 |
183
+ | `GAS_PREFERENCE` | no | `sponsored` or `self` — the default gas payer for the Verified AI Birthcert flow (`request_ai_birthcert_verification`). **Defaults to `sponsored`; any unrecognised value also falls back to `sponsored`** rather than throwing. Sponsorship only actually engages when the resource server's x402 quote offers a sponsored option (`extra.gasModel: "facilitator"`) — a server that only quotes self-pay behaves unchanged. **Sponsorship is testnet-only today**; override per-call with the tool's `gasPayer` parameter |
184
+ | `MAX_SETTLEMENT_ATTEMPTS` | no | Cap on retry attempts while polling a sponsored payment for settlement after SSIVC returns `202 Accepted` (queued). **Defaults to `20`; a non-positive or unparseable value also falls back to `20`** |
183
185
 
184
186
  \* sensitive — never logged, never returned in a tool result, and never a tool parameter.
185
187
 
@@ -12537,6 +12537,7 @@ var require_dist2 = __commonJS({
12537
12537
  // src/index.ts
12538
12538
  var index_exports = {};
12539
12539
  __export(index_exports, {
12540
+ asPayRequest: () => asPayRequest,
12540
12541
  buildToolList: () => buildToolList
12541
12542
  });
12542
12543
  module.exports = __toCommonJS(index_exports);
@@ -12548,7 +12549,7 @@ var import_node_path6 = require("node:path");
12548
12549
  // package.json
12549
12550
  var package_default = {
12550
12551
  name: "agentic-wallet-mcp",
12551
- version: "0.8.1",
12552
+ version: "0.9.1",
12552
12553
  description: "Agent-facing MCP wallet for Zetrix \u2014 orchestrates x401 identity proof, x402 payment, and MBI VC issuance",
12553
12554
  keywords: [
12554
12555
  "mcp",
@@ -21623,7 +21624,14 @@ function loadConfig(env) {
21623
21624
  const v = opt("SSIVC_BASE_URL") ?? deriveSsivcBaseUrl(network);
21624
21625
  return v ? stripTrailingSlash(v) : void 0;
21625
21626
  })(),
21626
- aiBirthcertVerifiedTemplateId: opt("AI_BIRTHCERT_VERIFIED_TEMPLATE_ID") ?? deriveAiBirthcertVerifiedTemplateId(network)
21627
+ aiBirthcertVerifiedTemplateId: opt("AI_BIRTHCERT_VERIFIED_TEMPLATE_ID") ?? deriveAiBirthcertVerifiedTemplateId(network),
21628
+ // Unrecognised values fall back to the default rather than throwing — a typo here must not
21629
+ // brick startup, and 'sponsored' is always attemptable thanks to Task 5's self-pay fallback.
21630
+ gasPreference: env.GAS_PREFERENCE === "self" ? "self" : "sponsored",
21631
+ maxSettlementAttempts: (() => {
21632
+ const attempts = Number(env.MAX_SETTLEMENT_ATTEMPTS);
21633
+ return Number.isInteger(attempts) && attempts > 0 ? attempts : 20;
21634
+ })()
21627
21635
  };
21628
21636
  }
21629
21637
 
@@ -23164,6 +23172,7 @@ var SsivcError = class extends Error {
23164
23172
  this.kind = kind;
23165
23173
  }
23166
23174
  };
23175
+ var DEFAULT_RETRY_AFTER_SECONDS = 15;
23167
23176
  var SESSIONS_PATH = "/v2/verify/ai-birthcert/sessions";
23168
23177
  var SsivcClient = class {
23169
23178
  baseUrl;
@@ -23188,16 +23197,22 @@ var SsivcClient = class {
23188
23197
  async createSessionPaid(body, paymentHeader) {
23189
23198
  const res = await this.fetch("POST", SESSIONS_PATH, body, paymentHeader);
23190
23199
  if (!res.ok) throw await this.error(res, "createSession (paid) failed");
23200
+ const paymentReceipt = res.headers.get("x-payment-response");
23201
+ if (!paymentReceipt) {
23202
+ throw new SsivcError("SSIVC createSession succeeded (2xx) but returned no X-Payment-Response settlement receipt header", res.status);
23203
+ }
23204
+ if (res.status === 202) {
23205
+ const header = Number(res.headers.get("retry-after"));
23206
+ const retryAfterSeconds = Number.isFinite(header) && header > 0 ? header : DEFAULT_RETRY_AFTER_SECONDS;
23207
+ return { kind: "queued", paymentReceipt, retryAfterSeconds };
23208
+ }
23191
23209
  const env = await res.json();
23192
23210
  const data = env.data;
23193
23211
  if (!data || typeof data !== "object" || !data.sessionId || !data.verification_url || !data.expiresAt) {
23194
23212
  throw new SsivcError("SSIVC createSession succeeded (2xx) but returned a malformed data envelope", res.status);
23195
23213
  }
23196
- const paymentReceipt = res.headers.get("x-payment-response");
23197
- if (!paymentReceipt) {
23198
- throw new SsivcError("SSIVC createSession succeeded (2xx) but returned no X-Payment-Response settlement receipt header", res.status);
23199
- }
23200
23214
  return {
23215
+ kind: "settled",
23201
23216
  session: { sessionId: data.sessionId, verificationUrl: data.verification_url, expiresAt: data.expiresAt },
23202
23217
  paymentReceipt
23203
23218
  };
@@ -23323,6 +23338,43 @@ function canonicalizeJson(value) {
23323
23338
  return JSON.stringify(value);
23324
23339
  }
23325
23340
 
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
+
23326
23378
  // src/orchestrator/verify-ai-birthcert.ts
23327
23379
  function subjectMatches(vc, holderDid) {
23328
23380
  if (typeof vc !== "object" || vc === null) return false;
@@ -23350,16 +23402,18 @@ async function getConfirmedStatus(deps, sessionId) {
23350
23402
  async function decidePriorSession(deps, agentName) {
23351
23403
  const stored = await deps.sessionStore.get();
23352
23404
  if (!stored) return { kind: "pay_fresh" };
23405
+ if (stored.sessionId === "") return { kind: "replay_receipt", receipt: stored.paymentReceipt };
23353
23406
  if (stored.agentName !== agentName) {
23354
23407
  const otherStatus = await getConfirmedStatus(deps, stored.sessionId);
23355
23408
  if (otherStatus === "gone") return { kind: "replay_receipt", receipt: stored.paymentReceipt };
23356
- if (otherStatus.status !== "issued") {
23409
+ if (otherStatus.status === "pending") {
23357
23410
  return {
23358
23411
  kind: "blocked",
23359
- message: `a verification session for a different agent ("${stored.agentName}") is still in progress (status: "${otherStatus.status}") and its payment has not been consumed yet \u2014 starting a new session for "${agentName}" would lose track of it. Call check_ai_birthcert_verification to resolve or confirm it first.`
23412
+ message: `a verification session for a different agent ("${stored.agentName}") is still in progress (status: "pending") and its payment has not been consumed yet \u2014 starting a new session for "${agentName}" would lose track of it. Call check_ai_birthcert_verification to resolve or confirm it first.`
23360
23413
  };
23361
23414
  }
23362
- return { kind: "pay_fresh" };
23415
+ if (otherStatus.status === "issued") return { kind: "pay_fresh" };
23416
+ return { kind: "replay_receipt", receipt: stored.paymentReceipt };
23363
23417
  }
23364
23418
  const status = await getConfirmedStatus(deps, stored.sessionId);
23365
23419
  if (status === "gone") return { kind: "replay_receipt", receipt: stored.paymentReceipt };
@@ -23370,9 +23424,7 @@ async function decidePriorSession(deps, agentName) {
23370
23424
  };
23371
23425
  }
23372
23426
  if (status.status === "issued") return { kind: "pay_fresh" };
23373
- throw new Error(
23374
- `requestAiBirthcertVerification: unrecognized prior session status "${status.status}" for session ${stored.sessionId} \u2014 refusing to guess whether its settlement receipt is safe to replay (see SPEC.md D8)`
23375
- );
23427
+ return { kind: "replay_receipt", receipt: stored.paymentReceipt };
23376
23428
  }
23377
23429
  var requestQueue = Promise.resolve();
23378
23430
  function withRequestLock(fn) {
@@ -23385,12 +23437,107 @@ function withRequestLock(fn) {
23385
23437
  }
23386
23438
  var NoPaymentOptionsError = class extends Error {
23387
23439
  };
23388
- async function payAndCreateSession(deps, body) {
23389
- const challenge = await deps.ssivc.createSessionChallenge(body);
23390
- const accept = challenge.accepts[0];
23391
- if (!accept) throw new NoPaymentOptionsError("SSIVC 402 returned no payment options");
23392
- const xPayment = await deps.pay(accept);
23393
- return deps.ssivc.createSessionSettle(body, xPayment);
23440
+ var DEFAULT_MAX_SETTLEMENT_ATTEMPTS = 20;
23441
+ var MAX_RETRY_DELAY_MS = 6e4;
23442
+ var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
23443
+ var SettlementStillQueuedError = class extends Error {
23444
+ constructor(message, paymentReceipt) {
23445
+ super(message);
23446
+ this.paymentReceipt = paymentReceipt;
23447
+ this.name = "SettlementStillQueuedError";
23448
+ }
23449
+ };
23450
+ var SettlementOutcomeUnknownError = class extends Error {
23451
+ constructor(message, paymentReceipt) {
23452
+ super(message);
23453
+ this.paymentReceipt = paymentReceipt;
23454
+ this.name = "SettlementOutcomeUnknownError";
23455
+ }
23456
+ };
23457
+ async function resolveSettlement(deps, buildBody, initialOutcome, onQueued) {
23458
+ const sleep = deps.sleep ?? defaultSleep;
23459
+ const maxAttempts = deps.maxSettlementAttempts ?? DEFAULT_MAX_SETTLEMENT_ATTEMPTS;
23460
+ let outcome = initialOutcome;
23461
+ let lastPersistedReceipt;
23462
+ if (outcome.kind === "queued") {
23463
+ lastPersistedReceipt = outcome.paymentReceipt;
23464
+ await onQueued(outcome.paymentReceipt);
23465
+ }
23466
+ for (let attempt = 0; outcome.kind === "queued" && attempt < maxAttempts; attempt++) {
23467
+ const delayMs = Math.min(outcome.retryAfterSeconds * 1e3, MAX_RETRY_DELAY_MS);
23468
+ await sleep(delayMs);
23469
+ const receiptSent = outcome.paymentReceipt;
23470
+ try {
23471
+ outcome = await deps.ssivc.createSessionWithReceipt(await buildBody(), receiptSent);
23472
+ } catch (err) {
23473
+ throw new SettlementOutcomeUnknownError(
23474
+ `the settlement outcome for payment receipt ${receiptSent} could not be determined \u2014 the receipt-retry call itself failed (${err instanceof Error ? err.message : String(err)})`,
23475
+ receiptSent
23476
+ );
23477
+ }
23478
+ if (outcome.kind === "queued" && outcome.paymentReceipt !== lastPersistedReceipt) {
23479
+ lastPersistedReceipt = outcome.paymentReceipt;
23480
+ await onQueued(outcome.paymentReceipt);
23481
+ }
23482
+ }
23483
+ if (outcome.kind === "queued") {
23484
+ throw new SettlementStillQueuedError(
23485
+ "the sponsored payment is being processed and has not settled yet \u2014 no funds are lost and the receipt has been kept; run request_ai_birthcert_verification again shortly to resume",
23486
+ outcome.paymentReceipt
23487
+ );
23488
+ }
23489
+ return { session: outcome.session, paymentReceipt: outcome.paymentReceipt };
23490
+ }
23491
+ var DEFINITIVE_PREPARE_REFUSALS = /* @__PURE__ */ new Set([
23492
+ 461411,
23493
+ // X402_UNSUPPORTED_NETWORK — this network is not sponsored
23494
+ 461414,
23495
+ // X402_RATE_LIMIT_EXCEEDED — pool rate limit, or the per-address active-prepare cap (default 5)
23496
+ 461415
23497
+ // X402_UNSUPPORTED_ASSET — sponsorship is ZTP-20 only
23498
+ ]);
23499
+ function facilitatorErrorCode(err) {
23500
+ const m = /"errorCode"\s*:\s*(\d{6})/.exec(err instanceof Error ? err.message : String(err));
23501
+ return m ? Number(m[1]) : void 0;
23502
+ }
23503
+ var PrepareStageError = class extends Error {
23504
+ constructor(cause) {
23505
+ super("prepare stage failed");
23506
+ this.cause = cause;
23507
+ this.name = "PrepareStageError";
23508
+ }
23509
+ };
23510
+ function isDefinitiveSponsorshipFailure(err) {
23511
+ if (!(err instanceof PrepareStageError)) return false;
23512
+ const cause = err.cause;
23513
+ if (cause instanceof SettlementStillQueuedError) return false;
23514
+ const code = facilitatorErrorCode(cause);
23515
+ if (code === void 0) return false;
23516
+ return DEFINITIVE_PREPARE_REFUSALS.has(code);
23517
+ }
23518
+ async function attemptCandidate(deps, accept, buildBody, onQueued) {
23519
+ let xPayment;
23520
+ try {
23521
+ xPayment = await deps.pay(accept);
23522
+ } catch (err) {
23523
+ throw new PrepareStageError(err);
23524
+ }
23525
+ const initialOutcome = await deps.ssivc.createSessionSettle(await buildBody(), xPayment);
23526
+ return resolveSettlement(deps, buildBody, initialOutcome, onQueued);
23527
+ }
23528
+ async function payAndCreateSession(deps, buildBody, onQueued) {
23529
+ const challenge = await deps.ssivc.createSessionChallenge(await buildBody());
23530
+ const candidates = orderAccepts(challenge.accepts, deps.gasPreference ?? "sponsored");
23531
+ const primary = candidates[0];
23532
+ if (!primary) throw new NoPaymentOptionsError("SSIVC 402 returned no usable payment options");
23533
+ try {
23534
+ return await attemptCandidate(deps, primary, buildBody, onQueued);
23535
+ } catch (err) {
23536
+ const fallback = candidates[1];
23537
+ if (!fallback || !isSponsored(primary) || isSponsored(fallback)) throw err;
23538
+ if (!isDefinitiveSponsorshipFailure(err)) throw err;
23539
+ return await attemptCandidate(deps, fallback, buildBody, onQueued);
23540
+ }
23394
23541
  }
23395
23542
  async function requestAiBirthcertVerification(deps, input) {
23396
23543
  if (!input.agentName || !input.agentName.trim()) {
@@ -23403,31 +23550,85 @@ async function requestAiBirthcertVerificationLocked(deps, agentName, input) {
23403
23550
  const decision = await decidePriorSession(deps, agentName);
23404
23551
  if (decision.kind === "still_pending") return decision.result;
23405
23552
  if (decision.kind === "blocked") return { error: decision.message };
23406
- const fields = {
23407
- publicKey: deps.publicKeyHex,
23408
- address: deps.address,
23409
- timestamp: isoSeconds(deps.now()),
23410
- agentName,
23411
- id: agentName,
23412
- ownerReference: deps.holderDid
23553
+ const buildBody = async () => {
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
+ };
23570
+ const persistQueuedReceipt = async (receipt) => {
23571
+ try {
23572
+ await deps.sessionStore.set({
23573
+ sessionId: "",
23574
+ agentName,
23575
+ createdAt: deps.now().toISOString(),
23576
+ verificationUrl: "",
23577
+ paymentReceipt: receipt
23578
+ });
23579
+ } catch {
23580
+ }
23413
23581
  };
23414
- if (input.agentPurpose) fields.agentPurpose = input.agentPurpose;
23415
- if (input.evidenceAssuranceLevel) fields.evidenceAssuranceLevel = input.evidenceAssuranceLevel;
23416
- if (input.ownerType) fields.ownerType = input.ownerType;
23417
- if (input.ownerVerified) fields.ownerVerified = input.ownerVerified;
23418
- const digestHex = (0, import_node_crypto4.createHash)("sha256").update(canonicalizeJson(fields), "utf8").digest("hex");
23419
- const { signBlob: signedData } = await deps.signHexBlob(digestHex);
23420
- const body = { ...fields, signedData };
23582
+ const requestedGasPayer = input.gasPayer === "self" || input.gasPayer === "sponsored" ? input.gasPayer : void 0;
23421
23583
  let paid;
23422
23584
  try {
23423
- paid = decision.kind === "replay_receipt" ? await deps.ssivc.createSessionWithReceipt(body, decision.receipt) : await payAndCreateSession(deps, body);
23424
- } catch (err) {
23585
+ if (decision.kind === "replay_receipt") {
23586
+ let initialOutcome;
23587
+ try {
23588
+ initialOutcome = await deps.ssivc.createSessionWithReceipt(await buildBody(), decision.receipt);
23589
+ } catch (err) {
23590
+ throw new SettlementOutcomeUnknownError(
23591
+ `the settlement outcome for payment receipt ${decision.receipt} could not be determined \u2014 the receipt-replay call itself failed (${err instanceof Error ? err.message : String(err)})`,
23592
+ decision.receipt
23593
+ );
23594
+ }
23595
+ paid = await resolveSettlement(deps, buildBody, initialOutcome, persistQueuedReceipt);
23596
+ } else {
23597
+ paid = await payAndCreateSession(
23598
+ { ...deps, gasPreference: requestedGasPayer ?? deps.gasPreference },
23599
+ buildBody,
23600
+ persistQueuedReceipt
23601
+ );
23602
+ }
23603
+ } catch (rawErr) {
23604
+ const err = rawErr instanceof PrepareStageError ? rawErr.cause : rawErr;
23425
23605
  if (err instanceof PaymentReadinessError) return { error: `insufficient funds: ${err.message}` };
23426
23606
  if (err instanceof PaymentCapError) return { error: err.message };
23427
23607
  if (err instanceof NoPaymentOptionsError) return { error: err.message };
23608
+ if (err instanceof SettlementStillQueuedError) {
23609
+ try {
23610
+ await deps.sessionStore.set({
23611
+ sessionId: "",
23612
+ agentName,
23613
+ createdAt: deps.now().toISOString(),
23614
+ verificationUrl: "",
23615
+ paymentReceipt: err.paymentReceipt
23616
+ });
23617
+ } catch {
23618
+ return {
23619
+ error: err.message + ` (could not save the receipt locally \u2014 keep this value to resume manually: ${err.paymentReceipt})`
23620
+ };
23621
+ }
23622
+ return { error: err.message };
23623
+ }
23428
23624
  if (err instanceof SsivcError && err.kind === "blob_already_settled") {
23429
23625
  return { error: `payment already settled for this attempt: ${err.message}` };
23430
23626
  }
23627
+ if (err instanceof SettlementOutcomeUnknownError) {
23628
+ return {
23629
+ error: `could not determine whether the sponsored settlement succeeded or failed for payment receipt ${err.paymentReceipt} (${err.message}) \u2014 this is NOT a confirmed failure, so the receipt has been kept as-is and no new payment has been attempted; this requires manual investigation by an operator before retrying, rather than calling request_ai_birthcert_verification again`
23630
+ };
23631
+ }
23431
23632
  throw err;
23432
23633
  }
23433
23634
  await deps.sessionStore.set({
@@ -23447,6 +23648,12 @@ async function checkAiBirthcertVerification(deps) {
23447
23648
  message: "No verification session found for this wallet \u2014 call request_ai_birthcert_verification first."
23448
23649
  };
23449
23650
  }
23651
+ if (stored.sessionId === "") {
23652
+ return {
23653
+ status: "no_session",
23654
+ message: "a sponsored payment is still settling and no verification session exists yet \u2014 the payment receipt has been kept, so calling request_ai_birthcert_verification again resumes it and will not pay twice."
23655
+ };
23656
+ }
23450
23657
  const status = await deps.ssivc.getSession(stored.sessionId);
23451
23658
  if (status.status !== "issued" || !status.vcId) return status;
23452
23659
  if (deps.verifiedTemplateId && deps.cache) {
@@ -23623,7 +23830,12 @@ function buildToolList() {
23623
23830
  agentPurpose: { type: "string", description: 'Optional \u2014 what this agent does, e.g. "Negotiate and settle supplier invoices".' },
23624
23831
  evidenceAssuranceLevel: { type: "string", description: 'Optional \u2014 assurance level of the identity evidence, e.g. "high".' },
23625
23832
  ownerType: { type: "string", description: `Optional \u2014 the owner's type, e.g. "Individual".` },
23626
- ownerVerified: { type: "string", description: 'Optional \u2014 whether the owner is already verified, as the string "true" or "false".' }
23833
+ ownerVerified: { type: "string", description: 'Optional \u2014 whether the owner is already verified, as the string "true" or "false".' },
23834
+ gasPayer: {
23835
+ type: "string",
23836
+ enum: ["sponsored", "self"],
23837
+ 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.`
23838
+ }
23627
23839
  },
23628
23840
  required: ["agentName"]
23629
23841
  }
@@ -23652,7 +23864,9 @@ function buildToolList() {
23652
23864
  }
23653
23865
  function asPayRequest(accept) {
23654
23866
  const extra = accept.extra ?? {};
23655
- return { ...accept, extra: { gasModel: "client", ...extra } };
23867
+ const prepareEndpoint = extra.prepareEndpoint;
23868
+ const normalizedExtra = typeof prepareEndpoint === "string" && prepareEndpoint !== "" ? { ...extra, prepareEndpoint: prepareBaseUrl(prepareEndpoint) } : extra;
23869
+ return { ...accept, extra: { gasModel: "client", ...normalizedExtra } };
23656
23870
  }
23657
23871
  async function main() {
23658
23872
  const fileEnv = loadConfigFileEnv(process.argv, (p) => (0, import_node_fs.readFileSync)(p, "utf8"));
@@ -23754,7 +23968,7 @@ async function main() {
23754
23968
  }
23755
23969
  throw err;
23756
23970
  }
23757
- if (rawAsset !== "" && rawAsset !== "ZTX") {
23971
+ if (needsNativeGasCheck(accept)) {
23758
23972
  const gasBalance = await fetchNativeBalance(zetrixAddress).catch(() => null);
23759
23973
  if (gasBalance === "0") {
23760
23974
  const { symbol } = await resolveAssetInfo(rawAsset, contractQuery);
@@ -23799,7 +24013,9 @@ async function main() {
23799
24013
  sessionStore: ssivcSessionStore,
23800
24014
  verifiedTemplateId: config2.aiBirthcertVerifiedTemplateId,
23801
24015
  cache: vcCache,
23802
- quarantine: downloadQuarantine
24016
+ quarantine: downloadQuarantine,
24017
+ gasPreference: config2.gasPreference,
24018
+ maxSettlementAttempts: config2.maxSettlementAttempts
23803
24019
  };
23804
24020
  return {
23805
24021
  request: (input) => requestAiBirthcertVerification(verifyAiBirthcertDeps, input),
@@ -23894,5 +24110,6 @@ if (process.env.NODE_ENV !== "test") {
23894
24110
  }
23895
24111
  // Annotate the CommonJS export names for ESM import in node:
23896
24112
  0 && (module.exports = {
24113
+ asPayRequest,
23897
24114
  buildToolList
23898
24115
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-wallet-mcp",
3
- "version": "0.8.1",
3
+ "version": "0.9.1",
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",