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