@haven_ai/sdk 0.10.0-alpha.0 → 0.11.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 (36) hide show
  1. package/README.md +8 -5
  2. package/dist/{base64-CiR5BJhk.d.cts → base64-D4VZhgUg.d.ts} +2 -2
  3. package/dist/{base64-CxkinHPQ.d.ts → base64-DR4Gl8cD.d.cts} +2 -2
  4. package/dist/{chunk-EWTHDEIW.js → chunk-34YJQFCM.js} +3 -3
  5. package/dist/{chunk-EWTHDEIW.js.map → chunk-34YJQFCM.js.map} +1 -1
  6. package/dist/{chunk-6LEXYQ7J.cjs → chunk-DXBAN4SN.cjs} +4 -4
  7. package/dist/{chunk-6LEXYQ7J.cjs.map → chunk-DXBAN4SN.cjs.map} +1 -1
  8. package/dist/{chunk-TD2PHUYP.js → chunk-I7SL4A6X.js} +19 -3
  9. package/dist/chunk-I7SL4A6X.js.map +1 -0
  10. package/dist/{chunk-22FJ4JQO.cjs → chunk-IKQIIMM6.cjs} +35 -35
  11. package/dist/{chunk-22FJ4JQO.cjs.map → chunk-IKQIIMM6.cjs.map} +1 -1
  12. package/dist/{chunk-H37YVYZA.cjs → chunk-MZKLAHBK.cjs} +19 -3
  13. package/dist/chunk-MZKLAHBK.cjs.map +1 -0
  14. package/dist/{chunk-MLUWABVT.js → chunk-ZDJ55LR7.js} +4 -4
  15. package/dist/{chunk-MLUWABVT.js.map → chunk-ZDJ55LR7.js.map} +1 -1
  16. package/dist/edge.cjs +93 -93
  17. package/dist/edge.d.cts +4 -4
  18. package/dist/edge.d.ts +4 -4
  19. package/dist/edge.js +4 -4
  20. package/dist/index.cjs +755 -450
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.d.cts +55 -26
  23. package/dist/index.d.ts +55 -26
  24. package/dist/index.js +439 -134
  25. package/dist/index.js.map +1 -1
  26. package/dist/{receipt-Cg9a5Seq.d.ts → receipt-CbD8L0WL.d.ts} +1 -1
  27. package/dist/{receipt-q7ez8Zb2.d.cts → receipt-_bWCELEH.d.cts} +1 -1
  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.cts → types-DKmvjXK6.d.cts} +65 -10
  33. package/dist/{types-BKgqzvnk.d.ts → types-DKmvjXK6.d.ts} +65 -10
  34. package/package.json +1 -1
  35. package/dist/chunk-H37YVYZA.cjs.map +0 -1
  36. 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-ZDJ55LR7.js';
2
+ export { DEFAULT_NEXT_TOOL_BY_ACTION, ERC7710_ASSET_TRANSFER_METHOD, HAVEN_CLIENT_HEADER, HAVEN_MINIMUM_NODE_VERSION, HavenTypedDataRefusedError, MAX_TASK_BUDGET_TTL_SECONDS, NEXT_TOOL_SERVER_NAMES, NEXT_TOOL_SERVER_ROLES, SDK_CLIENT_IDENTITY, SDK_VERSION, SWEEP_BASE_CHAIN_ID, SWEEP_BASE_SEPOLIA_CHAIN_ID, SWEEP_BASE_SEPOLIA_USDC_ADDRESS, SWEEP_BASE_USDC_ADDRESS, TRANSFER_WITH_AUTHORIZATION_TYPES, TYPED_DATA_NOT_ALLOWED, X402PaymentHeaderValidationError, X402_LEGACY_PAYMENT_HEADER_NAME, X402_MAX_AUTHORIZATION_WINDOW_SECONDS, X402_PAYMENT_HEADER_NAME, X402_PAYMENT_HEADER_NAMES_SENT, X402_PAYMENT_NONCE_TAG, X402_PAYMENT_REQUIRED_HEADER_NAME, X402_PAYMENT_RESPONSE_HEADER_NAME, X402_SETTLEMENT_FORWARD_MARGIN_SECONDS, assertBoundDirectPaymentUserOp, assertOwnTaskBudgetCloseUserOp, assertOwnTaskChild, buildSweepAuthorizationMessage, buildSweepTypedData, buildX402ExpectedMessage, compareNodeVersions, createNextStepBuilder, decodeBase64Json, decodeBase64Utf8, defaultNextToolFor, deriveX402PaymentNonce, encodeBase64Json, encodeBase64Utf8, encodePaymentProof, hashDelegation, havenClientIdentity, isErc7710Option, isSupportedNodeVersion, isSweepableChain, isTaskChildTypedData, normalizePaymentRequired, parseNextTool, parsePaymentRequired, parsePaymentRequiredResponse, readClientUpdate, renderNextTool, resolveTokenBySymbol, resolveTokenFromAddress, selectErc7710PaymentOption, selectPaymentOption, selectStandardPaymentOption, selectX402SettlementScheme, sweepUsdcAddress, sweepUsdcDomain, toStandardPaymentRequirements, unsupportedNodeVersionMessage, validateStandardX402PaymentHeader, x402AssetTransferMethod, x402AuthorizationAmount, x402FacilitatorAddresses, x402PaymentHeaderNamesFor, x402RetryHeadersFor, x402V2PaymentEnvelope } from './chunk-ZDJ55LR7.js';
3
+ import { verifyPaymentReceipt } from './chunk-34YJQFCM.js';
4
+ export { RECEIPT_VERSION, verifyPaymentReceipt } from './chunk-34YJQFCM.js';
5
+ import { HavenSigningError, HavenError, DEFAULT_CONFIRMATION_TIMEOUT_MS, HavenApiError, assertUserOpTypedDataBinding, AgentPaymentRail, AgentPaymentPhase, HavenTimeoutError, X402TaskBudgetMismatchError, X402UnexpectedStatusError, X402AlreadySettledError, MerchantTimeoutError, AgentPaymentWarningCode, AgentPaymentNextAction, HavenPaymentStateError, HavenZeroSettlementHashError, X402Erc7710AlreadySettledError, chainIdForNetwork } from './chunk-I7SL4A6X.js';
6
+ export { AGENT_PAYMENT_FAILURE_CODE_VALUES, AGENT_PAYMENT_NEXT_ACTION_VALUES, AGENT_PAYMENT_PHASE_VALUES, AGENT_PAYMENT_RAIL_VALUES, AgentPaymentFailureCode, AgentPaymentFailureCodeDescriptions, AgentPaymentFailureCodeSchema, AgentPaymentNextAction, AgentPaymentNextActionDescriptions, AgentPaymentNextActionSchema, AgentPaymentPhase, AgentPaymentPhaseDescriptions, AgentPaymentPhaseSchema, AgentPaymentRail, AgentPaymentRailDescriptions, AgentPaymentRailSchema, AgentPaymentWarningCode, CONNECTOR_PACKAGE_NAME, DEFAULT_CONFIRMATION_TIMEOUT_MS, DIRECT_SIGN_CONTEXT_VERSION, ENTRY_POINT_V07, HAVEN_CONNECTOR_CHANNEL, HYBRID_DELEGATOR_DOMAIN_NAME, HYBRID_DELEGATOR_DOMAIN_VERSION, HavenApiError, HavenError, HavenPaymentStateError, HavenSigningError, HavenTimeoutError, HavenUnsupportedSignerVersionError, HavenUserOpBindingError, HavenZeroSettlementHashError, MerchantTimeoutError, PACKED_USER_OPERATION_FIELDS, ROOT_AUTHORITY, SIGNER_UPDATE_FALLBACK, SignerRefusalCode, TASK_SIGN_CONTEXT_VERSION, X402AlreadySettledError, X402Erc7710AlreadySettledError, X402TaskBudgetMismatchError, X402UnexpectedStatusError, assertUserOpTypedDataBinding, connectorRerunCommand, connectorSpec, connectorUpgradeCommand, deriveDelegateAccountAddress, isConnectorChannel, isPackedUserOperationTypedData, packedUserOperationHash, resolveConnectorChannel, signerUpdateFallback, verifySettlementChild } from './chunk-I7SL4A6X.js';
7
7
  import { hashTypedData } from 'viem';
8
8
  import { ethers } from 'ethers';
9
9
  import { AsyncLocalStorage } from 'async_hooks';
@@ -542,12 +542,30 @@ var MerchantEgressRefusedError = class extends HavenError {
542
542
  url;
543
543
  refusal;
544
544
  beforeRequest;
545
- constructor(message, url, refusal, beforeRequest) {
546
- super(message, MERCHANT_EGRESS_REFUSED_CODE, 400);
545
+ /** #4257: the redirect status that was not followed, when a redirect was refused. */
546
+ redirectStatus;
547
+ /**
548
+ * #4257: set only when `url` IS the redirect target (a cross-origin body
549
+ * re-send, a spent hop budget, a hop `assertUrl` refused). A refusal that
550
+ * stopped at the answering URL itself (a non-GET redirect, a missing or
551
+ * unresolvable Location) leaves it unset, and `url` is that answering URL.
552
+ */
553
+ redirectTarget;
554
+ /**
555
+ * #4257: set when the refusal interrupted a PAID delivery and the local
556
+ * completion has re-thrown it with the payment it belongs to — the funded,
557
+ * undelivered state, never "nothing happened".
558
+ */
559
+ settlementScheme;
560
+ constructor(message, url, refusal, beforeRequest, extra = {}) {
561
+ super(message, MERCHANT_EGRESS_REFUSED_CODE, 400, extra.paymentId);
547
562
  this.name = "MerchantEgressRefusedError";
548
563
  this.url = url;
549
564
  this.refusal = refusal;
550
565
  this.beforeRequest = beforeRequest;
566
+ this.redirectStatus = extra.redirectStatus;
567
+ this.redirectTarget = extra.redirectTarget;
568
+ this.settlementScheme = extra.settlementScheme;
551
569
  }
552
570
  };
553
571
  var MERCHANT_EGRESS_RESPONSE_CAP_CODE = "MERCHANT_EGRESS_RESPONSE_CAP";
@@ -662,6 +680,29 @@ var MCP_X402_PAYMENT_META_KEY = "x402/payment";
662
680
  var MCP_X402_PAYMENT_RESPONSE_META_KEY = "x402/payment-response";
663
681
  var X402_RETRY_REJECTED_STATUS = 402;
664
682
  var SESSION_NOT_FOUND_NEXT_ACTION = "reinitialize_then_retry_same_payment_header";
683
+ var CROSS_ORIGIN_REDIRECT_HEADERS = /* @__PURE__ */ new Set([
684
+ "accept",
685
+ "accept-language",
686
+ "user-agent",
687
+ "x402-wallet"
688
+ ]);
689
+ function crossOriginRedirectHeaders(headers) {
690
+ const kept = new Headers();
691
+ for (const [name, value] of new Headers(headers)) {
692
+ if (CROSS_ORIGIN_REDIRECT_HEADERS.has(name)) kept.set(name, value);
693
+ }
694
+ return kept;
695
+ }
696
+ var NO_POLICY_MAX_REDIRECTS = 20;
697
+ var REQUEST_BODY_HEADERS = ["content-encoding", "content-language", "content-location", "content-type", "content-length"];
698
+ function redirectedInit(init, status) {
699
+ const method = (init.method ?? "GET").toUpperCase();
700
+ const toGet = (status === 301 || status === 302) && method === "POST" || status === 303 && method !== "GET" && method !== "HEAD";
701
+ if (!toGet) return init;
702
+ const headers = new Headers(init.headers);
703
+ for (const name of REQUEST_BODY_HEADERS) headers.delete(name);
704
+ return { ...init, method: "GET", body: null, headers };
705
+ }
665
706
  var McpMerchantTransport = class {
666
707
  merchantTimeout;
667
708
  fetchImpl;
@@ -692,11 +733,24 @@ var McpMerchantTransport = class {
692
733
  }
693
734
  /** Fetch a merchant with a settlement-sized timeout and caller cancellation. */
694
735
  async fetch(url, init = {}, timeoutMs = this.merchantTimeout, maxBytes) {
736
+ return this.send(url, init, timeoutMs, maxBytes, false);
737
+ }
738
+ /**
739
+ * `fetch`, plus whether this request is the paid delivery (#4257). A paid
740
+ * delivery, or any body carrying an x402 payment in `_meta`, is never
741
+ * re-sent to another origin by a redirect that keeps the body; without a policy every other
742
+ * request follows that redirect as the platform would, headers reduced.
743
+ */
744
+ async send(url, init, timeoutMs, maxBytes, paidDelivery) {
745
+ const carriesPayment = paidDelivery || bodyCarriesX402Payment(init.body);
695
746
  if (!this.egress) {
696
747
  const timeoutSignal = AbortSignal.timeout(timeoutMs);
697
748
  const signal = init.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal;
698
749
  try {
699
- return await this.fetchImpl(url, { ...init, signal });
750
+ if (init.redirect === "manual" || init.redirect === "error") {
751
+ return await this.fetchImpl(url, { ...init, signal });
752
+ }
753
+ return await this.followRedirects(url, init, signal, void 0, carriesPayment);
700
754
  } catch (err) {
701
755
  if (timeoutSignal.aborted) {
702
756
  throw new MerchantTimeoutError(`Merchant request timed out after ${timeoutMs}ms: ${url}`);
@@ -704,87 +758,144 @@ var McpMerchantTransport = class {
704
758
  throw err;
705
759
  }
706
760
  }
707
- return this.fetchUnderPolicy(url, init, timeoutMs, maxBytes);
761
+ return this.fetchUnderPolicy(url, init, timeoutMs, maxBytes, carriesPayment);
708
762
  }
709
763
  /**
710
764
  * #3747: the policy path. Every URL — the initial request and each redirect
711
765
  * hop — is `assertUrl`-checked BEFORE it connects; redirects run `manual`
712
766
  * 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
767
+ * `maxGetRedirects` (a redirect on any other method is refused); a hop to
768
+ * another origin carries only `CROSS_ORIGIN_REDIRECT_HEADERS` (#4244);
769
+ * response bodies are capped WHILE being read, not after buffering. The timeout
715
770
  * signal spans the WHOLE redirect chain, and `MerchantTimeoutError`
716
771
  * classification is preserved.
717
772
  */
718
- async fetchUnderPolicy(url, init, timeoutMs, maxBytes) {
773
+ async fetchUnderPolicy(url, init, timeoutMs, maxBytes, carriesPayment) {
719
774
  const egress = this.egress;
720
775
  try {
721
776
  egress.assertUrl(url);
722
777
  } catch (err) {
723
778
  throw asRefusal(err, url, true);
724
779
  }
725
- const method = (init.method ?? "GET").toUpperCase();
726
780
  const timeoutSignal = AbortSignal.timeout(timeoutMs);
727
781
  const signal = init.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal;
728
- const maxRedirects = egress.maxGetRedirects ?? 0;
782
+ let finalUrl = url;
783
+ try {
784
+ const response = await this.followRedirects(url, init, signal, egress, carriesPayment, (hopUrl) => {
785
+ finalUrl = hopUrl;
786
+ });
787
+ return this.capResponse(response, finalUrl, maxBytes);
788
+ } catch (err) {
789
+ if (timeoutSignal.aborted) {
790
+ throw new MerchantTimeoutError(`Merchant request timed out after ${timeoutMs}ms: ${finalUrl}`);
791
+ }
792
+ throw err;
793
+ }
794
+ }
795
+ /**
796
+ * The redirect follower both paths share (#3747, #4244, #4257). Redirects
797
+ * run `manual` and every hop is decided here:
798
+ *
799
+ * - Under a `policy`, every hop is `assertUrl`-checked before it connects,
800
+ * only GET redirects are followed (any other method is refused), up to
801
+ * `maxGetRedirects`; a missing or unresolvable Location is refused.
802
+ * - Without one, the platform's rules are kept: up to
803
+ * `NO_POLICY_MAX_REDIRECTS` hops, the Fetch spec's method/body rewrite,
804
+ * and a 3xx with no Location returned as the answer.
805
+ * - On both, a hop to another origin carries only
806
+ * `CROSS_ORIGIN_REDIRECT_HEADERS`, never restored on a later hop.
807
+ * - A hop that would re-send a request body to another origin is refused
808
+ * under a policy (which follows GET alone, so this is unreachable there),
809
+ * and without one only when `carriesPayment`: the paid delivery, or a body
810
+ * carrying an x402 payment in `_meta` (#4257). Any other body-keeping redirect is
811
+ * followed with its body, as the platform did, headers reduced.
812
+ *
813
+ * `onHop` sees every URL a request goes to, so the caller can name the hop
814
+ * it was on when the chain stops.
815
+ */
816
+ async followRedirects(url, init, signal, policy, carriesPayment, onHop) {
817
+ const maxRedirects = policy ? policy.maxGetRedirects ?? 0 : NO_POLICY_MAX_REDIRECTS;
729
818
  let currentUrl = url;
819
+ let hopInit = init;
730
820
  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;
821
+ while (true) {
822
+ onHop?.(currentUrl);
823
+ const response = await this.fetchImpl(currentUrl, {
824
+ ...hopInit,
825
+ redirect: init.redirect === "error" ? "error" : "manual",
826
+ signal
827
+ });
828
+ if (!isRedirectStatus(response.status)) return response;
829
+ const method = (hopInit.method ?? "GET").toUpperCase();
830
+ if (policy && method !== "GET") {
831
+ throw new MerchantEgressRefusedError(
832
+ `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.`,
833
+ currentUrl,
834
+ "redirect_on_non_get",
835
+ false,
836
+ { redirectStatus: response.status }
837
+ );
838
+ }
839
+ const location = response.headers.get("location");
840
+ if (!location) {
841
+ if (!policy) return response;
842
+ throw new MerchantEgressRefusedError(
843
+ `Hosted egress policy refuses the ${response.status} redirect from ${currentUrl}: it carries no Location header, so there is no checked target to follow.`,
844
+ currentUrl,
845
+ "redirect_missing_location",
846
+ false,
847
+ { redirectStatus: response.status }
848
+ );
849
+ }
850
+ let next;
851
+ try {
852
+ next = new URL(location, currentUrl).toString();
853
+ } catch {
854
+ if (!policy) throw new TypeError(`fetch failed: the redirect from ${currentUrl} has an unresolvable Location header`);
855
+ throw new MerchantEgressRefusedError(
856
+ `Hosted egress policy refuses the redirect from ${currentUrl}: its Location header is not a resolvable URL.`,
857
+ currentUrl,
858
+ "redirect_invalid_location",
859
+ false,
860
+ { redirectStatus: response.status }
861
+ );
862
+ }
863
+ if (!policy && !/^https?:$/.test(new URL(next).protocol)) {
864
+ throw new TypeError(`fetch failed: the redirect from ${currentUrl} names a non-HTTP(S) URL`);
865
+ }
866
+ if (hops >= maxRedirects) {
867
+ if (!policy) throw new TypeError(`fetch failed: redirect count exceeded (${maxRedirects}) at ${currentUrl}`);
868
+ throw new MerchantEgressRefusedError(
869
+ `Hosted egress policy refuses the redirect from ${currentUrl} to ${next}: the ${maxRedirects}-redirect budget for this request is spent.`,
870
+ next,
871
+ "redirect_budget_exceeded",
872
+ false,
873
+ { redirectStatus: response.status, redirectTarget: next }
874
+ );
875
+ }
876
+ if (policy) {
757
877
  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
- );
878
+ policy.assertUrl(next);
879
+ } catch (err) {
880
+ throw asRefusal(err, next, false, { redirectStatus: response.status, redirectTarget: next });
766
881
  }
767
- if (hops >= maxRedirects) {
882
+ }
883
+ hopInit = redirectedInit(hopInit, response.status);
884
+ if (new URL(next).origin !== new URL(currentUrl).origin) {
885
+ if (hopInit.body != null && (policy || carriesPayment)) {
768
886
  throw new MerchantEgressRefusedError(
769
- `Hosted egress policy refuses the redirect from ${currentUrl} to ${next}: the ${maxRedirects}-redirect budget for this request is spent.`,
887
+ 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
888
  next,
771
- "redirect_budget_exceeded",
772
- false
889
+ "redirect_cross_origin_body",
890
+ false,
891
+ { redirectStatus: response.status, redirectTarget: next }
773
892
  );
774
893
  }
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}`);
894
+ hopInit = { ...hopInit, headers: crossOriginRedirectHeaders(hopInit.headers) };
786
895
  }
787
- throw err;
896
+ await response.body?.cancel().catch(() => void 0);
897
+ hops++;
898
+ currentUrl = next;
788
899
  }
789
900
  }
790
901
  /**
@@ -962,7 +1073,7 @@ var McpMerchantTransport = class {
962
1073
  if (send.includes(name)) headers.set(name, paymentHeader);
963
1074
  else headers.delete(name);
964
1075
  }
965
- return this.fetch(url, withMcpPaymentMeta({ ...init, headers }, paymentHeader), this.budgetFor("delivery"), this.capFor("delivery"));
1076
+ return this.send(url, withMcpPaymentMeta({ ...init, headers }, paymentHeader), this.budgetFor("delivery"), this.capFor("delivery"), true);
966
1077
  }
967
1078
  /**
968
1079
  * #3171: deliver the payment; if the merchant answers its unknown-session
@@ -1099,6 +1210,27 @@ function withMcpPaymentMeta(init, paymentHeader) {
1099
1210
  function encodeMcpSettlementReceipt(settlement) {
1100
1211
  return encodeBase64Json(settlement);
1101
1212
  }
1213
+ function bodyCarriesX402Payment(body) {
1214
+ let text;
1215
+ if (typeof body === "string") text = body;
1216
+ else if (body instanceof Uint8Array || body instanceof ArrayBuffer) {
1217
+ try {
1218
+ text = new TextDecoder("utf-8", { fatal: true }).decode(body);
1219
+ } catch {
1220
+ return false;
1221
+ }
1222
+ } else return false;
1223
+ let request;
1224
+ try {
1225
+ request = JSON.parse(text);
1226
+ } catch {
1227
+ return false;
1228
+ }
1229
+ return Array.isArray(request) ? request.some(messageCarriesX402Payment) : messageCarriesX402Payment(request);
1230
+ }
1231
+ function messageCarriesX402Payment(message) {
1232
+ return isRecord(message) && isRecord(message.params) && isRecord(message.params._meta) && message.params._meta[MCP_X402_PAYMENT_META_KEY] !== void 0;
1233
+ }
1102
1234
  function parseJsonRpcToolsCall(body) {
1103
1235
  if (typeof body !== "string") return void 0;
1104
1236
  let request;
@@ -1137,10 +1269,10 @@ function isRecord(value) {
1137
1269
  function isRedirectStatus(status) {
1138
1270
  return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
1139
1271
  }
1140
- function asRefusal(err, url, beforeRequest) {
1272
+ function asRefusal(err, url, beforeRequest, redirect) {
1141
1273
  if (!(err instanceof MerchantEgressRefusedError)) return err;
1142
- if (err.beforeRequest === beforeRequest) return err;
1143
- return new MerchantEgressRefusedError(err.message, url, err.refusal, beforeRequest);
1274
+ if (err.beforeRequest === beforeRequest && !redirect) return err;
1275
+ return new MerchantEgressRefusedError(err.message, url, err.refusal, beforeRequest, redirect);
1144
1276
  }
1145
1277
  async function captureMerchantResponse(response) {
1146
1278
  const merchant_body = await response.text().catch((err) => {
@@ -1260,6 +1392,7 @@ function mapTaskBudget(raw) {
1260
1392
  status: raw.status,
1261
1393
  expiresAt: raw.expires_at,
1262
1394
  isExpired: raw.is_expired,
1395
+ ...raw.is_live === void 0 ? {} : { isLive: raw.is_live },
1263
1396
  createdAt: raw.created_at,
1264
1397
  openedAt: raw.opened_at,
1265
1398
  closedAt: raw.closed_at,
@@ -1413,7 +1546,8 @@ var AccountReads = class {
1413
1546
  * an agent can still act on, and read-by-id (`haven_get_task_budget`) is
1414
1547
  * the any-status surface for everything else:
1415
1548
  *
1416
- * - `closing` ALWAYS stays visible, expired or not — the close submit is
1549
+ * - `closing` stays visible until its expiry (#3773; an expired `closing`
1550
+ * row drops, #3923) — the close submit is
1417
1551
  * in flight and the signer's close refusal tells the caller to re-check
1418
1552
  * exactly this row's status;
1419
1553
  * - `closed` rows drop (terminal — `closed_at` names what happened, and
@@ -1422,10 +1556,14 @@ var AccountReads = class {
1422
1556
  * budget reserves nothing and pays nothing);
1423
1557
  * - unexpired `pending` and `open` rows stay.
1424
1558
  *
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
1559
+ * A filter also runs here, client-side. A row carrying the backend's
1560
+ * `is_live` (#3922) is kept exactly when it is true — the server's own
1561
+ * `status=live` verdict. A row without it (an older backend) is derived by
1562
+ * the same rule: `closing` stays until its expiry, then drops (#3923), as
1563
+ * the backend does since #3773. That is
1564
+ * what keeps the list bounded against a backend that predates `status=live`,
1565
+ * which refuses the value with a 400 — that refusal (and only that) falls
1566
+ * back to `status=all`. A row whose wire carries
1429
1567
  * no `is_expired` is treated as unexpired — the degraded read errs toward
1430
1568
  * KEEPING rows, never toward hiding one the agent could still act on.
1431
1569
  */
@@ -1439,7 +1577,9 @@ var AccountReads = class {
1439
1577
  });
1440
1578
  const rows = raw?.task_budgets;
1441
1579
  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));
1580
+ return rows.map((row) => mapTaskBudget(row)).filter(
1581
+ (tb) => tb.isLive !== void 0 ? tb.isLive : !tb.isExpired && tb.status !== "closed"
1582
+ ).map((tb) => summarizeTaskBudget(tb));
1443
1583
  } catch {
1444
1584
  return [];
1445
1585
  }
@@ -2615,6 +2755,26 @@ function toolError(err) {
2615
2755
  };
2616
2756
  }
2617
2757
 
2758
+ // src/merchant-answer.ts
2759
+ function decidePostFundingMerchantAnswer(input) {
2760
+ const inBandChallenge = input.toolResult ? extractMcpPaymentRequired(input.toolResult) : void 0;
2761
+ const headerReceipt = input.paymentResponseHeader;
2762
+ const protocolReceiptHeaderName = headerReceipt ? "PAYMENT-RESPONSE" : input.metaSettlement ? `_meta.${MCP_X402_PAYMENT_RESPONSE_META_KEY}` : void 0;
2763
+ const protocolReceiptHeader = headerReceipt ?? (input.metaSettlement ? encodeMcpSettlementReceipt(input.metaSettlement) : void 0);
2764
+ const settlement = parseMerchantSettlement(headerReceipt ?? null);
2765
+ const settlementTxHash = settlement.settlementTxHash ?? (typeof input.metaSettlement?.transaction === "string" ? input.metaSettlement.transaction : void 0);
2766
+ const plainToolErrorAfterFunding = input.toolResult?.isError === true && inBandChallenge === void 0 && !settlementTxHash;
2767
+ const inBandRejection = inBandChallenge !== void 0 || input.metaSettlement?.success === false || plainToolErrorAfterFunding;
2768
+ return {
2769
+ accepted: input.httpOk && !inBandRejection,
2770
+ inBandRejection,
2771
+ plainToolErrorAfterFunding,
2772
+ settlementTxHash,
2773
+ protocolReceiptHeader,
2774
+ protocolReceiptHeaderName
2775
+ };
2776
+ }
2777
+
2618
2778
  // src/merchant-completion.ts
2619
2779
  var MERCHANT_BODY_SNIPPET_LIMIT = 1e3;
2620
2780
  var EVIDENCE_RETRY_DELAYS_MS = [1e3, 2e3, 4e3];
@@ -2664,16 +2824,31 @@ var MerchantCompletion = class {
2664
2824
  retryInit = withTaxDeclarationHeader(initialInit, void 0);
2665
2825
  }
2666
2826
  }
2667
- const retryResponse = await this.merchantTransport.deliverPaymentRecoveringSession(
2668
- url,
2669
- retryInit,
2670
- receipt.paymentHeader,
2671
- () => this.merchantTransport.initialize(url, initialInit)
2672
- );
2827
+ let retryResponse;
2828
+ try {
2829
+ retryResponse = await this.merchantTransport.deliverPaymentRecoveringSession(
2830
+ url,
2831
+ retryInit,
2832
+ receipt.paymentHeader,
2833
+ () => this.merchantTransport.initialize(url, initialInit)
2834
+ );
2835
+ } catch (err) {
2836
+ if (err instanceof MerchantEgressRefusedError && !err.beforeRequest) {
2837
+ throw await this.paidDeliveryInterrupted(err, url, receipt, receipt.paymentHeader, retryInit?.body != null);
2838
+ }
2839
+ throw err;
2840
+ }
2673
2841
  const toolResult = retryResponse.ok ? await this.merchantTransport.readToolResult(retryResponse) : void 0;
2674
- const inBandChallenge = toolResult ? extractMcpPaymentRequired(toolResult) : void 0;
2675
2842
  const metaSettlement = toolResult ? mcpSettlementFromToolResult(toolResult) : void 0;
2676
- const inBandRejection = inBandChallenge !== void 0 || metaSettlement?.success === false;
2843
+ const headerReceipt = retryResponse.headers.get("PAYMENT-RESPONSE");
2844
+ const answer = decidePostFundingMerchantAnswer({
2845
+ toolResult,
2846
+ httpOk: retryResponse.ok,
2847
+ paymentResponseHeader: headerReceipt ?? void 0,
2848
+ metaSettlement
2849
+ });
2850
+ const reportedSettlementTxHash = answer.settlementTxHash;
2851
+ const inBandRejection = answer.inBandRejection;
2677
2852
  if (!retryResponse.ok || inBandRejection) {
2678
2853
  const merchant = await captureMerchantResponse(retryResponse);
2679
2854
  await this.recordRetryRejected({
@@ -2688,7 +2863,7 @@ var MerchantCompletion = class {
2688
2863
  }
2689
2864
  });
2690
2865
  throw new HavenApiError(
2691
- "x402 retry failed after Haven funded the delegate wallet; reconciliation may be required.",
2866
+ "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
2867
  inBandRejection ? X402_RETRY_REJECTED_STATUS : merchant.merchant_status,
2693
2868
  {
2694
2869
  marker: "x402_retry_rejected_after_funding",
@@ -2701,10 +2876,10 @@ var MerchantCompletion = class {
2701
2876
  }
2702
2877
  );
2703
2878
  }
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);
2879
+ const { protocolReceiptHeader, protocolReceiptHeaderName } = answer;
2880
+ const merchantSettlement = {
2881
+ settlementTxHash: reportedSettlementTxHash ?? null
2882
+ };
2708
2883
  if (receipt.merchant && merchantSettlement.settlementTxHash) {
2709
2884
  receipt.merchant.settlementTxHash = merchantSettlement.settlementTxHash;
2710
2885
  receipt.merchant.settlementExplorerUrl = buildExplorerUrl(receipt.chainId, merchantSettlement.settlementTxHash) || null;
@@ -2724,7 +2899,12 @@ var MerchantCompletion = class {
2724
2899
  });
2725
2900
  const settlementCandidate = merchantSettlement.settlementTxHash;
2726
2901
  if (settlementCandidate && isWellFormedSettlementTxHash(settlementCandidate) && !isZeroSettlementTxHash(settlementCandidate) && settlementCandidate.toLowerCase() !== receipt.txHash.toLowerCase()) {
2727
- await this.reportSettlementEvidenceOnce(receipt.paymentId, settlementCandidate);
2902
+ await this.reportEvidence({
2903
+ paymentId: receipt.paymentId,
2904
+ rail: "x402",
2905
+ txHash: settlementCandidate,
2906
+ ...settlementProofFields(receipt.paymentHeader)
2907
+ });
2728
2908
  }
2729
2909
  await this.reportMerchantReceipt(receipt.paymentId, retryResponse);
2730
2910
  return retryResponse;
@@ -2827,6 +3007,51 @@ var MerchantCompletion = class {
2827
3007
  // and MACHINE-PAYMENT-CHALLENGE was never produced by any other Haven
2828
3008
  // surface. Use the x402 flow (authorizeX402 / fetch / quoteX402 / payX402Quote)
2829
3009
  // for agent-to-merchant payments.
3010
+ /**
3011
+ * #4257: record a paid delivery that a refused redirect interrupted, and
3012
+ * return the refusal re-thrown with its payment and an accurate account:
3013
+ * the payment header (and the body, when there is one) reached `url`, the
3014
+ * merchant answered with a redirect Haven did not follow, and the merchant
3015
+ * may still settle. The record is the same
3016
+ * `merchant_retry_rejected_after_payment` event a non-2xx answer writes: its
3017
+ * `retry_status` is the redirect status, `retry_body` is null, and `details`
3018
+ * carry `refusal` plus `redirect_location` when the refusal names a redirect
3019
+ * target, or `refused_url` (the URL that answered) otherwise. The payment
3020
+ * reads as funded and undelivered.
3021
+ */
3022
+ async paidDeliveryInterrupted(err, url, receipt, paymentHeader, hasBody) {
3023
+ const scheme = x402PaymentHeaderNamesFor(paymentHeader).length > 1 ? "eip3009" : "erc7710";
3024
+ const status = err.redirectStatus ?? 0;
3025
+ await this.recordRetryRejected({
3026
+ rail: "x402",
3027
+ paymentId: receipt.paymentId,
3028
+ txHash: receipt.txHash,
3029
+ resourceUrl: receipt.resourceUrl,
3030
+ merchant: {
3031
+ merchant_status: status,
3032
+ merchant_status_text: "redirect not followed",
3033
+ merchant_headers: {},
3034
+ merchant_body: ""
3035
+ },
3036
+ details: {
3037
+ merchant_to: receipt.merchantTo,
3038
+ delegate_to: receipt.to,
3039
+ refusal: err.refusal,
3040
+ // The refusal's url is the redirect target only when it carries one;
3041
+ // otherwise it is the URL that answered (a non-GET redirect, a missing
3042
+ // or unresolvable Location), recorded under a neutral key.
3043
+ ...err.redirectTarget ? { redirect_location: err.redirectTarget } : { refused_url: err.url }
3044
+ }
3045
+ });
3046
+ 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: `;
3047
+ 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.";
3048
+ return new MerchantEgressRefusedError(what + next, err.url, err.refusal, false, {
3049
+ redirectStatus: err.redirectStatus,
3050
+ redirectTarget: err.redirectTarget,
3051
+ paymentId: receipt.paymentId,
3052
+ settlementScheme: scheme
3053
+ });
3054
+ }
2830
3055
  async recordRetryRejected(input) {
2831
3056
  try {
2832
3057
  await this.post("/machine-payments/reconciliation-events", {
@@ -2976,10 +3201,11 @@ var MerchantCompletion = class {
2976
3201
  }
2977
3202
  }
2978
3203
  /**
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).
3204
+ * ONE evidence attempt — the body of `reportEvidence`'s loop above. A 503
3205
+ * answers `retryable` immediately; the caller decides whether to sleep and
3206
+ * re-attempt (`reportEvidence`, and — #4215 — the local settlement report
3207
+ * in `retryRequest`) or hand the outcome to its consumer (#3764 D2, the
3208
+ * hosted settlement report).
2983
3209
  */
2984
3210
  async postEvidenceOnce(body) {
2985
3211
  try {
@@ -2989,10 +3215,12 @@ var MerchantCompletion = class {
2989
3215
  const statusCode = err instanceof HavenApiError ? err.statusCode : void 0;
2990
3216
  if (statusCode !== EVIDENCE_RETRYABLE_STATUS) {
2991
3217
  const reason = refusalReasonFromBody(err);
3218
+ const errorCode = refusalErrorCodeFromBody(err);
2992
3219
  return {
2993
3220
  outcome: "refused",
2994
3221
  statusCode: statusCode ?? 0,
2995
- ...reason ? { reason } : {}
3222
+ ...reason ? { reason } : {},
3223
+ ...errorCode ? { errorCode } : {}
2996
3224
  };
2997
3225
  }
2998
3226
  return { outcome: "retryable", statusCode };
@@ -3029,39 +3257,64 @@ var MerchantCompletion = class {
3029
3257
  * gate uses, so a caller cannot "fix" a missing hash by reporting the demo
3030
3258
  * merchant's own marker and getting a different verdict than the settle
3031
3259
  * path already gave it.
3260
+ *
3261
+ * #4061: `options.paymentHeader` is the payment header the caller sent to
3262
+ * the merchant on its paid retry. On eip3009 it is REQUIRED for the record
3263
+ * (#3925): its signed authorization proves the nonce the settlement burned
3264
+ * is this payment's, and a report without it is refused with
3265
+ * `errorCode: 'settlement_proof_header_missing'`. It is forwarded exactly
3266
+ * as the funding report forwards it (`paymentProofHeader` plus the names
3267
+ * `x402PaymentHeaderNamesSent` derives from the same payload). erc7710
3268
+ * needs none — Haven assembled that header itself.
3032
3269
  */
3033
- async reportSettlementEvidence(paymentId, settlementTxHash) {
3270
+ async reportSettlementEvidence(paymentId, settlementTxHash, options = {}) {
3034
3271
  if (isZeroSettlementTxHash(settlementTxHash)) {
3035
3272
  throw new HavenZeroSettlementHashError(paymentId);
3036
3273
  }
3037
3274
  return this.reportEvidence({
3038
3275
  paymentId,
3039
3276
  rail: "x402",
3040
- txHash: settlementTxHash
3277
+ txHash: settlementTxHash,
3278
+ ...settlementProofFields(options.paymentHeader)
3041
3279
  });
3042
3280
  }
3043
3281
  /**
3044
3282
  * #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
3283
+ * after a completed hosted purchase — the second leg of a payment that
3284
+ * already has a funding-evidence row. Same endpoint and payload shape as
3047
3285
  * `reportSettlementEvidence` above, but with #3764's D2 budget: exactly ONE
3048
3286
  * attempt, no `EVIDENCE_RETRY_DELAYS_MS` backoff. The resource is already
3049
3287
  * paid for and delivered, so a retryable answer must not stretch the
3050
3288
  * 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
3289
+ * (`settlementEvidenceOutcome` → `haven_report_settlement_evidence`). Since
3290
+ * #4215 this ONE-attempt form serves the HOSTED path only
3291
+ * (`completeX402MerchantCall`); the local `retryRequest` posts the same
3292
+ * report through `reportEvidence`, whose bounded loop re-attempts while
3293
+ * Haven answers `retryable`. Callers validate the hash before this call
3294
+ * (well-formed, non-zero, different from the funding hash
3054
3295
  * case-insensitively), so there is no client-side refusal here — the
3055
3296
  * backend verifies on-chain and is the only judge.
3297
+ *
3298
+ * #4061: `paymentHeader` is the header the paid retry sent — the #3925
3299
+ * nonce proof, forwarded as `reportSettlementEvidence` forwards it. Both
3300
+ * callers hold it; without it the backend refuses the record.
3056
3301
  */
3057
- async reportSettlementEvidenceOnce(paymentId, settlementTxHash) {
3302
+ async reportSettlementEvidenceOnce(paymentId, settlementTxHash, paymentHeader) {
3058
3303
  return this.postEvidenceOnce({
3059
3304
  paymentId,
3060
3305
  rail: "x402",
3061
- txHash: settlementTxHash
3306
+ txHash: settlementTxHash,
3307
+ ...settlementProofFields(paymentHeader)
3062
3308
  });
3063
3309
  }
3064
3310
  };
3311
+ function settlementProofFields(paymentHeader) {
3312
+ if (!paymentHeader || !paymentHeader.trim()) return {};
3313
+ return {
3314
+ paymentProofHeaderName: x402PaymentHeaderNamesSent(paymentHeader),
3315
+ paymentProofHeader: paymentHeader
3316
+ };
3317
+ }
3065
3318
  function refusalReasonFromBody(err) {
3066
3319
  if (!(err instanceof HavenApiError)) return void 0;
3067
3320
  const body = err.body;
@@ -3069,6 +3322,13 @@ function refusalReasonFromBody(err) {
3069
3322
  const reason = body.reason;
3070
3323
  return typeof reason === "string" && reason.length > 0 ? reason : void 0;
3071
3324
  }
3325
+ function refusalErrorCodeFromBody(err) {
3326
+ if (!(err instanceof HavenApiError)) return void 0;
3327
+ const body = err.body;
3328
+ if (body === null || typeof body !== "object" || Array.isArray(body)) return void 0;
3329
+ const code = body.error_code;
3330
+ return typeof code === "string" && code.length > 0 ? code : void 0;
3331
+ }
3072
3332
  function isZeroSettlementTxHash(hash) {
3073
3333
  return typeof hash === "string" && /^0x0+$/i.test(hash);
3074
3334
  }
@@ -3089,6 +3349,25 @@ function parseProtocolReceiptHeader(value) {
3089
3349
  }
3090
3350
  }
3091
3351
  }
3352
+ function unfollowedRedirect(err) {
3353
+ const target = err.redirectTarget ?? err.url;
3354
+ switch (err.refusal) {
3355
+ case "redirect_cross_origin_body":
3356
+ return ` to a different origin (${target}). Haven did not follow it, so the payment was not re-sent there.`;
3357
+ case "redirect_on_non_get":
3358
+ return ". Under the egress policy only GET redirects are followed, so Haven did not follow it and nothing was sent to the redirect target.";
3359
+ case "redirect_missing_location":
3360
+ return " that named no target (no Location header). Haven did not follow it.";
3361
+ case "redirect_invalid_location":
3362
+ return " whose Location header is not a resolvable URL. Haven did not follow it.";
3363
+ case "redirect_budget_exceeded":
3364
+ return ` past the redirect budget. The chain stopped before ${target}, and nothing was sent there.`;
3365
+ case "url_not_allowed":
3366
+ return ` to a URL the egress policy does not allow (${target}). Haven did not follow it, so nothing was sent there.`;
3367
+ default:
3368
+ return ". Haven did not follow it.";
3369
+ }
3370
+ }
3092
3371
 
3093
3372
  // src/client.ts
3094
3373
  var DEFAULT_POLLING_INTERVAL = 3e3;
@@ -3648,7 +3927,8 @@ var HavenClient = class {
3648
3927
  amountAtomic: input.amountAtomic,
3649
3928
  ...input.merchantTo !== void 0 ? { merchantTo: input.merchantTo } : {},
3650
3929
  ...input.resourceUrl !== void 0 ? { resourceUrl: input.resourceUrl } : {},
3651
- ...input.idempotencyKey !== void 0 ? { idempotencyKey: input.idempotencyKey } : {}
3930
+ ...input.idempotencyKey !== void 0 ? { idempotencyKey: input.idempotencyKey } : {},
3931
+ ...input.recordRefusal !== void 0 ? { recordRefusal: input.recordRefusal } : {}
3652
3932
  });
3653
3933
  }
3654
3934
  /**
@@ -3755,10 +4035,10 @@ var HavenClient = class {
3755
4035
  return this.get(`/catalog/submit/${encodeURIComponent(id)}`);
3756
4036
  }
3757
4037
  /**
3758
- * Fetch one curated catalog entry by id (#1306).
4038
+ * Fetch one catalog entry by id (#1306).
3759
4039
  *
3760
4040
  * 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
4041
+ * agent-authenticated (#1299): an unknown id and an id listed for a
3762
4042
  * DIFFERENT chain than this agent's both 404 identically — this method does
3763
4043
  * not (and must not) re-filter by chain in JS. Read-only, like
3764
4044
  * {@link discoverTools}.
@@ -3914,9 +4194,10 @@ var HavenClient = class {
3914
4194
  return this.accountReads.listPendingSubBudgetSignatures();
3915
4195
  }
3916
4196
  /**
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
4197
+ * Close a sub-budget row (delegating agent only). A `pending` row, or an
4198
+ * `open` one already expired, closes immediately; otherwise (a live child, or
4199
+ * any `closing` row — its disable is still owed, #3923) it returns `signData`
4200
+ * for the on-chain revocation — sign it (`haven_sign` with `sub_budget_id`) and call
3920
4201
  * `submitSubBudget`.
3921
4202
  */
3922
4203
  async closeSubBudget(id) {
@@ -4342,13 +4623,15 @@ var HavenClient = class {
4342
4623
  body = text;
4343
4624
  }
4344
4625
  const toolResult = mcpToolResultOf(body);
4345
- const inBandChallenge = toolResult ? extractMcpPaymentRequired(toolResult) : void 0;
4346
4626
  const metaSettlement = toolResult ? mcpSettlementFromToolResult(toolResult) : void 0;
4347
4627
  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;
4628
+ const answer = decidePostFundingMerchantAnswer({
4629
+ toolResult,
4630
+ httpOk: surfaced.ok,
4631
+ paymentResponseHeader: headerReceipt,
4632
+ metaSettlement
4633
+ });
4634
+ const merchantAccepted = answer.accepted;
4352
4635
  let evidenceOutcome;
4353
4636
  let settlementEvidenceOutcome;
4354
4637
  if (!merchantAccepted) {
@@ -4370,7 +4653,7 @@ var HavenClient = class {
4370
4653
  });
4371
4654
  }
4372
4655
  } else {
4373
- const rawEvidenceTxHash = input.noFundingLeg ? settlement.settlementTxHash ?? void 0 : fundingTxHash ?? void 0;
4656
+ const rawEvidenceTxHash = input.noFundingLeg ? answer.settlementTxHash ?? void 0 : fundingTxHash ?? void 0;
4374
4657
  const evidenceTxHash = rawEvidenceTxHash && !isZeroSettlementTxHash(rawEvidenceTxHash) ? rawEvidenceTxHash : void 0;
4375
4658
  if (evidenceTxHash) {
4376
4659
  evidenceOutcome = await this.merchantCompletion.reportEvidence({
@@ -4381,19 +4664,20 @@ var HavenClient = class {
4381
4664
  merchantStatus: surfaced.status,
4382
4665
  paymentProofHeaderName: x402PaymentHeaderNamesSent(input.paymentHeader),
4383
4666
  paymentProofHeader: input.paymentHeader,
4384
- protocolReceiptHeaderName,
4385
- protocolReceiptHeader,
4667
+ protocolReceiptHeaderName: answer.protocolReceiptHeaderName,
4668
+ protocolReceiptHeader: answer.protocolReceiptHeader,
4386
4669
  // #3778: the delivery pointer rides the accepted arm's evidence
4387
4670
  // report — a rejection delivered nothing to point at.
4388
4671
  ...input.deliveryReference ? { deliveryReference: input.deliveryReference } : {}
4389
4672
  });
4390
4673
  }
4391
4674
  if (!input.noFundingLeg) {
4392
- const settlementCandidate = settlement.settlementTxHash;
4675
+ const settlementCandidate = answer.settlementTxHash;
4393
4676
  if (settlementCandidate && isWellFormedSettlementTxHash(settlementCandidate) && !isZeroSettlementTxHash(settlementCandidate) && fundingTxHash && settlementCandidate.toLowerCase() !== fundingTxHash.toLowerCase()) {
4394
4677
  settlementEvidenceOutcome = await this.merchantCompletion.reportSettlementEvidenceOnce(
4395
4678
  evidenceContext.paymentId,
4396
- settlementCandidate
4679
+ settlementCandidate,
4680
+ input.paymentHeader
4397
4681
  );
4398
4682
  }
4399
4683
  }
@@ -4404,7 +4688,7 @@ var HavenClient = class {
4404
4688
  ok: merchantAccepted,
4405
4689
  body,
4406
4690
  // #3155 review S3: a transaction beside a rejection is not a settlement.
4407
- settlementTxHash: merchantAccepted ? settlement.settlementTxHash ?? void 0 : void 0,
4691
+ settlementTxHash: merchantAccepted ? answer.settlementTxHash ?? void 0 : void 0,
4408
4692
  evidenceOutcome,
4409
4693
  settlementEvidenceOutcome
4410
4694
  };
@@ -4437,9 +4721,15 @@ var HavenClient = class {
4437
4721
  * Haven verifies the delegate → merchant transfer on-chain
4438
4722
  * (`observeEip3009MerchantSettlement`) and records it beside the funding
4439
4723
  * hash, so receipts name the merchant's settlement.
4724
+ *
4725
+ * #4061: on eip3009 pass `options.paymentHeader` — the payment header you
4726
+ * sent the merchant on your paid retry. Since #3925 it is the proof the
4727
+ * settlement is recorded against; without it the report is `refused` with
4728
+ * `errorCode: 'settlement_proof_header_missing'`, and repeating it WITH the
4729
+ * header can succeed.
4440
4730
  */
4441
- async reportSettlementEvidence(paymentId, settlementTxHash) {
4442
- return await this.merchantCompletion.reportSettlementEvidence(paymentId, settlementTxHash);
4731
+ async reportSettlementEvidence(paymentId, settlementTxHash, options = {}) {
4732
+ return await this.merchantCompletion.reportSettlementEvidence(paymentId, settlementTxHash, options);
4443
4733
  }
4444
4734
  /**
4445
4735
  * #3770: record the agent's evidence-only delivery verdict on a settled
@@ -4688,7 +4978,7 @@ var toolDescriptions = {
4688
4978
  checkFunds: {
4689
4979
  summary: "Check whether the agent's account actually holds at least the given amount of a token \u2014 funds held, not spend permitted.",
4690
4980
  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.",
4981
+ 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
4982
  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
4983
  },
4694
4984
  listReceipts: {
@@ -4716,9 +5006,9 @@ var toolDescriptions = {
4716
5006
  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
5007
  },
4718
5008
  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.",
5009
+ summary: "Step 1 of a purchase: discover payable services from Haven's merchant catalog \u2014 names, prices, the next call.",
5010
+ 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.",
5011
+ 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
5012
  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
5013
  },
4724
5014
  submitCatalogEntry: {
@@ -4741,8 +5031,8 @@ var toolDescriptions = {
4741
5031
  },
4742
5032
  reportSettlementEvidence: {
4743
5033
  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."
5034
+ 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.",
5035
+ 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
5036
  }
4747
5037
  };
4748
5038
 
@@ -5002,7 +5292,7 @@ Your user gives you a **budget on their own account** \u2014 not their wallet, a
5002
5292
 
5003
5293
  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
5294
 
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.
5295
+ 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
5296
  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
5297
  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
5298
  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 +5316,7 @@ Send these as your own message, \`<host>\` replaced by the host you fetched this
5026
5316
 
5027
5317
  **Before signup** (step 1):
5028
5318
 
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\`.
5319
+ > 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
5320
 
5031
5321
  **At the passkey step:**
5032
5322
 
@@ -5394,10 +5684,13 @@ request, this is the only way it can learn the purchase failed \u2014 without it
5394
5684
  failed purchase reads as complete for fifteen minutes. If the merchant's
5395
5685
  \`PAYMENT-RESPONSE\` header names a \`transaction\`, pass that raw header as
5396
5686
  \`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
5687
+ call, together with \`payment_header\`: the exact header you retried the
5688
+ merchant with. Haven records a reported settlement only with that header,
5689
+ which proves the settlement is this payment's. It decodes the response,
5690
+ verifies the settlement on-chain, and the receipt then shows the merchant's
5691
+ settlement, not only the funding transaction. Call
5399
5692
  \`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
5693
+ names it as the next step, and pass the same \`payment_header\` there too. On the **erc7710** scheme (the pay result says
5401
5694
  \`settlement_scheme: "erc7710"\`), sign with \`mcp__haven-signer__haven_sign\`,
5402
5695
  then \`mcp__haven__haven_submit\` with \`settlement_scheme: "erc7710"\`
5403
5696
  returns the \`payment_header\`; retry the merchant yourself with it as
@@ -5427,7 +5720,9 @@ for example, an invoice carrying an \`x402_payment_url\` to POST with its
5427
5720
  \`invoice_id\`, beside a raw deposit address and a web payment link.
5428
5721
  - **Quote the exact request:** \`mcp__haven__haven_quote_x402\` with \`url\`,
5429
5722
  \`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
5723
+ \`body\` is a JSON **string**, not an object. When the price is stated up
5724
+ front (an invoice total, say), also pass \`expected_amount\` \u2014 the price you
5725
+ expect, in whole tokens (\`"0.10"\`). On a JSON POST always pass
5431
5726
  \`headers: {"Content-Type": "application/json"}\` \u2014 the tool does not infer
5432
5727
  it, and a merchant may answer a request without it with a generic challenge
5433
5728
  Haven cannot pay (for example, only the \`upto\` scheme).
@@ -5456,6 +5751,16 @@ for example, an invoice carrying an \`x402_payment_url\` to POST with its
5456
5751
  report it, the merchant's settlement evidence. (A deposit transfer is bound
5457
5752
  to its payee and amount on-chain too; the difference is the record, not
5458
5753
  the binding.)
5754
+ - **If the quote says stop, stop.** The quote checks the agent's remaining
5755
+ budget at quote time, and it can check a price you state (\`expected_amount\`).
5756
+ When it returns \`safe_to_continue: false\` \u2014 the amount exceeds the budget, or
5757
+ the price does not match what you expected \u2014 it prefills no cap and names no
5758
+ next tool: stop and tell the user. If the x402 challenge itself is wrong (a
5759
+ price wildly off the invoice) and NOTHING has been paid via x402, the
5760
+ invoice's own deposit address MAY be an alternative, but only with the
5761
+ user's explicit go-ahead \u2014 never automatically. Once an x402 payment has
5762
+ been attempted or refused, never pay the deposit address: it can pay the
5763
+ invoice twice.
5459
5764
  - **Pay exactly one route, and never fall back silently.** If the x402 route
5460
5765
  is refused or fails, do not then pay the deposit address \u2014 that pays the
5461
5766
  invoice twice. Follow the result's guidance fields instead: on EIP-3009, a