@haven_ai/sdk 0.1.14-alpha.0 → 0.1.15-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/README.md CHANGED
@@ -229,6 +229,8 @@ The enum values and JSON Schema fragments are exported from `@haven_ai/sdk`:
229
229
  import {
230
230
  AgentPaymentNextAction,
231
231
  AgentPaymentNextActionSchema,
232
+ AgentPaymentFailureCode,
233
+ AgentPaymentFailureCodeSchema,
232
234
  AgentPaymentPhase,
233
235
  AgentPaymentPhaseSchema,
234
236
  AgentPaymentRail,
@@ -277,6 +279,10 @@ Terminal from any non-confirmed phase:
277
279
  rejected → stop_and_tell_user
278
280
  failed → stop_and_tell_user
279
281
  expired → request_again_if_user_still_wants_it
282
+
283
+ x402 tool-window failures:
284
+ expired funding/quote window → PAYMENT_WINDOW_EXPIRED → re-quote with same idempotency_key
285
+ merchant rejection after funding → MERCHANT_REJECTED_AFTER_FUNDING → haven_sweep_delegate
280
286
  ```
281
287
 
282
288
  ### `phase` reference
@@ -308,6 +314,18 @@ The merchant settlement leg of x402 (and the MPP retry) is the agent's own reque
308
314
  | `retry_original_x402_request` | Resume this payment id and retry the original x402 request with the merchant payment header. Do not start a new merchant session. |
309
315
  | `stop_and_tell_user` | Stop retrying and tell the user the payment failed or was rejected. |
310
316
  | `request_again_if_user_still_wants_it` | The request expired; ask again only if the user still wants the payment. |
317
+ | `payment_window_expired` | The x402 funding/quote window expired. Re-quote the same paid MCP tool call with the same `idempotency_key`, then sign the fresh `payload_hash`. |
318
+ | `sweep_stranded_funds` | A funding leg succeeded but the merchant/protocol leg did not settle. Stop retrying and use `haven_sweep_delegate` to recover stranded delegate funds. |
319
+
320
+ ### Machine-readable recovery codes
321
+
322
+ Hosted MCP and signer tools also return stable `code` values on recoverable x402 failures:
323
+
324
+ | `code` | Meaning | Agent recovery |
325
+ |--------|---------|----------------|
326
+ | `PRICE_EXCEEDS_MAX` | The merchant-authoritative x402 price is above the caller's `max_amount` cap. No funding transfer was created. | Tell the user the live price exceeded the cap and retry only after they confirm a higher `max_amount`. |
327
+ | `PAYMENT_WINDOW_EXPIRED` | The funding/quote window closed before `haven_x402_sign_header`, `haven_submit`, or `haven_complete_mcp_tool` could finish. | Re-run `haven_pay_mcp_tool` with the same `idempotency_key`, then sign and complete the fresh quote. Payloads include `retry_with_new_quote: true`. |
328
+ | `MERCHANT_REJECTED_AFTER_FUNDING` | Haven's funding leg succeeded, but the merchant rejected the paid retry. | Stop retrying the merchant and call `haven_sweep_delegate` so the user can recover stranded delegate USDC. |
311
329
 
312
330
  ## Payments above the on-chain allowance
313
331
 
package/dist/index.cjs CHANGED
@@ -63,6 +63,11 @@ var AgentPaymentNextAction = {
63
63
  StopAndTellUser: "stop_and_tell_user",
64
64
  /** Ask again only if the user still wants the payment after expiry. */
65
65
  RequestAgainIfUserStillWantsIt: "request_again_if_user_still_wants_it",
66
+ /**
67
+ * The x402 funding/quote window expired. Re-quote the same logical merchant
68
+ * operation with the same idempotency key to stay double-charge-safe.
69
+ */
70
+ PaymentWindowExpired: "payment_window_expired",
66
71
  /**
67
72
  * Stop and tell the user that the originating Safe needs to be funded or
68
73
  * the agent's per-token allowance needs to be raised before the payment
@@ -76,6 +81,14 @@ var AgentPaymentNextAction = {
76
81
  */
77
82
  SweepStrandedFunds: "sweep_stranded_funds"
78
83
  };
84
+ var AgentPaymentFailureCode = {
85
+ /** A merchant-authoritative x402 price exceeds the caller's pre-funding max_amount cap. */
86
+ PriceExceedsMax: "PRICE_EXCEEDS_MAX",
87
+ /** The x402 funding/quote window expired before the signer or hosted settle step could finish. */
88
+ PaymentWindowExpired: "PAYMENT_WINDOW_EXPIRED",
89
+ /** The Haven funding leg succeeded, but the merchant rejected the paid retry. */
90
+ MerchantRejectedAfterFunding: "MERCHANT_REJECTED_AFTER_FUNDING"
91
+ };
79
92
  var AgentPaymentRail = {
80
93
  /** Standard Haven payment from the user's Safe through an approved delegate allowance. */
81
94
  Direct: "direct",
@@ -94,6 +107,7 @@ var AgentPaymentRail = {
94
107
  };
95
108
  var AGENT_PAYMENT_PHASE_VALUES = Object.values(AgentPaymentPhase);
96
109
  var AGENT_PAYMENT_NEXT_ACTION_VALUES = Object.values(AgentPaymentNextAction);
110
+ var AGENT_PAYMENT_FAILURE_CODE_VALUES = Object.values(AgentPaymentFailureCode);
97
111
  var AGENT_PAYMENT_RAIL_VALUES = Object.values(AgentPaymentRail);
98
112
  var AgentPaymentPhaseDescriptions = {
99
113
  [AgentPaymentPhase.AgentSignatureRequired]: "The agent must sign and submit the prepared payment before Haven can relay it.",
@@ -118,9 +132,15 @@ var AgentPaymentNextActionDescriptions = {
118
132
  [AgentPaymentNextAction.RetryOriginalX402Request]: "Resume this payment id and retry the original x402 request with the merchant payment header.",
119
133
  [AgentPaymentNextAction.StopAndTellUser]: "Stop retrying this payment and tell the user what happened.",
120
134
  [AgentPaymentNextAction.RequestAgainIfUserStillWantsIt]: "Ask again only if the user still wants the payment after expiry.",
135
+ [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.",
121
136
  [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.",
122
137
  [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."
123
138
  };
139
+ var AgentPaymentFailureCodeDescriptions = {
140
+ [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.",
141
+ [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.",
142
+ [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."
143
+ };
124
144
  var AgentPaymentRailDescriptions = {
125
145
  [AgentPaymentRail.Direct]: "Standard Haven payment from the user-controlled Safe through an approved delegate allowance.",
126
146
  [AgentPaymentRail.X402]: "x402 HTTP 402 payment flow with a Haven funding leg and merchant retry leg.",
@@ -142,6 +162,12 @@ var AgentPaymentNextActionSchema = {
142
162
  description: "Stable next action an agent should take for a Haven payment state.",
143
163
  "x-enumDescriptions": AgentPaymentNextActionDescriptions
144
164
  };
165
+ var AgentPaymentFailureCodeSchema = {
166
+ type: "string",
167
+ enum: AGENT_PAYMENT_FAILURE_CODE_VALUES,
168
+ description: "Stable machine-readable failure codes for Haven agent payment recovery paths.",
169
+ "x-enumDescriptions": AgentPaymentFailureCodeDescriptions
170
+ };
145
171
  var AgentPaymentRailSchema = {
146
172
  type: "string",
147
173
  enum: AGENT_PAYMENT_RAIL_VALUES,
@@ -329,7 +355,8 @@ function normalizePaymentRequired(value) {
329
355
  x402Version: candidate.x402Version,
330
356
  resource,
331
357
  accepts,
332
- error: candidate.error
358
+ error: candidate.error,
359
+ ...candidate.extensions && typeof candidate.extensions === "object" ? { extensions: candidate.extensions } : {}
333
360
  };
334
361
  }
335
362
  var SUPPORTED_X402_NETWORKS = {
@@ -425,8 +452,7 @@ function x402AuthorizationAmount(option) {
425
452
  return amount;
426
453
  }
427
454
  function buildX402ExpectedMessage(context) {
428
- return `Haven x402 expected context v1
429
- ${stableStringify({
455
+ const payload = {
430
456
  version: 1,
431
457
  kind: "haven.x402.expected",
432
458
  paymentId: context.paymentId,
@@ -436,7 +462,12 @@ ${stableStringify({
436
462
  amount: context.amount,
437
463
  asset: context.asset.toLowerCase(),
438
464
  network: context.network
439
- })}`;
465
+ };
466
+ if (context.expiresAt) {
467
+ payload.expiresAt = context.expiresAt;
468
+ }
469
+ return `Haven x402 expected context v1
470
+ ${stableStringify(payload)}`;
440
471
  }
441
472
  function toStandardPaymentRequirements(paymentRequired, option) {
442
473
  const network = STANDARD_X402_NETWORKS[option.network];
@@ -892,6 +923,7 @@ var HavenClient = class {
892
923
  }
893
924
  return {
894
925
  paymentId: raw.payment_id,
926
+ idempotencyKey,
895
927
  status: "pending_signature",
896
928
  expiresAt: raw.expires_at,
897
929
  signData: raw.sign_data,
@@ -1043,6 +1075,30 @@ var HavenClient = class {
1043
1075
  transfers
1044
1076
  };
1045
1077
  }
1078
+ /**
1079
+ * Hosted (keyless) split-signer sweep — step 1 of 2.
1080
+ *
1081
+ * Asks the backend to build a gasless EIP-3009 sweep authorization for the
1082
+ * delegate's stranded USDC. Returns `nothing_stranded` when the delegate is
1083
+ * empty, otherwise an `authorization` + Haven `expected_auth` to hand to the
1084
+ * edge signer's `haven_sign_sweep_delegate`. No key is required on this client.
1085
+ */
1086
+ async prepareSweep() {
1087
+ return this.post("/machine-payments/sweep/prepare", {});
1088
+ }
1089
+ /**
1090
+ * Hosted (keyless) split-signer sweep — step 2 of 2.
1091
+ *
1092
+ * Relays the delegate-signed authorization. The Haven relayer submits the
1093
+ * on-chain `transferWithAuthorization` and pays gas; this client never holds
1094
+ * the key.
1095
+ */
1096
+ async submitSweep(authorization, signature) {
1097
+ return this.post("/machine-payments/sweep/submit", {
1098
+ authorization,
1099
+ signature
1100
+ });
1101
+ }
1046
1102
  /**
1047
1103
  * Get configured and on-chain allowances for the authenticated agent.
1048
1104
  */
@@ -1198,7 +1254,8 @@ var HavenClient = class {
1198
1254
  throw new HavenApiError("quoteX402 only supports standard x402 Payment Required responses.", 400);
1199
1255
  }
1200
1256
  const paymentRequired = await parsePaymentRequiredResponse(response);
1201
- return this.buildX402Quote(paymentRequired, request, options.idempotencyKey);
1257
+ const mcpTransport = await this.detectX402McpTransport(url, paymentRequired, response);
1258
+ return this.buildX402Quote(paymentRequired, request, options.idempotencyKey, mcpTransport);
1202
1259
  }
1203
1260
  /**
1204
1261
  * Pay a previously inspected x402 quote and retry the exact captured request.
@@ -1403,12 +1460,11 @@ var HavenClient = class {
1403
1460
  * a transport/HTTP error, a missing session id, or a JSON-RPC error in the
1404
1461
  * handshake response — so the caller can fall back to plain x402.
1405
1462
  */
1406
- async mcpInitialize(url, init) {
1463
+ async mcpInitialize(url, init, wallet = this.x402PayerAddress()) {
1407
1464
  try {
1408
1465
  const headers = new Headers(init?.headers);
1409
1466
  headers.set("Content-Type", "application/json");
1410
1467
  headers.set("Accept", MCP_ACCEPT);
1411
- const wallet = this.x402PayerAddress();
1412
1468
  if (wallet && !headers.has("x402-wallet")) headers.set("x402-wallet", wallet);
1413
1469
  const response = await globalThis.fetch(url, {
1414
1470
  method: "POST",
@@ -1429,7 +1485,7 @@ var HavenClient = class {
1429
1485
  if (!sessionId) return void 0;
1430
1486
  const message = await this.readMcpMessage(response);
1431
1487
  if (message && "error" in message) return void 0;
1432
- await this.mcpNotifyInitialized(url, init, sessionId);
1488
+ await this.mcpNotifyInitialized(url, init, sessionId, wallet);
1433
1489
  return sessionId;
1434
1490
  } catch {
1435
1491
  return void 0;
@@ -1440,13 +1496,12 @@ var HavenClient = class {
1440
1496
  * lifecycle handshake. Best-effort: the session is already established, so a
1441
1497
  * failed notification must not abort the payment.
1442
1498
  */
1443
- async mcpNotifyInitialized(url, init, sessionId) {
1499
+ async mcpNotifyInitialized(url, init, sessionId, wallet = this.x402PayerAddress()) {
1444
1500
  try {
1445
1501
  const headers = new Headers(init?.headers);
1446
1502
  headers.set("Content-Type", "application/json");
1447
1503
  headers.set("Accept", MCP_ACCEPT);
1448
1504
  headers.set("mcp-session-id", sessionId);
1449
- const wallet = this.x402PayerAddress();
1450
1505
  if (wallet && !headers.has("x402-wallet")) headers.set("x402-wallet", wallet);
1451
1506
  await globalThis.fetch(url, {
1452
1507
  method: "POST",
@@ -1624,30 +1679,33 @@ var HavenClient = class {
1624
1679
  * amount/merchant/nonce-bound EIP-3009 authorization the edge signer already
1625
1680
  * produced — the hosted server cannot mint or reuse signing authority.
1626
1681
  *
1627
- * When the URL is MCP-shaped (`/mcp` path), runs a fresh `initialize`
1682
+ * When the URL is MCP-shaped (`/mcp` path) or the quote-time transport context
1683
+ * says the merchant was Bazaar-discoverable, runs a fresh `initialize`
1628
1684
  * handshake (the quote-time session is gone once funding confirms; the x402
1629
1685
  * challenge is stateless w.r.t. the MCP session, so a fresh session is
1630
1686
  * accepted), threads the session + wallet headers, sets `X-PAYMENT`, and
1631
1687
  * collapses an SSE JSON-RPC response to its `result`.
1632
- *
1633
- * Limitation: detects MCP only by the `/mcp` path convention, not the
1634
- * Coinbase Bazaar `extensions.bazaar` 402 signal that `fetch()` also honors.
1635
- * A Bazaar-discoverable merchant on a non-`/mcp` URL would need the standard
1636
- * `fetch()` path. All current MCP-tool merchants use the `/mcp` convention.
1637
1688
  */
1638
1689
  async completeX402MerchantCall(input) {
1690
+ const evidenceContext = await this.resolveX402MerchantCompletionContext({
1691
+ paymentId: input.paymentId,
1692
+ url: input.url
1693
+ });
1694
+ const shouldHandshakeMcp = isMcpUrl(input.url) || input.mcpTransport?.handshakeRequired === true;
1695
+ const x402Wallet = shouldHandshakeMcp ? await this.resolveX402WalletForMerchantCall() : this.x402PayerAddress();
1639
1696
  let mcpSessionId;
1640
- if (isMcpUrl(input.url)) {
1641
- mcpSessionId = await this.mcpInitialize(input.url, input.init);
1697
+ if (shouldHandshakeMcp) {
1698
+ mcpSessionId = await this.mcpInitialize(input.url, input.init, x402Wallet);
1642
1699
  }
1643
- let requestInit = this.withX402Wallet(input.init, this.x402PayerAddress()) ?? {};
1700
+ let requestInit = this.withX402Wallet(input.init, x402Wallet) ?? {};
1644
1701
  if (mcpSessionId) requestInit = this.withMcpHeaders(requestInit, mcpSessionId);
1645
1702
  const headers = new Headers(requestInit.headers);
1646
1703
  headers.set("X-PAYMENT", input.paymentHeader);
1647
1704
  requestInit = { ...requestInit, headers };
1648
1705
  const response = await globalThis.fetch(input.url, requestInit);
1649
1706
  const surfaced = mcpSessionId ? await this.surfaceMcpResult(response) : response;
1650
- const settlement = parseMerchantSettlement(surfaced.headers.get("PAYMENT-RESPONSE"));
1707
+ const protocolReceiptHeader = surfaced.headers.get("PAYMENT-RESPONSE") ?? void 0;
1708
+ const settlement = parseMerchantSettlement(protocolReceiptHeader ?? null);
1651
1709
  const text = await surfaced.text();
1652
1710
  let body;
1653
1711
  try {
@@ -1655,6 +1713,35 @@ var HavenClient = class {
1655
1713
  } catch {
1656
1714
  body = text;
1657
1715
  }
1716
+ if (!surfaced.ok) {
1717
+ await this.recordMerchantRetryRejected({
1718
+ rail: "x402",
1719
+ paymentId: evidenceContext.paymentId,
1720
+ txHash: evidenceContext.txHash,
1721
+ resourceUrl: evidenceContext.resourceUrl,
1722
+ merchant: {
1723
+ merchant_status: surfaced.status,
1724
+ merchant_status_text: surfaced.statusText,
1725
+ merchant_headers: Object.fromEntries(surfaced.headers.entries()),
1726
+ merchant_body: text
1727
+ },
1728
+ details: {
1729
+ merchant_to: evidenceContext.merchantAddress
1730
+ }
1731
+ });
1732
+ } else {
1733
+ await this.reportMachinePaymentEvidence({
1734
+ paymentId: evidenceContext.paymentId,
1735
+ rail: "x402",
1736
+ txHash: evidenceContext.txHash,
1737
+ resourceUrl: evidenceContext.resourceUrl,
1738
+ merchantStatus: surfaced.status,
1739
+ paymentProofHeaderName: "X-PAYMENT",
1740
+ paymentProofHeader: input.paymentHeader,
1741
+ protocolReceiptHeaderName: protocolReceiptHeader ? "PAYMENT-RESPONSE" : void 0,
1742
+ protocolReceiptHeader
1743
+ });
1744
+ }
1658
1745
  return {
1659
1746
  status: surfaced.status,
1660
1747
  ok: surfaced.ok,
@@ -1662,6 +1749,53 @@ var HavenClient = class {
1662
1749
  settlementTxHash: settlement.settlementTxHash ?? void 0
1663
1750
  };
1664
1751
  }
1752
+ async resolveX402MerchantCompletionContext(input) {
1753
+ const status = await this.getPaymentStatus(input.paymentId);
1754
+ if (status.rail !== "x402") {
1755
+ throw new HavenPaymentStateError(
1756
+ `Payment ${status.paymentId} is ${status.rail}, not x402.`,
1757
+ 409,
1758
+ status
1759
+ );
1760
+ }
1761
+ const readyForMerchantCompletion = status.nextAction === AgentPaymentNextAction.RetryOriginalX402Request || status.kind === "payment_intent" && status.status === "confirmed" && status.phase === AgentPaymentPhase.PaymentConfirmed && status.nextAction === AgentPaymentNextAction.None;
1762
+ if (!readyForMerchantCompletion) {
1763
+ throw new HavenPaymentStateError(status.message, PAYMENT_STATE_STATUS_CODES[status.status] ?? 409, status);
1764
+ }
1765
+ if (!status.txHash) {
1766
+ throw new HavenApiError(
1767
+ `x402 payment ${status.paymentId} is ready for merchant completion but has no Haven transaction hash.`,
1768
+ 502,
1769
+ status,
1770
+ status.paymentId
1771
+ );
1772
+ }
1773
+ const approvedResourceUrl = status.resourceUrl ?? status.x402?.resourceUrl ?? null;
1774
+ if (approvedResourceUrl && approvedResourceUrl !== input.url) {
1775
+ throw new HavenApiError(
1776
+ "x402 merchant completion does not match the approved resource URL.",
1777
+ 409,
1778
+ { status, url: input.url },
1779
+ status.paymentId
1780
+ );
1781
+ }
1782
+ return {
1783
+ paymentId: status.paymentId,
1784
+ txHash: status.txHash,
1785
+ resourceUrl: approvedResourceUrl ?? input.url,
1786
+ merchantAddress: status.merchantAddress ?? status.x402?.merchantAddress ?? null
1787
+ };
1788
+ }
1789
+ async resolveX402WalletForMerchantCall() {
1790
+ const localWallet = this.x402PayerAddress();
1791
+ if (localWallet) return localWallet;
1792
+ try {
1793
+ const agent = await this.getAgent();
1794
+ return agent.delegateAddress ?? void 0;
1795
+ } catch {
1796
+ return void 0;
1797
+ }
1798
+ }
1665
1799
  async authorizeMachinePayment(challenge, options = {}) {
1666
1800
  if (!this.delegateKey) {
1667
1801
  throw new HavenSigningError(
@@ -2259,7 +2393,7 @@ var HavenClient = class {
2259
2393
  headers
2260
2394
  };
2261
2395
  }
2262
- buildX402Quote(paymentRequired, request, idempotencyKey) {
2396
+ buildX402Quote(paymentRequired, request, idempotencyKey, mcpTransport) {
2263
2397
  const option = selectStandardPaymentOption(paymentRequired.accepts);
2264
2398
  if (!option) {
2265
2399
  throw new HavenApiError(
@@ -2274,6 +2408,7 @@ var HavenClient = class {
2274
2408
  paymentRequired,
2275
2409
  accepted: option,
2276
2410
  request,
2411
+ ...mcpTransport ? { mcpTransport } : {},
2277
2412
  resourceUrl: paymentRequired.resource.url,
2278
2413
  description: paymentRequired.resource.description ?? option.description ?? null,
2279
2414
  mimeType: paymentRequired.resource.mimeType ?? option.mimeType ?? null,
@@ -2287,6 +2422,18 @@ var HavenClient = class {
2287
2422
  maxTimeoutSeconds: option.maxTimeoutSeconds
2288
2423
  };
2289
2424
  }
2425
+ async detectX402McpTransport(url, paymentRequired, response) {
2426
+ if (isMcpUrl(url)) {
2427
+ return { handshakeRequired: true, source: "path" };
2428
+ }
2429
+ if (paymentRequired.extensions?.bazaar != null) {
2430
+ return { handshakeRequired: true, source: "bazaar" };
2431
+ }
2432
+ if (await responseHasBazaarExtension(response)) {
2433
+ return { handshakeRequired: true, source: "bazaar" };
2434
+ }
2435
+ return void 0;
2436
+ }
2290
2437
  buildX402ResumeState(input) {
2291
2438
  const token = resolveTokenFromAddress(input.accepted.asset, input.accepted.network);
2292
2439
  return {
@@ -2839,8 +2986,8 @@ var toolDescriptions = {
2839
2986
  discoverTools: {
2840
2987
  summary: "Discover payable services from Haven's curated merchant catalog \u2014 names, prices, and which pay tool to use.",
2841
2988
  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.",
2842
- 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. Never creates a payment, signature, or approval.",
2843
- nextActionGuidance: "Pick an entry, confirm the price with the user if it is non-trivial, and pay it with the tool named in suggested_tool, passing the entry's resource_url (and tool_name for MCP merchants)."
2989
+ 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.",
2990
+ 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."
2844
2991
  },
2845
2992
  sweep_delegate: {
2846
2993
  summary: "Sweep stranded USDC and/or ETH from the delegate wallet back to the originating Safe.",
@@ -3107,6 +3254,9 @@ the Haven MCP tools. Every payment is checked against the agent's on-chain
3107
3254
  budget before money moves; payments above the remaining budget wait for the
3108
3255
  user's approval in Haven.
3109
3256
 
3257
+ Hosted tools run in the \`mcp__haven__\` namespace. Local signing tools run in
3258
+ the \`mcp__haven-signer__\` namespace and keep the delegate key on this machine.
3259
+
3110
3260
  ## When to use this skill
3111
3261
 
3112
3262
  - The user asks to send money, pay someone, tip, donate, or transfer tokens.
@@ -3132,13 +3282,33 @@ normal, not an error.
3132
3282
  the local Haven signer; follow the tool results \u2014 they tell you the next
3133
3283
  action at every step. Retry the original request only when the result says
3134
3284
  \`retry_original_x402_request\`.
3135
- - **Paid MCP tool call:** \`haven_pay_mcp_tool\` with the merchant URL, tool
3136
- name, and arguments. Then follow the returned steps: \`haven_sign\` the
3137
- funding hash, \`haven_submit\` the signature, \`haven_x402_sign_header\` to
3138
- build the payment header, and finally \`haven_complete_mcp_tool\` to settle
3139
- with the merchant and get the tool result. Pass \`payment_required\` and
3140
- \`arguments\` through verbatim from the \`haven_pay_mcp_tool\` result. Do not
3141
- call the merchant yourself \u2014 Haven completes the merchant leg for you.
3285
+ - **Paid MCP tool call:** \`mcp__haven__haven_pay_mcp_tool\` with the merchant
3286
+ URL, tool name, and arguments, then finish in two calls (fast path):
3287
+ \`mcp__haven-signer__haven_sign_x402\` on the local signer (pass
3288
+ \`payload_hash\`, \`x402_expected\` as the nested \`x402.expected\` object, and
3289
+ \`payment_required\`) returns \`{ signature, payment_header }\`; then
3290
+ \`mcp__haven__haven_settle_mcp_tool\` (pass \`payment_id\`, \`signature\`,
3291
+ \`payment_header\`, \`merchant_url\`, \`tool_name\`, \`arguments\`,
3292
+ \`mcp_transport\`) funds and settles in one step and returns the tool result.
3293
+ If it returns \`settled: false\`, funding is queued for the user's approval \u2014
3294
+ tell them and check status later, do not re-pay. Step-by-step alternative:
3295
+ \`mcp__haven-signer__haven_sign\` \u2192 \`mcp__haven__haven_submit\` \u2192
3296
+ \`mcp__haven-signer__haven_x402_sign_header\` \u2192
3297
+ \`mcp__haven__haven_complete_mcp_tool\`. Pass \`payment_required\`,
3298
+ \`arguments\`, and \`mcp_transport\` verbatim from the
3299
+ \`mcp__haven__haven_pay_mcp_tool\` result. The returned \`expires_at\` is the
3300
+ signing window; if a tool returns \`PAYMENT_WINDOW_EXPIRED\`, re-run
3301
+ \`mcp__haven__haven_pay_mcp_tool\` with the same
3302
+ \`idempotency_key\`. Do not call the merchant yourself \u2014 Haven completes the
3303
+ merchant leg for you.
3304
+ - **Prices:** show the user the live price from the pay-tool result, never a
3305
+ catalog price. \`haven_discover_tools\` prices are indicative
3306
+ (\`price_is_indicative\`) and can be stale. The pay-tool result's \`amount\` /
3307
+ \`amount_atomic\` is the amount Haven authorizes for the call \u2014 a ceiling the
3308
+ merchant settles at or below \u2014 so present it as the most the user will pay.
3309
+ Pass \`max_amount\` (atomic units) to \`haven_pay_mcp_tool\` /
3310
+ \`haven_pay_x402_quote\` to reject a quote whose authorized amount is above the
3311
+ user's cap, before any funds move.
3142
3312
  - **Status:** \`haven_get_payment_status\` with a \`payment_id\` to check on
3143
3313
  queued or in-flight payments. Do not poll in a tight loop.
3144
3314
 
@@ -3147,18 +3317,26 @@ normal, not an error.
3147
3317
  - A result with \`pending_approval\` means the payment exceeded the remaining
3148
3318
  budget and is waiting for the user in Haven. Tell the user, then check
3149
3319
  status later.
3150
- - Never ask the user for private keys and never try to sign anything
3151
- yourself \u2014 Haven signs. If a tool reports a missing or invalid credential,
3152
- tell the user to re-run the Haven setup command.
3320
+ - Never ask the user for private keys. Signing happens only in the local Haven
3321
+ signer; the hosted Haven tools never receive the signing key. If a tool
3322
+ reports a missing or invalid credential, tell the user to re-run the Haven
3323
+ setup command.
3153
3324
 
3154
3325
  ## Failure handling
3155
3326
 
3156
- Haven errors are shaped \`{ error, status, details? }\` and written for
3157
- humans \u2014 surface the message verbatim. Common cases:
3327
+ Haven tool failures are shaped like \`{ success: false, code, message, ... }\`
3328
+ or older \`{ error, status, details? }\` responses. Branch on \`code\` when
3329
+ present and surface \`message\` or \`error\` verbatim. Common cases:
3158
3330
 
3159
3331
  - \`pending_approval\`: queued for the user's approval (see above).
3160
3332
  - \`insufficient_funds\`: the Haven wallet doesn't hold enough of that token.
3161
3333
  Suggest the user add funds in the Haven dashboard.
3334
+ - \`PRICE_EXCEEDS_MAX\`: the live merchant price exceeded your \`max_amount\`.
3335
+ No funds moved; ask the user before retrying with a higher cap.
3336
+ - \`PAYMENT_WINDOW_EXPIRED\`: re-run \`mcp__haven__haven_pay_mcp_tool\` with the same
3337
+ \`idempotency_key\`, then sign the fresh \`payload_hash\`.
3338
+ - \`MERCHANT_REJECTED_AFTER_FUNDING\`: stop retrying the merchant and use
3339
+ \`mcp__haven__haven_sweep_delegate\` to recover stranded delegate funds.
3162
3340
  - Budget exceeded: tell the user how much remains (from
3163
3341
  \`haven_get_allowances\`) and that they can raise the budget in Haven.
3164
3342
 
@@ -3170,9 +3348,105 @@ for that credential.
3170
3348
  `;
3171
3349
  var SKILL_FOLDER_NAME = "haven-pay";
3172
3350
 
3351
+ // src/sweep.ts
3352
+ var SWEEP_BASE_CHAIN_ID = 8453;
3353
+ var SWEEP_BASE_USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
3354
+ var USDC_EIP712_DOMAIN_BY_CHAIN = {
3355
+ [SWEEP_BASE_CHAIN_ID]: {
3356
+ name: "USD Coin",
3357
+ version: "2",
3358
+ chainId: SWEEP_BASE_CHAIN_ID,
3359
+ verifyingContract: SWEEP_BASE_USDC_ADDRESS
3360
+ }
3361
+ };
3362
+ var USDC_ADDRESS_BY_CHAIN = {
3363
+ [SWEEP_BASE_CHAIN_ID]: SWEEP_BASE_USDC_ADDRESS
3364
+ };
3365
+ var TRANSFER_WITH_AUTHORIZATION_TYPES = {
3366
+ TransferWithAuthorization: [
3367
+ { name: "from", type: "address" },
3368
+ { name: "to", type: "address" },
3369
+ { name: "value", type: "uint256" },
3370
+ { name: "validAfter", type: "uint256" },
3371
+ { name: "validBefore", type: "uint256" },
3372
+ { name: "nonce", type: "bytes32" }
3373
+ ]
3374
+ };
3375
+ function sweepUsdcAddress(chainId) {
3376
+ const address = USDC_ADDRESS_BY_CHAIN[chainId];
3377
+ if (!address) {
3378
+ throw new HavenSigningError(
3379
+ `Sweep is only supported on Base (chainId ${SWEEP_BASE_CHAIN_ID}). Got chainId ${chainId}.`
3380
+ );
3381
+ }
3382
+ return address;
3383
+ }
3384
+ function sweepUsdcDomain(chainId) {
3385
+ const domain = USDC_EIP712_DOMAIN_BY_CHAIN[chainId];
3386
+ if (!domain) {
3387
+ throw new HavenSigningError(
3388
+ `Sweep is only supported on Base (chainId ${SWEEP_BASE_CHAIN_ID}). Got chainId ${chainId}.`
3389
+ );
3390
+ }
3391
+ return domain;
3392
+ }
3393
+ function sameAddress2(a, b) {
3394
+ return a.toLowerCase() === b.toLowerCase();
3395
+ }
3396
+ function buildSweepTypedData(auth) {
3397
+ const domain = sweepUsdcDomain(auth.chainId);
3398
+ const expectedToken = sweepUsdcAddress(auth.chainId);
3399
+ if (!sameAddress2(auth.token, expectedToken)) {
3400
+ throw new HavenSigningError(
3401
+ `Sweep token ${auth.token} is not the canonical USDC contract for chain ${auth.chainId}.`
3402
+ );
3403
+ }
3404
+ if (!/^0x[0-9a-fA-F]{64}$/.test(auth.nonce)) {
3405
+ throw new HavenSigningError("Sweep nonce must be a 0x-prefixed 32-byte hex string.");
3406
+ }
3407
+ return {
3408
+ domain,
3409
+ types: TRANSFER_WITH_AUTHORIZATION_TYPES,
3410
+ primaryType: "TransferWithAuthorization",
3411
+ message: {
3412
+ from: auth.from,
3413
+ to: auth.to,
3414
+ value: BigInt(auth.value),
3415
+ validAfter: BigInt(auth.validAfter),
3416
+ validBefore: BigInt(auth.validBefore),
3417
+ nonce: auth.nonce
3418
+ }
3419
+ };
3420
+ }
3421
+ function buildSweepAuthorizationMessage(auth) {
3422
+ return `Haven sweep authorization v1
3423
+ ${stableStringify2({
3424
+ version: 1,
3425
+ kind: "haven.sweep.authorization",
3426
+ from: auth.from.toLowerCase(),
3427
+ to: auth.to.toLowerCase(),
3428
+ value: auth.value,
3429
+ validAfter: auth.validAfter,
3430
+ validBefore: auth.validBefore,
3431
+ nonce: auth.nonce.toLowerCase(),
3432
+ token: auth.token.toLowerCase(),
3433
+ chainId: auth.chainId
3434
+ })}`;
3435
+ }
3436
+ function stableStringify2(value) {
3437
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
3438
+ if (Array.isArray(value)) return `[${value.map((item) => stableStringify2(item)).join(",")}]`;
3439
+ const object = value;
3440
+ return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${stableStringify2(object[key])}`).join(",")}}`;
3441
+ }
3442
+
3443
+ exports.AGENT_PAYMENT_FAILURE_CODE_VALUES = AGENT_PAYMENT_FAILURE_CODE_VALUES;
3173
3444
  exports.AGENT_PAYMENT_NEXT_ACTION_VALUES = AGENT_PAYMENT_NEXT_ACTION_VALUES;
3174
3445
  exports.AGENT_PAYMENT_PHASE_VALUES = AGENT_PAYMENT_PHASE_VALUES;
3175
3446
  exports.AGENT_PAYMENT_RAIL_VALUES = AGENT_PAYMENT_RAIL_VALUES;
3447
+ exports.AgentPaymentFailureCode = AgentPaymentFailureCode;
3448
+ exports.AgentPaymentFailureCodeDescriptions = AgentPaymentFailureCodeDescriptions;
3449
+ exports.AgentPaymentFailureCodeSchema = AgentPaymentFailureCodeSchema;
3176
3450
  exports.AgentPaymentNextAction = AgentPaymentNextAction;
3177
3451
  exports.AgentPaymentNextActionDescriptions = AgentPaymentNextActionDescriptions;
3178
3452
  exports.AgentPaymentNextActionSchema = AgentPaymentNextActionSchema;
@@ -3190,8 +3464,13 @@ exports.HavenPaymentStateError = HavenPaymentStateError;
3190
3464
  exports.HavenSigningError = HavenSigningError;
3191
3465
  exports.HavenTimeoutError = HavenTimeoutError;
3192
3466
  exports.SKILL_FOLDER_NAME = SKILL_FOLDER_NAME;
3467
+ exports.SWEEP_BASE_CHAIN_ID = SWEEP_BASE_CHAIN_ID;
3468
+ exports.SWEEP_BASE_USDC_ADDRESS = SWEEP_BASE_USDC_ADDRESS;
3469
+ exports.TRANSFER_WITH_AUTHORIZATION_TYPES = TRANSFER_WITH_AUTHORIZATION_TYPES;
3193
3470
  exports.addressFromKey = addressFromKey;
3194
3471
  exports.buildMachinePaymentIdempotencyKey = buildMachinePaymentIdempotencyKey;
3472
+ exports.buildSweepAuthorizationMessage = buildSweepAuthorizationMessage;
3473
+ exports.buildSweepTypedData = buildSweepTypedData;
3195
3474
  exports.buildX402ExpectedMessage = buildX402ExpectedMessage;
3196
3475
  exports.composeDescription = composeDescription;
3197
3476
  exports.decodeBase64Json = decodeBase64Json;
@@ -3208,6 +3487,8 @@ exports.parsePaymentRequiredResponse = parsePaymentRequiredResponse;
3208
3487
  exports.selectPaymentOption = selectPaymentOption;
3209
3488
  exports.selectStandardPaymentOption = selectStandardPaymentOption;
3210
3489
  exports.signHash = signHash;
3490
+ exports.sweepUsdcAddress = sweepUsdcAddress;
3491
+ exports.sweepUsdcDomain = sweepUsdcDomain;
3211
3492
  exports.toStandardPaymentRequirements = toStandardPaymentRequirements;
3212
3493
  exports.toolDescriptions = toolDescriptions;
3213
3494
  exports.verifySignature = verifySignature;