@supercuts-walrus/backend 0.1.0 → 0.1.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/dist/mcp.js CHANGED
@@ -8,13 +8,16 @@ import { DealJournal } from "./deal.js";
8
8
  import { createConfiguredDealEngine } from "./deal-adapter.js";
9
9
  import { LiveDealPorts } from "./live-deal-ports.js";
10
10
  import { createRequire } from "node:module";
11
+ import { fail } from "./errors.js";
12
+ import { acquireRoleHomeLease } from "./role-lock.js";
11
13
  const PACKAGE_VERSION = createRequire(import.meta.url)("../package.json").version;
12
14
  const MAX_REQUEST_BYTES = 1024 * 1024;
13
15
  const home = homePath({});
14
16
  const app = new Phase3App(home);
17
+ const roleLease = acquireRoleHomeLease(home);
15
18
  const mandate = loadMandate(home);
16
- const remote = app.profile.role === "buyer" && !app.profile.mock && process.env.SUPERCUTS_SELLER_URL ? new RemoteBuyer(app, mandate, process.env.SUPERCUTS_SELLER_URL) : undefined;
17
- const eventLog = process.env.SUPERCUTS_EVENT_PATH && process.env.SUPERCUTS_RUN_ID ? new DemoEventLog(process.env.SUPERCUTS_EVENT_PATH, process.env.SUPERCUTS_RUN_ID) : undefined;
19
+ const remote = roleLease.acquired && app.profile.role === "buyer" && !app.profile.mock && process.env.SUPERCUTS_SELLER_URL ? new RemoteBuyer(app, mandate, process.env.SUPERCUTS_SELLER_URL) : undefined;
20
+ const eventLog = roleLease.acquired && process.env.SUPERCUTS_EVENT_PATH && process.env.SUPERCUTS_RUN_ID ? new DemoEventLog(process.env.SUPERCUTS_EVENT_PATH, process.env.SUPERCUTS_RUN_ID) : undefined;
18
21
  if (eventLog)
19
22
  eventLog.append({ actor: "operator", stage: "run", status: "started", publicDetails: { flow: remote ? "codex-remote" : "codex-deal-v2" }, evidenceHashes: [], explorerLinks: [] });
20
23
  let memoryTickRunning = false;
@@ -32,7 +35,7 @@ const memoryTick = async () => {
32
35
  memoryTickRunning = false;
33
36
  }
34
37
  };
35
- const memoryConfigured = app.profile.role === "buyer" && !app.profile.mock && Boolean(app.store.getSecret("live:memory-config"));
38
+ const memoryConfigured = roleLease.acquired && app.profile.role === "buyer" && !app.profile.mock && Boolean(app.store.getSecret("live:memory-config"));
36
39
  if (memoryConfigured)
37
40
  app.recoverStaleMemorySubmissions();
38
41
  const memoryTimer = memoryConfigured ? setInterval(() => { void memoryTick(); }, 30_000) : undefined;
@@ -41,7 +44,7 @@ if (memoryTimer) {
41
44
  void memoryTick();
42
45
  }
43
46
  const tools = {
44
- buyer: "status,doctor,deal.catalog,deal.policy,deal.execute,deal.get,deal.resume,deal.reconcile,deal.evidence,deal.delivery,capability.discover,settlement.plan,wallet.deposit,wallet.balance,wallet.recover,wallet.withdraw,wallet.exit.queue,wallet.exit.status,wallet.exit.finalize,offer.open,offer.propose,offer.accept,offer.show,pay.quote,pay.shielded,pay.x402,pay.status,resource.fetch,memory.remember,memory.recall,memory.status,memory.inventory,remote.catalog,remote.rank-briefs,remote.negotiate,remote.quote,remote.purchase-brief,remote.agreement,remote.settle-analysis,remote.delivery,remote.events,disclose",
47
+ buyer: "status,doctor,deal.catalog,deal.policy,deal.execute,deal.get,deal.resume,deal.reconcile,deal.evidence,deal.delivery,capability.discover,settlement.plan,wallet.deposit,wallet.balance,wallet.recover,wallet.withdraw,wallet.exit.queue,wallet.exit.status,wallet.exit.finalize,offer.open,offer.propose,offer.accept,offer.show,pay.quote,pay.shielded,pay.x402,pay.status,resource.fetch,memory.remember,memory.recall,memory.status,memory.inventory,remote.catalog,remote.rank-briefs,remote.negotiate,remote.quote,remote.purchase-brief,remote.agreement,remote.settle-analysis,remote.delivery,remote.events,remote.research,disclose",
45
48
  seller: "status,doctor,capability.publish,wallet.balance,wallet.recover,wallet.withdraw,wallet.exit.queue,wallet.exit.status,wallet.exit.finalize,offer.counter,offer.show,pay.status,resource.serve,resource.fetch,memory.remember,memory.recall,memory.status",
46
49
  auditor: "status,doctor,audit.verify",
47
50
  };
@@ -50,6 +53,16 @@ function logTool(name, data) {
50
53
  if (!eventLog)
51
54
  return;
52
55
  const append = (actor, stage, details, hashes = []) => eventLog.append({ actor, stage, status: "complete", publicDetails: details, evidenceHashes: hashes, explorerLinks: [] });
56
+ if (name === "remote.research") {
57
+ if (data.replayed === true)
58
+ return;
59
+ logTool("remote.rank-briefs", data.selection);
60
+ logTool("remote.purchase-brief", data.brief);
61
+ logTool("remote.agreement", data.agreement);
62
+ logTool("remote.settle-analysis", data.settlement);
63
+ logTool("remote.delivery", data.analysis);
64
+ return;
65
+ }
53
66
  if (name === "remote.rank-briefs")
54
67
  append("buyer", "seller-selection", { winner: data.winner?.capabilityId, rankings: data.rankings?.map((item) => ({ capabilityId: item.capabilityId, score: item.score, priceMist: item.priceMist })) });
55
68
  if (name === "remote.negotiate")
@@ -108,7 +121,7 @@ rl.on("line", (line) => {
108
121
  return send({ jsonrpc: "2.0", id: request.id, result: { protocolVersion: "2025-06-18", capabilities: { tools: {} }, serverInfo: { name: `supercuts-${app.profile.role}`, version: PACKAGE_VERSION } } });
109
122
  if (request.method === "notifications/initialized")
110
123
  return;
111
- const available = tools[app.profile.role].split(",").filter((name) => (remote || !name.startsWith("remote.")) && (app.profile.mock || memoryConfigured || !name.startsWith("memory.")) && (!name.startsWith("deal.") || mandate.version === "supercuts.mandate.v2") && (["status", "doctor"].includes(name) || mandate.allowedCommands.includes(name === "deal.reconcile" ? "deal.resume" : name)));
124
+ const available = tools[app.profile.role].split(",").filter((name) => (roleLease.acquired || name === "status") && (remote || !name.startsWith("remote.")) && (app.profile.mock || memoryConfigured || !name.startsWith("memory.")) && (!name.startsWith("deal.") || mandate.version === "supercuts.mandate.v2") && (["status", "doctor"].includes(name) || mandate.allowedCommands.includes(name === "deal.reconcile" ? "deal.resume" : name)));
112
125
  if (request.method === "tools/list")
113
126
  return send({ jsonrpc: "2.0", id: request.id, result: { tools: available.map((name) => { const spec = toolSchema(name); return { name, description: spec.description, inputSchema: spec.inputSchema }; }) } });
114
127
  if (request.method === "tools/call") {
@@ -123,7 +136,7 @@ rl.on("line", (line) => {
123
136
  const { validateToolArguments } = await import("./mandate.js");
124
137
  validateToolArguments(spec, raw);
125
138
  if (mandate.version !== "supercuts.mandate.v2" || !mandate.allowedCommands.includes(name === "deal.reconcile" ? "deal.resume" : name) || (name === "deal.execute" || name === "deal.resume") && Date.now() > mandate.expiresAt)
126
- throw new Error("deal tool is not authorized by an active v2 mandate");
139
+ fail("DEAL_MANDATE_INACTIVE", "deal tool is not authorized by an active v2 mandate");
127
140
  if (name === "deal.catalog")
128
141
  data = { capabilities: (await new LiveDealPorts(app, mandate).catalog()).map(({ origin, registration }) => ({ origin, manifest: registration.manifest, manifestBlobId: registration.manifestBlobId })) };
129
142
  else if (name === "deal.policy") {
@@ -151,25 +164,31 @@ rl.on("line", (line) => {
151
164
  data = new DealJournal(app.store).get(String(raw.operationId));
152
165
  }
153
166
  else if (name.startsWith("remote.")) {
154
- if (!remote || !mandate.allowedCommands.includes(name) || Date.now() > mandate.expiresAt)
155
- throw new Error("remote buyer tool is not authorized or configured");
167
+ const buyer = remote ?? fail("REMOTE_MANDATE_INACTIVE", "remote buyer tool is not configured");
168
+ if (!mandate.allowedCommands.includes(name) || Date.now() > mandate.expiresAt)
169
+ fail("REMOTE_MANDATE_INACTIVE", "remote buyer tool is not authorized by an active mandate");
170
+ if (!["remote.catalog", "remote.rank-briefs"].includes(name) && raw.runId === undefined && process.env.SUPERCUTS_RUN_ID)
171
+ raw.runId = process.env.SUPERCUTS_RUN_ID;
156
172
  const spec = toolSchema(name);
157
173
  const { validateToolArguments } = await import("./mandate.js");
158
174
  validateToolArguments(spec, raw);
159
175
  if (process.env.SUPERCUTS_RUN_ID && name !== "remote.catalog" && name !== "remote.rank-briefs" && raw.runId !== undefined && raw.runId !== process.env.SUPERCUTS_RUN_ID)
160
- throw new Error("remote tool run ID differs from this buyer demo run");
161
- data = name === "remote.catalog" ? await remote.catalog() :
162
- name === "remote.rank-briefs" ? await remote.rankBriefs(raw) :
163
- name === "remote.negotiate" ? await remote.negotiate(raw) :
164
- name === "remote.quote" ? await remote.quote(raw) :
165
- name === "remote.purchase-brief" ? await remote.purchaseBrief(raw) :
166
- name === "remote.agreement" ? await remote.agreement(raw) :
167
- name === "remote.settle-analysis" ? await remote.settleAnalysis(raw) :
168
- name === "remote.delivery" ? await remote.delivery(raw) :
169
- name === "remote.events" ? await remote.events(String(raw.runId)) : {};
176
+ fail("SESSION_MISMATCH", "remote tool run ID differs from the active buyer session");
177
+ data = name === "remote.catalog" ? await buyer.catalog() :
178
+ name === "remote.rank-briefs" ? await buyer.rankBriefs(raw) :
179
+ name === "remote.negotiate" ? await buyer.negotiate(raw) :
180
+ name === "remote.quote" ? await buyer.quote(raw) :
181
+ name === "remote.purchase-brief" ? await buyer.purchaseBrief(raw) :
182
+ name === "remote.agreement" ? await buyer.agreement(raw) :
183
+ name === "remote.settle-analysis" ? await buyer.settleAnalysis(raw) :
184
+ name === "remote.delivery" ? await buyer.delivery(raw) :
185
+ name === "remote.events" ? await buyer.events(String(raw.runId)) :
186
+ name === "remote.research" ? await buyer.research({ operationId: String(raw["operation-id"]), runId: String(raw.runId), topic: String(raw.topic), budget: String(raw.budget) }) : {};
170
187
  }
171
188
  else
172
189
  data = await executeMandated(app, mandate, name, raw);
190
+ if (name === "status")
191
+ data = { ...data, session: { runId: process.env.SUPERCUTS_RUN_ID ?? null }, roleHomeLease: roleLease.acquired ? "acquired" : "busy", ...(roleLease.ownerPid ? { roleHomeOwnerPid: roleLease.ownerPid } : {}) };
173
192
  logTool(name, data);
174
193
  const result = { schemaVersion: "supercuts.v1", ok: true, command: name, data };
175
194
  return send({ jsonrpc: "2.0", id: request.id, result: { content: [{ type: "text", text: JSON.stringify(result) }], structuredContent: result } });
@@ -182,6 +201,7 @@ rl.on("line", (line) => {
182
201
  }).catch((error) => send({ jsonrpc: "2.0", error: { code: -32603, message: error instanceof Error ? error.message : "Internal error" }, id: null }));
183
202
  });
184
203
  const close = () => { if (memoryTimer)
185
- clearInterval(memoryTimer); app.close(); process.exit(0); };
204
+ clearInterval(memoryTimer); roleLease.release(); app.close(); process.exit(0); };
186
205
  process.on("SIGINT", close);
187
206
  process.on("SIGTERM", close);
207
+ process.on("exit", () => roleLease.release());
@@ -42,8 +42,19 @@ export async function onboardingDoctor(home, sellerConfig) {
42
42
  }
43
43
  const endpoint = app.profile.live?.writeGrpcEndpoint ?? fail("LIVE_PROFILE_INVALID", "buyer write RPC is missing");
44
44
  const signer = String(runtime.data.signer ?? "");
45
- const { balance } = await writeClient(endpoint).getBalance({ owner: signer, coinType: "0x2::sui::SUI" });
46
- return { role: "buyer", healthy: true, runtime: runtime.data, approvedSellers: mandate.allowedCounterparties.length, nativeSuiMist: String(balance.balance), funded: BigInt(balance.balance) > 0n, ...(directoryListings !== undefined ? { directoryListings } : {}) };
45
+ const [{ balance }, shielded] = await Promise.all([
46
+ writeClient(endpoint).getBalance({ owner: signer, coinType: "0x2::sui::SUI" }),
47
+ app.execute("wallet.balance", {}),
48
+ ]);
49
+ const nativeSuiMist = String(balance.balance), shieldedMist = String(shielded.data.available ?? "0");
50
+ const checks = [
51
+ { id: "runtime", status: "ready", detail: "Pinned deployment, prover, RPC, and Walrus prerequisites verified." },
52
+ { id: "public-balance", status: BigInt(nativeSuiMist) > 0n ? "ready" : "action_required", detail: BigInt(nativeSuiMist) > 0n ? `${nativeSuiMist} MIST available.` : "Fund only this generated buyer address before a public payment." },
53
+ { id: "shielded-balance", status: BigInt(shieldedMist) > 0n ? "ready" : "action_required", detail: BigInt(shieldedMist) > 0n ? `${shieldedMist} MIST available in the experimental pool.` : "Create a buyer-owned pool deposit before a shielded deal." },
54
+ { id: "seller-approval", status: mandate.allowedCounterparties.length > 0 ? "ready" : "action_required", detail: mandate.allowedCounterparties.length > 0 ? `${mandate.allowedCounterparties.length} seller identity or identities approved.` : "Review and approve a signed seller capability locally." },
55
+ ...(directoryListings !== undefined ? [{ id: "directory", status: directoryListings > 0 ? "ready" : "action_required", detail: `${directoryListings} live directory listing or listings.` }] : []),
56
+ ];
57
+ return { role: "buyer", healthy: true, readyForPublicPayment: BigInt(nativeSuiMist) > 0n && mandate.allowedCounterparties.length > 0, readyForShieldedPayment: BigInt(shieldedMist) > 0n && mandate.allowedCounterparties.length > 0, runtime: runtime.data, checks, approvedSellers: mandate.allowedCounterparties.length, nativeSuiMist, shieldedMist, funded: BigInt(nativeSuiMist) > 0n, ...(directoryListings !== undefined ? { directoryListings } : {}) };
47
58
  }
48
59
  if (app.profile.role === "seller") {
49
60
  if (!sellerConfig)
package/dist/phase3.js CHANGED
@@ -19,7 +19,7 @@ const V2_RELAYER = "0x405f473ed1f7f609b16d3d7dabc7cd04831cfc73b4a7fcebe9fa502d72
19
19
  const COIN = "0x2::sui::SUI";
20
20
  const WRITE = new Set(["wallet.deposit", "wallet.recover", "wallet.withdraw", "wallet.exit.queue", "wallet.exit.finalize", "offer.open", "offer.propose", "offer.counter", "offer.accept", "pay.shielded", "pay.x402", "resource.serve", "memory.remember", "disclose", "capability.publish", "settlement.plan", "settlement.approve-review", "memory.policy"]);
21
21
  const ROLE_COMMANDS = {
22
- buyer: new Set(["status", "doctor", "capability.discover", "settlement.plan", "settlement.approve-review", "memory.policy", "wallet.deposit", "wallet.balance", "wallet.recover", "wallet.withdraw", "wallet.exit.queue", "wallet.exit.status", "wallet.exit.finalize", "offer.open", "offer.propose", "offer.accept", "offer.show", "pay.quote", "pay.shielded", "pay.x402", "pay.status", "resource.fetch", "memory.remember", "memory.recall", "memory.status", "memory.inventory", "remote.catalog", "remote.rank-briefs", "remote.negotiate", "remote.quote", "remote.purchase-brief", "remote.agreement", "remote.settle-analysis", "remote.delivery", "remote.events", "disclose"]),
22
+ buyer: new Set(["status", "doctor", "capability.discover", "settlement.plan", "settlement.approve-review", "memory.policy", "wallet.deposit", "wallet.balance", "wallet.recover", "wallet.withdraw", "wallet.exit.queue", "wallet.exit.status", "wallet.exit.finalize", "offer.open", "offer.propose", "offer.accept", "offer.show", "pay.quote", "pay.shielded", "pay.x402", "pay.status", "resource.fetch", "memory.remember", "memory.recall", "memory.status", "memory.inventory", "remote.catalog", "remote.rank-briefs", "remote.negotiate", "remote.quote", "remote.purchase-brief", "remote.agreement", "remote.settle-analysis", "remote.delivery", "remote.events", "remote.research", "disclose"]),
23
23
  seller: new Set(["status", "doctor", "capability.publish", "wallet.balance", "wallet.recover", "wallet.withdraw", "wallet.exit.queue", "wallet.exit.status", "wallet.exit.finalize", "offer.counter", "offer.show", "pay.status", "resource.serve", "resource.fetch", "memory.remember", "memory.recall", "memory.status", "memory.inventory"]),
24
24
  auditor: new Set(["status", "doctor", "audit.verify"]),
25
25
  };
@@ -444,6 +444,18 @@ export class Phase3App {
444
444
  fail("CAPABILITY_LIST_INVALID", "alternatives must be an array");
445
445
  for (const item of alternatives)
446
446
  validateCapabilityManifest(item);
447
+ const operationId = required(args, "operation-id"), mandate = this.planningMandate(args);
448
+ const persisted = this.store.db.prepare("SELECT plan_hash FROM settlement_plans WHERE operation_id=?").get(operationId);
449
+ if (persisted) {
450
+ const existing = this.storedPlan(persisted.plan_hash), manifests = [primary, ...alternatives];
451
+ const requestedAmount = typeof args["requested-amount"] === "string" ? BigInt(args["requested-amount"]) : BigInt(existing.manifestMinimum);
452
+ const boundedAmount = requestedAmount < BigInt(existing.manifestMinimum) ? BigInt(existing.manifestMinimum) : requestedAmount > BigInt(existing.maximumAmount) ? BigInt(existing.maximumAmount) : requestedAmount;
453
+ const requestedRail = args["requested-rail"] === "x402" || args["requested-rail"] === "shielded" ? args["requested-rail"] : undefined;
454
+ if (existing.mandateHash !== settlementMandateHash(mandate) || !manifests.some((manifest) => manifest.manifestHash === existing.manifestHash) || requestedRail && requestedRail !== existing.rail || args["privacy-hint"] === "shielded" && existing.rail !== "shielded" || boundedAmount.toString() !== existing.openingAmount) {
455
+ fail("OPERATION_ID_CONFLICT", "planning operation already identifies another immutable plan");
456
+ }
457
+ return existing;
458
+ }
447
459
  let recalled = this.recalledMockMemories();
448
460
  if (!this.profile.mock && this.store.getSecret("live:memory-config")) {
449
461
  const encoded = this.store.getSecret("live:memory-config");
@@ -467,7 +479,7 @@ export class Phase3App {
467
479
  manual.destroy();
468
480
  }
469
481
  }
470
- const plan = createSettlementPlan({ operationId: required(args, "operation-id"), mandate: this.planningMandate(args), manifests: [primary, ...alternatives], ...(typeof args["requested-amount"] === "string" ? { requestedAmount: args["requested-amount"] } : {}), ...(args["privacy-hint"] === "shielded" ? { privacyHint: "shielded" } : {}), ...(args["requested-rail"] === "x402" || args["requested-rail"] === "shielded" ? { requestedRail: args["requested-rail"] } : {}), recalled, maxMemoryDistance: this.profile.policy.maxMemoryDistance ?? 0.35 });
482
+ const plan = createSettlementPlan({ operationId, mandate, manifests: [primary, ...alternatives], ...(typeof args["requested-amount"] === "string" ? { requestedAmount: args["requested-amount"] } : {}), ...(args["privacy-hint"] === "shielded" ? { privacyHint: "shielded" } : {}), ...(args["requested-rail"] === "x402" || args["requested-rail"] === "shielded" ? { requestedRail: args["requested-rail"] } : {}), recalled, maxMemoryDistance: this.profile.policy.maxMemoryDistance ?? 0.35 });
471
483
  return this.persistPlan(plan);
472
484
  }
473
485
  typedMemory(args, provenance) {
@@ -691,8 +703,6 @@ export class Phase3App {
691
703
  const plan = this.storedPlan(supplied), rail = command === "pay.x402" ? "x402" : "shielded", now = Date.now();
692
704
  if (plan.status !== "approved" || plan.riskLevel === "review_required")
693
705
  fail("SETTLEMENT_PLAN_REVIEW_REQUIRED", "settlement plan requires owner review");
694
- if (now >= plan.expiresAt)
695
- fail("SETTLEMENT_PLAN_EXPIRED", "settlement plan has expired");
696
706
  if (plan.mandateHash !== settlementMandateHash(mandate))
697
707
  fail("SETTLEMENT_PLAN_MANDATE_MISMATCH", "owner mandate changed after planning");
698
708
  const operationId = required(args, "operation-id");
@@ -712,6 +722,8 @@ export class Phase3App {
712
722
  const bindingHash = hash(canonicalJson({ domain: "supercuts-settlement-execution-v1", binding })), existing = this.store.db.prepare("SELECT binding_hash FROM settlement_execution_bindings WHERE operation_id=? OR plan_hash=?").get(operationId, plan.planHash);
713
723
  if (existing && existing.binding_hash !== bindingHash)
714
724
  fail("SETTLEMENT_PLAN_CONSUMED", "plan or operation is already bound to another execution");
725
+ if (!existing && now >= plan.expiresAt)
726
+ fail("SETTLEMENT_PLAN_EXPIRED", "settlement plan has expired");
715
727
  if (!existing) {
716
728
  this.store.db.prepare("INSERT INTO settlement_execution_bindings(operation_id,plan_hash,binding_hash,binding_json,created_at) VALUES(?,?,?,?,?)").run(operationId, plan.planHash, bindingHash, canonicalJson(binding), now);
717
729
  this.store.db.prepare("UPDATE settlement_plans SET consumed_at=? WHERE plan_hash=?").run(now, plan.planHash);
@@ -819,6 +831,8 @@ export class Phase3App {
819
831
  this.enqueueVerifiedOutcome("settlement", required(args, "operation-id"), evidence, typeof args["plan-hash"] === "string" ? args["plan-hash"] : undefined, { counterparty: String(receipt.counterparty), asset: String(receipt.asset) });
820
832
  return output(command, receipt);
821
833
  }
834
+ if (command === "pay.status")
835
+ return output(command, await runtime.paymentStatus(required(args, "operation-id"), args.reconcile === true));
822
836
  if (command === "resource.serve") {
823
837
  const requestId = required(args, "request-id"), body = required(args, "body"), resource = { requestId, responseHash: hash(Buffer.from(body, "base64")), body, deliveryStatus: "ready" };
824
838
  record(this.store, "resource", requestId, resource);
@@ -1129,7 +1143,9 @@ export class Phase3App {
1129
1143
  export function output(command, data) { return { schemaVersion: "supercuts.v1", ok: true, command, data }; }
1130
1144
  export function errorOutput(error) {
1131
1145
  const known = error instanceof SupercutsError ? error : new SupercutsError("INTERNAL_ERROR", error instanceof Error ? error.message : "unknown error");
1132
- return { schemaVersion: "supercuts.v1", ok: false, error: { code: known.code, message: known.message, recovery: recovery(known.code) } };
1146
+ const retryable = ["BROADCAST_UNRESOLVED", "DELIVERY_IN_PROGRESS", "DELIVERY_PENDING", "ONBOARDING_SERVICE_UNAVAILABLE", "SELLER_DIRECTORY_UNAVAILABLE", "WALRUS_WRITE_FAILED", "WALRUS_READ_FAILED", "X402_FACILITATOR_UNAVAILABLE"].includes(known.code);
1147
+ const sameOperation = retryable || ["OPERATION_ID_CONFLICT", "DEAL_OPERATION_CONFLICT"].includes(known.code);
1148
+ return { schemaVersion: "supercuts.v1", ok: false, error: { code: known.code, message: known.message, recovery: recovery(known.code), retryable, retryWithSameOperationId: sameOperation } };
1133
1149
  }
1134
1150
  function recovery(code) {
1135
1151
  if (code === "LOCAL_AUTH_REQUIRED")
@@ -1140,5 +1156,19 @@ function recovery(code) {
1140
1156
  return "change the request or update the owner-controlled profile policy";
1141
1157
  if (code === "ARTIFACT_HASH_MISMATCH")
1142
1158
  return "restore the pinned artifact or reinitialize with an audited manifest";
1159
+ if (code === "SESSION_MISMATCH")
1160
+ return "read status.session.runId and retry with that session, or omit runId on the v2 deal path";
1161
+ if (code === "DEAL_MANDATE_INACTIVE")
1162
+ return "renew the owner mandate locally, then resume the same deal operation ID";
1163
+ if (code === "BROADCAST_UNRESOLVED")
1164
+ return "reconcile or resume with the same operation ID; never create a replacement payment";
1165
+ if (code === "DELIVERY_PENDING")
1166
+ return "resume the same root operation ID; completed selection, payment, and agreement stages will not run again";
1167
+ if (code === "X402_FACILITATOR_UNAVAILABLE")
1168
+ return "the signed transaction remains journaled; resume the same operation ID after facilitator health recovers";
1169
+ if (code === "OPERATION_ID_CONFLICT" || code === "DEAL_OPERATION_CONFLICT")
1170
+ return "reuse the original exact input for this operation ID or start a deliberately new deal";
1171
+ if (code === "X402_TRANSACTION_EXPIRY_INVALID")
1172
+ return "run onboarding doctor against the configured Sui RPC and retry the same operation ID after compatibility is restored";
1143
1173
  return "inspect the stable error code and retry with corrected input";
1144
1174
  }
@@ -1,6 +1,18 @@
1
1
  import { type CapabilityManifest } from "./capability.js";
2
2
  import { type AgentMandate } from "./mandate.js";
3
3
  import type { Phase3App } from "./phase3.js";
4
+ export declare function analysisAgreementDraft(input: {
5
+ sessionId: string;
6
+ transcriptHash: string;
7
+ serviceHash: string;
8
+ paymentAmount: string;
9
+ buyerPublicKey: string[];
10
+ counterparty: string;
11
+ capabilityId: string;
12
+ manifestHash: string;
13
+ now?: number;
14
+ scalar?: () => string;
15
+ }): Record<string, unknown>;
4
16
  interface Entry {
5
17
  manifest: CapabilityManifest;
6
18
  blobId: string;
@@ -62,6 +74,12 @@ export declare class RemoteBuyer {
62
74
  sessionId: string;
63
75
  operationId: string;
64
76
  }): Promise<Record<string, unknown>>;
77
+ research(input: {
78
+ operationId: string;
79
+ runId: string;
80
+ topic: string;
81
+ budget: string;
82
+ }): Promise<Record<string, unknown>>;
65
83
  events(runId: string): Promise<Record<string, unknown>>;
66
84
  }
67
85
  export {};
@@ -5,10 +5,23 @@ import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
5
5
  import { validateCapabilityManifest, verifySignedCapability } from "./capability.js";
6
6
  import { fail } from "./errors.js";
7
7
  import { executeMandated } from "./mandate.js";
8
+ export function analysisAgreementDraft(input) {
9
+ const scalar = input.scalar ?? (() => BigInt(`0x${randomBytes(16).toString("hex")}`).toString());
10
+ return {
11
+ sessionId: input.sessionId, rail: "shielded", dealId: scalar(), revision: "1",
12
+ transcriptRootLo: BigInt(`0x${input.transcriptHash.slice(0, 32)}`).toString(),
13
+ transcriptRootHi: BigInt(`0x${input.transcriptHash.slice(32)}`).toString(),
14
+ serviceDigestLo: BigInt(`0x${input.serviceHash.slice(0, 32)}`).toString(),
15
+ serviceDigestHi: BigInt(`0x${input.serviceHash.slice(32)}`).toString(),
16
+ settlementNonceLo: scalar(), settlementNonceHi: scalar(), paymentAmount: input.paymentAmount,
17
+ blinding: scalar(), expiry: String((input.now ?? Date.now()) + 30 * 60_000),
18
+ buyerPublicKey: input.buyerPublicKey, counterparty: input.counterparty,
19
+ capabilityId: input.capabilityId, manifestHash: input.manifestHash,
20
+ };
21
+ }
8
22
  import { decryptSessionMessage, encryptSessionMessage } from "./negotiation.js";
9
23
  const sha256 = (value) => createHash("sha256").update(value).digest("hex");
10
24
  const num = (value, name) => typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1 ? value : fail("SELLER_PREVIEW_INVALID", `${name} must be between zero and one`);
11
- const halves = (content) => { const hash = sha256(content); return [BigInt(`0x${hash.slice(0, 32)}`).toString(), BigInt(`0x${hash.slice(32)}`).toString()]; };
12
25
  export class RemoteBuyer {
13
26
  app;
14
27
  mandate;
@@ -192,10 +205,15 @@ export class RemoteBuyer {
192
205
  fail("BUYER_PRIVACY_REQUIRED", "saved owner privacy rule did not select shielded settlement");
193
206
  const publicKey = this.app.store.getSecret("identity:x25519-public")?.toString("base64url") ?? fail("IDENTITY_MISSING", "buyer delivery key is missing");
194
207
  const identity = (await this.app.execute("doctor", {})).data;
195
- const [transcriptRootLo, transcriptRootHi] = [BigInt(`0x${String(negotiated.transcriptHash).slice(0, 32)}`).toString(), BigInt(`0x${String(negotiated.transcriptHash).slice(32)}`).toString()];
196
- const [serviceDigestLo, serviceDigestHi] = halves(JSON.stringify({ capabilityId: input.capabilityId, briefHash: input.briefHash, artifactSha256, deliveryPublicKey: publicKey }));
197
- const scalar = () => BigInt(`0x${randomBytes(16).toString("hex")}`).toString();
198
- const draft = { dealId: scalar(), revision: "1", transcriptRootLo, transcriptRootHi, serviceDigestLo, serviceDigestHi, settlementNonceLo: scalar(), settlementNonceHi: scalar(), paymentAmount: entry.manifest.priceBand.minimum, blinding: scalar(), expiry: String(Date.now() + 30 * 60_000), buyerPublicKey: identity.babyJubPublicKey, counterparty: entry.manifest.sellerId, capabilityId: entry.manifest.capabilityId, manifestHash: entry.manifest.manifestHash };
208
+ const serviceHash = sha256(JSON.stringify({ capabilityId: input.capabilityId, briefHash: input.briefHash, artifactSha256, deliveryPublicKey: publicKey }));
209
+ this.app.store.db.exec("CREATE TABLE IF NOT EXISTS remote_buyer_agreements(session_id TEXT PRIMARY KEY,context_hash TEXT NOT NULL,draft_json TEXT NOT NULL,created_at INTEGER NOT NULL)");
210
+ const contextHash = sha256(JSON.stringify({ capabilityId: input.capabilityId, operationId: input.operationId, sessionId: input.sessionId, runId: input.runId, briefHash: input.briefHash, transcriptHash: negotiated.transcriptHash, serviceHash, artifactSha256 }));
211
+ const priorDraft = this.app.store.db.prepare("SELECT context_hash AS contextHash,draft_json AS draftJson FROM remote_buyer_agreements WHERE session_id=?").get(input.sessionId);
212
+ if (priorDraft && priorDraft.contextHash !== contextHash)
213
+ fail("AGREEMENT_SESSION_CONFLICT", "analysis session identifies a different agreement context");
214
+ const draft = priorDraft ? JSON.parse(priorDraft.draftJson) : analysisAgreementDraft({ sessionId: input.sessionId, transcriptHash: String(negotiated.transcriptHash), serviceHash, paymentAmount: entry.manifest.priceBand.minimum, buyerPublicKey: identity.babyJubPublicKey, counterparty: entry.manifest.sellerId, capabilityId: entry.manifest.capabilityId, manifestHash: entry.manifest.manifestHash });
215
+ if (!priorDraft)
216
+ this.app.store.db.prepare("INSERT INTO remote_buyer_agreements(session_id,context_hash,draft_json,created_at) VALUES(?,?,?,?)").run(input.sessionId, contextHash, JSON.stringify(draft), Date.now());
199
217
  const signed = await this.request("/v1/agreement", { method: "POST", body: JSON.stringify({ capabilityId: input.capabilityId, sessionId: input.sessionId, runId: input.runId, briefHash: input.briefHash, deliveryPublicKey: publicKey, agreement: Buffer.from(JSON.stringify(draft)).toString("base64url") }) });
200
218
  if (signed.artifactSha256 !== artifactSha256 || signed.capabilityId !== input.capabilityId || signed.sessionId !== input.sessionId)
201
219
  fail("AGREEMENT_BINDING_MISMATCH", "seller response changed the prepared artifact or session");
@@ -227,5 +245,41 @@ export class RemoteBuyer {
227
245
  const recorded = (await this.app.execute("resource.fetch", { "request-id": input.sessionId, "operation-id": input.operationId, response: artifact.toString("base64url") })).data;
228
246
  return { status: "delivered", sessionId: input.sessionId, artifactSha256: response.artifactSha256, body: artifact.toString("utf8"), record: { deliveryStatus: recorded.deliveryStatus, responseHash: recorded.responseHash } };
229
247
  }
248
+ async research(input) {
249
+ if (!input.operationId || input.operationId.length > 256 || !/^[1-9][0-9]*$/.test(input.budget))
250
+ fail("BUYER_WORKFLOW_INVALID", "research requires a stable operation ID and positive total budget");
251
+ this.app.store.db.exec("CREATE TABLE IF NOT EXISTS remote_research_workflows(operation_id TEXT PRIMARY KEY,input_hash TEXT NOT NULL,created_at INTEGER NOT NULL,updated_at INTEGER NOT NULL,completed_at INTEGER)");
252
+ const inputHash = sha256(JSON.stringify({ runId: input.runId, topic: input.topic, budget: input.budget })), existing = this.app.store.db.prepare("SELECT input_hash AS inputHash,completed_at AS completedAt FROM remote_research_workflows WHERE operation_id=?").get(input.operationId);
253
+ if (existing && existing.inputHash !== inputHash)
254
+ fail("DEAL_OPERATION_CONFLICT", "research operation ID identifies another topic, budget, or run");
255
+ if (!existing)
256
+ this.app.store.db.prepare("INSERT INTO remote_research_workflows(operation_id,input_hash,created_at,updated_at) VALUES(?,?,?,?)").run(input.operationId, inputHash, Date.now(), Date.now());
257
+ const checkpoint = async (stage, action) => {
258
+ const key = `research:${input.operationId}:${stage}`, prior = this.app.store.getSecret(key);
259
+ if (prior)
260
+ return JSON.parse(prior.toString());
261
+ const value = await action();
262
+ this.app.store.putSecretOnce(key, Buffer.from(JSON.stringify(value)));
263
+ this.app.store.db.prepare("UPDATE remote_research_workflows SET updated_at=? WHERE operation_id=?").run(Date.now(), input.operationId);
264
+ return value;
265
+ };
266
+ const child = (stage) => `research:${stage}:${sha256(`${input.operationId}:${stage}`).slice(0, 40)}`;
267
+ const ranked = await checkpoint("selection", () => this.rankBriefs({ topic: input.topic, budget: input.budget }));
268
+ const winnerCapabilityId = String(ranked.winner?.capabilityId ?? "");
269
+ const analysisCapabilityId = String(ranked.analysisCapabilityId ?? "");
270
+ if (!winnerCapabilityId || !analysisCapabilityId)
271
+ fail("ANALYSIS_UNAVAILABLE", "ranked catalog has no affordable brief and matching analysis");
272
+ const brief = await checkpoint("brief", () => this.purchaseBrief({ capabilityId: winnerCapabilityId, operationId: child("brief-payment"), requestId: child("brief-request"), runId: input.runId, task: `Sui research: ${input.topic}`, budget: input.budget }));
273
+ const briefHash = String(brief.artifactSha256);
274
+ const sessionId = child("analysis-session"), paymentOperationId = child("analysis-payment");
275
+ const agreement = await checkpoint("agreement", () => this.agreement({ capabilityId: analysisCapabilityId, operationId: paymentOperationId, sessionId, runId: input.runId, briefHash }));
276
+ const settlement = await checkpoint("settlement", () => this.settleAnalysis({ sessionId, operationId: paymentOperationId }));
277
+ const analysis = await checkpoint("delivery", async () => {
278
+ const value = await this.delivery({ sessionId, operationId: paymentOperationId });
279
+ return value.status === "delivered" ? value : fail("DELIVERY_PENDING", "analysis payment is finalized and seller delivery is still pending; resume the same research operation");
280
+ });
281
+ this.app.store.db.prepare("UPDATE remote_research_workflows SET completed_at=COALESCE(completed_at,?),updated_at=? WHERE operation_id=?").run(Date.now(), Date.now(), input.operationId);
282
+ return { operationId: input.operationId, state: "complete", replayed: Boolean(existing?.completedAt), selection: ranked, brief, agreement, settlement, analysis };
283
+ }
230
284
  async events(runId) { return this.request(`/v1/events?runId=${encodeURIComponent(runId)}`); }
231
285
  }
@@ -0,0 +1,8 @@
1
+ export interface RoleHomeLease {
2
+ acquired: boolean;
3
+ ownerPid?: number;
4
+ release(): void;
5
+ }
6
+ /** Best-effort process lease. SQLite still owns data integrity; this prevents
7
+ * multiple MCP event/memory workers from operating one role home at once. */
8
+ export declare function acquireRoleHomeLease(home: string): RoleHomeLease;
@@ -0,0 +1,54 @@
1
+ import { closeSync, openSync, readFileSync, unlinkSync, writeSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { randomUUID } from "node:crypto";
4
+ function active(pid) {
5
+ if (!Number.isSafeInteger(pid) || pid <= 0)
6
+ return false;
7
+ try {
8
+ process.kill(pid, 0);
9
+ return true;
10
+ }
11
+ catch {
12
+ return false;
13
+ }
14
+ }
15
+ /** Best-effort process lease. SQLite still owns data integrity; this prevents
16
+ * multiple MCP event/memory workers from operating one role home at once. */
17
+ export function acquireRoleHomeLease(home) {
18
+ const path = join(home, ".mcp.lock"), token = randomUUID();
19
+ for (let attempt = 0; attempt < 2; attempt++) {
20
+ try {
21
+ const fd = openSync(path, "wx", 0o600);
22
+ writeSync(fd, `${JSON.stringify({ pid: process.pid, token, startedAt: Date.now() })}\n`);
23
+ closeSync(fd);
24
+ let released = false;
25
+ return { acquired: true, release: () => {
26
+ if (released)
27
+ return;
28
+ released = true;
29
+ try {
30
+ const value = JSON.parse(readFileSync(path, "utf8"));
31
+ if (value.token === token)
32
+ unlinkSync(path);
33
+ }
34
+ catch { /* already removed */ }
35
+ } };
36
+ }
37
+ catch (error) {
38
+ if (error.code !== "EEXIST")
39
+ throw error;
40
+ let ownerPid;
41
+ try {
42
+ ownerPid = Number(JSON.parse(readFileSync(path, "utf8")).pid);
43
+ }
44
+ catch { /* stale malformed lock */ }
45
+ if (ownerPid !== undefined && active(ownerPid))
46
+ return { acquired: false, ownerPid, release: () => { } };
47
+ try {
48
+ unlinkSync(path);
49
+ }
50
+ catch { /* another process won the cleanup race */ }
51
+ }
52
+ }
53
+ return { acquired: false, release: () => { } };
54
+ }
@@ -0,0 +1,9 @@
1
+ export declare function packageHostedSeller(input: {
2
+ home: string;
3
+ spec: string;
4
+ artifact: string;
5
+ metadata: string;
6
+ registration: string;
7
+ bundle: string;
8
+ contentType?: string;
9
+ }): Promise<Record<string, unknown>>;
@@ -0,0 +1,50 @@
1
+ import { readFileSync, statSync, writeFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { decodeSuiPrivateKey } from "@mysten/sui/cryptography";
4
+ import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
5
+ import { createCapabilityManifest, validateCapabilityPayload } from "./capability.js";
6
+ import { fail } from "./errors.js";
7
+ import { executeMandated, loadMandate } from "./mandate.js";
8
+ import { Phase3App } from "./phase3.js";
9
+ const ownerFile = (path) => {
10
+ const value = resolve(path);
11
+ if ((statSync(value).mode & 0o077) !== 0)
12
+ fail("SELLER_CONFIG_PERMISSIONS", `${value} must be owner-only`);
13
+ return value;
14
+ };
15
+ export async function packageHostedSeller(input) {
16
+ if (![input.home, input.spec, input.artifact, input.metadata, input.registration, input.bundle].every((value) => typeof value === "string" && value.length > 0))
17
+ fail("ARGUMENT_REQUIRED", "seller package requires --home, --spec, --artifact, --metadata, --registration, and --bundle");
18
+ const specPath = ownerFile(input.spec), artifactPath = ownerFile(input.artifact), metadataPath = ownerFile(input.metadata);
19
+ const artifact = readFileSync(artifactPath);
20
+ if (!artifact.length || artifact.length > 10 * 1024 * 1024)
21
+ fail("SELLER_ARTIFACT_INVALID", "hosted artifact must contain 1 byte to 10 MiB");
22
+ const spec = JSON.parse(readFileSync(specPath, "utf8"));
23
+ const metadata = JSON.parse(readFileSync(metadataPath, "utf8"));
24
+ const home = resolve(input.home), app = new Phase3App(home);
25
+ try {
26
+ if (app.profile.role !== "seller" || app.profile.mock || app.profile.verifierVersion !== 2)
27
+ fail("SELLER_POOL_VERSION_MISMATCH", "hosted packaging requires a separate live v2 seller role home");
28
+ const encoded = app.store.getSecret("live:sui-private-key")?.toString() ?? fail("LIVE_CREDENTIAL_MISSING", "seller signer is missing");
29
+ const decoded = decodeSuiPrivateKey(encoded);
30
+ if (decoded.scheme !== "ED25519")
31
+ fail("SIGNER_UNSUPPORTED", "seller requires an ED25519 signer");
32
+ const signer = Ed25519Keypair.fromSecretKey(decoded.secretKey), manifest = createCapabilityManifest({ ...spec, sellerId: signer.toSuiAddress() });
33
+ validateCapabilityPayload(manifest, "output", metadata);
34
+ const published = await executeMandated(app, loadMandate(home), "capability.publish", { "operation-id": `publish-${manifest.manifestHash}`, manifest });
35
+ const manifestBlobId = String(published.blobId ?? "");
36
+ if (!manifestBlobId)
37
+ fail("WALRUS_PUBLISH_FAILED", "capability publication omitted a Walrus blob ID");
38
+ const transportPublicKey = app.store.getSecret("identity:x25519-public")?.toString("base64url") ?? fail("IDENTITY_MISSING", "seller transport key is missing");
39
+ const sellerSignature = (await signer.signPersonalMessage(Buffer.from(`${manifest.manifestHash}:${transportPublicKey}`))).signature;
40
+ const registration = { manifest, manifestBlobId, transportPublicKey, sellerSignature };
41
+ const registrationPath = resolve(input.registration), bundlePath = resolve(input.bundle);
42
+ writeFileSync(registrationPath, `${JSON.stringify(registration, null, 2)}\n`, { mode: 0o600, flag: "wx" });
43
+ const bundle = { sellers: [{ sellerId: manifest.sellerId, home, capabilities: [{ registrationFile: registrationPath, artifactFile: artifactPath, outputMetadata: metadata, contentType: input.contentType ?? "application/octet-stream" }] }] };
44
+ writeFileSync(bundlePath, `${JSON.stringify(bundle, null, 2)}\n`, { mode: 0o600, flag: "wx" });
45
+ return { sellerId: manifest.sellerId, capabilityId: manifest.capabilityId, manifestHash: manifest.manifestHash, manifestBlobId, registrationFile: registrationPath, provisioningBundle: bundlePath, nextCommand: `supercuts-seller-v2-provision ${JSON.stringify(bundlePath)}` };
46
+ }
47
+ finally {
48
+ app.close();
49
+ }
50
+ }
@@ -182,7 +182,13 @@ export class SellerService {
182
182
  try {
183
183
  const url = new URL(request.url ?? "/", this.origin);
184
184
  if (request.method === "GET" && url.pathname === "/health")
185
- return this.reply(response, this.#healthy ? 200 : 503, { ok: this.#healthy, service: "supercuts-sellers" });
185
+ return this.reply(response, this.#healthy ? 200 : 503, { ok: this.#healthy, service: "supercuts-sellers", buyerAccess: this.allowedBuyers.size ? "restricted" : "open" });
186
+ if (request.method === "GET" && url.pathname === "/v1/access") {
187
+ const buyerAddress = String(url.searchParams.get("buyerAddress") ?? "").toLowerCase();
188
+ if (!/^0x[0-9a-f]{64}$/.test(buyerAddress))
189
+ fail("SELLER_REQUEST_INVALID", "buyerAddress is invalid");
190
+ return this.reply(response, 200, { buyerAddress, allowed: this.allowedBuyers.size === 0 || this.allowedBuyers.has(buyerAddress), mode: this.allowedBuyers.size ? "restricted" : "open" });
191
+ }
186
192
  if (request.method === "GET" && url.pathname === "/v2/directory/health") {
187
193
  if (!this.directoryAggregator)
188
194
  return this.reply(response, 503, { ok: false, service: "supercuts-directory", reason: "aggregator not configured" });
@@ -241,7 +247,7 @@ export class SellerService {
241
247
  const sessionId = validId(input.sessionId, "sessionId"), runId = validId(input.runId, "runId"), buyerAddress = String(input.buyerAddress ?? ""), buyerPublicKey = String(input.buyerPublicKey ?? ""), ciphertext = String(input.ciphertext ?? ""), buyerSignature = String(input.buyerSignature ?? "");
242
248
  if (!/^0x[0-9a-f]{64}$/.test(buyerAddress) || buyerPublicKey.length > 1024 || ciphertext.length > 16384 || !ciphertext || !buyerSignature)
243
249
  fail("SELLER_REQUEST_INVALID", "buyer identity or encrypted offer is invalid");
244
- if (!this.allowedBuyers.has(buyerAddress))
250
+ if (this.allowedBuyers.size > 0 && !this.allowedBuyers.has(buyerAddress))
245
251
  fail("SELLER_BUYER_FORBIDDEN", "buyer is outside the seller service allowlist");
246
252
  const signedBytes = Buffer.from(`${sessionId}:${capabilityId}:${buyerPublicKey}:${sha256(Buffer.from(ciphertext, "base64"))}`);
247
253
  if (!(await isValidPersonalMessageSignature(signedBytes, buyerSignature, { address: buyerAddress })))
@@ -410,7 +416,7 @@ if (process.argv[1] && import.meta.url.endsWith(process.argv[1])) {
410
416
  const required = (key) => process.env[key] ?? fail("SELLER_CONFIG_MISSING", `${key} is required`);
411
417
  const port = Number(process.env.PORT ?? "3000");
412
418
  const origin = required("SELLER_ORIGIN");
413
- SellerService.create({ db: new SellerDatabase(required("SUPABASE_URL"), required("SUPABASE_SERVICE_ROLE_KEY")), snapshotKey: required("SELLER_STATE_KEY"), origin, facilitatorUrl: required("FACILITATOR_URL"), facilitatorKey: required("FACILITATOR_SHARED_KEY"), allowedBuyers: new Set(required("SELLER_ALLOWED_BUYERS").split(",")), claudeKey: required("CLAUDE_API_KEY"), ...(process.env.DIRECTORY_WALRUS_AGGREGATOR ? { directoryAggregator: process.env.DIRECTORY_WALRUS_AGGREGATOR } : {}) })
419
+ SellerService.create({ db: new SellerDatabase(required("SUPABASE_URL"), required("SUPABASE_SERVICE_ROLE_KEY")), snapshotKey: required("SELLER_STATE_KEY"), origin, facilitatorUrl: required("FACILITATOR_URL"), facilitatorKey: required("FACILITATOR_SHARED_KEY"), allowedBuyers: new Set((process.env.SELLER_ALLOWED_BUYERS ?? "").split(",").map((item) => item.trim().toLowerCase()).filter(Boolean)), claudeKey: required("CLAUDE_API_KEY"), ...(process.env.DIRECTORY_WALRUS_AGGREGATOR ? { directoryAggregator: process.env.DIRECTORY_WALRUS_AGGREGATOR } : {}) })
414
420
  .then((service) => { const server = service.createServer(); server.listen(port, "0.0.0.0"); const close = () => server.close(() => { service.close(); process.exit(0); }); process.on("SIGINT", close); process.on("SIGTERM", close); })
415
421
  .catch((error) => { process.stderr.write(`${error instanceof Error ? error.message : "seller service failed"}\n`); process.exitCode = 1; });
416
422
  }
@@ -11,6 +11,8 @@ export interface X402ResourceServiceConfig {
11
11
  payTo: string;
12
12
  amount: string;
13
13
  maxTimeoutSeconds?: number;
14
+ /** Bounded retry schedule for transient facilitator cold starts. */
15
+ facilitatorRetryDelaysMs?: number[];
14
16
  }
15
17
  /** Seller-owned standard x402 v2 exact resource with additional request-binding replay protection. */
16
18
  export declare class X402ResourceService {