@haven_ai/sdk 0.6.0-alpha.0 → 0.7.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
- import { selectStandardPaymentOption, buildX402IdempotencyKey, x402AuthorizationAmount, assertBoundDirectPaymentUserOp, assertFundingLegPaysDelegate, HavenTypedDataRefusedError, assertOwnSettlementChild, parsePaymentRequiredResponse, x402PaymentHeaderNamesSent, SDK_CLIENT_IDENTITY, readClientUpdate, HAVEN_CLIENT_HEADER, resolveTokenFromAddress, isSweepableChain, sweepUsdcAddress, x402PaymentHeaderNamesFor, X402_PAYMENT_HEADER_NAMES, toStandardPaymentRequirements, decodeBase64Json, encodeBase64Json, x402V2PaymentEnvelope, selectX402SettlementScheme, selectErc7710PaymentOption, isErc7710Option, x402AssetTransferMethod, normalizePaymentRequired } from './chunk-3WSEX3GW.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_REQUIRED_HEADER_NAME, X402_PAYMENT_RESPONSE_HEADER_NAME, X402_SETTLEMENT_FORWARD_MARGIN_SECONDS, assertBoundDirectPaymentUserOp, assertOwnTaskBudgetCloseUserOp, assertOwnTaskChild, buildSweepAuthorizationMessage, buildSweepTypedData, buildX402ExpectedMessage, compareNodeVersions, createNextStepBuilder, decodeBase64Json, decodeBase64Utf8, defaultNextToolFor, encodeBase64Json, encodeBase64Utf8, encodePaymentProof, hashDelegation, havenClientIdentity, isErc7710Option, isSupportedNodeVersion, isSweepableChain, isTaskChildTypedData, normalizePaymentRequired, parseNextTool, parsePaymentRequired, parsePaymentRequiredResponse, readClientUpdate, renderNextTool, resolveTokenFromAddress, selectErc7710PaymentOption, selectPaymentOption, selectStandardPaymentOption, selectX402SettlementScheme, sweepUsdcAddress, sweepUsdcDomain, toStandardPaymentRequirements, unsupportedNodeVersionMessage, validateStandardX402PaymentHeader, x402AssetTransferMethod, x402AuthorizationAmount, x402FacilitatorAddresses, x402V2PaymentEnvelope } from './chunk-3WSEX3GW.js';
3
- import { HavenSigningError, HavenError, DEFAULT_CONFIRMATION_TIMEOUT_MS, HavenApiError, assertUserOpTypedDataBinding, AgentPaymentRail, AgentPaymentPhase, HavenTimeoutError, X402UnexpectedStatusError, X402AlreadySettledError, AgentPaymentWarningCode, MerchantTimeoutError, AgentPaymentNextAction, HavenPaymentStateError, HavenZeroSettlementHashError, chainIdForNetwork } from './chunk-63GPTEEY.js';
4
- 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, X402AlreadySettledError, X402UnexpectedStatusError, assertUserOpTypedDataBinding, connectorRerunCommand, connectorSpec, deriveDelegateAccountAddress, isConnectorChannel, isPackedUserOperationTypedData, packedUserOperationHash, resolveConnectorChannel, signerUpdateFallback, verifySettlementChild } from './chunk-63GPTEEY.js';
5
- import { hashTypedData } from 'viem';
1
+ import { selectStandardPaymentOption, buildX402IdempotencyKey, x402AuthorizationAmount, assertBoundDirectPaymentUserOp, assertFundingLegPaysDelegate, HavenTypedDataRefusedError, assertOwnSettlementChild, parsePaymentRequiredResponse, x402PaymentHeaderNamesSent, SDK_CLIENT_IDENTITY, readClientUpdate, HAVEN_CLIENT_HEADER, resolveTokenFromAddress, isSweepableChain, sweepUsdcAddress, x402PaymentHeaderNamesFor, X402_PAYMENT_HEADER_NAMES, toStandardPaymentRequirements, decodeBase64Json, encodeBase64Json, x402V2PaymentEnvelope, selectX402SettlementScheme, selectErc7710PaymentOption, isErc7710Option, x402AssetTransferMethod, normalizePaymentRequired } from './chunk-R3RZJJWL.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_REQUIRED_HEADER_NAME, X402_PAYMENT_RESPONSE_HEADER_NAME, X402_SETTLEMENT_FORWARD_MARGIN_SECONDS, assertBoundDirectPaymentUserOp, assertOwnTaskBudgetCloseUserOp, assertOwnTaskChild, buildSweepAuthorizationMessage, buildSweepTypedData, buildX402ExpectedMessage, compareNodeVersions, createNextStepBuilder, decodeBase64Json, decodeBase64Utf8, defaultNextToolFor, 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, x402V2PaymentEnvelope } from './chunk-R3RZJJWL.js';
3
+ import { HavenSigningError, HavenError, DEFAULT_CONFIRMATION_TIMEOUT_MS, HavenApiError, assertUserOpTypedDataBinding, AgentPaymentRail, AgentPaymentPhase, HavenTimeoutError, X402TaskBudgetMismatchError, X402UnexpectedStatusError, X402AlreadySettledError, AgentPaymentWarningCode, MerchantTimeoutError, AgentPaymentNextAction, HavenPaymentStateError, HavenZeroSettlementHashError, X402Erc7710AlreadySettledError, DELEGATION_MANAGER, chainIdForNetwork } from './chunk-NNIT7WAI.js';
4
+ 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-NNIT7WAI.js';
5
+ import { keccak256, toBytes, concatHex, hashTypedData, encodeAbiParameters } from 'viem';
6
6
  import { ethers } from 'ethers';
7
7
  import { AsyncLocalStorage } from 'async_hooks';
8
8
  import { exact } from 'x402/schemes';
@@ -161,7 +161,18 @@ function mapParties(raw) {
161
161
  treasuryAccount: raw.treasury_account,
162
162
  delegate: raw.delegate,
163
163
  delegateAccount: raw.delegate_account,
164
- merchant: raw.merchant
164
+ merchant: raw.merchant,
165
+ // #3332: additive — absent unless the wire shape carries it.
166
+ ...raw.buyer ? {
167
+ buyer: {
168
+ legalName: raw.buyer.legal_name,
169
+ country: raw.buyer.country,
170
+ orgNumber: raw.buyer.org_number,
171
+ vatNumber: raw.buyer.vat_number,
172
+ viesStatus: raw.buyer.vies_status,
173
+ viesCheckedAt: raw.buyer.vies_checked_at
174
+ }
175
+ } : {}
165
176
  };
166
177
  }
167
178
  function mapPaymentResult(raw, buildExplorerUrl2) {
@@ -205,6 +216,9 @@ function mapPaymentStatusResult(raw) {
205
216
  expiresAt: raw.expires_at,
206
217
  chainId: raw.chain_id,
207
218
  message: raw.message,
219
+ // #3420: the delivered half of the settle vocabulary, additive — absent
220
+ // from the raw payload (older backend) stays absent here.
221
+ ...raw.delivered === true ? { delivered: true } : {},
208
222
  fee: raw.fee ? {
209
223
  amount: raw.fee.amount,
210
224
  token: raw.fee.token,
@@ -838,22 +852,93 @@ function selectJsonRpcResult(messages) {
838
852
  return messages.find((message) => "result" in message || "error" in message) ?? messages.at(-1);
839
853
  }
840
854
  var RECEIPT_VERSION = "haven-receipt-1";
855
+ var DELEGATION_MANAGER_CHAIN_IDS = /* @__PURE__ */ new Set([8453, 84532]);
856
+ var EIP712_DOMAIN_TYPEHASH = keccak256(
857
+ toBytes("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")
858
+ );
859
+ function delegationDomainSeparator(chainId) {
860
+ return keccak256(
861
+ encodeAbiParameters(
862
+ [{ type: "bytes32" }, { type: "bytes32" }, { type: "bytes32" }, { type: "uint256" }, { type: "address" }],
863
+ [
864
+ EIP712_DOMAIN_TYPEHASH,
865
+ keccak256(toBytes("DelegationManager")),
866
+ keccak256(toBytes("1")),
867
+ BigInt(chainId),
868
+ DELEGATION_MANAGER
869
+ ]
870
+ )
871
+ );
872
+ }
873
+ function delegationDigest(structHash, chainId) {
874
+ return keccak256(concatHex(["0x1901", delegationDomainSeparator(chainId), structHash]));
875
+ }
841
876
  function defaultRecover(hash, signature) {
842
877
  return ethers.recoverAddress(hash, signature);
843
878
  }
844
- function verifyPaymentReceipt(receipt, recover = defaultRecover) {
845
- const { delegate, signHash: signHash2, signature } = receipt.authorization;
846
- if (!signature) return { verified: false, reason: "missing_signature" };
879
+ var NOT_A_SIGNED_RECEIPT = { verified: false, reason: "not_a_signed_receipt" };
880
+ function match(delegate, hash, signature, recover) {
847
881
  let recovered;
848
882
  try {
849
- recovered = recover(signHash2, signature);
883
+ recovered = recover(hash, signature);
850
884
  } catch {
851
- return { verified: false, reason: "bad_signature" };
885
+ return { ok: false, reason: "bad_signature" };
852
886
  }
853
887
  if (recovered.toLowerCase() !== delegate.toLowerCase()) {
854
- return { verified: false, reason: "signer_mismatch", recoveredSigner: recovered };
888
+ return { ok: false, reason: "signer_mismatch", recoveredSigner: recovered };
855
889
  }
856
- return { verified: true, recoveredSigner: recovered };
890
+ return { ok: true, recoveredSigner: recovered };
891
+ }
892
+ function finish(result, verifiedOver) {
893
+ if (result.ok) {
894
+ return { verified: true, recoveredSigner: result.recoveredSigner, verifiedOver };
895
+ }
896
+ return result.recoveredSigner !== void 0 ? { verified: false, reason: result.reason, recoveredSigner: result.recoveredSigner } : { verified: false, reason: result.reason };
897
+ }
898
+ function verifyPaymentReceipt(receipt, recover = defaultRecover) {
899
+ if (typeof receipt !== "object" || receipt === null) return NOT_A_SIGNED_RECEIPT;
900
+ const source = receipt;
901
+ if (typeof source.authorization !== "object" || source.authorization === null) {
902
+ return NOT_A_SIGNED_RECEIPT;
903
+ }
904
+ const auth = source.authorization;
905
+ const { delegate, signHash: signHash2, signature, signatureScheme } = auth;
906
+ if (typeof delegate !== "string" || delegate === "" || typeof signHash2 !== "string" || signHash2 === "") {
907
+ return NOT_A_SIGNED_RECEIPT;
908
+ }
909
+ if (typeof signature !== "string" || signature === "") {
910
+ return { verified: false, reason: "missing_signature" };
911
+ }
912
+ const chainId = typeof source.onChain?.chainId === "number" ? source.onChain.chainId : void 0;
913
+ const digestVerifiable = chainId !== void 0 && DELEGATION_MANAGER_CHAIN_IDS.has(chainId);
914
+ if (signatureScheme === "eip712_userop") {
915
+ return { verified: false, reason: "not_verifiable_offline" };
916
+ }
917
+ if (signatureScheme === "eip712_delegation") {
918
+ if (!digestVerifiable) {
919
+ return { verified: false, reason: "not_verifiable_offline" };
920
+ }
921
+ let digest;
922
+ try {
923
+ digest = delegationDigest(signHash2, chainId);
924
+ } catch {
925
+ return { verified: false, reason: "bad_signature" };
926
+ }
927
+ return finish(match(delegate, digest, signature, recover), "delegation_digest");
928
+ }
929
+ const raw = match(delegate, signHash2, signature, recover);
930
+ if (raw.ok) return finish(raw, "sign_hash");
931
+ if (digestVerifiable) {
932
+ let digest;
933
+ try {
934
+ digest = delegationDigest(signHash2, chainId);
935
+ } catch {
936
+ return finish(raw, "sign_hash");
937
+ }
938
+ const delegated = match(delegate, digest, signature, recover);
939
+ if (delegated.ok) return finish(delegated, "delegation_digest");
940
+ }
941
+ return finish(raw, "sign_hash");
857
942
  }
858
943
 
859
944
  // src/account-reads.ts
@@ -879,6 +964,14 @@ function formatRemainingDisplay(tokenAddress, tokenSymbol, remainingAtomic) {
879
964
  const token = resolveTokenFromAddress(tokenAddress);
880
965
  return token ? `${formatAtomicAmount(safeBigInt(remainingAtomic), token.decimals)} ${tokenSymbol}` : `${remainingAtomic} ${tokenSymbol} (atomic; unknown decimals)`;
881
966
  }
967
+ function formatTokenAmount(atomic, decimals) {
968
+ const value = safeBigInt(atomic);
969
+ if (value < 0n) return "0";
970
+ const digits = value.toString().padStart(decimals + 1, "0");
971
+ const intPart = digits.slice(0, digits.length - decimals) || "0";
972
+ const fracPart = digits.slice(digits.length - decimals).replace(/0+$/, "");
973
+ return fracPart ? `${intPart}.${fracPart}` : intPart;
974
+ }
882
975
  function mapTaskBudget(raw) {
883
976
  return {
884
977
  id: raw.id,
@@ -912,6 +1005,13 @@ function summarizeTaskBudget(taskBudget) {
912
1005
  expiresAt: taskBudget.expiresAt
913
1006
  };
914
1007
  }
1008
+ function compactReceipt(receipt) {
1009
+ const row = { ...receipt };
1010
+ delete row.challengePayload;
1011
+ delete row.selectedPayment;
1012
+ delete row.protocolReceiptPayload;
1013
+ return row;
1014
+ }
915
1015
  var AccountReads = class {
916
1016
  transport;
917
1017
  getPaymentStatus;
@@ -1062,22 +1162,22 @@ var AccountReads = class {
1062
1162
  try {
1063
1163
  const tokenAddress = payment.asset ?? payment.x402?.asset ?? null;
1064
1164
  if (!tokenAddress) return unavailable("the settled payment does not carry a resolvable token address", payment);
1065
- const match = allowanceResult.value.allowances.find(
1165
+ const match2 = allowanceResult.value.allowances.find(
1066
1166
  (allowance) => allowance.tokenAddress.toLowerCase() === tokenAddress.toLowerCase()
1067
1167
  );
1068
- if (!match) return unavailable("no allowance/budget row matches the settled token", payment);
1069
- const token = resolveTokenFromAddress(match.tokenAddress);
1070
- const remainingDisplay = token ? `${formatAtomicAmount(safeBigInt(match.onchain.remaining), token.decimals)} ${match.tokenSymbol}` : void 0;
1168
+ if (!match2) return unavailable("no allowance/budget row matches the settled token", payment);
1169
+ const token = resolveTokenFromAddress(match2.tokenAddress);
1170
+ const remainingDisplay = token ? `${formatAtomicAmount(safeBigInt(match2.onchain.remaining), token.decimals)} ${match2.tokenSymbol}` : void 0;
1071
1171
  const rail = agentResult.value.executionRail;
1072
1172
  return {
1073
1173
  payment,
1074
1174
  allowance: {
1075
1175
  rail,
1076
- remaining_atomic: match.onchain.remaining,
1176
+ remaining_atomic: match2.onchain.remaining,
1077
1177
  ...remainingDisplay ? { remaining_display: remainingDisplay } : {},
1078
- token_symbol: match.tokenSymbol,
1079
- token_address: match.tokenAddress,
1080
- reset_period: match.resetPeriodMin,
1178
+ token_symbol: match2.tokenSymbol,
1179
+ token_address: match2.tokenAddress,
1180
+ reset_period: match2.resetPeriodMin,
1081
1181
  source: rail === "delegation" ? "active_delegations" : "allowance_module"
1082
1182
  },
1083
1183
  warnings: []
@@ -1097,8 +1197,13 @@ var AccountReads = class {
1097
1197
  if (options.cursor) params.set("cursor", options.cursor);
1098
1198
  const query = params.size > 0 ? `?${params.toString()}` : "";
1099
1199
  const raw = await this.transport.get(`/machine-payments/receipts${query}`);
1200
+ const receipts = raw.receipts.map(mapPaymentReceipt);
1100
1201
  return {
1101
- receipts: raw.receipts.map(mapPaymentReceipt),
1202
+ // #3423 (owner decision, opt-in): `compact` drops the three bulky
1203
+ // payload echoes (the merchant's full 402 challenge, the selected
1204
+ // option, and the merchant's PAYMENT-RESPONSE) from each row. The keys
1205
+ // are absent, not null, and the default shape is unchanged.
1206
+ receipts: options.compact ? receipts.map(compactReceipt) : receipts,
1102
1207
  total: typeof raw.total === "number" ? raw.total : null,
1103
1208
  hasMore: typeof raw.has_more === "boolean" ? raw.has_more : null,
1104
1209
  nextCursor: typeof raw.next_cursor === "string" ? raw.next_cursor : null
@@ -1464,6 +1569,10 @@ function readX402ReceiptPayer(raw) {
1464
1569
  }
1465
1570
 
1466
1571
  // src/x402-funding-leg.ts
1572
+ function sameX402TaskBudget(a, b) {
1573
+ if (a === void 0 || b === void 0) return a === b;
1574
+ return a.toLowerCase() === b.toLowerCase();
1575
+ }
1467
1576
  var X402FundingLeg = class {
1468
1577
  delegateKey;
1469
1578
  delegateAddress;
@@ -1489,20 +1598,35 @@ var X402FundingLeg = class {
1489
1598
  this.assertSignableAuthorizationState = options.assertSignableAuthorizationState;
1490
1599
  }
1491
1600
  // ── Receipt cache ────────────────────────────────────────────────
1492
- /** A still-valid cached receipt for this key, or undefined. */
1493
- cachedReceipt(idempotencyKey) {
1601
+ /**
1602
+ * #3392: each cached receipt records the authorizing budget id(s) it was
1603
+ * created under (absent when none) — `taskBudgetId` since #3329, and since
1604
+ * #3330 `subBudgetId` (agent B's grant). A call naming a DIFFERENT
1605
+ * combination is refused here — before any network call — instead of
1606
+ * silently returning a receipt paid under another budget. Absent-vs-absent
1607
+ * and equal ids (case-insensitive) hit as before.
1608
+ */
1609
+ cachedReceipt(idempotencyKey, taskBudgetId, subBudgetId) {
1494
1610
  const cached = this.receiptCache.get(idempotencyKey);
1495
- if (cached && cached.expiresAt > Date.now()) return cached.receipt;
1496
- return void 0;
1611
+ if (!cached || cached.expiresAt <= Date.now()) return void 0;
1612
+ if (!sameX402TaskBudget(cached.taskBudgetId, taskBudgetId) || !sameX402TaskBudget(cached.subBudgetId, subBudgetId)) {
1613
+ throw new X402TaskBudgetMismatchError(
1614
+ `The x402 idempotency key '${idempotencyKey}' was already used under a different authorizing budget (task '${cached.taskBudgetId ?? "none"}' vs '${taskBudgetId ?? "none"}', sub '${cached.subBudgetId ?? "none"}' vs '${subBudgetId ?? "none"}'). Pass a new idempotencyKey to pay under this budget.`,
1615
+ idempotencyKey,
1616
+ cached.taskBudgetId,
1617
+ taskBudgetId
1618
+ );
1619
+ }
1620
+ return cached.receipt;
1497
1621
  }
1498
- cacheReceipt(idempotencyKey, paymentHeader, receipt) {
1622
+ cacheReceipt(idempotencyKey, paymentHeader, receipt, taskBudgetId, subBudgetId) {
1499
1623
  const expiresAt = getPaymentHeaderValidBefore(paymentHeader);
1500
1624
  if (expiresAt > Date.now()) {
1501
- this.receiptCache.set(idempotencyKey, { expiresAt, receipt });
1625
+ this.receiptCache.set(idempotencyKey, { expiresAt, taskBudgetId, subBudgetId, receipt });
1502
1626
  }
1503
1627
  }
1504
1628
  // ── Authorization ────────────────────────────────────────────────
1505
- async authorize(paymentRequired, option, idempotencyKey, taskBudgetId) {
1629
+ async authorize(paymentRequired, option, idempotencyKey, taskBudgetId, subBudgetId) {
1506
1630
  const raw = await this.post("/x402", {
1507
1631
  url: paymentRequired.resource.url,
1508
1632
  payTo: this.delegateAddress,
@@ -1518,7 +1642,12 @@ var X402FundingLeg = class {
1518
1642
  settlementScheme: "eip3009",
1519
1643
  // #3329: `/x402` bodies are camelCase (`X402AuthorizeRequest`) — do not
1520
1644
  // switch this to `task_budget_id`, which is only the `POST /payments` key.
1521
- ...taskBudgetId ? { taskBudgetId } : {}
1645
+ ...taskBudgetId ? { taskBudgetId } : {},
1646
+ // #3330: same camelCase channel one level deeper — agent B pays through
1647
+ // the sub-budget agent A granted it (`[funding, B grant, A parent-child,
1648
+ // A budget]`). Mutually exclusive with `taskBudgetId` server-side; the
1649
+ // backend refuses a body naming both.
1650
+ ...subBudgetId ? { subBudgetId } : {}
1522
1651
  });
1523
1652
  const state = paymentStateFromRaw("x402 payment", raw);
1524
1653
  const executedReplay = raw.success && raw.tx_hash ? "idempotency-collision" : state?.nextAction === AgentPaymentNextAction.RetryOriginalX402Request ? "approval-resume" : null;
@@ -1541,12 +1670,12 @@ var X402FundingLeg = class {
1541
1670
  const paymentHeader = await this.createPaymentHeader(paymentRequired, option);
1542
1671
  if (raw.success && raw.tx_hash) {
1543
1672
  const receipt2 = this.receiptFromAuthorization(paymentRequired, option, paymentHeader, raw);
1544
- this.cacheReceipt(idempotencyKey, paymentHeader, receipt2);
1673
+ this.cacheReceipt(idempotencyKey, paymentHeader, receipt2, taskBudgetId, subBudgetId);
1545
1674
  return receipt2;
1546
1675
  }
1547
1676
  if (state?.nextAction === AgentPaymentNextAction.RetryOriginalX402Request) {
1548
1677
  const receipt2 = this.receiptFromStatus(paymentRequired, option, paymentHeader, state);
1549
- this.cacheReceipt(idempotencyKey, paymentHeader, receipt2);
1678
+ this.cacheReceipt(idempotencyKey, paymentHeader, receipt2, taskBudgetId, subBudgetId);
1550
1679
  return receipt2;
1551
1680
  }
1552
1681
  this.assertSignableAuthorizationState("x402 payment", raw);
@@ -1569,7 +1698,7 @@ var X402FundingLeg = class {
1569
1698
  execResult.chain_id ?? chainIdFromNetwork(option.network)
1570
1699
  );
1571
1700
  const receipt = this.receiptFromAuthorization(paymentRequired, option, paymentHeader, raw, execResult);
1572
- this.cacheReceipt(idempotencyKey, paymentHeader, receipt);
1701
+ this.cacheReceipt(idempotencyKey, paymentHeader, receipt, taskBudgetId, subBudgetId);
1573
1702
  return receipt;
1574
1703
  }
1575
1704
  // ── Header minting ───────────────────────────────────────────────
@@ -1835,11 +1964,27 @@ var X402Erc7710 = class {
1835
1964
  ...options.mcpCallContext ? { mcpCallContext: options.mcpCallContext } : {},
1836
1965
  // #3329: `/x402` bodies are camelCase (`X402AuthorizeRequest`) — do not
1837
1966
  // switch this to `task_budget_id`, which is only the `POST /payments` key.
1838
- ...options.taskBudgetId ? { taskBudgetId: options.taskBudgetId } : {}
1967
+ ...options.taskBudgetId ? { taskBudgetId: options.taskBudgetId } : {},
1968
+ // #3330: same camelCase channel one level deeper; mutually exclusive
1969
+ // with `taskBudgetId` server-side.
1970
+ ...options.subBudgetId ? { subBudgetId: options.subBudgetId } : {}
1839
1971
  });
1840
1972
  if (!raw.payment_id) {
1841
1973
  throw new HavenApiError("No payment_id returned from x402/authorize", 500, raw);
1842
1974
  }
1975
+ if (raw.status === "confirmed" && raw.tx_hash) {
1976
+ const requestedUrl = options.resourceUrl ?? paymentRequired.resource?.url;
1977
+ const samePurchase = typeof raw.to === "string" && raw.to.toLowerCase() === merchantPayTo.toLowerCase() && raw.resource_url === requestedUrl;
1978
+ if (!samePurchase) {
1979
+ throw new HavenApiError(
1980
+ `Idempotency key already belongs to confirmed payment ${raw.payment_id}, which is not this erc7710 purchase (different payee or resource). Nothing was signed and nothing was charged. Check that payment's status before buying again: a funded but undelivered EIP-3009 payment is recovered, not paid twice. Otherwise use a new idempotency key for this purchase.`,
1981
+ 409,
1982
+ raw,
1983
+ raw.payment_id
1984
+ );
1985
+ }
1986
+ throw new X402Erc7710AlreadySettledError(raw.payment_id, raw.tx_hash, raw);
1987
+ }
1843
1988
  const signData = raw.sign_data;
1844
1989
  if (signData?.signature_scheme !== "eip712_delegation" || !signData.typed_data) {
1845
1990
  throw new HavenApiError(
@@ -1870,10 +2015,20 @@ var X402Erc7710 = class {
1870
2015
  * hosted boundary) is irrelevant here.
1871
2016
  */
1872
2017
  async submit(paymentId, signature) {
1873
- const settled = await this.post(
1874
- `/x402/${paymentId}/settle`,
1875
- { signature }
1876
- );
2018
+ let settled;
2019
+ try {
2020
+ settled = await this.post(`/x402/${paymentId}/settle`, { signature });
2021
+ } catch (err) {
2022
+ const body = err instanceof HavenApiError ? err.body : void 0;
2023
+ if (err instanceof HavenApiError && err.statusCode === 409 && body?.code === "payment_already_settled" && typeof body.tx_hash === "string") {
2024
+ throw new X402Erc7710AlreadySettledError(
2025
+ typeof body.payment_id === "string" ? body.payment_id : paymentId,
2026
+ body.tx_hash,
2027
+ body
2028
+ );
2029
+ }
2030
+ throw err;
2031
+ }
1877
2032
  if (!settled.payment_header) {
1878
2033
  throw new HavenApiError(
1879
2034
  "x402 settle returned no payment_header \u2014 the merchant cannot be retried.",
@@ -2436,6 +2591,15 @@ var HavenClient = class {
2436
2591
  pollingInterval;
2437
2592
  chainRpcs;
2438
2593
  inFlightX402 = /* @__PURE__ */ new Map();
2594
+ /**
2595
+ * #3392: the task budget each in-flight x402 entry was created under
2596
+ * (`undefined` = none). A call joining an in-flight entry under a
2597
+ * different budget is refused before any network call — never silently
2598
+ * charged to another budget's payment.
2599
+ */
2600
+ inFlightX402TaskBudget = /* @__PURE__ */ new Map();
2601
+ /** #3330: the sub-budget each in-flight entry was created under, same pin as `inFlightX402TaskBudget`. */
2602
+ inFlightX402SubBudget = /* @__PURE__ */ new Map();
2439
2603
  /**
2440
2604
  * The EIP-3009 funding-leg lifecycle (#1618). The facade holds a reference
2441
2605
  * and delegates; it does not reimplement any of it.
@@ -2559,7 +2723,10 @@ var HavenClient = class {
2559
2723
  amount: request.amount,
2560
2724
  to: request.to,
2561
2725
  ...request.idempotencyKey ? { idempotency_key: request.idempotencyKey } : {},
2562
- ...request.taskBudgetId ? { task_budget_id: request.taskBudgetId } : {}
2726
+ ...request.taskBudgetId ? { task_budget_id: request.taskBudgetId } : {},
2727
+ // #3330: `POST /payments` is snake_case — agent B pays through the
2728
+ // sub-budget A granted it; the backend refuses a body naming both ids.
2729
+ ...request.subBudgetId ? { sub_budget_id: request.subBudgetId } : {}
2563
2730
  });
2564
2731
  if (raw.status === "pending_approval") {
2565
2732
  throwPaymentStateError("Payment", raw);
@@ -2623,7 +2790,10 @@ var HavenClient = class {
2623
2790
  // #3329: `/x402` bodies are camelCase (`X402AuthorizeRequest`), unlike
2624
2791
  // `POST /payments`'s snake_case `task_budget_id` above — the two
2625
2792
  // surfaces use different conventions, so this key is NOT `task_budget_id`.
2626
- ...options.taskBudgetId ? { taskBudgetId: options.taskBudgetId } : {}
2793
+ ...options.taskBudgetId ? { taskBudgetId: options.taskBudgetId } : {},
2794
+ // #3330: same camelCase channel one level deeper (`X402AuthorizeRequest`);
2795
+ // mutually exclusive with `taskBudgetId` server-side.
2796
+ ...options.subBudgetId ? { subBudgetId: options.subBudgetId } : {}
2627
2797
  });
2628
2798
  if (raw.status !== "pending_signature") {
2629
2799
  throwPaymentStateError("x402 payment", raw);
@@ -3003,7 +3173,12 @@ var HavenClient = class {
3003
3173
  async listReceipts(options = {}) {
3004
3174
  return this.accountReads.listReceipts(options);
3005
3175
  }
3006
- /** #3128: one page of receipts with `total`, `hasMore` and `nextCursor`. */
3176
+ /**
3177
+ * One page of receipts with `total`, `hasMore` and `nextCursor` (#3128).
3178
+ * `compact: true` (#3423) drops each row's
3179
+ * `challengePayload`, `selectedPayment` and `protocolReceiptPayload`; the
3180
+ * default shape is unchanged.
3181
+ */
3007
3182
  async listReceiptsPage(options = {}) {
3008
3183
  return this.accountReads.listReceiptsPage(options);
3009
3184
  }
@@ -3154,12 +3329,30 @@ var HavenClient = class {
3154
3329
  throw noCompatiblePaymentOptionError(paymentRequired.accepts);
3155
3330
  }
3156
3331
  const idempotencyKey = options.idempotencyKey ?? buildX402IdempotencyKey(paymentRequired, option);
3157
- const cached = this.fundingLeg.cachedReceipt(idempotencyKey);
3332
+ const cached = this.fundingLeg.cachedReceipt(idempotencyKey, options.taskBudgetId, options.subBudgetId);
3158
3333
  if (cached) return cached;
3159
3334
  const inFlight = this.inFlightX402.get(idempotencyKey);
3160
- if (inFlight) return inFlight;
3161
- const promise = this.fundingLeg.authorize(paymentRequired, option, idempotencyKey, options.taskBudgetId);
3335
+ if (inFlight) {
3336
+ if (!sameX402TaskBudget(this.inFlightX402TaskBudget.get(idempotencyKey), options.taskBudgetId) || !sameX402TaskBudget(this.inFlightX402SubBudget.get(idempotencyKey), options.subBudgetId)) {
3337
+ throw new X402TaskBudgetMismatchError(
3338
+ `The x402 idempotency key '${idempotencyKey}' has an in-flight payment created under a different authorizing budget (task '${this.inFlightX402TaskBudget.get(idempotencyKey) ?? "none"}' vs '${options.taskBudgetId ?? "none"}', sub '${this.inFlightX402SubBudget.get(idempotencyKey) ?? "none"}' vs '${options.subBudgetId ?? "none"}'). Pass a new idempotencyKey to pay under this budget.`,
3339
+ idempotencyKey,
3340
+ this.inFlightX402TaskBudget.get(idempotencyKey),
3341
+ options.taskBudgetId
3342
+ );
3343
+ }
3344
+ return inFlight;
3345
+ }
3346
+ const promise = this.fundingLeg.authorize(
3347
+ paymentRequired,
3348
+ option,
3349
+ idempotencyKey,
3350
+ options.taskBudgetId,
3351
+ options.subBudgetId
3352
+ );
3162
3353
  this.inFlightX402.set(idempotencyKey, promise);
3354
+ this.inFlightX402TaskBudget.set(idempotencyKey, options.taskBudgetId);
3355
+ this.inFlightX402SubBudget.set(idempotencyKey, options.subBudgetId);
3163
3356
  try {
3164
3357
  return await promise;
3165
3358
  } catch (err) {
@@ -3171,6 +3364,8 @@ var HavenClient = class {
3171
3364
  throw err;
3172
3365
  } finally {
3173
3366
  this.inFlightX402.delete(idempotencyKey);
3367
+ this.inFlightX402TaskBudget.delete(idempotencyKey);
3368
+ this.inFlightX402SubBudget.delete(idempotencyKey);
3174
3369
  }
3175
3370
  }
3176
3371
  /**
@@ -3306,7 +3501,7 @@ var HavenClient = class {
3306
3501
  throw noCompatiblePaymentOptionError(input.paymentRequired.accepts);
3307
3502
  }
3308
3503
  const idempotencyKey = input.idempotencyKey ?? buildX402IdempotencyKey(input.paymentRequired, option);
3309
- const cached = this.fundingLeg.cachedReceipt(idempotencyKey);
3504
+ const cached = this.fundingLeg.cachedReceipt(idempotencyKey, input.taskBudgetId);
3310
3505
  if (cached) return cached;
3311
3506
  const status = await this.getPaymentStatus(input.paymentId);
3312
3507
  assertCanResumeX402(status, input.paymentRequired, option);
@@ -3324,7 +3519,7 @@ var HavenClient = class {
3324
3519
  }
3325
3520
  const paymentHeader = await this.fundingLeg.createPaymentHeader(input.paymentRequired, option);
3326
3521
  const receipt = this.fundingLeg.receiptFromStatus(input.paymentRequired, option, paymentHeader, status);
3327
- this.fundingLeg.cacheReceipt(idempotencyKey, paymentHeader, receipt);
3522
+ this.fundingLeg.cacheReceipt(idempotencyKey, paymentHeader, receipt, input.taskBudgetId);
3328
3523
  return receipt;
3329
3524
  }
3330
3525
  async resumeX402Payment(input) {
@@ -3751,7 +3946,7 @@ var toolDescriptions = {
3751
3946
  // deleted `/demo/mpp/*` route. Use the x402 fragments above instead.
3752
3947
  getPaymentStatus: {
3753
3948
  summary: "Fetch structured Haven payment status for agent recovery.",
3754
- behavior: "State: phase, nextAction, rail, amount, merchant, resource, idempotency, message; parties: treasury/delegate/delegateAccount/merchant. awaiting_settlement_evidence: poll once, else unverified.",
3949
+ behavior: "State: phase, nextAction, rail, amount, merchant, resource, idempotency, message; delivered when the merchant answered; parties: treasury/delegate/delegateAccount/merchant. awaiting_settlement_evidence: poll once, else unverified. delivered_unverified: terminal, stop.",
3755
3950
  nextActionGuidance: ""
3756
3951
  },
3757
3952
  getResumeState: {
@@ -3786,9 +3981,9 @@ var toolDescriptions = {
3786
3981
  nextActionGuidance: ""
3787
3982
  },
3788
3983
  verifyReceipt: {
3789
- summary: "Verify a payment receipt offline \u2014 confirm the agent authorised the transfer.",
3790
- selectionGuidance: "Use this to check a receipt you already hold; it needs no network and does not trust Haven. Use the history tool to fetch receipts in the first place.",
3791
- behavior: "Recovers the signer from the receipt authorisation and confirms it matches the agent delegate. Returns verified true/false with the recovered signer or a reason. Pure and local \u2014 no backend call.",
3984
+ summary: "Verify a signed payment receipt offline \u2014 confirm the agent delegate signed its authorisation.",
3985
+ selectionGuidance: "Takes the signed bundle from GET /payments/{id}/receipt, not a haven_list_receipts row \u2014 a row carries no signature and returns not_a_signed_receipt. No network.",
3986
+ behavior: "Never throws. An erc7710 bundle verifies over the delegation digest the delegate signed; a direct or eip3009 bundle returns not_verifiable_offline. verified true means only that the delegate signed the hash named by verifiedOver \u2014 payment facts are Haven-asserted and settlement is not proven; check settlementTxHash on an explorer.",
3792
3987
  nextActionGuidance: ""
3793
3988
  },
3794
3989
  payMcpTool: {
@@ -4075,7 +4270,7 @@ Your user gives you a **budget on their own account** \u2014 not their wallet, a
4075
4270
 
4076
4271
  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.
4077
4272
 
4078
- 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, \`npx @haven_ai/cli@<channel> 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. The \`<channel>\` in that command is the tag your deployment names \u2014 read it from \`/.well-known/haven.json\` (\`packages.cli.channel\`), never a tag you pick. 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.
4273
+ 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.
4079
4274
  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.
4080
4275
  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.
4081
4276
  4. **YOU \u2014 run the connector command** in that prompt (below). It makes your signing key locally, registering only the public address.
@@ -4153,7 +4348,7 @@ Nothing here needs you to. Steps 1-3 are links: hand your user the full \`<host>
4153
4348
 
4154
4349
  ## If something breaks
4155
4350
 
4156
- If a Haven result carries \`client_update\`, that client is out of date: update it \u2014 \`upgrade_command\` names the package and channel \u2014 then retry. \`required: true\` means payments are refused until you do. What changed: [/releases](/releases).
4351
+ If a Haven result carries \`client_update\`, that client is out of date: run its \`upgrade_command\` as given, then any repair line it prints, then retry. \`required: true\` means payments are refused until you do. What changed: [/releases](/releases).
4157
4352
 
4158
4353
  ## Vocabulary
4159
4354
 
@@ -4173,12 +4368,12 @@ Start by reading {{HAVEN_ORIGIN}}/for-agents.md \u2014 it is written for you and
4173
4368
 
4174
4369
  Then:
4175
4370
 
4176
- 1. Run \`npx @haven_ai/cli@<channel> login\`. The \`<channel>\` in that command is the tag your deployment names \u2014 read it from \`/.well-known/haven.json\` (\`packages.cli.channel\`), never a tag you pick. 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.
4371
+ 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.
4177
4372
  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.
4178
4373
  3. ${AGENT_APPROVAL_RELAY_JSON_SENTENCE}
4179
4374
  4. Once I have approved the budget, verify with the \`haven_get_agent\` tool: \`ready\` means you can pay, \`needs_approval\` means my approval has not landed yet.
4180
4375
 
4181
- Two things only I can do: approving that budget with my passkey, and funding the account with USDC on Base \u2014 no ETH, Haven sponsors the gas. Tell me if either is missing rather than working around it.
4376
+ 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.
4182
4377
 
4183
4378
  ${AGENT_SECRET_HYGIENE_SENTENCE}`;
4184
4379
 
@@ -4322,28 +4517,43 @@ the budget in the Haven dashboard, or wait for the period reset.
4322
4517
  4. Then FOLLOW THE RESPONSE'S GUIDANCE FIELDS: \`next_action\`, \`next_tool\`,
4323
4518
  and \`next_arguments\` name the exact next call \u2014 act on those first; the
4324
4519
  prose in this section is fallback and debugging detail. If the catalog
4325
- entry is missing or degraded, the response instead names
4326
- \`mcp__haven__haven_pay_mcp_tool\` (merchant URL, tool name, arguments) as
4327
- the manual fallback.
4520
+ row is a plain-HTTP x402 paywall (no MCP tool metadata), the response
4521
+ instead names \`mcp__haven__haven_quote_x402\` with the entry's resource
4522
+ URL as \`url\`. If an MCP row is degraded or has no tool name, the response
4523
+ instead names \`mcp__haven__haven_pay_mcp_tool\` (merchant URL, tool name,
4524
+ arguments) as the manual fallback.
4328
4525
 
4329
- **Signing:** \`mcp__haven-signer__haven_sign_x402\` with \`payment_id\` ONLY \u2014
4526
+ **Signing:** which signer tool to call depends on the settlement scheme the
4527
+ prepare/pay response already named as \`next_tool\` \u2014 follow that field, never
4528
+ hard-code a choice. **erc7710** (direct settlement \u2014 chosen per merchant when
4529
+ its 402 advertises \`assetTransferMethod: "erc7710"\` AND your account is on
4530
+ the delegation rail; never a blanket default): \`mcp__haven-signer__haven_sign\`
4531
+ with \`payment_id\` ONLY; the signer fetches the settlement child itself.
4532
+ **EIP-3009** (the bridge \u2014 used otherwise, whenever the merchant offers a
4533
+ standard entry): \`mcp__haven-signer__haven_sign_x402\` with \`payment_id\` ONLY \u2014
4330
4534
  the local signer fetches the exact signing bytes AND \`payment_required\`
4331
- itself, so never relay \`typed_data\` or the 402 blob yourself. If the signer
4332
- reports its fetched context carried no \`payment_required\` (older backend),
4333
- re-call with \`payment_required\` added verbatim. Fallback for an older signer
4334
- or backend: re-run the quote/prepare tool with the SAME \`idempotency_key\`
4335
- plus \`include_signing_payload=true\`, then pass \`payload_hash\`,
4336
- \`x402_expected\` (the nested \`x402.expected\` object), and
4535
+ itself, so never relay \`typed_data\` or the 402 blob yourself. A merchant
4536
+ that advertises ONLY erc7710 against an account that is NOT on the
4537
+ delegation rail is refused outright \u2014 it does not fall back to EIP-3009. If
4538
+ the signer reports its fetched context carried no \`payment_required\` (older
4539
+ backend), re-call with \`payment_required\` added verbatim. Fallback for an
4540
+ older signer or backend: re-run the quote/prepare tool with the SAME
4541
+ \`idempotency_key\` plus \`include_signing_payload=true\`, then pass
4542
+ \`payload_hash\`, \`x402_expected\` (the nested \`x402.expected\` object), and
4337
4543
  \`typed_data\`/\`typed_data_b64\` through unchanged.
4338
4544
 
4339
- **Settle:** \`mcp__haven__haven_settle_mcp_tool\` with \`payment_id\`,
4340
- \`signature\`, and \`payment_header\` ONLY \u2014 Haven rehydrates the merchant call
4545
+ **Settle:** \`mcp__haven__haven_settle_mcp_tool\` with \`payment_id\` and
4546
+ \`signature\` always. On **erc7710**, pass no \`payment_header\` \u2014 Haven
4547
+ assembles it at settle, so there is nothing to build locally and no funding
4548
+ transaction to wait for. On **EIP-3009**, also pass \`payment_header\` (from
4549
+ \`haven_sign_x402\`) ONLY. Either way Haven rehydrates the merchant call
4341
4550
  context (\`merchant_url\`, \`tool_name\`, \`arguments\`, \`mcp_transport\`)
4342
4551
  server-side from \`payment_id\`. Pass those four fields explicitly only as a
4343
4552
  version-skew fallback when Haven has no stored context for the id \u2014 both or
4344
- none together, never just one. If the settle result carries \`settled: false\`,
4345
- funding has not confirmed \u2014 follow the result's guidance fields and check
4346
- status later, do not re-pay.
4553
+ none together, never just one. On **EIP-3009**, \`settled: false\` means
4554
+ funding has not confirmed; on **erc7710** it means the merchant delivered but
4555
+ settlement is not yet verified. Either way, follow the result's guidance
4556
+ fields and do not re-pay.
4347
4557
 
4348
4558
  Step-by-step alternative (also key-safe; for an older signer or backend, or
4349
4559
  when you already have a merchant URL and tool name instead of a
@@ -4533,6 +4743,6 @@ function sameUrl(a, b) {
4533
4743
  }
4534
4744
  }
4535
4745
 
4536
- export { AGENT_APPROVAL_RELAY_JSON_SENTENCE, AGENT_APPROVAL_RELAY_PROSE_SENTENCE, AGENT_COMMAND_MODIFICATION_SENTENCE, AGENT_JSON_MODE_SENTENCE, AGENT_LOCAL_KEY_SENTENCE, AGENT_NETWORK_ACCESS_SENTENCE, AGENT_ONBOARDING_PROMPT, AGENT_README_SECTION_MD, AGENT_SECRET_HYGIENE_SENTENCE, AGENT_WIRING_COLLISION_RELAY_SENTENCE, DISCOVERY_MAX_BYTES, HAVEN_AGENT_RUNBOOK_MD, HAVEN_SKILL_BODY_MD, HAVEN_SKILL_MD, HavenClient, HavenInsecureRetryTargetError, INSECURE_RETRY_TARGET_CODE, MERCHANT_DISCOVERY_PATHS, RECEIPT_VERSION, SKILL_FOLDER_NAME, addressFromKey, assertSecureX402RetryTarget, composeDescription, discoverMerchantMcpUrl, havenTools, isSecureX402RetryTarget, isZeroSettlementTxHash, readX402ReceiptPayer, resolveX402RetryTarget, sameUrl, signHash, signUserOpTypedDataForDelegation, toolDescriptions, verifyPaymentReceipt, verifySignature };
4746
+ export { AGENT_APPROVAL_RELAY_JSON_SENTENCE, AGENT_APPROVAL_RELAY_PROSE_SENTENCE, AGENT_COMMAND_MODIFICATION_SENTENCE, AGENT_JSON_MODE_SENTENCE, AGENT_LOCAL_KEY_SENTENCE, AGENT_NETWORK_ACCESS_SENTENCE, AGENT_ONBOARDING_PROMPT, AGENT_README_SECTION_MD, AGENT_SECRET_HYGIENE_SENTENCE, AGENT_WIRING_COLLISION_RELAY_SENTENCE, DISCOVERY_MAX_BYTES, HAVEN_AGENT_RUNBOOK_MD, HAVEN_SKILL_BODY_MD, HAVEN_SKILL_MD, HavenClient, HavenInsecureRetryTargetError, INSECURE_RETRY_TARGET_CODE, MERCHANT_DISCOVERY_PATHS, RECEIPT_VERSION, SKILL_FOLDER_NAME, addressFromKey, assertSecureX402RetryTarget, composeDescription, discoverMerchantMcpUrl, formatTokenAmount, havenTools, isSecureX402RetryTarget, isZeroSettlementTxHash, readX402ReceiptPayer, resolveX402RetryTarget, sameUrl, signHash, signUserOpTypedDataForDelegation, toolDescriptions, verifyPaymentReceipt, verifySignature };
4537
4747
  //# sourceMappingURL=index.js.map
4538
4748
  //# sourceMappingURL=index.js.map