@haven_ai/sdk 0.1.21-alpha.0 → 0.1.22-alpha.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/dist/index.cjs CHANGED
@@ -64,6 +64,8 @@ var AgentPaymentNextAction = {
64
64
  StopAndTellUser: "stop_and_tell_user",
65
65
  /** Ask again only if the user still wants the payment after expiry. */
66
66
  RequestAgainIfUserStillWantsIt: "request_again_if_user_still_wants_it",
67
+ /** #1307: retry the SAME tool call, supplying the explicit context fields the server could not rehydrate. */
68
+ RetryWithExplicitContext: "retry_with_explicit_context",
67
69
  /**
68
70
  * The x402 funding/quote window expired. Re-quote the same logical merchant
69
71
  * operation with the same idempotency key to stay double-charge-safe.
@@ -88,7 +90,21 @@ var AgentPaymentFailureCode = {
88
90
  /** The x402 funding/quote window expired before the signer or hosted settle step could finish. */
89
91
  PaymentWindowExpired: "PAYMENT_WINDOW_EXPIRED",
90
92
  /** The Haven funding leg succeeded, but the merchant rejected the paid retry. */
91
- MerchantRejectedAfterFunding: "MERCHANT_REJECTED_AFTER_FUNDING"
93
+ MerchantRejectedAfterFunding: "MERCHANT_REJECTED_AFTER_FUNDING",
94
+ /** #1300 review: funding is on-chain but the merchant never ANSWERED the
95
+ * paid retry within the timeout. NOT proof of rejection — the merchant
96
+ * holds a valid EIP-3009 authorization and may still settle late, so the
97
+ * guidance is verify-then-sweep, never blind sweep. */
98
+ MerchantUnresponsiveAfterFunding: "MERCHANT_UNRESPONSIVE_AFTER_FUNDING",
99
+ /**
100
+ * #1307: the caller omitted merchant_url/tool_name (asking Haven to
101
+ * rehydrate the stored MCP merchant-call context by payment_id), but no
102
+ * usable context was stored for this intent — either it was never an
103
+ * MCP-tool quote, or the stored context is incomplete. The fallback is
104
+ * mechanical: re-send merchant_url, tool_name, arguments, and
105
+ * mcp_transport explicitly (the version-skew path).
106
+ */
107
+ MerchantCallContextUnavailable: "MERCHANT_CALL_CONTEXT_UNAVAILABLE"
92
108
  };
93
109
  var AgentPaymentRail = {
94
110
  /** Standard Haven payment from the user's Safe through an approved delegate allowance. */
@@ -135,12 +151,46 @@ var AgentPaymentNextActionDescriptions = {
135
151
  [AgentPaymentNextAction.RequestAgainIfUserStillWantsIt]: "Ask again only if the user still wants the payment after expiry.",
136
152
  [AgentPaymentNextAction.PaymentWindowExpired]: "The x402 funding/quote window expired. Re-quote with the same idempotency key before asking the signer to build a merchant payment header again.",
137
153
  [AgentPaymentNextAction.FundSafeOrRaiseAllowance]: "Stop and tell the user that the originating Safe needs to be funded or the agent allowance raised before the payment can succeed.",
154
+ [AgentPaymentNextAction.RetryWithExplicitContext]: "Retry the same tool call, this time passing merchant_url, tool_name, arguments, and mcp_transport explicitly \u2014 the server had no stored context to rehydrate for this payment id.",
138
155
  [AgentPaymentNextAction.SweepStrandedFunds]: "Tell the user that funds may be stranded in the delegate wallet and prompt them to initiate a sweep in Haven to return them to the originating Safe."
139
156
  };
140
157
  var AgentPaymentFailureCodeDescriptions = {
141
158
  [AgentPaymentFailureCode.PriceExceedsMax]: "The merchant-authoritative x402 amount exceeds the caller's max_amount cap. No funding transfer was created; ask the user before retrying with a larger cap.",
142
159
  [AgentPaymentFailureCode.PaymentWindowExpired]: "The x402 funding/quote window expired before the signer or hosted settle step could finish. Re-quote via haven_pay_mcp_tool with the same idempotency key to avoid duplicate funding.",
143
- [AgentPaymentFailureCode.MerchantRejectedAfterFunding]: "The Haven funding leg succeeded, but the merchant rejected the paid retry. Stop retrying the merchant and reconcile stranded delegate funds with haven_sweep_delegate."
160
+ [AgentPaymentFailureCode.MerchantRejectedAfterFunding]: "The Haven funding leg succeeded, but the merchant rejected the paid retry. Stop retrying the merchant and reconcile stranded delegate funds with haven_sweep_delegate.",
161
+ [AgentPaymentFailureCode.MerchantUnresponsiveAfterFunding]: "The Haven funding leg succeeded, but the merchant did not answer the paid retry before the timeout. The merchant may still settle late \u2014 check haven_get_payment_status (and retry haven_complete_mcp_tool once) BEFORE sweeping; sweep only if no settlement appears.",
162
+ [AgentPaymentFailureCode.MerchantCallContextUnavailable]: "merchant_url/tool_name were omitted and no stored merchant call context is available for this payment_id. Re-send merchant_url, tool_name, arguments, and mcp_transport explicitly."
163
+ };
164
+ var AgentPaymentWarningCode = {
165
+ /** No max_amount cap was supplied — the live quoted price was accepted as-is. */
166
+ MissingMaxAmount: "MISSING_MAX_AMOUNT",
167
+ /** The signing window closes soon; sign promptly or re-quote with the same idempotency key. */
168
+ QuoteExpiresSoon: "QUOTE_EXPIRES_SOON",
169
+ /** The merchant URL was resolved via discovery — pass the RESOLVED url forward. */
170
+ MerchantUrlDiscovered: "MERCHANT_URL_DISCOVERED",
171
+ /**
172
+ * #1306: the catalog's last-verified price_atomic differs from the LIVE
173
+ * merchant quote for a guided catalog purchase. The catalog price is only
174
+ * ever indicative; the live quote in the same response is authoritative.
175
+ */
176
+ CatalogPriceDiffers: "CATALOG_PRICE_DIFFERS",
177
+ /**
178
+ * #1306: the rail-aware allowance/budget pre-check could not be read (RPC
179
+ * failure, etc). `sufficient` is reported as null rather than a fabricated
180
+ * true/false — the on-chain policy remains the actual gate either way.
181
+ */
182
+ AllowanceCheckUnavailable: "ALLOWANCE_CHECK_UNAVAILABLE",
183
+ /**
184
+ * #1319: the delegation-rail read itself SUCCEEDED, but the remaining
185
+ * figure it returned is the #1145 fallback (the full configured budget)
186
+ * rather than a live ERC20PeriodTransferEnforcer read — `sufficient` is a
187
+ * real true/false, just computed from an optimistic number. Distinct from
188
+ * {@link AgentPaymentWarningCode.AllowanceCheckUnavailable}, which fires
189
+ * when the read failed outright and `sufficient` degrades to null. The
190
+ * on-chain policy re-checks at redemption either way; this only says the
191
+ * guidance shown here may be optimistic.
192
+ */
193
+ AllowanceReadOptimistic: "ALLOWANCE_READ_OPTIMISTIC"
144
194
  };
145
195
  var AgentPaymentRailDescriptions = {
146
196
  [AgentPaymentRail.Direct]: "Standard Haven payment from the user-controlled Safe through an approved delegate allowance.",
@@ -195,6 +245,20 @@ var HavenApiError = class extends HavenError {
195
245
  }
196
246
  body;
197
247
  };
248
+ var MerchantTimeoutError = class extends HavenApiError {
249
+ merchantErrorCode = "merchant_timeout";
250
+ constructor(message) {
251
+ super(message, 504);
252
+ this.name = "MerchantTimeoutError";
253
+ }
254
+ };
255
+ var X402UnexpectedStatusError = class extends HavenApiError {
256
+ x402ErrorCode = "unexpected_non_402_status";
257
+ constructor(message, statusCode) {
258
+ super(message, statusCode);
259
+ this.name = "X402UnexpectedStatusError";
260
+ }
261
+ };
198
262
  var HavenPaymentStateError = class extends HavenApiError {
199
263
  constructor(message, statusCode, state, body) {
200
264
  super(message, statusCode, body, state.paymentId);
@@ -219,6 +283,25 @@ var HavenSigningError = class extends HavenError {
219
283
  this.name = "HavenSigningError";
220
284
  }
221
285
  };
286
+ var SignerRefusalCode = {
287
+ /** `SUPPORTED_X402_EXPECTED_VERSIONS` in `@haven_ai/signer` does not include the received version. */
288
+ UnsupportedExpectedContextVersion: "UNSUPPORTED_EXPECTED_CONTEXT_VERSION",
289
+ /** `SUPPORTED_SWEEP_BINDING_VERSIONS` in `@haven_ai/signer` does not include the received version. */
290
+ UnsupportedSweepBindingVersion: "UNSUPPORTED_SWEEP_BINDING_VERSION"
291
+ };
292
+ var SIGNER_UPDATE_FALLBACK = "Update @haven_ai/signer by rerunning `npx @haven_ai/connect@alpha`, which reinstalls the pinned MCP runtime, then retry the same signing call. Nothing was signed or spent \u2014 the quote or payment this version came from is unaffected and does not need to be re-quoted.";
293
+ var HavenUnsupportedSignerVersionError = class extends HavenError {
294
+ constructor(message, code, supportedVersions, receivedVersion, fallback) {
295
+ super(message, code);
296
+ this.supportedVersions = supportedVersions;
297
+ this.receivedVersion = receivedVersion;
298
+ this.fallback = fallback;
299
+ this.name = "HavenUnsupportedSignerVersionError";
300
+ }
301
+ supportedVersions;
302
+ receivedVersion;
303
+ fallback;
304
+ };
222
305
  var HavenTimeoutError = class extends HavenError {
223
306
  constructor(paymentId) {
224
307
  super(
@@ -697,6 +780,8 @@ function explorerUrlOrEmpty(chainId, txHash) {
697
780
  return txHash ? buildExplorerUrl(chainId, txHash) : "";
698
781
  }
699
782
  var DEFAULT_REQUEST_TIMEOUT = 3e4;
783
+ var DEFAULT_MERCHANT_TIMEOUT = 3e5;
784
+ var NOTIFY_TIMEOUT = 1e4;
700
785
  var DEFAULT_CONFIRMATION_TIMEOUT = 9e4;
701
786
  var DEFAULT_POLLING_INTERVAL = 3e3;
702
787
  function formatAtomicAmount(atomic, decimals) {
@@ -860,12 +945,32 @@ function x402TypedDataDigest(typedData) {
860
945
  );
861
946
  }
862
947
  }
948
+ function mapCatalogEntry(entry) {
949
+ return {
950
+ id: entry.id,
951
+ name: entry.name,
952
+ description: entry.description,
953
+ category: entry.category,
954
+ resourceUrl: entry.resource_url,
955
+ rail: entry.rail,
956
+ protocol: entry.protocol,
957
+ toolName: entry.tool_name,
958
+ toolArguments: entry.tool_arguments ?? null,
959
+ priceDisplay: entry.price_display,
960
+ priceAtomic: entry.price_atomic,
961
+ asset: entry.asset,
962
+ network: entry.network,
963
+ status: entry.status,
964
+ verifiedAt: entry.verified_at
965
+ };
966
+ }
863
967
  var HavenClient = class {
864
968
  apiKey;
865
969
  delegateKey;
866
970
  baseUrl;
867
971
  x402Wallet;
868
972
  requestTimeout;
973
+ merchantTimeout;
869
974
  confirmationTimeout;
870
975
  pollingInterval;
871
976
  chainRpcs;
@@ -895,6 +1000,7 @@ var HavenClient = class {
895
1000
  this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
896
1001
  this.x402Wallet = config.x402Wallet;
897
1002
  this.requestTimeout = config.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT;
1003
+ this.merchantTimeout = config.merchantTimeout ?? DEFAULT_MERCHANT_TIMEOUT;
898
1004
  this.confirmationTimeout = config.confirmationTimeout ?? DEFAULT_CONFIRMATION_TIMEOUT;
899
1005
  this.pollingInterval = config.pollingInterval ?? DEFAULT_POLLING_INTERVAL;
900
1006
  this.chainRpcs = config.chainRpcs ?? {};
@@ -1002,7 +1108,9 @@ var HavenClient = class {
1002
1108
  asset: option.asset,
1003
1109
  network: option.network,
1004
1110
  description: paymentRequired.resource.description,
1005
- idempotencyKey
1111
+ idempotencyKey,
1112
+ // #1307: persisted so the settle leg can rehydrate it by payment_id.
1113
+ ...options.mcpCallContext ? { mcpCallContext: options.mcpCallContext } : {}
1006
1114
  });
1007
1115
  if (raw.status !== "pending_signature") {
1008
1116
  this.throwPaymentStateError("x402 payment", raw);
@@ -1141,7 +1249,12 @@ var HavenClient = class {
1141
1249
  status: raw.status,
1142
1250
  safeAddress: raw.safe_address,
1143
1251
  delegateAddress: raw.delegate_address,
1144
- chainId: raw.chain_id
1252
+ chainId: raw.chain_id,
1253
+ // Defensive normalization, not trust: the backend contract is exactly
1254
+ // 'legacy' | 'delegation' (#1306), but an older/mismatched backend
1255
+ // during a rollout window should degrade to the wider legacy bucket
1256
+ // rather than propagate an unrecognized string.
1257
+ executionRail: raw.execution_rail === "delegation" ? "delegation" : "legacy"
1145
1258
  };
1146
1259
  }
1147
1260
  /**
@@ -1291,11 +1404,108 @@ var HavenClient = class {
1291
1404
  resetTimeMin: allowance.onchain.reset_time_min,
1292
1405
  lastResetMin: allowance.onchain.last_reset_min,
1293
1406
  nonce: allowance.onchain.nonce,
1294
- isResetPending: allowance.onchain.is_reset_pending
1407
+ isResetPending: allowance.onchain.is_reset_pending,
1408
+ remainingIsFromChain: allowance.onchain.remaining_is_from_chain
1295
1409
  }
1296
1410
  }))
1297
1411
  };
1298
1412
  }
1413
+ /**
1414
+ * Post-purchase allowance/budget summary for a settled payment (#1310).
1415
+ *
1416
+ * Reuses the EXACT rail-aware read path {@link getAllowances} / #1306's
1417
+ * catalog-purchase preflight `allowance` block use — `GET
1418
+ * /machine-payments/allowances`, with delegation-rail values coming from
1419
+ * the #1090 `deriveDelegationBudgets`-backed enforcer read, never
1420
+ * `agent_allowances` — so this can never disagree with
1421
+ * {@link getAllowances} for the same fixture. The settled token is
1422
+ * resolved from {@link getPaymentStatus} so callers pass only
1423
+ * `paymentId`, never a second haven_get_agent-style round trip.
1424
+ *
1425
+ * NEVER throws: any failed read (status lookup, agent lookup, or the
1426
+ * allowance/budget lookup itself) degrades to `{ allowance: null,
1427
+ * warnings: [ALLOWANCE_CHECK_UNAVAILABLE] }` rather than converting a
1428
+ * successful settlement into a failure — the on-chain policy remains the
1429
+ * actual spend gate regardless of whether this report can be produced.
1430
+ *
1431
+ * Freshness caveat (#1319): the delegation rail's on-chain enforcer read
1432
+ * can silently fall back to the optimistic full period budget without
1433
+ * throwing when the RPC read itself fails (#1145's fund-safe design,
1434
+ * unchanged here). {@link getAllowances}'s `onchain.remainingIsFromChain`
1435
+ * now carries that provenance on the wire, and the #1306 catalog-purchase
1436
+ * preflight (`haven_prepare_catalog_purchase`) surfaces it as a warning —
1437
+ * this summary does not (yet). `remaining_atomic` here reflects the last
1438
+ * successful chain read, not a guaranteed-live one, and callers should not
1439
+ * phrase it as guaranteed-fresh.
1440
+ */
1441
+ async getPostPurchaseAllowanceSummary(paymentId) {
1442
+ const unavailable = (detail) => ({
1443
+ allowance: null,
1444
+ warnings: [
1445
+ {
1446
+ code: AgentPaymentWarningCode.AllowanceCheckUnavailable,
1447
+ message: `Could not read the post-purchase allowance/budget for payment ${paymentId} (${detail}). The payment itself succeeded \u2014 the on-chain policy remains the actual spend gate; this only affects the remaining-budget figure reported here.`
1448
+ }
1449
+ ]
1450
+ });
1451
+ try {
1452
+ const [status, agent, allowanceSummary] = await Promise.all([
1453
+ this.getPaymentStatus(paymentId),
1454
+ this.getAgent(),
1455
+ this.getAllowances()
1456
+ ]);
1457
+ const tokenAddress = status.asset ?? status.x402?.asset ?? null;
1458
+ if (!tokenAddress) {
1459
+ return unavailable("the settled payment does not carry a resolvable token address");
1460
+ }
1461
+ const rail = agent.executionRail;
1462
+ const source = rail === "delegation" ? "active_delegations" : "allowance_module";
1463
+ const match = allowanceSummary.allowances.find(
1464
+ (a) => a.tokenAddress.toLowerCase() === tokenAddress.toLowerCase()
1465
+ );
1466
+ if (!match) {
1467
+ return unavailable("no allowance/budget row matches the settled token");
1468
+ }
1469
+ const token = resolveTokenFromAddress(match.tokenAddress);
1470
+ const remainingDisplay = token ? `${formatAtomicAmount(safeBigInt(match.onchain.remaining), token.decimals)} ${match.tokenSymbol}` : void 0;
1471
+ return {
1472
+ allowance: {
1473
+ rail,
1474
+ remaining_atomic: match.onchain.remaining,
1475
+ ...remainingDisplay ? { remaining_display: remainingDisplay } : {},
1476
+ token_symbol: match.tokenSymbol,
1477
+ token_address: match.tokenAddress,
1478
+ reset_period: match.resetPeriodMin,
1479
+ source
1480
+ },
1481
+ warnings: []
1482
+ };
1483
+ } catch (err) {
1484
+ return unavailable(err instanceof Error ? err.message : String(err));
1485
+ }
1486
+ }
1487
+ /**
1488
+ * `haven_get_payment_status` convenience: fetch status and, for a
1489
+ * genuinely SETTLED x402 payment, attach the same post-purchase
1490
+ * allowance/budget summary a settle response carries.
1491
+ *
1492
+ * #1310/#1311 parity: this is the ONE home for logic that was duplicated
1493
+ * verbatim in `packages/mcp-server/src/tools.ts` and `packages/mcp/src/tools.ts`
1494
+ * (both hosted and local `haven_get_payment_status` handlers) — extracted
1495
+ * here because both packages already depend on `@haven_ai/sdk` and call
1496
+ * methods on a `HavenClient` instance, so this needed no new dependency
1497
+ * edge. `funded_but_unsettled` is deliberately excluded: that phase means
1498
+ * the merchant did NOT accept the retry. Every other phase/rail returns
1499
+ * the status untouched.
1500
+ */
1501
+ async getPaymentStatusWithPostPurchaseAllowance(paymentId) {
1502
+ const status = await this.getPaymentStatus(paymentId);
1503
+ if (status.rail === AgentPaymentRail.X402 && status.phase === AgentPaymentPhase.PaymentConfirmed) {
1504
+ const { allowance, warnings } = await this.getPostPurchaseAllowanceSummary(paymentId);
1505
+ return { ...status, allowance, ...warnings.length > 0 ? { warnings } : {} };
1506
+ }
1507
+ return status;
1508
+ }
1299
1509
  /**
1300
1510
  * Discover payable services from Haven's curated merchant catalog.
1301
1511
  *
@@ -1309,22 +1519,20 @@ var HavenClient = class {
1309
1519
  if (options.rail) params.set("rail", options.rail);
1310
1520
  const query = params.size > 0 ? `?${params.toString()}` : "";
1311
1521
  const raw = await this.get(`/catalog${query}`);
1312
- return raw.entries.map((entry) => ({
1313
- id: entry.id,
1314
- name: entry.name,
1315
- description: entry.description,
1316
- category: entry.category,
1317
- resourceUrl: entry.resource_url,
1318
- rail: entry.rail,
1319
- protocol: entry.protocol,
1320
- toolName: entry.tool_name,
1321
- priceDisplay: entry.price_display,
1322
- priceAtomic: entry.price_atomic,
1323
- asset: entry.asset,
1324
- network: entry.network,
1325
- status: entry.status,
1326
- verifiedAt: entry.verified_at
1327
- }));
1522
+ return raw.entries.map(mapCatalogEntry);
1523
+ }
1524
+ /**
1525
+ * Fetch one curated catalog entry by id (#1306).
1526
+ *
1527
+ * Chain-scoped for free by the backend's SQL when the client is
1528
+ * agent-authenticated (#1299): an unknown id and an id curated for a
1529
+ * DIFFERENT chain than this agent's both 404 identically — this method does
1530
+ * not (and must not) re-filter by chain in JS. Read-only, like
1531
+ * {@link discoverTools}.
1532
+ */
1533
+ async getCatalogEntry(id) {
1534
+ const raw = await this.get(`/catalog/${encodeURIComponent(id)}`);
1535
+ return mapCatalogEntry(raw);
1328
1536
  }
1329
1537
  /**
1330
1538
  * List recent machine-payment receipts/evidence for bookkeeping.
@@ -1423,9 +1631,9 @@ var HavenClient = class {
1423
1631
  async quoteX402(url, init, options = {}) {
1424
1632
  const initialInit = this.withX402Wallet(init, this.x402PayerAddress());
1425
1633
  const request = this.snapshotX402Request(url, initialInit);
1426
- const response = await globalThis.fetch(url, initialInit);
1634
+ const response = await this.merchantFetch(url, initialInit);
1427
1635
  if (response.status !== 402) {
1428
- throw new HavenApiError(
1636
+ throw new X402UnexpectedStatusError(
1429
1637
  `Expected an x402 quote response with HTTP 402, got HTTP ${response.status}.`,
1430
1638
  response.status || 400
1431
1639
  );
@@ -1437,6 +1645,34 @@ var HavenClient = class {
1437
1645
  const mcpTransport = await this.detectX402McpTransport(url, paymentRequired, response);
1438
1646
  return this.buildX402Quote(paymentRequired, request, options.idempotencyKey, mcpTransport);
1439
1647
  }
1648
+ /**
1649
+ * Probe an MCP tool for its x402 quote without creating a payment.
1650
+ *
1651
+ * Unlike the generic {@link quoteX402} helper, this completes the
1652
+ * Streamable-HTTP MCP lifecycle before sending the unpaid `tools/call`.
1653
+ * Hosted MCP uses this path while remaining keyless: it resolves only the
1654
+ * agent's public delegate address for `x402-wallet`; signing remains local.
1655
+ * It refuses before the quote when the merchant does not establish a session;
1656
+ * callers that need a plain x402 endpoint must use {@link quoteX402}.
1657
+ */
1658
+ async quoteMcpX402(url, init, options = {}) {
1659
+ const wallet = await this.resolveX402WalletForMerchantCall();
1660
+ const sessionId = await this.mcpInitialize(url, init, wallet);
1661
+ if (!sessionId) {
1662
+ throw new HavenApiError(
1663
+ "The merchant did not establish an MCP session before the x402 quote. No payment was created.",
1664
+ 502,
1665
+ { mcpSessionNotEstablished: true }
1666
+ );
1667
+ }
1668
+ let requestInit = this.withX402Wallet(init, wallet);
1669
+ requestInit = this.withMcpHeaders(requestInit, sessionId);
1670
+ const quote = await this.quoteX402(url, requestInit, options);
1671
+ return {
1672
+ ...quote,
1673
+ mcpTransport: quote.mcpTransport ?? { handshakeRequired: true, source: "path" }
1674
+ };
1675
+ }
1440
1676
  /**
1441
1677
  * Pay a previously inspected x402 quote and retry the exact captured request.
1442
1678
  */
@@ -1542,7 +1778,7 @@ var HavenClient = class {
1542
1778
  if (!url) {
1543
1779
  throw new HavenApiError("x402 resume requires the original URL or a captured request snapshot.", 400);
1544
1780
  }
1545
- const response = await globalThis.fetch(url, initialInit);
1781
+ const response = await this.merchantFetch(url, initialInit);
1546
1782
  if (response.status !== 402) {
1547
1783
  throw new HavenApiError("Expected the original x402 request to return HTTP 402 before resuming.", 400);
1548
1784
  }
@@ -1585,7 +1821,7 @@ var HavenClient = class {
1585
1821
  }
1586
1822
  let requestInit = this.withX402Wallet(init, this.x402PayerAddress());
1587
1823
  if (mcpSessionId) requestInit = this.withMcpHeaders(requestInit, mcpSessionId);
1588
- const response = await globalThis.fetch(url, requestInit);
1824
+ const response = await this.merchantFetch(url, requestInit);
1589
1825
  if (response.status !== 402) {
1590
1826
  return mcpSessionId ? this.surfaceMcpResult(response) : response;
1591
1827
  }
@@ -1646,7 +1882,7 @@ var HavenClient = class {
1646
1882
  headers.set("Content-Type", "application/json");
1647
1883
  headers.set("Accept", MCP_ACCEPT);
1648
1884
  if (wallet && !headers.has("x402-wallet")) headers.set("x402-wallet", wallet);
1649
- const response = await globalThis.fetch(url, {
1885
+ const response = await this.merchantFetch(url, {
1650
1886
  method: "POST",
1651
1887
  headers,
1652
1888
  body: JSON.stringify({
@@ -1683,11 +1919,15 @@ var HavenClient = class {
1683
1919
  headers.set("Accept", MCP_ACCEPT);
1684
1920
  headers.set("mcp-session-id", sessionId);
1685
1921
  if (wallet && !headers.has("x402-wallet")) headers.set("x402-wallet", wallet);
1686
- await globalThis.fetch(url, {
1687
- method: "POST",
1688
- headers,
1689
- body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" })
1690
- });
1922
+ await this.merchantFetch(
1923
+ url,
1924
+ {
1925
+ method: "POST",
1926
+ headers,
1927
+ body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" })
1928
+ },
1929
+ NOTIFY_TIMEOUT
1930
+ );
1691
1931
  } catch {
1692
1932
  }
1693
1933
  }
@@ -1753,7 +1993,7 @@ var HavenClient = class {
1753
1993
  return this.buildMppQuote(challengeOrUrl, request2, options.idempotencyKey);
1754
1994
  }
1755
1995
  const request = this.snapshotX402Request(challengeOrUrl, init);
1756
- const response = await globalThis.fetch(challengeOrUrl, init);
1996
+ const response = await this.merchantFetch(challengeOrUrl, init);
1757
1997
  if (response.status !== 402) {
1758
1998
  throw new HavenApiError(
1759
1999
  `Expected an MPP quote response with HTTP 402, got HTTP ${response.status}.`,
@@ -1795,7 +2035,7 @@ var HavenClient = class {
1795
2035
  }
1796
2036
  const retryHeaders = new Headers(initialInit?.headers);
1797
2037
  retryHeaders.set("X-PAYMENT", receipt.paymentHeader);
1798
- const retryResponse = await globalThis.fetch(url, {
2038
+ const retryResponse = await this.merchantFetch(url, {
1799
2039
  ...initialInit,
1800
2040
  headers: retryHeaders
1801
2041
  });
@@ -1928,7 +2168,7 @@ var HavenClient = class {
1928
2168
  const headers = new Headers(requestInit.headers);
1929
2169
  headers.set("X-PAYMENT", input.paymentHeader);
1930
2170
  requestInit = { ...requestInit, headers };
1931
- const response = await globalThis.fetch(input.url, requestInit);
2171
+ const response = await this.merchantFetch(input.url, requestInit);
1932
2172
  const surfaced = mcpSessionId ? await this.surfaceMcpResult(response) : response;
1933
2173
  const protocolReceiptHeader = surfaced.headers.get("PAYMENT-RESPONSE") ?? void 0;
1934
2174
  const settlement = parseMerchantSettlement(protocolReceiptHeader ?? null);
@@ -1976,6 +2216,33 @@ var HavenClient = class {
1976
2216
  settlementTxHash: settlement.settlementTxHash ?? void 0
1977
2217
  };
1978
2218
  }
2219
+ /**
2220
+ * GET /x402/:id/merchant-call-context — the settle-leg twin of #1263's
2221
+ * sign-context fetch (#1307). Re-serves the stored merchant MCP-tool call
2222
+ * context (merchant_url, tool_name, arguments, mcp_transport) recorded at
2223
+ * quote time, so `haven_settle_mcp_tool` / `haven_complete_mcp_tool` can
2224
+ * omit those fields and let Haven rehydrate them by payment_id instead of
2225
+ * the caller re-threading them. Throws `HavenApiError` (404 unknown/foreign
2226
+ * payment_id, 409 no stored context, 410 expired) — the caller decides the
2227
+ * fallback (re-send the full context explicitly).
2228
+ */
2229
+ async getX402MerchantCallContext(paymentId) {
2230
+ const raw = await this.get(
2231
+ `/x402/${paymentId}/merchant-call-context`
2232
+ );
2233
+ return {
2234
+ paymentId: raw.payment_id,
2235
+ merchantUrl: raw.merchant_url,
2236
+ toolName: raw.tool_name,
2237
+ arguments: raw.arguments ?? {},
2238
+ ...raw.mcp_transport ? {
2239
+ mcpTransport: {
2240
+ handshakeRequired: raw.mcp_transport.handshake_required,
2241
+ source: raw.mcp_transport.source
2242
+ }
2243
+ } : {}
2244
+ };
2245
+ }
1979
2246
  async resolveX402MerchantCompletionContext(input) {
1980
2247
  const status = await this.getPaymentStatus(input.paymentId);
1981
2248
  if (status.rail !== "x402") {
@@ -2091,7 +2358,7 @@ var HavenClient = class {
2091
2358
  if (!url) {
2092
2359
  throw new HavenApiError("MPP resume requires the original URL or a captured request snapshot.", 400);
2093
2360
  }
2094
- const response = await globalThis.fetch(url, initialInit);
2361
+ const response = await this.merchantFetch(url, initialInit);
2095
2362
  if (response.status !== 402) {
2096
2363
  throw new HavenApiError("Expected the original MPP request to return HTTP 402 before resuming.", 400);
2097
2364
  }
@@ -2124,7 +2391,7 @@ var HavenClient = class {
2124
2391
  async retryMppRequest(url, initialInit, challenge, receipt) {
2125
2392
  const retryHeaders = new Headers(initialInit?.headers);
2126
2393
  retryHeaders.set("MACHINE-PAYMENT-PROOF", receipt.proofHeader);
2127
- const retryResponse = await globalThis.fetch(url, {
2394
+ const retryResponse = await this.merchantFetch(url, {
2128
2395
  ...initialInit,
2129
2396
  headers: retryHeaders
2130
2397
  });
@@ -2976,6 +3243,26 @@ var HavenClient = class {
2976
3243
  async get(path) {
2977
3244
  return this.request("GET", path);
2978
3245
  }
3246
+ /**
3247
+ * #1300: every MERCHANT-facing fetch goes through here. Haven API calls
3248
+ * have always been bounded (request() below); the merchant probes/retries
3249
+ * called globalThis.fetch bare, so a slow-loris merchant could hold a tool
3250
+ * call open forever. A caller-supplied signal still applies (combined via
3251
+ * AbortSignal.any); a timeout abort surfaces as a clear HavenApiError 504
3252
+ * naming the URL rather than a bare AbortError.
3253
+ */
3254
+ async merchantFetch(url, init = {}, timeoutMs = this.merchantTimeout) {
3255
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
3256
+ const signal = init.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal;
3257
+ try {
3258
+ return await globalThis.fetch(url, { ...init, signal });
3259
+ } catch (err) {
3260
+ if (timeoutSignal.aborted) {
3261
+ throw new MerchantTimeoutError(`Merchant request timed out after ${timeoutMs}ms: ${url}`);
3262
+ }
3263
+ throw err;
3264
+ }
3265
+ }
2979
3266
  async request(method, path, body) {
2980
3267
  const url = `${this.baseUrl}${path}`;
2981
3268
  const controller = new AbortController();
@@ -3229,16 +3516,16 @@ var toolDescriptions = {
3229
3516
  nextActionGuidance: ""
3230
3517
  },
3231
3518
  payMcpTool: {
3232
- summary: "Call a named tool on an MCP merchant that requires an x402 payment, handling the full initialize \u2192 pay \u2192 retry round trip.",
3519
+ summary: "Call a named tool on an MCP merchant that requires an x402 payment, handling the full initialize \u2192 pay \u2192 retry round trip in one call.",
3233
3520
  selectionGuidance: "Use this when the agent wants to call a specific tool on an MCP merchant (e.g. Soundside, Coinbase Bazaar) and payment is required. Prefer this over haven_pay_x402 when you know the merchant_url and tool_name \u2014 it builds the JSON-RPC envelope internally. Use haven_pay_x402 for arbitrary HTTP resources. Do NOT use for read-only allowance or budget questions \u2014 use haven_get_allowances.",
3234
- behavior: "Builds the JSON-RPC tools/call envelope, runs the MCP Streamable-HTTP initialize handshake automatically (if the endpoint is MCP-shaped), pays any HTTP 402 x402 challenge through Haven's AllowanceModule path, and retries the request. Returns the JSON-RPC result (the actual merchant output) on success. Amounts within the on-chain allowance execute automatically; over-allowance transfers are queued as pending_approval.",
3521
+ behavior: "Builds the JSON-RPC tools/call envelope, runs the MCP Streamable-HTTP initialize handshake automatically (if the endpoint is MCP-shaped), pays any HTTP 402 x402 challenge through Haven's AllowanceModule path, and retries the request, returning the JSON-RPC result (the actual merchant output) on success. Amounts within the on-chain allowance execute automatically; over-allowance transfers are queued as pending_approval \u2014 follow the response's nextAction when present.",
3235
3522
  nextActionGuidance: "If pending_approval is returned, preserve payment_id and resume_state and wait for the wallet owner to approve in Haven. Use haven_resume_x402_payment once nextAction=retry_original_x402_request."
3236
3523
  },
3237
3524
  discoverTools: {
3238
- summary: "Discover payable services from Haven's curated merchant catalog \u2014 names, prices, and which pay tool to use.",
3525
+ summary: "Step 1 of a purchase: discover payable services from Haven's curated merchant catalog \u2014 names, prices, and which pay tool to use next.",
3239
3526
  selectionGuidance: "Use this when the user asks what the agent can buy, pay for, or which paid services exist \u2014 or when you need a resource URL for a service the user described. Do NOT use for balance, budget, or spend-limit questions \u2014 use haven_get_allowances. Do NOT use to pay \u2014 each returned entry names the pay tool to use next.",
3240
- behavior: "Read-only lookup against Haven's curated catalog. Entries are periodically re-verified against the live merchant; degraded entries are flagged. Returns name, description, price, rail, resource URL, and a suggested_tool field naming the exact Haven pay tool for that entry. The catalog price (price_display/price_atomic, marked price_is_indicative) is a last-verified hint, NOT authoritative \u2014 the real price comes from the merchant's live 402 at pay time. Never creates a payment, signature, or approval.",
3241
- nextActionGuidance: "Pick an entry and pay it with the tool named in suggested_tool, passing the entry's resource_url (and tool_name for MCP merchants). Confirm the price from the live pay-tool result (not the catalog), and pass max_amount when the user has a cap."
3527
+ behavior: "Use each entry's suggested_tool field first \u2014 it names the exact next call. Read-only lookup against Haven's curated catalog; entries are periodically re-verified against the live merchant and degraded entries are flagged. Returns name, description, price, rail, resource URL, tool_name, tool_arguments, and suggested_tool. The catalog price (price_display/price_atomic, marked price_is_indicative) is a last-verified hint, NOT authoritative \u2014 the real price comes from the merchant's live 402 at pay time. Never creates a payment, signature, or approval.",
3528
+ nextActionGuidance: "Pick an entry and pay it with the tool named in suggested_tool, passing the entry's resource_url, tool_name, and tool_arguments for MCP merchants. Confirm the price from the live pay-tool result (not the catalog), and pass max_amount when the user has a cap."
3242
3529
  },
3243
3530
  sweep_delegate: {
3244
3531
  summary: "Sweep stranded USDC and/or ETH from the delegate wallet back to the originating Safe.",
@@ -3507,6 +3794,9 @@ user's approval in Haven.
3507
3794
 
3508
3795
  Hosted tools run in the \`mcp__haven__\` namespace. Local signing tools run in
3509
3796
  the \`mcp__haven-signer__\` namespace and keep the delegate key on this machine.
3797
+ Tool results carry the exact next step (\`next_action\`, \`next_tool\`,
3798
+ \`next_arguments\`) \u2014 follow those fields first; the prose below is fallback
3799
+ and orientation, not the source of truth.
3510
3800
 
3511
3801
  ## When to use this skill
3512
3802
 
@@ -3531,11 +3821,11 @@ Before any payment, confirm the *live remaining* budget with the tools \u2014
3531
3821
  \`agent.json\` shows the configured budget, not what is left after recent
3532
3822
  spending:
3533
3823
 
3534
- - \`haven_get_agent\` \u2014 the recommended first call: identity (wallet, network)
3535
- plus a readiness signal (\`ready\` / \`needs_approval\` / \`revoked\`) and live
3536
- remaining per-token allowance, in one shot.
3537
- - \`haven_get_allowances\` \u2014 detailed per-token breakdown (configured, spent,
3538
- reset window) when you need more than the summary.
3824
+ - \`mcp__haven__haven_get_agent\` \u2014 the recommended first call: identity
3825
+ (wallet, network) plus a readiness signal (\`ready\` / \`needs_approval\` /
3826
+ \`revoked\`) and live remaining per-token allowance, in one shot.
3827
+ - \`mcp__haven__haven_get_allowances\` \u2014 detailed per-token breakdown
3828
+ (configured, spent, reset window) when you need more than the summary.
3539
3829
 
3540
3830
  Budgets reset on a period the user chose. If a payment exceeds the remaining
3541
3831
  budget it is queued for the user to approve in the Haven dashboard \u2014 this is
@@ -3543,47 +3833,79 @@ normal, not an error.
3543
3833
 
3544
3834
  ## Paying
3545
3835
 
3546
- - **Direct transfer:** \`haven_pay\` with recipient, amount, and token.
3547
- - **x402 paywall:** \`haven_quote_x402\` to get a quote, then
3548
- \`haven_pay_x402_quote\`. In the hosted setup the signing step happens in
3549
- the local Haven signer; follow the tool results \u2014 they tell you the next
3550
- action at every step. Retry the original request only when the result says
3551
- \`retry_original_x402_request\`.
3552
- - **Paid MCP tool call:** \`mcp__haven__haven_pay_mcp_tool\` with the merchant
3553
- URL, tool name, and arguments, then finish in two calls (fast path):
3554
- \`mcp__haven-signer__haven_sign_x402\` on the local signer (pass
3555
- \`payload_hash\`, \`x402_expected\` as the nested \`x402.expected\` object, and
3556
- \`payment_required\`) returns \`{ signature, payment_header }\`; then
3557
- \`mcp__haven__haven_settle_mcp_tool\` (pass \`payment_id\`, \`signature\`,
3558
- \`payment_header\`, \`merchant_url\`, \`tool_name\`, \`arguments\`,
3559
- \`mcp_transport\`) funds and settles in one step and returns the tool result.
3560
- If it returns \`settled: false\`, funding is queued for the user's approval \u2014
3561
- tell them and check status later, do not re-pay. Step-by-step alternative:
3562
- \`mcp__haven-signer__haven_sign\` \u2192 \`mcp__haven__haven_submit\` \u2192
3563
- \`mcp__haven-signer__haven_x402_sign_header\` \u2192
3564
- \`mcp__haven__haven_complete_mcp_tool\`. Pass \`payment_required\`,
3565
- \`arguments\`, and \`mcp_transport\` verbatim from the
3566
- \`mcp__haven__haven_pay_mcp_tool\` result. The returned \`expires_at\` is the
3567
- signing window; if a tool returns \`PAYMENT_WINDOW_EXPIRED\`, re-run
3568
- \`mcp__haven__haven_pay_mcp_tool\` with the same
3569
- \`idempotency_key\`. Do not call the merchant yourself \u2014 Haven completes the
3570
- merchant leg for you.
3571
- - **Prices:** show the user the live price from the pay-tool result, never a
3572
- catalog price. \`haven_discover_tools\` prices are indicative
3573
- (\`price_is_indicative\`) and can be stale. The pay-tool result's \`amount\` /
3574
- \`amount_atomic\` is the amount Haven authorizes for the call \u2014 a ceiling the
3575
- merchant settles at or below \u2014 so present it as the most the user will pay.
3576
- Pass \`max_amount\` (atomic units) to \`haven_pay_mcp_tool\` /
3577
- \`haven_pay_x402_quote\` to reject a quote whose authorized amount is above the
3578
- user's cap, before any funds move.
3579
- - **Status:** \`haven_get_payment_status\` with a \`payment_id\` to check on
3580
- queued or in-flight payments. Do not poll in a tight loop.
3836
+ **Catalog purchases \u2014 the primary path for MCP merchants:**
3837
+
3838
+ 1. \`mcp__haven__haven_discover_tools\` to find a payable service and its
3839
+ \`catalog_id\`.
3840
+ 2. \`mcp__haven__haven_prepare_catalog_purchase\` with \`catalog_id\` and
3841
+ \`max_amount\`. \`max_amount\` (atomic units) is REQUIRED on this tool, and
3842
+ is best practice on every paid call below too \u2014 it caps what the LIVE
3843
+ merchant quote may charge, checked before any funding intent is created.
3844
+ 3. Then FOLLOW THE RESPONSE'S GUIDANCE FIELDS: \`next_action\`, \`next_tool\`,
3845
+ and \`next_arguments\` name the exact next call \u2014 act on those first; the
3846
+ prose in this section is fallback and debugging detail. If the catalog
3847
+ entry is missing or degraded, the response instead names
3848
+ \`mcp__haven__haven_pay_mcp_tool\` (merchant URL, tool name, arguments) as
3849
+ the manual fallback.
3850
+
3851
+ **Signing:** \`mcp__haven-signer__haven_sign_x402\` with \`payment_id\` and
3852
+ \`payment_required\` ONLY \u2014 the local signer fetches the exact signing bytes
3853
+ itself, so never relay \`typed_data\` yourself. Fallback for an older signer
3854
+ or backend: re-run the quote/prepare tool with the SAME \`idempotency_key\`
3855
+ plus \`include_signing_payload=true\`, then pass \`payload_hash\`,
3856
+ \`x402_expected\` (the nested \`x402.expected\` object), and
3857
+ \`typed_data\`/\`typed_data_b64\` through unchanged.
3858
+
3859
+ **Settle:** \`mcp__haven__haven_settle_mcp_tool\` with \`payment_id\`,
3860
+ \`signature\`, and \`payment_header\` ONLY \u2014 Haven rehydrates the merchant call
3861
+ context (\`merchant_url\`, \`tool_name\`, \`arguments\`, \`mcp_transport\`)
3862
+ server-side from \`payment_id\`. Pass those four fields explicitly only as a
3863
+ version-skew fallback when Haven has no stored context for the id \u2014 both or
3864
+ none together, never just one. If the settle result carries \`settled: false\`,
3865
+ funding is queued for the user's approval \u2014 tell them and check status later,
3866
+ do not re-pay.
3867
+
3868
+ Step-by-step alternative (also key-safe; for an older signer or backend, or
3869
+ when you already have a merchant URL and tool name instead of a
3870
+ \`catalog_id\`): \`mcp__haven__haven_pay_mcp_tool\` then
3871
+ \`mcp__haven-signer__haven_sign\` \u2192 \`mcp__haven__haven_submit\` \u2192
3872
+ \`mcp__haven-signer__haven_x402_sign_header\` \u2192
3873
+ \`mcp__haven__haven_complete_mcp_tool\`. Pass \`payment_required\`,
3874
+ \`arguments\`, and \`mcp_transport\` verbatim from the quote/prepare result.
3875
+ The returned \`expires_at\` is the signing window; if a tool returns
3876
+ \`PAYMENT_WINDOW_EXPIRED\`, re-run the same quote/prepare tool with the same
3877
+ \`idempotency_key\`. Do not call the merchant yourself \u2014 Haven completes the
3878
+ merchant leg for you.
3879
+
3880
+ **Direct transfer / non-MCP paywall:** \`mcp__haven__haven_pay\` with
3881
+ recipient, amount, and token for a plain transfer. For an arbitrary,
3882
+ non-MCP x402 paywall: \`mcp__haven__haven_quote_x402\` to get a quote, then
3883
+ \`mcp__haven__haven_pay_x402_quote\` \u2014 follow the result's guidance fields
3884
+ first, sign in the local Haven signer, and retry the original request only
3885
+ when the result says \`retry_original_x402_request\`.
3886
+
3887
+ **Catalog tool arguments:** when \`haven_discover_tools\` returns
3888
+ \`tool_arguments\`, pass that object unchanged as the pay tool's
3889
+ \`arguments\` field (for example
3890
+ \`tool_arguments: { "tier": "50gb" }\` -> \`arguments: { "tier": "50gb" }\`).
3891
+
3892
+ **Prices:** show the user the live price from the pay-tool result, never a
3893
+ catalog price. \`haven_discover_tools\` prices are indicative
3894
+ (\`price_is_indicative\`) and can be stale. The pay-tool result's \`amount\` /
3895
+ \`amount_atomic\` is the amount Haven authorizes for the call \u2014 a ceiling the
3896
+ merchant settles at or below \u2014 so present it as the most the user will pay.
3897
+
3898
+ **Status:** \`mcp__haven__haven_get_payment_status\` with a \`payment_id\` to
3899
+ check on queued or in-flight payments. Do not poll in a tight loop.
3581
3900
 
3582
3901
  ## Approval semantics
3583
3902
 
3584
3903
  - A result with \`pending_approval\` means the payment exceeded the remaining
3585
3904
  budget and is waiting for the user in Haven. Tell the user, then check
3586
3905
  status later.
3906
+ - \`safe_to_continue: false\` on a guidance block is the same signal in
3907
+ machine-readable form: stop and involve the user before calling anything
3908
+ else for this payment.
3587
3909
  - Never ask the user for private keys. Signing happens only in the local Haven
3588
3910
  signer; the hosted Haven tools never receive the signing key. If a tool
3589
3911
  reports a missing or invalid credential, tell the user to re-run the Haven
@@ -3600,12 +3922,28 @@ present and surface \`message\` or \`error\` verbatim. Common cases:
3600
3922
  Suggest the user add funds in the Haven dashboard.
3601
3923
  - \`PRICE_EXCEEDS_MAX\`: the live merchant price exceeded your \`max_amount\`.
3602
3924
  No funds moved; ask the user before retrying with a higher cap.
3603
- - \`PAYMENT_WINDOW_EXPIRED\`: re-run \`mcp__haven__haven_pay_mcp_tool\` with the same
3604
- \`idempotency_key\`, then sign the fresh \`payload_hash\`.
3605
- - \`MERCHANT_REJECTED_AFTER_FUNDING\`: stop retrying the merchant and use
3925
+ - \`PAYMENT_WINDOW_EXPIRED\`: re-run the quote/prepare tool with the same
3926
+ \`idempotency_key\`, then sign the fresh payload.
3927
+ - \`MERCHANT_REJECTED_AFTER_FUNDING\`: the merchant refused the paid retry.
3928
+ Stop-and-sweep \u2014 stop retrying the merchant and use
3606
3929
  \`mcp__haven__haven_sweep_delegate\` to recover stranded delegate funds.
3930
+ - \`MERCHANT_UNRESPONSIVE_AFTER_FUNDING\`: funding confirmed on-chain, but the
3931
+ merchant never answered the paid retry. This is NOT proof of rejection \u2014 the
3932
+ merchant may still settle late. Verify-then-sweep, never a blind sweep:
3933
+ check \`mcp__haven__haven_get_payment_status\`, retry
3934
+ \`mcp__haven__haven_complete_mcp_tool\` ONCE, and only sweep with
3935
+ \`mcp__haven__haven_sweep_delegate\` if no settlement appears.
3607
3936
  - Budget exceeded: tell the user how much remains (from
3608
- \`haven_get_allowances\`) and that they can raise the budget in Haven.
3937
+ \`mcp__haven__haven_get_allowances\`) and that they can raise the budget in
3938
+ Haven.
3939
+
3940
+ ## Reporting after a purchase
3941
+
3942
+ A settled \`mcp__haven__haven_settle_mcp_tool\` response carries
3943
+ \`agent_summary\` and the remaining post-purchase allowance in \`allowance\` \u2014
3944
+ report the amount paid and what is left from those fields directly. Do not
3945
+ call \`haven_get_agent\` or \`haven_get_allowances\` again just to report a
3946
+ purchase you already made.
3609
3947
 
3610
3948
  ## Revoke
3611
3949
 
@@ -3759,6 +4097,50 @@ function stableStringify2(value) {
3759
4097
  return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${stableStringify2(object[key])}`).join(",")}}`;
3760
4098
  }
3761
4099
 
4100
+ // src/merchant-discovery.ts
4101
+ var MERCHANT_DISCOVERY_PATHS = ["/.well-known/haven-demo-merchant", "/"];
4102
+ var DISCOVERY_MAX_BYTES = 64 * 1024;
4103
+ async function discoverMerchantMcpUrl(inputUrl) {
4104
+ let input;
4105
+ try {
4106
+ input = new URL(inputUrl);
4107
+ } catch {
4108
+ return null;
4109
+ }
4110
+ for (const path of MERCHANT_DISCOVERY_PATHS) {
4111
+ try {
4112
+ const res = await globalThis.fetch(`${input.origin}${path}`, {
4113
+ method: "GET",
4114
+ headers: { accept: "application/json" },
4115
+ redirect: "error",
4116
+ signal: AbortSignal.timeout(5e3)
4117
+ });
4118
+ if (!res.ok) continue;
4119
+ const contentLength = Number(res.headers.get("content-length") ?? 0);
4120
+ if (contentLength > DISCOVERY_MAX_BYTES) continue;
4121
+ const text = await res.text();
4122
+ if (text.length > DISCOVERY_MAX_BYTES) continue;
4123
+ const doc = JSON.parse(text);
4124
+ if (typeof doc.mcp_url !== "string") continue;
4125
+ const resolved = new URL(doc.mcp_url);
4126
+ if (resolved.origin !== input.origin) continue;
4127
+ return resolved.toString();
4128
+ } catch {
4129
+ continue;
4130
+ }
4131
+ }
4132
+ return null;
4133
+ }
4134
+ function sameUrl(a, b) {
4135
+ try {
4136
+ const ua = new URL(a);
4137
+ const ub = new URL(b);
4138
+ return ua.origin === ub.origin && ua.pathname.replace(/\/+$/, "") === ub.pathname.replace(/\/+$/, "");
4139
+ } catch {
4140
+ return false;
4141
+ }
4142
+ }
4143
+
3762
4144
  exports.AGENT_PAYMENT_FAILURE_CODE_VALUES = AGENT_PAYMENT_FAILURE_CODE_VALUES;
3763
4145
  exports.AGENT_PAYMENT_NEXT_ACTION_VALUES = AGENT_PAYMENT_NEXT_ACTION_VALUES;
3764
4146
  exports.AGENT_PAYMENT_PHASE_VALUES = AGENT_PAYMENT_PHASE_VALUES;
@@ -3775,6 +4157,8 @@ exports.AgentPaymentPhaseSchema = AgentPaymentPhaseSchema;
3775
4157
  exports.AgentPaymentRail = AgentPaymentRail;
3776
4158
  exports.AgentPaymentRailDescriptions = AgentPaymentRailDescriptions;
3777
4159
  exports.AgentPaymentRailSchema = AgentPaymentRailSchema;
4160
+ exports.AgentPaymentWarningCode = AgentPaymentWarningCode;
4161
+ exports.DISCOVERY_MAX_BYTES = DISCOVERY_MAX_BYTES;
3778
4162
  exports.HAVEN_MINIMUM_NODE_VERSION = HAVEN_MINIMUM_NODE_VERSION;
3779
4163
  exports.HAVEN_SKILL_MD = HAVEN_SKILL_MD;
3780
4164
  exports.HavenApiError = HavenApiError;
@@ -3783,13 +4167,19 @@ exports.HavenError = HavenError;
3783
4167
  exports.HavenPaymentStateError = HavenPaymentStateError;
3784
4168
  exports.HavenSigningError = HavenSigningError;
3785
4169
  exports.HavenTimeoutError = HavenTimeoutError;
4170
+ exports.HavenUnsupportedSignerVersionError = HavenUnsupportedSignerVersionError;
4171
+ exports.MERCHANT_DISCOVERY_PATHS = MERCHANT_DISCOVERY_PATHS;
4172
+ exports.MerchantTimeoutError = MerchantTimeoutError;
3786
4173
  exports.RECEIPT_VERSION = RECEIPT_VERSION;
4174
+ exports.SIGNER_UPDATE_FALLBACK = SIGNER_UPDATE_FALLBACK;
3787
4175
  exports.SKILL_FOLDER_NAME = SKILL_FOLDER_NAME;
3788
4176
  exports.SWEEP_BASE_CHAIN_ID = SWEEP_BASE_CHAIN_ID;
3789
4177
  exports.SWEEP_BASE_SEPOLIA_CHAIN_ID = SWEEP_BASE_SEPOLIA_CHAIN_ID;
3790
4178
  exports.SWEEP_BASE_SEPOLIA_USDC_ADDRESS = SWEEP_BASE_SEPOLIA_USDC_ADDRESS;
3791
4179
  exports.SWEEP_BASE_USDC_ADDRESS = SWEEP_BASE_USDC_ADDRESS;
4180
+ exports.SignerRefusalCode = SignerRefusalCode;
3792
4181
  exports.TRANSFER_WITH_AUTHORIZATION_TYPES = TRANSFER_WITH_AUTHORIZATION_TYPES;
4182
+ exports.X402UnexpectedStatusError = X402UnexpectedStatusError;
3793
4183
  exports.X402_MAX_AUTHORIZATION_WINDOW_SECONDS = X402_MAX_AUTHORIZATION_WINDOW_SECONDS;
3794
4184
  exports.X402_SETTLEMENT_FORWARD_MARGIN_SECONDS = X402_SETTLEMENT_FORWARD_MARGIN_SECONDS;
3795
4185
  exports.addressFromKey = addressFromKey;
@@ -3801,6 +4191,7 @@ exports.compareNodeVersions = compareNodeVersions;
3801
4191
  exports.composeDescription = composeDescription;
3802
4192
  exports.decodeBase64Json = decodeBase64Json;
3803
4193
  exports.decodeBase64Utf8 = decodeBase64Utf8;
4194
+ exports.discoverMerchantMcpUrl = discoverMerchantMcpUrl;
3804
4195
  exports.encodeBase64Json = encodeBase64Json;
3805
4196
  exports.encodeBase64Utf8 = encodeBase64Utf8;
3806
4197
  exports.encodeMachinePaymentProof = encodeMachinePaymentProof;
@@ -3812,6 +4203,7 @@ exports.parseMachinePaymentChallenge = parseMachinePaymentChallenge;
3812
4203
  exports.parseMachinePaymentChallengeResponse = parseMachinePaymentChallengeResponse;
3813
4204
  exports.parsePaymentRequired = parsePaymentRequired;
3814
4205
  exports.parsePaymentRequiredResponse = parsePaymentRequiredResponse;
4206
+ exports.sameUrl = sameUrl;
3815
4207
  exports.selectPaymentOption = selectPaymentOption;
3816
4208
  exports.selectStandardPaymentOption = selectStandardPaymentOption;
3817
4209
  exports.signHash = signHash;