@haven_ai/sdk 0.3.0-alpha.0 → 0.5.0-alpha.1

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,464 +1,13 @@
1
- import { recoverTypedDataAddress, hashTypedData } from 'viem';
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';
2
6
  import { ethers } from 'ethers';
3
- import { createHash } from 'crypto';
4
7
  import { AsyncLocalStorage } from 'async_hooks';
5
8
  import { exact } from 'x402/schemes';
6
9
  import { privateKeyToAccount } from 'viem/accounts';
7
10
 
8
- // src/client.ts
9
-
10
- // src/connector-channel.ts
11
- var CONNECTOR_PACKAGE_NAME = "@haven_ai/connect";
12
- var HAVEN_CONNECTOR_CHANNEL = "alpha";
13
- var CHANNEL_PATTERN = /^[a-z][a-z0-9-]{0,31}$/;
14
- function isConnectorChannel(value) {
15
- return CHANNEL_PATTERN.test(value);
16
- }
17
- function resolveConnectorChannel(raw, fallback = HAVEN_CONNECTOR_CHANNEL) {
18
- const trimmed = (raw ?? "").trim();
19
- if (trimmed === "") return fallback;
20
- if (!isConnectorChannel(trimmed)) {
21
- throw new Error(
22
- `HAVEN_CONNECTOR_CHANNEL is set to ${JSON.stringify(raw)}, which is not a valid npm dist-tag (lowercase letter first, then letters, digits or hyphens). Refusing to start rather than fall back to the default channel, because falling back would hand out the production connector while looking configured.`
23
- );
24
- }
25
- return trimmed;
26
- }
27
- function connectorSpec(channel = HAVEN_CONNECTOR_CHANNEL) {
28
- return `${CONNECTOR_PACKAGE_NAME}@${channel}`;
29
- }
30
- function connectorRerunCommand(args, options) {
31
- const channel = options?.channel ?? HAVEN_CONNECTOR_CHANNEL;
32
- const flags = options?.npxFlags ? `${options.npxFlags} ` : "";
33
- const command = `npx ${flags}${connectorSpec(channel)}`;
34
- return args ? `${command} ${args}` : command;
35
- }
36
-
37
- // src/types.ts
38
- var DEFAULT_CONFIRMATION_TIMEOUT_MS = 9e4;
39
- var AgentPaymentPhase = {
40
- /** The agent must sign and submit the prepared payment before Haven can relay it. */
41
- AgentSignatureRequired: "agent_signature_required",
42
- /** Haven has received the signed payment and the agent should poll for confirmation. */
43
- PaymentSubmitted: "payment_submitted",
44
- /** The direct payment is confirmed; the agent does not need to do more for this payment id. */
45
- PaymentConfirmed: "payment_confirmed",
46
- /**
47
- * #2115: RETIRED wire value — no live rail produces it. It described the
48
- * Safe rail's approval queue, which no longer exists. Kept so a stored value
49
- * still typechecks; see `AgentPaymentPhaseDescriptions` below for the
50
- * agent-visible wording, which this comment used to contradict.
51
- */
52
- UserApprovalRequired: "user_approval_required",
53
- /** #2115: RETIRED wire value — no live rail produces it. Stop and tell the user. */
54
- UserExecutionRequired: "user_execution_required",
55
- /** #2115: RETIRED wire value — no live rail produces it. Stop and tell the user. */
56
- WaitingForAdditionalApprovals: "waiting_for_additional_approvals",
57
- /** The Haven funding leg was sent; the agent can continue the merchant/protocol leg. */
58
- FundingSent: "funding_sent",
59
- /** The payment was rejected and cannot proceed; the agent should stop and tell the user. */
60
- Rejected: "rejected",
61
- /** The payment expired before completion. */
62
- Expired: "expired",
63
- /** Haven could not complete the payment; the agent should stop and surface the failure. */
64
- Failed: "failed",
65
- /**
66
- * Pre-flight check determined the delegate's existing balance plus the
67
- * remaining on-chain budget cannot cover the requested amount, so no
68
- * payment intent was created. The account must be funded or the agent's
69
- * budget raised before retrying — #2115: the old wording contrasted this
70
- * with `UserApprovalRequired` as if that were a live alternative, and named
71
- * the retired rail's Safe and per-token allowance as the fix.
72
- */
73
- InsufficientFunds: "insufficient_funds",
74
- /**
75
- * Haven's funding leg (account → delegate, the #946 EIP-3009 bridge)
76
- * confirmed on-chain, but the merchant rejected the x402 retry. The delegate
77
- * wallet may hold stranded USDC that was never settled to the merchant. The
78
- * agent should stop, tell the user, and wait for the sweep flow to reclaim
79
- * the funds.
80
- */
81
- FundedButUnsettled: "funded_but_unsettled"
82
- };
83
- var AgentPaymentNextAction = {
84
- /** Sign with the delegate key and submit the payment to Haven. */
85
- SignAndSubmitPayment: "sign_and_submit_payment",
86
- /** Poll getPaymentStatus later using this payment id. */
87
- CheckStatusLater: "check_status_later",
88
- /** No further agent action is required for this payment id. */
89
- None: "none",
90
- /**
91
- * #2115: RETIRED wire value — no live rail produces it and nothing maps to
92
- * it. Stop and tell the user rather than polling; no approval will arrive.
93
- */
94
- WaitForUserApproval: "wait_for_user_approval",
95
- /** #2115: RETIRED wire value — no live rail produces it. Stop and tell the user rather than polling. */
96
- WaitForUserToCompletePayment: "wait_for_user_to_complete_payment",
97
- /** Resume this payment id and retry the original x402 request with the merchant payment header. */
98
- RetryOriginalX402Request: "retry_original_x402_request",
99
- /** Stop retrying this payment and tell the user what happened. */
100
- StopAndTellUser: "stop_and_tell_user",
101
- /** Ask again only if the user still wants the payment after expiry. */
102
- RequestAgainIfUserStillWantsIt: "request_again_if_user_still_wants_it",
103
- /** #1307: retry the SAME tool call, supplying the explicit context fields the server could not rehydrate. */
104
- RetryWithExplicitContext: "retry_with_explicit_context",
105
- /**
106
- * The x402 funding/quote window expired. Re-quote the same logical merchant
107
- * operation with the same idempotency key to stay double-charge-safe.
108
- */
109
- PaymentWindowExpired: "payment_window_expired",
110
- /**
111
- * Stop and tell the user that the originating account needs to be funded or
112
- * the agent's per-token allowance needs to be raised before the payment
113
- * can succeed. A user approval will not fix this state on its own.
114
- *
115
- * #2914: the account-vocabulary spelling, and the only one — the
116
- * pre-#2907 `fund_safe_or_raise_allowance` wire value (and the
117
- * `AgentPaymentNextActionAccountAlias` seam #2908 added to bridge it) are
118
- * retired along with the rest of the #2908 compatibility window.
119
- */
120
- FundAccountOrRaiseAllowance: "fund_account_or_raise_allowance",
121
- /**
122
- * The delegate wallet may hold funds that were sent from the Safe but never
123
- * settled to the merchant. The wallet owner should initiate a sweep to
124
- * return those funds to the originating Safe.
125
- */
126
- SweepStrandedFunds: "sweep_stranded_funds",
127
- /**
128
- * #2970: a `submitted` erc7710 x402 intent whose settlement window has
129
- * passed with no on-chain settlement evidence Haven could verify. Distinct
130
- * from {@link CheckStatusLater}, which this REPLACES once the window is
131
- * past — but it is not futile: Haven's settlement sweep (120s tick) scans
132
- * each candidate over its own window plus a 120s clock-skew allowance, so
133
- * it can still attribute the settlement for a short while after this value
134
- * first appears. Poll {@link CheckStatusLater}'s tool
135
- * (`haven_get_payment_status`) once more, roughly two minutes later; if it
136
- * still shows no evidence, tell the user the goods were delivered but
137
- * Haven holds no verified settlement evidence for this payment. If the
138
- * agent holds the merchant's real settlement transaction hash (from
139
- * `PAYMENT-RESPONSE`'s `transaction` field, or a prior settle/complete
140
- * result's `settlement_tx_hash`), report it with the hosted
141
- * `haven_report_settlement_evidence` tool instead of waiting —
142
- * `haven_report_x402_outcome` takes no hash and refuses a non-`confirmed`
143
- * intent.
144
- */
145
- AwaitingSettlementEvidence: "awaiting_settlement_evidence"
146
- };
147
- var AgentPaymentFailureCode = {
148
- /** A merchant-authoritative x402 price exceeds the caller's pre-funding max_amount cap. */
149
- PriceExceedsMax: "PRICE_EXCEEDS_MAX",
150
- /** The x402 funding/quote window expired before the signer or hosted settle step could finish. */
151
- PaymentWindowExpired: "PAYMENT_WINDOW_EXPIRED",
152
- /** The merchant rejected the paid retry. On eip3009 the funding leg had succeeded (sweep);
153
- * on erc7710 there is no funding leg — nothing to sweep, follow the message (#2983). */
154
- MerchantRejectedAfterFunding: "MERCHANT_REJECTED_AFTER_FUNDING",
155
- /** #1300 review: funding is on-chain but the merchant never ANSWERED the
156
- * paid retry within the timeout. NOT proof of rejection — the merchant
157
- * may still settle late, so the guidance is verify-then-act. On eip3009
158
- * the funding leg had succeeded (verify-then-sweep); on erc7710 there is
159
- * no funding leg — nothing to sweep, follow the message (#3000). */
160
- MerchantUnresponsiveAfterFunding: "MERCHANT_UNRESPONSIVE_AFTER_FUNDING",
161
- /**
162
- * #1307: the caller omitted merchant_url/tool_name (asking Haven to
163
- * rehydrate the stored MCP merchant-call context by payment_id), but no
164
- * usable context was stored for this intent — either it was never an
165
- * MCP-tool quote, or the stored context is incomplete. The fallback is
166
- * mechanical: re-send merchant_url, tool_name, arguments, and
167
- * mcp_transport explicitly (the version-skew path).
168
- */
169
- MerchantCallContextUnavailable: "MERCHANT_CALL_CONTEXT_UNAVAILABLE",
170
- /**
171
- * #1351: the caller supplied BOTH the atomic `max_amount` and the
172
- * human-denominated `max_amount_human` cap for one purchase. Haven refuses
173
- * to guess which the user meant — the two differ by a factor of 10^decimals,
174
- * so picking wrong is exactly the silent-overspend this cap exists to
175
- * prevent. Rejected before any merchant probe, funding intent, or signature.
176
- */
177
- AmbiguousMaxAmount: "AMBIGUOUS_MAX_AMOUNT",
178
- /**
179
- * #1351: a human-denominated cap was supplied, but it cannot be converted to
180
- * atomic units against THIS quote — either the quote's asset has no known
181
- * decimals on its network, or the cap carries more fraction digits than the
182
- * asset can represent (truncating it would silently change the user's cap).
183
- * The fallback is the exact atomic `max_amount`.
184
- */
185
- MaxAmountUnconvertible: "MAX_AMOUNT_UNCONVERTIBLE",
186
- /**
187
- * #2979: the merchant answered a `tools/call` probe with its own
188
- * machine-readable "cannot settle right now" refusal (HTTP 503,
189
- * `{ error: 'merchant_not_ready', reason_code, ... }`) instead of a 402
190
- * challenge — e.g. its settlement wallet is out of gas. No 402 was ever
191
- * issued and no payment was created; this is honest and (per
192
- * `retry_after_s`, when present) usually transient, unlike a permanent
193
- * endpoint miss.
194
- */
195
- MerchantNotReady: "MERCHANT_NOT_READY"
196
- };
197
- var AgentPaymentRail = {
198
- /** Standard Haven payment from the user's Safe through an approved delegate allowance. */
199
- Direct: "direct",
200
- /** x402 HTTP 402 payment flow with a Haven funding leg and merchant retry leg. */
201
- X402: "x402",
202
- /** Machine Payment Protocol family — categorical value used as a resume-state discriminator. */
203
- Mpp: "mpp",
204
- /** Haven internal MPP demo rail. Not for production traffic. */
205
- MppDemo: "mpp_demo",
206
- /** Crypto-settled MPP rail. */
207
- MppCrypto: "mpp_crypto",
208
- /** Stripe-deposit-backed MPP rail. */
209
- StripeDeposit: "stripe_deposit",
210
- /** Stripe Payment Token MPP rail. */
211
- Spt: "spt"
212
- };
213
- var AGENT_PAYMENT_PHASE_VALUES = Object.values(AgentPaymentPhase);
214
- var AGENT_PAYMENT_NEXT_ACTION_VALUES = Object.values(AgentPaymentNextAction);
215
- var AGENT_PAYMENT_FAILURE_CODE_VALUES = Object.values(AgentPaymentFailureCode);
216
- var AGENT_PAYMENT_RAIL_VALUES = Object.values(AgentPaymentRail);
217
- var AgentPaymentPhaseDescriptions = {
218
- [AgentPaymentPhase.AgentSignatureRequired]: "The agent must sign and submit the prepared payment before Haven can relay it.",
219
- [AgentPaymentPhase.PaymentSubmitted]: "Haven has received the signed payment and the agent should poll for confirmation.",
220
- [AgentPaymentPhase.PaymentConfirmed]: "The direct payment is confirmed; the agent does not need to do more for this payment id.",
221
- [AgentPaymentPhase.UserApprovalRequired]: "Retired wire value: no live rail produces it. It described the Safe rail's approval queue, which no longer exists \u2014 an out-of-policy payment is declined before any money moves. If it is ever seen, stop and tell the user; no approval is pending.",
222
- [AgentPaymentPhase.UserExecutionRequired]: "Retired wire value: no live rail produces it. Stop and tell the user.",
223
- [AgentPaymentPhase.WaitingForAdditionalApprovals]: "Retired wire value: no live rail produces it. Stop and tell the user.",
224
- [AgentPaymentPhase.FundingSent]: "The Haven funding leg was sent; the agent can continue the merchant/protocol leg.",
225
- [AgentPaymentPhase.Rejected]: "The payment was rejected and cannot proceed; the agent should stop and tell the user.",
226
- [AgentPaymentPhase.Expired]: "The payment expired before completion.",
227
- [AgentPaymentPhase.Failed]: "Haven could not complete the payment; the agent should stop and surface the failure.",
228
- [AgentPaymentPhase.InsufficientFunds]: "Pre-flight check determined the delegate balance plus the remaining on-chain budget cannot cover the requested amount, so no payment was created. The account must be funded or the agent budget raised before retrying.",
229
- [AgentPaymentPhase.FundedButUnsettled]: "Haven's funding leg confirmed on-chain but the merchant rejected the x402 retry. The delegate wallet may hold stranded funds. The agent should stop and wait for the wallet owner to sweep the stranded funds back to the account."
230
- };
231
- var AgentPaymentNextActionDescriptions = {
232
- [AgentPaymentNextAction.SignAndSubmitPayment]: "Sign with the delegate key and submit the payment to Haven.",
233
- [AgentPaymentNextAction.CheckStatusLater]: "Poll getPaymentStatus later using this payment id.",
234
- [AgentPaymentNextAction.None]: "No further agent action is required for this payment id.",
235
- [AgentPaymentNextAction.WaitForUserApproval]: "Retired wire value: no live rail produces it, and nothing maps to it. It described a per-payment approval queue that no longer exists. If it is ever seen, stop and tell the user rather than polling \u2014 no approval will arrive.",
236
- [AgentPaymentNextAction.WaitForUserToCompletePayment]: "Retired wire value: no live rail produces it. Stop and tell the user rather than polling.",
237
- [AgentPaymentNextAction.RetryOriginalX402Request]: "Resume this payment id and retry the original x402 request with the merchant payment header.",
238
- [AgentPaymentNextAction.StopAndTellUser]: "Stop retrying this payment and tell the user what happened.",
239
- [AgentPaymentNextAction.RequestAgainIfUserStillWantsIt]: "Ask again only if the user still wants the payment after expiry.",
240
- [AgentPaymentNextAction.PaymentWindowExpired]: "The x402 funding/quote window expired. Re-quote with the same idempotency key before asking the signer to build a merchant payment header again.",
241
- [AgentPaymentNextAction.FundAccountOrRaiseAllowance]: "Stop and tell the user that the account needs to be funded or the agent budget raised before the payment can succeed.",
242
- [AgentPaymentNextAction.RetryWithExplicitContext]: "Retry the same tool call, this time passing merchant_url, tool_name, arguments, and mcp_transport explicitly \u2014 the server had no stored context to rehydrate for this payment id.",
243
- [AgentPaymentNextAction.SweepStrandedFunds]: "Tell the user that funds may be stranded in the delegate wallet and prompt them to initiate a sweep in Haven to return them to the originating account.",
244
- [AgentPaymentNextAction.AwaitingSettlementEvidence]: "The settlement window passed with no verified on-chain evidence yet. If you hold the merchant's real settlement transaction hash, report it with haven_report_settlement_evidence. Otherwise, Haven's settlement sweep may still attribute it within about two minutes \u2014 poll getPaymentStatus once more, then tell the user the goods were delivered but unverified if it still shows nothing."
245
- };
246
- var AgentPaymentFailureCodeDescriptions = {
247
- [AgentPaymentFailureCode.PriceExceedsMax]: "The merchant-authoritative x402 amount exceeds the caller's max_amount cap. No funding transfer was created; ask the user before retrying with a larger cap.",
248
- [AgentPaymentFailureCode.PaymentWindowExpired]: "The x402 funding/quote window expired before the signer or hosted settle step could finish. Re-quote via haven_pay_mcp_tool with the same idempotency key to avoid duplicate funding.",
249
- [AgentPaymentFailureCode.MerchantRejectedAfterFunding]: "The merchant rejected the paid retry. eip3009: the funding leg succeeded \u2014 stop retrying the merchant and reconcile stranded delegate funds with haven_sweep_delegate. erc7710: no funding leg, nothing to sweep \u2014 follow the message (re-quote later, or check haven_get_payment_status after the window first).",
250
- [AgentPaymentFailureCode.MerchantUnresponsiveAfterFunding]: "The merchant did not answer the paid retry before the timeout. The merchant may still settle late. eip3009: the funding leg succeeded \u2014 check haven_get_payment_status, retry haven_complete_mcp_tool once, sweep only if no settlement appears. erc7710: no funding leg, nothing to sweep, and haven_complete_mcp_tool has no erc7710 branch \u2014 do not retry it; check haven_get_payment_status after the payment window and re-quote only if it shows no settlement.",
251
- [AgentPaymentFailureCode.MerchantCallContextUnavailable]: "merchant_url/tool_name were omitted and no stored merchant call context is available for this payment_id. Re-send merchant_url, tool_name, arguments, and mcp_transport explicitly.",
252
- [AgentPaymentFailureCode.AmbiguousMaxAmount]: "Both max_amount (atomic units) and max_amount_human (whole tokens) were supplied for one purchase. Nothing was contacted and nothing was spent. Re-send with exactly ONE: max_amount_human for a cap the user stated in tokens, max_amount for an exact atomic figure.",
253
- [AgentPaymentFailureCode.MaxAmountUnconvertible]: "max_amount_human could not be converted to atomic units against this quote's asset \u2014 either its decimals are unknown to Haven or the cap has more decimal places than the asset supports. Nothing was spent. Round the cap, or re-send it as an exact atomic max_amount.",
254
- [AgentPaymentFailureCode.MerchantNotReady]: 'The merchant refused the probe with its own "cannot settle right now" signal instead of a 402 challenge. No payment was created. Often transient \u2014 retry later (see retry_after_s in the message, if given) rather than treating this as a broken or wrong endpoint.'
255
- };
256
- var AgentPaymentWarningCode = {
257
- /** No max_amount cap was supplied — the live quoted price was accepted as-is. */
258
- MissingMaxAmount: "MISSING_MAX_AMOUNT",
259
- /** The signing window closes soon; sign promptly or re-quote with the same idempotency key. */
260
- QuoteExpiresSoon: "QUOTE_EXPIRES_SOON",
261
- /** The merchant URL was resolved via discovery — pass the RESOLVED url forward. */
262
- MerchantUrlDiscovered: "MERCHANT_URL_DISCOVERED",
263
- /**
264
- * #1306: the catalog's last-verified price_atomic differs from the LIVE
265
- * merchant quote for a guided catalog purchase. The catalog price is only
266
- * ever indicative; the live quote in the same response is authoritative.
267
- */
268
- CatalogPriceDiffers: "CATALOG_PRICE_DIFFERS",
269
- /**
270
- * #1306: the rail-aware allowance/budget pre-check could not be read (RPC
271
- * failure, etc). `sufficient` is reported as null rather than a fabricated
272
- * true/false — the on-chain policy remains the actual gate either way.
273
- */
274
- AllowanceCheckUnavailable: "ALLOWANCE_CHECK_UNAVAILABLE",
275
- /**
276
- * #1319: the delegation-rail read itself SUCCEEDED, but the remaining
277
- * figure it returned is the #1145 fallback (the full configured budget)
278
- * rather than a live ERC20PeriodTransferEnforcer read — `sufficient` is a
279
- * real true/false, just computed from an optimistic number. Distinct from
280
- * {@link AgentPaymentWarningCode.AllowanceCheckUnavailable}, which fires
281
- * when the read failed outright and `sufficient` degrades to null. The
282
- * on-chain policy re-checks at redemption either way; this only says the
283
- * guidance shown here may be optimistic.
284
- */
285
- AllowanceReadOptimistic: "ALLOWANCE_READ_OPTIMISTIC",
286
- /**
287
- * #2991: the quote tools' `expected_settlement_scheme` prediction of what
288
- * `haven_prepare_catalog_purchase` / `haven_pay_mcp_tool` will actually
289
- * select could not be computed — the agent's execution rail could not be
290
- * read from Haven, so `expected_settlement_scheme` is `null` rather than a
291
- * guess. `accepted_scheme` (the merchant's offer) is unaffected.
292
- */
293
- X402SchemeUnknown: "X402_SCHEME_UNKNOWN",
294
- /**
295
- * #2968: the merchant answered 200 and handed over goods, but Haven holds NO
296
- * on-chain evidence that the payment moved. `settled: false` beside this code
297
- * is not a failure — it is the absence of proof, and the two must travel
298
- * together so an agent can tell "the user has the goods" apart from "the
299
- * money moved". Carries the intent's `expires_at`: after that instant the
300
- * settlement can no longer land at all.
301
- */
302
- SettlementUnconfirmed: "SETTLEMENT_UNCONFIRMED"
303
- };
304
- var AgentPaymentRailDescriptions = {
305
- [AgentPaymentRail.Direct]: "Standard Haven payment from the user-controlled account, redeeming the agent's on-chain budget delegation.",
306
- [AgentPaymentRail.X402]: "x402 HTTP 402 payment flow with a Haven funding leg and merchant retry leg.",
307
- [AgentPaymentRail.Mpp]: "Categorical MPP rail value used as a resume-state discriminator. Response bodies carry a granular mpp_* value instead.",
308
- [AgentPaymentRail.MppDemo]: "Haven internal MPP demo rail. Not for production traffic.",
309
- [AgentPaymentRail.MppCrypto]: "Crypto-settled MPP rail.",
310
- [AgentPaymentRail.StripeDeposit]: "Stripe-deposit-backed MPP rail.",
311
- [AgentPaymentRail.Spt]: "Stripe Payment Token MPP rail."
312
- };
313
- var AgentPaymentPhaseSchema = {
314
- type: "string",
315
- enum: AGENT_PAYMENT_PHASE_VALUES,
316
- description: "Stable Haven agent payment state phase.",
317
- "x-enumDescriptions": AgentPaymentPhaseDescriptions
318
- };
319
- var AgentPaymentNextActionSchema = {
320
- type: "string",
321
- enum: AGENT_PAYMENT_NEXT_ACTION_VALUES,
322
- description: "Stable next action an agent should take for a Haven payment state.",
323
- "x-enumDescriptions": AgentPaymentNextActionDescriptions
324
- };
325
- var AgentPaymentFailureCodeSchema = {
326
- type: "string",
327
- enum: AGENT_PAYMENT_FAILURE_CODE_VALUES,
328
- description: "Stable machine-readable failure codes for Haven agent payment recovery paths.",
329
- "x-enumDescriptions": AgentPaymentFailureCodeDescriptions
330
- };
331
- var AgentPaymentRailSchema = {
332
- type: "string",
333
- enum: AGENT_PAYMENT_RAIL_VALUES,
334
- description: "Stable rail identifier for Haven agent payment states.",
335
- "x-enumDescriptions": AgentPaymentRailDescriptions
336
- };
337
- var HavenError = class extends Error {
338
- constructor(message, code, statusCode, paymentId) {
339
- super(message);
340
- this.code = code;
341
- this.statusCode = statusCode;
342
- this.paymentId = paymentId;
343
- this.name = "HavenError";
344
- }
345
- code;
346
- statusCode;
347
- paymentId;
348
- };
349
- var HavenApiError = class extends HavenError {
350
- constructor(message, statusCode, body, paymentId) {
351
- super(message, "API_ERROR", statusCode, paymentId);
352
- this.body = body;
353
- this.name = "HavenApiError";
354
- }
355
- body;
356
- };
357
- var MerchantTimeoutError = class extends HavenApiError {
358
- merchantErrorCode = "merchant_timeout";
359
- constructor(message) {
360
- super(message, 504);
361
- this.name = "MerchantTimeoutError";
362
- }
363
- };
364
- var X402UnexpectedStatusError = class extends HavenApiError {
365
- x402ErrorCode = "unexpected_non_402_status";
366
- /**
367
- * #2979: `body` is the merchant's own JSON, when the non-402 response
368
- * carried one — e.g. the demo merchant's `/mcp` readiness gate answers
369
- * `503 { error: 'merchant_not_ready', reason_code, ... }`. Optional and
370
- * best-effort: a non-JSON or unreadable body leaves this `undefined`, same
371
- * as before this field existed. Consumers key on it (not on the message
372
- * string) to distinguish an honest, machine-readable merchant refusal from
373
- * a genuine "this is not the x402 endpoint" miss, which otherwise look
374
- * identical — both are just "some non-402 status".
375
- */
376
- constructor(message, statusCode, body) {
377
- super(message, statusCode, body);
378
- this.name = "X402UnexpectedStatusError";
379
- }
380
- };
381
- var X402AlreadySettledError = class extends HavenApiError {
382
- constructor(message, receipt, basis) {
383
- super(message, 409, void 0, receipt.paymentId);
384
- this.receipt = receipt;
385
- this.basis = basis;
386
- this.name = "X402AlreadySettledError";
387
- }
388
- receipt;
389
- basis;
390
- x402ErrorCode = "already_settled";
391
- };
392
- var HavenPaymentStateError = class extends HavenApiError {
393
- constructor(message, statusCode, state, body) {
394
- super(message, statusCode, body, state.paymentId);
395
- this.state = state;
396
- this.name = "HavenPaymentStateError";
397
- }
398
- state;
399
- resumeState;
400
- get status() {
401
- return this.state.status;
402
- }
403
- get phase() {
404
- return this.state.phase;
405
- }
406
- get nextAction() {
407
- return this.state.nextAction;
408
- }
409
- };
410
- var HavenSigningError = class extends HavenError {
411
- constructor(message) {
412
- super(message, "SIGNING_ERROR");
413
- this.name = "HavenSigningError";
414
- }
415
- };
416
- var HavenZeroSettlementHashError = class extends HavenError {
417
- constructor(paymentId) {
418
- super(
419
- "settlement_tx_hash is the zero hash (0x00\u202600), which is never a real settlement transaction \u2014 refused before any report was sent.",
420
- "ZERO_SETTLEMENT_HASH",
421
- 400,
422
- paymentId
423
- );
424
- this.name = "HavenZeroSettlementHashError";
425
- }
426
- };
427
- var SignerRefusalCode = {
428
- /** `SUPPORTED_X402_EXPECTED_VERSIONS` in `@haven_ai/signer` does not include the received version. */
429
- UnsupportedExpectedContextVersion: "UNSUPPORTED_EXPECTED_CONTEXT_VERSION",
430
- /** `SUPPORTED_SWEEP_BINDING_VERSIONS` in `@haven_ai/signer` does not include the received version. */
431
- UnsupportedSweepBindingVersion: "UNSUPPORTED_SWEEP_BINDING_VERSION"
432
- };
433
- function signerUpdateFallback(channel = HAVEN_CONNECTOR_CHANNEL) {
434
- return `Update @haven_ai/signer by rerunning \`${connectorRerunCommand(void 0, { channel })}\`, which reinstalls the pinned MCP runtime, then retry the same signing call. Nothing was signed or spent \u2014 the quote or payment this version came from is unaffected and does not need to be re-quoted.`;
435
- }
436
- var SIGNER_UPDATE_FALLBACK = signerUpdateFallback();
437
- var HavenUnsupportedSignerVersionError = class extends HavenError {
438
- constructor(message, code, supportedVersions, receivedVersion, fallback) {
439
- super(message, code);
440
- this.supportedVersions = supportedVersions;
441
- this.receivedVersion = receivedVersion;
442
- this.fallback = fallback;
443
- this.name = "HavenUnsupportedSignerVersionError";
444
- }
445
- supportedVersions;
446
- receivedVersion;
447
- fallback;
448
- };
449
- var HavenTimeoutError = class extends HavenError {
450
- constructor(paymentId) {
451
- super(
452
- `Timed out waiting for payment ${paymentId} to confirm`,
453
- "TIMEOUT",
454
- void 0,
455
- paymentId
456
- );
457
- this.name = "HavenTimeoutError";
458
- }
459
- };
460
-
461
- // src/signer.ts
462
11
  function signHash(privateKey, hash) {
463
12
  try {
464
13
  const signingKey = new ethers.SigningKey(privateKey);
@@ -509,615 +58,43 @@ function verifySignature(hash, signature, expectedAddress) {
509
58
  return false;
510
59
  }
511
60
  }
512
-
513
- // src/base64.ts
514
- function normalizeBase64(value) {
515
- const standard = value.replace(/-/g, "+").replace(/_/g, "/");
516
- const remainder = standard.length % 4;
517
- return remainder === 0 ? standard : standard + "=".repeat(4 - remainder);
518
- }
519
- function encodeBase64Utf8(value) {
520
- if (typeof Buffer !== "undefined") {
521
- return Buffer.from(value, "utf8").toString("base64");
522
- }
523
- const bytes = new TextEncoder().encode(value);
524
- let binary = "";
525
- for (let i = 0; i < bytes.length; i++) {
526
- binary += String.fromCharCode(bytes[i]);
527
- }
528
- return btoa(binary);
529
- }
530
- function decodeBase64Utf8(value) {
531
- const normalized = normalizeBase64(value);
532
- if (typeof Buffer !== "undefined") {
533
- return Buffer.from(normalized, "base64").toString("utf8");
534
- }
535
- const binary = atob(normalized);
536
- const bytes = new Uint8Array(binary.length);
537
- for (let i = 0; i < binary.length; i++) {
538
- bytes[i] = binary.charCodeAt(i);
539
- }
540
- return new TextDecoder().decode(bytes);
541
- }
542
- function encodeBase64Json(value) {
543
- return encodeBase64Utf8(JSON.stringify(value));
544
- }
545
- function decodeBase64Json(value, label) {
546
- try {
547
- return JSON.parse(decodeBase64Utf8(value));
548
- } catch (err) {
549
- if (label) throw new Error(`Failed to decode ${label}`);
550
- throw err;
551
- }
552
- }
553
-
554
- // src/sweep.ts
555
- var SWEEP_BASE_CHAIN_ID = 8453;
556
- var SWEEP_BASE_SEPOLIA_CHAIN_ID = 84532;
557
- var SWEEP_BASE_USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
558
- var SWEEP_BASE_SEPOLIA_USDC_ADDRESS = "0x036CbD53842c5426634e7929541eC2318f3dCF7e";
559
- var USDC_EIP712_DOMAIN_BY_CHAIN = {
560
- [SWEEP_BASE_CHAIN_ID]: {
561
- name: "USD Coin",
562
- version: "2",
563
- chainId: SWEEP_BASE_CHAIN_ID,
564
- verifyingContract: SWEEP_BASE_USDC_ADDRESS
565
- },
566
- [SWEEP_BASE_SEPOLIA_CHAIN_ID]: {
567
- name: "USDC",
568
- version: "2",
569
- chainId: SWEEP_BASE_SEPOLIA_CHAIN_ID,
570
- verifyingContract: SWEEP_BASE_SEPOLIA_USDC_ADDRESS
571
- }
572
- };
573
- var USDC_ADDRESS_BY_CHAIN = {
574
- [SWEEP_BASE_CHAIN_ID]: SWEEP_BASE_USDC_ADDRESS,
575
- [SWEEP_BASE_SEPOLIA_CHAIN_ID]: SWEEP_BASE_SEPOLIA_USDC_ADDRESS
576
- };
577
- var SWEEPABLE_CHAIN_IDS = Object.keys(USDC_ADDRESS_BY_CHAIN).map(Number);
578
- function isSweepableChain(chainId) {
579
- return chainId in USDC_ADDRESS_BY_CHAIN;
580
- }
581
- var TRANSFER_WITH_AUTHORIZATION_TYPES = {
582
- TransferWithAuthorization: [
583
- { name: "from", type: "address" },
584
- { name: "to", type: "address" },
585
- { name: "value", type: "uint256" },
586
- { name: "validAfter", type: "uint256" },
587
- { name: "validBefore", type: "uint256" },
588
- { name: "nonce", type: "bytes32" }
589
- ]
590
- };
591
- function sweepUsdcAddress(chainId) {
592
- const address = USDC_ADDRESS_BY_CHAIN[chainId];
593
- if (!address) {
594
- throw new HavenSigningError(
595
- `Sweep is not supported on chain ${chainId}. Supported: ${SWEEPABLE_CHAIN_IDS.join(", ")}.`
596
- );
597
- }
598
- return address;
599
- }
600
- function sweepUsdcDomain(chainId) {
601
- const domain = USDC_EIP712_DOMAIN_BY_CHAIN[chainId];
602
- if (!domain) {
603
- throw new HavenSigningError(
604
- `Sweep is not supported on chain ${chainId}. Supported: ${SWEEPABLE_CHAIN_IDS.join(", ")}.`
605
- );
606
- }
607
- return domain;
608
- }
609
- function sameAddress(a, b) {
610
- return a.toLowerCase() === b.toLowerCase();
611
- }
612
- function buildSweepTypedData(auth) {
613
- const domain = sweepUsdcDomain(auth.chainId);
614
- const expectedToken = sweepUsdcAddress(auth.chainId);
615
- if (!sameAddress(auth.token, expectedToken)) {
616
- throw new HavenSigningError(
617
- `Sweep token ${auth.token} is not the canonical USDC contract for chain ${auth.chainId}.`
618
- );
619
- }
620
- if (!/^0x[0-9a-fA-F]{64}$/.test(auth.nonce)) {
621
- throw new HavenSigningError("Sweep nonce must be a 0x-prefixed 32-byte hex string.");
622
- }
623
- return {
624
- domain,
625
- types: TRANSFER_WITH_AUTHORIZATION_TYPES,
626
- primaryType: "TransferWithAuthorization",
627
- message: {
628
- from: auth.from,
629
- to: auth.to,
630
- value: BigInt(auth.value),
631
- validAfter: BigInt(auth.validAfter),
632
- validBefore: BigInt(auth.validBefore),
633
- nonce: auth.nonce
634
- }
635
- };
636
- }
637
- function buildSweepAuthorizationMessage(auth) {
638
- return `Haven sweep authorization v1
639
- ${stableStringify({
640
- version: 1,
641
- kind: "haven.sweep.authorization",
642
- from: auth.from.toLowerCase(),
643
- to: auth.to.toLowerCase(),
644
- value: auth.value,
645
- validAfter: auth.validAfter,
646
- validBefore: auth.validBefore,
647
- nonce: auth.nonce.toLowerCase(),
648
- token: auth.token.toLowerCase(),
649
- chainId: auth.chainId
650
- })}`;
651
- }
652
- function stableStringify(value) {
653
- if (value === null || typeof value !== "object") return JSON.stringify(value);
654
- if (Array.isArray(value)) return `[${value.map((item) => stableStringify(item)).join(",")}]`;
655
- const object = value;
656
- return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(object[key])}`).join(",")}}`;
657
- }
658
-
659
- // src/x402.ts
660
- var BASE_USDC_ADDRESS = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913";
661
- var BASE_SEPOLIA_USDC_ADDRESS = "0x036cbd53842c5426634e7929541ec2318f3dcf7e";
662
- var STANDARD_X402_USDC_ADDRESSES = /* @__PURE__ */ new Set([BASE_USDC_ADDRESS, BASE_SEPOLIA_USDC_ADDRESS]);
663
- var X402_IDEMPOTENCY_BUCKET_MS = 3e5;
664
- var DECIMAL_ATOMIC_AMOUNT_RE = /^[0-9]+$/;
665
- var X402_PAYMENT_HEADER_MAX_LENGTH = 65536;
666
- var BASE64_RE = /^[A-Za-z0-9+/]*={0,2}$/;
667
- var ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
668
- var SIGNATURE_RE = /^0x[0-9a-fA-F]{130}$/;
669
- var NONCE_RE = /^0x[0-9a-fA-F]{64}$/;
670
- var X402PaymentHeaderValidationError = class extends Error {
671
- constructor() {
672
- super("Invalid X-PAYMENT header.");
673
- this.name = "X402PaymentHeaderValidationError";
674
- }
675
- };
676
- function isPositiveDecimalAtomicAmount(value) {
677
- return DECIMAL_ATOMIC_AMOUNT_RE.test(value) && BigInt(value) > 0n;
678
- }
679
- function optionAuthorizationAmount(option) {
680
- return option.maxAmountRequired ?? option.amount;
681
- }
682
- var X402_MAX_AUTHORIZATION_WINDOW_SECONDS = 600;
683
- var X402_SETTLEMENT_FORWARD_MARGIN_SECONDS = 300;
684
- function clampAuthorizationWindow(seconds) {
685
- const requested = typeof seconds === "number" && Number.isFinite(seconds) ? seconds : 30;
686
- return Math.min(Math.max(Math.floor(requested), 1), X402_MAX_AUTHORIZATION_WINDOW_SECONDS);
687
- }
688
- function normalizePaymentOption(value) {
689
- const candidate = value;
690
- if (!candidate || typeof candidate !== "object" || typeof candidate.scheme !== "string" || typeof candidate.network !== "string" || typeof candidate.asset !== "string" || typeof candidate.payTo !== "string") {
691
- return null;
692
- }
693
- const amount = typeof candidate.amount === "string" ? candidate.amount : typeof candidate.maxAmountRequired === "string" ? candidate.maxAmountRequired : null;
694
- if (!amount) return null;
695
- if (!isPositiveDecimalAtomicAmount(amount)) return null;
696
- if (candidate.maxAmountRequired !== void 0 && (typeof candidate.maxAmountRequired !== "string" || !isPositiveDecimalAtomicAmount(candidate.maxAmountRequired))) {
697
- return null;
698
- }
699
- return {
700
- scheme: candidate.scheme,
701
- network: candidate.network,
702
- amount,
703
- maxAmountRequired: candidate.maxAmountRequired,
704
- resource: candidate.resource,
705
- description: candidate.description,
706
- mimeType: candidate.mimeType,
707
- asset: candidate.asset,
708
- payTo: candidate.payTo,
709
- maxTimeoutSeconds: clampAuthorizationWindow(candidate.maxTimeoutSeconds),
710
- extra: candidate.extra
711
- };
712
- }
713
- function normalizePaymentRequired(value) {
714
- const candidate = value;
715
- if (!candidate || typeof candidate !== "object" || typeof candidate.x402Version !== "number" || !Array.isArray(candidate.accepts)) {
716
- return null;
717
- }
718
- const accepts = candidate.accepts.map((option) => normalizePaymentOption(option)).filter((option) => !!option && typeof option === "object");
719
- if (accepts.length === 0) return null;
720
- const first = accepts[0];
721
- const resourceUrl = candidate.resource?.url ?? first.resource;
722
- if (!resourceUrl) return null;
723
- const resource = {
724
- ...candidate.resource && typeof candidate.resource === "object" ? candidate.resource : {},
725
- url: resourceUrl,
726
- description: candidate.resource?.description ?? first.description,
727
- mimeType: candidate.resource?.mimeType ?? first.mimeType
728
- };
729
- return {
730
- x402Version: candidate.x402Version,
731
- resource,
732
- accepts,
733
- error: candidate.error,
734
- ...candidate.extensions && typeof candidate.extensions === "object" ? { extensions: candidate.extensions } : {}
735
- };
736
- }
737
- var SUPPORTED_X402_NETWORKS = {
738
- "eip155:100": "Gnosis Chain",
739
- "eip155:8453": "Base",
740
- "base": "Base",
741
- "eip155:84532": "Base Sepolia",
742
- "base-sepolia": "Base Sepolia"
743
- };
744
- var STANDARD_X402_NETWORKS = {
745
- "eip155:8453": "base",
746
- "base": "base",
747
- "eip155:84532": "base-sepolia",
748
- "base-sepolia": "base-sepolia"
749
- };
750
- var GNOSIS_TOKENS = {
751
- "0x0000000000000000000000000000000000000000": { symbol: "xDAI", decimals: 18 },
752
- "0xcb444e90d8198415266c6a2724b7900fb12fc56e": { symbol: "EURe", decimals: 18 },
753
- "0x2a22f9c3b484c3629090feed35f17ff8f88f76f0": { symbol: "USDC.e", decimals: 6 }
754
- };
755
- var BASE_TOKENS = {
756
- "0x0000000000000000000000000000000000000000": { symbol: "ETH", decimals: 18 },
757
- "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": { symbol: "USDC", decimals: 6 }
758
- };
759
- var BASE_SEPOLIA_TOKENS = {
760
- "0x0000000000000000000000000000000000000000": { symbol: "ETH", decimals: 18 },
761
- "0x036cbd53842c5426634e7929541ec2318f3dcf7e": { symbol: "USDC", decimals: 6 }
762
- };
763
- var ALL_TOKENS = {
764
- ...GNOSIS_TOKENS,
765
- ...BASE_TOKENS,
766
- ...BASE_SEPOLIA_TOKENS
767
- };
768
- var NETWORK_TOKENS = {
769
- "eip155:100": GNOSIS_TOKENS,
770
- "eip155:8453": BASE_TOKENS,
771
- "base": BASE_TOKENS,
772
- "eip155:84532": BASE_SEPOLIA_TOKENS,
773
- "base-sepolia": BASE_SEPOLIA_TOKENS
774
- };
775
- var X402_PAYMENT_HEADER_NAME = "PAYMENT-SIGNATURE";
776
- var X402_LEGACY_PAYMENT_HEADER_NAME = "X-PAYMENT";
777
- var X402_PAYMENT_REQUIRED_HEADER_NAME = "PAYMENT-REQUIRED";
778
- var X402_PAYMENT_RESPONSE_HEADER_NAME = "PAYMENT-RESPONSE";
779
- var X402_PAYMENT_HEADER_NAMES_SENT = `${X402_PAYMENT_HEADER_NAME}, ${X402_LEGACY_PAYMENT_HEADER_NAME}`;
780
- var X402_PAYMENT_HEADER_NAMES = [
781
- X402_PAYMENT_HEADER_NAME,
782
- X402_LEGACY_PAYMENT_HEADER_NAME
783
- ];
784
- function x402PaymentHeaderNamesFor(paymentHeader) {
785
- const both = [X402_PAYMENT_HEADER_NAME, X402_LEGACY_PAYMENT_HEADER_NAME];
786
- let decoded;
787
- try {
788
- decoded = decodeBase64Json(paymentHeader);
789
- } catch {
790
- return both;
791
- }
792
- const accepted = decoded?.accepted;
793
- if (!accepted || typeof accepted !== "object" || Array.isArray(accepted)) return both;
794
- return isErc7710Option(accepted) ? [X402_PAYMENT_HEADER_NAME] : both;
795
- }
796
- function x402PaymentHeaderNamesSent(paymentHeader) {
797
- return x402PaymentHeaderNamesFor(paymentHeader).join(", ");
798
- }
799
- function x402V2PaymentEnvelope(paymentRequired, accepted, payload) {
800
- const resource = paymentRequired.resource;
801
- const extensions = paymentRequired.extensions;
802
- return {
803
- x402Version: paymentRequired.x402Version,
804
- ...resource && typeof resource === "object" && !Array.isArray(resource) ? { resource } : {},
805
- accepted,
806
- payload,
807
- ...extensions && typeof extensions === "object" && !Array.isArray(extensions) ? { extensions } : {}
808
- };
809
- }
810
- function parsePaymentRequired(response) {
811
- const v2Header = response.headers.get("PAYMENT-REQUIRED");
812
- if (v2Header) {
813
- const parsed = normalizePaymentRequired(
814
- decodeBase64Json(v2Header, "PAYMENT-REQUIRED header")
815
- );
816
- if (parsed) return parsed;
817
- }
818
- const v1Header = response.headers.get("X-PAYMENT");
819
- if (v1Header) {
820
- const parsed = normalizePaymentRequired(
821
- decodeBase64Json(v1Header, "X-PAYMENT header")
822
- );
823
- if (parsed) return parsed;
824
- }
825
- throw new Error(
826
- "No x402 payment headers found in 402 response. Expected PAYMENT-REQUIRED (v2) or X-PAYMENT (v1) header."
827
- );
828
- }
829
- async function parsePaymentRequiredResponse(response) {
830
- try {
831
- return parsePaymentRequired(response);
832
- } catch (headerErr) {
833
- try {
834
- const body = await response.clone().json();
835
- const parsed = normalizePaymentRequired(body);
836
- if (parsed) return parsed;
837
- } catch {
838
- }
839
- throw headerErr;
840
- }
841
- }
842
- function selectPaymentOption(accepts) {
843
- if (!accepts || accepts.length === 0) return null;
844
- for (const opt of accepts) {
845
- if (opt.network in SUPPORTED_X402_NETWORKS) {
846
- const networkTokens = NETWORK_TOKENS[opt.network];
847
- if (networkTokens?.[opt.asset.toLowerCase()] && isPositiveDecimalAtomicAmount(optionAuthorizationAmount(opt))) {
848
- return opt;
849
- }
850
- }
851
- }
852
- for (const opt of accepts) {
853
- if (opt.network in SUPPORTED_X402_NETWORKS && isPositiveDecimalAtomicAmount(optionAuthorizationAmount(opt))) {
854
- return opt;
855
- }
856
- }
857
- return null;
858
- }
859
- var ERC7710_ASSET_TRANSFER_METHOD = "erc7710";
860
- function x402AssetTransferMethod(option) {
861
- const raw = option.extra?.assetTransferMethod;
862
- return typeof raw === "string" ? raw : null;
863
- }
864
- function isErc7710Option(option) {
865
- return x402AssetTransferMethod(option) === ERC7710_ASSET_TRANSFER_METHOD;
866
- }
867
- function x402FacilitatorAddresses(option) {
868
- const raw = option.extra?.facilitatorAddresses;
869
- if (!Array.isArray(raw)) return null;
870
- const addresses = raw.filter((a) => typeof a === "string" && ADDRESS_RE.test(a));
871
- return addresses.length > 0 ? addresses : null;
872
- }
873
- function isPayableStandardOption(opt) {
874
- return opt.scheme === "exact" && opt.network in STANDARD_X402_NETWORKS && STANDARD_X402_USDC_ADDRESSES.has(opt.asset.toLowerCase()) && isPositiveDecimalAtomicAmount(optionAuthorizationAmount(opt));
875
- }
876
- function selectStandardPaymentOption(accepts) {
877
- if (!accepts || accepts.length === 0) return null;
878
- for (const opt of accepts) {
879
- if (opt === null || typeof opt !== "object") continue;
880
- if (!isErc7710Option(opt) && isPayableStandardOption(opt)) return opt;
881
- }
882
- return null;
883
- }
884
- function selectErc7710PaymentOption(accepts) {
885
- if (!accepts || accepts.length === 0) return null;
886
- for (const opt of accepts) {
887
- if (opt === null || typeof opt !== "object") continue;
888
- if (isErc7710Option(opt) && isPayableStandardOption(opt)) return opt;
889
- }
890
- return null;
891
- }
892
- function selectX402SettlementScheme(accepts, opts) {
893
- if (opts.delegationRail) {
894
- const preferred = selectErc7710PaymentOption(accepts);
895
- if (preferred) {
896
- return {
897
- scheme: "erc7710",
898
- option: preferred,
899
- facilitatorAddresses: x402FacilitatorAddresses(preferred)
900
- };
901
- }
902
- }
903
- const fallback = selectStandardPaymentOption(accepts);
904
- if (fallback) return { scheme: "eip3009", option: fallback, facilitatorAddresses: null };
905
- return null;
906
- }
907
- function x402AuthorizationAmount(option) {
908
- const amount = optionAuthorizationAmount(option);
909
- if (!isPositiveDecimalAtomicAmount(amount)) {
910
- throw new Error("Invalid x402 amount: must be a positive decimal atomic amount");
911
- }
912
- return amount;
913
- }
914
- function buildX402ExpectedMessage(context) {
915
- const version = context.payerDelegate ? 3 : context.typedDataHash ? 2 : 1;
916
- const payload = {
917
- version,
918
- kind: "haven.x402.expected",
919
- paymentId: context.paymentId,
920
- payloadHash: context.payloadHash.toLowerCase(),
921
- resourceUrl: context.resourceUrl,
922
- merchantTo: context.merchantTo.toLowerCase(),
923
- amount: context.amount,
924
- asset: context.asset.toLowerCase(),
925
- network: context.network
926
- };
927
- if (context.expiresAt) {
928
- payload.expiresAt = context.expiresAt;
929
- }
930
- if (context.typedDataHash) {
931
- payload.typedDataHash = context.typedDataHash.toLowerCase();
932
- }
933
- if (context.payerDelegate) {
934
- payload.payerDelegate = context.payerDelegate.toLowerCase();
935
- if (context.payerAgentId) {
936
- payload.payerAgentId = context.payerAgentId;
937
- }
938
- }
939
- return `Haven x402 expected context v${version}
940
- ${stableStringify2(payload)}`;
941
- }
942
- function toStandardPaymentRequirements(paymentRequired, option) {
943
- const network = STANDARD_X402_NETWORKS[option.network];
944
- if (!network) {
945
- throw new Error(`x402 exact payments are not supported on ${option.network}`);
946
- }
947
- if (option.scheme !== "exact") {
948
- throw new Error(`Unsupported x402 scheme: ${option.scheme}`);
949
- }
950
- return {
951
- scheme: "exact",
952
- network,
953
- maxAmountRequired: x402AuthorizationAmount(option),
954
- resource: option.resource ?? paymentRequired.resource.url,
955
- description: option.description ?? paymentRequired.resource.description ?? "Haven x402 payment",
956
- mimeType: option.mimeType ?? paymentRequired.resource.mimeType ?? "application/octet-stream",
957
- payTo: option.payTo,
958
- asset: option.asset,
959
- // Second enforcement point (#715): the parse path clamps too, but this is
960
- // the last stop before the x402 library turns the timeout into
961
- // `validBefore` — options constructed without parsing are bounded here.
962
- // The forward margin (#1256) is added ONLY here, at signing: the parse
963
- // path keeps recording the merchant's advertised timeout unchanged, and
964
- // the library's `validBefore = now + this value` then carries enough
965
- // slack to satisfy the facilitator's `validBefore ≥ now + maxTimeout`
966
- // verify rule after our funding leg confirms.
967
- maxTimeoutSeconds: clampAuthorizationWindow(option.maxTimeoutSeconds) + X402_SETTLEMENT_FORWARD_MARGIN_SECONDS,
968
- extra: option.extra
969
- };
970
- }
971
- async function validateStandardX402PaymentHeader(paymentHeader, context) {
972
- try {
973
- if (typeof paymentHeader !== "string" || paymentHeader.length === 0 || paymentHeader.length > X402_PAYMENT_HEADER_MAX_LENGTH || paymentHeader.length % 4 !== 0 || !BASE64_RE.test(paymentHeader)) {
974
- throw new Error("wire");
975
- }
976
- const decoded = decodeBase64Json(paymentHeader);
977
- if (!decoded || typeof decoded !== "object" || Array.isArray(decoded)) throw new Error("shape");
978
- const version = decoded.x402Version;
979
- const payload = decoded.payload;
980
- if (version !== 1 && version !== 2 || !payload || typeof payload !== "object" || Array.isArray(payload)) {
981
- throw new Error("shape");
982
- }
983
- if (version === 1) {
984
- if (!hasOnlyKeys(decoded, ["x402Version", "scheme", "network", "payload"])) throw new Error("shape");
985
- if (decoded.scheme !== "exact" || decoded.network !== standardX402WireNetwork(context.network)) {
986
- throw new Error("context");
987
- }
988
- } else {
989
- if (!hasOnlyKeys(decoded, ["x402Version", "accepted", "payload"], ["resource", "extensions"])) {
990
- throw new Error("shape");
991
- }
992
- if ("resource" in decoded && (!decoded.resource || typeof decoded.resource !== "object" || Array.isArray(decoded.resource))) {
993
- throw new Error("shape");
994
- }
995
- if ("extensions" in decoded && (!decoded.extensions || typeof decoded.extensions !== "object" || Array.isArray(decoded.extensions))) {
996
- throw new Error("shape");
997
- }
998
- const accepted = selectStandardPaymentOption([decoded.accepted]);
999
- if (!accepted || !matchesHeaderContext(accepted, context)) throw new Error("context");
1000
- }
1001
- const record = payload;
1002
- if (!hasOnlyKeys(record, ["signature", "authorization"])) throw new Error("shape");
1003
- if (typeof record.signature !== "string" || !SIGNATURE_RE.test(record.signature)) throw new Error("shape");
1004
- const authorization = record.authorization;
1005
- if (!authorization || typeof authorization !== "object" || Array.isArray(authorization)) throw new Error("shape");
1006
- const auth = authorization;
1007
- if (!hasOnlyKeys(auth, ["from", "to", "value", "validAfter", "validBefore", "nonce"])) throw new Error("shape");
1008
- if (typeof auth.from !== "string" || !ADDRESS_RE.test(auth.from) || typeof auth.to !== "string" || !ADDRESS_RE.test(auth.to) || typeof auth.value !== "string" || !isPositiveDecimalAtomicAmount(auth.value) || typeof auth.validAfter !== "string" || !DECIMAL_ATOMIC_AMOUNT_RE.test(auth.validAfter) || typeof auth.validBefore !== "string" || !DECIMAL_ATOMIC_AMOUNT_RE.test(auth.validBefore) || typeof auth.nonce !== "string" || !NONCE_RE.test(auth.nonce)) {
1009
- throw new Error("shape");
1010
- }
1011
- if (!sameAddress2(auth.from, context.payer) || !sameAddress2(auth.to, context.merchantTo) || auth.value !== context.amountAtomic) {
1012
- throw new Error("context");
1013
- }
1014
- const validAfter = BigInt(auth.validAfter);
1015
- const validBefore = BigInt(auth.validBefore);
1016
- const now = BigInt(Math.floor(Date.now() / 1e3));
1017
- if (validBefore <= now || validAfter > validBefore) throw new Error("expired");
1018
- const typedData = buildSweepTypedData({
1019
- from: auth.from,
1020
- to: auth.to,
1021
- value: auth.value,
1022
- validAfter: auth.validAfter,
1023
- validBefore: auth.validBefore,
1024
- nonce: auth.nonce,
1025
- token: context.asset,
1026
- chainId: context.chainId
1027
- });
1028
- const recovered = await recoverTypedDataAddress({
1029
- ...typedData,
1030
- // `buildSweepTypedData` keeps the public domain framework-neutral;
1031
- // viem brands contract addresses at this crypto call boundary only.
1032
- domain: {
1033
- ...typedData.domain,
1034
- verifyingContract: typedData.domain.verifyingContract
1035
- },
1036
- message: {
1037
- ...typedData.message,
1038
- from: typedData.message.from,
1039
- to: typedData.message.to,
1040
- nonce: typedData.message.nonce
1041
- },
1042
- signature: record.signature
1043
- });
1044
- if (!sameAddress2(recovered, context.payer)) throw new Error("recovery");
1045
- } catch {
1046
- throw new X402PaymentHeaderValidationError();
1047
- }
1048
- }
1049
- function hasOnlyKeys(value, required, optional = []) {
1050
- return Object.keys(value).every((key) => required.includes(key) || optional.includes(key)) && required.every((key) => key in value);
1051
- }
1052
- function sameAddress2(left, right) {
1053
- return left.toLowerCase() === right.toLowerCase();
1054
- }
1055
- function standardX402WireNetwork(network) {
1056
- return STANDARD_X402_NETWORKS[network] ?? null;
1057
- }
1058
- function matchesHeaderContext(option, context) {
1059
- if (option.scheme !== "exact" || !sameAddress2(option.payTo, context.merchantTo) || !sameAddress2(option.asset, context.asset) || option.network !== context.network || x402AuthorizationAmount(option) !== context.amountAtomic) return false;
1060
- return option.resource === void 0 || option.resource === context.resourceUrl;
1061
- }
1062
- function buildX402IdempotencyKey(paymentRequired, option, now = Date.now()) {
1063
- const bucket = Math.floor(now / X402_IDEMPOTENCY_BUCKET_MS);
1064
- const material = [
1065
- paymentRequired.resource.url,
1066
- paymentRequired.resource.description ?? "",
1067
- option.payTo.toLowerCase(),
1068
- option.asset.toLowerCase(),
1069
- x402AuthorizationAmount(option),
1070
- option.network,
1071
- bucket
1072
- ].join("|");
1073
- return `x402:${createHash("sha256").update(material).digest("hex").slice(0, 16)}`;
1074
- }
1075
- function encodePaymentProof(receipt) {
1076
- const payload = {
1077
- x402Version: 2,
1078
- resource: receipt.resourceUrl ? { url: receipt.resourceUrl } : void 0,
1079
- accepted: receipt.accepted,
1080
- payload: {
1081
- type: "haven_tx_hash",
1082
- txHash: receipt.txHash,
1083
- paymentId: receipt.paymentId,
1084
- settledVia: "haven",
1085
- payer: receipt.payer,
1086
- chainId: receipt.chainId
1087
- }
1088
- };
1089
- return encodeBase64Json(payload);
1090
- }
1091
- function resolveTokenFromAddress(address, network) {
1092
- const lower = address.toLowerCase();
1093
- if (network && network in NETWORK_TOKENS) {
1094
- return NETWORK_TOKENS[network][lower] ?? null;
1095
- }
1096
- return ALL_TOKENS[lower] ?? null;
1097
- }
1098
- function stableStringify2(value) {
1099
- if (value === null || typeof value !== "object") {
1100
- const primitive = JSON.stringify(value);
1101
- return primitive === void 0 ? "undefined" : primitive;
1102
- }
1103
- if (value instanceof Date) return JSON.stringify(value.toISOString());
1104
- if (Array.isArray(value)) return `[${value.map((item) => stableStringify2(item)).join(",")}]`;
1105
- const object = value;
1106
- return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${stableStringify2(object[key])}`).join(",")}}`;
1107
- }
1108
61
  var DEFAULT_BASE_URL = "http://localhost:3001";
1109
62
  var DEFAULT_REQUEST_TIMEOUT = 3e4;
63
+ function withoutClientHeader(headers) {
64
+ const target = HAVEN_CLIENT_HEADER.toLowerCase();
65
+ return Object.fromEntries(Object.entries(headers).filter(([name]) => name.toLowerCase() !== target));
66
+ }
1110
67
  var HavenApiTransport = class {
1111
68
  apiKey;
1112
69
  baseUrl;
1113
70
  requestTimeout;
1114
71
  defaultHeaders;
72
+ clientIdentity;
1115
73
  requestContext = new AsyncLocalStorage();
74
+ lastClientUpdate;
1116
75
  constructor(config) {
1117
76
  this.apiKey = config.apiKey;
1118
77
  this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
1119
78
  this.requestTimeout = config.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT;
1120
79
  this.defaultHeaders = { ...config.defaultHeaders ?? {} };
80
+ this.clientIdentity = config.clientIdentity ?? SDK_CLIENT_IDENTITY;
81
+ }
82
+ /**
83
+ * The newest `client_update` the backend sent (#3303): inside a
84
+ * `withRequestContext` dispatch, the one that dispatch's own requests
85
+ * received; outside one, the latest this transport has seen.
86
+ */
87
+ clientUpdate() {
88
+ const store = this.requestContext.getStore();
89
+ return store ? store.clientUpdate : this.lastClientUpdate;
90
+ }
91
+ recordClientUpdate(data) {
92
+ if (data === null || typeof data !== "object" || Array.isArray(data)) return;
93
+ const hint = readClientUpdate(data.client_update);
94
+ if (!hint) return;
95
+ this.lastClientUpdate = hint;
96
+ const store = this.requestContext.getStore();
97
+ if (store) store.clientUpdate = hint;
1121
98
  }
1122
99
  /** Run `fn` with extra headers scoped to its asynchronous Haven API work. */
1123
100
  withRequestContext(headers, fn) {
@@ -1140,13 +117,19 @@ var HavenApiTransport = class {
1140
117
  headers: {
1141
118
  "Content-Type": "application/json",
1142
119
  "Authorization": `Bearer ${this.apiKey}`,
1143
- ...this.defaultHeaders,
1144
- ...contextHeaders
120
+ ...withoutClientHeader(this.defaultHeaders),
121
+ ...withoutClientHeader(contextHeaders),
122
+ // Last, and any other spelling of it dropped above (fetch would
123
+ // JOIN `x-haven-client` and `X-Haven-Client` into one unparseable
124
+ // value), so neither `defaultHeaders` nor a dispatch context can
125
+ // misname the client; `clientIdentity` is the one way to set it.
126
+ [HAVEN_CLIENT_HEADER]: this.clientIdentity
1145
127
  },
1146
128
  body: body ? JSON.stringify(body) : void 0,
1147
129
  signal: controller.signal
1148
130
  });
1149
131
  const data = await res.json();
132
+ this.recordClientUpdate(data);
1150
133
  if (!res.ok) {
1151
134
  const record = data;
1152
135
  const errorText = typeof record.error === "string" ? record.error : void 0;
@@ -1248,13 +231,17 @@ function mapPaymentReceipt(raw) {
1248
231
  const receipt = {
1249
232
  id: raw.id,
1250
233
  paymentId: raw.payment_id,
234
+ source: raw.rail,
1251
235
  rail: raw.rail,
236
+ paymentProofStatus: raw.proof_status,
1252
237
  proofStatus: raw.proof_status,
1253
238
  txHash: raw.tx_hash,
1254
239
  fundingTxHash: raw.funding_tx_hash ?? null,
1255
240
  settlementTxHash: raw.settlement_tx_hash ?? null,
1256
241
  chainId: raw.chain_id,
242
+ x402ResourceUrl: raw.resource_url,
1257
243
  resourceUrl: raw.resource_url,
244
+ x402MerchantAddress: raw.merchant_address,
1258
245
  merchantAddress: raw.merchant_address,
1259
246
  payerAddress: raw.payer_address,
1260
247
  parties: mapParties(raw.parties),
@@ -1278,6 +265,9 @@ function mapPaymentReceipt(raw) {
1278
265
  if ("payment_intent_id" in raw) {
1279
266
  receipt.paymentIntentId = raw.payment_intent_id ?? null;
1280
267
  }
268
+ if (raw.scope) {
269
+ receipt.scope = { source: raw.scope.source, filter: raw.scope.filter };
270
+ }
1281
271
  if ("approval_request_id" in raw) {
1282
272
  receipt.approvalRequestId = raw.approval_request_id ?? null;
1283
273
  }
@@ -1418,12 +408,61 @@ function throwPaymentStateError(label, raw) {
1418
408
  throw new HavenApiError(message, statusCode, raw);
1419
409
  }
1420
410
 
411
+ // src/x402-retry-target.ts
412
+ function resolveX402RetryTarget(input) {
413
+ const requestUrl = input.requestUrl?.trim() || void 0;
414
+ if (requestUrl) {
415
+ return {
416
+ url: requestUrl,
417
+ source: "request",
418
+ resourceUrlDiffersFromRequest: requestUrl !== input.resourceUrl
419
+ };
420
+ }
421
+ return { url: input.resourceUrl, source: "resource" };
422
+ }
423
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "::1", "[::1]"]);
424
+ var RESERVED_SUFFIXES = [".test", ".localhost", ".invalid", ".example"];
425
+ var IPV4_LOOPBACK = /^127(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}$/;
426
+ function isSecureX402RetryTarget(url) {
427
+ let parsed;
428
+ try {
429
+ parsed = new URL(url);
430
+ } catch {
431
+ return false;
432
+ }
433
+ if (parsed.protocol === "https:") return true;
434
+ if (parsed.protocol !== "http:") return false;
435
+ const host = parsed.hostname.toLowerCase();
436
+ if (LOOPBACK_HOSTS.has(host) || IPV4_LOOPBACK.test(host)) return true;
437
+ return RESERVED_SUFFIXES.some((suffix) => host.endsWith(suffix));
438
+ }
439
+ var INSECURE_RETRY_TARGET_CODE = "INSECURE_RETRY_TARGET";
440
+ var HavenInsecureRetryTargetError = class extends HavenError {
441
+ constructor(url) {
442
+ super(
443
+ `Refusing to send a payment header to ${url}: the paid x402 retry must go to an https URL (or a loopback / reserved test host). The merchant's challenge declared this resource URL; retry with the https URL you quoted (pass it as \`url\`), and report a merchant whose challenge downgrades the scheme.`,
444
+ INSECURE_RETRY_TARGET_CODE,
445
+ 400
446
+ );
447
+ this.url = url;
448
+ this.name = "HavenInsecureRetryTargetError";
449
+ }
450
+ url;
451
+ };
452
+ function assertSecureX402RetryTarget(url) {
453
+ if (!isSecureX402RetryTarget(url)) throw new HavenInsecureRetryTargetError(url);
454
+ }
455
+
1421
456
  // src/mcp-merchant-transport.ts
1422
457
  var DEFAULT_MERCHANT_TIMEOUT = 3e5;
1423
458
  var MCP_NOTIFICATION_TIMEOUT = 1e4;
1424
459
  var MCP_PROTOCOL_VERSION = "2025-06-18";
1425
460
  var MCP_ACCEPT = "application/json, text/event-stream";
1426
461
  var MCP_CLIENT_INFO = { name: "haven-sdk", version: "1" };
462
+ var MCP_X402_PAYMENT_META_KEY = "x402/payment";
463
+ var MCP_X402_PAYMENT_RESPONSE_META_KEY = "x402/payment-response";
464
+ var X402_RETRY_REJECTED_STATUS = 402;
465
+ var SESSION_NOT_FOUND_NEXT_ACTION = "reinitialize_then_retry_same_payment_header";
1427
466
  var McpMerchantTransport = class {
1428
467
  merchantTimeout;
1429
468
  fetchImpl;
@@ -1519,7 +558,7 @@ var McpMerchantTransport = class {
1519
558
  return response;
1520
559
  }
1521
560
  const message = selectJsonRpcResult(parseSseJsonRpcMessages(text));
1522
- if (!message) return response;
561
+ if (!message || !("result" in message || "error" in message)) return response;
1523
562
  const body = "result" in message ? message.result : message;
1524
563
  const headers = new Headers(response.headers);
1525
564
  headers.set("content-type", "application/json");
@@ -1575,13 +614,64 @@ var McpMerchantTransport = class {
1575
614
  * overwrite, which is worse than the duplicate this change removes.
1576
615
  */
1577
616
  async deliverPayment(url, init, paymentHeader) {
617
+ assertSecureX402RetryTarget(url);
1578
618
  const headers = new Headers(init?.headers);
1579
619
  const send = x402PaymentHeaderNamesFor(paymentHeader);
1580
620
  for (const name of X402_PAYMENT_HEADER_NAMES) {
1581
621
  if (send.includes(name)) headers.set(name, paymentHeader);
1582
622
  else headers.delete(name);
1583
623
  }
1584
- return this.fetch(url, { ...init, headers });
624
+ return this.fetch(url, withMcpPaymentMeta({ ...init, headers }, paymentHeader));
625
+ }
626
+ /**
627
+ * #3171: deliver the payment; if the merchant answers its unknown-session
628
+ * 404 WITH the settled-nothing recovery data, re-initialize once and resend
629
+ * the SAME header on the new session. The merchant's own guarantee (#1578:
630
+ * the session guard runs before its payment gate) is what makes the resend
631
+ * safe — a bare -32001, or any other 404, is returned to the caller
632
+ * untouched (body re-wrapped, since reading it consumed the stream). One
633
+ * recovery only: a second 404 is the answer.
634
+ */
635
+ async deliverPaymentRecoveringSession(url, init, paymentHeader, reinitialize) {
636
+ const first = await this.deliverPayment(url, init, paymentHeader);
637
+ if (first.status !== 404) return first;
638
+ let text;
639
+ try {
640
+ text = await first.clone().text();
641
+ } catch {
642
+ return first;
643
+ }
644
+ if (!sessionNotFoundRecovery(text)) return first;
645
+ const sessionId = await reinitialize();
646
+ if (!sessionId) return first;
647
+ return this.deliverPayment(url, this.withSessionHeaders(init, sessionId), paymentHeader);
648
+ }
649
+ /**
650
+ * #3118: read a native MCP payment-required tool result from a response
651
+ * WITHOUT consuming it — JSON or SSE framed. `undefined` for anything that
652
+ * is not an `isError: true` tool result carrying a valid `PaymentRequired`
653
+ * (structured content first, text fallback second), so an ordinary tool
654
+ * error, a successful result that merely resembles a challenge, or an
655
+ * unparseable body is never mistaken for a payment demand.
656
+ */
657
+ async extractToolResultChallenge(response, init) {
658
+ const toolResult = await this.readToolResult(response, init);
659
+ return toolResult ? extractMcpPaymentRequired(toolResult) : void 0;
660
+ }
661
+ /**
662
+ * #3118: the tool RESULT a response carries, read WITHOUT consuming it, or
663
+ * `undefined`. Only a JSON or SSE body is read at all (#3155 review S2):
664
+ * a non-402 answer used to be returned untouched, and buffering an
665
+ * arbitrary body — a streaming or binary paid resource — to look for a
666
+ * challenge would block the caller until the stream ended or the merchant
667
+ * timeout fired. A merchant that speaks JSON-RPC declares one of those two
668
+ * content types.
669
+ */
670
+ async readToolResult(response, init) {
671
+ if (init !== void 0 && !isJsonRpcToolsCallBody(init.body)) return void 0;
672
+ const contentType = (response.headers.get("content-type") ?? "").toLowerCase();
673
+ if (!contentType.includes("application/json") && !contentType.includes("text/event-stream")) return void 0;
674
+ return mcpToolResultOf(await this.readMessage(response));
1585
675
  }
1586
676
  async notifyInitialized(url, init, sessionId, wallet) {
1587
677
  try {
@@ -1603,6 +693,106 @@ var McpMerchantTransport = class {
1603
693
  }
1604
694
  }
1605
695
  };
696
+ function mcpToolResultOf(value) {
697
+ if (!isRecord(value)) return void 0;
698
+ if ("jsonrpc" in value) {
699
+ return isRecord(value.result) ? value.result : void 0;
700
+ }
701
+ if ("isError" in value || Array.isArray(value.content) || isRecord(value._meta)) return value;
702
+ return void 0;
703
+ }
704
+ function extractMcpPaymentRequired(toolResult) {
705
+ if (toolResult.isError !== true) return void 0;
706
+ if (isRecord(toolResult.structuredContent)) {
707
+ const parsed = normalizePaymentRequired(toolResult.structuredContent);
708
+ if (parsed) return parsed;
709
+ }
710
+ if (Array.isArray(toolResult.content)) {
711
+ for (const item of toolResult.content) {
712
+ if (!isRecord(item) || item.type !== "text" || typeof item.text !== "string") continue;
713
+ let candidate;
714
+ try {
715
+ candidate = JSON.parse(item.text);
716
+ } catch {
717
+ continue;
718
+ }
719
+ const parsed = normalizePaymentRequired(candidate);
720
+ if (parsed) return parsed;
721
+ }
722
+ }
723
+ return void 0;
724
+ }
725
+ function mcpSettlementFromToolResult(toolResult) {
726
+ const meta = toolResult._meta;
727
+ if (!isRecord(meta)) return void 0;
728
+ const settlement = meta[MCP_X402_PAYMENT_RESPONSE_META_KEY];
729
+ if (!isRecord(settlement) || typeof settlement.success !== "boolean") return void 0;
730
+ return {
731
+ success: settlement.success,
732
+ ...typeof settlement.transaction === "string" ? { transaction: settlement.transaction } : {},
733
+ ...typeof settlement.network === "string" ? { network: settlement.network } : {},
734
+ ...typeof settlement.payer === "string" ? { payer: settlement.payer } : {},
735
+ ...typeof settlement.errorReason === "string" ? { errorReason: settlement.errorReason } : {}
736
+ };
737
+ }
738
+ function withMcpPaymentMeta(init, paymentHeader) {
739
+ const request = parseJsonRpcToolsCall(init.body);
740
+ if (!request) return init;
741
+ let envelope;
742
+ try {
743
+ envelope = decodeBase64Json(paymentHeader);
744
+ } catch {
745
+ return init;
746
+ }
747
+ if (!isRecord(envelope)) return init;
748
+ const params = request.params;
749
+ const existingMeta = isRecord(params._meta) ? params._meta : {};
750
+ return {
751
+ ...init,
752
+ body: JSON.stringify({
753
+ ...request,
754
+ params: { ...params, _meta: { ...existingMeta, [MCP_X402_PAYMENT_META_KEY]: envelope } }
755
+ })
756
+ };
757
+ }
758
+ function encodeMcpSettlementReceipt(settlement) {
759
+ return encodeBase64Json(settlement);
760
+ }
761
+ function parseJsonRpcToolsCall(body) {
762
+ if (typeof body !== "string") return void 0;
763
+ let request;
764
+ try {
765
+ request = JSON.parse(body);
766
+ } catch {
767
+ return void 0;
768
+ }
769
+ if (!isRecord(request) || request.method !== "tools/call" || !isRecord(request.params)) return void 0;
770
+ return request;
771
+ }
772
+ function isJsonRpcToolsCallBody(body) {
773
+ return parseJsonRpcToolsCall(body) !== void 0;
774
+ }
775
+ function sessionNotFoundRecovery(body) {
776
+ let parsed;
777
+ try {
778
+ parsed = JSON.parse(body);
779
+ } catch {
780
+ return void 0;
781
+ }
782
+ if (!isRecord(parsed) || !isRecord(parsed.error)) return void 0;
783
+ if (parsed.error.code !== -32001) return void 0;
784
+ const data = parsed.error.data;
785
+ if (!isRecord(data)) return void 0;
786
+ if (data.settled !== false || data.next_action !== SESSION_NOT_FOUND_NEXT_ACTION) return void 0;
787
+ return {
788
+ reason: typeof data.reason === "string" ? data.reason : "session_expired",
789
+ settled: false,
790
+ next_action: SESSION_NOT_FOUND_NEXT_ACTION
791
+ };
792
+ }
793
+ function isRecord(value) {
794
+ return typeof value === "object" && value !== null && !Array.isArray(value);
795
+ }
1606
796
  async function captureMerchantResponse(response) {
1607
797
  const merchant_body = await response.text().catch(() => "");
1608
798
  return {
@@ -1689,6 +879,10 @@ function deriveReadiness(status, allowances) {
1689
879
  if (status !== "active") return "revoked";
1690
880
  return allowances.some((allowance) => safeBigInt(allowance.remainingAtomic) > 0n) ? "ready" : "needs_approval";
1691
881
  }
882
+ function formatRemainingDisplay(tokenAddress, tokenSymbol, remainingAtomic) {
883
+ const token = resolveTokenFromAddress(tokenAddress);
884
+ return token ? `${formatAtomicAmount(safeBigInt(remainingAtomic), token.decimals)} ${tokenSymbol}` : `${remainingAtomic} ${tokenSymbol} (atomic; unknown decimals)`;
885
+ }
1692
886
  var AccountReads = class {
1693
887
  transport;
1694
888
  getPaymentStatus;
@@ -1710,12 +904,12 @@ var AccountReads = class {
1710
904
  async getAgentSummary() {
1711
905
  const [agent, allowanceSummary] = await Promise.all([this.getAgent(), this.getAllowances()]);
1712
906
  const allowances = allowanceSummary.allowances.map((allowance) => {
1713
- const token = resolveTokenFromAddress(allowance.tokenAddress);
1714
- const remainingDisplay = token ? `${formatAtomicAmount(safeBigInt(allowance.onchain.remaining), token.decimals)} ${allowance.tokenSymbol}` : `${allowance.onchain.remaining} ${allowance.tokenSymbol} (atomic; unknown decimals)`;
1715
907
  return {
908
+ id: allowance.id,
1716
909
  tokenSymbol: allowance.tokenSymbol,
910
+ tokenAddress: allowance.tokenAddress,
1717
911
  remainingAtomic: allowance.onchain.remaining,
1718
- remainingDisplay,
912
+ remainingDisplay: allowance.remainingDisplay,
1719
913
  configuredAmount: allowance.configuredAmount,
1720
914
  resetPeriodMin: allowance.resetPeriodMin,
1721
915
  isResetPending: allowance.onchain.isResetPending
@@ -1743,6 +937,7 @@ var AccountReads = class {
1743
937
  tokenSymbol: allowance.token_symbol,
1744
938
  configuredAmount: allowance.configured_amount,
1745
939
  resetPeriodMin: allowance.reset_period_min,
940
+ remainingDisplay: formatRemainingDisplay(allowance.token_address, allowance.token_symbol, allowance.onchain.remaining),
1746
941
  onchain: {
1747
942
  amount: allowance.onchain.amount,
1748
943
  spent: allowance.onchain.spent,
@@ -1757,6 +952,33 @@ var AccountReads = class {
1757
952
  }))
1758
953
  };
1759
954
  }
955
+ /**
956
+ * #3126 — the sufficiency signal behind {@link HavenBalanceCoverage}.
957
+ *
958
+ * Deliberately NOT a balance read: the endpoint answers whether the
959
+ * account HOLDS at least the checked amount, as `covered
960
+ * true/false/null`, and never returns the balance itself. The camelCase
961
+ * mapping is permissive (raw fields flow through; the server owns the
962
+ * wire shape, pinned by the backend's `expectMatchesSpec` assertion), so
963
+ * an older server that has not deployed the endpoint surfaces its 404 as
964
+ * a thrown error rather than a fabricated answer.
965
+ */
966
+ async checkFunds(input) {
967
+ const query = `token=${encodeURIComponent(input.token)}&amount_atomic=${encodeURIComponent(input.amountAtomic)}`;
968
+ const raw = await this.transport.get(
969
+ `/machine-payments/balance-coverage?${query}`
970
+ );
971
+ return {
972
+ covered: raw.covered,
973
+ ...raw.coverage_error !== void 0 ? { coverageError: raw.coverage_error } : {},
974
+ chainId: raw.chain_id,
975
+ tokenAddress: raw.token_address,
976
+ tokenSymbol: raw.token_symbol,
977
+ checkedAmountAtomic: raw.checked_amount_atomic,
978
+ budgetRemainingAtomic: raw.budget_remaining_atomic,
979
+ ...raw.budget_remaining_is_from_chain !== void 0 ? { budgetRemainingIsFromChain: raw.budget_remaining_is_from_chain } : {}
980
+ };
981
+ }
1760
982
  async getPostPurchaseAllowanceSummary(paymentId) {
1761
983
  const unavailable = (detail, payment2 = null) => ({
1762
984
  payment: payment2,
@@ -1808,10 +1030,23 @@ var AccountReads = class {
1808
1030
  return unavailable(error instanceof Error ? error.message : String(error));
1809
1031
  }
1810
1032
  }
1033
+ /** The first page's receipts as a bare array — the pre-#3128 shape, kept for callers that never page. */
1811
1034
  async listReceipts(options = {}) {
1812
- const query = options.limit ? `?limit=${encodeURIComponent(String(options.limit))}` : "";
1035
+ return (await this.listReceiptsPage(options)).receipts;
1036
+ }
1037
+ /** #3128: one page with `total`, `hasMore` and `nextCursor` — see {@link HavenPaymentReceiptsPage}. */
1038
+ async listReceiptsPage(options = {}) {
1039
+ const params = new URLSearchParams();
1040
+ if (options.limit) params.set("limit", String(options.limit));
1041
+ if (options.cursor) params.set("cursor", options.cursor);
1042
+ const query = params.size > 0 ? `?${params.toString()}` : "";
1813
1043
  const raw = await this.transport.get(`/machine-payments/receipts${query}`);
1814
- return raw.receipts.map(mapPaymentReceipt);
1044
+ return {
1045
+ receipts: raw.receipts.map(mapPaymentReceipt),
1046
+ total: typeof raw.total === "number" ? raw.total : null,
1047
+ hasMore: typeof raw.has_more === "boolean" ? raw.has_more : null,
1048
+ nextCursor: typeof raw.next_cursor === "string" ? raw.next_cursor : null
1049
+ };
1815
1050
  }
1816
1051
  async getReceipt(paymentId) {
1817
1052
  const { receipt } = await this.transport.get(`/payments/${paymentId}/receipt`);
@@ -1927,7 +1162,7 @@ function chainIdFromNetwork(network) {
1927
1162
  function chainIdOrNull(network) {
1928
1163
  return chainIdFromNetwork(network) ?? null;
1929
1164
  }
1930
- function sameAddress3(a, b) {
1165
+ function sameAddress(a, b) {
1931
1166
  return Boolean(a && b && a.toLowerCase() === b.toLowerCase());
1932
1167
  }
1933
1168
  function decimalFromUsdcAtomic(value) {
@@ -1983,8 +1218,11 @@ function requestInitFromSnapshot(request) {
1983
1218
  }
1984
1219
  function noCompatiblePaymentOptionError(accepts) {
1985
1220
  const erc7710Only = selectErc7710PaymentOption(accepts) !== null;
1221
+ const unsupportedOnly = !erc7710Only && accepts.some(
1222
+ (opt) => opt !== null && typeof opt === "object" && !isErc7710Option(opt) && (x402AssetTransferMethod(opt) !== null || opt.extra?.paymentFlow !== void 0)
1223
+ );
1986
1224
  return new HavenApiError(
1987
- "No compatible payment option found in x402 requirements. Haven supports standard x402 exact payments on Base USDC." + (erc7710Only ? " The only Haven-compatible option this merchant advertises is tagged extra.assetTransferMethod: 'erc7710' (direct settlement), which this EIP-3009 payment path cannot settle \u2014 the limitation is the settlement scheme, not the asset. Paying this merchant requires a delegation-rail erc7710 flow (settleX402Erc7710, or the hosted MCP purchase tools)." : ""),
1225
+ "No compatible payment option found in x402 requirements. Haven supports standard x402 exact payments on Base USDC." + (erc7710Only ? " The only Haven-compatible option this merchant advertises is tagged extra.assetTransferMethod: 'erc7710' (direct settlement), which this EIP-3009 payment path cannot settle \u2014 the limitation is the settlement scheme, not the asset. Paying this merchant requires a delegation-rail erc7710 flow (settleX402Erc7710, or the hosted MCP purchase tools)." : unsupportedOnly ? " Every option this merchant advertises asks for a transfer method or payment flow Haven cannot construct (for example extra.assetTransferMethod: 'permit2', or an unrecognized extra.paymentFlow). No payment intent was created and no funds moved." : ""),
1988
1226
  400
1989
1227
  );
1990
1228
  }
@@ -2004,6 +1242,11 @@ function buildX402Quote(paymentRequired, request, idempotencyKey, mcpTransport)
2004
1242
  request,
2005
1243
  ...mcpTransport ? { mcpTransport } : {},
2006
1244
  resourceUrl: paymentRequired.resource.url,
1245
+ // #3097: the merchant's declaration vs the URL the caller quoted. The
1246
+ // paid retry goes to the caller's URL; a quote that says the two disagree
1247
+ // is how an agent sees a challenge that downgrades the scheme or moves
1248
+ // the host before it pays.
1249
+ resourceUrlDiffersFromRequest: paymentRequired.resource.url !== request.url,
2007
1250
  description: paymentRequired.resource.description ?? option.description ?? null,
2008
1251
  mimeType: paymentRequired.resource.mimeType ?? option.mimeType ?? null,
2009
1252
  amountAtomic: x402AuthorizationAmount(option),
@@ -2122,7 +1365,7 @@ function assertCanResumeX402(status, paymentRequired, option) {
2122
1365
  status.paymentId
2123
1366
  );
2124
1367
  }
2125
- if (status.merchantAddress && !sameAddress3(status.merchantAddress, option.payTo)) {
1368
+ if (status.merchantAddress && !sameAddress(status.merchantAddress, option.payTo)) {
2126
1369
  throw new HavenApiError(
2127
1370
  "x402 resume request does not match the approved merchant.",
2128
1371
  409,
@@ -2458,7 +1701,10 @@ var X402Erc7710 = class {
2458
1701
  );
2459
1702
  }
2460
1703
  const prepared = await this.prepare(paymentRequired, options);
2461
- const signature = await this.signForData(prepared.signData);
1704
+ const signature = await this.signForData(
1705
+ prepared.signData,
1706
+ settlementExpectationFromQuote(prepared.settlement)
1707
+ );
2462
1708
  const paymentHeader = await this.submit(prepared.paymentId, signature);
2463
1709
  return { ...prepared.settlement, paymentHeader };
2464
1710
  }
@@ -2573,6 +1819,21 @@ var X402Erc7710 = class {
2573
1819
  return settled.payment_header;
2574
1820
  }
2575
1821
  };
1822
+ function settlementExpectationFromQuote(settlement) {
1823
+ const chainId = chainIdForNetwork(settlement.network);
1824
+ if (chainId === void 0) {
1825
+ throw new HavenSigningError(
1826
+ `Refusing to sign the x402 settlement child: the merchant's network ${JSON.stringify(settlement.network)} is not one this SDK can verify a settlement child on.`
1827
+ );
1828
+ }
1829
+ return {
1830
+ merchantTo: settlement.merchantPayTo,
1831
+ amount: settlement.amountAtomic,
1832
+ asset: settlement.asset,
1833
+ chainId,
1834
+ ...settlement.facilitatorAddresses && settlement.facilitatorAddresses.length > 0 ? { redeemers: settlement.facilitatorAddresses } : {}
1835
+ };
1836
+ }
2576
1837
 
2577
1838
  // src/tool-adapter.ts
2578
1839
  function toolX402PaymentRequired(input) {
@@ -2698,12 +1959,17 @@ var MerchantCompletion = class {
2698
1959
  if (!receipt.paymentHeader) {
2699
1960
  throw new HavenApiError("No x402 payment header was returned for payment retry", 500);
2700
1961
  }
2701
- const retryResponse = await this.merchantTransport.deliverPayment(
1962
+ const retryResponse = await this.merchantTransport.deliverPaymentRecoveringSession(
2702
1963
  url,
2703
1964
  initialInit,
2704
- receipt.paymentHeader
1965
+ receipt.paymentHeader,
1966
+ () => this.merchantTransport.initialize(url, initialInit)
2705
1967
  );
2706
- if (!retryResponse.ok) {
1968
+ const toolResult = retryResponse.ok ? await this.merchantTransport.readToolResult(retryResponse) : void 0;
1969
+ const inBandChallenge = toolResult ? extractMcpPaymentRequired(toolResult) : void 0;
1970
+ const metaSettlement = toolResult ? mcpSettlementFromToolResult(toolResult) : void 0;
1971
+ const inBandRejection = inBandChallenge !== void 0 || metaSettlement?.success === false;
1972
+ if (!retryResponse.ok || inBandRejection) {
2707
1973
  const merchant = await captureMerchantResponse(retryResponse);
2708
1974
  await this.recordRetryRejected({
2709
1975
  rail: "x402",
@@ -2718,7 +1984,7 @@ var MerchantCompletion = class {
2718
1984
  });
2719
1985
  throw new HavenApiError(
2720
1986
  "x402 retry failed after Haven funded the delegate wallet; reconciliation may be required.",
2721
- merchant.merchant_status,
1987
+ inBandRejection ? X402_RETRY_REJECTED_STATUS : merchant.merchant_status,
2722
1988
  {
2723
1989
  marker: "x402_retry_rejected_after_funding",
2724
1990
  payment_id: receipt.paymentId,
@@ -2730,7 +1996,10 @@ var MerchantCompletion = class {
2730
1996
  }
2731
1997
  );
2732
1998
  }
2733
- const merchantSettlement = parseMerchantSettlement(retryResponse.headers.get("PAYMENT-RESPONSE"));
1999
+ const headerReceipt = retryResponse.headers.get("PAYMENT-RESPONSE");
2000
+ const protocolReceiptHeader = headerReceipt ?? (metaSettlement ? encodeMcpSettlementReceipt(metaSettlement) : void 0);
2001
+ const protocolReceiptHeaderName = headerReceipt ? "PAYMENT-RESPONSE" : metaSettlement ? `_meta.${MCP_X402_PAYMENT_RESPONSE_META_KEY}` : void 0;
2002
+ const merchantSettlement = parseMerchantSettlement(protocolReceiptHeader ?? null);
2734
2003
  if (receipt.merchant && merchantSettlement.settlementTxHash) {
2735
2004
  receipt.merchant.settlementTxHash = merchantSettlement.settlementTxHash;
2736
2005
  receipt.merchant.settlementExplorerUrl = buildExplorerUrl(
@@ -2748,8 +2017,8 @@ var MerchantCompletion = class {
2748
2017
  selectedPayment: receipt.accepted,
2749
2018
  paymentProofHeaderName: x402PaymentHeaderNamesSent(receipt.paymentHeader),
2750
2019
  paymentProofHeader: receipt.paymentHeader,
2751
- protocolReceiptHeaderName: "PAYMENT-RESPONSE",
2752
- protocolReceiptHeader: retryResponse.headers.get("PAYMENT-RESPONSE") ?? void 0
2020
+ protocolReceiptHeaderName,
2021
+ protocolReceiptHeader
2753
2022
  });
2754
2023
  await this.reportMerchantReceipt(receipt.paymentId, retryResponse);
2755
2024
  return retryResponse;
@@ -3077,7 +2346,18 @@ function mapCatalogEntry(entry) {
3077
2346
  verifiedAt: entry.verified_at,
3078
2347
  source: entry.source,
3079
2348
  domainVerified: entry.domain_verified,
3080
- verifiedPayable: entry.verified_payable
2349
+ verifiedPayable: entry.verified_payable,
2350
+ // #3078: absent (older backend) or null (unresolved join) both mean "no
2351
+ // merchant known" — the public field is then absent, never null.
2352
+ ...entry.merchant ? {
2353
+ merchant: {
2354
+ id: entry.merchant.id,
2355
+ slug: entry.merchant.slug,
2356
+ name: entry.merchant.name,
2357
+ listingStatus: entry.merchant.listing_status,
2358
+ isTestMerchant: entry.merchant.is_test_merchant
2359
+ }
2360
+ } : {}
3081
2361
  };
3082
2362
  }
3083
2363
  var HavenClient = class {
@@ -3150,7 +2430,7 @@ var HavenClient = class {
3150
2430
  this.erc7710 = new X402Erc7710({
3151
2431
  delegateKey: this.delegateKey,
3152
2432
  post: (path, body) => this.post(path, body),
3153
- signForData: (signData) => this.signForData(signData),
2433
+ signForData: (signData, settlementExpectation) => this.signForData(signData, settlementExpectation),
3154
2434
  getAgent: () => this.getAgent()
3155
2435
  });
3156
2436
  }
@@ -3172,11 +2452,21 @@ var HavenClient = class {
3172
2452
  withRequestContext(headers, fn) {
3173
2453
  return this.havenApi.withRequestContext(headers, fn);
3174
2454
  }
2455
+ /**
2456
+ * The backend's newest `client_update` hint for this client (#3303): inside
2457
+ * a `withRequestContext` dispatch, the one that dispatch's own requests
2458
+ * received; outside one, the latest seen. Undefined when the client is
2459
+ * current or the backend predates #3303.
2460
+ */
2461
+ clientUpdate() {
2462
+ return this.havenApi.clientUpdate();
2463
+ }
3175
2464
  // ── High-Level API ───────────────────────────────────────────────
3176
2465
  /**
3177
2466
  * Send a payment in one call.
3178
2467
  *
3179
- * Creates the intent, signs the hash, submits the signature,
2468
+ * Creates the intent, signs its `sign_data` (a delegation-rail intent's
2469
+ * EIP-712 typed data, after the #3271 UserOp binding check), submits the signature,
3180
2470
  * and polls until confirmed (or throws on failure/timeout).
3181
2471
  *
3182
2472
  * Requires `delegateKey` to be set in the client config.
@@ -3188,7 +2478,7 @@ var HavenClient = class {
3188
2478
  );
3189
2479
  }
3190
2480
  const intent = await this.createIntent(request);
3191
- const signature = this.sign(intent.signData.hash);
2481
+ const signature = await this.signForData(intent.signData);
3192
2482
  await this.submitSignature(intent.paymentId, signature);
3193
2483
  return this.waitForConfirmation(intent.paymentId);
3194
2484
  }
@@ -3335,7 +2625,7 @@ var HavenClient = class {
3335
2625
  * (#834) — the backend refuses those intents with HTTP 410 before any
3336
2626
  * sign_data reaches a client, so encountering it here is a hard error too.
3337
2627
  */
3338
- async signForData(signData) {
2628
+ async signForData(signData, settlementExpectation) {
3339
2629
  if (!this.delegateKey) {
3340
2630
  throw new HavenSigningError(
3341
2631
  "Cannot sign without a delegateKey. Pass the private key in HavenClient config, or sign externally."
@@ -3353,6 +2643,11 @@ var HavenClient = class {
3353
2643
  "sign_data.signature_scheme is eip712_userop but typed_data is missing \u2014 refusing to sign the bare hash (the account would reject it)."
3354
2644
  );
3355
2645
  }
2646
+ assertUserOpTypedDataBinding(signData.typed_data, signData.hash);
2647
+ assertBoundDirectPaymentUserOp(
2648
+ signData.typed_data,
2649
+ this.delegateAddress
2650
+ );
3356
2651
  return signUserOpTypedDataForDelegation(this.delegateKey, signData.typed_data);
3357
2652
  }
3358
2653
  if (scheme === "eip712_delegation") {
@@ -3361,6 +2656,12 @@ var HavenClient = class {
3361
2656
  "sign_data.signature_scheme is eip712_delegation but typed_data is missing \u2014 refusing to sign the bare hash (the settlement would be rejected on redemption)."
3362
2657
  );
3363
2658
  }
2659
+ if (!settlementExpectation) {
2660
+ throw new HavenTypedDataRefusedError(
2661
+ "Refusing to sign an x402 settlement child without an independent expectation of what it must pay (the merchant's 402). Pay erc7710 merchants through settleX402Erc7710(), which supplies it."
2662
+ );
2663
+ }
2664
+ assertOwnSettlementChild(signData.typed_data, settlementExpectation, this.delegateAddress);
3364
2665
  return signSettlementDelegationTypedData(this.delegateKey, signData.typed_data);
3365
2666
  }
3366
2667
  if (scheme === void 0) {
@@ -3421,11 +2722,12 @@ var HavenClient = class {
3421
2722
  return this.accountReads.getAgentSummary();
3422
2723
  }
3423
2724
  /**
3424
- * Sweep stranded USDC and ETH from the delegate EOA back to the originating Safe.
2725
+ * Sweep stranded USDC and ETH from the delegate EOA back to the agent's Haven account.
3425
2726
  *
3426
2727
  * The delegate key held by this client signs and submits the transfer transactions
3427
2728
  * directly — Haven's backend never handles the key or constructs signed txs
3428
- * (CASP/MiCA Red Line #2). Funds always go to the Safe linked to this agent.
2729
+ * (CASP/MiCA Red Line #2). Funds always go to the Haven account linked to this agent
2730
+ * (`accountAddress` from `getAgent()`).
3429
2731
  *
3430
2732
  * Requires `chainRpcs` to be set for the agent's chain in `HavenClientConfig`.
3431
2733
  */
@@ -3459,6 +2761,49 @@ var HavenClient = class {
3459
2761
  async getAllowances() {
3460
2762
  return this.accountReads.getAllowances();
3461
2763
  }
2764
+ /**
2765
+ * #3126 — is the checked amount of the token actually HELD on the
2766
+ * agent's own account?
2767
+ *
2768
+ * This is the companion to {@link getAllowances}, not a variant of it:
2769
+ * allowances answer what the agent is PERMITTED to spend this period;
2770
+ * this answers whether the account HOLDS funds behind that permission,
2771
+ * as a sufficiency signal — `covered: true | false | null` — never as a
2772
+ * balance. `covered: null` means the chain read failed: treat it as
2773
+ * unverifiable, not as absence (`coverageError` says why). The account's
2774
+ * balance itself is deliberately not returned.
2775
+ */
2776
+ async checkFunds(input) {
2777
+ return this.accountReads.checkFunds(input);
2778
+ }
2779
+ /**
2780
+ * `POST /machine-payments/budget-precheck` (#3054): ask Haven to decide —
2781
+ * server-side — whether `amountAtomic` of `token` fits the agent's
2782
+ * remaining delegation budget, the same compare the guided prepare used to
2783
+ * run locally over its allowances read.
2784
+ *
2785
+ * On insufficiency Haven refuses (403, `delegation_budget_exceeded`) and
2786
+ * the refusal reaches the `payment_refusals` ledger with
2787
+ * `source: 'hosted_prepare'` — the point of the endpoint. This method
2788
+ * surfaces that decision as a thrown {@link HavenApiError}; it does NOT
2789
+ * swallow it, because swallowing would turn a decided refusal into the
2790
+ * degrade-to-warning path and the ledger row would still land while the
2791
+ * purchase proceeded.
2792
+ *
2793
+ * camelCase body like the route family; the response mirrors the wire
2794
+ * (`sufficient`, `remaining_atomic`). `resourceUrl` is the merchant
2795
+ * resource being bought — the ledger dedupe window's discriminating
2796
+ * column — never this request's own URL.
2797
+ */
2798
+ async precheckBudget(input) {
2799
+ return this.post("/machine-payments/budget-precheck", {
2800
+ chainId: input.chainId,
2801
+ token: input.token,
2802
+ amountAtomic: input.amountAtomic,
2803
+ ...input.merchantTo !== void 0 ? { merchantTo: input.merchantTo } : {},
2804
+ ...input.resourceUrl !== void 0 ? { resourceUrl: input.resourceUrl } : {}
2805
+ });
2806
+ }
3462
2807
  /**
3463
2808
  * Post-purchase allowance/budget summary for a settled payment (#1310).
3464
2809
  *
@@ -3581,6 +2926,10 @@ var HavenClient = class {
3581
2926
  async listReceipts(options = {}) {
3582
2927
  return this.accountReads.listReceipts(options);
3583
2928
  }
2929
+ /** #3128: one page of receipts with `total`, `hasMore` and `nextCursor`. */
2930
+ async listReceiptsPage(options = {}) {
2931
+ return this.accountReads.listReceiptsPage(options);
2932
+ }
3584
2933
  /**
3585
2934
  * Fetch the verifiable receipt bundle for a settled payment and verify it
3586
2935
  * locally. The server's own verification is ignored — the receipt is verified
@@ -3666,6 +3015,11 @@ var HavenClient = class {
3666
3015
  const request = snapshotX402Request(url, initialInit);
3667
3016
  const response = await this.merchantTransport.fetch(url, initialInit);
3668
3017
  if (response.status !== 402) {
3018
+ const toolResultChallenge = await this.merchantTransport.extractToolResultChallenge(response, initialInit);
3019
+ if (toolResultChallenge) {
3020
+ const mcpTransport2 = await this.merchantTransport.detect(url, toolResultChallenge, response);
3021
+ return buildX402Quote(toolResultChallenge, request, options.idempotencyKey, mcpTransport2);
3022
+ }
3669
3023
  let body;
3670
3024
  try {
3671
3025
  body = await response.clone().json();
@@ -3832,10 +3186,11 @@ var HavenClient = class {
3832
3186
  return this.merchantCompletion.retryRequest(url ?? paymentRequired.resource.url, initialInit, paymentRequired, receipt);
3833
3187
  }
3834
3188
  /**
3835
- * Fetch wrapper that automatically handles HTTP 402 responses.
3189
+ * Fetch wrapper that automatically handles HTTP 402 responses — and, since
3190
+ * #3118, a native-MCP payment-required tool result answered under HTTP 200.
3836
3191
  *
3837
- * Works like the standard `fetch()` but intercepts 402 responses,
3838
- * pays via x402 through Haven, and retries the request.
3192
+ * Works like the standard `fetch()` but intercepts 402 responses (or that
3193
+ * tool-result challenge), pays via x402 through Haven, and retries the request.
3839
3194
  *
3840
3195
  * ```ts
3841
3196
  * const response = await haven.fetch('https://paid-api.com/data')
@@ -3843,8 +3198,9 @@ var HavenClient = class {
3843
3198
  * ```
3844
3199
  *
3845
3200
  * **MCP-over-x402 auto-handshake (issue #315):** when the endpoint is
3846
- * MCP-shaped — the URL path ends in `/mcp`, or the 402 body carries a
3847
- * Coinbase Bazaar `extensions.bazaar` block — the SDK runs the MCP
3201
+ * MCP-shaped — the URL path ends in `/mcp`, or the 402 body (or, since
3202
+ * #3118, the tool-result challenge) carries a Coinbase Bazaar
3203
+ * `extensions.bazaar` block — the SDK runs the MCP
3848
3204
  * `initialize` handshake, threads the resulting `mcp-session-id`,
3849
3205
  * `Accept: application/json, text/event-stream`, and `x402-wallet` headers
3850
3206
  * through every request, and collapses SSE responses to the JSON-RPC
@@ -3862,16 +3218,23 @@ var HavenClient = class {
3862
3218
  let requestInit = withX402Wallet(init, x402PayerAddress(this.delegateAddress, this.x402Wallet));
3863
3219
  if (mcpSessionId) requestInit = this.merchantTransport.withSessionHeaders(requestInit, mcpSessionId);
3864
3220
  const response = await this.merchantTransport.fetch(url, requestInit);
3865
- if (response.status !== 402) {
3866
- return mcpSessionId ? this.merchantTransport.surfaceResult(response) : response;
3867
- }
3868
3221
  let paymentRequired;
3869
- try {
3870
- paymentRequired = await parsePaymentRequiredResponse(response);
3871
- } catch {
3872
- return response;
3222
+ let nativeChallenge = false;
3223
+ if (response.status !== 402) {
3224
+ const toolResultChallenge = await this.merchantTransport.extractToolResultChallenge(response, requestInit);
3225
+ if (!toolResultChallenge) {
3226
+ return mcpSessionId ? this.merchantTransport.surfaceResult(response) : response;
3227
+ }
3228
+ paymentRequired = toolResultChallenge;
3229
+ nativeChallenge = true;
3230
+ } else {
3231
+ try {
3232
+ paymentRequired = await parsePaymentRequiredResponse(response);
3233
+ } catch {
3234
+ return response;
3235
+ }
3873
3236
  }
3874
- if (!mcpSessionId && await this.merchantTransport.hasBazaarExtension(response)) {
3237
+ if (!mcpSessionId && (paymentRequired.extensions?.bazaar != null || await this.merchantTransport.hasBazaarExtension(response))) {
3875
3238
  mcpSessionId = await this.merchantTransport.initialize(url, init);
3876
3239
  if (mcpSessionId) requestInit = this.merchantTransport.withSessionHeaders(requestInit, mcpSessionId);
3877
3240
  }
@@ -3893,7 +3256,7 @@ var HavenClient = class {
3893
3256
  throw err;
3894
3257
  }
3895
3258
  const retryResponse = await this.merchantCompletion.retryRequest(url, requestInit, paymentRequired, receipt);
3896
- return mcpSessionId ? this.merchantTransport.surfaceResult(retryResponse) : retryResponse;
3259
+ return mcpSessionId || nativeChallenge ? this.merchantTransport.surfaceResult(retryResponse) : retryResponse;
3897
3260
  }
3898
3261
  /**
3899
3262
  * Deliver an already-signed x402 payment header to the merchant and return
@@ -3914,7 +3277,7 @@ var HavenClient = class {
3914
3277
  * collapses an SSE JSON-RPC response to its `result`.
3915
3278
  */
3916
3279
  /**
3917
- * Wait for a payment's Safe→delegate funding tx to reach ≥1 on-chain
3280
+ * Wait for a payment's account→delegate funding tx to reach ≥1 on-chain
3918
3281
  * confirmation. The hosted x402 completion path MUST call this after funding
3919
3282
  * and before delivering the merchant payment header, so the merchant's
3920
3283
  * balanceOf(delegate) / transferWithAuthorization verification sees the funded
@@ -3953,10 +3316,13 @@ var HavenClient = class {
3953
3316
  }
3954
3317
  let requestInit = withX402Wallet(input.init, x402Wallet) ?? {};
3955
3318
  if (mcpSessionId) requestInit = this.merchantTransport.withSessionHeaders(requestInit, mcpSessionId);
3956
- const response = await this.merchantTransport.deliverPayment(input.url, requestInit, input.paymentHeader);
3957
- const surfaced = mcpSessionId ? await this.merchantTransport.surfaceResult(response) : response;
3958
- const protocolReceiptHeader = surfaced.headers.get("PAYMENT-RESPONSE") ?? void 0;
3959
- const settlement = parseMerchantSettlement(protocolReceiptHeader ?? null);
3319
+ const response = await this.merchantTransport.deliverPaymentRecoveringSession(
3320
+ input.url,
3321
+ requestInit,
3322
+ input.paymentHeader,
3323
+ () => this.merchantTransport.initialize(input.url, input.init, x402Wallet)
3324
+ );
3325
+ const surfaced = await this.merchantTransport.surfaceResult(response);
3960
3326
  const text = await surfaced.text();
3961
3327
  let body;
3962
3328
  try {
@@ -3964,8 +3330,16 @@ var HavenClient = class {
3964
3330
  } catch {
3965
3331
  body = text;
3966
3332
  }
3333
+ const toolResult = mcpToolResultOf(body);
3334
+ const inBandChallenge = toolResult ? extractMcpPaymentRequired(toolResult) : void 0;
3335
+ const metaSettlement = toolResult ? mcpSettlementFromToolResult(toolResult) : void 0;
3336
+ const headerReceipt = surfaced.headers.get("PAYMENT-RESPONSE") ?? void 0;
3337
+ const protocolReceiptHeaderName = headerReceipt ? "PAYMENT-RESPONSE" : metaSettlement ? `_meta.${MCP_X402_PAYMENT_RESPONSE_META_KEY}` : void 0;
3338
+ const protocolReceiptHeader = headerReceipt ?? (metaSettlement ? encodeMcpSettlementReceipt(metaSettlement) : void 0);
3339
+ const settlement = parseMerchantSettlement(protocolReceiptHeader ?? null);
3340
+ const merchantAccepted = surfaced.ok && inBandChallenge === void 0 && metaSettlement?.success !== false;
3967
3341
  let evidenceOutcome;
3968
- if (!surfaced.ok) {
3342
+ if (!merchantAccepted) {
3969
3343
  if (!input.noFundingLeg && fundingTxHash) {
3970
3344
  await this.merchantCompletion.recordRetryRejected({
3971
3345
  rail: "x402",
@@ -3995,7 +3369,7 @@ var HavenClient = class {
3995
3369
  merchantStatus: surfaced.status,
3996
3370
  paymentProofHeaderName: x402PaymentHeaderNamesSent(input.paymentHeader),
3997
3371
  paymentProofHeader: input.paymentHeader,
3998
- protocolReceiptHeaderName: protocolReceiptHeader ? "PAYMENT-RESPONSE" : void 0,
3372
+ protocolReceiptHeaderName,
3999
3373
  protocolReceiptHeader
4000
3374
  });
4001
3375
  }
@@ -4003,9 +3377,10 @@ var HavenClient = class {
4003
3377
  }
4004
3378
  return {
4005
3379
  status: surfaced.status,
4006
- ok: surfaced.ok,
3380
+ ok: merchantAccepted,
4007
3381
  body,
4008
- settlementTxHash: settlement.settlementTxHash ?? void 0,
3382
+ // #3155 review S3: a transaction beside a rejection is not a settlement.
3383
+ settlementTxHash: merchantAccepted ? settlement.settlementTxHash ?? void 0 : void 0,
4009
3384
  evidenceOutcome
4010
3385
  };
4011
3386
  }
@@ -4195,12 +3570,12 @@ var toolDescriptions = {
4195
3570
  payX402OneShot: {
4196
3571
  summary: "Fetch an x402 paid HTTP resource in a single call. Handles the full probe -> pay -> retry round trip and returns the merchant response.",
4197
3572
  selectionGuidance: "Prefer this over the quote+pay split when the agent just wants the paid resource and does not need to inspect the price first. If you already have a quote from haven_quote_x402, use haven_pay_x402_quote instead. Do not use for read-only allowance, budget, spend-limit, remaining-amount, reset-period, or what-can-I-spend questions; use the allowance lookup tool instead.",
4198
- behavior: "Calls the URL, parses any HTTP 402 x402 challenge, signs the payment locally, then retries the original request with the signed payment header (sent under PAYMENT-SIGNATURE, plus the legacy X-PAYMENT on the EIP-3009 path only) and returns the merchant response. Settlement is either direct account-to-merchant with no funding leg, or a bridge that first redeems the agent's budget delegation to fund the delegate wallet for an EIP-3009 authorization. A payment outside the on-chain budget is declined before any money moves; nothing is queued for a human to approve later. If the resource returns a non-402 status, returns it unchanged without contacting Haven.",
3573
+ behavior: "Calls the URL, parses any HTTP 402 x402 challenge (or, since #3118, a native-MCP payment-required tool result under HTTP 200), signs the payment locally, then retries the original request with the signed payment header (sent under PAYMENT-SIGNATURE, plus the legacy X-PAYMENT on the EIP-3009 path only) and returns the merchant response. Settlement is either direct account-to-merchant with no funding leg, or a bridge that first redeems the agent's budget delegation to fund the delegate wallet for an EIP-3009 authorization. A payment outside the on-chain budget is declined before any money moves; nothing is queued for a human to approve later. If the resource returns a non-402 status with no payment-required tool result, returns it unchanged without contacting Haven.",
4199
3574
  nextActionGuidance: "Preserve the returned resume_state or paymentId \u2014 either identifies this payment if you need to ask about it later. This tool performs the merchant retry itself, so do not wait on a signal while the call is in flight. If the process crashes after this call and a later haven_get_payment_status reports nextAction=retry_original_x402_request, Haven's funding leg confirmed but no merchant response was ever recorded \u2014 call the resume tool with the preserved resume_state or payment_id instead of paying again. If the response carries phase=insufficient_funds and nextAction=fund_account_or_raise_allowance, the payment cannot be retried until the account is funded or the agent budget raised \u2014 stop and tell the user the shortfall reported on the response."
4200
3575
  },
4201
3576
  resumeX402: {
4202
3577
  summary: "Resume an x402 payment whose Haven-side authorization already succeeded but whose merchant retry did not complete.",
4203
- behavior: "Accepts either resume_state or payment_id, validates the original x402 details against the authorized Haven funding, and retries the merchant request with the signed payment header (sent under PAYMENT-SIGNATURE, plus the legacy X-PAYMENT on the EIP-3009 path only). No new Haven payment is created.",
3578
+ behavior: "Accepts either resume_state or payment_id, validates the x402 details against the authorized funding, and retries the merchant request with the signed payment header (PAYMENT-SIGNATURE, plus legacy X-PAYMENT on EIP-3009 only; also params._meta on an MCP tools/call body). No new Haven payment is created.",
4204
3579
  nextActionGuidance: "Only call this after haven_get_payment_status reports nextAction=retry_original_x402_request \u2014 that means Haven's funding leg confirmed but no merchant response was ever recorded, most often because the process crashed between funding and the merchant retry. Any other nextAction reports a conflict instead of retrying, so do not call this speculatively. Do not start a new merchant session and do not pay again \u2014 that would pay twice for one resource."
4205
3580
  },
4206
3581
  // #1328: quoteMpp / payMpp / resumeMpp (the mpp_demo challenge/quote/resume
@@ -4218,21 +3593,29 @@ var toolDescriptions = {
4218
3593
  nextActionGuidance: ""
4219
3594
  },
4220
3595
  getAgent: {
4221
- summary: "Return the authenticated agent identity AND its live spend authority in one call: Haven wallet, delegate, chain, raw status, spend_authority_readiness, and per-token remaining allowance (atomic + human-readable). The recommended first call in a new session to confirm who you are and whether Haven will let you spend right now.",
4222
- selectionGuidance: "Use this as the one-shot orientation/bootstrap at the start of a session, or whenever you need to confirm identity together with whether the agent can spend right now. For a detailed per-token breakdown (configured vs spent vs reset window) use haven_get_allowances.",
4223
- behavior: `Reads identity plus the live spend-authority snapshot in one shot \u2014 the agent's active on-chain budget delegation. spend_authority_readiness (readiness is a deprecated alias, same value) is "ready" when at least one token has remaining spend authority, "needs_approval" when the agent is active but has none, and "revoked" when the credential is not active. It covers hosted identity + on-chain spend authority ONLY \u2014 the hosted server cannot see the LOCAL signer, so "ready" does not mean the signer can start; verify the signer with a signer tool call or connect --doctor. An over-budget payment is declined before any money moves: there is no approval queue, so ask the owner to grant or raise the budget in Haven rather than waiting for an approval. allowances[] carries remainingAtomic and remainingDisplay per token. Identity fields: id, name, status, accountAddress, delegateAddress, chainId.`,
3596
+ summary: "Return the authenticated agent identity AND its live spend authority in one call: Haven wallet, delegate, chain, raw status, spend_authority_readiness, per-token remaining allowance (atomic + human-readable). The recommended first call in a new session.",
3597
+ selectionGuidance: "Use this as the session bootstrap, or to confirm identity together with whether the agent can spend right now. For per-token detail (configured vs spent vs reset window) use haven_get_allowances.",
3598
+ behavior: 'Reads identity plus the live spend-authority snapshot \u2014 the active on-chain budget delegation. spend_authority_readiness (readiness is a deprecated alias, same value) is "ready" when at least one token has remaining spend authority, "needs_approval" when the agent is active but has none, and "revoked" when the credential is not active. It covers hosted identity + on-chain spend authority ONLY \u2014 the hosted server cannot see the LOCAL signer, so "ready" does not mean the signer can start; verify the signer with a signer tool call or connect --doctor. An over-budget payment is declined before any money moves; there is no approval queue \u2014 ask the owner to grant or raise the budget in Haven. allowances[] carries id, tokenAddress, remainingAtomic, remainingDisplay per token. Identity fields: id, name, status, accountAddress, delegateAddress, chainId.',
4224
3599
  nextActionGuidance: ""
4225
3600
  },
4226
3601
  getAllowances: {
4227
3602
  summary: "Return configured and on-chain allowance state for the authenticated agent. On-chain allowance is the real spend gate.",
4228
- selectionGuidance: "Use this when the user asks about allowance, budget, spend limit, remaining amount, remaining allowance, remaining budget, daily limit, reset period, what can I spend, or what the agent can still spend.",
4229
- behavior: "Returns the per-token spend authority for the account: the active budget delegation (remaining = the period budget, which re-arms natively at the period boundary). An over-budget payment is declined before any money moves; nothing queues. Configured amounts from Haven are returned alongside.",
3603
+ selectionGuidance: "Use this when the user asks about allowance, budget, spend limit, remaining amount, remaining allowance, remaining budget, daily limit, reset period, what can I spend, or what the agent can still spend. For whether the account actually HOLDS funds behind the budget use haven_check_funds.",
3604
+ behavior: "Returns the per-token spend authority for the account: the active budget delegation (remaining = the period budget, which re-arms natively at the period boundary), each with id, onchain.remaining, remainingDisplay. An over-budget payment is declined before any money moves; nothing queues. Configured amounts from Haven are returned alongside.",
4230
3605
  nextActionGuidance: ""
4231
3606
  },
3607
+ // #3126 — the sufficiency signal, deliberately NOT a balance tool. The
3608
+ // constrained actor reads a boolean, never the treasury total.
3609
+ checkFunds: {
3610
+ summary: "Check whether the agent's account actually holds at least the given amount of a token \u2014 funds held, not spend permitted.",
3611
+ selectionGuidance: "Use this before attempting a payment when it matters whether the money is really there: allowance answers say what you are PERMITTED to spend, never whether the account HOLDS it. For allowance, budget, spend-limit, remaining-budget, reset-period, or what-can-I-spend questions use the allowance lookup tool instead.",
3612
+ behavior: "Returns covered: true (the account holds at least the checked amount), false (a live chain read reports less \u2014 the budget is backed by an empty account; stop and tell the user funds are missing), or null (the chain read failed \u2014 unverifiable, never treat it as absence; coverageError says why). The account balance itself is deliberately not returned: this is a sufficiency signal, not a balance read. budget_remaining_atomic is the permitted figure from the allowance lookup (the SDK spells it budgetRemainingAtomic), named so it can never be confused with holdings.",
3613
+ nextActionGuidance: "On covered=false, do not attempt the payment \u2014 tell the user the account is short and let them fund it; on covered=null, retry the check shortly or proceed knowing the payment may fail on-chain."
3614
+ },
4232
3615
  listReceipts: {
4233
- summary: "List recent machine-payment receipts and evidence for bookkeeping.",
4234
- selectionGuidance: "Use this for transaction history, receipts, payment evidence, or bookkeeping; use the allowance tool instead for remaining allowance, budget, spend-limit, or what-can-I-spend questions.",
4235
- behavior: "Returns the agent's recent machine-payment receipts ordered by recency. Proof header values are not returned.",
3616
+ summary: "List machine-payment receipts, newest first, by page.",
3617
+ selectionGuidance: "This agent's payment evidence, not the wallet's transaction history (sweeps, funding legs, other agents); use the allowance tool instead for remaining allowance or what-can-I-spend questions.",
3618
+ behavior: "Page { receipts, total, hasMore, nextCursor }; hasMore: pass nextCursor as cursor. parties.treasuryAccount is Haven's authoritative payer. protocolReceiptPayload is the merchant's PAYMENT-RESPONSE relayed verbatim: merchant-controlled, unverified, not Haven's record; payer may differ from payerAddress. Proof header values omitted.",
4236
3619
  nextActionGuidance: ""
4237
3620
  },
4238
3621
  verifyReceipt: {
@@ -4248,10 +3631,10 @@ var toolDescriptions = {
4248
3631
  nextActionGuidance: "On a decline, report the reason to the user and ask them to raise the budget in Haven \u2014 there is no approval queue to wait on. This tool retries the merchant itself while it runs, so do not wait on a signal mid-call. If the process crashes after payment, a later haven_get_payment_status call may report nextAction=retry_original_x402_request \u2014 resume via haven_resume_x402_payment instead of paying again."
4249
3632
  },
4250
3633
  discoverTools: {
4251
- summary: "Step 1 of a purchase: discover payable services from Haven's curated merchant catalog \u2014 names, prices, and which pay tool to use next.",
4252
- selectionGuidance: "Use this when the user asks what the agent can buy, pay for, or which paid services exist \u2014 or when you need a resource URL for a service the user described. Use verified=verified for entries Haven watched pass a live quote probe (operator-curated or self-submitted) \u2014 domain_verified is the only ownership claim; never treat these badges as proof of merchant honesty, quality, or reliability. Do NOT use for balance, budget, or spend-limit questions \u2014 use haven_get_allowances. Do NOT use to pay \u2014 each returned entry names the pay tool to use next.",
4253
- behavior: "Use each entry's suggested_tool field first \u2014 it names the exact next call. Read-only lookup against Haven's curated catalog; entries are periodically re-verified against the live merchant and degraded entries are flagged. Use category for a case-insensitive category filter (for example, VPN or vpn), or search for a product name, category, or description term. Returns name, description, price, rail, resource URL, tool_name, tool_arguments, suggested_tool, and the provenance badges source/domain_verified/verified_payable. The catalog price (price_display/price_atomic, marked price_is_indicative) is a last-verified hint, NOT authoritative \u2014 the real price comes from the merchant's live 402 at pay time. Never creates a payment, signature, or approval.",
4254
- nextActionGuidance: `Pick an entry and pay it with the tool named in suggested_tool, passing the entry's resource_url, tool_name, and tool_arguments for MCP merchants. Confirm the price from the live pay-tool result (not the catalog), and pass the user's cap as max_amount_human in whole tokens ("no more than 1 USDC" \u2192 max_amount_human: "1") \u2014 never convert it to atomic units by hand.`
3634
+ summary: "Step 1 of a purchase: discover payable services from Haven's curated merchant catalog \u2014 names, prices, the next call.",
3635
+ selectionGuidance: "Use this when the user asks what the agent can buy, pay for, or which paid services exist \u2014 or when you need a resource URL for a service the user described. Use verified=verified for entries Haven watched pass a live quote probe (operator-curated or self-submitted) \u2014 domain_verified is the only ownership claim; never treat these badges as proof of merchant honesty, quality, or reliability. Do NOT use for balance, budget, or spend-limit questions \u2014 use haven_get_allowances. Do NOT use to pay \u2014 each returned entry names the next tool to call.",
3636
+ behavior: "Use each entry's suggested_tool + suggested_arguments first; an entry without them says why. Read-only lookup against Haven's curated catalog; entries are periodically re-verified against the live merchant and degraded entries are flagged. Use category for a case-insensitive category filter (for example, VPN or vpn), or search for a product name, category, or description term. Returns name, description, price, rail, resource URL, tool_name, tool_arguments, the hint, and the provenance badges source/domain_verified/verified_payable. The catalog price (price_display/price_atomic, marked price_is_indicative) is a last-verified hint, NOT authoritative \u2014 the real price comes from the merchant's live 402 at pay time. Never creates a payment, signature, or approval.",
3637
+ nextActionGuidance: `Call suggested_tool with suggested_arguments VERBATIM (no hint: read suggested_tool_omitted_reason). Confirm the price from the live quote or pay result, not the catalog; if the next tool takes a cap, pass the user's cap as max_amount_human in whole tokens ("no more than 1 USDC" \u2192 max_amount_human: "1"), never atomic units by hand.`
4255
3638
  },
4256
3639
  submitCatalogEntry: {
4257
3640
  summary: "Submit a merchant's payable (x402/MCP) endpoint to Haven's Verified Payable Directory for verification and listing.",
@@ -4261,14 +3644,14 @@ var toolDescriptions = {
4261
3644
  },
4262
3645
  sweep_delegate: {
4263
3646
  summary: "Sweep stranded USDC and/or ETH from the delegate wallet back to the originating account.",
4264
- selectionGuidance: "Use this when the user instructs you to recover stranded funds on the delegate wallet, or when a payment status returns nextAction=sweep_stranded_funds. Do NOT use for normal payments \u2014 use haven_pay_x402. Do NOT use to read balances only \u2014 use haven_get_allowances.",
3647
+ selectionGuidance: "Use this when the user instructs you to recover stranded funds on the delegate wallet, or when a payment status returns nextAction=sweep_stranded_funds. Do NOT use for normal payments \u2014 use haven_pay_x402. Do NOT use to read balances only \u2014 use haven_get_allowances, or haven_check_funds for whether the account holds funds.",
4265
3648
  behavior: `Reads the delegate EOA's on-chain USDC and ETH balances. For each non-zero balance, signs and submits a transfer from the delegate EOA to the originating account (hardcoded destination). The delegate key signs locally \u2014 Haven never sees it and the backend never constructs signed transactions (CASP/MiCA Red Line #2). Returns tx hashes and recovered amounts. Returns an empty transfers list when nothing is stranded. Each transfer carries confirmation: "confirmed" (a receipt was seen \u2014 the funds are in the account) or "unconfirmed" (broadcast but not confirmed within 90 seconds \u2014 still in the mempool, may still land). The top-level unconfirmed flag is true when any transfer is unconfirmed.`,
4266
3649
  nextActionGuidance: 'If transfers is non-empty, confirm the amounts with the user. Report a transfer as recovered ONLY when its confirmation is "confirmed". For an "unconfirmed" transfer, tell the user it was submitted but not yet confirmed, give them its txHash and explorerUrl to check, and do not re-run the sweep immediately \u2014 a re-run after it lands will simply find nothing stranded.'
4267
3650
  },
4268
3651
  send: {
4269
3652
  summary: "Send ETH or USDC directly from the agent's Haven wallet to a recipient address.",
4270
- 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.",
4271
- behavior: "Sends the requested 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 and before the agent is asked to sign \u2014 it is never queued for a human to approve later. The agent's signing key signs the account's typed data; Haven never receives the key.",
3653
+ 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.",
4272
3655
  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."
4273
3656
  },
4274
3657
  reportSettlementEvidence: {
@@ -4646,8 +4029,10 @@ the \`mcp__haven-signer__\` namespace and keep the delegate key on this machine.
4646
4029
  That namespacing is Claude-family; other runtimes name the servers by their
4647
4030
  own config keys (Codex: \`haven\`, \`haven_signer\`). Tool results carry the
4648
4031
  exact next step (\`next_action\`, \`next_tool\`, \`next_arguments\`, plus the
4649
- runtime-neutral \`next_tool_server\` + \`next_tool_name\` \u2014 the bare tool name
4650
- on that logical server, whatever your runtime calls it).
4032
+ runtime-neutral \`next_tool_server\` + \`next_tool_name\` + \`next_tool_server_role\`
4033
+ \u2014 the bare tool name on that logical server, whatever your runtime calls it).
4034
+ When no tool follows, \`next_tool\` is absent and \`next_tool_omitted_reason\`
4035
+ says why; that is a complete answer.
4651
4036
  Follow those fields first; the prose below is fallback and orientation, not
4652
4037
  the source of truth.
4653
4038
 
@@ -4734,6 +4119,13 @@ spending:
4734
4119
  local signer; the signer is verified by calling any signer tool.
4735
4120
  - \`mcp__haven__haven_get_allowances\` \u2014 detailed per-token breakdown
4736
4121
  (configured, spent, reset window) when you need more than the summary.
4122
+ - \`mcp__haven__haven_check_funds\` \u2014 whether the account actually HOLDS at
4123
+ least a given amount of a token. Allowance answers above say what you are
4124
+ permitted to spend; this one says whether the money is really there,
4125
+ answered as \`covered\` true/false/null \u2014 never as a balance. On
4126
+ \`covered: false\`, stop and tell the user the account is short; on
4127
+ \`covered: null\` (the chain read failed), treat it as unverifiable rather
4128
+ than as absence.
4737
4129
 
4738
4130
  Budgets reset on a period the user chose. If a payment exceeds the remaining
4739
4131
  budget it is declined before any money moves \u2014 tell the user; they can raise
@@ -4862,8 +4254,10 @@ check on in-flight payments. Do not poll in a tight loop.
4862
4254
  ## Failure handling
4863
4255
 
4864
4256
  Haven tool failures are shaped like \`{ success: false, code, message, ... }\`
4865
- or older \`{ error, status, details? }\` responses. Branch on \`code\` when
4866
- present and surface \`message\` or \`error\` verbatim. Common cases:
4257
+ or older \`{ error, status, details? }\` responses. A failure carries the same
4258
+ \`next_action\` / \`next_tool\` / \`next_arguments\` / \`next_tool_omitted_reason\`
4259
+ fields a success does; follow them first, then branch on \`code\` and surface
4260
+ \`message\` or \`error\` verbatim. Common cases:
4867
4261
 
4868
4262
  - \`insufficient_funds\`: the Haven wallet doesn't hold enough of that token.
4869
4263
  Suggest the user add funds in the Haven dashboard.
@@ -4924,45 +4318,6 @@ for that credential.
4924
4318
  var SKILL_FOLDER_NAME = "haven-pay";
4925
4319
  var HAVEN_SKILL_BODY_MD = HAVEN_SKILL_MD.replace(/^---\n[\s\S]*?\n---\n+/, "");
4926
4320
 
4927
- // src/node-version.ts
4928
- var HAVEN_MINIMUM_NODE_VERSION = "22.0.0";
4929
- function parseNodeVersion(value) {
4930
- const match = value.trim().match(/^v?(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
4931
- if (!match) return [0, 0, 0];
4932
- return [Number(match[1] ?? 0), Number(match[2] ?? 0), Number(match[3] ?? 0)];
4933
- }
4934
- function compareNodeVersions(left, right) {
4935
- const leftParts = parseNodeVersion(left);
4936
- const rightParts = parseNodeVersion(right);
4937
- for (let i = 0; i < 3; i += 1) {
4938
- if (leftParts[i] !== rightParts[i]) return leftParts[i] > rightParts[i] ? 1 : -1;
4939
- }
4940
- return 0;
4941
- }
4942
- function isSupportedNodeVersion(nodeVersion = process.versions.node, minimumNodeVersion = HAVEN_MINIMUM_NODE_VERSION) {
4943
- return compareNodeVersions(nodeVersion, minimumNodeVersion) >= 0;
4944
- }
4945
- function unsupportedNodeVersionMessage(options) {
4946
- const nodeVersion = options.nodeVersion ?? process.versions.node;
4947
- const minimum = options.minimumNodeVersion ?? HAVEN_MINIMUM_NODE_VERSION;
4948
- const lines = [
4949
- `${options.subject} requires Node.js >=${minimum}, but this is Node.js ${nodeVersion}.`,
4950
- "",
4951
- "Upgrade Node, then try again:",
4952
- ` nvm install ${major(minimum)} && nvm use ${major(minimum)} (nvm)`,
4953
- ` fnm install ${major(minimum)} && fnm use ${major(minimum)} (fnm)`,
4954
- ` volta install node@${major(minimum)} (volta)`,
4955
- ` or download Node ${major(minimum)} from https://nodejs.org`,
4956
- "",
4957
- "If you use a version manager, check that the agent runtime launching Haven picks up the same version \u2014 upgrading your shell does not always change what a desktop app spawns."
4958
- ];
4959
- if (options.retryHint) lines.push("", options.retryHint);
4960
- return lines.join("\n");
4961
- }
4962
- function major(version) {
4963
- return String(parseNodeVersion(version)[0]);
4964
- }
4965
-
4966
4321
  // src/merchant-discovery.ts
4967
4322
  var MERCHANT_DISCOVERY_PATHS = ["/.well-known/haven-demo-merchant", "/"];
4968
4323
  var DISCOVERY_MAX_BYTES = 64 * 1024;
@@ -5007,6 +4362,6 @@ function sameUrl(a, b) {
5007
4362
  }
5008
4363
  }
5009
4364
 
5010
- 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_PAYMENT_FAILURE_CODE_VALUES, AGENT_PAYMENT_NEXT_ACTION_VALUES, AGENT_PAYMENT_PHASE_VALUES, AGENT_PAYMENT_RAIL_VALUES, AGENT_README_SECTION_MD, AGENT_SECRET_HYGIENE_SENTENCE, AGENT_WIRING_COLLISION_RELAY_SENTENCE, AgentPaymentFailureCode, AgentPaymentFailureCodeDescriptions, AgentPaymentFailureCodeSchema, AgentPaymentNextAction, AgentPaymentNextActionDescriptions, AgentPaymentNextActionSchema, AgentPaymentPhase, AgentPaymentPhaseDescriptions, AgentPaymentPhaseSchema, AgentPaymentRail, AgentPaymentRailDescriptions, AgentPaymentRailSchema, AgentPaymentWarningCode, CONNECTOR_PACKAGE_NAME, DEFAULT_CONFIRMATION_TIMEOUT_MS, DISCOVERY_MAX_BYTES, ERC7710_ASSET_TRANSFER_METHOD, HAVEN_AGENT_RUNBOOK_MD, HAVEN_CONNECTOR_CHANNEL, HAVEN_MINIMUM_NODE_VERSION, HAVEN_SKILL_BODY_MD, HAVEN_SKILL_MD, HavenApiError, HavenClient, HavenError, HavenPaymentStateError, HavenSigningError, HavenTimeoutError, HavenUnsupportedSignerVersionError, HavenZeroSettlementHashError, MERCHANT_DISCOVERY_PATHS, MerchantTimeoutError, RECEIPT_VERSION, SIGNER_UPDATE_FALLBACK, SKILL_FOLDER_NAME, SWEEP_BASE_CHAIN_ID, SWEEP_BASE_SEPOLIA_CHAIN_ID, SWEEP_BASE_SEPOLIA_USDC_ADDRESS, SWEEP_BASE_USDC_ADDRESS, SignerRefusalCode, TRANSFER_WITH_AUTHORIZATION_TYPES, X402AlreadySettledError, X402PaymentHeaderValidationError, X402UnexpectedStatusError, 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, addressFromKey, buildSweepAuthorizationMessage, buildSweepTypedData, buildX402ExpectedMessage, compareNodeVersions, composeDescription, connectorRerunCommand, connectorSpec, decodeBase64Json, decodeBase64Utf8, discoverMerchantMcpUrl, encodeBase64Json, encodeBase64Utf8, encodePaymentProof, havenTools, isConnectorChannel, isErc7710Option, isSupportedNodeVersion, isSweepableChain, isZeroSettlementTxHash, normalizePaymentRequired, parsePaymentRequired, parsePaymentRequiredResponse, readX402ReceiptPayer, resolveConnectorChannel, resolveTokenFromAddress, sameUrl, selectErc7710PaymentOption, selectPaymentOption, selectStandardPaymentOption, selectX402SettlementScheme, signHash, signUserOpTypedDataForDelegation, signerUpdateFallback, sweepUsdcAddress, sweepUsdcDomain, toStandardPaymentRequirements, toolDescriptions, unsupportedNodeVersionMessage, validateStandardX402PaymentHeader, verifyPaymentReceipt, verifySignature, x402AssetTransferMethod, x402AuthorizationAmount, x402FacilitatorAddresses, x402V2PaymentEnvelope };
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 };
5011
4366
  //# sourceMappingURL=index.js.map
5012
4367
  //# sourceMappingURL=index.js.map