@haven_ai/sdk 0.11.0-alpha.0 → 0.12.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.
Files changed (40) hide show
  1. package/README.md +37 -4
  2. package/dist/{base64-D4VZhgUg.d.ts → base64-C5OrJLij.d.ts} +10 -2
  3. package/dist/{base64-DR4Gl8cD.d.cts → base64-COaV1Sis.d.cts} +10 -2
  4. package/dist/{chunk-DXBAN4SN.cjs → chunk-4JTLJVBS.cjs} +4 -4
  5. package/dist/chunk-4JTLJVBS.cjs.map +1 -0
  6. package/dist/{chunk-ZDJ55LR7.js → chunk-7M3CIW7W.js} +7 -6
  7. package/dist/chunk-7M3CIW7W.js.map +1 -0
  8. package/dist/{chunk-I7SL4A6X.js → chunk-UPEDQLAL.js} +55 -5
  9. package/dist/chunk-UPEDQLAL.js.map +1 -0
  10. package/dist/{chunk-IKQIIMM6.cjs → chunk-V2ODMLDD.cjs} +38 -37
  11. package/dist/chunk-V2ODMLDD.cjs.map +1 -0
  12. package/dist/{chunk-34YJQFCM.js → chunk-WFTGZS24.js} +3 -3
  13. package/dist/chunk-WFTGZS24.js.map +1 -0
  14. package/dist/{chunk-MZKLAHBK.cjs → chunk-ZA3NOSO2.cjs} +56 -4
  15. package/dist/chunk-ZA3NOSO2.cjs.map +1 -0
  16. package/dist/edge.cjs +93 -93
  17. package/dist/edge.d.cts +11 -8
  18. package/dist/edge.d.ts +11 -8
  19. package/dist/edge.js +4 -4
  20. package/dist/index.cjs +441 -355
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.d.cts +22 -15
  23. package/dist/index.d.ts +22 -15
  24. package/dist/index.js +125 -47
  25. package/dist/index.js.map +1 -1
  26. package/dist/{receipt-_bWCELEH.d.cts → receipt-Dy77vCea.d.cts} +5 -4
  27. package/dist/{receipt-CbD8L0WL.d.ts → receipt-DyUInpvF.d.ts} +5 -4
  28. package/dist/test-support.cjs +21 -21
  29. package/dist/test-support.d.cts +2 -2
  30. package/dist/test-support.d.ts +2 -2
  31. package/dist/test-support.js +2 -2
  32. package/dist/{types-DKmvjXK6.d.cts → types-CAvsjRKi.d.cts} +144 -2
  33. package/dist/{types-DKmvjXK6.d.ts → types-CAvsjRKi.d.ts} +144 -2
  34. package/package.json +1 -1
  35. package/dist/chunk-34YJQFCM.js.map +0 -1
  36. package/dist/chunk-DXBAN4SN.cjs.map +0 -1
  37. package/dist/chunk-I7SL4A6X.js.map +0 -1
  38. package/dist/chunk-IKQIIMM6.cjs.map +0 -1
  39. package/dist/chunk-MZKLAHBK.cjs.map +0 -1
  40. package/dist/chunk-ZDJ55LR7.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
- import { decodeBase64Json, selectStandardPaymentOption, buildX402IdempotencyKey, x402AuthorizationAmount, assertBoundDirectPaymentUserOp, assertFundingLegPaysDelegate, HavenTypedDataRefusedError, assertOwnSettlementChild, parsePaymentRequiredResponse, x402PaymentHeaderNamesSent, SDK_CLIENT_IDENTITY, readClientUpdate, HAVEN_CLIENT_HEADER, isSweepableChain, sweepUsdcAddress, x402PaymentHeaderNamesFor, X402_PAYMENT_HEADER_NAMES, toStandardPaymentRequirements, deriveX402PaymentNonce, encodeBase64Json, x402V2PaymentEnvelope, isErc7710Option, buildSignedTaxDeclaration, encodeTaxDeclarationHeader, selectX402SettlementScheme, selectErc7710PaymentOption, x402AssetTransferMethod, resolveTokenFromAddress, normalizePaymentRequired, X_TAX_DECLARATION_HEADER } from './chunk-ZDJ55LR7.js';
2
- export { DEFAULT_NEXT_TOOL_BY_ACTION, ERC7710_ASSET_TRANSFER_METHOD, HAVEN_CLIENT_HEADER, HAVEN_MINIMUM_NODE_VERSION, HavenTypedDataRefusedError, MAX_TASK_BUDGET_TTL_SECONDS, NEXT_TOOL_SERVER_NAMES, NEXT_TOOL_SERVER_ROLES, SDK_CLIENT_IDENTITY, SDK_VERSION, SWEEP_BASE_CHAIN_ID, SWEEP_BASE_SEPOLIA_CHAIN_ID, SWEEP_BASE_SEPOLIA_USDC_ADDRESS, SWEEP_BASE_USDC_ADDRESS, TRANSFER_WITH_AUTHORIZATION_TYPES, TYPED_DATA_NOT_ALLOWED, X402PaymentHeaderValidationError, X402_LEGACY_PAYMENT_HEADER_NAME, X402_MAX_AUTHORIZATION_WINDOW_SECONDS, X402_PAYMENT_HEADER_NAME, X402_PAYMENT_HEADER_NAMES_SENT, X402_PAYMENT_NONCE_TAG, X402_PAYMENT_REQUIRED_HEADER_NAME, X402_PAYMENT_RESPONSE_HEADER_NAME, X402_SETTLEMENT_FORWARD_MARGIN_SECONDS, assertBoundDirectPaymentUserOp, assertOwnTaskBudgetCloseUserOp, assertOwnTaskChild, buildSweepAuthorizationMessage, buildSweepTypedData, buildX402ExpectedMessage, compareNodeVersions, createNextStepBuilder, decodeBase64Json, decodeBase64Utf8, defaultNextToolFor, deriveX402PaymentNonce, encodeBase64Json, encodeBase64Utf8, encodePaymentProof, hashDelegation, havenClientIdentity, isErc7710Option, isSupportedNodeVersion, isSweepableChain, isTaskChildTypedData, normalizePaymentRequired, parseNextTool, parsePaymentRequired, parsePaymentRequiredResponse, readClientUpdate, renderNextTool, resolveTokenBySymbol, resolveTokenFromAddress, selectErc7710PaymentOption, selectPaymentOption, selectStandardPaymentOption, selectX402SettlementScheme, sweepUsdcAddress, sweepUsdcDomain, toStandardPaymentRequirements, unsupportedNodeVersionMessage, validateStandardX402PaymentHeader, x402AssetTransferMethod, x402AuthorizationAmount, x402FacilitatorAddresses, x402PaymentHeaderNamesFor, x402RetryHeadersFor, x402V2PaymentEnvelope } from './chunk-ZDJ55LR7.js';
3
- import { verifyPaymentReceipt } from './chunk-34YJQFCM.js';
4
- export { RECEIPT_VERSION, verifyPaymentReceipt } from './chunk-34YJQFCM.js';
5
- import { HavenSigningError, HavenError, DEFAULT_CONFIRMATION_TIMEOUT_MS, HavenApiError, assertUserOpTypedDataBinding, AgentPaymentRail, AgentPaymentPhase, HavenTimeoutError, X402TaskBudgetMismatchError, X402UnexpectedStatusError, X402AlreadySettledError, MerchantTimeoutError, AgentPaymentWarningCode, AgentPaymentNextAction, HavenPaymentStateError, HavenZeroSettlementHashError, X402Erc7710AlreadySettledError, chainIdForNetwork } from './chunk-I7SL4A6X.js';
6
- export { AGENT_PAYMENT_FAILURE_CODE_VALUES, AGENT_PAYMENT_NEXT_ACTION_VALUES, AGENT_PAYMENT_PHASE_VALUES, AGENT_PAYMENT_RAIL_VALUES, AgentPaymentFailureCode, AgentPaymentFailureCodeDescriptions, AgentPaymentFailureCodeSchema, AgentPaymentNextAction, AgentPaymentNextActionDescriptions, AgentPaymentNextActionSchema, AgentPaymentPhase, AgentPaymentPhaseDescriptions, AgentPaymentPhaseSchema, AgentPaymentRail, AgentPaymentRailDescriptions, AgentPaymentRailSchema, AgentPaymentWarningCode, CONNECTOR_PACKAGE_NAME, DEFAULT_CONFIRMATION_TIMEOUT_MS, DIRECT_SIGN_CONTEXT_VERSION, ENTRY_POINT_V07, HAVEN_CONNECTOR_CHANNEL, HYBRID_DELEGATOR_DOMAIN_NAME, HYBRID_DELEGATOR_DOMAIN_VERSION, HavenApiError, HavenError, HavenPaymentStateError, HavenSigningError, HavenTimeoutError, HavenUnsupportedSignerVersionError, HavenUserOpBindingError, HavenZeroSettlementHashError, MerchantTimeoutError, PACKED_USER_OPERATION_FIELDS, ROOT_AUTHORITY, SIGNER_UPDATE_FALLBACK, SignerRefusalCode, TASK_SIGN_CONTEXT_VERSION, X402AlreadySettledError, X402Erc7710AlreadySettledError, X402TaskBudgetMismatchError, X402UnexpectedStatusError, assertUserOpTypedDataBinding, connectorRerunCommand, connectorSpec, connectorUpgradeCommand, deriveDelegateAccountAddress, isConnectorChannel, isPackedUserOperationTypedData, packedUserOperationHash, resolveConnectorChannel, signerUpdateFallback, verifySettlementChild } from './chunk-I7SL4A6X.js';
1
+ import { decodeBase64Json, selectStandardPaymentOption, buildX402IdempotencyKey, x402AuthorizationAmount, assertBoundDirectPaymentUserOp, assertFundingLegPaysDelegate, HavenTypedDataRefusedError, assertOwnSettlementChild, parsePaymentRequiredResponse, x402PaymentHeaderNamesSent, SDK_CLIENT_IDENTITY, readClientUpdate, HAVEN_CLIENT_HEADER, isSweepableChain, sweepUsdcAddress, x402PaymentHeaderNamesFor, X402_PAYMENT_HEADER_NAMES, toStandardPaymentRequirements, deriveX402PaymentNonce, encodeBase64Json, x402V2PaymentEnvelope, isErc7710Option, buildSignedTaxDeclaration, encodeTaxDeclarationHeader, selectX402SettlementScheme, selectErc7710PaymentOption, x402AssetTransferMethod, resolveTokenFromAddress, normalizePaymentRequired, X_TAX_DECLARATION_HEADER } from './chunk-7M3CIW7W.js';
2
+ export { DEFAULT_NEXT_TOOL_BY_ACTION, ERC7710_ASSET_TRANSFER_METHOD, HAVEN_CLIENT_HEADER, HAVEN_MINIMUM_NODE_VERSION, HavenTypedDataRefusedError, MAX_TASK_BUDGET_TTL_SECONDS, NEXT_TOOL_SERVER_NAMES, NEXT_TOOL_SERVER_ROLES, SDK_CLIENT_IDENTITY, SDK_VERSION, SWEEP_BASE_CHAIN_ID, SWEEP_BASE_SEPOLIA_CHAIN_ID, SWEEP_BASE_SEPOLIA_USDC_ADDRESS, SWEEP_BASE_USDC_ADDRESS, TRANSFER_WITH_AUTHORIZATION_TYPES, TYPED_DATA_NOT_ALLOWED, X402PaymentHeaderValidationError, X402_LEGACY_PAYMENT_HEADER_NAME, X402_MAX_AUTHORIZATION_WINDOW_SECONDS, X402_PAYMENT_HEADER_NAME, X402_PAYMENT_HEADER_NAMES_SENT, X402_PAYMENT_NONCE_TAG, X402_PAYMENT_REQUIRED_HEADER_NAME, X402_PAYMENT_RESPONSE_HEADER_NAME, X402_SETTLEMENT_FORWARD_MARGIN_SECONDS, assertBoundDirectPaymentUserOp, assertOwnTaskBudgetCloseUserOp, assertOwnTaskChild, buildSweepAuthorizationMessage, buildSweepTypedData, buildX402ExpectedMessage, compareNodeVersions, createNextStepBuilder, decodeBase64Json, decodeBase64Utf8, defaultNextToolFor, deriveX402PaymentNonce, encodeBase64Json, encodeBase64Utf8, encodePaymentProof, hashDelegation, havenClientIdentity, isErc7710Option, isSupportedNodeVersion, isSweepableChain, isTaskChildTypedData, normalizePaymentRequired, parseNextTool, parsePaymentRequired, parsePaymentRequiredResponse, readClientUpdate, renderNextTool, resolveTokenBySymbol, resolveTokenFromAddress, selectErc7710PaymentOption, selectPaymentOption, selectStandardPaymentOption, selectX402SettlementScheme, sweepUsdcAddress, sweepUsdcDomain, toStandardPaymentRequirements, unsupportedNodeVersionMessage, validateStandardX402PaymentHeader, x402AssetTransferMethod, x402AuthorizationAmount, x402FacilitatorAddresses, x402PaymentHeaderNamesFor, x402RetryHeadersFor, x402V2PaymentEnvelope } from './chunk-7M3CIW7W.js';
3
+ import { verifyPaymentReceipt } from './chunk-WFTGZS24.js';
4
+ export { RECEIPT_VERSION, verifyPaymentReceipt } from './chunk-WFTGZS24.js';
5
+ import { HavenSigningError, HavenError, DEFAULT_CONFIRMATION_TIMEOUT_MS, HavenApiError, assertUserOpTypedDataBinding, AgentPaymentRail, AgentPaymentPhase, HavenTimeoutError, X402TaskBudgetMismatchError, X402UnexpectedStatusError, X402AlreadySettledError, MerchantTimeoutError, AgentPaymentWarningCode, AgentPaymentNextAction, FundingConfirmationUnknownError, FundingRevertedError, HavenPaymentStateError, HavenZeroSettlementHashError, X402Erc7710AlreadySettledError, chainIdForNetwork } from './chunk-UPEDQLAL.js';
6
+ export { AGENT_PAYMENT_FAILURE_CODE_VALUES, AGENT_PAYMENT_NEXT_ACTION_VALUES, AGENT_PAYMENT_PHASE_VALUES, AGENT_PAYMENT_RAIL_VALUES, AgentPaymentFailureCode, AgentPaymentFailureCodeDescriptions, AgentPaymentFailureCodeSchema, AgentPaymentNextAction, AgentPaymentNextActionDescriptions, AgentPaymentNextActionSchema, AgentPaymentPhase, AgentPaymentPhaseDescriptions, AgentPaymentPhaseSchema, AgentPaymentRail, AgentPaymentRailDescriptions, AgentPaymentRailSchema, AgentPaymentWarningCode, CONNECTOR_PACKAGE_NAME, DEFAULT_CONFIRMATION_TIMEOUT_MS, DIRECT_SIGN_CONTEXT_VERSION, ENTRY_POINT_V07, FundingConfirmationUnknownError, FundingRevertedError, HAVEN_CONNECTOR_CHANNEL, HYBRID_DELEGATOR_DOMAIN_NAME, HYBRID_DELEGATOR_DOMAIN_VERSION, HavenApiError, HavenError, HavenPaymentStateError, HavenSigningError, HavenTimeoutError, HavenUnsupportedSignerVersionError, HavenUserOpBindingError, HavenZeroSettlementHashError, MerchantTimeoutError, PACKED_USER_OPERATION_FIELDS, ROOT_AUTHORITY, SIGNER_UPDATE_FALLBACK, SignerRefusalCode, TASK_SIGN_CONTEXT_VERSION, X402AlreadySettledError, X402Erc7710AlreadySettledError, X402TaskBudgetMismatchError, X402UnexpectedStatusError, assertUserOpTypedDataBinding, connectorRerunCommand, connectorSpec, connectorUpgradeCommand, deriveDelegateAccountAddress, isConnectorChannel, isPackedUserOperationTypedData, packedUserOperationHash, resolveConnectorChannel, signerUpdateFallback, verifySettlementChild } from './chunk-UPEDQLAL.js';
7
7
  import { hashTypedData } from 'viem';
8
8
  import { ethers } from 'ethers';
9
9
  import { AsyncLocalStorage } from 'async_hooks';
@@ -143,12 +143,16 @@ var HavenApiTransport = class {
143
143
  return data;
144
144
  } catch (err) {
145
145
  if (err instanceof HavenApiError) throw err;
146
+ const failureCause = { kind: "no_answer", method };
146
147
  if (err instanceof Error && err.name === "AbortError") {
147
- throw new HavenApiError(`Request to ${path} timed out`, 408);
148
+ throw new HavenApiError(`Request to ${path} timed out`, 408, void 0, void 0, failureCause);
148
149
  }
149
150
  throw new HavenApiError(
150
151
  `Request to ${path} failed: ${err instanceof Error ? err.message : String(err)}`,
151
- 0
152
+ 0,
153
+ void 0,
154
+ void 0,
155
+ failureCause
152
156
  );
153
157
  } finally {
154
158
  clearTimeout(timeout);
@@ -354,6 +358,24 @@ function mapPaymentReceipt(raw) {
354
358
  if ("approval_request_id" in raw) {
355
359
  receipt.approvalRequestId = raw.approval_request_id ?? null;
356
360
  }
361
+ if ("settlement_scheme" in raw) {
362
+ receipt.settlementScheme = raw.settlement_scheme ?? null;
363
+ }
364
+ if ("budget_delegation_hash" in raw) {
365
+ receipt.budgetDelegationHash = raw.budget_delegation_hash ?? null;
366
+ }
367
+ if ("delivery_quality" in raw) {
368
+ receipt.deliveryQuality = raw.delivery_quality ?? null;
369
+ }
370
+ if ("delivery_note" in raw) {
371
+ receipt.deliveryNote = raw.delivery_note ?? null;
372
+ }
373
+ if ("delivery_reported_at" in raw) {
374
+ receipt.deliveryReportedAt = raw.delivery_reported_at ?? null;
375
+ }
376
+ if ("delivery_reference" in raw) {
377
+ receipt.deliveryReference = raw.delivery_reference ?? null;
378
+ }
357
379
  return receipt;
358
380
  }
359
381
 
@@ -473,7 +495,7 @@ function throwPaymentStateError(label, raw) {
473
495
  throw new HavenPaymentStateError(state.message, statusCode, state, raw);
474
496
  }
475
497
  if (raw.status === "pending_approval") {
476
- throw new HavenApiError(
498
+ throw HavenApiError.deterministic(
477
499
  `${label} exceeds the agent's on-chain budget and was declined; no approval is pending and none will arrive (payment_id: ${raw.payment_id}).`,
478
500
  statusCode,
479
501
  raw
@@ -488,6 +510,7 @@ function throwPaymentStateError(label, raw) {
488
510
  }
489
511
  const paymentId = raw.payment_id ? ` (payment_id: ${raw.payment_id})` : "";
490
512
  const message = raw.error ?? `${label} ${raw.status}${paymentId}`;
513
+ if (statusCode >= 500) throw HavenApiError.deterministic(message, statusCode, raw);
491
514
  throw new HavenApiError(message, statusCode, raw);
492
515
  }
493
516
 
@@ -1346,6 +1369,17 @@ function deriveReadiness(status, allowances) {
1346
1369
  if (status !== "active") return "revoked";
1347
1370
  return allowances.some((allowance) => safeBigInt(allowance.remainingAtomic) > 0n) ? "ready" : "needs_approval";
1348
1371
  }
1372
+ function deriveBudgetRefillsAt(readiness, allowances) {
1373
+ if (readiness !== "needs_approval" || allowances.length === 0) return null;
1374
+ const times = allowances.map((allowance) => allowance.nextRefillAt).filter((at) => at !== null && !Number.isNaN(Date.parse(at)));
1375
+ if (times.length === 0) return null;
1376
+ return times.reduce((earliest, at) => Date.parse(at) < Date.parse(earliest) ? at : earliest);
1377
+ }
1378
+ function deriveBudgetEndedAt(readiness, allowances, budgetEndedAt) {
1379
+ if (readiness !== "needs_approval" || allowances.length > 0) return null;
1380
+ if (!budgetEndedAt || Number.isNaN(Date.parse(budgetEndedAt))) return null;
1381
+ return budgetEndedAt;
1382
+ }
1349
1383
  function formatRemainingDisplay(tokenAddress, tokenSymbol, remainingAtomic) {
1350
1384
  const token = resolveTokenFromAddress(tokenAddress);
1351
1385
  return token ? `${formatAtomicAmount(safeBigInt(remainingAtomic), token.decimals)} ${tokenSymbol}` : `${remainingAtomic} ${tokenSymbol} (atomic; unknown decimals)`;
@@ -1514,11 +1548,23 @@ var AccountReads = class {
1514
1548
  isResetPending: allowance.onchain.isResetPending,
1515
1549
  // #3731: same provenance as the detailed read's field — the compact
1516
1550
  // row is a projection of the same allowance, never a second source.
1517
- fundsCoverRemaining: allowance.fundsCoverRemaining
1551
+ fundsCoverRemaining: allowance.fundsCoverRemaining,
1552
+ nextRefillAt: allowance.nextRefillAt
1518
1553
  };
1519
1554
  });
1520
1555
  const readiness = deriveReadiness(agent.status, allowances);
1521
- return { ...agent, readiness, spend_authority_readiness: readiness, allowances, taskBudgets, pendingSubBudgetSignatures };
1556
+ const budgetRefillsAt = deriveBudgetRefillsAt(readiness, allowances);
1557
+ const budgetEndedAt = deriveBudgetEndedAt(readiness, allowances, allowanceSummary.budgetEndedAt);
1558
+ return {
1559
+ ...agent,
1560
+ readiness,
1561
+ spend_authority_readiness: readiness,
1562
+ budgetRefillsAt,
1563
+ budgetEndedAt,
1564
+ allowances,
1565
+ taskBudgets,
1566
+ pendingSubBudgetSignatures
1567
+ };
1522
1568
  }
1523
1569
  /**
1524
1570
  * #3329: `GET /task-budgets?status=open`, mapped to the condensed summary
@@ -1614,6 +1660,9 @@ var AccountReads = class {
1614
1660
  accountAddress: raw.account_address,
1615
1661
  delegateAddress: raw.delegate_address,
1616
1662
  chainId: raw.chain_id,
1663
+ // #4352: a non-empty parseable date → itself; anything else (absent on
1664
+ // an older backend, null when no budget has ended) → null.
1665
+ budgetEndedAt: typeof raw.budget_ended_at === "string" && !Number.isNaN(Date.parse(raw.budget_ended_at)) ? raw.budget_ended_at : null,
1617
1666
  supersededBy: mapSupersession(raw.superseded_by),
1618
1667
  allowances: raw.allowances.map((allowance) => ({
1619
1668
  id: allowance.id,
@@ -1632,6 +1681,9 @@ var AccountReads = class {
1632
1681
  // "unverifiable", not a missing key. The key is absent on the wire
1633
1682
  // when the remaining is 0; that maps to null here too.
1634
1683
  fundsCoverRemaining: allowance.funds_cover_remaining === true || allowance.funds_cover_remaining === false ? allowance.funds_cover_remaining : null,
1684
+ // #4355: a non-empty string → itself; anything else (absent on an
1685
+ // older backend, null when the budget never refills) → null.
1686
+ nextRefillAt: typeof allowance.next_refill_at === "string" && allowance.next_refill_at !== "" ? allowance.next_refill_at : null,
1635
1687
  remainingDisplay: formatRemainingDisplay(allowance.token_address, allowance.token_symbol, allowance.onchain.remaining),
1636
1688
  onchain: {
1637
1689
  amount: allowance.onchain.amount,
@@ -2257,7 +2309,7 @@ var X402FundingLeg = class {
2257
2309
  }
2258
2310
  this.assertSignableAuthorizationState("x402 payment", raw);
2259
2311
  if (!raw.sign_data?.hash) {
2260
- throw new HavenApiError("No sign_hash returned from x402/authorize", 500, raw);
2312
+ throw HavenApiError.deterministic("No sign_hash returned from x402/authorize", 500, raw);
2261
2313
  }
2262
2314
  const sig = await this.signForData(raw.sign_data, {
2263
2315
  asset: option.asset,
@@ -2379,20 +2431,27 @@ var X402FundingLeg = class {
2379
2431
  * Skipped when `chainRpcs` does not include the chain; in that case Haven's
2380
2432
  * backend has already confirmed on-chain submission and callers accept the
2381
2433
  * small propagation window as a trade-off for not configuring an RPC URL.
2434
+ *
2435
+ * #4470: the two outcomes are typed apart. ethers 6 `waitForTransaction`
2436
+ * REJECTS on its timeout (and on an RPC failure) rather than resolving
2437
+ * null, so that path is `FundingConfirmationUnknownError` — funding may
2438
+ * have moved. A mined receipt with `status !== 1` is `FundingRevertedError`.
2382
2439
  */
2383
2440
  async waitForFundingTx(txHash, chainId, timeoutMs = 3e4) {
2384
2441
  if (!txHash || !chainId) return;
2385
2442
  const rpcUrl = this.chainRpcs[chainId];
2386
2443
  if (!rpcUrl) return;
2387
2444
  const provider = createJsonRpcProvider(rpcUrl);
2388
- const onChainReceipt = await provider.waitForTransaction(txHash, 1, timeoutMs);
2389
- if (!onChainReceipt || onChainReceipt.status !== 1) {
2390
- throw new HavenApiError(
2391
- "Funding tx did not confirm on-chain within the timeout window.",
2392
- 500,
2393
- { txHash, chainId }
2394
- );
2445
+ let onChainReceipt;
2446
+ try {
2447
+ onChainReceipt = await provider.waitForTransaction(txHash, 1, timeoutMs);
2448
+ } catch (err) {
2449
+ throw new FundingConfirmationUnknownError(txHash, chainId, err instanceof Error ? err.message : String(err));
2450
+ }
2451
+ if (!onChainReceipt) {
2452
+ throw new FundingConfirmationUnknownError(txHash, chainId, "no receipt within the timeout window");
2395
2453
  }
2454
+ if (onChainReceipt.status !== 1) throw new FundingRevertedError(txHash, chainId);
2396
2455
  }
2397
2456
  /**
2398
2457
  * Can the delegate EOA still fund an authorization for `amountAtomic`?
@@ -2574,7 +2633,7 @@ var X402Erc7710 = class {
2574
2633
  ...options.subBudgetId ? { subBudgetId: options.subBudgetId } : {}
2575
2634
  });
2576
2635
  if (!raw.payment_id) {
2577
- throw new HavenApiError("No payment_id returned from x402/authorize", 500, raw);
2636
+ throw HavenApiError.deterministic("No payment_id returned from x402/authorize", 500, raw);
2578
2637
  }
2579
2638
  if (raw.status === "confirmed" && raw.tx_hash) {
2580
2639
  const requestedUrl = options.resourceUrl ?? paymentRequired.resource?.url;
@@ -2591,7 +2650,7 @@ var X402Erc7710 = class {
2591
2650
  }
2592
2651
  const signData = raw.sign_data;
2593
2652
  if (signData?.signature_scheme !== "eip712_delegation" || !signData.typed_data) {
2594
- throw new HavenApiError(
2653
+ throw HavenApiError.deterministic(
2595
2654
  `x402/authorize did not return an erc7710 settlement child (signature_scheme was ${JSON.stringify(signData?.signature_scheme)}). Refusing to sign a payload this path did not ask for.`,
2596
2655
  500,
2597
2656
  raw
@@ -2634,7 +2693,7 @@ var X402Erc7710 = class {
2634
2693
  throw err;
2635
2694
  }
2636
2695
  if (!settled.payment_header) {
2637
- throw new HavenApiError(
2696
+ throw HavenApiError.deterministic(
2638
2697
  "x402 settle returned no payment_header \u2014 the merchant cannot be retried.",
2639
2698
  500,
2640
2699
  settled
@@ -2805,10 +2864,10 @@ var MerchantCompletion = class {
2805
2864
  }
2806
2865
  async retryRequest(url, initialInit, paymentRequired, receipt) {
2807
2866
  if (!receipt.accepted) {
2808
- throw new HavenApiError("No accepted x402 option was recorded for payment retry", 500);
2867
+ throw HavenApiError.deterministic("No accepted x402 option was recorded for payment retry", 500);
2809
2868
  }
2810
2869
  if (!receipt.paymentHeader) {
2811
- throw new HavenApiError("No x402 payment header was returned for payment retry", 500);
2870
+ throw HavenApiError.deterministic("No x402 payment header was returned for payment retry", 500);
2812
2871
  }
2813
2872
  let retryInit = initialInit;
2814
2873
  if (this.getTaxDeclarationHeader && this.delegateKey) {
@@ -3194,15 +3253,16 @@ var MerchantCompletion = class {
3194
3253
  };
3195
3254
  for (let attempt = 0; ; attempt += 1) {
3196
3255
  const result = await this.postEvidenceOnce(body);
3197
- if (result.outcome !== "retryable" || attempt >= EVIDENCE_RETRY_DELAYS_MS.length) {
3256
+ if (result.outcome !== "retryable" || result.statusCode !== EVIDENCE_RETRYABLE_STATUS || attempt >= EVIDENCE_RETRY_DELAYS_MS.length) {
3198
3257
  return result;
3199
3258
  }
3200
3259
  await this.sleep(EVIDENCE_RETRY_DELAYS_MS[attempt]);
3201
3260
  }
3202
3261
  }
3203
3262
  /**
3204
- * ONE evidence attempt — the body of `reportEvidence`'s loop above. A 503
3205
- * answers `retryable` immediately; the caller decides whether to sleep and
3263
+ * ONE evidence attempt — the body of `reportEvidence`'s loop above. A 503,
3264
+ * or no verdict at all (a non-answer or a 408, #4470), answers `retryable`
3265
+ * immediately; the caller decides whether to sleep and
3206
3266
  * re-attempt (`reportEvidence`, and — #4215 — the local settlement report
3207
3267
  * in `retryRequest`) or hand the outcome to its consumer (#3764 D2, the
3208
3268
  * hosted settlement report).
@@ -3213,7 +3273,8 @@ var MerchantCompletion = class {
3213
3273
  return { outcome: "confirmed" };
3214
3274
  } catch (err) {
3215
3275
  const statusCode = err instanceof HavenApiError ? err.statusCode : void 0;
3216
- if (statusCode !== EVIDENCE_RETRYABLE_STATUS) {
3276
+ const noVerdict = statusCode === 408 || err instanceof HavenApiError && err.failureCause?.kind === "no_answer";
3277
+ if (statusCode !== EVIDENCE_RETRYABLE_STATUS && !noVerdict) {
3217
3278
  const reason = refusalReasonFromBody(err);
3218
3279
  const errorCode = refusalErrorCodeFromBody(err);
3219
3280
  return {
@@ -3634,7 +3695,7 @@ var HavenClient = class {
3634
3695
  }
3635
3696
  const fundingTo = options.delegateAddress ?? (await this.getAgent()).delegateAddress;
3636
3697
  if (!fundingTo) {
3637
- throw new HavenApiError("Authenticated agent has no delegate address registered.", 502);
3698
+ throw HavenApiError.deterministic("Authenticated agent has no delegate address registered.", 502);
3638
3699
  }
3639
3700
  const idempotencyKey = options.idempotencyKey ?? buildX402IdempotencyKey(paymentRequired, option);
3640
3701
  const raw = await this.post("/x402", {
@@ -3672,7 +3733,7 @@ var HavenClient = class {
3672
3733
  throwPaymentStateError("x402 payment", raw);
3673
3734
  }
3674
3735
  if (!raw.sign_data?.hash) {
3675
- throw new HavenApiError("No sign_hash returned from x402/authorize", 500, raw);
3736
+ throw HavenApiError.deterministic("No sign_hash returned from x402/authorize", 500, raw);
3676
3737
  }
3677
3738
  if (raw.sign_data.signature_scheme !== void 0 && !raw.sign_data.typed_data) {
3678
3739
  throw new HavenSigningError(
@@ -3680,7 +3741,7 @@ var HavenClient = class {
3680
3741
  );
3681
3742
  }
3682
3743
  if (!raw.x402_expected_auth) {
3683
- throw new HavenApiError("No x402 expected-context binding returned from x402/authorize", 500, raw);
3744
+ throw HavenApiError.deterministic("No x402 expected-context binding returned from x402/authorize", 500, raw);
3684
3745
  }
3685
3746
  return {
3686
3747
  paymentId: raw.payment_id,
@@ -3910,6 +3971,12 @@ var HavenClient = class {
3910
3971
  * degrade-to-warning path and the ledger row would still land while the
3911
3972
  * purchase proceeded.
3912
3973
  *
3974
+ * #4458: an agent whose only budget for the token has ENDED is refused the
3975
+ * same way with `error_code: 'delegation_expired'` and `budget_ended_at`
3976
+ * (ISO 8601) — also thrown, also recorded (`source: 'hosted_prepare'`).
3977
+ * The remedy differs: the owner sets a NEW budget, waiting for a period to
3978
+ * reset cannot help.
3979
+ *
3913
3980
  * #3518: without `merchantTo`, an agent whose budgets for the token are all
3914
3981
  * merchant-locked gets 409 `budget_requires_recipient` (thrown, nothing
3915
3982
  * recorded) naming the pins in `budget_recipient_addresses` — repeat the
@@ -3934,12 +4001,13 @@ var HavenClient = class {
3934
4001
  /**
3935
4002
  * Post-purchase allowance/budget summary for a settled payment (#1310).
3936
4003
  *
3937
- * Reuses the EXACT rail-aware read path {@link getAllowances} / #1306's
3938
- * catalog-purchase preflight `allowance` block use — `GET
3939
- * /machine-payments/allowances`, with delegation-rail values coming from
3940
- * the #1090 `deriveDelegationBudgets`-backed enforcer read, never
4004
+ * Reuses the EXACT rail-aware read path {@link getAllowances} uses —
4005
+ * `GET /machine-payments/allowances`, with delegation-rail values coming
4006
+ * from the #1090 `deriveDelegationBudgets`-backed enforcer read, never
3941
4007
  * `agent_allowances` — so this can never disagree with
3942
- * {@link getAllowances} for the same fixture. The settled token is
4008
+ * {@link getAllowances} for the same fixture. (#1306's catalog-purchase
4009
+ * preflight read the same path until #3054; it now calls
4010
+ * `POST /machine-payments/budget-precheck` instead.) The settled token is
3943
4011
  * resolved from {@link getPaymentStatus} so callers pass only
3944
4012
  * `paymentId`, never a second haven_get_agent-style round trip.
3945
4013
  *
@@ -4964,7 +5032,7 @@ var toolDescriptions = {
4964
5032
  getAgent: {
4965
5033
  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.",
4966
5034
  selectionGuidance: "Use this as the session bootstrap, or to confirm identity together with whether the agent has spend authority right now. For per-token detail (configured vs spent vs reset window) use haven_get_allowances.",
4967
- 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. Readiness is AUTHORITY: it says a budget is live, not that the account holds funds. Each allowances[] row carries fundsCoverRemaining: false when the account's balance cannot back that row's whole remaining budget \u2014 a heads-up to mention to the user, not a refusal (budgets above the balance are a normal setup; an owner may top up weekly) \u2014 and null when the coverage read failed or the remaining figure was not read live; the key is absent when the remaining is 0, and rows for one token are compared alone. 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. taskBudgets[] rows carry maxAtomic/maxDisplay plus spentAtomic/remainingAtomic/remainingDisplay \u2014 what the chain still allows that budget \u2014 and remainingIsFromChain: false marks a failed read (figures null, never the full cap); only live rows are listed (closed and expired omitted), each with its status (pending | open | closing) and isExpired, so a closing budget stays visible. pendingSubBudgetSignatures[] lists sub-budget rows awaiting YOUR signature, each with its haven_sign next step. Identity fields: id, name, status, accountAddress, delegateAddress, chainId.`,
5035
+ 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. needs_approval with no allowances[] rows and budgetEndedAt set: the budget ENDED on that date \u2014 ask the owner to set a new one, waiting will not help; with no rows and no budgetEndedAt: nothing approved yet; with rows: spent for this period, refilling at budgetRefillsAt (null: it ends first). Readiness is AUTHORITY \u2014 a budget is live, not that funds are held. Each allowances[] row carries fundsCoverRemaining: false when the account's balance cannot back that row's whole remaining budget \u2014 a heads-up to mention, not a refusal (budgets above the balance are normal; an owner may top up weekly) \u2014 and null when the coverage read failed or the remaining was not read live; the key is absent at remaining 0, and rows for one token are compared alone. 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 it 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. allowances[] carries id, tokenAddress, remainingAtomic, remainingDisplay, nextRefillAt per token. taskBudgets[] rows carry maxAtomic/maxDisplay plus spentAtomic/remainingAtomic/remainingDisplay \u2014 what the chain still allows that budget \u2014 and remainingIsFromChain: false marks a failed read (figures null, never the cap); only live rows are listed, each with its status (pending | open | closing) and isExpired. pendingSubBudgetSignatures[] lists sub-budget rows awaiting YOUR signature, with the haven_sign step. Identity fields: id, name, status, accountAddress, delegateAddress, chainId.`,
4968
5036
  nextActionGuidance: ""
4969
5037
  },
4970
5038
  getAllowances: {
@@ -5286,7 +5354,7 @@ You are an AI agent whose user has no Haven account yet, or has one but has not
5286
5354
 
5287
5355
  ## What Haven is, in three sentences
5288
5356
 
5289
- Your user gives you a **budget on their own account** \u2014 not their wallet, and not a key to their funds. The budget is a delegation they sign, enforced on-chain: a payment over it, to the wrong recipient, or past its expiry is refused at execution time, not by a dashboard promise. Haven constructs and relays the payments; you get an agent credential and a signing key made on your machine, and your user can revoke the budget without you and without Haven.
5357
+ Your user gives you a **budget on their own account** \u2014 not their wallet, and not a key to their funds. The budget is a delegation they sign, enforced on-chain: a payment over it, to the wrong recipient, or past its expiry is refused at execution time, not by a dashboard promise. The budget also ENDS on a date your user chooses \u2014 30, 90 (default), 180 or 365 days out \u2014 and that day is enforced on-chain too. When the budget ends, your authority to pay ends with it; ask your user to renew it (a new approval on the agent's page, or a new \`budget grant\`). Haven constructs and relays the payments; you get an agent credential and a signing key made on your machine, and your user can revoke the budget without you and without Haven.
5290
5358
 
5291
5359
  ## The sequence
5292
5360
 
@@ -5359,7 +5427,7 @@ If the approval wait times out, nothing is lost \u2014 your agent is registered
5359
5427
  Call \`haven_get_agent\`, one of the Haven MCP tools the connector wires into your runtime in step 4. It returns identity plus \`spend_authority_readiness\`:
5360
5428
 
5361
5429
  - \`ready\` \u2014 a budget is live: you have the authority to pay. It does not say the account holds funds \u2014 the \`allowances[]\` rows carry \`funds_cover_remaining\`, and \`false\` there is a heads-up to mention to your user, not a refusal.
5362
- - \`needs_approval\` \u2014 the connector finished, nobody approved yet. Ask your user again, in their Haven tab; there is no queue to wait in.
5430
+ - \`needs_approval\` \u2014 no budget can pay. \`allowances[]\` empty with \`budgetEndedAt\` set: your budget ENDED on that date; waiting will not help \u2014 ask your user to set a new one in their Haven tab. \`allowances[]\` empty with \`budgetEndedAt\` null: nobody approved one yet; ask your user again \u2014 there is no queue. Rows, all spent: it refills at \`budgetRefillsAt\`; tell your user, then wait or ask them to raise it (\`null\`: it ends first; ask them to renew it).
5363
5431
  - \`revoked\` \u2014 the credential is not active; ask your user to create a new agent.
5364
5432
 
5365
5433
  \`ready\` covers hosted identity and the budget only, not your local signer. Check that with \`npx -y @haven_ai/connect@<channel> --doctor\`, the same tag your prompt named \u2014 a separate command, so the command-modification rule does not bind it.
@@ -5395,7 +5463,7 @@ Then:
5395
5463
  1. Run \`<packages.cli.one_liner> login\`. That command is the manifest's \`packages.cli.one_liner\` (\`/.well-known/haven.json\`) \u2014 run it as given: \`packages.cli.channel\` serves the full spec. It prints a link and a code and does not need my password \u2014 it must never ask for it. Give me the link straight away and wait for me to approve it in my browser.
5396
5464
  2. Once I have approved, run \`haven agents connect --name <a name you choose> --budget <amount> --token USDC --period <minutes>\` with the budget I tell you. If I have not given you one, ask me before running it. Add \`--run\` to complete the connection in the same step.
5397
5465
  3. ${AGENT_APPROVAL_RELAY_JSON_SENTENCE}
5398
- 4. Once I have approved the budget, verify with the \`haven_get_agent\` tool: \`ready\` means I have given you the authority to pay \u2014 it does not confirm the account holds funds, so if its allowances rows carry \`funds_cover_remaining: false\`, tell me and still try the payment; \`needs_approval\` means my approval has not landed yet.
5466
+ 4. Once I have approved the budget, verify with the \`haven_get_agent\` tool: \`ready\` means I have given you the authority to pay \u2014 it does not confirm the account holds funds, so if its allowances rows carry \`funds_cover_remaining: false\`, tell me and still try the payment; \`needs_approval\` with no allowances rows and \`budgetEndedAt\` set means my budget ended on that date \u2014 tell me, and I will set a new one; with no rows and no \`budgetEndedAt\`, my approval has not landed yet; with rows, the budget is spent \u2014 it refills at \`budgetRefillsAt\` (\`null\`: it ends first), so tell me, and I can raise or renew it.
5399
5467
 
5400
5468
  Two things only I can do: approving that budget with my passkey, and funding the account with USDC \u2014 no ETH, Haven sponsors the gas. Tell me if either is missing rather than working around it.
5401
5469
 
@@ -5515,8 +5583,10 @@ Do not guess the wallet address, network, or budget.
5515
5583
 
5516
5584
  For instant orientation at the start of a session, read the non-secret
5517
5585
  \`agent.json\` the connector wrote to your Haven credential directory (typically
5518
- \`~/.haven/agents/<agent-id>/agent.json\` \u2014 if you don't know the agent id, list
5519
- \`~/.haven/agents/\` to find the folder). It
5586
+ \`~/.haven/agents/<slug>/agent.json\`, where \`<slug>\` is the wiring name of the
5587
+ \`haven-<slug>\` pair \u2014 only a \`--bare\` run, or a \`--replace\` without \`--name\`,
5588
+ names the directory by the agent id instead; if you don't know the folder, list
5589
+ \`~/.haven/agents/\` to find it). It
5520
5590
  holds your agent id, Haven wallet address, network, and *configured* per-token
5521
5591
  budget, and contains no keys \u2014 the fastest way to answer "who am I and what may
5522
5592
  I spend" with no round trip. If that file is absent (some setups don't write
@@ -5537,7 +5607,13 @@ spending:
5537
5607
  the user, not a refusal \u2014 a budget above the balance is a normal setup, so
5538
5608
  still try the payment; \`null\` means the coverage read failed or the remaining
5539
5609
  figure was not read live; the key is absent when the remaining is 0. Rows for
5540
- one token are compared alone.
5610
+ one token are compared alone. \`needs_approval\` with no \`allowances[]\`
5611
+ rows and \`budgetEndedAt\` set means the budget ENDED on that date \u2014 waiting
5612
+ will not help, ask the owner to set a new one; with no rows and no
5613
+ \`budgetEndedAt\`, nobody approved a budget yet; with rows (all at remaining 0) the
5614
+ budget is spent for this period, and \`budgetRefillsAt\` says when it refills
5615
+ (\`null\`: it ends before it refills) \u2014 tell the user, then wait or ask them
5616
+ to raise it.
5541
5617
  - \`mcp__haven__haven_get_allowances\` \u2014 detailed per-token breakdown
5542
5618
  (configured, spent, reset window) when you need more than the summary.
5543
5619
  - \`mcp__haven__haven_check_funds\` \u2014 whether the account actually HOLDS at
@@ -5751,8 +5827,10 @@ for example, an invoice carrying an \`x402_payment_url\` to POST with its
5751
5827
  report it, the merchant's settlement evidence. (A deposit transfer is bound
5752
5828
  to its payee and amount on-chain too; the difference is the record, not
5753
5829
  the binding.)
5754
- - **If the quote says stop, stop.** The quote checks the agent's remaining
5755
- budget at quote time, and it can check a price you state (\`expected_amount\`).
5830
+ - **If the quote says stop, stop.** The hosted quote checks the agent's
5831
+ remaining budget at quote time, and it can check a price you state
5832
+ (\`expected_amount\`). On the local runtime (\`@haven_ai/mcp\`) the quote
5833
+ checks only a price you state; the budget is checked when you pay.
5756
5834
  When it returns \`safe_to_continue: false\` \u2014 the amount exceeds the budget, or
5757
5835
  the price does not match what you expected \u2014 it prefills no cap and names no
5758
5836
  next tool: stop and tell the user. If the x402 challenge itself is wrong (a
@@ -5897,8 +5975,8 @@ only part of it was usable), and an optional \`note\` (max 2000 characters)
5897
5975
  saying what was wrong. It is evidence only: it moves no money, never changes
5898
5976
  \`settled\` or any money field, and works only on your own settled
5899
5977
  payments \u2014 another agent's payment is refused. A re-report replaces your
5900
- earlier verdict; the receipt then carries it beside the payment, so the
5901
- owner does not read a junk delivery as a success.
5978
+ earlier verdict; the receipt then carries it beside the payment, so a junk
5979
+ delivery does not read as a success.
5902
5980
  ### Relay a delivered code or credential to the owner \u2014 immediately, verbatim
5903
5981
 
5904
5982
  If a merchant response carries what the user PAID FOR \u2014 a redemption code, gift