@haven_ai/sdk 0.1.22-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/README.md CHANGED
@@ -323,7 +323,9 @@ Hosted MCP and signer tools also return stable `code` values on recoverable x402
323
323
 
324
324
  | `code` | Meaning | Agent recovery |
325
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`. |
326
+ | `PRICE_EXCEEDS_MAX` | The merchant-authoritative x402 price is above the caller's spending cap. No funding transfer was created. | Tell the user the live price exceeded the cap and retry only after they confirm a higher one. |
327
+ | `AMBIGUOUS_MAX_AMOUNT` | Both `max_amount` (atomic units) and `max_amount_human` (whole tokens) were sent 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. |
328
+ | `MAX_AMOUNT_UNCONVERTIBLE` | `max_amount_human` could not be converted against this quote's asset — its decimals are unknown to Haven, or the cap has more decimal places than the asset supports. Nothing was spent. | Round the cap to the asset's decimals, or re-send it as an exact atomic `max_amount`. |
327
329
  | `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
330
  | `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. |
329
331
 
package/dist/index.cjs CHANGED
@@ -104,7 +104,23 @@ var AgentPaymentFailureCode = {
104
104
  * mechanical: re-send merchant_url, tool_name, arguments, and
105
105
  * mcp_transport explicitly (the version-skew path).
106
106
  */
107
- MerchantCallContextUnavailable: "MERCHANT_CALL_CONTEXT_UNAVAILABLE"
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"
108
124
  };
109
125
  var AgentPaymentRail = {
110
126
  /** Standard Haven payment from the user's Safe through an approved delegate allowance. */
@@ -159,7 +175,9 @@ var AgentPaymentFailureCodeDescriptions = {
159
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.",
160
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.",
161
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.",
162
- [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."
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."
163
181
  };
164
182
  var AgentPaymentWarningCode = {
165
183
  /** No max_amount cap was supplied — the live quoted price was accepted as-is. */
@@ -1094,8 +1112,7 @@ var HavenClient = class {
1094
1112
  400
1095
1113
  );
1096
1114
  }
1097
- const agent = await this.getAgent();
1098
- const fundingTo = agent.delegateAddress;
1115
+ const fundingTo = options.delegateAddress ?? (await this.getAgent()).delegateAddress;
1099
1116
  if (!fundingTo) {
1100
1117
  throw new HavenApiError("Authenticated agent has no delegate address registered.", 502);
1101
1118
  }
@@ -1104,13 +1121,25 @@ var HavenClient = class {
1104
1121
  url: paymentRequired.resource.url,
1105
1122
  payTo: fundingTo,
1106
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",
1107
1130
  amount: x402AuthorizationAmount(option),
1108
1131
  asset: option.asset,
1109
1132
  network: option.network,
1110
1133
  description: paymentRequired.resource.description,
1111
1134
  idempotencyKey,
1112
1135
  // #1307: persisted so the settle leg can rehydrate it by payment_id.
1113
- ...options.mcpCallContext ? { mcpCallContext: options.mcpCallContext } : {}
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 } : {}
1114
1143
  });
1115
1144
  if (raw.status !== "pending_signature") {
1116
1145
  this.throwPaymentStateError("x402 payment", raw);
@@ -1242,6 +1271,17 @@ var HavenClient = class {
1242
1271
  * Get the agent identity tied to this API key.
1243
1272
  */
1244
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() {
1245
1285
  const raw = await this.get("/machine-payments/agent");
1246
1286
  return {
1247
1287
  id: raw.id,
@@ -1440,6 +1480,7 @@ var HavenClient = class {
1440
1480
  */
1441
1481
  async getPostPurchaseAllowanceSummary(paymentId) {
1442
1482
  const unavailable = (detail) => ({
1483
+ payment: null,
1443
1484
  allowance: null,
1444
1485
  warnings: [
1445
1486
  {
@@ -1448,27 +1489,38 @@ var HavenClient = class {
1448
1489
  }
1449
1490
  ]
1450
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
+ }
1451
1507
  try {
1452
- const [status, agent, allowanceSummary] = await Promise.all([
1453
- this.getPaymentStatus(paymentId),
1454
- this.getAgent(),
1455
- this.getAllowances()
1456
- ]);
1457
1508
  const tokenAddress = status.asset ?? status.x402?.asset ?? null;
1458
1509
  if (!tokenAddress) {
1459
- return unavailable("the settled payment does not carry a resolvable token address");
1510
+ return { ...unavailable("the settled payment does not carry a resolvable token address"), payment: status };
1460
1511
  }
1461
- const rail = agent.executionRail;
1512
+ const rail = agentResult.value.executionRail;
1462
1513
  const source = rail === "delegation" ? "active_delegations" : "allowance_module";
1463
- const match = allowanceSummary.allowances.find(
1514
+ const match = allowanceResult.value.allowances.find(
1464
1515
  (a) => a.tokenAddress.toLowerCase() === tokenAddress.toLowerCase()
1465
1516
  );
1466
1517
  if (!match) {
1467
- return unavailable("no allowance/budget row matches the settled token");
1518
+ return { ...unavailable("no allowance/budget row matches the settled token"), payment: status };
1468
1519
  }
1469
1520
  const token = resolveTokenFromAddress(match.tokenAddress);
1470
1521
  const remainingDisplay = token ? `${formatAtomicAmount(safeBigInt(match.onchain.remaining), token.decimals)} ${match.tokenSymbol}` : void 0;
1471
1522
  return {
1523
+ payment: status,
1472
1524
  allowance: {
1473
1525
  rail,
1474
1526
  remaining_atomic: match.onchain.remaining,
@@ -1516,6 +1568,7 @@ var HavenClient = class {
1516
1568
  async discoverTools(options = {}) {
1517
1569
  const params = new URLSearchParams();
1518
1570
  if (options.category) params.set("category", options.category);
1571
+ if (options.search !== void 0) params.set("search", options.search);
1519
1572
  if (options.rail) params.set("rail", options.rail);
1520
1573
  const query = params.size > 0 ? `?${params.toString()}` : "";
1521
1574
  const raw = await this.get(`/catalog${query}`);
@@ -1707,7 +1760,11 @@ var HavenClient = class {
1707
1760
  asset: option.asset,
1708
1761
  network: option.network,
1709
1762
  description: paymentRequired.resource.description,
1710
- 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"
1711
1768
  });
1712
1769
  if (raw.success && raw.tx_hash) {
1713
1770
  const receipt2 = this.mapX402ReceiptFromAuthorization(paymentRequired, option, paymentHeader, raw);
@@ -2910,6 +2967,11 @@ var HavenClient = class {
2910
2967
  amountAtomic: x402AuthorizationAmount(option),
2911
2968
  amount: decimalFromUsdcAtomic(x402AuthorizationAmount(option)),
2912
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,
2913
2975
  asset: option.asset,
2914
2976
  network: option.network,
2915
2977
  chainId: chainIdOrNull(option.network),
@@ -3524,8 +3586,8 @@ var toolDescriptions = {
3524
3586
  discoverTools: {
3525
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.",
3526
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.",
3527
- behavior: "Use each entry's suggested_tool field first \u2014 it names the exact next call. Read-only lookup against Haven's curated catalog; entries are periodically re-verified against the live merchant and degraded entries are flagged. Returns name, description, price, rail, resource URL, tool_name, tool_arguments, and suggested_tool. The catalog price (price_display/price_atomic, marked price_is_indicative) is a last-verified hint, NOT authoritative \u2014 the real price comes from the merchant's live 402 at pay time. Never creates a payment, signature, or approval.",
3528
- nextActionGuidance: "Pick an entry and pay it with the tool named in suggested_tool, passing the entry's resource_url, tool_name, and tool_arguments for MCP merchants. Confirm the price from the live pay-tool result (not the catalog), and pass max_amount when the user has a cap."
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).`
3529
3591
  },
3530
3592
  sweep_delegate: {
3531
3593
  summary: "Sweep stranded USDC and/or ETH from the delegate wallet back to the originating Safe.",
@@ -3837,10 +3899,13 @@ normal, not an error.
3837
3899
 
3838
3900
  1. \`mcp__haven__haven_discover_tools\` to find a payable service and its
3839
3901
  \`catalog_id\`.
3840
- 2. \`mcp__haven__haven_prepare_catalog_purchase\` with \`catalog_id\` and
3841
- \`max_amount\`. \`max_amount\` (atomic units) is REQUIRED on this tool, and
3842
- is best practice on every paid call below too \u2014 it caps what the LIVE
3843
- merchant quote may charge, checked before any funding intent is created.
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.)
3844
3909
  3. Then FOLLOW THE RESPONSE'S GUIDANCE FIELDS: \`next_action\`, \`next_tool\`,
3845
3910
  and \`next_arguments\` name the exact next call \u2014 act on those first; the
3846
3911
  prose in this section is fallback and debugging detail. If the catalog
@@ -3848,9 +3913,11 @@ normal, not an error.
3848
3913
  \`mcp__haven__haven_pay_mcp_tool\` (merchant URL, tool name, arguments) as
3849
3914
  the manual fallback.
3850
3915
 
3851
- **Signing:** \`mcp__haven-signer__haven_sign_x402\` with \`payment_id\` and
3852
- \`payment_required\` ONLY \u2014 the local signer fetches the exact signing bytes
3853
- itself, so never relay \`typed_data\` yourself. Fallback for an older signer
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
3854
3921
  or backend: re-run the quote/prepare tool with the SAME \`idempotency_key\`
3855
3922
  plus \`include_signing_payload=true\`, then pass \`payload_hash\`,
3856
3923
  \`x402_expected\` (the nested \`x402.expected\` object), and
@@ -3920,8 +3987,14 @@ present and surface \`message\` or \`error\` verbatim. Common cases:
3920
3987
  - \`pending_approval\`: queued for the user's approval (see above).
3921
3988
  - \`insufficient_funds\`: the Haven wallet doesn't hold enough of that token.
3922
3989
  Suggest the user add funds in the Haven dashboard.
3923
- - \`PRICE_EXCEEDS_MAX\`: the live merchant price exceeded your \`max_amount\`.
3924
- No funds moved; ask the user before retrying with a higher cap.
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\`.
3925
3998
  - \`PAYMENT_WINDOW_EXPIRED\`: re-run the quote/prepare tool with the same
3926
3999
  \`idempotency_key\`, then sign the fresh payload.
3927
4000
  - \`MERCHANT_REJECTED_AFTER_FUNDING\`: the merchant refused the paid retry.
@@ -3940,10 +4013,12 @@ present and surface \`message\` or \`error\` verbatim. Common cases:
3940
4013
  ## Reporting after a purchase
3941
4014
 
3942
4015
  A settled \`mcp__haven__haven_settle_mcp_tool\` response carries
3943
- \`agent_summary\` and the remaining post-purchase allowance in \`allowance\` \u2014
3944
- report the amount paid and what is left from those fields directly. Do not
3945
- call \`haven_get_agent\` or \`haven_get_allowances\` again just to report a
3946
- purchase you already made.
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.
3947
4022
 
3948
4023
  ## Revoke
3949
4024
 
@@ -3954,7 +4029,7 @@ for that credential.
3954
4029
  var SKILL_FOLDER_NAME = "haven-pay";
3955
4030
 
3956
4031
  // src/node-version.ts
3957
- var HAVEN_MINIMUM_NODE_VERSION = "24.0.0";
4032
+ var HAVEN_MINIMUM_NODE_VERSION = "22.0.0";
3958
4033
  function parseNodeVersion(value) {
3959
4034
  const match = value.trim().match(/^v?(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
3960
4035
  if (!match) return [0, 0, 0];
@@ -4203,6 +4278,7 @@ exports.parseMachinePaymentChallenge = parseMachinePaymentChallenge;
4203
4278
  exports.parseMachinePaymentChallengeResponse = parseMachinePaymentChallengeResponse;
4204
4279
  exports.parsePaymentRequired = parsePaymentRequired;
4205
4280
  exports.parsePaymentRequiredResponse = parsePaymentRequiredResponse;
4281
+ exports.resolveTokenFromAddress = resolveTokenFromAddress;
4206
4282
  exports.sameUrl = sameUrl;
4207
4283
  exports.selectPaymentOption = selectPaymentOption;
4208
4284
  exports.selectStandardPaymentOption = selectStandardPaymentOption;