agentic-wallet-mcp 0.8.1 → 0.9.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 CHANGED
@@ -8,6 +8,34 @@ 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.0] — 24 August 2026
12
+
13
+ ### Added
14
+
15
+ - **Paymaster-sponsored gas for `request_ai_birthcert_verification`.** When the resource server
16
+ offers a sponsored payment option (`extra.gasModel: "facilitator"`), the wallet now prefers it by
17
+ default — the ms-zetrix paymaster covers network gas, so a wallet holding the payment token but
18
+ zero ZTX can still complete the flow. Handles the server's asynchronous `202 Accepted` settlement
19
+ response: the wallet retries with the returned payment receipt (never re-sending the original
20
+ payment header) until the settlement confirms or the retry budget is exhausted, without ever
21
+ losing the receipt to a mid-retry crash or transient error.
22
+ - **Automatic self-pay fallback.** If sponsorship is refused before any money moved (the
23
+ paymaster pool is exhausted, rate-limited, or the network/asset isn't sponsorable), the wallet
24
+ falls back to self-pay automatically. It never falls back on an indeterminate outcome — a
25
+ payment that may still be settling is never retried as a fresh payment, which would risk paying
26
+ twice.
27
+ - **`gasPayer` parameter** on `request_ai_birthcert_verification` (`"sponsored"` | `"self"`) —
28
+ overrides the deployment default for a single call. An unrecognised value is treated as absent
29
+ rather than silently forcing self-pay.
30
+ - **`GAS_PREFERENCE`** env var (default `sponsored`) — deployment-wide default gas payer.
31
+ - **`MAX_SETTLEMENT_ATTEMPTS`** env var (default `20`) — cap on retry attempts while polling a
32
+ queued sponsored settlement.
33
+
34
+ ### Changed
35
+
36
+ - Existing self-pay behaviour is unchanged — a resource server that only quotes self-pay (no
37
+ sponsored option) behaves identically to before this release.
38
+
11
39
  ## [0.8.1] — 19 August 2026
12
40
 
13
41
  ### 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.0",
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,6 +23402,7 @@ 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 };
@@ -23385,12 +23438,107 @@ function withRequestLock(fn) {
23385
23438
  }
23386
23439
  var NoPaymentOptionsError = class extends Error {
23387
23440
  };
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);
23441
+ var DEFAULT_MAX_SETTLEMENT_ATTEMPTS = 20;
23442
+ var MAX_RETRY_DELAY_MS = 6e4;
23443
+ var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
23444
+ var SettlementStillQueuedError = class extends Error {
23445
+ constructor(message, paymentReceipt) {
23446
+ super(message);
23447
+ this.paymentReceipt = paymentReceipt;
23448
+ this.name = "SettlementStillQueuedError";
23449
+ }
23450
+ };
23451
+ var SettlementOutcomeUnknownError = class extends Error {
23452
+ constructor(message, paymentReceipt) {
23453
+ super(message);
23454
+ this.paymentReceipt = paymentReceipt;
23455
+ this.name = "SettlementOutcomeUnknownError";
23456
+ }
23457
+ };
23458
+ async function resolveSettlement(deps, buildBody, initialOutcome, onQueued) {
23459
+ const sleep = deps.sleep ?? defaultSleep;
23460
+ const maxAttempts = deps.maxSettlementAttempts ?? DEFAULT_MAX_SETTLEMENT_ATTEMPTS;
23461
+ let outcome = initialOutcome;
23462
+ let lastPersistedReceipt;
23463
+ if (outcome.kind === "queued") {
23464
+ lastPersistedReceipt = outcome.paymentReceipt;
23465
+ await onQueued(outcome.paymentReceipt);
23466
+ }
23467
+ for (let attempt = 0; outcome.kind === "queued" && attempt < maxAttempts; attempt++) {
23468
+ const delayMs = Math.min(outcome.retryAfterSeconds * 1e3, MAX_RETRY_DELAY_MS);
23469
+ await sleep(delayMs);
23470
+ const receiptSent = outcome.paymentReceipt;
23471
+ try {
23472
+ outcome = await deps.ssivc.createSessionWithReceipt(await buildBody(), receiptSent);
23473
+ } catch (err) {
23474
+ throw new SettlementOutcomeUnknownError(
23475
+ `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)})`,
23476
+ receiptSent
23477
+ );
23478
+ }
23479
+ if (outcome.kind === "queued" && outcome.paymentReceipt !== lastPersistedReceipt) {
23480
+ lastPersistedReceipt = outcome.paymentReceipt;
23481
+ await onQueued(outcome.paymentReceipt);
23482
+ }
23483
+ }
23484
+ if (outcome.kind === "queued") {
23485
+ throw new SettlementStillQueuedError(
23486
+ "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",
23487
+ outcome.paymentReceipt
23488
+ );
23489
+ }
23490
+ return { session: outcome.session, paymentReceipt: outcome.paymentReceipt };
23491
+ }
23492
+ var DEFINITIVE_PREPARE_REFUSALS = /* @__PURE__ */ new Set([
23493
+ 461411,
23494
+ // X402_UNSUPPORTED_NETWORK — this network is not sponsored
23495
+ 461414,
23496
+ // X402_RATE_LIMIT_EXCEEDED — pool rate limit, or the per-address active-prepare cap (default 5)
23497
+ 461415
23498
+ // X402_UNSUPPORTED_ASSET — sponsorship is ZTP-20 only
23499
+ ]);
23500
+ function facilitatorErrorCode(err) {
23501
+ const m = /"errorCode"\s*:\s*(\d{6})/.exec(err instanceof Error ? err.message : String(err));
23502
+ return m ? Number(m[1]) : void 0;
23503
+ }
23504
+ var PrepareStageError = class extends Error {
23505
+ constructor(cause) {
23506
+ super("prepare stage failed");
23507
+ this.cause = cause;
23508
+ this.name = "PrepareStageError";
23509
+ }
23510
+ };
23511
+ function isDefinitiveSponsorshipFailure(err) {
23512
+ if (!(err instanceof PrepareStageError)) return false;
23513
+ const cause = err.cause;
23514
+ if (cause instanceof SettlementStillQueuedError) return false;
23515
+ const code = facilitatorErrorCode(cause);
23516
+ if (code === void 0) return false;
23517
+ return DEFINITIVE_PREPARE_REFUSALS.has(code);
23518
+ }
23519
+ async function attemptCandidate(deps, accept, buildBody, onQueued) {
23520
+ let xPayment;
23521
+ try {
23522
+ xPayment = await deps.pay(accept);
23523
+ } catch (err) {
23524
+ throw new PrepareStageError(err);
23525
+ }
23526
+ const initialOutcome = await deps.ssivc.createSessionSettle(await buildBody(), xPayment);
23527
+ return resolveSettlement(deps, buildBody, initialOutcome, onQueued);
23528
+ }
23529
+ async function payAndCreateSession(deps, buildBody, onQueued) {
23530
+ const challenge = await deps.ssivc.createSessionChallenge(await buildBody());
23531
+ const candidates = orderAccepts(challenge.accepts, deps.gasPreference ?? "sponsored");
23532
+ const primary = candidates[0];
23533
+ if (!primary) throw new NoPaymentOptionsError("SSIVC 402 returned no usable payment options");
23534
+ try {
23535
+ return await attemptCandidate(deps, primary, buildBody, onQueued);
23536
+ } catch (err) {
23537
+ const fallback = candidates[1];
23538
+ if (!fallback || !isSponsored(primary) || isSponsored(fallback)) throw err;
23539
+ if (!isDefinitiveSponsorshipFailure(err)) throw err;
23540
+ return await attemptCandidate(deps, fallback, buildBody, onQueued);
23541
+ }
23394
23542
  }
23395
23543
  async function requestAiBirthcertVerification(deps, input) {
23396
23544
  if (!input.agentName || !input.agentName.trim()) {
@@ -23403,31 +23551,85 @@ async function requestAiBirthcertVerificationLocked(deps, agentName, input) {
23403
23551
  const decision = await decidePriorSession(deps, agentName);
23404
23552
  if (decision.kind === "still_pending") return decision.result;
23405
23553
  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
23554
+ const buildBody = async () => {
23555
+ const fields = {
23556
+ publicKey: deps.publicKeyHex,
23557
+ address: deps.address,
23558
+ timestamp: isoSeconds(deps.now()),
23559
+ agentName,
23560
+ id: agentName,
23561
+ ownerReference: deps.holderDid
23562
+ };
23563
+ if (input.agentPurpose) fields.agentPurpose = input.agentPurpose;
23564
+ if (input.evidenceAssuranceLevel) fields.evidenceAssuranceLevel = input.evidenceAssuranceLevel;
23565
+ if (input.ownerType) fields.ownerType = input.ownerType;
23566
+ if (input.ownerVerified) fields.ownerVerified = input.ownerVerified;
23567
+ const digestHex = (0, import_node_crypto4.createHash)("sha256").update(canonicalizeJson(fields), "utf8").digest("hex");
23568
+ const { signBlob: signedData } = await deps.signHexBlob(digestHex);
23569
+ return { ...fields, signedData };
23570
+ };
23571
+ const persistQueuedReceipt = async (receipt) => {
23572
+ try {
23573
+ await deps.sessionStore.set({
23574
+ sessionId: "",
23575
+ agentName,
23576
+ createdAt: deps.now().toISOString(),
23577
+ verificationUrl: "",
23578
+ paymentReceipt: receipt
23579
+ });
23580
+ } catch {
23581
+ }
23413
23582
  };
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 };
23583
+ const requestedGasPayer = input.gasPayer === "self" || input.gasPayer === "sponsored" ? input.gasPayer : void 0;
23421
23584
  let paid;
23422
23585
  try {
23423
- paid = decision.kind === "replay_receipt" ? await deps.ssivc.createSessionWithReceipt(body, decision.receipt) : await payAndCreateSession(deps, body);
23424
- } catch (err) {
23586
+ if (decision.kind === "replay_receipt") {
23587
+ let initialOutcome;
23588
+ try {
23589
+ initialOutcome = await deps.ssivc.createSessionWithReceipt(await buildBody(), decision.receipt);
23590
+ } catch (err) {
23591
+ throw new SettlementOutcomeUnknownError(
23592
+ `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)})`,
23593
+ decision.receipt
23594
+ );
23595
+ }
23596
+ paid = await resolveSettlement(deps, buildBody, initialOutcome, persistQueuedReceipt);
23597
+ } else {
23598
+ paid = await payAndCreateSession(
23599
+ { ...deps, gasPreference: requestedGasPayer ?? deps.gasPreference },
23600
+ buildBody,
23601
+ persistQueuedReceipt
23602
+ );
23603
+ }
23604
+ } catch (rawErr) {
23605
+ const err = rawErr instanceof PrepareStageError ? rawErr.cause : rawErr;
23425
23606
  if (err instanceof PaymentReadinessError) return { error: `insufficient funds: ${err.message}` };
23426
23607
  if (err instanceof PaymentCapError) return { error: err.message };
23427
23608
  if (err instanceof NoPaymentOptionsError) return { error: err.message };
23609
+ if (err instanceof SettlementStillQueuedError) {
23610
+ try {
23611
+ await deps.sessionStore.set({
23612
+ sessionId: "",
23613
+ agentName,
23614
+ createdAt: deps.now().toISOString(),
23615
+ verificationUrl: "",
23616
+ paymentReceipt: err.paymentReceipt
23617
+ });
23618
+ } catch {
23619
+ return {
23620
+ error: err.message + ` (could not save the receipt locally \u2014 keep this value to resume manually: ${err.paymentReceipt})`
23621
+ };
23622
+ }
23623
+ return { error: err.message };
23624
+ }
23428
23625
  if (err instanceof SsivcError && err.kind === "blob_already_settled") {
23429
23626
  return { error: `payment already settled for this attempt: ${err.message}` };
23430
23627
  }
23628
+ if (err instanceof SettlementOutcomeUnknownError) {
23629
+ return {
23630
+ 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`
23631
+ };
23632
+ }
23431
23633
  throw err;
23432
23634
  }
23433
23635
  await deps.sessionStore.set({
@@ -23447,6 +23649,12 @@ async function checkAiBirthcertVerification(deps) {
23447
23649
  message: "No verification session found for this wallet \u2014 call request_ai_birthcert_verification first."
23448
23650
  };
23449
23651
  }
23652
+ if (stored.sessionId === "") {
23653
+ return {
23654
+ status: "no_session",
23655
+ 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."
23656
+ };
23657
+ }
23450
23658
  const status = await deps.ssivc.getSession(stored.sessionId);
23451
23659
  if (status.status !== "issued" || !status.vcId) return status;
23452
23660
  if (deps.verifiedTemplateId && deps.cache) {
@@ -23623,7 +23831,12 @@ function buildToolList() {
23623
23831
  agentPurpose: { type: "string", description: 'Optional \u2014 what this agent does, e.g. "Negotiate and settle supplier invoices".' },
23624
23832
  evidenceAssuranceLevel: { type: "string", description: 'Optional \u2014 assurance level of the identity evidence, e.g. "high".' },
23625
23833
  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".' }
23834
+ ownerVerified: { type: "string", description: 'Optional \u2014 whether the owner is already verified, as the string "true" or "false".' },
23835
+ gasPayer: {
23836
+ type: "string",
23837
+ enum: ["sponsored", "self"],
23838
+ 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.`
23839
+ }
23627
23840
  },
23628
23841
  required: ["agentName"]
23629
23842
  }
@@ -23652,7 +23865,9 @@ function buildToolList() {
23652
23865
  }
23653
23866
  function asPayRequest(accept) {
23654
23867
  const extra = accept.extra ?? {};
23655
- return { ...accept, extra: { gasModel: "client", ...extra } };
23868
+ const prepareEndpoint = extra.prepareEndpoint;
23869
+ const normalizedExtra = typeof prepareEndpoint === "string" && prepareEndpoint !== "" ? { ...extra, prepareEndpoint: prepareBaseUrl(prepareEndpoint) } : extra;
23870
+ return { ...accept, extra: { gasModel: "client", ...normalizedExtra } };
23656
23871
  }
23657
23872
  async function main() {
23658
23873
  const fileEnv = loadConfigFileEnv(process.argv, (p) => (0, import_node_fs.readFileSync)(p, "utf8"));
@@ -23754,7 +23969,7 @@ async function main() {
23754
23969
  }
23755
23970
  throw err;
23756
23971
  }
23757
- if (rawAsset !== "" && rawAsset !== "ZTX") {
23972
+ if (needsNativeGasCheck(accept)) {
23758
23973
  const gasBalance = await fetchNativeBalance(zetrixAddress).catch(() => null);
23759
23974
  if (gasBalance === "0") {
23760
23975
  const { symbol } = await resolveAssetInfo(rawAsset, contractQuery);
@@ -23799,7 +24014,9 @@ async function main() {
23799
24014
  sessionStore: ssivcSessionStore,
23800
24015
  verifiedTemplateId: config2.aiBirthcertVerifiedTemplateId,
23801
24016
  cache: vcCache,
23802
- quarantine: downloadQuarantine
24017
+ quarantine: downloadQuarantine,
24018
+ gasPreference: config2.gasPreference,
24019
+ maxSettlementAttempts: config2.maxSettlementAttempts
23803
24020
  };
23804
24021
  return {
23805
24022
  request: (input) => requestAiBirthcertVerification(verifyAiBirthcertDeps, input),
@@ -23894,5 +24111,6 @@ if (process.env.NODE_ENV !== "test") {
23894
24111
  }
23895
24112
  // Annotate the CommonJS export names for ESM import in node:
23896
24113
  0 && (module.exports = {
24114
+ asPayRequest,
23897
24115
  buildToolList
23898
24116
  });
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.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",