@haven_ai/sdk 0.5.0-alpha.1 → 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, 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-KQ7JXKTV.js';
2
- export { DEFAULT_NEXT_TOOL_BY_ACTION, ERC7710_ASSET_TRANSFER_METHOD, HAVEN_CLIENT_HEADER, HAVEN_MINIMUM_NODE_VERSION, HavenTypedDataRefusedError, 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, buildSweepAuthorizationMessage, buildSweepTypedData, buildX402ExpectedMessage, compareNodeVersions, createNextStepBuilder, decodeBase64Json, decodeBase64Utf8, defaultNextToolFor, encodeBase64Json, encodeBase64Utf8, encodePaymentProof, havenClientIdentity, isErc7710Option, isSupportedNodeVersion, isSweepableChain, normalizePaymentRequired, parseNextTool, parsePaymentRequired, parsePaymentRequiredResponse, readClientUpdate, renderNextTool, resolveTokenFromAddress, selectErc7710PaymentOption, selectPaymentOption, selectStandardPaymentOption, selectX402SettlementScheme, sweepUsdcAddress, sweepUsdcDomain, toStandardPaymentRequirements, unsupportedNodeVersionMessage, validateStandardX402PaymentHeader, x402AssetTransferMethod, x402AuthorizationAmount, x402FacilitatorAddresses, x402V2PaymentEnvelope } from './chunk-KQ7JXKTV.js';
3
- import { HavenSigningError, HavenError, DEFAULT_CONFIRMATION_TIMEOUT_MS, HavenApiError, assertUserOpTypedDataBinding, AgentPaymentRail, AgentPaymentPhase, HavenTimeoutError, X402UnexpectedStatusError, X402AlreadySettledError, AgentPaymentWarningCode, MerchantTimeoutError, AgentPaymentNextAction, HavenPaymentStateError, HavenZeroSettlementHashError, chainIdForNetwork } from './chunk-GLNG2GF7.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-GLNG2GF7.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,
@@ -232,17 +246,13 @@ function mapPaymentReceipt(raw) {
232
246
  id: raw.id,
233
247
  paymentId: raw.payment_id,
234
248
  source: raw.rail,
235
- rail: raw.rail,
236
249
  paymentProofStatus: raw.proof_status,
237
- proofStatus: raw.proof_status,
238
250
  txHash: raw.tx_hash,
239
251
  fundingTxHash: raw.funding_tx_hash ?? null,
240
252
  settlementTxHash: raw.settlement_tx_hash ?? null,
241
253
  chainId: raw.chain_id,
242
254
  x402ResourceUrl: raw.resource_url,
243
- resourceUrl: raw.resource_url,
244
255
  x402MerchantAddress: raw.merchant_address,
245
- merchantAddress: raw.merchant_address,
246
256
  payerAddress: raw.payer_address,
247
257
  parties: mapParties(raw.parties),
248
258
  settlementAddress: raw.settlement_address,
@@ -842,22 +852,93 @@ function selectJsonRpcResult(messages) {
842
852
  return messages.find((message) => "result" in message || "error" in message) ?? messages.at(-1);
843
853
  }
844
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
+ }
845
876
  function defaultRecover(hash, signature) {
846
877
  return ethers.recoverAddress(hash, signature);
847
878
  }
848
- function verifyPaymentReceipt(receipt, recover = defaultRecover) {
849
- const { delegate, signHash: signHash2, signature } = receipt.authorization;
850
- 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) {
851
881
  let recovered;
852
882
  try {
853
- recovered = recover(signHash2, signature);
883
+ recovered = recover(hash, signature);
854
884
  } catch {
855
- return { verified: false, reason: "bad_signature" };
885
+ return { ok: false, reason: "bad_signature" };
856
886
  }
857
887
  if (recovered.toLowerCase() !== delegate.toLowerCase()) {
858
- return { verified: false, reason: "signer_mismatch", recoveredSigner: recovered };
888
+ return { ok: false, reason: "signer_mismatch", recoveredSigner: recovered };
889
+ }
890
+ return { ok: true, recoveredSigner: recovered };
891
+ }
892
+ function finish(result, verifiedOver) {
893
+ if (result.ok) {
894
+ return { verified: true, recoveredSigner: result.recoveredSigner, verifiedOver };
859
895
  }
860
- return { verified: true, recoveredSigner: recovered };
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");
861
942
  }
862
943
 
863
944
  // src/account-reads.ts
@@ -883,6 +964,54 @@ function formatRemainingDisplay(tokenAddress, tokenSymbol, remainingAtomic) {
883
964
  const token = resolveTokenFromAddress(tokenAddress);
884
965
  return token ? `${formatAtomicAmount(safeBigInt(remainingAtomic), token.decimals)} ${tokenSymbol}` : `${remainingAtomic} ${tokenSymbol} (atomic; unknown decimals)`;
885
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
+ }
975
+ function mapTaskBudget(raw) {
976
+ return {
977
+ id: raw.id,
978
+ agentId: raw.agent_id,
979
+ chainId: raw.chain_id,
980
+ tokenAddress: raw.token_address,
981
+ recipientAddress: raw.recipient_address,
982
+ parentDelegationHash: raw.parent_delegation_hash,
983
+ delegationHash: raw.delegation_hash,
984
+ label: raw.label,
985
+ maxAtomic: raw.max_atomic,
986
+ status: raw.status,
987
+ expiresAt: raw.expires_at,
988
+ isExpired: raw.is_expired,
989
+ createdAt: raw.created_at,
990
+ openedAt: raw.opened_at,
991
+ closedAt: raw.closed_at,
992
+ closeTxHash: raw.close_tx_hash
993
+ };
994
+ }
995
+ function summarizeTaskBudget(taskBudget) {
996
+ const token = resolveTokenFromAddress(taskBudget.tokenAddress);
997
+ const maxDisplay = token ? `${formatAtomicAmount(safeBigInt(taskBudget.maxAtomic), token.decimals)} ${token.symbol}` : `${taskBudget.maxAtomic} (atomic; unknown decimals)`;
998
+ return {
999
+ id: taskBudget.id,
1000
+ label: taskBudget.label,
1001
+ tokenAddress: taskBudget.tokenAddress,
1002
+ maxAtomic: taskBudget.maxAtomic,
1003
+ maxDisplay,
1004
+ recipientAddress: taskBudget.recipientAddress,
1005
+ expiresAt: taskBudget.expiresAt
1006
+ };
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
+ }
886
1015
  var AccountReads = class {
887
1016
  transport;
888
1017
  getPaymentStatus;
@@ -902,7 +1031,11 @@ var AccountReads = class {
902
1031
  return request;
903
1032
  }
904
1033
  async getAgentSummary() {
905
- const [agent, allowanceSummary] = await Promise.all([this.getAgent(), this.getAllowances()]);
1034
+ const [agent, allowanceSummary, taskBudgets] = await Promise.all([
1035
+ this.getAgent(),
1036
+ this.getAllowances(),
1037
+ this.listOpenTaskBudgetsSummary()
1038
+ ]);
906
1039
  const allowances = allowanceSummary.allowances.map((allowance) => {
907
1040
  return {
908
1041
  id: allowance.id,
@@ -916,7 +1049,30 @@ var AccountReads = class {
916
1049
  };
917
1050
  });
918
1051
  const readiness = deriveReadiness(agent.status, allowances);
919
- return { ...agent, readiness, spend_authority_readiness: readiness, allowances };
1052
+ return { ...agent, readiness, spend_authority_readiness: readiness, allowances, taskBudgets };
1053
+ }
1054
+ /**
1055
+ * #3329: `GET /task-budgets?status=open`, mapped to the condensed summary
1056
+ * row `getAgentSummary()` carries. Fails SOFT per the #3093 rule (default
1057
+ * every array wire key to `[]` so an absent/malformed key degrades instead
1058
+ * of taking the route down): a 404 (a backend that predates #3329's
1059
+ * route), ANY other transport error, an undefined/null body, or a missing
1060
+ * or non-array `task_budgets` key all return `[]` rather than throwing —
1061
+ * `getAgentSummary`'s identity + readiness + allowances fields are what
1062
+ * callers depend on, and this read must never be why the whole summary
1063
+ * fails.
1064
+ */
1065
+ async listOpenTaskBudgetsSummary() {
1066
+ try {
1067
+ const raw = await this.transport.get(
1068
+ "/task-budgets?status=open"
1069
+ );
1070
+ const rows = raw?.task_budgets;
1071
+ if (!Array.isArray(rows)) return [];
1072
+ return rows.map((row) => summarizeTaskBudget(mapTaskBudget(row)));
1073
+ } catch {
1074
+ return [];
1075
+ }
920
1076
  }
921
1077
  async getAllowances() {
922
1078
  const raw = await this.transport.get("/machine-payments/allowances");
@@ -1006,22 +1162,22 @@ var AccountReads = class {
1006
1162
  try {
1007
1163
  const tokenAddress = payment.asset ?? payment.x402?.asset ?? null;
1008
1164
  if (!tokenAddress) return unavailable("the settled payment does not carry a resolvable token address", payment);
1009
- const match = allowanceResult.value.allowances.find(
1165
+ const match2 = allowanceResult.value.allowances.find(
1010
1166
  (allowance) => allowance.tokenAddress.toLowerCase() === tokenAddress.toLowerCase()
1011
1167
  );
1012
- if (!match) return unavailable("no allowance/budget row matches the settled token", payment);
1013
- const token = resolveTokenFromAddress(match.tokenAddress);
1014
- 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;
1015
1171
  const rail = agentResult.value.executionRail;
1016
1172
  return {
1017
1173
  payment,
1018
1174
  allowance: {
1019
1175
  rail,
1020
- remaining_atomic: match.onchain.remaining,
1176
+ remaining_atomic: match2.onchain.remaining,
1021
1177
  ...remainingDisplay ? { remaining_display: remainingDisplay } : {},
1022
- token_symbol: match.tokenSymbol,
1023
- token_address: match.tokenAddress,
1024
- reset_period: match.resetPeriodMin,
1178
+ token_symbol: match2.tokenSymbol,
1179
+ token_address: match2.tokenAddress,
1180
+ reset_period: match2.resetPeriodMin,
1025
1181
  source: rail === "delegation" ? "active_delegations" : "allowance_module"
1026
1182
  },
1027
1183
  warnings: []
@@ -1041,8 +1197,13 @@ var AccountReads = class {
1041
1197
  if (options.cursor) params.set("cursor", options.cursor);
1042
1198
  const query = params.size > 0 ? `?${params.toString()}` : "";
1043
1199
  const raw = await this.transport.get(`/machine-payments/receipts${query}`);
1200
+ const receipts = raw.receipts.map(mapPaymentReceipt);
1044
1201
  return {
1045
- 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,
1046
1207
  total: typeof raw.total === "number" ? raw.total : null,
1047
1208
  hasMore: typeof raw.has_more === "boolean" ? raw.has_more : null,
1048
1209
  nextCursor: typeof raw.next_cursor === "string" ? raw.next_cursor : null
@@ -1408,6 +1569,10 @@ function readX402ReceiptPayer(raw) {
1408
1569
  }
1409
1570
 
1410
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
+ }
1411
1576
  var X402FundingLeg = class {
1412
1577
  delegateKey;
1413
1578
  delegateAddress;
@@ -1433,20 +1598,35 @@ var X402FundingLeg = class {
1433
1598
  this.assertSignableAuthorizationState = options.assertSignableAuthorizationState;
1434
1599
  }
1435
1600
  // ── Receipt cache ────────────────────────────────────────────────
1436
- /** A still-valid cached receipt for this key, or undefined. */
1437
- 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) {
1438
1610
  const cached = this.receiptCache.get(idempotencyKey);
1439
- if (cached && cached.expiresAt > Date.now()) return cached.receipt;
1440
- 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;
1441
1621
  }
1442
- cacheReceipt(idempotencyKey, paymentHeader, receipt) {
1622
+ cacheReceipt(idempotencyKey, paymentHeader, receipt, taskBudgetId, subBudgetId) {
1443
1623
  const expiresAt = getPaymentHeaderValidBefore(paymentHeader);
1444
1624
  if (expiresAt > Date.now()) {
1445
- this.receiptCache.set(idempotencyKey, { expiresAt, receipt });
1625
+ this.receiptCache.set(idempotencyKey, { expiresAt, taskBudgetId, subBudgetId, receipt });
1446
1626
  }
1447
1627
  }
1448
1628
  // ── Authorization ────────────────────────────────────────────────
1449
- async authorize(paymentRequired, option, idempotencyKey) {
1629
+ async authorize(paymentRequired, option, idempotencyKey, taskBudgetId, subBudgetId) {
1450
1630
  const raw = await this.post("/x402", {
1451
1631
  url: paymentRequired.resource.url,
1452
1632
  payTo: this.delegateAddress,
@@ -1459,7 +1639,15 @@ var X402FundingLeg = class {
1459
1639
  // #1360: same explicit funding-leg declaration as createX402Intent —
1460
1640
  // this local-key path derives payTo from the key (never stale), but the
1461
1641
  // declaration keeps both writers of the 3009 shape loud-by-default.
1462
- settlementScheme: "eip3009"
1642
+ settlementScheme: "eip3009",
1643
+ // #3329: `/x402` bodies are camelCase (`X402AuthorizeRequest`) — do not
1644
+ // switch this to `task_budget_id`, which is only the `POST /payments` key.
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 } : {}
1463
1651
  });
1464
1652
  const state = paymentStateFromRaw("x402 payment", raw);
1465
1653
  const executedReplay = raw.success && raw.tx_hash ? "idempotency-collision" : state?.nextAction === AgentPaymentNextAction.RetryOriginalX402Request ? "approval-resume" : null;
@@ -1482,19 +1670,22 @@ var X402FundingLeg = class {
1482
1670
  const paymentHeader = await this.createPaymentHeader(paymentRequired, option);
1483
1671
  if (raw.success && raw.tx_hash) {
1484
1672
  const receipt2 = this.receiptFromAuthorization(paymentRequired, option, paymentHeader, raw);
1485
- this.cacheReceipt(idempotencyKey, paymentHeader, receipt2);
1673
+ this.cacheReceipt(idempotencyKey, paymentHeader, receipt2, taskBudgetId, subBudgetId);
1486
1674
  return receipt2;
1487
1675
  }
1488
1676
  if (state?.nextAction === AgentPaymentNextAction.RetryOriginalX402Request) {
1489
1677
  const receipt2 = this.receiptFromStatus(paymentRequired, option, paymentHeader, state);
1490
- this.cacheReceipt(idempotencyKey, paymentHeader, receipt2);
1678
+ this.cacheReceipt(idempotencyKey, paymentHeader, receipt2, taskBudgetId, subBudgetId);
1491
1679
  return receipt2;
1492
1680
  }
1493
1681
  this.assertSignableAuthorizationState("x402 payment", raw);
1494
1682
  if (!raw.sign_data?.hash) {
1495
1683
  throw new HavenApiError("No sign_hash returned from x402/authorize", 500, raw);
1496
1684
  }
1497
- const sig = await this.signForData(raw.sign_data);
1685
+ const sig = await this.signForData(raw.sign_data, {
1686
+ asset: option.asset,
1687
+ amount: x402AuthorizationAmount(option)
1688
+ });
1498
1689
  const execResult = await this.post(
1499
1690
  `/payments/${raw.payment_id}/sign`,
1500
1691
  { signature: sig }
@@ -1507,7 +1698,7 @@ var X402FundingLeg = class {
1507
1698
  execResult.chain_id ?? chainIdFromNetwork(option.network)
1508
1699
  );
1509
1700
  const receipt = this.receiptFromAuthorization(paymentRequired, option, paymentHeader, raw, execResult);
1510
- this.cacheReceipt(idempotencyKey, paymentHeader, receipt);
1701
+ this.cacheReceipt(idempotencyKey, paymentHeader, receipt, taskBudgetId, subBudgetId);
1511
1702
  return receipt;
1512
1703
  }
1513
1704
  // ── Header minting ───────────────────────────────────────────────
@@ -1770,11 +1961,30 @@ var X402Erc7710 = class {
1770
1961
  ...selection.facilitatorAddresses ? { facilitatorAddresses: selection.facilitatorAddresses } : {},
1771
1962
  // #1307/#1547: persisted so the settle leg can rehydrate the merchant
1772
1963
  // call by payment_id on this scheme too, not only on the 3009 bridge.
1773
- ...options.mcpCallContext ? { mcpCallContext: options.mcpCallContext } : {}
1964
+ ...options.mcpCallContext ? { mcpCallContext: options.mcpCallContext } : {},
1965
+ // #3329: `/x402` bodies are camelCase (`X402AuthorizeRequest`) — do not
1966
+ // switch this to `task_budget_id`, which is only the `POST /payments` key.
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 } : {}
1774
1971
  });
1775
1972
  if (!raw.payment_id) {
1776
1973
  throw new HavenApiError("No payment_id returned from x402/authorize", 500, raw);
1777
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
+ }
1778
1988
  const signData = raw.sign_data;
1779
1989
  if (signData?.signature_scheme !== "eip712_delegation" || !signData.typed_data) {
1780
1990
  throw new HavenApiError(
@@ -1805,10 +2015,20 @@ var X402Erc7710 = class {
1805
2015
  * hosted boundary) is irrelevant here.
1806
2016
  */
1807
2017
  async submit(paymentId, signature) {
1808
- const settled = await this.post(
1809
- `/x402/${paymentId}/settle`,
1810
- { signature }
1811
- );
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
+ }
1812
2032
  if (!settled.payment_header) {
1813
2033
  throw new HavenApiError(
1814
2034
  "x402 settle returned no payment_header \u2014 the merchant cannot be retried.",
@@ -2371,6 +2591,15 @@ var HavenClient = class {
2371
2591
  pollingInterval;
2372
2592
  chainRpcs;
2373
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();
2374
2603
  /**
2375
2604
  * The EIP-3009 funding-leg lifecycle (#1618). The facade holds a reference
2376
2605
  * and delegates; it does not reimplement any of it.
@@ -2416,7 +2645,7 @@ var HavenClient = class {
2416
2645
  x402Wallet: this.x402Wallet,
2417
2646
  chainRpcs: this.chainRpcs,
2418
2647
  post: (path, body) => this.post(path, body),
2419
- signForData: (signData) => this.signForData(signData),
2648
+ signForData: (signData, fundingExpectation) => this.signForData(signData, void 0, fundingExpectation),
2420
2649
  assertSignableAuthorizationState: (label, raw) => this.throwIfNonSignableAuthorizationState(label, raw)
2421
2650
  });
2422
2651
  this.merchantCompletion = new MerchantCompletion({
@@ -2493,7 +2722,11 @@ var HavenClient = class {
2493
2722
  token: request.token,
2494
2723
  amount: request.amount,
2495
2724
  to: request.to,
2496
- ...request.idempotencyKey ? { idempotency_key: request.idempotencyKey } : {}
2725
+ ...request.idempotencyKey ? { idempotency_key: request.idempotencyKey } : {},
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 } : {}
2497
2730
  });
2498
2731
  if (raw.status === "pending_approval") {
2499
2732
  throwPaymentStateError("Payment", raw);
@@ -2553,7 +2786,14 @@ var HavenClient = class {
2553
2786
  // the agent passes only payment_id. Bounded: the backend rejects >64KB,
2554
2787
  // so an oversized blob is omitted here (signer falls back to the
2555
2788
  // caller-supplied copy) rather than failing the intent.
2556
- ...new TextEncoder().encode(JSON.stringify(paymentRequired)).length <= 65536 ? { paymentRequired } : {}
2789
+ ...new TextEncoder().encode(JSON.stringify(paymentRequired)).length <= 65536 ? { paymentRequired } : {},
2790
+ // #3329: `/x402` bodies are camelCase (`X402AuthorizeRequest`), unlike
2791
+ // `POST /payments`'s snake_case `task_budget_id` above — the two
2792
+ // surfaces use different conventions, so this key is NOT `task_budget_id`.
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 } : {}
2557
2797
  });
2558
2798
  if (raw.status !== "pending_signature") {
2559
2799
  throwPaymentStateError("x402 payment", raw);
@@ -2625,7 +2865,7 @@ var HavenClient = class {
2625
2865
  * (#834) — the backend refuses those intents with HTTP 410 before any
2626
2866
  * sign_data reaches a client, so encountering it here is a hard error too.
2627
2867
  */
2628
- async signForData(signData, settlementExpectation) {
2868
+ async signForData(signData, settlementExpectation, fundingExpectation) {
2629
2869
  if (!this.delegateKey) {
2630
2870
  throw new HavenSigningError(
2631
2871
  "Cannot sign without a delegateKey. Pass the private key in HavenClient config, or sign externally."
@@ -2648,6 +2888,13 @@ var HavenClient = class {
2648
2888
  signData.typed_data,
2649
2889
  this.delegateAddress
2650
2890
  );
2891
+ if (fundingExpectation) {
2892
+ assertFundingLegPaysDelegate(signData.typed_data, {
2893
+ delegateAddress: this.delegateAddress,
2894
+ asset: fundingExpectation.asset,
2895
+ amount: fundingExpectation.amount
2896
+ });
2897
+ }
2651
2898
  return signUserOpTypedDataForDelegation(this.delegateKey, signData.typed_data);
2652
2899
  }
2653
2900
  if (scheme === "eip712_delegation") {
@@ -2926,7 +3173,12 @@ var HavenClient = class {
2926
3173
  async listReceipts(options = {}) {
2927
3174
  return this.accountReads.listReceipts(options);
2928
3175
  }
2929
- /** #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
+ */
2930
3182
  async listReceiptsPage(options = {}) {
2931
3183
  return this.accountReads.listReceiptsPage(options);
2932
3184
  }
@@ -2939,6 +3191,96 @@ var HavenClient = class {
2939
3191
  async getReceipt(paymentId) {
2940
3192
  return this.accountReads.getReceipt(paymentId);
2941
3193
  }
3194
+ // ── Task Budgets (#3329) ────────────────────────────────────────────
3195
+ /**
3196
+ * Open a task budget: a self-delegated, time-boxed slice of the agent's own
3197
+ * budget delegation. Returns the pending row plus the child delegation's
3198
+ * `sign_data` — sign it (via `signForData` / `haven_sign` with
3199
+ * `task_budget_id`) and pass the signature to `submitTaskBudget` to make it
3200
+ * OPEN. Nothing is on-chain until then.
3201
+ */
3202
+ async openTaskBudget(request) {
3203
+ const raw = await this.post("/task-budgets", {
3204
+ ...request.tokenAddress ? { token_address: request.tokenAddress } : {},
3205
+ max_amount_atomic: request.maxAmountAtomic,
3206
+ ttl_seconds: request.ttlSeconds,
3207
+ ...request.recipientAddress ? { recipient_address: request.recipientAddress } : {},
3208
+ ...request.label ? { label: request.label } : {}
3209
+ });
3210
+ return {
3211
+ taskBudget: mapTaskBudget(raw.task_budget),
3212
+ signData: raw.sign_data,
3213
+ nextAction: raw.next_action,
3214
+ instructions: raw.instructions
3215
+ };
3216
+ }
3217
+ /** Read one task budget by id. */
3218
+ async getTaskBudget(id) {
3219
+ const raw = await this.get(`/task-budgets/${encodeURIComponent(id)}`);
3220
+ return mapTaskBudget(raw.task_budget);
3221
+ }
3222
+ /** List this agent's task budgets — `status: 'open'` (default; unexpired only) or `'all'`. */
3223
+ async listTaskBudgets(options = {}) {
3224
+ const query = options.status ? `?status=${encodeURIComponent(options.status)}` : "";
3225
+ const raw = await this.get(`/task-budgets${query}`);
3226
+ return raw.task_budgets.map(mapTaskBudget);
3227
+ }
3228
+ /**
3229
+ * The exact signing payload for a pending (purpose `open`) or closing
3230
+ * (purpose `close`) task budget — the `task_budget_id` counterpart of
3231
+ * `getResumeState` / the payment sign-context reads. `haven_sign` with
3232
+ * `task_budget_id` fetches this itself; call it directly only for manual
3233
+ * signing.
3234
+ */
3235
+ async getTaskBudgetSignContext(id) {
3236
+ const raw = await this.get(`/task-budgets/${encodeURIComponent(id)}/sign-context`);
3237
+ if (raw.purpose === "close") {
3238
+ return {
3239
+ taskBudgetId: raw.task_budget_id,
3240
+ purpose: "close",
3241
+ taskSignContextVersion: raw.task_sign_context_version,
3242
+ typedData: raw.typed_data,
3243
+ userOperation: raw.user_operation,
3244
+ userOpHash: raw.user_op_hash,
3245
+ expected: raw.expected
3246
+ };
3247
+ }
3248
+ return {
3249
+ taskBudgetId: raw.task_budget_id,
3250
+ purpose: "open",
3251
+ taskSignContextVersion: raw.task_sign_context_version,
3252
+ typedData: raw.typed_data,
3253
+ expected: raw.expected
3254
+ };
3255
+ }
3256
+ /** Submit a signature for a pending (open) or closing task budget. */
3257
+ async submitTaskBudget(id, signature) {
3258
+ const raw = await this.post(
3259
+ `/task-budgets/${encodeURIComponent(id)}/submit`,
3260
+ { signature }
3261
+ );
3262
+ return {
3263
+ taskBudget: mapTaskBudget(raw.task_budget),
3264
+ status: raw.status,
3265
+ ...raw.close_tx_hash ? { closeTxHash: raw.close_tx_hash } : {}
3266
+ };
3267
+ }
3268
+ /**
3269
+ * Close a task budget early. A `pending` or already-expired `open` budget
3270
+ * closes immediately (nothing was ever signed or is on-chain). A live
3271
+ * `open` budget returns `sign_data` for an on-chain `disableDelegation`
3272
+ * UserOp — sign it (`haven_sign` with `task_budget_id`) and call
3273
+ * `submitTaskBudget`.
3274
+ */
3275
+ async closeTaskBudget(id) {
3276
+ const raw = await this.post(`/task-budgets/${encodeURIComponent(id)}/close`, {});
3277
+ return {
3278
+ taskBudget: mapTaskBudget(raw.task_budget),
3279
+ status: raw.status,
3280
+ ...raw.sign_data ? { signData: raw.sign_data } : {},
3281
+ ...raw.next_action ? { nextAction: raw.next_action } : {}
3282
+ };
3283
+ }
2942
3284
  /**
2943
3285
  * Rehydrate the x402 resume-state bundle for a payment id (#1328: the MPP
2944
3286
  * resume-state variant retired along with the rest of the mpp_demo surface).
@@ -2987,12 +3329,30 @@ var HavenClient = class {
2987
3329
  throw noCompatiblePaymentOptionError(paymentRequired.accepts);
2988
3330
  }
2989
3331
  const idempotencyKey = options.idempotencyKey ?? buildX402IdempotencyKey(paymentRequired, option);
2990
- const cached = this.fundingLeg.cachedReceipt(idempotencyKey);
3332
+ const cached = this.fundingLeg.cachedReceipt(idempotencyKey, options.taskBudgetId, options.subBudgetId);
2991
3333
  if (cached) return cached;
2992
3334
  const inFlight = this.inFlightX402.get(idempotencyKey);
2993
- if (inFlight) return inFlight;
2994
- const promise = this.fundingLeg.authorize(paymentRequired, option, idempotencyKey);
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
+ );
2995
3353
  this.inFlightX402.set(idempotencyKey, promise);
3354
+ this.inFlightX402TaskBudget.set(idempotencyKey, options.taskBudgetId);
3355
+ this.inFlightX402SubBudget.set(idempotencyKey, options.subBudgetId);
2996
3356
  try {
2997
3357
  return await promise;
2998
3358
  } catch (err) {
@@ -3004,6 +3364,8 @@ var HavenClient = class {
3004
3364
  throw err;
3005
3365
  } finally {
3006
3366
  this.inFlightX402.delete(idempotencyKey);
3367
+ this.inFlightX402TaskBudget.delete(idempotencyKey);
3368
+ this.inFlightX402SubBudget.delete(idempotencyKey);
3007
3369
  }
3008
3370
  }
3009
3371
  /**
@@ -3073,7 +3435,7 @@ var HavenClient = class {
3073
3435
  async payX402Quote(quote, options = {}) {
3074
3436
  const idempotencyKey = options.idempotencyKey ?? quote.idempotencyKey;
3075
3437
  try {
3076
- const receipt = await this.authorizeX402(quote.paymentRequired, { idempotencyKey });
3438
+ const receipt = await this.authorizeX402(quote.paymentRequired, { ...options, idempotencyKey });
3077
3439
  return this.merchantCompletion.retryRequest(
3078
3440
  quote.request.url,
3079
3441
  requestInitFromSnapshot(quote.request),
@@ -3139,7 +3501,7 @@ var HavenClient = class {
3139
3501
  throw noCompatiblePaymentOptionError(input.paymentRequired.accepts);
3140
3502
  }
3141
3503
  const idempotencyKey = input.idempotencyKey ?? buildX402IdempotencyKey(input.paymentRequired, option);
3142
- const cached = this.fundingLeg.cachedReceipt(idempotencyKey);
3504
+ const cached = this.fundingLeg.cachedReceipt(idempotencyKey, input.taskBudgetId);
3143
3505
  if (cached) return cached;
3144
3506
  const status = await this.getPaymentStatus(input.paymentId);
3145
3507
  assertCanResumeX402(status, input.paymentRequired, option);
@@ -3157,7 +3519,7 @@ var HavenClient = class {
3157
3519
  }
3158
3520
  const paymentHeader = await this.fundingLeg.createPaymentHeader(input.paymentRequired, option);
3159
3521
  const receipt = this.fundingLeg.receiptFromStatus(input.paymentRequired, option, paymentHeader, status);
3160
- this.fundingLeg.cacheReceipt(idempotencyKey, paymentHeader, receipt);
3522
+ this.fundingLeg.cacheReceipt(idempotencyKey, paymentHeader, receipt, input.taskBudgetId);
3161
3523
  return receipt;
3162
3524
  }
3163
3525
  async resumeX402Payment(input) {
@@ -3584,7 +3946,7 @@ var toolDescriptions = {
3584
3946
  // deleted `/demo/mpp/*` route. Use the x402 fragments above instead.
3585
3947
  getPaymentStatus: {
3586
3948
  summary: "Fetch structured Haven payment status for agent recovery.",
3587
- 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.",
3588
3950
  nextActionGuidance: ""
3589
3951
  },
3590
3952
  getResumeState: {
@@ -3619,9 +3981,9 @@ var toolDescriptions = {
3619
3981
  nextActionGuidance: ""
3620
3982
  },
3621
3983
  verifyReceipt: {
3622
- summary: "Verify a payment receipt offline \u2014 confirm the agent authorised the transfer.",
3623
- 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.",
3624
- 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.",
3625
3987
  nextActionGuidance: ""
3626
3988
  },
3627
3989
  payMcpTool: {
@@ -3651,7 +4013,7 @@ var toolDescriptions = {
3651
4013
  send: {
3652
4014
  summary: "Send ETH or USDC directly from the agent's Haven wallet to a recipient address.",
3653
4015
  selectionGuidance: "Use this for plain transfers \u2014 refunding a user, paying a freelancer, topping up a co-agent's wallet, or moving funds between addresses. Do NOT use for x402 paid endpoints \u2014 use haven_pay_x402 instead. Do NOT use for read-only allowance, budget, or what-can-I-spend questions \u2014 use haven_get_allowances. Do NOT use to check whether funds are held before sending \u2014 use haven_check_funds.",
3654
- behavior: "Sends the amount by redeeming the agent's on-chain budget delegation, account to recipient with no funding leg. Budget, recipient and expiry are enforced on-chain while the transfer is prepared, so a request outside them is declined before any money moves or any signing \u2014 never queued for a human to approve. The agent's signing key signs the account's typed data, refusing any that mismatches the payment's hash; Haven never receives the key.",
4016
+ behavior: "Sends the amount by redeeming the agent's on-chain budget delegation, account to recipient with no funding leg. Budget, recipient and expiry are enforced on-chain while the transfer is prepared, so a request outside them is declined before any money moves or any signing \u2014 never queued for a human to approve. The agent's signing key signs the account's typed data; a current signer refuses any that does not match the payment's hash (USEROP_BINDING_MISMATCH), an older signer signs it and the account rejects it on-chain. Haven never receives the key.",
3655
4017
  nextActionGuidance: "On a decline, report the reason to the user and ask them to grant or raise the budget in Haven \u2014 there is nothing to poll and no approval will arrive. After a successful send, poll haven_get_payment_status until nextAction=none."
3656
4018
  },
3657
4019
  reportSettlementEvidence: {
@@ -3908,7 +4270,7 @@ Your user gives you a **budget on their own account** \u2014 not their wallet, a
3908
4270
 
3909
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.
3910
4272
 
3911
- 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.
3912
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.
3913
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.
3914
4276
  4. **YOU \u2014 run the connector command** in that prompt (below). It makes your signing key locally, registering only the public address.
@@ -3984,6 +4346,10 @@ Call \`haven_get_agent\`, one of the Haven MCP tools the connector wires into yo
3984
4346
 
3985
4347
  Nothing here needs you to. Steps 1-3 are links: hand your user the full \`<host>/\u2026\` URL and ask them to say when it is done. Step 5 is a link only when your run reported one in \`approval.url\` \u2014 otherwise it is the tab they already have open, as above. Then poll \`haven_get_agent\` until it reads \`ready\`. Do not route around the sign-in wall \u2014 it makes the account theirs, not yours.
3986
4348
 
4349
+ ## If something breaks
4350
+
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).
4352
+
3987
4353
  ## Vocabulary
3988
4354
 
3989
4355
  | Term | What it is |
@@ -4002,12 +4368,12 @@ Start by reading {{HAVEN_ORIGIN}}/for-agents.md \u2014 it is written for you and
4002
4368
 
4003
4369
  Then:
4004
4370
 
4005
- 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.
4006
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.
4007
4373
  3. ${AGENT_APPROVAL_RELAY_JSON_SENTENCE}
4008
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.
4009
4375
 
4010
- 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.
4011
4377
 
4012
4378
  ${AGENT_SECRET_HYGIENE_SENTENCE}`;
4013
4379
 
@@ -4151,28 +4517,43 @@ the budget in the Haven dashboard, or wait for the period reset.
4151
4517
  4. Then FOLLOW THE RESPONSE'S GUIDANCE FIELDS: \`next_action\`, \`next_tool\`,
4152
4518
  and \`next_arguments\` name the exact next call \u2014 act on those first; the
4153
4519
  prose in this section is fallback and debugging detail. If the catalog
4154
- entry is missing or degraded, the response instead names
4155
- \`mcp__haven__haven_pay_mcp_tool\` (merchant URL, tool name, arguments) as
4156
- 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.
4157
4525
 
4158
- **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
4159
4534
  the local signer fetches the exact signing bytes AND \`payment_required\`
4160
- itself, so never relay \`typed_data\` or the 402 blob yourself. If the signer
4161
- reports its fetched context carried no \`payment_required\` (older backend),
4162
- re-call with \`payment_required\` added verbatim. Fallback for an older signer
4163
- or backend: re-run the quote/prepare tool with the SAME \`idempotency_key\`
4164
- plus \`include_signing_payload=true\`, then pass \`payload_hash\`,
4165
- \`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
4166
4543
  \`typed_data\`/\`typed_data_b64\` through unchanged.
4167
4544
 
4168
- **Settle:** \`mcp__haven__haven_settle_mcp_tool\` with \`payment_id\`,
4169
- \`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
4170
4550
  context (\`merchant_url\`, \`tool_name\`, \`arguments\`, \`mcp_transport\`)
4171
4551
  server-side from \`payment_id\`. Pass those four fields explicitly only as a
4172
4552
  version-skew fallback when Haven has no stored context for the id \u2014 both or
4173
- none together, never just one. If the settle result carries \`settled: false\`,
4174
- funding has not confirmed \u2014 follow the result's guidance fields and check
4175
- 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.
4176
4557
 
4177
4558
  Step-by-step alternative (also key-safe; for an older signer or backend, or
4178
4559
  when you already have a merchant URL and tool name instead of a
@@ -4362,6 +4743,6 @@ function sameUrl(a, b) {
4362
4743
  }
4363
4744
  }
4364
4745
 
4365
- 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 };
4366
4747
  //# sourceMappingURL=index.js.map
4367
4748
  //# sourceMappingURL=index.js.map