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