@haven_ai/sdk 0.10.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 +45 -9
  2. package/dist/{base64-CiR5BJhk.d.cts → base64-C5OrJLij.d.ts} +10 -2
  3. package/dist/{base64-CxkinHPQ.d.ts → base64-COaV1Sis.d.cts} +10 -2
  4. package/dist/{chunk-6LEXYQ7J.cjs → chunk-4JTLJVBS.cjs} +4 -4
  5. package/dist/chunk-4JTLJVBS.cjs.map +1 -0
  6. package/dist/{chunk-MLUWABVT.js → chunk-7M3CIW7W.js} +7 -6
  7. package/dist/chunk-7M3CIW7W.js.map +1 -0
  8. package/dist/{chunk-TD2PHUYP.js → chunk-UPEDQLAL.js} +72 -6
  9. package/dist/chunk-UPEDQLAL.js.map +1 -0
  10. package/dist/{chunk-22FJ4JQO.cjs → chunk-V2ODMLDD.cjs} +38 -37
  11. package/dist/chunk-V2ODMLDD.cjs.map +1 -0
  12. package/dist/{chunk-EWTHDEIW.js → chunk-WFTGZS24.js} +3 -3
  13. package/dist/chunk-WFTGZS24.js.map +1 -0
  14. package/dist/{chunk-H37YVYZA.cjs → chunk-ZA3NOSO2.cjs} +73 -5
  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 +866 -475
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.d.cts +69 -33
  23. package/dist/index.d.ts +69 -33
  24. package/dist/index.js +554 -171
  25. package/dist/index.js.map +1 -1
  26. package/dist/{receipt-q7ez8Zb2.d.cts → receipt-Dy77vCea.d.cts} +5 -4
  27. package/dist/{receipt-Cg9a5Seq.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-BKgqzvnk.d.ts → types-CAvsjRKi.d.cts} +209 -12
  33. package/dist/{types-BKgqzvnk.d.cts → types-CAvsjRKi.d.ts} +209 -12
  34. package/package.json +1 -1
  35. package/dist/chunk-22FJ4JQO.cjs.map +0 -1
  36. package/dist/chunk-6LEXYQ7J.cjs.map +0 -1
  37. package/dist/chunk-EWTHDEIW.js.map +0 -1
  38. package/dist/chunk-H37YVYZA.cjs.map +0 -1
  39. package/dist/chunk-MLUWABVT.js.map +0 -1
  40. package/dist/chunk-TD2PHUYP.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-MLUWABVT.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-MLUWABVT.js';
3
- import { verifyPaymentReceipt } from './chunk-EWTHDEIW.js';
4
- export { RECEIPT_VERSION, verifyPaymentReceipt } from './chunk-EWTHDEIW.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-TD2PHUYP.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-TD2PHUYP.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
 
@@ -542,12 +565,30 @@ var MerchantEgressRefusedError = class extends HavenError {
542
565
  url;
543
566
  refusal;
544
567
  beforeRequest;
545
- constructor(message, url, refusal, beforeRequest) {
546
- super(message, MERCHANT_EGRESS_REFUSED_CODE, 400);
568
+ /** #4257: the redirect status that was not followed, when a redirect was refused. */
569
+ redirectStatus;
570
+ /**
571
+ * #4257: set only when `url` IS the redirect target (a cross-origin body
572
+ * re-send, a spent hop budget, a hop `assertUrl` refused). A refusal that
573
+ * stopped at the answering URL itself (a non-GET redirect, a missing or
574
+ * unresolvable Location) leaves it unset, and `url` is that answering URL.
575
+ */
576
+ redirectTarget;
577
+ /**
578
+ * #4257: set when the refusal interrupted a PAID delivery and the local
579
+ * completion has re-thrown it with the payment it belongs to — the funded,
580
+ * undelivered state, never "nothing happened".
581
+ */
582
+ settlementScheme;
583
+ constructor(message, url, refusal, beforeRequest, extra = {}) {
584
+ super(message, MERCHANT_EGRESS_REFUSED_CODE, 400, extra.paymentId);
547
585
  this.name = "MerchantEgressRefusedError";
548
586
  this.url = url;
549
587
  this.refusal = refusal;
550
588
  this.beforeRequest = beforeRequest;
589
+ this.redirectStatus = extra.redirectStatus;
590
+ this.redirectTarget = extra.redirectTarget;
591
+ this.settlementScheme = extra.settlementScheme;
551
592
  }
552
593
  };
553
594
  var MERCHANT_EGRESS_RESPONSE_CAP_CODE = "MERCHANT_EGRESS_RESPONSE_CAP";
@@ -662,6 +703,29 @@ var MCP_X402_PAYMENT_META_KEY = "x402/payment";
662
703
  var MCP_X402_PAYMENT_RESPONSE_META_KEY = "x402/payment-response";
663
704
  var X402_RETRY_REJECTED_STATUS = 402;
664
705
  var SESSION_NOT_FOUND_NEXT_ACTION = "reinitialize_then_retry_same_payment_header";
706
+ var CROSS_ORIGIN_REDIRECT_HEADERS = /* @__PURE__ */ new Set([
707
+ "accept",
708
+ "accept-language",
709
+ "user-agent",
710
+ "x402-wallet"
711
+ ]);
712
+ function crossOriginRedirectHeaders(headers) {
713
+ const kept = new Headers();
714
+ for (const [name, value] of new Headers(headers)) {
715
+ if (CROSS_ORIGIN_REDIRECT_HEADERS.has(name)) kept.set(name, value);
716
+ }
717
+ return kept;
718
+ }
719
+ var NO_POLICY_MAX_REDIRECTS = 20;
720
+ var REQUEST_BODY_HEADERS = ["content-encoding", "content-language", "content-location", "content-type", "content-length"];
721
+ function redirectedInit(init, status) {
722
+ const method = (init.method ?? "GET").toUpperCase();
723
+ const toGet = (status === 301 || status === 302) && method === "POST" || status === 303 && method !== "GET" && method !== "HEAD";
724
+ if (!toGet) return init;
725
+ const headers = new Headers(init.headers);
726
+ for (const name of REQUEST_BODY_HEADERS) headers.delete(name);
727
+ return { ...init, method: "GET", body: null, headers };
728
+ }
665
729
  var McpMerchantTransport = class {
666
730
  merchantTimeout;
667
731
  fetchImpl;
@@ -692,11 +756,24 @@ var McpMerchantTransport = class {
692
756
  }
693
757
  /** Fetch a merchant with a settlement-sized timeout and caller cancellation. */
694
758
  async fetch(url, init = {}, timeoutMs = this.merchantTimeout, maxBytes) {
759
+ return this.send(url, init, timeoutMs, maxBytes, false);
760
+ }
761
+ /**
762
+ * `fetch`, plus whether this request is the paid delivery (#4257). A paid
763
+ * delivery, or any body carrying an x402 payment in `_meta`, is never
764
+ * re-sent to another origin by a redirect that keeps the body; without a policy every other
765
+ * request follows that redirect as the platform would, headers reduced.
766
+ */
767
+ async send(url, init, timeoutMs, maxBytes, paidDelivery) {
768
+ const carriesPayment = paidDelivery || bodyCarriesX402Payment(init.body);
695
769
  if (!this.egress) {
696
770
  const timeoutSignal = AbortSignal.timeout(timeoutMs);
697
771
  const signal = init.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal;
698
772
  try {
699
- return await this.fetchImpl(url, { ...init, signal });
773
+ if (init.redirect === "manual" || init.redirect === "error") {
774
+ return await this.fetchImpl(url, { ...init, signal });
775
+ }
776
+ return await this.followRedirects(url, init, signal, void 0, carriesPayment);
700
777
  } catch (err) {
701
778
  if (timeoutSignal.aborted) {
702
779
  throw new MerchantTimeoutError(`Merchant request timed out after ${timeoutMs}ms: ${url}`);
@@ -704,87 +781,144 @@ var McpMerchantTransport = class {
704
781
  throw err;
705
782
  }
706
783
  }
707
- return this.fetchUnderPolicy(url, init, timeoutMs, maxBytes);
784
+ return this.fetchUnderPolicy(url, init, timeoutMs, maxBytes, carriesPayment);
708
785
  }
709
786
  /**
710
787
  * #3747: the policy path. Every URL — the initial request and each redirect
711
788
  * hop — is `assertUrl`-checked BEFORE it connects; redirects run `manual`
712
789
  * so nothing is ever followed un-checked; GET redirects are followed up to
713
- * `maxGetRedirects` (a redirect on any other method is refused); response
714
- * bodies are capped WHILE being read, not after buffering. The timeout
790
+ * `maxGetRedirects` (a redirect on any other method is refused); a hop to
791
+ * another origin carries only `CROSS_ORIGIN_REDIRECT_HEADERS` (#4244);
792
+ * response bodies are capped WHILE being read, not after buffering. The timeout
715
793
  * signal spans the WHOLE redirect chain, and `MerchantTimeoutError`
716
794
  * classification is preserved.
717
795
  */
718
- async fetchUnderPolicy(url, init, timeoutMs, maxBytes) {
796
+ async fetchUnderPolicy(url, init, timeoutMs, maxBytes, carriesPayment) {
719
797
  const egress = this.egress;
720
798
  try {
721
799
  egress.assertUrl(url);
722
800
  } catch (err) {
723
801
  throw asRefusal(err, url, true);
724
802
  }
725
- const method = (init.method ?? "GET").toUpperCase();
726
803
  const timeoutSignal = AbortSignal.timeout(timeoutMs);
727
804
  const signal = init.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal;
728
- const maxRedirects = egress.maxGetRedirects ?? 0;
805
+ let finalUrl = url;
806
+ try {
807
+ const response = await this.followRedirects(url, init, signal, egress, carriesPayment, (hopUrl) => {
808
+ finalUrl = hopUrl;
809
+ });
810
+ return this.capResponse(response, finalUrl, maxBytes);
811
+ } catch (err) {
812
+ if (timeoutSignal.aborted) {
813
+ throw new MerchantTimeoutError(`Merchant request timed out after ${timeoutMs}ms: ${finalUrl}`);
814
+ }
815
+ throw err;
816
+ }
817
+ }
818
+ /**
819
+ * The redirect follower both paths share (#3747, #4244, #4257). Redirects
820
+ * run `manual` and every hop is decided here:
821
+ *
822
+ * - Under a `policy`, every hop is `assertUrl`-checked before it connects,
823
+ * only GET redirects are followed (any other method is refused), up to
824
+ * `maxGetRedirects`; a missing or unresolvable Location is refused.
825
+ * - Without one, the platform's rules are kept: up to
826
+ * `NO_POLICY_MAX_REDIRECTS` hops, the Fetch spec's method/body rewrite,
827
+ * and a 3xx with no Location returned as the answer.
828
+ * - On both, a hop to another origin carries only
829
+ * `CROSS_ORIGIN_REDIRECT_HEADERS`, never restored on a later hop.
830
+ * - A hop that would re-send a request body to another origin is refused
831
+ * under a policy (which follows GET alone, so this is unreachable there),
832
+ * and without one only when `carriesPayment`: the paid delivery, or a body
833
+ * carrying an x402 payment in `_meta` (#4257). Any other body-keeping redirect is
834
+ * followed with its body, as the platform did, headers reduced.
835
+ *
836
+ * `onHop` sees every URL a request goes to, so the caller can name the hop
837
+ * it was on when the chain stops.
838
+ */
839
+ async followRedirects(url, init, signal, policy, carriesPayment, onHop) {
840
+ const maxRedirects = policy ? policy.maxGetRedirects ?? 0 : NO_POLICY_MAX_REDIRECTS;
729
841
  let currentUrl = url;
842
+ let hopInit = init;
730
843
  let hops = 0;
731
- try {
732
- while (true) {
733
- const response = await this.fetchImpl(currentUrl, {
734
- ...init,
735
- redirect: init.redirect === "error" ? "error" : "manual",
736
- signal
737
- });
738
- if (!isRedirectStatus(response.status)) return this.capResponse(response, currentUrl, maxBytes);
739
- if (method !== "GET") {
740
- throw new MerchantEgressRefusedError(
741
- `Hosted egress policy refuses the ${response.status} redirect from ${currentUrl}: redirects are followed for GET requests only, and this was a ${method}. The request itself was already sent to the checked URL; nothing was sent to the redirect target.`,
742
- currentUrl,
743
- "redirect_on_non_get",
744
- false
745
- );
746
- }
747
- const location = response.headers.get("location");
748
- if (!location) {
749
- throw new MerchantEgressRefusedError(
750
- `Hosted egress policy refuses the ${response.status} redirect from ${currentUrl}: it carries no Location header, so there is no checked target to follow.`,
751
- currentUrl,
752
- "redirect_missing_location",
753
- false
754
- );
755
- }
756
- let next;
844
+ while (true) {
845
+ onHop?.(currentUrl);
846
+ const response = await this.fetchImpl(currentUrl, {
847
+ ...hopInit,
848
+ redirect: init.redirect === "error" ? "error" : "manual",
849
+ signal
850
+ });
851
+ if (!isRedirectStatus(response.status)) return response;
852
+ const method = (hopInit.method ?? "GET").toUpperCase();
853
+ if (policy && method !== "GET") {
854
+ throw new MerchantEgressRefusedError(
855
+ `Hosted egress policy refuses the ${response.status} redirect from ${currentUrl}: redirects are followed for GET requests only, and this was a ${method}. The request itself was already sent to the checked URL; nothing was sent to the redirect target.`,
856
+ currentUrl,
857
+ "redirect_on_non_get",
858
+ false,
859
+ { redirectStatus: response.status }
860
+ );
861
+ }
862
+ const location = response.headers.get("location");
863
+ if (!location) {
864
+ if (!policy) return response;
865
+ throw new MerchantEgressRefusedError(
866
+ `Hosted egress policy refuses the ${response.status} redirect from ${currentUrl}: it carries no Location header, so there is no checked target to follow.`,
867
+ currentUrl,
868
+ "redirect_missing_location",
869
+ false,
870
+ { redirectStatus: response.status }
871
+ );
872
+ }
873
+ let next;
874
+ try {
875
+ next = new URL(location, currentUrl).toString();
876
+ } catch {
877
+ if (!policy) throw new TypeError(`fetch failed: the redirect from ${currentUrl} has an unresolvable Location header`);
878
+ throw new MerchantEgressRefusedError(
879
+ `Hosted egress policy refuses the redirect from ${currentUrl}: its Location header is not a resolvable URL.`,
880
+ currentUrl,
881
+ "redirect_invalid_location",
882
+ false,
883
+ { redirectStatus: response.status }
884
+ );
885
+ }
886
+ if (!policy && !/^https?:$/.test(new URL(next).protocol)) {
887
+ throw new TypeError(`fetch failed: the redirect from ${currentUrl} names a non-HTTP(S) URL`);
888
+ }
889
+ if (hops >= maxRedirects) {
890
+ if (!policy) throw new TypeError(`fetch failed: redirect count exceeded (${maxRedirects}) at ${currentUrl}`);
891
+ throw new MerchantEgressRefusedError(
892
+ `Hosted egress policy refuses the redirect from ${currentUrl} to ${next}: the ${maxRedirects}-redirect budget for this request is spent.`,
893
+ next,
894
+ "redirect_budget_exceeded",
895
+ false,
896
+ { redirectStatus: response.status, redirectTarget: next }
897
+ );
898
+ }
899
+ if (policy) {
757
900
  try {
758
- next = new URL(location, currentUrl).toString();
759
- } catch {
760
- throw new MerchantEgressRefusedError(
761
- `Hosted egress policy refuses the redirect from ${currentUrl}: its Location header is not a resolvable URL.`,
762
- currentUrl,
763
- "redirect_invalid_location",
764
- false
765
- );
901
+ policy.assertUrl(next);
902
+ } catch (err) {
903
+ throw asRefusal(err, next, false, { redirectStatus: response.status, redirectTarget: next });
766
904
  }
767
- if (hops >= maxRedirects) {
905
+ }
906
+ hopInit = redirectedInit(hopInit, response.status);
907
+ if (new URL(next).origin !== new URL(currentUrl).origin) {
908
+ if (hopInit.body != null && (policy || carriesPayment)) {
768
909
  throw new MerchantEgressRefusedError(
769
- `Hosted egress policy refuses the redirect from ${currentUrl} to ${next}: the ${maxRedirects}-redirect budget for this request is spent.`,
910
+ carriesPayment ? `The ${response.status} redirect of a paid request from ${currentUrl} to ${next} was not followed: following it would re-send the paid request to a different origin. The paid request, payment header and body included, was already delivered to ${currentUrl}; nothing was sent to ${next}.` : `Refusing the ${response.status} redirect from ${currentUrl} to ${next}: it would re-send the request body to a different origin. The request was already sent to the first URL; nothing was sent to the redirect target.`,
770
911
  next,
771
- "redirect_budget_exceeded",
772
- false
912
+ "redirect_cross_origin_body",
913
+ false,
914
+ { redirectStatus: response.status, redirectTarget: next }
773
915
  );
774
916
  }
775
- try {
776
- egress.assertUrl(next);
777
- } catch (err) {
778
- throw asRefusal(err, next, false);
779
- }
780
- hops++;
781
- currentUrl = next;
782
- }
783
- } catch (err) {
784
- if (timeoutSignal.aborted) {
785
- throw new MerchantTimeoutError(`Merchant request timed out after ${timeoutMs}ms: ${currentUrl}`);
917
+ hopInit = { ...hopInit, headers: crossOriginRedirectHeaders(hopInit.headers) };
786
918
  }
787
- throw err;
919
+ await response.body?.cancel().catch(() => void 0);
920
+ hops++;
921
+ currentUrl = next;
788
922
  }
789
923
  }
790
924
  /**
@@ -962,7 +1096,7 @@ var McpMerchantTransport = class {
962
1096
  if (send.includes(name)) headers.set(name, paymentHeader);
963
1097
  else headers.delete(name);
964
1098
  }
965
- return this.fetch(url, withMcpPaymentMeta({ ...init, headers }, paymentHeader), this.budgetFor("delivery"), this.capFor("delivery"));
1099
+ return this.send(url, withMcpPaymentMeta({ ...init, headers }, paymentHeader), this.budgetFor("delivery"), this.capFor("delivery"), true);
966
1100
  }
967
1101
  /**
968
1102
  * #3171: deliver the payment; if the merchant answers its unknown-session
@@ -1099,6 +1233,27 @@ function withMcpPaymentMeta(init, paymentHeader) {
1099
1233
  function encodeMcpSettlementReceipt(settlement) {
1100
1234
  return encodeBase64Json(settlement);
1101
1235
  }
1236
+ function bodyCarriesX402Payment(body) {
1237
+ let text;
1238
+ if (typeof body === "string") text = body;
1239
+ else if (body instanceof Uint8Array || body instanceof ArrayBuffer) {
1240
+ try {
1241
+ text = new TextDecoder("utf-8", { fatal: true }).decode(body);
1242
+ } catch {
1243
+ return false;
1244
+ }
1245
+ } else return false;
1246
+ let request;
1247
+ try {
1248
+ request = JSON.parse(text);
1249
+ } catch {
1250
+ return false;
1251
+ }
1252
+ return Array.isArray(request) ? request.some(messageCarriesX402Payment) : messageCarriesX402Payment(request);
1253
+ }
1254
+ function messageCarriesX402Payment(message) {
1255
+ return isRecord(message) && isRecord(message.params) && isRecord(message.params._meta) && message.params._meta[MCP_X402_PAYMENT_META_KEY] !== void 0;
1256
+ }
1102
1257
  function parseJsonRpcToolsCall(body) {
1103
1258
  if (typeof body !== "string") return void 0;
1104
1259
  let request;
@@ -1137,10 +1292,10 @@ function isRecord(value) {
1137
1292
  function isRedirectStatus(status) {
1138
1293
  return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
1139
1294
  }
1140
- function asRefusal(err, url, beforeRequest) {
1295
+ function asRefusal(err, url, beforeRequest, redirect) {
1141
1296
  if (!(err instanceof MerchantEgressRefusedError)) return err;
1142
- if (err.beforeRequest === beforeRequest) return err;
1143
- return new MerchantEgressRefusedError(err.message, url, err.refusal, beforeRequest);
1297
+ if (err.beforeRequest === beforeRequest && !redirect) return err;
1298
+ return new MerchantEgressRefusedError(err.message, url, err.refusal, beforeRequest, redirect);
1144
1299
  }
1145
1300
  async function captureMerchantResponse(response) {
1146
1301
  const merchant_body = await response.text().catch((err) => {
@@ -1214,6 +1369,17 @@ function deriveReadiness(status, allowances) {
1214
1369
  if (status !== "active") return "revoked";
1215
1370
  return allowances.some((allowance) => safeBigInt(allowance.remainingAtomic) > 0n) ? "ready" : "needs_approval";
1216
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
+ }
1217
1383
  function formatRemainingDisplay(tokenAddress, tokenSymbol, remainingAtomic) {
1218
1384
  const token = resolveTokenFromAddress(tokenAddress);
1219
1385
  return token ? `${formatAtomicAmount(safeBigInt(remainingAtomic), token.decimals)} ${tokenSymbol}` : `${remainingAtomic} ${tokenSymbol} (atomic; unknown decimals)`;
@@ -1260,6 +1426,7 @@ function mapTaskBudget(raw) {
1260
1426
  status: raw.status,
1261
1427
  expiresAt: raw.expires_at,
1262
1428
  isExpired: raw.is_expired,
1429
+ ...raw.is_live === void 0 ? {} : { isLive: raw.is_live },
1263
1430
  createdAt: raw.created_at,
1264
1431
  openedAt: raw.opened_at,
1265
1432
  closedAt: raw.closed_at,
@@ -1381,11 +1548,23 @@ var AccountReads = class {
1381
1548
  isResetPending: allowance.onchain.isResetPending,
1382
1549
  // #3731: same provenance as the detailed read's field — the compact
1383
1550
  // row is a projection of the same allowance, never a second source.
1384
- fundsCoverRemaining: allowance.fundsCoverRemaining
1551
+ fundsCoverRemaining: allowance.fundsCoverRemaining,
1552
+ nextRefillAt: allowance.nextRefillAt
1385
1553
  };
1386
1554
  });
1387
1555
  const readiness = deriveReadiness(agent.status, allowances);
1388
- 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
+ };
1389
1568
  }
1390
1569
  /**
1391
1570
  * #3329: `GET /task-budgets?status=open`, mapped to the condensed summary
@@ -1413,7 +1592,8 @@ var AccountReads = class {
1413
1592
  * an agent can still act on, and read-by-id (`haven_get_task_budget`) is
1414
1593
  * the any-status surface for everything else:
1415
1594
  *
1416
- * - `closing` ALWAYS stays visible, expired or not — the close submit is
1595
+ * - `closing` stays visible until its expiry (#3773; an expired `closing`
1596
+ * row drops, #3923) — the close submit is
1417
1597
  * in flight and the signer's close refusal tells the caller to re-check
1418
1598
  * exactly this row's status;
1419
1599
  * - `closed` rows drop (terminal — `closed_at` names what happened, and
@@ -1422,10 +1602,14 @@ var AccountReads = class {
1422
1602
  * budget reserves nothing and pays nothing);
1423
1603
  * - unexpired `pending` and `open` rows stay.
1424
1604
  *
1425
- * The same filter also runs here, client-side: it is what keeps the list
1426
- * bounded against a backend that predates `status=live`, which refuses the
1427
- * value with a 400 — that refusal (and only that) falls back to
1428
- * `status=all`. A row whose wire carries
1605
+ * A filter also runs here, client-side. A row carrying the backend's
1606
+ * `is_live` (#3922) is kept exactly when it is true — the server's own
1607
+ * `status=live` verdict. A row without it (an older backend) is derived by
1608
+ * the same rule: `closing` stays until its expiry, then drops (#3923), as
1609
+ * the backend does since #3773. That is
1610
+ * what keeps the list bounded against a backend that predates `status=live`,
1611
+ * which refuses the value with a 400 — that refusal (and only that) falls
1612
+ * back to `status=all`. A row whose wire carries
1429
1613
  * no `is_expired` is treated as unexpired — the degraded read errs toward
1430
1614
  * KEEPING rows, never toward hiding one the agent could still act on.
1431
1615
  */
@@ -1439,7 +1623,9 @@ var AccountReads = class {
1439
1623
  });
1440
1624
  const rows = raw?.task_budgets;
1441
1625
  if (!Array.isArray(rows)) return [];
1442
- return rows.map((row) => mapTaskBudget(row)).filter((tb) => tb.status === "closing" || !tb.isExpired && tb.status !== "closed").map((tb) => summarizeTaskBudget(tb));
1626
+ return rows.map((row) => mapTaskBudget(row)).filter(
1627
+ (tb) => tb.isLive !== void 0 ? tb.isLive : !tb.isExpired && tb.status !== "closed"
1628
+ ).map((tb) => summarizeTaskBudget(tb));
1443
1629
  } catch {
1444
1630
  return [];
1445
1631
  }
@@ -1474,6 +1660,9 @@ var AccountReads = class {
1474
1660
  accountAddress: raw.account_address,
1475
1661
  delegateAddress: raw.delegate_address,
1476
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,
1477
1666
  supersededBy: mapSupersession(raw.superseded_by),
1478
1667
  allowances: raw.allowances.map((allowance) => ({
1479
1668
  id: allowance.id,
@@ -1492,6 +1681,9 @@ var AccountReads = class {
1492
1681
  // "unverifiable", not a missing key. The key is absent on the wire
1493
1682
  // when the remaining is 0; that maps to null here too.
1494
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,
1495
1687
  remainingDisplay: formatRemainingDisplay(allowance.token_address, allowance.token_symbol, allowance.onchain.remaining),
1496
1688
  onchain: {
1497
1689
  amount: allowance.onchain.amount,
@@ -2117,7 +2309,7 @@ var X402FundingLeg = class {
2117
2309
  }
2118
2310
  this.assertSignableAuthorizationState("x402 payment", raw);
2119
2311
  if (!raw.sign_data?.hash) {
2120
- 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);
2121
2313
  }
2122
2314
  const sig = await this.signForData(raw.sign_data, {
2123
2315
  asset: option.asset,
@@ -2239,20 +2431,27 @@ var X402FundingLeg = class {
2239
2431
  * Skipped when `chainRpcs` does not include the chain; in that case Haven's
2240
2432
  * backend has already confirmed on-chain submission and callers accept the
2241
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`.
2242
2439
  */
2243
2440
  async waitForFundingTx(txHash, chainId, timeoutMs = 3e4) {
2244
2441
  if (!txHash || !chainId) return;
2245
2442
  const rpcUrl = this.chainRpcs[chainId];
2246
2443
  if (!rpcUrl) return;
2247
2444
  const provider = createJsonRpcProvider(rpcUrl);
2248
- const onChainReceipt = await provider.waitForTransaction(txHash, 1, timeoutMs);
2249
- if (!onChainReceipt || onChainReceipt.status !== 1) {
2250
- throw new HavenApiError(
2251
- "Funding tx did not confirm on-chain within the timeout window.",
2252
- 500,
2253
- { txHash, chainId }
2254
- );
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));
2255
2450
  }
2451
+ if (!onChainReceipt) {
2452
+ throw new FundingConfirmationUnknownError(txHash, chainId, "no receipt within the timeout window");
2453
+ }
2454
+ if (onChainReceipt.status !== 1) throw new FundingRevertedError(txHash, chainId);
2256
2455
  }
2257
2456
  /**
2258
2457
  * Can the delegate EOA still fund an authorization for `amountAtomic`?
@@ -2434,7 +2633,7 @@ var X402Erc7710 = class {
2434
2633
  ...options.subBudgetId ? { subBudgetId: options.subBudgetId } : {}
2435
2634
  });
2436
2635
  if (!raw.payment_id) {
2437
- 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);
2438
2637
  }
2439
2638
  if (raw.status === "confirmed" && raw.tx_hash) {
2440
2639
  const requestedUrl = options.resourceUrl ?? paymentRequired.resource?.url;
@@ -2451,7 +2650,7 @@ var X402Erc7710 = class {
2451
2650
  }
2452
2651
  const signData = raw.sign_data;
2453
2652
  if (signData?.signature_scheme !== "eip712_delegation" || !signData.typed_data) {
2454
- throw new HavenApiError(
2653
+ throw HavenApiError.deterministic(
2455
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.`,
2456
2655
  500,
2457
2656
  raw
@@ -2494,7 +2693,7 @@ var X402Erc7710 = class {
2494
2693
  throw err;
2495
2694
  }
2496
2695
  if (!settled.payment_header) {
2497
- throw new HavenApiError(
2696
+ throw HavenApiError.deterministic(
2498
2697
  "x402 settle returned no payment_header \u2014 the merchant cannot be retried.",
2499
2698
  500,
2500
2699
  settled
@@ -2615,6 +2814,26 @@ function toolError(err) {
2615
2814
  };
2616
2815
  }
2617
2816
 
2817
+ // src/merchant-answer.ts
2818
+ function decidePostFundingMerchantAnswer(input) {
2819
+ const inBandChallenge = input.toolResult ? extractMcpPaymentRequired(input.toolResult) : void 0;
2820
+ const headerReceipt = input.paymentResponseHeader;
2821
+ const protocolReceiptHeaderName = headerReceipt ? "PAYMENT-RESPONSE" : input.metaSettlement ? `_meta.${MCP_X402_PAYMENT_RESPONSE_META_KEY}` : void 0;
2822
+ const protocolReceiptHeader = headerReceipt ?? (input.metaSettlement ? encodeMcpSettlementReceipt(input.metaSettlement) : void 0);
2823
+ const settlement = parseMerchantSettlement(headerReceipt ?? null);
2824
+ const settlementTxHash = settlement.settlementTxHash ?? (typeof input.metaSettlement?.transaction === "string" ? input.metaSettlement.transaction : void 0);
2825
+ const plainToolErrorAfterFunding = input.toolResult?.isError === true && inBandChallenge === void 0 && !settlementTxHash;
2826
+ const inBandRejection = inBandChallenge !== void 0 || input.metaSettlement?.success === false || plainToolErrorAfterFunding;
2827
+ return {
2828
+ accepted: input.httpOk && !inBandRejection,
2829
+ inBandRejection,
2830
+ plainToolErrorAfterFunding,
2831
+ settlementTxHash,
2832
+ protocolReceiptHeader,
2833
+ protocolReceiptHeaderName
2834
+ };
2835
+ }
2836
+
2618
2837
  // src/merchant-completion.ts
2619
2838
  var MERCHANT_BODY_SNIPPET_LIMIT = 1e3;
2620
2839
  var EVIDENCE_RETRY_DELAYS_MS = [1e3, 2e3, 4e3];
@@ -2645,10 +2864,10 @@ var MerchantCompletion = class {
2645
2864
  }
2646
2865
  async retryRequest(url, initialInit, paymentRequired, receipt) {
2647
2866
  if (!receipt.accepted) {
2648
- 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);
2649
2868
  }
2650
2869
  if (!receipt.paymentHeader) {
2651
- 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);
2652
2871
  }
2653
2872
  let retryInit = initialInit;
2654
2873
  if (this.getTaxDeclarationHeader && this.delegateKey) {
@@ -2664,16 +2883,31 @@ var MerchantCompletion = class {
2664
2883
  retryInit = withTaxDeclarationHeader(initialInit, void 0);
2665
2884
  }
2666
2885
  }
2667
- const retryResponse = await this.merchantTransport.deliverPaymentRecoveringSession(
2668
- url,
2669
- retryInit,
2670
- receipt.paymentHeader,
2671
- () => this.merchantTransport.initialize(url, initialInit)
2672
- );
2886
+ let retryResponse;
2887
+ try {
2888
+ retryResponse = await this.merchantTransport.deliverPaymentRecoveringSession(
2889
+ url,
2890
+ retryInit,
2891
+ receipt.paymentHeader,
2892
+ () => this.merchantTransport.initialize(url, initialInit)
2893
+ );
2894
+ } catch (err) {
2895
+ if (err instanceof MerchantEgressRefusedError && !err.beforeRequest) {
2896
+ throw await this.paidDeliveryInterrupted(err, url, receipt, receipt.paymentHeader, retryInit?.body != null);
2897
+ }
2898
+ throw err;
2899
+ }
2673
2900
  const toolResult = retryResponse.ok ? await this.merchantTransport.readToolResult(retryResponse) : void 0;
2674
- const inBandChallenge = toolResult ? extractMcpPaymentRequired(toolResult) : void 0;
2675
2901
  const metaSettlement = toolResult ? mcpSettlementFromToolResult(toolResult) : void 0;
2676
- const inBandRejection = inBandChallenge !== void 0 || metaSettlement?.success === false;
2902
+ const headerReceipt = retryResponse.headers.get("PAYMENT-RESPONSE");
2903
+ const answer = decidePostFundingMerchantAnswer({
2904
+ toolResult,
2905
+ httpOk: retryResponse.ok,
2906
+ paymentResponseHeader: headerReceipt ?? void 0,
2907
+ metaSettlement
2908
+ });
2909
+ const reportedSettlementTxHash = answer.settlementTxHash;
2910
+ const inBandRejection = answer.inBandRejection;
2677
2911
  if (!retryResponse.ok || inBandRejection) {
2678
2912
  const merchant = await captureMerchantResponse(retryResponse);
2679
2913
  await this.recordRetryRejected({
@@ -2688,7 +2922,7 @@ var MerchantCompletion = class {
2688
2922
  }
2689
2923
  });
2690
2924
  throw new HavenApiError(
2691
- "x402 retry failed after Haven funded the delegate wallet; reconciliation may be required.",
2925
+ "x402 retry failed after Haven funded the delegate wallet; reconciliation may be required. The merchant may still settle late: check the payment status before sweeping the delegate.",
2692
2926
  inBandRejection ? X402_RETRY_REJECTED_STATUS : merchant.merchant_status,
2693
2927
  {
2694
2928
  marker: "x402_retry_rejected_after_funding",
@@ -2701,10 +2935,10 @@ var MerchantCompletion = class {
2701
2935
  }
2702
2936
  );
2703
2937
  }
2704
- const headerReceipt = retryResponse.headers.get("PAYMENT-RESPONSE");
2705
- const protocolReceiptHeader = headerReceipt ?? (metaSettlement ? encodeMcpSettlementReceipt(metaSettlement) : void 0);
2706
- const protocolReceiptHeaderName = headerReceipt ? "PAYMENT-RESPONSE" : metaSettlement ? `_meta.${MCP_X402_PAYMENT_RESPONSE_META_KEY}` : void 0;
2707
- const merchantSettlement = parseMerchantSettlement(protocolReceiptHeader ?? null);
2938
+ const { protocolReceiptHeader, protocolReceiptHeaderName } = answer;
2939
+ const merchantSettlement = {
2940
+ settlementTxHash: reportedSettlementTxHash ?? null
2941
+ };
2708
2942
  if (receipt.merchant && merchantSettlement.settlementTxHash) {
2709
2943
  receipt.merchant.settlementTxHash = merchantSettlement.settlementTxHash;
2710
2944
  receipt.merchant.settlementExplorerUrl = buildExplorerUrl(receipt.chainId, merchantSettlement.settlementTxHash) || null;
@@ -2724,7 +2958,12 @@ var MerchantCompletion = class {
2724
2958
  });
2725
2959
  const settlementCandidate = merchantSettlement.settlementTxHash;
2726
2960
  if (settlementCandidate && isWellFormedSettlementTxHash(settlementCandidate) && !isZeroSettlementTxHash(settlementCandidate) && settlementCandidate.toLowerCase() !== receipt.txHash.toLowerCase()) {
2727
- await this.reportSettlementEvidenceOnce(receipt.paymentId, settlementCandidate);
2961
+ await this.reportEvidence({
2962
+ paymentId: receipt.paymentId,
2963
+ rail: "x402",
2964
+ txHash: settlementCandidate,
2965
+ ...settlementProofFields(receipt.paymentHeader)
2966
+ });
2728
2967
  }
2729
2968
  await this.reportMerchantReceipt(receipt.paymentId, retryResponse);
2730
2969
  return retryResponse;
@@ -2827,6 +3066,51 @@ var MerchantCompletion = class {
2827
3066
  // and MACHINE-PAYMENT-CHALLENGE was never produced by any other Haven
2828
3067
  // surface. Use the x402 flow (authorizeX402 / fetch / quoteX402 / payX402Quote)
2829
3068
  // for agent-to-merchant payments.
3069
+ /**
3070
+ * #4257: record a paid delivery that a refused redirect interrupted, and
3071
+ * return the refusal re-thrown with its payment and an accurate account:
3072
+ * the payment header (and the body, when there is one) reached `url`, the
3073
+ * merchant answered with a redirect Haven did not follow, and the merchant
3074
+ * may still settle. The record is the same
3075
+ * `merchant_retry_rejected_after_payment` event a non-2xx answer writes: its
3076
+ * `retry_status` is the redirect status, `retry_body` is null, and `details`
3077
+ * carry `refusal` plus `redirect_location` when the refusal names a redirect
3078
+ * target, or `refused_url` (the URL that answered) otherwise. The payment
3079
+ * reads as funded and undelivered.
3080
+ */
3081
+ async paidDeliveryInterrupted(err, url, receipt, paymentHeader, hasBody) {
3082
+ const scheme = x402PaymentHeaderNamesFor(paymentHeader).length > 1 ? "eip3009" : "erc7710";
3083
+ const status = err.redirectStatus ?? 0;
3084
+ await this.recordRetryRejected({
3085
+ rail: "x402",
3086
+ paymentId: receipt.paymentId,
3087
+ txHash: receipt.txHash,
3088
+ resourceUrl: receipt.resourceUrl,
3089
+ merchant: {
3090
+ merchant_status: status,
3091
+ merchant_status_text: "redirect not followed",
3092
+ merchant_headers: {},
3093
+ merchant_body: ""
3094
+ },
3095
+ details: {
3096
+ merchant_to: receipt.merchantTo,
3097
+ delegate_to: receipt.to,
3098
+ refusal: err.refusal,
3099
+ // The refusal's url is the redirect target only when it carries one;
3100
+ // otherwise it is the URL that answered (a non-GET redirect, a missing
3101
+ // or unresolvable Location), recorded under a neutral key.
3102
+ ...err.redirectTarget ? { redirect_location: err.redirectTarget } : { refused_url: err.url }
3103
+ }
3104
+ });
3105
+ const what = `The paid request was delivered to ${url}, ${hasBody ? "payment header and body" : "payment header"} included, and the merchant answered ${status ? `HTTP ${status} ` : ""}with a redirect${unfollowedRedirect(err)} Do not pay again: `;
3106
+ const next = scheme === "erc7710" ? "erc7710 has no funding leg, so there is no delegate balance to strand or sweep. The merchant may still redeem the settlement authorization within its window: check haven_get_payment_status after that window and re-quote only if it shows no settlement." : "the funding leg is confirmed on-chain and the merchant may still settle late. Check haven_get_payment_status before anything else, and sweep the delegate only if no settlement appears.";
3107
+ return new MerchantEgressRefusedError(what + next, err.url, err.refusal, false, {
3108
+ redirectStatus: err.redirectStatus,
3109
+ redirectTarget: err.redirectTarget,
3110
+ paymentId: receipt.paymentId,
3111
+ settlementScheme: scheme
3112
+ });
3113
+ }
2830
3114
  async recordRetryRejected(input) {
2831
3115
  try {
2832
3116
  await this.post("/machine-payments/reconciliation-events", {
@@ -2969,17 +3253,19 @@ var MerchantCompletion = class {
2969
3253
  };
2970
3254
  for (let attempt = 0; ; attempt += 1) {
2971
3255
  const result = await this.postEvidenceOnce(body);
2972
- 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) {
2973
3257
  return result;
2974
3258
  }
2975
3259
  await this.sleep(EVIDENCE_RETRY_DELAYS_MS[attempt]);
2976
3260
  }
2977
3261
  }
2978
3262
  /**
2979
- * ONE evidence attempt — the body of `reportEvidence`'s loop above, and the
2980
- * whole of the #3764 settlement report below. A 503 answers `retryable`
2981
- * immediately; the caller decides whether to sleep and re-attempt
2982
- * (`reportEvidence`) or hand the outcome to its consumer (#3764 D2).
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
3266
+ * re-attempt (`reportEvidence`, and — #4215 — the local settlement report
3267
+ * in `retryRequest`) or hand the outcome to its consumer (#3764 D2, the
3268
+ * hosted settlement report).
2983
3269
  */
2984
3270
  async postEvidenceOnce(body) {
2985
3271
  try {
@@ -2987,12 +3273,15 @@ var MerchantCompletion = class {
2987
3273
  return { outcome: "confirmed" };
2988
3274
  } catch (err) {
2989
3275
  const statusCode = err instanceof HavenApiError ? err.statusCode : void 0;
2990
- 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) {
2991
3278
  const reason = refusalReasonFromBody(err);
3279
+ const errorCode = refusalErrorCodeFromBody(err);
2992
3280
  return {
2993
3281
  outcome: "refused",
2994
3282
  statusCode: statusCode ?? 0,
2995
- ...reason ? { reason } : {}
3283
+ ...reason ? { reason } : {},
3284
+ ...errorCode ? { errorCode } : {}
2996
3285
  };
2997
3286
  }
2998
3287
  return { outcome: "retryable", statusCode };
@@ -3029,39 +3318,64 @@ var MerchantCompletion = class {
3029
3318
  * gate uses, so a caller cannot "fix" a missing hash by reporting the demo
3030
3319
  * merchant's own marker and getting a different verdict than the settle
3031
3320
  * path already gave it.
3321
+ *
3322
+ * #4061: `options.paymentHeader` is the payment header the caller sent to
3323
+ * the merchant on its paid retry. On eip3009 it is REQUIRED for the record
3324
+ * (#3925): its signed authorization proves the nonce the settlement burned
3325
+ * is this payment's, and a report without it is refused with
3326
+ * `errorCode: 'settlement_proof_header_missing'`. It is forwarded exactly
3327
+ * as the funding report forwards it (`paymentProofHeader` plus the names
3328
+ * `x402PaymentHeaderNamesSent` derives from the same payload). erc7710
3329
+ * needs none — Haven assembled that header itself.
3032
3330
  */
3033
- async reportSettlementEvidence(paymentId, settlementTxHash) {
3331
+ async reportSettlementEvidence(paymentId, settlementTxHash, options = {}) {
3034
3332
  if (isZeroSettlementTxHash(settlementTxHash)) {
3035
3333
  throw new HavenZeroSettlementHashError(paymentId);
3036
3334
  }
3037
3335
  return this.reportEvidence({
3038
3336
  paymentId,
3039
3337
  rail: "x402",
3040
- txHash: settlementTxHash
3338
+ txHash: settlementTxHash,
3339
+ ...settlementProofFields(options.paymentHeader)
3041
3340
  });
3042
3341
  }
3043
3342
  /**
3044
3343
  * #3764: report the merchant's OWN EIP-3009 settlement transaction right
3045
- * after a completed purchase — the second leg of a payment that already has
3046
- * a funding-evidence row. Same endpoint and payload shape as
3344
+ * after a completed hosted purchase — the second leg of a payment that
3345
+ * already has a funding-evidence row. Same endpoint and payload shape as
3047
3346
  * `reportSettlementEvidence` above, but with #3764's D2 budget: exactly ONE
3048
3347
  * attempt, no `EVIDENCE_RETRY_DELAYS_MS` backoff. The resource is already
3049
3348
  * paid for and delivered, so a retryable answer must not stretch the
3050
3349
  * completed purchase — the HOSTED tools hand the outcome to the agent
3051
- * (`settlementEvidenceOutcome` → `haven_report_settlement_evidence`), and
3052
- * the local caller swallows it by design (D1). Callers validate the hash
3053
- * before this call (well-formed, non-zero, different from the funding hash
3350
+ * (`settlementEvidenceOutcome` → `haven_report_settlement_evidence`). Since
3351
+ * #4215 this ONE-attempt form serves the HOSTED path only
3352
+ * (`completeX402MerchantCall`); the local `retryRequest` posts the same
3353
+ * report through `reportEvidence`, whose bounded loop re-attempts while
3354
+ * Haven answers `retryable`. Callers validate the hash before this call
3355
+ * (well-formed, non-zero, different from the funding hash
3054
3356
  * case-insensitively), so there is no client-side refusal here — the
3055
3357
  * backend verifies on-chain and is the only judge.
3358
+ *
3359
+ * #4061: `paymentHeader` is the header the paid retry sent — the #3925
3360
+ * nonce proof, forwarded as `reportSettlementEvidence` forwards it. Both
3361
+ * callers hold it; without it the backend refuses the record.
3056
3362
  */
3057
- async reportSettlementEvidenceOnce(paymentId, settlementTxHash) {
3363
+ async reportSettlementEvidenceOnce(paymentId, settlementTxHash, paymentHeader) {
3058
3364
  return this.postEvidenceOnce({
3059
3365
  paymentId,
3060
3366
  rail: "x402",
3061
- txHash: settlementTxHash
3367
+ txHash: settlementTxHash,
3368
+ ...settlementProofFields(paymentHeader)
3062
3369
  });
3063
3370
  }
3064
3371
  };
3372
+ function settlementProofFields(paymentHeader) {
3373
+ if (!paymentHeader || !paymentHeader.trim()) return {};
3374
+ return {
3375
+ paymentProofHeaderName: x402PaymentHeaderNamesSent(paymentHeader),
3376
+ paymentProofHeader: paymentHeader
3377
+ };
3378
+ }
3065
3379
  function refusalReasonFromBody(err) {
3066
3380
  if (!(err instanceof HavenApiError)) return void 0;
3067
3381
  const body = err.body;
@@ -3069,6 +3383,13 @@ function refusalReasonFromBody(err) {
3069
3383
  const reason = body.reason;
3070
3384
  return typeof reason === "string" && reason.length > 0 ? reason : void 0;
3071
3385
  }
3386
+ function refusalErrorCodeFromBody(err) {
3387
+ if (!(err instanceof HavenApiError)) return void 0;
3388
+ const body = err.body;
3389
+ if (body === null || typeof body !== "object" || Array.isArray(body)) return void 0;
3390
+ const code = body.error_code;
3391
+ return typeof code === "string" && code.length > 0 ? code : void 0;
3392
+ }
3072
3393
  function isZeroSettlementTxHash(hash) {
3073
3394
  return typeof hash === "string" && /^0x0+$/i.test(hash);
3074
3395
  }
@@ -3089,6 +3410,25 @@ function parseProtocolReceiptHeader(value) {
3089
3410
  }
3090
3411
  }
3091
3412
  }
3413
+ function unfollowedRedirect(err) {
3414
+ const target = err.redirectTarget ?? err.url;
3415
+ switch (err.refusal) {
3416
+ case "redirect_cross_origin_body":
3417
+ return ` to a different origin (${target}). Haven did not follow it, so the payment was not re-sent there.`;
3418
+ case "redirect_on_non_get":
3419
+ return ". Under the egress policy only GET redirects are followed, so Haven did not follow it and nothing was sent to the redirect target.";
3420
+ case "redirect_missing_location":
3421
+ return " that named no target (no Location header). Haven did not follow it.";
3422
+ case "redirect_invalid_location":
3423
+ return " whose Location header is not a resolvable URL. Haven did not follow it.";
3424
+ case "redirect_budget_exceeded":
3425
+ return ` past the redirect budget. The chain stopped before ${target}, and nothing was sent there.`;
3426
+ case "url_not_allowed":
3427
+ return ` to a URL the egress policy does not allow (${target}). Haven did not follow it, so nothing was sent there.`;
3428
+ default:
3429
+ return ". Haven did not follow it.";
3430
+ }
3431
+ }
3092
3432
 
3093
3433
  // src/client.ts
3094
3434
  var DEFAULT_POLLING_INTERVAL = 3e3;
@@ -3355,7 +3695,7 @@ var HavenClient = class {
3355
3695
  }
3356
3696
  const fundingTo = options.delegateAddress ?? (await this.getAgent()).delegateAddress;
3357
3697
  if (!fundingTo) {
3358
- throw new HavenApiError("Authenticated agent has no delegate address registered.", 502);
3698
+ throw HavenApiError.deterministic("Authenticated agent has no delegate address registered.", 502);
3359
3699
  }
3360
3700
  const idempotencyKey = options.idempotencyKey ?? buildX402IdempotencyKey(paymentRequired, option);
3361
3701
  const raw = await this.post("/x402", {
@@ -3393,7 +3733,7 @@ var HavenClient = class {
3393
3733
  throwPaymentStateError("x402 payment", raw);
3394
3734
  }
3395
3735
  if (!raw.sign_data?.hash) {
3396
- 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);
3397
3737
  }
3398
3738
  if (raw.sign_data.signature_scheme !== void 0 && !raw.sign_data.typed_data) {
3399
3739
  throw new HavenSigningError(
@@ -3401,7 +3741,7 @@ var HavenClient = class {
3401
3741
  );
3402
3742
  }
3403
3743
  if (!raw.x402_expected_auth) {
3404
- 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);
3405
3745
  }
3406
3746
  return {
3407
3747
  paymentId: raw.payment_id,
@@ -3631,6 +3971,12 @@ var HavenClient = class {
3631
3971
  * degrade-to-warning path and the ledger row would still land while the
3632
3972
  * purchase proceeded.
3633
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
+ *
3634
3980
  * #3518: without `merchantTo`, an agent whose budgets for the token are all
3635
3981
  * merchant-locked gets 409 `budget_requires_recipient` (thrown, nothing
3636
3982
  * recorded) naming the pins in `budget_recipient_addresses` — repeat the
@@ -3648,18 +3994,20 @@ var HavenClient = class {
3648
3994
  amountAtomic: input.amountAtomic,
3649
3995
  ...input.merchantTo !== void 0 ? { merchantTo: input.merchantTo } : {},
3650
3996
  ...input.resourceUrl !== void 0 ? { resourceUrl: input.resourceUrl } : {},
3651
- ...input.idempotencyKey !== void 0 ? { idempotencyKey: input.idempotencyKey } : {}
3997
+ ...input.idempotencyKey !== void 0 ? { idempotencyKey: input.idempotencyKey } : {},
3998
+ ...input.recordRefusal !== void 0 ? { recordRefusal: input.recordRefusal } : {}
3652
3999
  });
3653
4000
  }
3654
4001
  /**
3655
4002
  * Post-purchase allowance/budget summary for a settled payment (#1310).
3656
4003
  *
3657
- * Reuses the EXACT rail-aware read path {@link getAllowances} / #1306's
3658
- * catalog-purchase preflight `allowance` block use — `GET
3659
- * /machine-payments/allowances`, with delegation-rail values coming from
3660
- * 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
3661
4007
  * `agent_allowances` — so this can never disagree with
3662
- * {@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
3663
4011
  * resolved from {@link getPaymentStatus} so callers pass only
3664
4012
  * `paymentId`, never a second haven_get_agent-style round trip.
3665
4013
  *
@@ -3755,10 +4103,10 @@ var HavenClient = class {
3755
4103
  return this.get(`/catalog/submit/${encodeURIComponent(id)}`);
3756
4104
  }
3757
4105
  /**
3758
- * Fetch one curated catalog entry by id (#1306).
4106
+ * Fetch one catalog entry by id (#1306).
3759
4107
  *
3760
4108
  * Chain-scoped for free by the backend's SQL when the client is
3761
- * agent-authenticated (#1299): an unknown id and an id curated for a
4109
+ * agent-authenticated (#1299): an unknown id and an id listed for a
3762
4110
  * DIFFERENT chain than this agent's both 404 identically — this method does
3763
4111
  * not (and must not) re-filter by chain in JS. Read-only, like
3764
4112
  * {@link discoverTools}.
@@ -3914,9 +4262,10 @@ var HavenClient = class {
3914
4262
  return this.accountReads.listPendingSubBudgetSignatures();
3915
4263
  }
3916
4264
  /**
3917
- * Close a sub-budget row (delegating agent only). A `pending` or expired row
3918
- * closes immediately; a live child returns `signData` for the on-chain
3919
- * revocation — sign it (`haven_sign` with `sub_budget_id`) and call
4265
+ * Close a sub-budget row (delegating agent only). A `pending` row, or an
4266
+ * `open` one already expired, closes immediately; otherwise (a live child, or
4267
+ * any `closing` row — its disable is still owed, #3923) it returns `signData`
4268
+ * for the on-chain revocation — sign it (`haven_sign` with `sub_budget_id`) and call
3920
4269
  * `submitSubBudget`.
3921
4270
  */
3922
4271
  async closeSubBudget(id) {
@@ -4342,13 +4691,15 @@ var HavenClient = class {
4342
4691
  body = text;
4343
4692
  }
4344
4693
  const toolResult = mcpToolResultOf(body);
4345
- const inBandChallenge = toolResult ? extractMcpPaymentRequired(toolResult) : void 0;
4346
4694
  const metaSettlement = toolResult ? mcpSettlementFromToolResult(toolResult) : void 0;
4347
4695
  const headerReceipt = surfaced.headers.get("PAYMENT-RESPONSE") ?? void 0;
4348
- const protocolReceiptHeaderName = headerReceipt ? "PAYMENT-RESPONSE" : metaSettlement ? `_meta.${MCP_X402_PAYMENT_RESPONSE_META_KEY}` : void 0;
4349
- const protocolReceiptHeader = headerReceipt ?? (metaSettlement ? encodeMcpSettlementReceipt(metaSettlement) : void 0);
4350
- const settlement = parseMerchantSettlement(protocolReceiptHeader ?? null);
4351
- const merchantAccepted = surfaced.ok && inBandChallenge === void 0 && metaSettlement?.success !== false;
4696
+ const answer = decidePostFundingMerchantAnswer({
4697
+ toolResult,
4698
+ httpOk: surfaced.ok,
4699
+ paymentResponseHeader: headerReceipt,
4700
+ metaSettlement
4701
+ });
4702
+ const merchantAccepted = answer.accepted;
4352
4703
  let evidenceOutcome;
4353
4704
  let settlementEvidenceOutcome;
4354
4705
  if (!merchantAccepted) {
@@ -4370,7 +4721,7 @@ var HavenClient = class {
4370
4721
  });
4371
4722
  }
4372
4723
  } else {
4373
- const rawEvidenceTxHash = input.noFundingLeg ? settlement.settlementTxHash ?? void 0 : fundingTxHash ?? void 0;
4724
+ const rawEvidenceTxHash = input.noFundingLeg ? answer.settlementTxHash ?? void 0 : fundingTxHash ?? void 0;
4374
4725
  const evidenceTxHash = rawEvidenceTxHash && !isZeroSettlementTxHash(rawEvidenceTxHash) ? rawEvidenceTxHash : void 0;
4375
4726
  if (evidenceTxHash) {
4376
4727
  evidenceOutcome = await this.merchantCompletion.reportEvidence({
@@ -4381,19 +4732,20 @@ var HavenClient = class {
4381
4732
  merchantStatus: surfaced.status,
4382
4733
  paymentProofHeaderName: x402PaymentHeaderNamesSent(input.paymentHeader),
4383
4734
  paymentProofHeader: input.paymentHeader,
4384
- protocolReceiptHeaderName,
4385
- protocolReceiptHeader,
4735
+ protocolReceiptHeaderName: answer.protocolReceiptHeaderName,
4736
+ protocolReceiptHeader: answer.protocolReceiptHeader,
4386
4737
  // #3778: the delivery pointer rides the accepted arm's evidence
4387
4738
  // report — a rejection delivered nothing to point at.
4388
4739
  ...input.deliveryReference ? { deliveryReference: input.deliveryReference } : {}
4389
4740
  });
4390
4741
  }
4391
4742
  if (!input.noFundingLeg) {
4392
- const settlementCandidate = settlement.settlementTxHash;
4743
+ const settlementCandidate = answer.settlementTxHash;
4393
4744
  if (settlementCandidate && isWellFormedSettlementTxHash(settlementCandidate) && !isZeroSettlementTxHash(settlementCandidate) && fundingTxHash && settlementCandidate.toLowerCase() !== fundingTxHash.toLowerCase()) {
4394
4745
  settlementEvidenceOutcome = await this.merchantCompletion.reportSettlementEvidenceOnce(
4395
4746
  evidenceContext.paymentId,
4396
- settlementCandidate
4747
+ settlementCandidate,
4748
+ input.paymentHeader
4397
4749
  );
4398
4750
  }
4399
4751
  }
@@ -4404,7 +4756,7 @@ var HavenClient = class {
4404
4756
  ok: merchantAccepted,
4405
4757
  body,
4406
4758
  // #3155 review S3: a transaction beside a rejection is not a settlement.
4407
- settlementTxHash: merchantAccepted ? settlement.settlementTxHash ?? void 0 : void 0,
4759
+ settlementTxHash: merchantAccepted ? answer.settlementTxHash ?? void 0 : void 0,
4408
4760
  evidenceOutcome,
4409
4761
  settlementEvidenceOutcome
4410
4762
  };
@@ -4437,9 +4789,15 @@ var HavenClient = class {
4437
4789
  * Haven verifies the delegate → merchant transfer on-chain
4438
4790
  * (`observeEip3009MerchantSettlement`) and records it beside the funding
4439
4791
  * hash, so receipts name the merchant's settlement.
4792
+ *
4793
+ * #4061: on eip3009 pass `options.paymentHeader` — the payment header you
4794
+ * sent the merchant on your paid retry. Since #3925 it is the proof the
4795
+ * settlement is recorded against; without it the report is `refused` with
4796
+ * `errorCode: 'settlement_proof_header_missing'`, and repeating it WITH the
4797
+ * header can succeed.
4440
4798
  */
4441
- async reportSettlementEvidence(paymentId, settlementTxHash) {
4442
- return await this.merchantCompletion.reportSettlementEvidence(paymentId, settlementTxHash);
4799
+ async reportSettlementEvidence(paymentId, settlementTxHash, options = {}) {
4800
+ return await this.merchantCompletion.reportSettlementEvidence(paymentId, settlementTxHash, options);
4443
4801
  }
4444
4802
  /**
4445
4803
  * #3770: record the agent's evidence-only delivery verdict on a settled
@@ -4674,7 +5032,7 @@ var toolDescriptions = {
4674
5032
  getAgent: {
4675
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.",
4676
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.",
4677
- 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.`,
4678
5036
  nextActionGuidance: ""
4679
5037
  },
4680
5038
  getAllowances: {
@@ -4688,7 +5046,7 @@ var toolDescriptions = {
4688
5046
  checkFunds: {
4689
5047
  summary: "Check whether the agent's account actually holds at least the given amount of a token \u2014 funds held, not spend permitted.",
4690
5048
  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.",
4691
- 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.",
5049
+ behavior: "Returns covered: true (the account holds at least the checked amount), false (a live chain read reports the account holds less than the checked amount; stop and tell the user it is short), 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.",
4692
5050
  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."
4693
5051
  },
4694
5052
  listReceipts: {
@@ -4716,9 +5074,9 @@ var toolDescriptions = {
4716
5074
  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."
4717
5075
  },
4718
5076
  discoverTools: {
4719
- summary: "Step 1 of a purchase: discover payable services from Haven's curated merchant catalog \u2014 names, prices, the next call.",
4720
- 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.",
4721
- 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. " + FUNDING_LEG_DISCOVERY_GUIDANCE + " 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.",
5077
+ summary: "Step 1 of a purchase: discover payable services from Haven's merchant catalog \u2014 names, prices, the next call.",
5078
+ 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 (any source; bazaar seller-asserted) \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.",
5079
+ behavior: "Use each entry's suggested_tool + suggested_arguments first; an entry without them says why. Read-only lookup against Haven's 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. " + FUNDING_LEG_DISCOVERY_GUIDANCE + " 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.",
4722
5080
  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.`
4723
5081
  },
4724
5082
  submitCatalogEntry: {
@@ -4741,8 +5099,8 @@ var toolDescriptions = {
4741
5099
  },
4742
5100
  reportSettlementEvidence: {
4743
5101
  summary: "Report a payment's real merchant settlement transaction hash so Haven can verify it on-chain and record it.",
4744
- behavior: "Pass payment_id and, when you hold one, settlement_tx_hash (0x + 64 hex chars) \u2014 from PAYMENT-RESPONSE or a prior settlement_tx_hash. Omitting settlement_tx_hash succeeds as a no-op \u2014 nothing is checked or recorded, and no network call is made. Haven verifies on-chain before recording a hash; a zero, mismatched, or reverted hash is refused. Your own payments only.",
4745
- nextActionGuidance: "code SETTLEMENT_NOT_RECORDED (refusal_reason present): the payment's funding is confirmed and this hash was not accepted for it \u2014 do not retry it; refusal_reason carries why, verbatim. code DELIVERED_UNSETTLED: not verified, do not retry \u2014 poll haven_get_payment_status. code SETTLEMENT_PENDING (retryable:true): not mined or RPC unreachable \u2014 report the same hash again shortly."
5102
+ behavior: "Pass payment_id and, when you hold one, settlement_tx_hash (0x + 64 hex chars) \u2014 from PAYMENT-RESPONSE or a prior settlement_tx_hash. On EIP-3009 also pass payment_header (the header you retried the merchant with) or the report is refused. Omitting settlement_tx_hash succeeds as a no-op \u2014 nothing is checked or recorded, and no network call is made. Haven verifies on-chain before recording a hash; a zero, mismatched, or reverted hash is refused. Your own payments only.",
5103
+ nextActionGuidance: "code SETTLEMENT_PROOF_HEADER_MISSING: call again adding payment_header. code SETTLEMENT_NOT_RECORDED (refusal_reason present): the payment's funding is confirmed and this hash was not accepted for it \u2014 do not retry it; refusal_reason carries why, verbatim. code DELIVERED_UNSETTLED: not verified, do not retry \u2014 poll haven_get_payment_status. code SETTLEMENT_PENDING (retryable:true): not mined or RPC unreachable \u2014 report the same hash again shortly."
4746
5104
  }
4747
5105
  };
4748
5106
 
@@ -4996,13 +5354,13 @@ You are an AI agent whose user has no Haven account yet, or has one but has not
4996
5354
 
4997
5355
  ## What Haven is, in three sentences
4998
5356
 
4999
- 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.
5000
5358
 
5001
5359
  ## The sequence
5002
5360
 
5003
5361
  Four of the six steps are your user's \u2014 each needs a human signature or a human decision. The rest is yours. If they already have a funded account, start at step 3.
5004
5362
 
5005
- 1. **HUMAN \u2014 create the account.** Name, email, password, then a passkey (Face ID / Touch ID) or a wallet. Never offer to enter any of it: you must not have their password, and the passkey is bound to their device. With a terminal, \`<packages.cli.one_liner> login --api <api-url>\` afterwards gets you a scoped session for steps 3-4 (that flag or \`HAVEN_API_URL\`, on the first command \u2014 the session then remembers the backend; **the CLI's built-in default is Haven's hosted production backend**, so on any other deployment an omitted flag connects you somewhere real and wrong rather than failing) \u2014 they approve a code in the browser, you never hold their password. 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. Do not hold the process open while you wait: under \`--json\`, pass \`--no-wait\` to get the link object back at once, then poll it with \`haven login --poll <device_code>\` \u2014 one round per invocation, exit 3 while it is still pending, 0 once approved. It can set up agents and read the account; it cannot sign, approve a budget, move funds, or rotate any agent's keys.
5363
+ 1. **HUMAN \u2014 create the account.** Name, email, password, then a passkey or a wallet. Never offer to enter any of it: you must not have their password, and the passkey is bound to their device. With a terminal, \`<packages.cli.one_liner> login --api <api-url>\` afterwards gets you a scoped session for steps 3-4 (that flag or \`HAVEN_API_URL\`, on the first command \u2014 the session then remembers the backend; **the CLI's built-in default is Haven's hosted production backend**, so on any other deployment an omitted flag connects you somewhere real and wrong rather than failing) \u2014 they approve a code in the browser, you never hold their password. 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. Do not hold the process open while you wait: under \`--json\`, pass \`--no-wait\` to get the link object back at once, then poll it with \`haven login --poll <device_code>\` \u2014 one round per invocation, exit 3 while it is still pending, 0 once approved. It can set up agents and read the account; it cannot sign, approve a budget, move funds, or rotate any agent's keys.
5006
5364
  2. **HUMAN \u2014 fund it.** USDC only, no ETH: Haven sponsors the gas. \`/.well-known/haven.json\` names \`chains.default\` as the deployment's expected chain, but after login \`haven wallets funding\` prints the address, the amount and which chain in one place; confirm that chain before you message your user. Without a CLI session, the dashboard's funding card shows the address and amount and its Receive-funds screen names the chain; never assume one: a testnet deployment and production both call themselves Haven. Before you write that message, read the manifest: \`environment\` says whether this deployment is \`production\`, and each \`chains.supported\` entry says whether that chain is a \`testnet\`. Real money is at stake only on a non-testnet chain of a \`production\` deployment \u2014 tell your user which case theirs is.
5007
5365
  3. **HUMAN \u2014 create the agent, set its budget**, and paste you the **setup prompt** it hands back. With a CLI session (step 1) you can do this step yourself: \`haven agents connect --name <n> --budget <amount> --token USDC --period <minutes>\` prints the same connector command and approval link; add \`--run\` to do step 4 too.
5008
5366
  4. **YOU \u2014 run the connector command** in that prompt (see What you run). It makes your signing key locally, registering only the public address.
@@ -5026,7 +5384,7 @@ Send these as your own message, \`<host>\` replaced by the host you fetched this
5026
5384
 
5027
5385
  **Before signup** (step 1):
5028
5386
 
5029
- > I can do everything except the parts that need your signature. Please open \`<host>/signup?next=/agents&via=agent\` \u2014 name, email, password, then a passkey (Face ID / Touch ID) or a wallet. That is your account's key: I should not have it, and I will never ask for your password. Already have one? \`<host>/login?next=/agents\`.
5387
+ > I can do everything except the parts that need your signature. Please open \`<host>/signup?next=/agents&via=agent\` \u2014 name, email, password, then a passkey or a wallet. That is your account's key: I should not have it, and I will never ask for your password. Already have one? \`<host>/login?next=/agents\`.
5030
5388
 
5031
5389
  **At the passkey step:**
5032
5390
 
@@ -5069,7 +5427,7 @@ If the approval wait times out, nothing is lost \u2014 your agent is registered
5069
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\`:
5070
5428
 
5071
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.
5072
- - \`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).
5073
5431
  - \`revoked\` \u2014 the credential is not active; ask your user to create a new agent.
5074
5432
 
5075
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.
@@ -5105,7 +5463,7 @@ Then:
5105
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.
5106
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.
5107
5465
  3. ${AGENT_APPROVAL_RELAY_JSON_SENTENCE}
5108
- 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.
5109
5467
 
5110
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.
5111
5469
 
@@ -5225,8 +5583,10 @@ Do not guess the wallet address, network, or budget.
5225
5583
 
5226
5584
  For instant orientation at the start of a session, read the non-secret
5227
5585
  \`agent.json\` the connector wrote to your Haven credential directory (typically
5228
- \`~/.haven/agents/<agent-id>/agent.json\` \u2014 if you don't know the agent id, list
5229
- \`~/.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
5230
5590
  holds your agent id, Haven wallet address, network, and *configured* per-token
5231
5591
  budget, and contains no keys \u2014 the fastest way to answer "who am I and what may
5232
5592
  I spend" with no round trip. If that file is absent (some setups don't write
@@ -5247,7 +5607,13 @@ spending:
5247
5607
  the user, not a refusal \u2014 a budget above the balance is a normal setup, so
5248
5608
  still try the payment; \`null\` means the coverage read failed or the remaining
5249
5609
  figure was not read live; the key is absent when the remaining is 0. Rows for
5250
- 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.
5251
5617
  - \`mcp__haven__haven_get_allowances\` \u2014 detailed per-token breakdown
5252
5618
  (configured, spent, reset window) when you need more than the summary.
5253
5619
  - \`mcp__haven__haven_check_funds\` \u2014 whether the account actually HOLDS at
@@ -5394,10 +5760,13 @@ request, this is the only way it can learn the purchase failed \u2014 without it
5394
5760
  failed purchase reads as complete for fifteen minutes. If the merchant's
5395
5761
  \`PAYMENT-RESPONSE\` header names a \`transaction\`, pass that raw header as
5396
5762
  \`payment_response\` on the SAME \`mcp__haven__haven_report_x402_outcome\`
5397
- call: Haven decodes it, verifies the settlement on-chain, and the receipt then
5398
- shows the merchant's settlement, not only the funding transaction. Call
5763
+ call, together with \`payment_header\`: the exact header you retried the
5764
+ merchant with. Haven records a reported settlement only with that header,
5765
+ which proves the settlement is this payment's. It decodes the response,
5766
+ verifies the settlement on-chain, and the receipt then shows the merchant's
5767
+ settlement, not only the funding transaction. Call
5399
5768
  \`mcp__haven__haven_report_settlement_evidence\` only when the outcome answer
5400
- names it as the next step. On the **erc7710** scheme (the pay result says
5769
+ names it as the next step, and pass the same \`payment_header\` there too. On the **erc7710** scheme (the pay result says
5401
5770
  \`settlement_scheme: "erc7710"\`), sign with \`mcp__haven-signer__haven_sign\`,
5402
5771
  then \`mcp__haven__haven_submit\` with \`settlement_scheme: "erc7710"\`
5403
5772
  returns the \`payment_header\`; retry the merchant yourself with it as
@@ -5427,7 +5796,9 @@ for example, an invoice carrying an \`x402_payment_url\` to POST with its
5427
5796
  \`invoice_id\`, beside a raw deposit address and a web payment link.
5428
5797
  - **Quote the exact request:** \`mcp__haven__haven_quote_x402\` with \`url\`,
5429
5798
  \`method\`, \`headers\` and \`body\`, exactly as the merchant described it.
5430
- \`body\` is a JSON **string**, not an object. On a JSON POST always pass
5799
+ \`body\` is a JSON **string**, not an object. When the price is stated up
5800
+ front (an invoice total, say), also pass \`expected_amount\` \u2014 the price you
5801
+ expect, in whole tokens (\`"0.10"\`). On a JSON POST always pass
5431
5802
  \`headers: {"Content-Type": "application/json"}\` \u2014 the tool does not infer
5432
5803
  it, and a merchant may answer a request without it with a generic challenge
5433
5804
  Haven cannot pay (for example, only the \`upto\` scheme).
@@ -5456,6 +5827,18 @@ for example, an invoice carrying an \`x402_payment_url\` to POST with its
5456
5827
  report it, the merchant's settlement evidence. (A deposit transfer is bound
5457
5828
  to its payee and amount on-chain too; the difference is the record, not
5458
5829
  the binding.)
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.
5834
+ When it returns \`safe_to_continue: false\` \u2014 the amount exceeds the budget, or
5835
+ the price does not match what you expected \u2014 it prefills no cap and names no
5836
+ next tool: stop and tell the user. If the x402 challenge itself is wrong (a
5837
+ price wildly off the invoice) and NOTHING has been paid via x402, the
5838
+ invoice's own deposit address MAY be an alternative, but only with the
5839
+ user's explicit go-ahead \u2014 never automatically. Once an x402 payment has
5840
+ been attempted or refused, never pay the deposit address: it can pay the
5841
+ invoice twice.
5459
5842
  - **Pay exactly one route, and never fall back silently.** If the x402 route
5460
5843
  is refused or fails, do not then pay the deposit address \u2014 that pays the
5461
5844
  invoice twice. Follow the result's guidance fields instead: on EIP-3009, a
@@ -5592,8 +5975,8 @@ only part of it was usable), and an optional \`note\` (max 2000 characters)
5592
5975
  saying what was wrong. It is evidence only: it moves no money, never changes
5593
5976
  \`settled\` or any money field, and works only on your own settled
5594
5977
  payments \u2014 another agent's payment is refused. A re-report replaces your
5595
- earlier verdict; the receipt then carries it beside the payment, so the
5596
- 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.
5597
5980
  ### Relay a delivered code or credential to the owner \u2014 immediately, verbatim
5598
5981
 
5599
5982
  If a merchant response carries what the user PAID FOR \u2014 a redemption code, gift