@haven_ai/sdk 0.1.21-alpha.0 → 0.1.23-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,37 @@ 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",
106
+ /**
107
+ * #1351: the caller supplied BOTH the atomic `max_amount` and the
108
+ * human-denominated `max_amount_human` cap for one purchase. Haven refuses
109
+ * to guess which the user meant — the two differ by a factor of 10^decimals,
110
+ * so picking wrong is exactly the silent-overspend this cap exists to
111
+ * prevent. Rejected before any merchant probe, funding intent, or signature.
112
+ */
113
+ AmbiguousMaxAmount: "AMBIGUOUS_MAX_AMOUNT",
114
+ /**
115
+ * #1351: a human-denominated cap was supplied, but it cannot be converted to
116
+ * atomic units against THIS quote — either the quote's asset has no known
117
+ * decimals on its network, or the cap carries more fraction digits than the
118
+ * asset can represent (truncating it would silently change the user's cap).
119
+ * The fallback is the exact atomic `max_amount`.
120
+ */
121
+ MaxAmountUnconvertible: "MAX_AMOUNT_UNCONVERTIBLE"
90
122
  };
91
123
  var AgentPaymentRail = {
92
124
  /** Standard Haven payment from the user's Safe through an approved delegate allowance. */
@@ -133,12 +165,48 @@ var AgentPaymentNextActionDescriptions = {
133
165
  [AgentPaymentNextAction.RequestAgainIfUserStillWantsIt]: "Ask again only if the user still wants the payment after expiry.",
134
166
  [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
167
  [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.",
168
+ [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
169
  [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
170
  };
138
171
  var AgentPaymentFailureCodeDescriptions = {
139
172
  [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
173
  [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."
174
+ [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.",
175
+ [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.",
176
+ [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.",
177
+ [AgentPaymentFailureCode.AmbiguousMaxAmount]: "Both max_amount (atomic units) and max_amount_human (whole tokens) were supplied for one purchase. Nothing was contacted and nothing was spent. Re-send with exactly ONE: max_amount_human for a cap the user stated in tokens, max_amount for an exact atomic figure.",
178
+ [AgentPaymentFailureCode.MaxAmountUnconvertible]: "max_amount_human could not be converted to atomic units against this quote's asset \u2014 either its decimals are unknown to Haven or the cap has more decimal places than the asset supports. Nothing was spent. Round the cap, or re-send it as an exact atomic max_amount."
179
+ };
180
+ var AgentPaymentWarningCode = {
181
+ /** No max_amount cap was supplied — the live quoted price was accepted as-is. */
182
+ MissingMaxAmount: "MISSING_MAX_AMOUNT",
183
+ /** The signing window closes soon; sign promptly or re-quote with the same idempotency key. */
184
+ QuoteExpiresSoon: "QUOTE_EXPIRES_SOON",
185
+ /** The merchant URL was resolved via discovery — pass the RESOLVED url forward. */
186
+ MerchantUrlDiscovered: "MERCHANT_URL_DISCOVERED",
187
+ /**
188
+ * #1306: the catalog's last-verified price_atomic differs from the LIVE
189
+ * merchant quote for a guided catalog purchase. The catalog price is only
190
+ * ever indicative; the live quote in the same response is authoritative.
191
+ */
192
+ CatalogPriceDiffers: "CATALOG_PRICE_DIFFERS",
193
+ /**
194
+ * #1306: the rail-aware allowance/budget pre-check could not be read (RPC
195
+ * failure, etc). `sufficient` is reported as null rather than a fabricated
196
+ * true/false — the on-chain policy remains the actual gate either way.
197
+ */
198
+ AllowanceCheckUnavailable: "ALLOWANCE_CHECK_UNAVAILABLE",
199
+ /**
200
+ * #1319: the delegation-rail read itself SUCCEEDED, but the remaining
201
+ * figure it returned is the #1145 fallback (the full configured budget)
202
+ * rather than a live ERC20PeriodTransferEnforcer read — `sufficient` is a
203
+ * real true/false, just computed from an optimistic number. Distinct from
204
+ * {@link AgentPaymentWarningCode.AllowanceCheckUnavailable}, which fires
205
+ * when the read failed outright and `sufficient` degrades to null. The
206
+ * on-chain policy re-checks at redemption either way; this only says the
207
+ * guidance shown here may be optimistic.
208
+ */
209
+ AllowanceReadOptimistic: "ALLOWANCE_READ_OPTIMISTIC"
142
210
  };
143
211
  var AgentPaymentRailDescriptions = {
144
212
  [AgentPaymentRail.Direct]: "Standard Haven payment from the user-controlled Safe through an approved delegate allowance.",
@@ -193,6 +261,20 @@ var HavenApiError = class extends HavenError {
193
261
  }
194
262
  body;
195
263
  };
264
+ var MerchantTimeoutError = class extends HavenApiError {
265
+ merchantErrorCode = "merchant_timeout";
266
+ constructor(message) {
267
+ super(message, 504);
268
+ this.name = "MerchantTimeoutError";
269
+ }
270
+ };
271
+ var X402UnexpectedStatusError = class extends HavenApiError {
272
+ x402ErrorCode = "unexpected_non_402_status";
273
+ constructor(message, statusCode) {
274
+ super(message, statusCode);
275
+ this.name = "X402UnexpectedStatusError";
276
+ }
277
+ };
196
278
  var HavenPaymentStateError = class extends HavenApiError {
197
279
  constructor(message, statusCode, state, body) {
198
280
  super(message, statusCode, body, state.paymentId);
@@ -217,6 +299,25 @@ var HavenSigningError = class extends HavenError {
217
299
  this.name = "HavenSigningError";
218
300
  }
219
301
  };
302
+ var SignerRefusalCode = {
303
+ /** `SUPPORTED_X402_EXPECTED_VERSIONS` in `@haven_ai/signer` does not include the received version. */
304
+ UnsupportedExpectedContextVersion: "UNSUPPORTED_EXPECTED_CONTEXT_VERSION",
305
+ /** `SUPPORTED_SWEEP_BINDING_VERSIONS` in `@haven_ai/signer` does not include the received version. */
306
+ UnsupportedSweepBindingVersion: "UNSUPPORTED_SWEEP_BINDING_VERSION"
307
+ };
308
+ 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.";
309
+ var HavenUnsupportedSignerVersionError = class extends HavenError {
310
+ constructor(message, code, supportedVersions, receivedVersion, fallback) {
311
+ super(message, code);
312
+ this.supportedVersions = supportedVersions;
313
+ this.receivedVersion = receivedVersion;
314
+ this.fallback = fallback;
315
+ this.name = "HavenUnsupportedSignerVersionError";
316
+ }
317
+ supportedVersions;
318
+ receivedVersion;
319
+ fallback;
320
+ };
220
321
  var HavenTimeoutError = class extends HavenError {
221
322
  constructor(paymentId) {
222
323
  super(
@@ -695,6 +796,8 @@ function explorerUrlOrEmpty(chainId, txHash) {
695
796
  return txHash ? buildExplorerUrl(chainId, txHash) : "";
696
797
  }
697
798
  var DEFAULT_REQUEST_TIMEOUT = 3e4;
799
+ var DEFAULT_MERCHANT_TIMEOUT = 3e5;
800
+ var NOTIFY_TIMEOUT = 1e4;
698
801
  var DEFAULT_CONFIRMATION_TIMEOUT = 9e4;
699
802
  var DEFAULT_POLLING_INTERVAL = 3e3;
700
803
  function formatAtomicAmount(atomic, decimals) {
@@ -858,12 +961,32 @@ function x402TypedDataDigest(typedData) {
858
961
  );
859
962
  }
860
963
  }
964
+ function mapCatalogEntry(entry) {
965
+ return {
966
+ id: entry.id,
967
+ name: entry.name,
968
+ description: entry.description,
969
+ category: entry.category,
970
+ resourceUrl: entry.resource_url,
971
+ rail: entry.rail,
972
+ protocol: entry.protocol,
973
+ toolName: entry.tool_name,
974
+ toolArguments: entry.tool_arguments ?? null,
975
+ priceDisplay: entry.price_display,
976
+ priceAtomic: entry.price_atomic,
977
+ asset: entry.asset,
978
+ network: entry.network,
979
+ status: entry.status,
980
+ verifiedAt: entry.verified_at
981
+ };
982
+ }
861
983
  var HavenClient = class {
862
984
  apiKey;
863
985
  delegateKey;
864
986
  baseUrl;
865
987
  x402Wallet;
866
988
  requestTimeout;
989
+ merchantTimeout;
867
990
  confirmationTimeout;
868
991
  pollingInterval;
869
992
  chainRpcs;
@@ -893,6 +1016,7 @@ var HavenClient = class {
893
1016
  this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
894
1017
  this.x402Wallet = config.x402Wallet;
895
1018
  this.requestTimeout = config.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT;
1019
+ this.merchantTimeout = config.merchantTimeout ?? DEFAULT_MERCHANT_TIMEOUT;
896
1020
  this.confirmationTimeout = config.confirmationTimeout ?? DEFAULT_CONFIRMATION_TIMEOUT;
897
1021
  this.pollingInterval = config.pollingInterval ?? DEFAULT_POLLING_INTERVAL;
898
1022
  this.chainRpcs = config.chainRpcs ?? {};
@@ -986,8 +1110,7 @@ var HavenClient = class {
986
1110
  400
987
1111
  );
988
1112
  }
989
- const agent = await this.getAgent();
990
- const fundingTo = agent.delegateAddress;
1113
+ const fundingTo = options.delegateAddress ?? (await this.getAgent()).delegateAddress;
991
1114
  if (!fundingTo) {
992
1115
  throw new HavenApiError("Authenticated agent has no delegate address registered.", 502);
993
1116
  }
@@ -996,11 +1119,25 @@ var HavenClient = class {
996
1119
  url: paymentRequired.resource.url,
997
1120
  payTo: fundingTo,
998
1121
  merchantPayTo: option.payTo,
1122
+ // #1360: this path ALWAYS means the EIP-3009 funding leg (payTo is the
1123
+ // agent's own delegate EOA). Saying so explicitly turns a stale/rotated
1124
+ // delegate address into the backend's LOUD shape-mismatch 400 instead
1125
+ // of a silent reroute to the erc7710 settlement branch (the #1358
1126
+ // review's open-budget misroute). Legacy-rail backends ignore the field.
1127
+ settlementScheme: "eip3009",
999
1128
  amount: x402AuthorizationAmount(option),
1000
1129
  asset: option.asset,
1001
1130
  network: option.network,
1002
1131
  description: paymentRequired.resource.description,
1003
- idempotencyKey
1132
+ idempotencyKey,
1133
+ // #1307: persisted so the settle leg can rehydrate it by payment_id.
1134
+ ...options.mcpCallContext ? { mcpCallContext: options.mcpCallContext } : {},
1135
+ // #1355: persisted so the SIGN leg can rehydrate it by payment_id — the
1136
+ // local signer's context fetch then carries the 402 PaymentRequired and
1137
+ // the agent passes only payment_id. Bounded: the backend rejects >64KB,
1138
+ // so an oversized blob is omitted here (signer falls back to the
1139
+ // caller-supplied copy) rather than failing the intent.
1140
+ ...new TextEncoder().encode(JSON.stringify(paymentRequired)).length <= 65536 ? { paymentRequired } : {}
1004
1141
  });
1005
1142
  if (raw.status !== "pending_signature") {
1006
1143
  this.throwPaymentStateError("x402 payment", raw);
@@ -1132,6 +1269,17 @@ var HavenClient = class {
1132
1269
  * Get the agent identity tied to this API key.
1133
1270
  */
1134
1271
  async getAgent() {
1272
+ if (this.agentInFlight) return this.agentInFlight;
1273
+ const request = this.fetchAgent();
1274
+ this.agentInFlight = request;
1275
+ request.finally(() => {
1276
+ this.agentInFlight = null;
1277
+ }).catch(() => {
1278
+ });
1279
+ return request;
1280
+ }
1281
+ agentInFlight = null;
1282
+ async fetchAgent() {
1135
1283
  const raw = await this.get("/machine-payments/agent");
1136
1284
  return {
1137
1285
  id: raw.id,
@@ -1139,7 +1287,12 @@ var HavenClient = class {
1139
1287
  status: raw.status,
1140
1288
  safeAddress: raw.safe_address,
1141
1289
  delegateAddress: raw.delegate_address,
1142
- chainId: raw.chain_id
1290
+ chainId: raw.chain_id,
1291
+ // Defensive normalization, not trust: the backend contract is exactly
1292
+ // 'legacy' | 'delegation' (#1306), but an older/mismatched backend
1293
+ // during a rollout window should degrade to the wider legacy bucket
1294
+ // rather than propagate an unrecognized string.
1295
+ executionRail: raw.execution_rail === "delegation" ? "delegation" : "legacy"
1143
1296
  };
1144
1297
  }
1145
1298
  /**
@@ -1289,11 +1442,120 @@ var HavenClient = class {
1289
1442
  resetTimeMin: allowance.onchain.reset_time_min,
1290
1443
  lastResetMin: allowance.onchain.last_reset_min,
1291
1444
  nonce: allowance.onchain.nonce,
1292
- isResetPending: allowance.onchain.is_reset_pending
1445
+ isResetPending: allowance.onchain.is_reset_pending,
1446
+ remainingIsFromChain: allowance.onchain.remaining_is_from_chain
1293
1447
  }
1294
1448
  }))
1295
1449
  };
1296
1450
  }
1451
+ /**
1452
+ * Post-purchase allowance/budget summary for a settled payment (#1310).
1453
+ *
1454
+ * Reuses the EXACT rail-aware read path {@link getAllowances} / #1306's
1455
+ * catalog-purchase preflight `allowance` block use — `GET
1456
+ * /machine-payments/allowances`, with delegation-rail values coming from
1457
+ * the #1090 `deriveDelegationBudgets`-backed enforcer read, never
1458
+ * `agent_allowances` — so this can never disagree with
1459
+ * {@link getAllowances} for the same fixture. The settled token is
1460
+ * resolved from {@link getPaymentStatus} so callers pass only
1461
+ * `paymentId`, never a second haven_get_agent-style round trip.
1462
+ *
1463
+ * NEVER throws: any failed read (status lookup, agent lookup, or the
1464
+ * allowance/budget lookup itself) degrades to `{ allowance: null,
1465
+ * warnings: [ALLOWANCE_CHECK_UNAVAILABLE] }` rather than converting a
1466
+ * successful settlement into a failure — the on-chain policy remains the
1467
+ * actual spend gate regardless of whether this report can be produced.
1468
+ *
1469
+ * Freshness caveat (#1319): the delegation rail's on-chain enforcer read
1470
+ * can silently fall back to the optimistic full period budget without
1471
+ * throwing when the RPC read itself fails (#1145's fund-safe design,
1472
+ * unchanged here). {@link getAllowances}'s `onchain.remainingIsFromChain`
1473
+ * now carries that provenance on the wire, and the #1306 catalog-purchase
1474
+ * preflight (`haven_prepare_catalog_purchase`) surfaces it as a warning —
1475
+ * this summary does not (yet). `remaining_atomic` here reflects the last
1476
+ * successful chain read, not a guaranteed-live one, and callers should not
1477
+ * phrase it as guaranteed-fresh.
1478
+ */
1479
+ async getPostPurchaseAllowanceSummary(paymentId) {
1480
+ const unavailable = (detail) => ({
1481
+ payment: null,
1482
+ allowance: null,
1483
+ warnings: [
1484
+ {
1485
+ code: AgentPaymentWarningCode.AllowanceCheckUnavailable,
1486
+ 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.`
1487
+ }
1488
+ ]
1489
+ });
1490
+ const [statusResult, agentResult, allowanceResult] = await Promise.allSettled([
1491
+ this.getPaymentStatus(paymentId),
1492
+ this.getAgent(),
1493
+ this.getAllowances()
1494
+ ]);
1495
+ if (statusResult.status === "rejected") {
1496
+ return unavailable(statusResult.reason instanceof Error ? statusResult.reason.message : String(statusResult.reason));
1497
+ }
1498
+ const status = statusResult.value;
1499
+ if (agentResult.status === "rejected") {
1500
+ return { ...unavailable(agentResult.reason instanceof Error ? agentResult.reason.message : String(agentResult.reason)), payment: status };
1501
+ }
1502
+ if (allowanceResult.status === "rejected") {
1503
+ return { ...unavailable(allowanceResult.reason instanceof Error ? allowanceResult.reason.message : String(allowanceResult.reason)), payment: status };
1504
+ }
1505
+ try {
1506
+ const tokenAddress = status.asset ?? status.x402?.asset ?? null;
1507
+ if (!tokenAddress) {
1508
+ return { ...unavailable("the settled payment does not carry a resolvable token address"), payment: status };
1509
+ }
1510
+ const rail = agentResult.value.executionRail;
1511
+ const source = rail === "delegation" ? "active_delegations" : "allowance_module";
1512
+ const match = allowanceResult.value.allowances.find(
1513
+ (a) => a.tokenAddress.toLowerCase() === tokenAddress.toLowerCase()
1514
+ );
1515
+ if (!match) {
1516
+ return { ...unavailable("no allowance/budget row matches the settled token"), payment: status };
1517
+ }
1518
+ const token = resolveTokenFromAddress(match.tokenAddress);
1519
+ const remainingDisplay = token ? `${formatAtomicAmount(safeBigInt(match.onchain.remaining), token.decimals)} ${match.tokenSymbol}` : void 0;
1520
+ return {
1521
+ payment: status,
1522
+ allowance: {
1523
+ rail,
1524
+ remaining_atomic: match.onchain.remaining,
1525
+ ...remainingDisplay ? { remaining_display: remainingDisplay } : {},
1526
+ token_symbol: match.tokenSymbol,
1527
+ token_address: match.tokenAddress,
1528
+ reset_period: match.resetPeriodMin,
1529
+ source
1530
+ },
1531
+ warnings: []
1532
+ };
1533
+ } catch (err) {
1534
+ return unavailable(err instanceof Error ? err.message : String(err));
1535
+ }
1536
+ }
1537
+ /**
1538
+ * `haven_get_payment_status` convenience: fetch status and, for a
1539
+ * genuinely SETTLED x402 payment, attach the same post-purchase
1540
+ * allowance/budget summary a settle response carries.
1541
+ *
1542
+ * #1310/#1311 parity: this is the ONE home for logic that was duplicated
1543
+ * verbatim in `packages/mcp-server/src/tools.ts` and `packages/mcp/src/tools.ts`
1544
+ * (both hosted and local `haven_get_payment_status` handlers) — extracted
1545
+ * here because both packages already depend on `@haven_ai/sdk` and call
1546
+ * methods on a `HavenClient` instance, so this needed no new dependency
1547
+ * edge. `funded_but_unsettled` is deliberately excluded: that phase means
1548
+ * the merchant did NOT accept the retry. Every other phase/rail returns
1549
+ * the status untouched.
1550
+ */
1551
+ async getPaymentStatusWithPostPurchaseAllowance(paymentId) {
1552
+ const status = await this.getPaymentStatus(paymentId);
1553
+ if (status.rail === AgentPaymentRail.X402 && status.phase === AgentPaymentPhase.PaymentConfirmed) {
1554
+ const { allowance, warnings } = await this.getPostPurchaseAllowanceSummary(paymentId);
1555
+ return { ...status, allowance, ...warnings.length > 0 ? { warnings } : {} };
1556
+ }
1557
+ return status;
1558
+ }
1297
1559
  /**
1298
1560
  * Discover payable services from Haven's curated merchant catalog.
1299
1561
  *
@@ -1304,25 +1566,24 @@ var HavenClient = class {
1304
1566
  async discoverTools(options = {}) {
1305
1567
  const params = new URLSearchParams();
1306
1568
  if (options.category) params.set("category", options.category);
1569
+ if (options.search !== void 0) params.set("search", options.search);
1307
1570
  if (options.rail) params.set("rail", options.rail);
1308
1571
  const query = params.size > 0 ? `?${params.toString()}` : "";
1309
1572
  const raw = await this.get(`/catalog${query}`);
1310
- return raw.entries.map((entry) => ({
1311
- id: entry.id,
1312
- name: entry.name,
1313
- description: entry.description,
1314
- category: entry.category,
1315
- resourceUrl: entry.resource_url,
1316
- rail: entry.rail,
1317
- protocol: entry.protocol,
1318
- toolName: entry.tool_name,
1319
- priceDisplay: entry.price_display,
1320
- priceAtomic: entry.price_atomic,
1321
- asset: entry.asset,
1322
- network: entry.network,
1323
- status: entry.status,
1324
- verifiedAt: entry.verified_at
1325
- }));
1573
+ return raw.entries.map(mapCatalogEntry);
1574
+ }
1575
+ /**
1576
+ * Fetch one curated catalog entry by id (#1306).
1577
+ *
1578
+ * Chain-scoped for free by the backend's SQL when the client is
1579
+ * agent-authenticated (#1299): an unknown id and an id curated for a
1580
+ * DIFFERENT chain than this agent's both 404 identically — this method does
1581
+ * not (and must not) re-filter by chain in JS. Read-only, like
1582
+ * {@link discoverTools}.
1583
+ */
1584
+ async getCatalogEntry(id) {
1585
+ const raw = await this.get(`/catalog/${encodeURIComponent(id)}`);
1586
+ return mapCatalogEntry(raw);
1326
1587
  }
1327
1588
  /**
1328
1589
  * List recent machine-payment receipts/evidence for bookkeeping.
@@ -1421,9 +1682,9 @@ var HavenClient = class {
1421
1682
  async quoteX402(url, init, options = {}) {
1422
1683
  const initialInit = this.withX402Wallet(init, this.x402PayerAddress());
1423
1684
  const request = this.snapshotX402Request(url, initialInit);
1424
- const response = await globalThis.fetch(url, initialInit);
1685
+ const response = await this.merchantFetch(url, initialInit);
1425
1686
  if (response.status !== 402) {
1426
- throw new HavenApiError(
1687
+ throw new X402UnexpectedStatusError(
1427
1688
  `Expected an x402 quote response with HTTP 402, got HTTP ${response.status}.`,
1428
1689
  response.status || 400
1429
1690
  );
@@ -1435,6 +1696,34 @@ var HavenClient = class {
1435
1696
  const mcpTransport = await this.detectX402McpTransport(url, paymentRequired, response);
1436
1697
  return this.buildX402Quote(paymentRequired, request, options.idempotencyKey, mcpTransport);
1437
1698
  }
1699
+ /**
1700
+ * Probe an MCP tool for its x402 quote without creating a payment.
1701
+ *
1702
+ * Unlike the generic {@link quoteX402} helper, this completes the
1703
+ * Streamable-HTTP MCP lifecycle before sending the unpaid `tools/call`.
1704
+ * Hosted MCP uses this path while remaining keyless: it resolves only the
1705
+ * agent's public delegate address for `x402-wallet`; signing remains local.
1706
+ * It refuses before the quote when the merchant does not establish a session;
1707
+ * callers that need a plain x402 endpoint must use {@link quoteX402}.
1708
+ */
1709
+ async quoteMcpX402(url, init, options = {}) {
1710
+ const wallet = await this.resolveX402WalletForMerchantCall();
1711
+ const sessionId = await this.mcpInitialize(url, init, wallet);
1712
+ if (!sessionId) {
1713
+ throw new HavenApiError(
1714
+ "The merchant did not establish an MCP session before the x402 quote. No payment was created.",
1715
+ 502,
1716
+ { mcpSessionNotEstablished: true }
1717
+ );
1718
+ }
1719
+ let requestInit = this.withX402Wallet(init, wallet);
1720
+ requestInit = this.withMcpHeaders(requestInit, sessionId);
1721
+ const quote = await this.quoteX402(url, requestInit, options);
1722
+ return {
1723
+ ...quote,
1724
+ mcpTransport: quote.mcpTransport ?? { handshakeRequired: true, source: "path" }
1725
+ };
1726
+ }
1438
1727
  /**
1439
1728
  * Pay a previously inspected x402 quote and retry the exact captured request.
1440
1729
  */
@@ -1469,7 +1758,11 @@ var HavenClient = class {
1469
1758
  asset: option.asset,
1470
1759
  network: option.network,
1471
1760
  description: paymentRequired.resource.description,
1472
- idempotencyKey
1761
+ idempotencyKey,
1762
+ // #1360: same explicit funding-leg declaration as createX402Intent —
1763
+ // this local-key path derives payTo from the key (never stale), but the
1764
+ // declaration keeps both writers of the 3009 shape loud-by-default.
1765
+ settlementScheme: "eip3009"
1473
1766
  });
1474
1767
  if (raw.success && raw.tx_hash) {
1475
1768
  const receipt2 = this.mapX402ReceiptFromAuthorization(paymentRequired, option, paymentHeader, raw);
@@ -1540,7 +1833,7 @@ var HavenClient = class {
1540
1833
  if (!url) {
1541
1834
  throw new HavenApiError("x402 resume requires the original URL or a captured request snapshot.", 400);
1542
1835
  }
1543
- const response = await globalThis.fetch(url, initialInit);
1836
+ const response = await this.merchantFetch(url, initialInit);
1544
1837
  if (response.status !== 402) {
1545
1838
  throw new HavenApiError("Expected the original x402 request to return HTTP 402 before resuming.", 400);
1546
1839
  }
@@ -1583,7 +1876,7 @@ var HavenClient = class {
1583
1876
  }
1584
1877
  let requestInit = this.withX402Wallet(init, this.x402PayerAddress());
1585
1878
  if (mcpSessionId) requestInit = this.withMcpHeaders(requestInit, mcpSessionId);
1586
- const response = await globalThis.fetch(url, requestInit);
1879
+ const response = await this.merchantFetch(url, requestInit);
1587
1880
  if (response.status !== 402) {
1588
1881
  return mcpSessionId ? this.surfaceMcpResult(response) : response;
1589
1882
  }
@@ -1644,7 +1937,7 @@ var HavenClient = class {
1644
1937
  headers.set("Content-Type", "application/json");
1645
1938
  headers.set("Accept", MCP_ACCEPT);
1646
1939
  if (wallet && !headers.has("x402-wallet")) headers.set("x402-wallet", wallet);
1647
- const response = await globalThis.fetch(url, {
1940
+ const response = await this.merchantFetch(url, {
1648
1941
  method: "POST",
1649
1942
  headers,
1650
1943
  body: JSON.stringify({
@@ -1681,11 +1974,15 @@ var HavenClient = class {
1681
1974
  headers.set("Accept", MCP_ACCEPT);
1682
1975
  headers.set("mcp-session-id", sessionId);
1683
1976
  if (wallet && !headers.has("x402-wallet")) headers.set("x402-wallet", wallet);
1684
- await globalThis.fetch(url, {
1685
- method: "POST",
1686
- headers,
1687
- body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" })
1688
- });
1977
+ await this.merchantFetch(
1978
+ url,
1979
+ {
1980
+ method: "POST",
1981
+ headers,
1982
+ body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" })
1983
+ },
1984
+ NOTIFY_TIMEOUT
1985
+ );
1689
1986
  } catch {
1690
1987
  }
1691
1988
  }
@@ -1751,7 +2048,7 @@ var HavenClient = class {
1751
2048
  return this.buildMppQuote(challengeOrUrl, request2, options.idempotencyKey);
1752
2049
  }
1753
2050
  const request = this.snapshotX402Request(challengeOrUrl, init);
1754
- const response = await globalThis.fetch(challengeOrUrl, init);
2051
+ const response = await this.merchantFetch(challengeOrUrl, init);
1755
2052
  if (response.status !== 402) {
1756
2053
  throw new HavenApiError(
1757
2054
  `Expected an MPP quote response with HTTP 402, got HTTP ${response.status}.`,
@@ -1793,7 +2090,7 @@ var HavenClient = class {
1793
2090
  }
1794
2091
  const retryHeaders = new Headers(initialInit?.headers);
1795
2092
  retryHeaders.set("X-PAYMENT", receipt.paymentHeader);
1796
- const retryResponse = await globalThis.fetch(url, {
2093
+ const retryResponse = await this.merchantFetch(url, {
1797
2094
  ...initialInit,
1798
2095
  headers: retryHeaders
1799
2096
  });
@@ -1926,7 +2223,7 @@ var HavenClient = class {
1926
2223
  const headers = new Headers(requestInit.headers);
1927
2224
  headers.set("X-PAYMENT", input.paymentHeader);
1928
2225
  requestInit = { ...requestInit, headers };
1929
- const response = await globalThis.fetch(input.url, requestInit);
2226
+ const response = await this.merchantFetch(input.url, requestInit);
1930
2227
  const surfaced = mcpSessionId ? await this.surfaceMcpResult(response) : response;
1931
2228
  const protocolReceiptHeader = surfaced.headers.get("PAYMENT-RESPONSE") ?? void 0;
1932
2229
  const settlement = parseMerchantSettlement(protocolReceiptHeader ?? null);
@@ -1974,6 +2271,33 @@ var HavenClient = class {
1974
2271
  settlementTxHash: settlement.settlementTxHash ?? void 0
1975
2272
  };
1976
2273
  }
2274
+ /**
2275
+ * GET /x402/:id/merchant-call-context — the settle-leg twin of #1263's
2276
+ * sign-context fetch (#1307). Re-serves the stored merchant MCP-tool call
2277
+ * context (merchant_url, tool_name, arguments, mcp_transport) recorded at
2278
+ * quote time, so `haven_settle_mcp_tool` / `haven_complete_mcp_tool` can
2279
+ * omit those fields and let Haven rehydrate them by payment_id instead of
2280
+ * the caller re-threading them. Throws `HavenApiError` (404 unknown/foreign
2281
+ * payment_id, 409 no stored context, 410 expired) — the caller decides the
2282
+ * fallback (re-send the full context explicitly).
2283
+ */
2284
+ async getX402MerchantCallContext(paymentId) {
2285
+ const raw = await this.get(
2286
+ `/x402/${paymentId}/merchant-call-context`
2287
+ );
2288
+ return {
2289
+ paymentId: raw.payment_id,
2290
+ merchantUrl: raw.merchant_url,
2291
+ toolName: raw.tool_name,
2292
+ arguments: raw.arguments ?? {},
2293
+ ...raw.mcp_transport ? {
2294
+ mcpTransport: {
2295
+ handshakeRequired: raw.mcp_transport.handshake_required,
2296
+ source: raw.mcp_transport.source
2297
+ }
2298
+ } : {}
2299
+ };
2300
+ }
1977
2301
  async resolveX402MerchantCompletionContext(input) {
1978
2302
  const status = await this.getPaymentStatus(input.paymentId);
1979
2303
  if (status.rail !== "x402") {
@@ -2089,7 +2413,7 @@ var HavenClient = class {
2089
2413
  if (!url) {
2090
2414
  throw new HavenApiError("MPP resume requires the original URL or a captured request snapshot.", 400);
2091
2415
  }
2092
- const response = await globalThis.fetch(url, initialInit);
2416
+ const response = await this.merchantFetch(url, initialInit);
2093
2417
  if (response.status !== 402) {
2094
2418
  throw new HavenApiError("Expected the original MPP request to return HTTP 402 before resuming.", 400);
2095
2419
  }
@@ -2122,7 +2446,7 @@ var HavenClient = class {
2122
2446
  async retryMppRequest(url, initialInit, challenge, receipt) {
2123
2447
  const retryHeaders = new Headers(initialInit?.headers);
2124
2448
  retryHeaders.set("MACHINE-PAYMENT-PROOF", receipt.proofHeader);
2125
- const retryResponse = await globalThis.fetch(url, {
2449
+ const retryResponse = await this.merchantFetch(url, {
2126
2450
  ...initialInit,
2127
2451
  headers: retryHeaders
2128
2452
  });
@@ -2641,6 +2965,11 @@ var HavenClient = class {
2641
2965
  amountAtomic: x402AuthorizationAmount(option),
2642
2966
  amount: decimalFromUsdcAtomic(x402AuthorizationAmount(option)),
2643
2967
  token: token?.symbol ?? "USDC",
2968
+ // #1351: null when the asset is unrecognised on this network — the
2969
+ // `token` fallback above is a LABEL, not evidence of 6 decimals, and a
2970
+ // human-denominated cap must fail closed rather than convert against a
2971
+ // guess. Same resolution as `token`, so the two never disagree.
2972
+ decimals: token?.decimals ?? null,
2644
2973
  asset: option.asset,
2645
2974
  network: option.network,
2646
2975
  chainId: chainIdOrNull(option.network),
@@ -2974,6 +3303,26 @@ var HavenClient = class {
2974
3303
  async get(path) {
2975
3304
  return this.request("GET", path);
2976
3305
  }
3306
+ /**
3307
+ * #1300: every MERCHANT-facing fetch goes through here. Haven API calls
3308
+ * have always been bounded (request() below); the merchant probes/retries
3309
+ * called globalThis.fetch bare, so a slow-loris merchant could hold a tool
3310
+ * call open forever. A caller-supplied signal still applies (combined via
3311
+ * AbortSignal.any); a timeout abort surfaces as a clear HavenApiError 504
3312
+ * naming the URL rather than a bare AbortError.
3313
+ */
3314
+ async merchantFetch(url, init = {}, timeoutMs = this.merchantTimeout) {
3315
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
3316
+ const signal = init.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal;
3317
+ try {
3318
+ return await globalThis.fetch(url, { ...init, signal });
3319
+ } catch (err) {
3320
+ if (timeoutSignal.aborted) {
3321
+ throw new MerchantTimeoutError(`Merchant request timed out after ${timeoutMs}ms: ${url}`);
3322
+ }
3323
+ throw err;
3324
+ }
3325
+ }
2977
3326
  async request(method, path, body) {
2978
3327
  const url = `${this.baseUrl}${path}`;
2979
3328
  const controller = new AbortController();
@@ -3227,16 +3576,16 @@ var toolDescriptions = {
3227
3576
  nextActionGuidance: ""
3228
3577
  },
3229
3578
  payMcpTool: {
3230
- summary: "Call a named tool on an MCP merchant that requires an x402 payment, handling the full initialize \u2192 pay \u2192 retry round trip.",
3579
+ 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.",
3231
3580
  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.",
3232
- 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.",
3581
+ 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.",
3233
3582
  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."
3234
3583
  },
3235
3584
  discoverTools: {
3236
- summary: "Discover payable services from Haven's curated merchant catalog \u2014 names, prices, and which pay tool to use.",
3585
+ 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.",
3237
3586
  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.",
3238
- 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.",
3239
- 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."
3587
+ 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. Use category for a case-insensitive category filter (for example, VPN or vpn), or search for a product name, category, or description term. 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.",
3588
+ 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 the user's cap as max_amount_human in whole tokens ("no more than 1 USDC" \u2192 max_amount_human: "1") \u2014 never convert it to atomic units by hand (#1351).`
3240
3589
  },
3241
3590
  sweep_delegate: {
3242
3591
  summary: "Sweep stranded USDC and/or ETH from the delegate wallet back to the originating Safe.",
@@ -3505,6 +3854,9 @@ user's approval in Haven.
3505
3854
 
3506
3855
  Hosted tools run in the \`mcp__haven__\` namespace. Local signing tools run in
3507
3856
  the \`mcp__haven-signer__\` namespace and keep the delegate key on this machine.
3857
+ Tool results carry the exact next step (\`next_action\`, \`next_tool\`,
3858
+ \`next_arguments\`) \u2014 follow those fields first; the prose below is fallback
3859
+ and orientation, not the source of truth.
3508
3860
 
3509
3861
  ## When to use this skill
3510
3862
 
@@ -3529,11 +3881,11 @@ Before any payment, confirm the *live remaining* budget with the tools \u2014
3529
3881
  \`agent.json\` shows the configured budget, not what is left after recent
3530
3882
  spending:
3531
3883
 
3532
- - \`haven_get_agent\` \u2014 the recommended first call: identity (wallet, network)
3533
- plus a readiness signal (\`ready\` / \`needs_approval\` / \`revoked\`) and live
3534
- remaining per-token allowance, in one shot.
3535
- - \`haven_get_allowances\` \u2014 detailed per-token breakdown (configured, spent,
3536
- reset window) when you need more than the summary.
3884
+ - \`mcp__haven__haven_get_agent\` \u2014 the recommended first call: identity
3885
+ (wallet, network) plus a readiness signal (\`ready\` / \`needs_approval\` /
3886
+ \`revoked\`) and live remaining per-token allowance, in one shot.
3887
+ - \`mcp__haven__haven_get_allowances\` \u2014 detailed per-token breakdown
3888
+ (configured, spent, reset window) when you need more than the summary.
3537
3889
 
3538
3890
  Budgets reset on a period the user chose. If a payment exceeds the remaining
3539
3891
  budget it is queued for the user to approve in the Haven dashboard \u2014 this is
@@ -3541,47 +3893,84 @@ normal, not an error.
3541
3893
 
3542
3894
  ## Paying
3543
3895
 
3544
- - **Direct transfer:** \`haven_pay\` with recipient, amount, and token.
3545
- - **x402 paywall:** \`haven_quote_x402\` to get a quote, then
3546
- \`haven_pay_x402_quote\`. In the hosted setup the signing step happens in
3547
- the local Haven signer; follow the tool results \u2014 they tell you the next
3548
- action at every step. Retry the original request only when the result says
3549
- \`retry_original_x402_request\`.
3550
- - **Paid MCP tool call:** \`mcp__haven__haven_pay_mcp_tool\` with the merchant
3551
- URL, tool name, and arguments, then finish in two calls (fast path):
3552
- \`mcp__haven-signer__haven_sign_x402\` on the local signer (pass
3553
- \`payload_hash\`, \`x402_expected\` as the nested \`x402.expected\` object, and
3554
- \`payment_required\`) returns \`{ signature, payment_header }\`; then
3555
- \`mcp__haven__haven_settle_mcp_tool\` (pass \`payment_id\`, \`signature\`,
3556
- \`payment_header\`, \`merchant_url\`, \`tool_name\`, \`arguments\`,
3557
- \`mcp_transport\`) funds and settles in one step and returns the tool result.
3558
- If it returns \`settled: false\`, funding is queued for the user's approval \u2014
3559
- tell them and check status later, do not re-pay. Step-by-step alternative:
3560
- \`mcp__haven-signer__haven_sign\` \u2192 \`mcp__haven__haven_submit\` \u2192
3561
- \`mcp__haven-signer__haven_x402_sign_header\` \u2192
3562
- \`mcp__haven__haven_complete_mcp_tool\`. Pass \`payment_required\`,
3563
- \`arguments\`, and \`mcp_transport\` verbatim from the
3564
- \`mcp__haven__haven_pay_mcp_tool\` result. The returned \`expires_at\` is the
3565
- signing window; if a tool returns \`PAYMENT_WINDOW_EXPIRED\`, re-run
3566
- \`mcp__haven__haven_pay_mcp_tool\` with the same
3567
- \`idempotency_key\`. Do not call the merchant yourself \u2014 Haven completes the
3568
- merchant leg for you.
3569
- - **Prices:** show the user the live price from the pay-tool result, never a
3570
- catalog price. \`haven_discover_tools\` prices are indicative
3571
- (\`price_is_indicative\`) and can be stale. The pay-tool result's \`amount\` /
3572
- \`amount_atomic\` is the amount Haven authorizes for the call \u2014 a ceiling the
3573
- merchant settles at or below \u2014 so present it as the most the user will pay.
3574
- Pass \`max_amount\` (atomic units) to \`haven_pay_mcp_tool\` /
3575
- \`haven_pay_x402_quote\` to reject a quote whose authorized amount is above the
3576
- user's cap, before any funds move.
3577
- - **Status:** \`haven_get_payment_status\` with a \`payment_id\` to check on
3578
- queued or in-flight payments. Do not poll in a tight loop.
3896
+ **Catalog purchases \u2014 the primary path for MCP merchants:**
3897
+
3898
+ 1. \`mcp__haven__haven_discover_tools\` to find a payable service and its
3899
+ \`catalog_id\`.
3900
+ 2. \`mcp__haven__haven_prepare_catalog_purchase\` with \`catalog_id\` and a
3901
+ spending cap. A cap is REQUIRED on this tool and is best practice on every
3902
+ paid call below too \u2014 it caps what the LIVE merchant quote may charge,
3903
+ checked before any funding intent is created. Write it the way the user
3904
+ said it: \`max_amount_human\` is whole tokens, so "no more than 1 USDC" is
3905
+ \`max_amount_human: "1"\`. (\`max_amount\` is the atomic-unit form, where
3906
+ "1" means 0.000001 USDC \u2014 do not convert by hand, and never send both.)
3907
+ 3. Then FOLLOW THE RESPONSE'S GUIDANCE FIELDS: \`next_action\`, \`next_tool\`,
3908
+ and \`next_arguments\` name the exact next call \u2014 act on those first; the
3909
+ prose in this section is fallback and debugging detail. If the catalog
3910
+ entry is missing or degraded, the response instead names
3911
+ \`mcp__haven__haven_pay_mcp_tool\` (merchant URL, tool name, arguments) as
3912
+ the manual fallback.
3913
+
3914
+ **Signing:** \`mcp__haven-signer__haven_sign_x402\` with \`payment_id\` ONLY \u2014
3915
+ the local signer fetches the exact signing bytes AND \`payment_required\`
3916
+ itself, so never relay \`typed_data\` or the 402 blob yourself. If the signer
3917
+ reports its fetched context carried no \`payment_required\` (older backend),
3918
+ re-call with \`payment_required\` added verbatim. Fallback for an older signer
3919
+ or backend: re-run the quote/prepare tool with the SAME \`idempotency_key\`
3920
+ plus \`include_signing_payload=true\`, then pass \`payload_hash\`,
3921
+ \`x402_expected\` (the nested \`x402.expected\` object), and
3922
+ \`typed_data\`/\`typed_data_b64\` through unchanged.
3923
+
3924
+ **Settle:** \`mcp__haven__haven_settle_mcp_tool\` with \`payment_id\`,
3925
+ \`signature\`, and \`payment_header\` ONLY \u2014 Haven rehydrates the merchant call
3926
+ context (\`merchant_url\`, \`tool_name\`, \`arguments\`, \`mcp_transport\`)
3927
+ server-side from \`payment_id\`. Pass those four fields explicitly only as a
3928
+ version-skew fallback when Haven has no stored context for the id \u2014 both or
3929
+ none together, never just one. If the settle result carries \`settled: false\`,
3930
+ funding is queued for the user's approval \u2014 tell them and check status later,
3931
+ do not re-pay.
3932
+
3933
+ Step-by-step alternative (also key-safe; for an older signer or backend, or
3934
+ when you already have a merchant URL and tool name instead of a
3935
+ \`catalog_id\`): \`mcp__haven__haven_pay_mcp_tool\` then
3936
+ \`mcp__haven-signer__haven_sign\` \u2192 \`mcp__haven__haven_submit\` \u2192
3937
+ \`mcp__haven-signer__haven_x402_sign_header\` \u2192
3938
+ \`mcp__haven__haven_complete_mcp_tool\`. Pass \`payment_required\`,
3939
+ \`arguments\`, and \`mcp_transport\` verbatim from the quote/prepare result.
3940
+ The returned \`expires_at\` is the signing window; if a tool returns
3941
+ \`PAYMENT_WINDOW_EXPIRED\`, re-run the same quote/prepare tool with the same
3942
+ \`idempotency_key\`. Do not call the merchant yourself \u2014 Haven completes the
3943
+ merchant leg for you.
3944
+
3945
+ **Direct transfer / non-MCP paywall:** \`mcp__haven__haven_pay\` with
3946
+ recipient, amount, and token for a plain transfer. For an arbitrary,
3947
+ non-MCP x402 paywall: \`mcp__haven__haven_quote_x402\` to get a quote, then
3948
+ \`mcp__haven__haven_pay_x402_quote\` \u2014 follow the result's guidance fields
3949
+ first, sign in the local Haven signer, and retry the original request only
3950
+ when the result says \`retry_original_x402_request\`.
3951
+
3952
+ **Catalog tool arguments:** when \`haven_discover_tools\` returns
3953
+ \`tool_arguments\`, pass that object unchanged as the pay tool's
3954
+ \`arguments\` field (for example
3955
+ \`tool_arguments: { "tier": "50gb" }\` -> \`arguments: { "tier": "50gb" }\`).
3956
+
3957
+ **Prices:** show the user the live price from the pay-tool result, never a
3958
+ catalog price. \`haven_discover_tools\` prices are indicative
3959
+ (\`price_is_indicative\`) and can be stale. The pay-tool result's \`amount\` /
3960
+ \`amount_atomic\` is the amount Haven authorizes for the call \u2014 a ceiling the
3961
+ merchant settles at or below \u2014 so present it as the most the user will pay.
3962
+
3963
+ **Status:** \`mcp__haven__haven_get_payment_status\` with a \`payment_id\` to
3964
+ check on queued or in-flight payments. Do not poll in a tight loop.
3579
3965
 
3580
3966
  ## Approval semantics
3581
3967
 
3582
3968
  - A result with \`pending_approval\` means the payment exceeded the remaining
3583
3969
  budget and is waiting for the user in Haven. Tell the user, then check
3584
3970
  status later.
3971
+ - \`safe_to_continue: false\` on a guidance block is the same signal in
3972
+ machine-readable form: stop and involve the user before calling anything
3973
+ else for this payment.
3585
3974
  - Never ask the user for private keys. Signing happens only in the local Haven
3586
3975
  signer; the hosted Haven tools never receive the signing key. If a tool
3587
3976
  reports a missing or invalid credential, tell the user to re-run the Haven
@@ -3596,14 +3985,38 @@ present and surface \`message\` or \`error\` verbatim. Common cases:
3596
3985
  - \`pending_approval\`: queued for the user's approval (see above).
3597
3986
  - \`insufficient_funds\`: the Haven wallet doesn't hold enough of that token.
3598
3987
  Suggest the user add funds in the Haven dashboard.
3599
- - \`PRICE_EXCEEDS_MAX\`: the live merchant price exceeded your \`max_amount\`.
3600
- No funds moved; ask the user before retrying with a higher cap.
3601
- - \`PAYMENT_WINDOW_EXPIRED\`: re-run \`mcp__haven__haven_pay_mcp_tool\` with the same
3602
- \`idempotency_key\`, then sign the fresh \`payload_hash\`.
3603
- - \`MERCHANT_REJECTED_AFTER_FUNDING\`: stop retrying the merchant and use
3988
+ - \`PRICE_EXCEEDS_MAX\`: the live merchant price exceeded your cap. No funds
3989
+ moved; ask the user before retrying with a higher one.
3990
+ - \`AMBIGUOUS_MAX_AMOUNT\`: you sent both \`max_amount\` and
3991
+ \`max_amount_human\`. Nothing was contacted or spent \u2014 re-send with exactly
3992
+ one (\`max_amount_human\` for a cap the user stated in tokens).
3993
+ - \`MAX_AMOUNT_UNCONVERTIBLE\`: \`max_amount_human\` does not fit this quote's
3994
+ asset \u2014 unknown decimals, or more decimal places than the asset supports.
3995
+ Round the cap, or send an exact atomic \`max_amount\`.
3996
+ - \`PAYMENT_WINDOW_EXPIRED\`: re-run the quote/prepare tool with the same
3997
+ \`idempotency_key\`, then sign the fresh payload.
3998
+ - \`MERCHANT_REJECTED_AFTER_FUNDING\`: the merchant refused the paid retry.
3999
+ Stop-and-sweep \u2014 stop retrying the merchant and use
3604
4000
  \`mcp__haven__haven_sweep_delegate\` to recover stranded delegate funds.
4001
+ - \`MERCHANT_UNRESPONSIVE_AFTER_FUNDING\`: funding confirmed on-chain, but the
4002
+ merchant never answered the paid retry. This is NOT proof of rejection \u2014 the
4003
+ merchant may still settle late. Verify-then-sweep, never a blind sweep:
4004
+ check \`mcp__haven__haven_get_payment_status\`, retry
4005
+ \`mcp__haven__haven_complete_mcp_tool\` ONCE, and only sweep with
4006
+ \`mcp__haven__haven_sweep_delegate\` if no settlement appears.
3605
4007
  - Budget exceeded: tell the user how much remains (from
3606
- \`haven_get_allowances\`) and that they can raise the budget in Haven.
4008
+ \`mcp__haven__haven_get_allowances\`) and that they can raise the budget in
4009
+ Haven.
4010
+
4011
+ ## Reporting after a purchase
4012
+
4013
+ A settled \`mcp__haven__haven_settle_mcp_tool\` response carries
4014
+ \`agent_summary.purchase_summary\` and the remaining post-purchase allowance
4015
+ in \`allowance\` \u2014 report the product, Haven-derived payment/transaction
4016
+ fields, and what is left from those fields directly. \`result\` is optional
4017
+ raw merchant evidence; never use it to decide whether the purchase was paid.
4018
+ Do not call \`haven_get_agent\` or \`haven_get_allowances\` again just to
4019
+ report a purchase you already made.
3607
4020
 
3608
4021
  ## Revoke
3609
4022
 
@@ -3614,7 +4027,7 @@ for that credential.
3614
4027
  var SKILL_FOLDER_NAME = "haven-pay";
3615
4028
 
3616
4029
  // src/node-version.ts
3617
- var HAVEN_MINIMUM_NODE_VERSION = "24.0.0";
4030
+ var HAVEN_MINIMUM_NODE_VERSION = "22.0.0";
3618
4031
  function parseNodeVersion(value) {
3619
4032
  const match = value.trim().match(/^v?(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
3620
4033
  if (!match) return [0, 0, 0];
@@ -3757,6 +4170,50 @@ function stableStringify2(value) {
3757
4170
  return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${stableStringify2(object[key])}`).join(",")}}`;
3758
4171
  }
3759
4172
 
3760
- 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 };
4173
+ // src/merchant-discovery.ts
4174
+ var MERCHANT_DISCOVERY_PATHS = ["/.well-known/haven-demo-merchant", "/"];
4175
+ var DISCOVERY_MAX_BYTES = 64 * 1024;
4176
+ async function discoverMerchantMcpUrl(inputUrl) {
4177
+ let input;
4178
+ try {
4179
+ input = new URL(inputUrl);
4180
+ } catch {
4181
+ return null;
4182
+ }
4183
+ for (const path of MERCHANT_DISCOVERY_PATHS) {
4184
+ try {
4185
+ const res = await globalThis.fetch(`${input.origin}${path}`, {
4186
+ method: "GET",
4187
+ headers: { accept: "application/json" },
4188
+ redirect: "error",
4189
+ signal: AbortSignal.timeout(5e3)
4190
+ });
4191
+ if (!res.ok) continue;
4192
+ const contentLength = Number(res.headers.get("content-length") ?? 0);
4193
+ if (contentLength > DISCOVERY_MAX_BYTES) continue;
4194
+ const text = await res.text();
4195
+ if (text.length > DISCOVERY_MAX_BYTES) continue;
4196
+ const doc = JSON.parse(text);
4197
+ if (typeof doc.mcp_url !== "string") continue;
4198
+ const resolved = new URL(doc.mcp_url);
4199
+ if (resolved.origin !== input.origin) continue;
4200
+ return resolved.toString();
4201
+ } catch {
4202
+ continue;
4203
+ }
4204
+ }
4205
+ return null;
4206
+ }
4207
+ function sameUrl(a, b) {
4208
+ try {
4209
+ const ua = new URL(a);
4210
+ const ub = new URL(b);
4211
+ return ua.origin === ub.origin && ua.pathname.replace(/\/+$/, "") === ub.pathname.replace(/\/+$/, "");
4212
+ } catch {
4213
+ return false;
4214
+ }
4215
+ }
4216
+
4217
+ 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, resolveTokenFromAddress, sameUrl, selectPaymentOption, selectStandardPaymentOption, signHash, signUserOpTypedDataForDelegation, sweepUsdcAddress, sweepUsdcDomain, toStandardPaymentRequirements, toolDescriptions, unsupportedNodeVersionMessage, verifyPaymentReceipt, verifySignature, x402AuthorizationAmount };
3761
4218
  //# sourceMappingURL=index.js.map
3762
4219
  //# sourceMappingURL=index.js.map