@haven_ai/sdk 0.3.0-alpha.0 → 0.4.0-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
@@ -79,6 +79,7 @@ shows up in your Haven dashboard activity feed.
79
79
  - The delegate key signs payment payloads locally. Haven's backend never receives it.
80
80
  - The agent's on-chain budget delegation enforces the agent budget: budget, recipient and expiry are checked by audited caveat enforcers at redemption, not by an off-chain rules engine.
81
81
  - `getAllowances()` / `get_allowances` is the right path for budget, remaining amount, reset period, or "what can I spend?" questions.
82
+ - `checkFunds()` / `haven_check_funds` answers the different question of whether the account actually holds funds behind the budget — as a sufficiency signal (`covered` true/false/null), never as a balance.
82
83
  - If an API key is exposed or lost, rotate it from the Haven agent detail page. The new key is shown once and the old key stops working.
83
84
  - If a delegate key is exposed or lost, a delegation-rail agent is **re-keyed** rather than replaced — same agent, new signing key, budget remainder carried. See [Replacing an agent's signing key](../../docs/product/agent-key-rotation.md).
84
85
 
package/dist/index.cjs CHANGED
@@ -708,7 +708,13 @@ function normalizePaymentOption(value) {
708
708
  mimeType: candidate.mimeType,
709
709
  asset: candidate.asset,
710
710
  payTo: candidate.payTo,
711
- maxTimeoutSeconds: clampAuthorizationWindow(candidate.maxTimeoutSeconds),
711
+ // Keep the merchant offer intact for v2 `accepted` matching. Only the
712
+ // signing conversion may cap its authorization lifetime (#3117).
713
+ // Retain legacy fallback behavior for missing or unusable timeouts.
714
+ // Integer, because the authorize body types it `integer` and the child's
715
+ // timestamp caveat is built from it — the old clamp floored, so keeping a
716
+ // fractional value here would 400 a merchant that used to work (#3117).
717
+ maxTimeoutSeconds: typeof candidate.maxTimeoutSeconds === "number" && Number.isFinite(candidate.maxTimeoutSeconds) && candidate.maxTimeoutSeconds >= 1 ? Math.floor(candidate.maxTimeoutSeconds) : clampAuthorizationWindow(candidate.maxTimeoutSeconds),
712
718
  extra: candidate.extra
713
719
  };
714
720
  }
@@ -859,6 +865,13 @@ function selectPaymentOption(accepts) {
859
865
  return null;
860
866
  }
861
867
  var ERC7710_ASSET_TRANSFER_METHOD = "erc7710";
868
+ function isEip3009ConstructibleOption(option) {
869
+ const rawMethod = option.extra?.assetTransferMethod;
870
+ if (rawMethod !== void 0 && rawMethod !== "eip3009") return false;
871
+ const rawFlow = option.extra?.paymentFlow;
872
+ if (rawFlow !== void 0 && rawFlow !== "authorization") return false;
873
+ return true;
874
+ }
862
875
  function x402AssetTransferMethod(option) {
863
876
  const raw = option.extra?.assetTransferMethod;
864
877
  return typeof raw === "string" ? raw : null;
@@ -879,7 +892,8 @@ function selectStandardPaymentOption(accepts) {
879
892
  if (!accepts || accepts.length === 0) return null;
880
893
  for (const opt of accepts) {
881
894
  if (opt === null || typeof opt !== "object") continue;
882
- if (!isErc7710Option(opt) && isPayableStandardOption(opt)) return opt;
895
+ if (!isEip3009ConstructibleOption(opt)) continue;
896
+ if (isPayableStandardOption(opt)) return opt;
883
897
  }
884
898
  return null;
885
899
  }
@@ -888,6 +902,7 @@ function selectErc7710PaymentOption(accepts) {
888
902
  for (const opt of accepts) {
889
903
  if (opt === null || typeof opt !== "object") continue;
890
904
  if (isErc7710Option(opt) && isPayableStandardOption(opt)) return opt;
905
+ if (!isEip3009ConstructibleOption(opt)) continue;
891
906
  }
892
907
  return null;
893
908
  }
@@ -949,6 +964,11 @@ function toStandardPaymentRequirements(paymentRequired, option) {
949
964
  if (option.scheme !== "exact") {
950
965
  throw new Error(`Unsupported x402 scheme: ${option.scheme}`);
951
966
  }
967
+ if (!isEip3009ConstructibleOption(option)) {
968
+ throw new Error(
969
+ "Unsupported x402 payment requirements: this SDK constructs EIP-3009 authorization payments only. The merchant advertised a different extra.assetTransferMethod or an unrecognized extra.paymentFlow."
970
+ );
971
+ }
952
972
  return {
953
973
  scheme: "exact",
954
974
  network,
@@ -958,14 +978,11 @@ function toStandardPaymentRequirements(paymentRequired, option) {
958
978
  mimeType: option.mimeType ?? paymentRequired.resource.mimeType ?? "application/octet-stream",
959
979
  payTo: option.payTo,
960
980
  asset: option.asset,
961
- // Second enforcement point (#715): the parse path clamps too, but this is
962
- // the last stop before the x402 library turns the timeout into
963
- // `validBefore` — options constructed without parsing are bounded here.
964
- // The forward margin (#1256) is added ONLY here, at signing: the parse
965
- // path keeps recording the merchant's advertised timeout unchanged, and
966
- // the library's `validBefore = now + this value` then carries enough
967
- // slack to satisfy the facilitator's `validBefore ≥ now + maxTimeout`
968
- // verify rule after our funding leg confirms.
981
+ // Signing-only policy (#715/#3117): parsed and directly supplied options
982
+ // share this cap before the library computes validBefore. Keep the offer
983
+ // echoed in `accepted` unchanged. The forward margin (#1256) adds time
984
+ // for funding/retry, but does not guarantee a merchant timeout above the
985
+ // total bounded lifetime can pass facilitator verification.
969
986
  maxTimeoutSeconds: clampAuthorizationWindow(option.maxTimeoutSeconds) + X402_SETTLEMENT_FORWARD_MARGIN_SECONDS,
970
987
  extra: option.extra
971
988
  };
@@ -1420,6 +1437,51 @@ function throwPaymentStateError(label, raw) {
1420
1437
  throw new HavenApiError(message, statusCode, raw);
1421
1438
  }
1422
1439
 
1440
+ // src/x402-retry-target.ts
1441
+ function resolveX402RetryTarget(input) {
1442
+ const requestUrl = input.requestUrl?.trim() || void 0;
1443
+ if (requestUrl) {
1444
+ return {
1445
+ url: requestUrl,
1446
+ source: "request",
1447
+ resourceUrlDiffersFromRequest: requestUrl !== input.resourceUrl
1448
+ };
1449
+ }
1450
+ return { url: input.resourceUrl, source: "resource" };
1451
+ }
1452
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "::1", "[::1]"]);
1453
+ var RESERVED_SUFFIXES = [".test", ".localhost", ".invalid", ".example"];
1454
+ var IPV4_LOOPBACK = /^127(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}$/;
1455
+ function isSecureX402RetryTarget(url) {
1456
+ let parsed;
1457
+ try {
1458
+ parsed = new URL(url);
1459
+ } catch {
1460
+ return false;
1461
+ }
1462
+ if (parsed.protocol === "https:") return true;
1463
+ if (parsed.protocol !== "http:") return false;
1464
+ const host = parsed.hostname.toLowerCase();
1465
+ if (LOOPBACK_HOSTS.has(host) || IPV4_LOOPBACK.test(host)) return true;
1466
+ return RESERVED_SUFFIXES.some((suffix) => host.endsWith(suffix));
1467
+ }
1468
+ var INSECURE_RETRY_TARGET_CODE = "INSECURE_RETRY_TARGET";
1469
+ var HavenInsecureRetryTargetError = class extends HavenError {
1470
+ constructor(url) {
1471
+ super(
1472
+ `Refusing to send a payment header to ${url}: the paid x402 retry must go to an https URL (or a loopback / reserved test host). The merchant's challenge declared this resource URL; retry with the https URL you quoted (pass it as \`url\`), and report a merchant whose challenge downgrades the scheme.`,
1473
+ INSECURE_RETRY_TARGET_CODE,
1474
+ 400
1475
+ );
1476
+ this.url = url;
1477
+ this.name = "HavenInsecureRetryTargetError";
1478
+ }
1479
+ url;
1480
+ };
1481
+ function assertSecureX402RetryTarget(url) {
1482
+ if (!isSecureX402RetryTarget(url)) throw new HavenInsecureRetryTargetError(url);
1483
+ }
1484
+
1423
1485
  // src/mcp-merchant-transport.ts
1424
1486
  var DEFAULT_MERCHANT_TIMEOUT = 3e5;
1425
1487
  var MCP_NOTIFICATION_TIMEOUT = 1e4;
@@ -1577,6 +1639,7 @@ var McpMerchantTransport = class {
1577
1639
  * overwrite, which is worse than the duplicate this change removes.
1578
1640
  */
1579
1641
  async deliverPayment(url, init, paymentHeader) {
1642
+ assertSecureX402RetryTarget(url);
1580
1643
  const headers = new Headers(init?.headers);
1581
1644
  const send = x402PaymentHeaderNamesFor(paymentHeader);
1582
1645
  for (const name of X402_PAYMENT_HEADER_NAMES) {
@@ -1691,6 +1754,10 @@ function deriveReadiness(status, allowances) {
1691
1754
  if (status !== "active") return "revoked";
1692
1755
  return allowances.some((allowance) => safeBigInt(allowance.remainingAtomic) > 0n) ? "ready" : "needs_approval";
1693
1756
  }
1757
+ function formatRemainingDisplay(tokenAddress, tokenSymbol, remainingAtomic) {
1758
+ const token = resolveTokenFromAddress(tokenAddress);
1759
+ return token ? `${formatAtomicAmount(safeBigInt(remainingAtomic), token.decimals)} ${tokenSymbol}` : `${remainingAtomic} ${tokenSymbol} (atomic; unknown decimals)`;
1760
+ }
1694
1761
  var AccountReads = class {
1695
1762
  transport;
1696
1763
  getPaymentStatus;
@@ -1712,12 +1779,12 @@ var AccountReads = class {
1712
1779
  async getAgentSummary() {
1713
1780
  const [agent, allowanceSummary] = await Promise.all([this.getAgent(), this.getAllowances()]);
1714
1781
  const allowances = allowanceSummary.allowances.map((allowance) => {
1715
- const token = resolveTokenFromAddress(allowance.tokenAddress);
1716
- const remainingDisplay = token ? `${formatAtomicAmount(safeBigInt(allowance.onchain.remaining), token.decimals)} ${allowance.tokenSymbol}` : `${allowance.onchain.remaining} ${allowance.tokenSymbol} (atomic; unknown decimals)`;
1717
1782
  return {
1783
+ id: allowance.id,
1718
1784
  tokenSymbol: allowance.tokenSymbol,
1785
+ tokenAddress: allowance.tokenAddress,
1719
1786
  remainingAtomic: allowance.onchain.remaining,
1720
- remainingDisplay,
1787
+ remainingDisplay: allowance.remainingDisplay,
1721
1788
  configuredAmount: allowance.configuredAmount,
1722
1789
  resetPeriodMin: allowance.resetPeriodMin,
1723
1790
  isResetPending: allowance.onchain.isResetPending
@@ -1745,6 +1812,7 @@ var AccountReads = class {
1745
1812
  tokenSymbol: allowance.token_symbol,
1746
1813
  configuredAmount: allowance.configured_amount,
1747
1814
  resetPeriodMin: allowance.reset_period_min,
1815
+ remainingDisplay: formatRemainingDisplay(allowance.token_address, allowance.token_symbol, allowance.onchain.remaining),
1748
1816
  onchain: {
1749
1817
  amount: allowance.onchain.amount,
1750
1818
  spent: allowance.onchain.spent,
@@ -1759,6 +1827,33 @@ var AccountReads = class {
1759
1827
  }))
1760
1828
  };
1761
1829
  }
1830
+ /**
1831
+ * #3126 — the sufficiency signal behind {@link HavenBalanceCoverage}.
1832
+ *
1833
+ * Deliberately NOT a balance read: the endpoint answers whether the
1834
+ * account HOLDS at least the checked amount, as `covered
1835
+ * true/false/null`, and never returns the balance itself. The camelCase
1836
+ * mapping is permissive (raw fields flow through; the server owns the
1837
+ * wire shape, pinned by the backend's `expectMatchesSpec` assertion), so
1838
+ * an older server that has not deployed the endpoint surfaces its 404 as
1839
+ * a thrown error rather than a fabricated answer.
1840
+ */
1841
+ async checkFunds(input) {
1842
+ const query = `token=${encodeURIComponent(input.token)}&amount_atomic=${encodeURIComponent(input.amountAtomic)}`;
1843
+ const raw = await this.transport.get(
1844
+ `/machine-payments/balance-coverage?${query}`
1845
+ );
1846
+ return {
1847
+ covered: raw.covered,
1848
+ ...raw.coverage_error !== void 0 ? { coverageError: raw.coverage_error } : {},
1849
+ chainId: raw.chain_id,
1850
+ tokenAddress: raw.token_address,
1851
+ tokenSymbol: raw.token_symbol,
1852
+ checkedAmountAtomic: raw.checked_amount_atomic,
1853
+ budgetRemainingAtomic: raw.budget_remaining_atomic,
1854
+ ...raw.budget_remaining_is_from_chain !== void 0 ? { budgetRemainingIsFromChain: raw.budget_remaining_is_from_chain } : {}
1855
+ };
1856
+ }
1762
1857
  async getPostPurchaseAllowanceSummary(paymentId) {
1763
1858
  const unavailable = (detail, payment2 = null) => ({
1764
1859
  payment: payment2,
@@ -1810,10 +1905,23 @@ var AccountReads = class {
1810
1905
  return unavailable(error instanceof Error ? error.message : String(error));
1811
1906
  }
1812
1907
  }
1908
+ /** The first page's receipts as a bare array — the pre-#3128 shape, kept for callers that never page. */
1813
1909
  async listReceipts(options = {}) {
1814
- const query = options.limit ? `?limit=${encodeURIComponent(String(options.limit))}` : "";
1910
+ return (await this.listReceiptsPage(options)).receipts;
1911
+ }
1912
+ /** #3128: one page with `total`, `hasMore` and `nextCursor` — see {@link HavenPaymentReceiptsPage}. */
1913
+ async listReceiptsPage(options = {}) {
1914
+ const params = new URLSearchParams();
1915
+ if (options.limit) params.set("limit", String(options.limit));
1916
+ if (options.cursor) params.set("cursor", options.cursor);
1917
+ const query = params.size > 0 ? `?${params.toString()}` : "";
1815
1918
  const raw = await this.transport.get(`/machine-payments/receipts${query}`);
1816
- return raw.receipts.map(mapPaymentReceipt);
1919
+ return {
1920
+ receipts: raw.receipts.map(mapPaymentReceipt),
1921
+ total: typeof raw.total === "number" ? raw.total : null,
1922
+ hasMore: typeof raw.has_more === "boolean" ? raw.has_more : null,
1923
+ nextCursor: typeof raw.next_cursor === "string" ? raw.next_cursor : null
1924
+ };
1817
1925
  }
1818
1926
  async getReceipt(paymentId) {
1819
1927
  const { receipt } = await this.transport.get(`/payments/${paymentId}/receipt`);
@@ -1985,8 +2093,11 @@ function requestInitFromSnapshot(request) {
1985
2093
  }
1986
2094
  function noCompatiblePaymentOptionError(accepts) {
1987
2095
  const erc7710Only = selectErc7710PaymentOption(accepts) !== null;
2096
+ const unsupportedOnly = !erc7710Only && accepts.some(
2097
+ (opt) => opt !== null && typeof opt === "object" && !isErc7710Option(opt) && (x402AssetTransferMethod(opt) !== null || opt.extra?.paymentFlow !== void 0)
2098
+ );
1988
2099
  return new HavenApiError(
1989
- "No compatible payment option found in x402 requirements. Haven supports standard x402 exact payments on Base USDC." + (erc7710Only ? " The only Haven-compatible option this merchant advertises is tagged extra.assetTransferMethod: 'erc7710' (direct settlement), which this EIP-3009 payment path cannot settle \u2014 the limitation is the settlement scheme, not the asset. Paying this merchant requires a delegation-rail erc7710 flow (settleX402Erc7710, or the hosted MCP purchase tools)." : ""),
2100
+ "No compatible payment option found in x402 requirements. Haven supports standard x402 exact payments on Base USDC." + (erc7710Only ? " The only Haven-compatible option this merchant advertises is tagged extra.assetTransferMethod: 'erc7710' (direct settlement), which this EIP-3009 payment path cannot settle \u2014 the limitation is the settlement scheme, not the asset. Paying this merchant requires a delegation-rail erc7710 flow (settleX402Erc7710, or the hosted MCP purchase tools)." : unsupportedOnly ? " Every option this merchant advertises asks for a transfer method or payment flow Haven cannot construct (for example extra.assetTransferMethod: 'permit2', or an unrecognized extra.paymentFlow). No payment intent was created and no funds moved." : ""),
1990
2101
  400
1991
2102
  );
1992
2103
  }
@@ -2006,6 +2117,11 @@ function buildX402Quote(paymentRequired, request, idempotencyKey, mcpTransport)
2006
2117
  request,
2007
2118
  ...mcpTransport ? { mcpTransport } : {},
2008
2119
  resourceUrl: paymentRequired.resource.url,
2120
+ // #3097: the merchant's declaration vs the URL the caller quoted. The
2121
+ // paid retry goes to the caller's URL; a quote that says the two disagree
2122
+ // is how an agent sees a challenge that downgrades the scheme or moves
2123
+ // the host before it pays.
2124
+ resourceUrlDiffersFromRequest: paymentRequired.resource.url !== request.url,
2009
2125
  description: paymentRequired.resource.description ?? option.description ?? null,
2010
2126
  mimeType: paymentRequired.resource.mimeType ?? option.mimeType ?? null,
2011
2127
  amountAtomic: x402AuthorizationAmount(option),
@@ -3079,7 +3195,18 @@ function mapCatalogEntry(entry) {
3079
3195
  verifiedAt: entry.verified_at,
3080
3196
  source: entry.source,
3081
3197
  domainVerified: entry.domain_verified,
3082
- verifiedPayable: entry.verified_payable
3198
+ verifiedPayable: entry.verified_payable,
3199
+ // #3078: absent (older backend) or null (unresolved join) both mean "no
3200
+ // merchant known" — the public field is then absent, never null.
3201
+ ...entry.merchant ? {
3202
+ merchant: {
3203
+ id: entry.merchant.id,
3204
+ slug: entry.merchant.slug,
3205
+ name: entry.merchant.name,
3206
+ listingStatus: entry.merchant.listing_status,
3207
+ isTestMerchant: entry.merchant.is_test_merchant
3208
+ }
3209
+ } : {}
3083
3210
  };
3084
3211
  }
3085
3212
  var HavenClient = class {
@@ -3461,6 +3588,49 @@ var HavenClient = class {
3461
3588
  async getAllowances() {
3462
3589
  return this.accountReads.getAllowances();
3463
3590
  }
3591
+ /**
3592
+ * #3126 — is the checked amount of the token actually HELD on the
3593
+ * agent's own account?
3594
+ *
3595
+ * This is the companion to {@link getAllowances}, not a variant of it:
3596
+ * allowances answer what the agent is PERMITTED to spend this period;
3597
+ * this answers whether the account HOLDS funds behind that permission,
3598
+ * as a sufficiency signal — `covered: true | false | null` — never as a
3599
+ * balance. `covered: null` means the chain read failed: treat it as
3600
+ * unverifiable, not as absence (`coverageError` says why). The account's
3601
+ * balance itself is deliberately not returned.
3602
+ */
3603
+ async checkFunds(input) {
3604
+ return this.accountReads.checkFunds(input);
3605
+ }
3606
+ /**
3607
+ * `POST /machine-payments/budget-precheck` (#3054): ask Haven to decide —
3608
+ * server-side — whether `amountAtomic` of `token` fits the agent's
3609
+ * remaining delegation budget, the same compare the guided prepare used to
3610
+ * run locally over its allowances read.
3611
+ *
3612
+ * On insufficiency Haven refuses (403, `delegation_budget_exceeded`) and
3613
+ * the refusal reaches the `payment_refusals` ledger with
3614
+ * `source: 'hosted_prepare'` — the point of the endpoint. This method
3615
+ * surfaces that decision as a thrown {@link HavenApiError}; it does NOT
3616
+ * swallow it, because swallowing would turn a decided refusal into the
3617
+ * degrade-to-warning path and the ledger row would still land while the
3618
+ * purchase proceeded.
3619
+ *
3620
+ * camelCase body like the route family; the response mirrors the wire
3621
+ * (`sufficient`, `remaining_atomic`). `resourceUrl` is the merchant
3622
+ * resource being bought — the ledger dedupe window's discriminating
3623
+ * column — never this request's own URL.
3624
+ */
3625
+ async precheckBudget(input) {
3626
+ return this.post("/machine-payments/budget-precheck", {
3627
+ chainId: input.chainId,
3628
+ token: input.token,
3629
+ amountAtomic: input.amountAtomic,
3630
+ ...input.merchantTo !== void 0 ? { merchantTo: input.merchantTo } : {},
3631
+ ...input.resourceUrl !== void 0 ? { resourceUrl: input.resourceUrl } : {}
3632
+ });
3633
+ }
3464
3634
  /**
3465
3635
  * Post-purchase allowance/budget summary for a settled payment (#1310).
3466
3636
  *
@@ -3583,6 +3753,10 @@ var HavenClient = class {
3583
3753
  async listReceipts(options = {}) {
3584
3754
  return this.accountReads.listReceipts(options);
3585
3755
  }
3756
+ /** #3128: one page of receipts with `total`, `hasMore` and `nextCursor`. */
3757
+ async listReceiptsPage(options = {}) {
3758
+ return this.accountReads.listReceiptsPage(options);
3759
+ }
3586
3760
  /**
3587
3761
  * Fetch the verifiable receipt bundle for a settled payment and verify it
3588
3762
  * locally. The server's own verification is ignored — the receipt is verified
@@ -4220,21 +4394,29 @@ var toolDescriptions = {
4220
4394
  nextActionGuidance: ""
4221
4395
  },
4222
4396
  getAgent: {
4223
- summary: "Return the authenticated agent identity AND its live spend authority in one call: Haven wallet, delegate, chain, raw status, spend_authority_readiness, and per-token remaining allowance (atomic + human-readable). The recommended first call in a new session to confirm who you are and whether Haven will let you spend right now.",
4224
- selectionGuidance: "Use this as the one-shot orientation/bootstrap at the start of a session, or whenever you need to confirm identity together with whether the agent can spend right now. For a detailed per-token breakdown (configured vs spent vs reset window) use haven_get_allowances.",
4225
- behavior: `Reads identity plus the live spend-authority snapshot in one shot \u2014 the agent's active on-chain budget delegation. spend_authority_readiness (readiness is a deprecated alias, same value) is "ready" when at least one token has remaining spend authority, "needs_approval" when the agent is active but has none, and "revoked" when the credential is not active. It covers hosted identity + on-chain spend authority ONLY \u2014 the hosted server cannot see the LOCAL signer, so "ready" does not mean the signer can start; verify the signer with a signer tool call or connect --doctor. An over-budget payment is declined before any money moves: there is no approval queue, so ask the owner to grant or raise the budget in Haven rather than waiting for an approval. allowances[] carries remainingAtomic and remainingDisplay per token. Identity fields: id, name, status, accountAddress, delegateAddress, chainId.`,
4397
+ summary: "Return the authenticated agent identity AND its live spend authority in one call: Haven wallet, delegate, chain, raw status, spend_authority_readiness, per-token remaining allowance (atomic + human-readable). The recommended first call in a new session.",
4398
+ selectionGuidance: "Use this as the session bootstrap, or to confirm identity together with whether the agent can spend right now. For per-token detail (configured vs spent vs reset window) use haven_get_allowances.",
4399
+ behavior: 'Reads identity plus the live spend-authority snapshot \u2014 the active on-chain budget delegation. spend_authority_readiness (readiness is a deprecated alias, same value) is "ready" when at least one token has remaining spend authority, "needs_approval" when the agent is active but has none, and "revoked" when the credential is not active. It covers hosted identity + on-chain spend authority ONLY \u2014 the hosted server cannot see the LOCAL signer, so "ready" does not mean the signer can start; verify the signer with a signer tool call or connect --doctor. An over-budget payment is declined before any money moves; there is no approval queue \u2014 ask the owner to grant or raise the budget in Haven. allowances[] carries id, tokenAddress, remainingAtomic, remainingDisplay per token. Identity fields: id, name, status, accountAddress, delegateAddress, chainId.',
4226
4400
  nextActionGuidance: ""
4227
4401
  },
4228
4402
  getAllowances: {
4229
4403
  summary: "Return configured and on-chain allowance state for the authenticated agent. On-chain allowance is the real spend gate.",
4230
- selectionGuidance: "Use this when the user asks about allowance, budget, spend limit, remaining amount, remaining allowance, remaining budget, daily limit, reset period, what can I spend, or what the agent can still spend.",
4231
- behavior: "Returns the per-token spend authority for the account: the active budget delegation (remaining = the period budget, which re-arms natively at the period boundary). An over-budget payment is declined before any money moves; nothing queues. Configured amounts from Haven are returned alongside.",
4404
+ selectionGuidance: "Use this when the user asks about allowance, budget, spend limit, remaining amount, remaining allowance, remaining budget, daily limit, reset period, what can I spend, or what the agent can still spend. For whether the account actually HOLDS funds behind the budget use haven_check_funds.",
4405
+ behavior: "Returns the per-token spend authority for the account: the active budget delegation (remaining = the period budget, which re-arms natively at the period boundary), each with id, onchain.remaining, remainingDisplay. An over-budget payment is declined before any money moves; nothing queues. Configured amounts from Haven are returned alongside.",
4232
4406
  nextActionGuidance: ""
4233
4407
  },
4408
+ // #3126 — the sufficiency signal, deliberately NOT a balance tool. The
4409
+ // constrained actor reads a boolean, never the treasury total.
4410
+ checkFunds: {
4411
+ summary: "Check whether the agent's account actually holds at least the given amount of a token \u2014 funds held, not spend permitted.",
4412
+ selectionGuidance: "Use this before attempting a payment when it matters whether the money is really there: allowance answers say what you are PERMITTED to spend, never whether the account HOLDS it. For allowance, budget, spend-limit, remaining-budget, reset-period, or what-can-I-spend questions use the allowance lookup tool instead.",
4413
+ behavior: "Returns covered: true (the account holds at least the checked amount), false (a live chain read reports less \u2014 the budget is backed by an empty account; stop and tell the user funds are missing), or null (the chain read failed \u2014 unverifiable, never treat it as absence; coverageError says why). The account balance itself is deliberately not returned: this is a sufficiency signal, not a balance read. budget_remaining_atomic is the permitted figure from the allowance lookup (the SDK spells it budgetRemainingAtomic), named so it can never be confused with holdings.",
4414
+ nextActionGuidance: "On covered=false, do not attempt the payment \u2014 tell the user the account is short and let them fund it; on covered=null, retry the check shortly or proceed knowing the payment may fail on-chain."
4415
+ },
4234
4416
  listReceipts: {
4235
- summary: "List recent machine-payment receipts and evidence for bookkeeping.",
4236
- selectionGuidance: "Use this for transaction history, receipts, payment evidence, or bookkeeping; use the allowance tool instead for remaining allowance, budget, spend-limit, or what-can-I-spend questions.",
4237
- behavior: "Returns the agent's recent machine-payment receipts ordered by recency. Proof header values are not returned.",
4417
+ summary: "List machine-payment receipts, newest first, by page.",
4418
+ selectionGuidance: "For transaction history or payment evidence; use the allowance tool instead for remaining allowance or what-can-I-spend questions.",
4419
+ behavior: "Page: { receipts, total, hasMore, nextCursor }; total 0 = none exist (no indexing delay); hasMore = cut at limit, send nextCursor as cursor. parties.treasuryAccount is Haven's authoritative payer. protocolReceiptPayload is the merchant's PAYMENT-RESPONSE, relayed verbatim: merchant-controlled, unverified, not Haven's record; payer may differ from payerAddress. Proof header values are omitted.",
4238
4420
  nextActionGuidance: ""
4239
4421
  },
4240
4422
  verifyReceipt: {
@@ -4250,10 +4432,10 @@ var toolDescriptions = {
4250
4432
  nextActionGuidance: "On a decline, report the reason to the user and ask them to raise the budget in Haven \u2014 there is no approval queue to wait on. This tool retries the merchant itself while it runs, so do not wait on a signal mid-call. If the process crashes after payment, a later haven_get_payment_status call may report nextAction=retry_original_x402_request \u2014 resume via haven_resume_x402_payment instead of paying again."
4251
4433
  },
4252
4434
  discoverTools: {
4253
- 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.",
4254
- 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. Use verified=verified for entries Haven watched pass a live quote probe (operator-curated or self-submitted) \u2014 domain_verified is the only ownership claim; never treat these badges as proof of merchant honesty, quality, or reliability. 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.",
4255
- 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, suggested_tool, and the provenance badges source/domain_verified/verified_payable. 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.",
4256
- 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.`
4435
+ summary: "Step 1 of a purchase: discover payable services from Haven's curated merchant catalog \u2014 names, prices, the next call.",
4436
+ 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. Use verified=verified for entries Haven watched pass a live quote probe (operator-curated or self-submitted) \u2014 domain_verified is the only ownership claim; never treat these badges as proof of merchant honesty, quality, or reliability. 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 next tool to call.",
4437
+ behavior: "Use each entry's suggested_tool + suggested_arguments first; an entry without them says why. 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, the hint, and the provenance badges source/domain_verified/verified_payable. 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.",
4438
+ nextActionGuidance: `Call suggested_tool with suggested_arguments VERBATIM (no hint: read suggested_tool_omitted_reason). Confirm the price from the live quote or pay result, not the catalog; if the next tool takes a cap, pass the user's cap as max_amount_human in whole tokens ("no more than 1 USDC" \u2192 max_amount_human: "1"), never atomic units by hand.`
4257
4439
  },
4258
4440
  submitCatalogEntry: {
4259
4441
  summary: "Submit a merchant's payable (x402/MCP) endpoint to Haven's Verified Payable Directory for verification and listing.",
@@ -4263,13 +4445,13 @@ var toolDescriptions = {
4263
4445
  },
4264
4446
  sweep_delegate: {
4265
4447
  summary: "Sweep stranded USDC and/or ETH from the delegate wallet back to the originating account.",
4266
- selectionGuidance: "Use this when the user instructs you to recover stranded funds on the delegate wallet, or when a payment status returns nextAction=sweep_stranded_funds. Do NOT use for normal payments \u2014 use haven_pay_x402. Do NOT use to read balances only \u2014 use haven_get_allowances.",
4448
+ selectionGuidance: "Use this when the user instructs you to recover stranded funds on the delegate wallet, or when a payment status returns nextAction=sweep_stranded_funds. Do NOT use for normal payments \u2014 use haven_pay_x402. Do NOT use to read balances only \u2014 use haven_get_allowances, or haven_check_funds for whether the account holds funds.",
4267
4449
  behavior: `Reads the delegate EOA's on-chain USDC and ETH balances. For each non-zero balance, signs and submits a transfer from the delegate EOA to the originating account (hardcoded destination). The delegate key signs locally \u2014 Haven never sees it and the backend never constructs signed transactions (CASP/MiCA Red Line #2). Returns tx hashes and recovered amounts. Returns an empty transfers list when nothing is stranded. Each transfer carries confirmation: "confirmed" (a receipt was seen \u2014 the funds are in the account) or "unconfirmed" (broadcast but not confirmed within 90 seconds \u2014 still in the mempool, may still land). The top-level unconfirmed flag is true when any transfer is unconfirmed.`,
4268
4450
  nextActionGuidance: 'If transfers is non-empty, confirm the amounts with the user. Report a transfer as recovered ONLY when its confirmation is "confirmed". For an "unconfirmed" transfer, tell the user it was submitted but not yet confirmed, give them its txHash and explorerUrl to check, and do not re-run the sweep immediately \u2014 a re-run after it lands will simply find nothing stranded.'
4269
4451
  },
4270
4452
  send: {
4271
4453
  summary: "Send ETH or USDC directly from the agent's Haven wallet to a recipient address.",
4272
- selectionGuidance: "Use this for plain transfers \u2014 refunding a user, paying a freelancer, topping up a co-agent's wallet, or moving funds between addresses. Do NOT use for x402 paid endpoints \u2014 use haven_pay_x402 instead. Do NOT use for read-only allowance, budget, or what-can-I-spend questions \u2014 use haven_get_allowances.",
4454
+ selectionGuidance: "Use this for plain transfers \u2014 refunding a user, paying a freelancer, topping up a co-agent's wallet, or moving funds between addresses. Do NOT use for x402 paid endpoints \u2014 use haven_pay_x402 instead. Do NOT use for read-only allowance, budget, or what-can-I-spend questions \u2014 use haven_get_allowances. Do NOT use to check whether funds are held before sending \u2014 use haven_check_funds.",
4273
4455
  behavior: "Sends the requested amount by redeeming the agent's on-chain budget delegation, account to recipient with no funding leg. Budget, recipient and expiry are enforced on-chain while the transfer is prepared, so a request outside them is declined before any money moves and before the agent is asked to sign \u2014 it is never queued for a human to approve later. The agent's signing key signs the account's typed data; Haven never receives the key.",
4274
4456
  nextActionGuidance: "On a decline, report the reason to the user and ask them to grant or raise the budget in Haven \u2014 there is nothing to poll and no approval will arrive. After a successful send, poll haven_get_payment_status until nextAction=none."
4275
4457
  },
@@ -4648,8 +4830,10 @@ the \`mcp__haven-signer__\` namespace and keep the delegate key on this machine.
4648
4830
  That namespacing is Claude-family; other runtimes name the servers by their
4649
4831
  own config keys (Codex: \`haven\`, \`haven_signer\`). Tool results carry the
4650
4832
  exact next step (\`next_action\`, \`next_tool\`, \`next_arguments\`, plus the
4651
- runtime-neutral \`next_tool_server\` + \`next_tool_name\` \u2014 the bare tool name
4652
- on that logical server, whatever your runtime calls it).
4833
+ runtime-neutral \`next_tool_server\` + \`next_tool_name\` + \`next_tool_server_role\`
4834
+ \u2014 the bare tool name on that logical server, whatever your runtime calls it).
4835
+ When no tool follows, \`next_tool\` is absent and \`next_tool_omitted_reason\`
4836
+ says why; that is a complete answer.
4653
4837
  Follow those fields first; the prose below is fallback and orientation, not
4654
4838
  the source of truth.
4655
4839
 
@@ -4736,6 +4920,13 @@ spending:
4736
4920
  local signer; the signer is verified by calling any signer tool.
4737
4921
  - \`mcp__haven__haven_get_allowances\` \u2014 detailed per-token breakdown
4738
4922
  (configured, spent, reset window) when you need more than the summary.
4923
+ - \`mcp__haven__haven_check_funds\` \u2014 whether the account actually HOLDS at
4924
+ least a given amount of a token. Allowance answers above say what you are
4925
+ permitted to spend; this one says whether the money is really there,
4926
+ answered as \`covered\` true/false/null \u2014 never as a balance. On
4927
+ \`covered: false\`, stop and tell the user the account is short; on
4928
+ \`covered: null\` (the chain read failed), treat it as unverifiable rather
4929
+ than as absence.
4739
4930
 
4740
4931
  Budgets reset on a period the user chose. If a payment exceeds the remaining
4741
4932
  budget it is declined before any money moves \u2014 tell the user; they can raise
@@ -4864,8 +5055,10 @@ check on in-flight payments. Do not poll in a tight loop.
4864
5055
  ## Failure handling
4865
5056
 
4866
5057
  Haven tool failures are shaped like \`{ success: false, code, message, ... }\`
4867
- or older \`{ error, status, details? }\` responses. Branch on \`code\` when
4868
- present and surface \`message\` or \`error\` verbatim. Common cases:
5058
+ or older \`{ error, status, details? }\` responses. A failure carries the same
5059
+ \`next_action\` / \`next_tool\` / \`next_arguments\` / \`next_tool_omitted_reason\`
5060
+ fields a success does; follow them first, then branch on \`code\` and surface
5061
+ \`message\` or \`error\` verbatim. Common cases:
4869
5062
 
4870
5063
  - \`insufficient_funds\`: the Haven wallet doesn't hold enough of that token.
4871
5064
  Suggest the user add funds in the Haven dashboard.
@@ -4926,6 +5119,64 @@ for that credential.
4926
5119
  var SKILL_FOLDER_NAME = "haven-pay";
4927
5120
  var HAVEN_SKILL_BODY_MD = HAVEN_SKILL_MD.replace(/^---\n[\s\S]*?\n---\n+/, "");
4928
5121
 
5122
+ // src/next-step.ts
5123
+ var NEXT_TOOL_SERVER_NAMES = {
5124
+ hosted: "haven",
5125
+ signer: "haven-signer"
5126
+ };
5127
+ var NEXT_TOOL_SERVER_ROLES = {
5128
+ haven: "hosted",
5129
+ "haven-signer": "signer"
5130
+ };
5131
+ function renderNextTool(role, name) {
5132
+ return `mcp__${NEXT_TOOL_SERVER_NAMES[role]}__${name}`;
5133
+ }
5134
+ function parseNextTool(literal) {
5135
+ const m = /^mcp__([a-z0-9-]+)__([a-z0-9_]+)$/.exec(literal);
5136
+ if (!m) return null;
5137
+ const role = NEXT_TOOL_SERVER_ROLES[m[1]];
5138
+ return { server: m[1], name: m[2], ...role ? { role } : {} };
5139
+ }
5140
+ var DEFAULT_NEXT_TOOL_BY_ACTION = {
5141
+ check_status_later: "haven_get_payment_status",
5142
+ sweep_stranded_funds: "haven_sweep_delegate"
5143
+ // `retry_original_x402_request` is NOT here: its only live emitter
5144
+ // (state-direct-recovery.ts, erc7710) names no tool on purpose — the retry
5145
+ // is the agent's own HTTP call — so a default would contradict the site.
5146
+ };
5147
+ function defaultNextToolFor(action) {
5148
+ return DEFAULT_NEXT_TOOL_BY_ACTION[action];
5149
+ }
5150
+ function createNextStepBuilder(targets) {
5151
+ return function nextStep(input) {
5152
+ const i = input;
5153
+ const base = {
5154
+ next_action: input.nextAction,
5155
+ safe_to_continue: input.safeToContinue,
5156
+ reason: input.reason
5157
+ };
5158
+ if (i.nextTool === null) {
5159
+ return { ...base, next_tool_omitted_reason: i.nextToolOmittedReason ?? "no next tool" };
5160
+ }
5161
+ const target = targets[i.nextTool];
5162
+ if (!target) {
5163
+ return { ...base, next_tool_omitted_reason: `${i.nextTool} is not a registered next-step target` };
5164
+ }
5165
+ const problem = target.validate(i.nextArguments);
5166
+ if (problem) {
5167
+ return { ...base, next_tool_omitted_reason: `next_arguments do not parse under ${i.nextTool}: ${problem}` };
5168
+ }
5169
+ return {
5170
+ ...base,
5171
+ next_tool: renderNextTool(target.role, i.nextTool),
5172
+ next_tool_server: NEXT_TOOL_SERVER_NAMES[target.role],
5173
+ next_tool_name: i.nextTool,
5174
+ next_tool_server_role: target.role,
5175
+ next_arguments: i.nextArguments
5176
+ };
5177
+ };
5178
+ }
5179
+
4929
5180
  // src/node-version.ts
4930
5181
  var HAVEN_MINIMUM_NODE_VERSION = "22.0.0";
4931
5182
  function parseNodeVersion(value) {
@@ -5038,6 +5289,7 @@ exports.AgentPaymentRailSchema = AgentPaymentRailSchema;
5038
5289
  exports.AgentPaymentWarningCode = AgentPaymentWarningCode;
5039
5290
  exports.CONNECTOR_PACKAGE_NAME = CONNECTOR_PACKAGE_NAME;
5040
5291
  exports.DEFAULT_CONFIRMATION_TIMEOUT_MS = DEFAULT_CONFIRMATION_TIMEOUT_MS;
5292
+ exports.DEFAULT_NEXT_TOOL_BY_ACTION = DEFAULT_NEXT_TOOL_BY_ACTION;
5041
5293
  exports.DISCOVERY_MAX_BYTES = DISCOVERY_MAX_BYTES;
5042
5294
  exports.ERC7710_ASSET_TRANSFER_METHOD = ERC7710_ASSET_TRANSFER_METHOD;
5043
5295
  exports.HAVEN_AGENT_RUNBOOK_MD = HAVEN_AGENT_RUNBOOK_MD;
@@ -5048,13 +5300,17 @@ exports.HAVEN_SKILL_MD = HAVEN_SKILL_MD;
5048
5300
  exports.HavenApiError = HavenApiError;
5049
5301
  exports.HavenClient = HavenClient;
5050
5302
  exports.HavenError = HavenError;
5303
+ exports.HavenInsecureRetryTargetError = HavenInsecureRetryTargetError;
5051
5304
  exports.HavenPaymentStateError = HavenPaymentStateError;
5052
5305
  exports.HavenSigningError = HavenSigningError;
5053
5306
  exports.HavenTimeoutError = HavenTimeoutError;
5054
5307
  exports.HavenUnsupportedSignerVersionError = HavenUnsupportedSignerVersionError;
5055
5308
  exports.HavenZeroSettlementHashError = HavenZeroSettlementHashError;
5309
+ exports.INSECURE_RETRY_TARGET_CODE = INSECURE_RETRY_TARGET_CODE;
5056
5310
  exports.MERCHANT_DISCOVERY_PATHS = MERCHANT_DISCOVERY_PATHS;
5057
5311
  exports.MerchantTimeoutError = MerchantTimeoutError;
5312
+ exports.NEXT_TOOL_SERVER_NAMES = NEXT_TOOL_SERVER_NAMES;
5313
+ exports.NEXT_TOOL_SERVER_ROLES = NEXT_TOOL_SERVER_ROLES;
5058
5314
  exports.RECEIPT_VERSION = RECEIPT_VERSION;
5059
5315
  exports.SIGNER_UPDATE_FALLBACK = SIGNER_UPDATE_FALLBACK;
5060
5316
  exports.SKILL_FOLDER_NAME = SKILL_FOLDER_NAME;
@@ -5075,6 +5331,7 @@ exports.X402_PAYMENT_REQUIRED_HEADER_NAME = X402_PAYMENT_REQUIRED_HEADER_NAME;
5075
5331
  exports.X402_PAYMENT_RESPONSE_HEADER_NAME = X402_PAYMENT_RESPONSE_HEADER_NAME;
5076
5332
  exports.X402_SETTLEMENT_FORWARD_MARGIN_SECONDS = X402_SETTLEMENT_FORWARD_MARGIN_SECONDS;
5077
5333
  exports.addressFromKey = addressFromKey;
5334
+ exports.assertSecureX402RetryTarget = assertSecureX402RetryTarget;
5078
5335
  exports.buildSweepAuthorizationMessage = buildSweepAuthorizationMessage;
5079
5336
  exports.buildSweepTypedData = buildSweepTypedData;
5080
5337
  exports.buildX402ExpectedMessage = buildX402ExpectedMessage;
@@ -5082,8 +5339,10 @@ exports.compareNodeVersions = compareNodeVersions;
5082
5339
  exports.composeDescription = composeDescription;
5083
5340
  exports.connectorRerunCommand = connectorRerunCommand;
5084
5341
  exports.connectorSpec = connectorSpec;
5342
+ exports.createNextStepBuilder = createNextStepBuilder;
5085
5343
  exports.decodeBase64Json = decodeBase64Json;
5086
5344
  exports.decodeBase64Utf8 = decodeBase64Utf8;
5345
+ exports.defaultNextToolFor = defaultNextToolFor;
5087
5346
  exports.discoverMerchantMcpUrl = discoverMerchantMcpUrl;
5088
5347
  exports.encodeBase64Json = encodeBase64Json;
5089
5348
  exports.encodeBase64Utf8 = encodeBase64Utf8;
@@ -5091,15 +5350,19 @@ exports.encodePaymentProof = encodePaymentProof;
5091
5350
  exports.havenTools = havenTools;
5092
5351
  exports.isConnectorChannel = isConnectorChannel;
5093
5352
  exports.isErc7710Option = isErc7710Option;
5353
+ exports.isSecureX402RetryTarget = isSecureX402RetryTarget;
5094
5354
  exports.isSupportedNodeVersion = isSupportedNodeVersion;
5095
5355
  exports.isSweepableChain = isSweepableChain;
5096
5356
  exports.isZeroSettlementTxHash = isZeroSettlementTxHash;
5097
5357
  exports.normalizePaymentRequired = normalizePaymentRequired;
5358
+ exports.parseNextTool = parseNextTool;
5098
5359
  exports.parsePaymentRequired = parsePaymentRequired;
5099
5360
  exports.parsePaymentRequiredResponse = parsePaymentRequiredResponse;
5100
5361
  exports.readX402ReceiptPayer = readX402ReceiptPayer;
5362
+ exports.renderNextTool = renderNextTool;
5101
5363
  exports.resolveConnectorChannel = resolveConnectorChannel;
5102
5364
  exports.resolveTokenFromAddress = resolveTokenFromAddress;
5365
+ exports.resolveX402RetryTarget = resolveX402RetryTarget;
5103
5366
  exports.sameUrl = sameUrl;
5104
5367
  exports.selectErc7710PaymentOption = selectErc7710PaymentOption;
5105
5368
  exports.selectPaymentOption = selectPaymentOption;