@haven_ai/sdk 0.0.0-dev.202609031523.fd49e1a

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.cjs ADDED
@@ -0,0 +1,4726 @@
1
+ 'use strict';
2
+
3
+ var viem = require('viem');
4
+ var ethers = require('ethers');
5
+ var crypto = require('crypto');
6
+ var async_hooks = require('async_hooks');
7
+ var schemes = require('x402/schemes');
8
+ var accounts = require('viem/accounts');
9
+
10
+ // src/client.ts
11
+
12
+ // src/connector-channel.ts
13
+ var CONNECTOR_PACKAGE_NAME = "@haven_ai/connect";
14
+ var HAVEN_CONNECTOR_CHANNEL = "dev";
15
+ var CHANNEL_PATTERN = /^[a-z][a-z0-9-]{0,31}$/;
16
+ function isConnectorChannel(value) {
17
+ return CHANNEL_PATTERN.test(value);
18
+ }
19
+ function resolveConnectorChannel(raw, fallback = HAVEN_CONNECTOR_CHANNEL) {
20
+ const trimmed = (raw ?? "").trim();
21
+ if (trimmed === "") return fallback;
22
+ if (!isConnectorChannel(trimmed)) {
23
+ throw new Error(
24
+ `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.`
25
+ );
26
+ }
27
+ return trimmed;
28
+ }
29
+ function connectorSpec(channel = HAVEN_CONNECTOR_CHANNEL) {
30
+ return `${CONNECTOR_PACKAGE_NAME}@${channel}`;
31
+ }
32
+ function connectorRerunCommand(args, options) {
33
+ const channel = options?.channel ?? HAVEN_CONNECTOR_CHANNEL;
34
+ const flags = options?.npxFlags ? `${options.npxFlags} ` : "";
35
+ const command = `npx ${flags}${connectorSpec(channel)}`;
36
+ return args ? `${command} ${args}` : command;
37
+ }
38
+
39
+ // src/types.ts
40
+ var DEFAULT_CONFIRMATION_TIMEOUT_MS = 9e4;
41
+ var AgentPaymentPhase = {
42
+ /** The agent must sign and submit the prepared payment before Haven can relay it. */
43
+ AgentSignatureRequired: "agent_signature_required",
44
+ /** Haven has received the signed payment and the agent should poll for confirmation. */
45
+ PaymentSubmitted: "payment_submitted",
46
+ /** The direct payment is confirmed; the agent does not need to do more for this payment id. */
47
+ PaymentConfirmed: "payment_confirmed",
48
+ /**
49
+ * #2115: RETIRED wire value — no live rail produces it. It described the
50
+ * Safe rail's approval queue, which no longer exists. Kept so a stored value
51
+ * still typechecks; see `AgentPaymentPhaseDescriptions` below for the
52
+ * agent-visible wording, which this comment used to contradict.
53
+ */
54
+ UserApprovalRequired: "user_approval_required",
55
+ /** #2115: RETIRED wire value — no live rail produces it. Stop and tell the user. */
56
+ UserExecutionRequired: "user_execution_required",
57
+ /** #2115: RETIRED wire value — no live rail produces it. Stop and tell the user. */
58
+ WaitingForAdditionalApprovals: "waiting_for_additional_approvals",
59
+ /** The Haven funding leg was sent; the agent can continue the merchant/protocol leg. */
60
+ FundingSent: "funding_sent",
61
+ /** The payment was rejected and cannot proceed; the agent should stop and tell the user. */
62
+ Rejected: "rejected",
63
+ /** The payment expired before completion. */
64
+ Expired: "expired",
65
+ /** Haven could not complete the payment; the agent should stop and surface the failure. */
66
+ Failed: "failed",
67
+ /**
68
+ * Pre-flight check determined the delegate's existing balance plus the
69
+ * remaining on-chain budget cannot cover the requested amount, so no
70
+ * payment intent was created. The account must be funded or the agent's
71
+ * budget raised before retrying — #2115: the old wording contrasted this
72
+ * with `UserApprovalRequired` as if that were a live alternative, and named
73
+ * the retired rail's Safe and per-token allowance as the fix.
74
+ */
75
+ InsufficientFunds: "insufficient_funds",
76
+ /**
77
+ * Haven's funding leg (account → delegate, the #946 EIP-3009 bridge)
78
+ * confirmed on-chain, but the merchant rejected the x402 retry. The delegate
79
+ * wallet may hold stranded USDC that was never settled to the merchant. The
80
+ * agent should stop, tell the user, and wait for the sweep flow to reclaim
81
+ * the funds.
82
+ */
83
+ FundedButUnsettled: "funded_but_unsettled"
84
+ };
85
+ var AgentPaymentNextAction = {
86
+ /** Sign with the delegate key and submit the payment to Haven. */
87
+ SignAndSubmitPayment: "sign_and_submit_payment",
88
+ /** Poll getPaymentStatus later using this payment id. */
89
+ CheckStatusLater: "check_status_later",
90
+ /** No further agent action is required for this payment id. */
91
+ None: "none",
92
+ /**
93
+ * #2115: RETIRED wire value — no live rail produces it and nothing maps to
94
+ * it. Stop and tell the user rather than polling; no approval will arrive.
95
+ */
96
+ WaitForUserApproval: "wait_for_user_approval",
97
+ /** #2115: RETIRED wire value — no live rail produces it. Stop and tell the user rather than polling. */
98
+ WaitForUserToCompletePayment: "wait_for_user_to_complete_payment",
99
+ /** Resume this payment id and retry the original x402 request with the merchant payment header. */
100
+ RetryOriginalX402Request: "retry_original_x402_request",
101
+ /** Stop retrying this payment and tell the user what happened. */
102
+ StopAndTellUser: "stop_and_tell_user",
103
+ /** Ask again only if the user still wants the payment after expiry. */
104
+ RequestAgainIfUserStillWantsIt: "request_again_if_user_still_wants_it",
105
+ /** #1307: retry the SAME tool call, supplying the explicit context fields the server could not rehydrate. */
106
+ RetryWithExplicitContext: "retry_with_explicit_context",
107
+ /**
108
+ * The x402 funding/quote window expired. Re-quote the same logical merchant
109
+ * operation with the same idempotency key to stay double-charge-safe.
110
+ */
111
+ PaymentWindowExpired: "payment_window_expired",
112
+ /**
113
+ * Stop and tell the user that the originating Safe needs to be funded or
114
+ * the agent's per-token allowance needs to be raised before the payment
115
+ * can succeed. A user approval will not fix this state on its own.
116
+ */
117
+ FundSafeOrRaiseAllowance: "fund_safe_or_raise_allowance",
118
+ /**
119
+ * The delegate wallet may hold funds that were sent from the Safe but never
120
+ * settled to the merchant. The wallet owner should initiate a sweep to
121
+ * return those funds to the originating Safe.
122
+ */
123
+ SweepStrandedFunds: "sweep_stranded_funds"
124
+ };
125
+ var AgentPaymentFailureCode = {
126
+ /** A merchant-authoritative x402 price exceeds the caller's pre-funding max_amount cap. */
127
+ PriceExceedsMax: "PRICE_EXCEEDS_MAX",
128
+ /** The x402 funding/quote window expired before the signer or hosted settle step could finish. */
129
+ PaymentWindowExpired: "PAYMENT_WINDOW_EXPIRED",
130
+ /** The Haven funding leg succeeded, but the merchant rejected the paid retry. */
131
+ MerchantRejectedAfterFunding: "MERCHANT_REJECTED_AFTER_FUNDING",
132
+ /** #1300 review: funding is on-chain but the merchant never ANSWERED the
133
+ * paid retry within the timeout. NOT proof of rejection — the merchant
134
+ * holds a valid EIP-3009 authorization and may still settle late, so the
135
+ * guidance is verify-then-sweep, never blind sweep. */
136
+ MerchantUnresponsiveAfterFunding: "MERCHANT_UNRESPONSIVE_AFTER_FUNDING",
137
+ /**
138
+ * #1307: the caller omitted merchant_url/tool_name (asking Haven to
139
+ * rehydrate the stored MCP merchant-call context by payment_id), but no
140
+ * usable context was stored for this intent — either it was never an
141
+ * MCP-tool quote, or the stored context is incomplete. The fallback is
142
+ * mechanical: re-send merchant_url, tool_name, arguments, and
143
+ * mcp_transport explicitly (the version-skew path).
144
+ */
145
+ MerchantCallContextUnavailable: "MERCHANT_CALL_CONTEXT_UNAVAILABLE",
146
+ /**
147
+ * #1351: the caller supplied BOTH the atomic `max_amount` and the
148
+ * human-denominated `max_amount_human` cap for one purchase. Haven refuses
149
+ * to guess which the user meant — the two differ by a factor of 10^decimals,
150
+ * so picking wrong is exactly the silent-overspend this cap exists to
151
+ * prevent. Rejected before any merchant probe, funding intent, or signature.
152
+ */
153
+ AmbiguousMaxAmount: "AMBIGUOUS_MAX_AMOUNT",
154
+ /**
155
+ * #1351: a human-denominated cap was supplied, but it cannot be converted to
156
+ * atomic units against THIS quote — either the quote's asset has no known
157
+ * decimals on its network, or the cap carries more fraction digits than the
158
+ * asset can represent (truncating it would silently change the user's cap).
159
+ * The fallback is the exact atomic `max_amount`.
160
+ */
161
+ MaxAmountUnconvertible: "MAX_AMOUNT_UNCONVERTIBLE"
162
+ };
163
+ var AgentPaymentRail = {
164
+ /** Standard Haven payment from the user's Safe through an approved delegate allowance. */
165
+ Direct: "direct",
166
+ /** x402 HTTP 402 payment flow with a Haven funding leg and merchant retry leg. */
167
+ X402: "x402",
168
+ /** Machine Payment Protocol family — categorical value used as a resume-state discriminator. */
169
+ Mpp: "mpp",
170
+ /** Haven internal MPP demo rail. Not for production traffic. */
171
+ MppDemo: "mpp_demo",
172
+ /** Crypto-settled MPP rail. */
173
+ MppCrypto: "mpp_crypto",
174
+ /** Stripe-deposit-backed MPP rail. */
175
+ StripeDeposit: "stripe_deposit",
176
+ /** Stripe Payment Token MPP rail. */
177
+ Spt: "spt"
178
+ };
179
+ var AGENT_PAYMENT_PHASE_VALUES = Object.values(AgentPaymentPhase);
180
+ var AGENT_PAYMENT_NEXT_ACTION_VALUES = Object.values(AgentPaymentNextAction);
181
+ var AGENT_PAYMENT_FAILURE_CODE_VALUES = Object.values(AgentPaymentFailureCode);
182
+ var AGENT_PAYMENT_RAIL_VALUES = Object.values(AgentPaymentRail);
183
+ var AgentPaymentPhaseDescriptions = {
184
+ [AgentPaymentPhase.AgentSignatureRequired]: "The agent must sign and submit the prepared payment before Haven can relay it.",
185
+ [AgentPaymentPhase.PaymentSubmitted]: "Haven has received the signed payment and the agent should poll for confirmation.",
186
+ [AgentPaymentPhase.PaymentConfirmed]: "The direct payment is confirmed; the agent does not need to do more for this payment id.",
187
+ [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.",
188
+ [AgentPaymentPhase.UserExecutionRequired]: "Retired wire value: no live rail produces it. Stop and tell the user.",
189
+ [AgentPaymentPhase.WaitingForAdditionalApprovals]: "Retired wire value: no live rail produces it. Stop and tell the user.",
190
+ [AgentPaymentPhase.FundingSent]: "The Haven funding leg was sent; the agent can continue the merchant/protocol leg.",
191
+ [AgentPaymentPhase.Rejected]: "The payment was rejected and cannot proceed; the agent should stop and tell the user.",
192
+ [AgentPaymentPhase.Expired]: "The payment expired before completion.",
193
+ [AgentPaymentPhase.Failed]: "Haven could not complete the payment; the agent should stop and surface the failure.",
194
+ [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.",
195
+ [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."
196
+ };
197
+ var AgentPaymentNextActionDescriptions = {
198
+ [AgentPaymentNextAction.SignAndSubmitPayment]: "Sign with the delegate key and submit the payment to Haven.",
199
+ [AgentPaymentNextAction.CheckStatusLater]: "Poll getPaymentStatus later using this payment id.",
200
+ [AgentPaymentNextAction.None]: "No further agent action is required for this payment id.",
201
+ [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.",
202
+ [AgentPaymentNextAction.WaitForUserToCompletePayment]: "Retired wire value: no live rail produces it. Stop and tell the user rather than polling.",
203
+ [AgentPaymentNextAction.RetryOriginalX402Request]: "Resume this payment id and retry the original x402 request with the merchant payment header.",
204
+ [AgentPaymentNextAction.StopAndTellUser]: "Stop retrying this payment and tell the user what happened.",
205
+ [AgentPaymentNextAction.RequestAgainIfUserStillWantsIt]: "Ask again only if the user still wants the payment after expiry.",
206
+ [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.",
207
+ [AgentPaymentNextAction.FundSafeOrRaiseAllowance]: "Stop and tell the user that the account needs to be funded or the agent budget raised before the payment can succeed.",
208
+ [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.",
209
+ [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."
210
+ };
211
+ var AgentPaymentFailureCodeDescriptions = {
212
+ [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.",
213
+ [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.",
214
+ [AgentPaymentFailureCode.MerchantRejectedAfterFunding]: "The Haven funding leg succeeded, but the merchant rejected the paid retry. Stop retrying the merchant and reconcile stranded delegate funds with haven_sweep_delegate.",
215
+ [AgentPaymentFailureCode.MerchantUnresponsiveAfterFunding]: "The Haven funding leg succeeded, but the merchant did not answer the paid retry before the timeout. The merchant may still settle late \u2014 check haven_get_payment_status (and retry haven_complete_mcp_tool once) BEFORE sweeping; sweep only if no settlement appears.",
216
+ [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.",
217
+ [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.",
218
+ [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."
219
+ };
220
+ var AgentPaymentWarningCode = {
221
+ /** No max_amount cap was supplied — the live quoted price was accepted as-is. */
222
+ MissingMaxAmount: "MISSING_MAX_AMOUNT",
223
+ /** The signing window closes soon; sign promptly or re-quote with the same idempotency key. */
224
+ QuoteExpiresSoon: "QUOTE_EXPIRES_SOON",
225
+ /** The merchant URL was resolved via discovery — pass the RESOLVED url forward. */
226
+ MerchantUrlDiscovered: "MERCHANT_URL_DISCOVERED",
227
+ /**
228
+ * #1306: the catalog's last-verified price_atomic differs from the LIVE
229
+ * merchant quote for a guided catalog purchase. The catalog price is only
230
+ * ever indicative; the live quote in the same response is authoritative.
231
+ */
232
+ CatalogPriceDiffers: "CATALOG_PRICE_DIFFERS",
233
+ /**
234
+ * #1306: the rail-aware allowance/budget pre-check could not be read (RPC
235
+ * failure, etc). `sufficient` is reported as null rather than a fabricated
236
+ * true/false — the on-chain policy remains the actual gate either way.
237
+ */
238
+ AllowanceCheckUnavailable: "ALLOWANCE_CHECK_UNAVAILABLE",
239
+ /**
240
+ * #1319: the delegation-rail read itself SUCCEEDED, but the remaining
241
+ * figure it returned is the #1145 fallback (the full configured budget)
242
+ * rather than a live ERC20PeriodTransferEnforcer read — `sufficient` is a
243
+ * real true/false, just computed from an optimistic number. Distinct from
244
+ * {@link AgentPaymentWarningCode.AllowanceCheckUnavailable}, which fires
245
+ * when the read failed outright and `sufficient` degrades to null. The
246
+ * on-chain policy re-checks at redemption either way; this only says the
247
+ * guidance shown here may be optimistic.
248
+ */
249
+ AllowanceReadOptimistic: "ALLOWANCE_READ_OPTIMISTIC"
250
+ };
251
+ var AgentPaymentRailDescriptions = {
252
+ [AgentPaymentRail.Direct]: "Standard Haven payment from the user-controlled account, redeeming the agent's on-chain budget delegation.",
253
+ [AgentPaymentRail.X402]: "x402 HTTP 402 payment flow with a Haven funding leg and merchant retry leg.",
254
+ [AgentPaymentRail.Mpp]: "Categorical MPP rail value used as a resume-state discriminator. Response bodies carry a granular mpp_* value instead.",
255
+ [AgentPaymentRail.MppDemo]: "Haven internal MPP demo rail. Not for production traffic.",
256
+ [AgentPaymentRail.MppCrypto]: "Crypto-settled MPP rail.",
257
+ [AgentPaymentRail.StripeDeposit]: "Stripe-deposit-backed MPP rail.",
258
+ [AgentPaymentRail.Spt]: "Stripe Payment Token MPP rail."
259
+ };
260
+ var AgentPaymentPhaseSchema = {
261
+ type: "string",
262
+ enum: AGENT_PAYMENT_PHASE_VALUES,
263
+ description: "Stable Haven agent payment state phase.",
264
+ "x-enumDescriptions": AgentPaymentPhaseDescriptions
265
+ };
266
+ var AgentPaymentNextActionSchema = {
267
+ type: "string",
268
+ enum: AGENT_PAYMENT_NEXT_ACTION_VALUES,
269
+ description: "Stable next action an agent should take for a Haven payment state.",
270
+ "x-enumDescriptions": AgentPaymentNextActionDescriptions
271
+ };
272
+ var AgentPaymentFailureCodeSchema = {
273
+ type: "string",
274
+ enum: AGENT_PAYMENT_FAILURE_CODE_VALUES,
275
+ description: "Stable machine-readable failure codes for Haven agent payment recovery paths.",
276
+ "x-enumDescriptions": AgentPaymentFailureCodeDescriptions
277
+ };
278
+ var AgentPaymentRailSchema = {
279
+ type: "string",
280
+ enum: AGENT_PAYMENT_RAIL_VALUES,
281
+ description: "Stable rail identifier for Haven agent payment states.",
282
+ "x-enumDescriptions": AgentPaymentRailDescriptions
283
+ };
284
+ var HavenError = class extends Error {
285
+ constructor(message, code, statusCode, paymentId) {
286
+ super(message);
287
+ this.code = code;
288
+ this.statusCode = statusCode;
289
+ this.paymentId = paymentId;
290
+ this.name = "HavenError";
291
+ }
292
+ code;
293
+ statusCode;
294
+ paymentId;
295
+ };
296
+ var HavenApiError = class extends HavenError {
297
+ constructor(message, statusCode, body, paymentId) {
298
+ super(message, "API_ERROR", statusCode, paymentId);
299
+ this.body = body;
300
+ this.name = "HavenApiError";
301
+ }
302
+ body;
303
+ };
304
+ var MerchantTimeoutError = class extends HavenApiError {
305
+ merchantErrorCode = "merchant_timeout";
306
+ constructor(message) {
307
+ super(message, 504);
308
+ this.name = "MerchantTimeoutError";
309
+ }
310
+ };
311
+ var X402UnexpectedStatusError = class extends HavenApiError {
312
+ x402ErrorCode = "unexpected_non_402_status";
313
+ constructor(message, statusCode) {
314
+ super(message, statusCode);
315
+ this.name = "X402UnexpectedStatusError";
316
+ }
317
+ };
318
+ var X402AlreadySettledError = class extends HavenApiError {
319
+ constructor(message, receipt, basis) {
320
+ super(message, 409, void 0, receipt.paymentId);
321
+ this.receipt = receipt;
322
+ this.basis = basis;
323
+ this.name = "X402AlreadySettledError";
324
+ }
325
+ receipt;
326
+ basis;
327
+ x402ErrorCode = "already_settled";
328
+ };
329
+ var HavenPaymentStateError = class extends HavenApiError {
330
+ constructor(message, statusCode, state, body) {
331
+ super(message, statusCode, body, state.paymentId);
332
+ this.state = state;
333
+ this.name = "HavenPaymentStateError";
334
+ }
335
+ state;
336
+ resumeState;
337
+ get status() {
338
+ return this.state.status;
339
+ }
340
+ get phase() {
341
+ return this.state.phase;
342
+ }
343
+ get nextAction() {
344
+ return this.state.nextAction;
345
+ }
346
+ };
347
+ var HavenSigningError = class extends HavenError {
348
+ constructor(message) {
349
+ super(message, "SIGNING_ERROR");
350
+ this.name = "HavenSigningError";
351
+ }
352
+ };
353
+ var SignerRefusalCode = {
354
+ /** `SUPPORTED_X402_EXPECTED_VERSIONS` in `@haven_ai/signer` does not include the received version. */
355
+ UnsupportedExpectedContextVersion: "UNSUPPORTED_EXPECTED_CONTEXT_VERSION",
356
+ /** `SUPPORTED_SWEEP_BINDING_VERSIONS` in `@haven_ai/signer` does not include the received version. */
357
+ UnsupportedSweepBindingVersion: "UNSUPPORTED_SWEEP_BINDING_VERSION"
358
+ };
359
+ function signerUpdateFallback(channel = HAVEN_CONNECTOR_CHANNEL) {
360
+ 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.`;
361
+ }
362
+ var SIGNER_UPDATE_FALLBACK = signerUpdateFallback();
363
+ var HavenUnsupportedSignerVersionError = class extends HavenError {
364
+ constructor(message, code, supportedVersions, receivedVersion, fallback) {
365
+ super(message, code);
366
+ this.supportedVersions = supportedVersions;
367
+ this.receivedVersion = receivedVersion;
368
+ this.fallback = fallback;
369
+ this.name = "HavenUnsupportedSignerVersionError";
370
+ }
371
+ supportedVersions;
372
+ receivedVersion;
373
+ fallback;
374
+ };
375
+ var HavenTimeoutError = class extends HavenError {
376
+ constructor(paymentId) {
377
+ super(
378
+ `Timed out waiting for payment ${paymentId} to confirm`,
379
+ "TIMEOUT",
380
+ void 0,
381
+ paymentId
382
+ );
383
+ this.name = "HavenTimeoutError";
384
+ }
385
+ };
386
+
387
+ // src/signer.ts
388
+ function signHash(privateKey, hash) {
389
+ try {
390
+ const signingKey = new ethers.ethers.SigningKey(privateKey);
391
+ const sig = signingKey.sign(hash);
392
+ return sig.serialized;
393
+ } catch (err) {
394
+ throw new HavenSigningError(
395
+ `Failed to sign hash: ${err instanceof Error ? err.message : String(err)}`
396
+ );
397
+ }
398
+ }
399
+ async function signTypedDataVerbatim(privateKey, typedData, label) {
400
+ try {
401
+ const wallet = new ethers.ethers.Wallet(privateKey);
402
+ const types = { ...typedData.types };
403
+ delete types.EIP712Domain;
404
+ return await wallet.signTypedData(
405
+ typedData.domain,
406
+ types,
407
+ typedData.message
408
+ );
409
+ } catch (err) {
410
+ throw new HavenSigningError(
411
+ `Failed to sign ${label}: ${err instanceof Error ? err.message : String(err)}`
412
+ );
413
+ }
414
+ }
415
+ async function signUserOpTypedDataForDelegation(privateKey, typedData) {
416
+ return signTypedDataVerbatim(privateKey, typedData, "delegation UserOperation");
417
+ }
418
+ async function signSettlementDelegationTypedData(privateKey, typedData) {
419
+ return signTypedDataVerbatim(privateKey, typedData, "x402 settlement delegation");
420
+ }
421
+ function addressFromKey(privateKey) {
422
+ try {
423
+ return new ethers.ethers.Wallet(privateKey).address;
424
+ } catch (err) {
425
+ throw new HavenSigningError(
426
+ `Invalid private key: ${err instanceof Error ? err.message : String(err)}`
427
+ );
428
+ }
429
+ }
430
+ function verifySignature(hash, signature, expectedAddress) {
431
+ try {
432
+ const recovered = ethers.ethers.recoverAddress(hash, signature);
433
+ return recovered.toLowerCase() === expectedAddress.toLowerCase();
434
+ } catch {
435
+ return false;
436
+ }
437
+ }
438
+
439
+ // src/base64.ts
440
+ function normalizeBase64(value) {
441
+ const standard = value.replace(/-/g, "+").replace(/_/g, "/");
442
+ const remainder = standard.length % 4;
443
+ return remainder === 0 ? standard : standard + "=".repeat(4 - remainder);
444
+ }
445
+ function encodeBase64Utf8(value) {
446
+ if (typeof Buffer !== "undefined") {
447
+ return Buffer.from(value, "utf8").toString("base64");
448
+ }
449
+ const bytes = new TextEncoder().encode(value);
450
+ let binary = "";
451
+ for (let i = 0; i < bytes.length; i++) {
452
+ binary += String.fromCharCode(bytes[i]);
453
+ }
454
+ return btoa(binary);
455
+ }
456
+ function decodeBase64Utf8(value) {
457
+ const normalized = normalizeBase64(value);
458
+ if (typeof Buffer !== "undefined") {
459
+ return Buffer.from(normalized, "base64").toString("utf8");
460
+ }
461
+ const binary = atob(normalized);
462
+ const bytes = new Uint8Array(binary.length);
463
+ for (let i = 0; i < binary.length; i++) {
464
+ bytes[i] = binary.charCodeAt(i);
465
+ }
466
+ return new TextDecoder().decode(bytes);
467
+ }
468
+ function encodeBase64Json(value) {
469
+ return encodeBase64Utf8(JSON.stringify(value));
470
+ }
471
+ function decodeBase64Json(value, label) {
472
+ try {
473
+ return JSON.parse(decodeBase64Utf8(value));
474
+ } catch (err) {
475
+ if (label) throw new Error(`Failed to decode ${label}`);
476
+ throw err;
477
+ }
478
+ }
479
+
480
+ // src/sweep.ts
481
+ var SWEEP_BASE_CHAIN_ID = 8453;
482
+ var SWEEP_BASE_SEPOLIA_CHAIN_ID = 84532;
483
+ var SWEEP_BASE_USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
484
+ var SWEEP_BASE_SEPOLIA_USDC_ADDRESS = "0x036CbD53842c5426634e7929541eC2318f3dCF7e";
485
+ var USDC_EIP712_DOMAIN_BY_CHAIN = {
486
+ [SWEEP_BASE_CHAIN_ID]: {
487
+ name: "USD Coin",
488
+ version: "2",
489
+ chainId: SWEEP_BASE_CHAIN_ID,
490
+ verifyingContract: SWEEP_BASE_USDC_ADDRESS
491
+ },
492
+ [SWEEP_BASE_SEPOLIA_CHAIN_ID]: {
493
+ name: "USDC",
494
+ version: "2",
495
+ chainId: SWEEP_BASE_SEPOLIA_CHAIN_ID,
496
+ verifyingContract: SWEEP_BASE_SEPOLIA_USDC_ADDRESS
497
+ }
498
+ };
499
+ var USDC_ADDRESS_BY_CHAIN = {
500
+ [SWEEP_BASE_CHAIN_ID]: SWEEP_BASE_USDC_ADDRESS,
501
+ [SWEEP_BASE_SEPOLIA_CHAIN_ID]: SWEEP_BASE_SEPOLIA_USDC_ADDRESS
502
+ };
503
+ var SWEEPABLE_CHAIN_IDS = Object.keys(USDC_ADDRESS_BY_CHAIN).map(Number);
504
+ function isSweepableChain(chainId) {
505
+ return chainId in USDC_ADDRESS_BY_CHAIN;
506
+ }
507
+ var TRANSFER_WITH_AUTHORIZATION_TYPES = {
508
+ TransferWithAuthorization: [
509
+ { name: "from", type: "address" },
510
+ { name: "to", type: "address" },
511
+ { name: "value", type: "uint256" },
512
+ { name: "validAfter", type: "uint256" },
513
+ { name: "validBefore", type: "uint256" },
514
+ { name: "nonce", type: "bytes32" }
515
+ ]
516
+ };
517
+ function sweepUsdcAddress(chainId) {
518
+ const address = USDC_ADDRESS_BY_CHAIN[chainId];
519
+ if (!address) {
520
+ throw new HavenSigningError(
521
+ `Sweep is not supported on chain ${chainId}. Supported: ${SWEEPABLE_CHAIN_IDS.join(", ")}.`
522
+ );
523
+ }
524
+ return address;
525
+ }
526
+ function sweepUsdcDomain(chainId) {
527
+ const domain = USDC_EIP712_DOMAIN_BY_CHAIN[chainId];
528
+ if (!domain) {
529
+ throw new HavenSigningError(
530
+ `Sweep is not supported on chain ${chainId}. Supported: ${SWEEPABLE_CHAIN_IDS.join(", ")}.`
531
+ );
532
+ }
533
+ return domain;
534
+ }
535
+ function sameAddress(a, b) {
536
+ return a.toLowerCase() === b.toLowerCase();
537
+ }
538
+ function buildSweepTypedData(auth) {
539
+ const domain = sweepUsdcDomain(auth.chainId);
540
+ const expectedToken = sweepUsdcAddress(auth.chainId);
541
+ if (!sameAddress(auth.token, expectedToken)) {
542
+ throw new HavenSigningError(
543
+ `Sweep token ${auth.token} is not the canonical USDC contract for chain ${auth.chainId}.`
544
+ );
545
+ }
546
+ if (!/^0x[0-9a-fA-F]{64}$/.test(auth.nonce)) {
547
+ throw new HavenSigningError("Sweep nonce must be a 0x-prefixed 32-byte hex string.");
548
+ }
549
+ return {
550
+ domain,
551
+ types: TRANSFER_WITH_AUTHORIZATION_TYPES,
552
+ primaryType: "TransferWithAuthorization",
553
+ message: {
554
+ from: auth.from,
555
+ to: auth.to,
556
+ value: BigInt(auth.value),
557
+ validAfter: BigInt(auth.validAfter),
558
+ validBefore: BigInt(auth.validBefore),
559
+ nonce: auth.nonce
560
+ }
561
+ };
562
+ }
563
+ function buildSweepAuthorizationMessage(auth) {
564
+ return `Haven sweep authorization v1
565
+ ${stableStringify({
566
+ version: 1,
567
+ kind: "haven.sweep.authorization",
568
+ from: auth.from.toLowerCase(),
569
+ to: auth.to.toLowerCase(),
570
+ value: auth.value,
571
+ validAfter: auth.validAfter,
572
+ validBefore: auth.validBefore,
573
+ nonce: auth.nonce.toLowerCase(),
574
+ token: auth.token.toLowerCase(),
575
+ chainId: auth.chainId
576
+ })}`;
577
+ }
578
+ function stableStringify(value) {
579
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
580
+ if (Array.isArray(value)) return `[${value.map((item) => stableStringify(item)).join(",")}]`;
581
+ const object = value;
582
+ return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(object[key])}`).join(",")}}`;
583
+ }
584
+
585
+ // src/x402.ts
586
+ var BASE_USDC_ADDRESS = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913";
587
+ var BASE_SEPOLIA_USDC_ADDRESS = "0x036cbd53842c5426634e7929541ec2318f3dcf7e";
588
+ var STANDARD_X402_USDC_ADDRESSES = /* @__PURE__ */ new Set([BASE_USDC_ADDRESS, BASE_SEPOLIA_USDC_ADDRESS]);
589
+ var X402_IDEMPOTENCY_BUCKET_MS = 3e5;
590
+ var DECIMAL_ATOMIC_AMOUNT_RE = /^[0-9]+$/;
591
+ var X402_PAYMENT_HEADER_MAX_LENGTH = 65536;
592
+ var BASE64_RE = /^[A-Za-z0-9+/]*={0,2}$/;
593
+ var ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
594
+ var SIGNATURE_RE = /^0x[0-9a-fA-F]{130}$/;
595
+ var NONCE_RE = /^0x[0-9a-fA-F]{64}$/;
596
+ var X402PaymentHeaderValidationError = class extends Error {
597
+ constructor() {
598
+ super("Invalid X-PAYMENT header.");
599
+ this.name = "X402PaymentHeaderValidationError";
600
+ }
601
+ };
602
+ function isPositiveDecimalAtomicAmount(value) {
603
+ return DECIMAL_ATOMIC_AMOUNT_RE.test(value) && BigInt(value) > 0n;
604
+ }
605
+ function optionAuthorizationAmount(option) {
606
+ return option.maxAmountRequired ?? option.amount;
607
+ }
608
+ var X402_MAX_AUTHORIZATION_WINDOW_SECONDS = 600;
609
+ var X402_SETTLEMENT_FORWARD_MARGIN_SECONDS = 300;
610
+ function clampAuthorizationWindow(seconds) {
611
+ const requested = typeof seconds === "number" && Number.isFinite(seconds) ? seconds : 30;
612
+ return Math.min(Math.max(Math.floor(requested), 1), X402_MAX_AUTHORIZATION_WINDOW_SECONDS);
613
+ }
614
+ function normalizePaymentOption(value) {
615
+ const candidate = value;
616
+ if (!candidate || typeof candidate !== "object" || typeof candidate.scheme !== "string" || typeof candidate.network !== "string" || typeof candidate.asset !== "string" || typeof candidate.payTo !== "string") {
617
+ return null;
618
+ }
619
+ const amount = typeof candidate.amount === "string" ? candidate.amount : typeof candidate.maxAmountRequired === "string" ? candidate.maxAmountRequired : null;
620
+ if (!amount) return null;
621
+ if (!isPositiveDecimalAtomicAmount(amount)) return null;
622
+ if (candidate.maxAmountRequired !== void 0 && (typeof candidate.maxAmountRequired !== "string" || !isPositiveDecimalAtomicAmount(candidate.maxAmountRequired))) {
623
+ return null;
624
+ }
625
+ return {
626
+ scheme: candidate.scheme,
627
+ network: candidate.network,
628
+ amount,
629
+ maxAmountRequired: candidate.maxAmountRequired,
630
+ resource: candidate.resource,
631
+ description: candidate.description,
632
+ mimeType: candidate.mimeType,
633
+ asset: candidate.asset,
634
+ payTo: candidate.payTo,
635
+ maxTimeoutSeconds: clampAuthorizationWindow(candidate.maxTimeoutSeconds),
636
+ extra: candidate.extra
637
+ };
638
+ }
639
+ function normalizePaymentRequired(value) {
640
+ const candidate = value;
641
+ if (!candidate || typeof candidate !== "object" || typeof candidate.x402Version !== "number" || !Array.isArray(candidate.accepts)) {
642
+ return null;
643
+ }
644
+ const accepts = candidate.accepts.map((option) => normalizePaymentOption(option)).filter((option) => !!option && typeof option === "object");
645
+ if (accepts.length === 0) return null;
646
+ const first = accepts[0];
647
+ const resourceUrl = candidate.resource?.url ?? first.resource;
648
+ if (!resourceUrl) return null;
649
+ const resource = {
650
+ ...candidate.resource && typeof candidate.resource === "object" ? candidate.resource : {},
651
+ url: resourceUrl,
652
+ description: candidate.resource?.description ?? first.description,
653
+ mimeType: candidate.resource?.mimeType ?? first.mimeType
654
+ };
655
+ return {
656
+ x402Version: candidate.x402Version,
657
+ resource,
658
+ accepts,
659
+ error: candidate.error,
660
+ ...candidate.extensions && typeof candidate.extensions === "object" ? { extensions: candidate.extensions } : {}
661
+ };
662
+ }
663
+ var SUPPORTED_X402_NETWORKS = {
664
+ "eip155:100": "Gnosis Chain",
665
+ "eip155:8453": "Base",
666
+ "base": "Base",
667
+ "eip155:84532": "Base Sepolia",
668
+ "base-sepolia": "Base Sepolia"
669
+ };
670
+ var STANDARD_X402_NETWORKS = {
671
+ "eip155:8453": "base",
672
+ "base": "base",
673
+ "eip155:84532": "base-sepolia",
674
+ "base-sepolia": "base-sepolia"
675
+ };
676
+ var GNOSIS_TOKENS = {
677
+ "0x0000000000000000000000000000000000000000": { symbol: "xDAI", decimals: 18 },
678
+ "0xcb444e90d8198415266c6a2724b7900fb12fc56e": { symbol: "EURe", decimals: 18 },
679
+ "0x2a22f9c3b484c3629090feed35f17ff8f88f76f0": { symbol: "USDC.e", decimals: 6 }
680
+ };
681
+ var BASE_TOKENS = {
682
+ "0x0000000000000000000000000000000000000000": { symbol: "ETH", decimals: 18 },
683
+ "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": { symbol: "USDC", decimals: 6 }
684
+ };
685
+ var BASE_SEPOLIA_TOKENS = {
686
+ "0x0000000000000000000000000000000000000000": { symbol: "ETH", decimals: 18 },
687
+ "0x036cbd53842c5426634e7929541ec2318f3dcf7e": { symbol: "USDC", decimals: 6 }
688
+ };
689
+ var ALL_TOKENS = {
690
+ ...GNOSIS_TOKENS,
691
+ ...BASE_TOKENS,
692
+ ...BASE_SEPOLIA_TOKENS
693
+ };
694
+ var NETWORK_TOKENS = {
695
+ "eip155:100": GNOSIS_TOKENS,
696
+ "eip155:8453": BASE_TOKENS,
697
+ "base": BASE_TOKENS,
698
+ "eip155:84532": BASE_SEPOLIA_TOKENS,
699
+ "base-sepolia": BASE_SEPOLIA_TOKENS
700
+ };
701
+ var X402_PAYMENT_HEADER_NAME = "PAYMENT-SIGNATURE";
702
+ var X402_LEGACY_PAYMENT_HEADER_NAME = "X-PAYMENT";
703
+ var X402_PAYMENT_REQUIRED_HEADER_NAME = "PAYMENT-REQUIRED";
704
+ var X402_PAYMENT_RESPONSE_HEADER_NAME = "PAYMENT-RESPONSE";
705
+ var X402_PAYMENT_HEADER_NAMES_SENT = `${X402_PAYMENT_HEADER_NAME}, ${X402_LEGACY_PAYMENT_HEADER_NAME}`;
706
+ var X402_PAYMENT_HEADER_NAMES = [
707
+ X402_PAYMENT_HEADER_NAME,
708
+ X402_LEGACY_PAYMENT_HEADER_NAME
709
+ ];
710
+ function x402PaymentHeaderNamesFor(paymentHeader) {
711
+ const both = [X402_PAYMENT_HEADER_NAME, X402_LEGACY_PAYMENT_HEADER_NAME];
712
+ let decoded;
713
+ try {
714
+ decoded = decodeBase64Json(paymentHeader);
715
+ } catch {
716
+ return both;
717
+ }
718
+ const accepted = decoded?.accepted;
719
+ if (!accepted || typeof accepted !== "object" || Array.isArray(accepted)) return both;
720
+ return isErc7710Option(accepted) ? [X402_PAYMENT_HEADER_NAME] : both;
721
+ }
722
+ function x402PaymentHeaderNamesSent(paymentHeader) {
723
+ return x402PaymentHeaderNamesFor(paymentHeader).join(", ");
724
+ }
725
+ function x402V2PaymentEnvelope(paymentRequired, accepted, payload) {
726
+ const resource = paymentRequired.resource;
727
+ const extensions = paymentRequired.extensions;
728
+ return {
729
+ x402Version: paymentRequired.x402Version,
730
+ ...resource && typeof resource === "object" && !Array.isArray(resource) ? { resource } : {},
731
+ accepted,
732
+ payload,
733
+ ...extensions && typeof extensions === "object" && !Array.isArray(extensions) ? { extensions } : {}
734
+ };
735
+ }
736
+ function parsePaymentRequired(response) {
737
+ const v2Header = response.headers.get("PAYMENT-REQUIRED");
738
+ if (v2Header) {
739
+ const parsed = normalizePaymentRequired(
740
+ decodeBase64Json(v2Header, "PAYMENT-REQUIRED header")
741
+ );
742
+ if (parsed) return parsed;
743
+ }
744
+ const v1Header = response.headers.get("X-PAYMENT");
745
+ if (v1Header) {
746
+ const parsed = normalizePaymentRequired(
747
+ decodeBase64Json(v1Header, "X-PAYMENT header")
748
+ );
749
+ if (parsed) return parsed;
750
+ }
751
+ throw new Error(
752
+ "No x402 payment headers found in 402 response. Expected PAYMENT-REQUIRED (v2) or X-PAYMENT (v1) header."
753
+ );
754
+ }
755
+ async function parsePaymentRequiredResponse(response) {
756
+ try {
757
+ return parsePaymentRequired(response);
758
+ } catch (headerErr) {
759
+ try {
760
+ const body = await response.clone().json();
761
+ const parsed = normalizePaymentRequired(body);
762
+ if (parsed) return parsed;
763
+ } catch {
764
+ }
765
+ throw headerErr;
766
+ }
767
+ }
768
+ function selectPaymentOption(accepts) {
769
+ if (!accepts || accepts.length === 0) return null;
770
+ for (const opt of accepts) {
771
+ if (opt.network in SUPPORTED_X402_NETWORKS) {
772
+ const networkTokens = NETWORK_TOKENS[opt.network];
773
+ if (networkTokens?.[opt.asset.toLowerCase()] && isPositiveDecimalAtomicAmount(optionAuthorizationAmount(opt))) {
774
+ return opt;
775
+ }
776
+ }
777
+ }
778
+ for (const opt of accepts) {
779
+ if (opt.network in SUPPORTED_X402_NETWORKS && isPositiveDecimalAtomicAmount(optionAuthorizationAmount(opt))) {
780
+ return opt;
781
+ }
782
+ }
783
+ return null;
784
+ }
785
+ var ERC7710_ASSET_TRANSFER_METHOD = "erc7710";
786
+ function x402AssetTransferMethod(option) {
787
+ const raw = option.extra?.assetTransferMethod;
788
+ return typeof raw === "string" ? raw : null;
789
+ }
790
+ function isErc7710Option(option) {
791
+ return x402AssetTransferMethod(option) === ERC7710_ASSET_TRANSFER_METHOD;
792
+ }
793
+ function x402FacilitatorAddresses(option) {
794
+ const raw = option.extra?.facilitatorAddresses;
795
+ if (!Array.isArray(raw)) return null;
796
+ const addresses = raw.filter((a) => typeof a === "string" && ADDRESS_RE.test(a));
797
+ return addresses.length > 0 ? addresses : null;
798
+ }
799
+ function isPayableStandardOption(opt) {
800
+ return opt.scheme === "exact" && opt.network in STANDARD_X402_NETWORKS && STANDARD_X402_USDC_ADDRESSES.has(opt.asset.toLowerCase()) && isPositiveDecimalAtomicAmount(optionAuthorizationAmount(opt));
801
+ }
802
+ function selectStandardPaymentOption(accepts) {
803
+ if (!accepts || accepts.length === 0) return null;
804
+ for (const opt of accepts) {
805
+ if (opt === null || typeof opt !== "object") continue;
806
+ if (!isErc7710Option(opt) && isPayableStandardOption(opt)) return opt;
807
+ }
808
+ return null;
809
+ }
810
+ function selectErc7710PaymentOption(accepts) {
811
+ if (!accepts || accepts.length === 0) return null;
812
+ for (const opt of accepts) {
813
+ if (opt === null || typeof opt !== "object") continue;
814
+ if (isErc7710Option(opt) && isPayableStandardOption(opt)) return opt;
815
+ }
816
+ return null;
817
+ }
818
+ function selectX402SettlementScheme(accepts, opts) {
819
+ if (opts.delegationRail) {
820
+ const preferred = selectErc7710PaymentOption(accepts);
821
+ if (preferred) {
822
+ return {
823
+ scheme: "erc7710",
824
+ option: preferred,
825
+ facilitatorAddresses: x402FacilitatorAddresses(preferred)
826
+ };
827
+ }
828
+ }
829
+ const fallback = selectStandardPaymentOption(accepts);
830
+ if (fallback) return { scheme: "eip3009", option: fallback, facilitatorAddresses: null };
831
+ return null;
832
+ }
833
+ function x402AuthorizationAmount(option) {
834
+ const amount = optionAuthorizationAmount(option);
835
+ if (!isPositiveDecimalAtomicAmount(amount)) {
836
+ throw new Error("Invalid x402 amount: must be a positive decimal atomic amount");
837
+ }
838
+ return amount;
839
+ }
840
+ function buildX402ExpectedMessage(context) {
841
+ const version = context.payerDelegate ? 3 : context.typedDataHash ? 2 : 1;
842
+ const payload = {
843
+ version,
844
+ kind: "haven.x402.expected",
845
+ paymentId: context.paymentId,
846
+ payloadHash: context.payloadHash.toLowerCase(),
847
+ resourceUrl: context.resourceUrl,
848
+ merchantTo: context.merchantTo.toLowerCase(),
849
+ amount: context.amount,
850
+ asset: context.asset.toLowerCase(),
851
+ network: context.network
852
+ };
853
+ if (context.expiresAt) {
854
+ payload.expiresAt = context.expiresAt;
855
+ }
856
+ if (context.typedDataHash) {
857
+ payload.typedDataHash = context.typedDataHash.toLowerCase();
858
+ }
859
+ if (context.payerDelegate) {
860
+ payload.payerDelegate = context.payerDelegate.toLowerCase();
861
+ if (context.payerAgentId) {
862
+ payload.payerAgentId = context.payerAgentId;
863
+ }
864
+ }
865
+ return `Haven x402 expected context v${version}
866
+ ${stableStringify2(payload)}`;
867
+ }
868
+ function toStandardPaymentRequirements(paymentRequired, option) {
869
+ const network = STANDARD_X402_NETWORKS[option.network];
870
+ if (!network) {
871
+ throw new Error(`x402 exact payments are not supported on ${option.network}`);
872
+ }
873
+ if (option.scheme !== "exact") {
874
+ throw new Error(`Unsupported x402 scheme: ${option.scheme}`);
875
+ }
876
+ return {
877
+ scheme: "exact",
878
+ network,
879
+ maxAmountRequired: x402AuthorizationAmount(option),
880
+ resource: option.resource ?? paymentRequired.resource.url,
881
+ description: option.description ?? paymentRequired.resource.description ?? "Haven x402 payment",
882
+ mimeType: option.mimeType ?? paymentRequired.resource.mimeType ?? "application/octet-stream",
883
+ payTo: option.payTo,
884
+ asset: option.asset,
885
+ // Second enforcement point (#715): the parse path clamps too, but this is
886
+ // the last stop before the x402 library turns the timeout into
887
+ // `validBefore` — options constructed without parsing are bounded here.
888
+ // The forward margin (#1256) is added ONLY here, at signing: the parse
889
+ // path keeps recording the merchant's advertised timeout unchanged, and
890
+ // the library's `validBefore = now + this value` then carries enough
891
+ // slack to satisfy the facilitator's `validBefore ≥ now + maxTimeout`
892
+ // verify rule after our funding leg confirms.
893
+ maxTimeoutSeconds: clampAuthorizationWindow(option.maxTimeoutSeconds) + X402_SETTLEMENT_FORWARD_MARGIN_SECONDS,
894
+ extra: option.extra
895
+ };
896
+ }
897
+ async function validateStandardX402PaymentHeader(paymentHeader, context) {
898
+ try {
899
+ if (typeof paymentHeader !== "string" || paymentHeader.length === 0 || paymentHeader.length > X402_PAYMENT_HEADER_MAX_LENGTH || paymentHeader.length % 4 !== 0 || !BASE64_RE.test(paymentHeader)) {
900
+ throw new Error("wire");
901
+ }
902
+ const decoded = decodeBase64Json(paymentHeader);
903
+ if (!decoded || typeof decoded !== "object" || Array.isArray(decoded)) throw new Error("shape");
904
+ const version = decoded.x402Version;
905
+ const payload = decoded.payload;
906
+ if (version !== 1 && version !== 2 || !payload || typeof payload !== "object" || Array.isArray(payload)) {
907
+ throw new Error("shape");
908
+ }
909
+ if (version === 1) {
910
+ if (!hasOnlyKeys(decoded, ["x402Version", "scheme", "network", "payload"])) throw new Error("shape");
911
+ if (decoded.scheme !== "exact" || decoded.network !== standardX402WireNetwork(context.network)) {
912
+ throw new Error("context");
913
+ }
914
+ } else {
915
+ if (!hasOnlyKeys(decoded, ["x402Version", "accepted", "payload"], ["resource", "extensions"])) {
916
+ throw new Error("shape");
917
+ }
918
+ if ("resource" in decoded && (!decoded.resource || typeof decoded.resource !== "object" || Array.isArray(decoded.resource))) {
919
+ throw new Error("shape");
920
+ }
921
+ if ("extensions" in decoded && (!decoded.extensions || typeof decoded.extensions !== "object" || Array.isArray(decoded.extensions))) {
922
+ throw new Error("shape");
923
+ }
924
+ const accepted = selectStandardPaymentOption([decoded.accepted]);
925
+ if (!accepted || !matchesHeaderContext(accepted, context)) throw new Error("context");
926
+ }
927
+ const record = payload;
928
+ if (!hasOnlyKeys(record, ["signature", "authorization"])) throw new Error("shape");
929
+ if (typeof record.signature !== "string" || !SIGNATURE_RE.test(record.signature)) throw new Error("shape");
930
+ const authorization = record.authorization;
931
+ if (!authorization || typeof authorization !== "object" || Array.isArray(authorization)) throw new Error("shape");
932
+ const auth = authorization;
933
+ if (!hasOnlyKeys(auth, ["from", "to", "value", "validAfter", "validBefore", "nonce"])) throw new Error("shape");
934
+ 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)) {
935
+ throw new Error("shape");
936
+ }
937
+ if (!sameAddress2(auth.from, context.payer) || !sameAddress2(auth.to, context.merchantTo) || auth.value !== context.amountAtomic) {
938
+ throw new Error("context");
939
+ }
940
+ const validAfter = BigInt(auth.validAfter);
941
+ const validBefore = BigInt(auth.validBefore);
942
+ const now = BigInt(Math.floor(Date.now() / 1e3));
943
+ if (validBefore <= now || validAfter > validBefore) throw new Error("expired");
944
+ const typedData = buildSweepTypedData({
945
+ from: auth.from,
946
+ to: auth.to,
947
+ value: auth.value,
948
+ validAfter: auth.validAfter,
949
+ validBefore: auth.validBefore,
950
+ nonce: auth.nonce,
951
+ token: context.asset,
952
+ chainId: context.chainId
953
+ });
954
+ const recovered = await viem.recoverTypedDataAddress({
955
+ ...typedData,
956
+ // `buildSweepTypedData` keeps the public domain framework-neutral;
957
+ // viem brands contract addresses at this crypto call boundary only.
958
+ domain: {
959
+ ...typedData.domain,
960
+ verifyingContract: typedData.domain.verifyingContract
961
+ },
962
+ message: {
963
+ ...typedData.message,
964
+ from: typedData.message.from,
965
+ to: typedData.message.to,
966
+ nonce: typedData.message.nonce
967
+ },
968
+ signature: record.signature
969
+ });
970
+ if (!sameAddress2(recovered, context.payer)) throw new Error("recovery");
971
+ } catch {
972
+ throw new X402PaymentHeaderValidationError();
973
+ }
974
+ }
975
+ function hasOnlyKeys(value, required, optional = []) {
976
+ return Object.keys(value).every((key) => required.includes(key) || optional.includes(key)) && required.every((key) => key in value);
977
+ }
978
+ function sameAddress2(left, right) {
979
+ return left.toLowerCase() === right.toLowerCase();
980
+ }
981
+ function standardX402WireNetwork(network) {
982
+ return STANDARD_X402_NETWORKS[network] ?? null;
983
+ }
984
+ function matchesHeaderContext(option, context) {
985
+ if (option.scheme !== "exact" || !sameAddress2(option.payTo, context.merchantTo) || !sameAddress2(option.asset, context.asset) || option.network !== context.network || x402AuthorizationAmount(option) !== context.amountAtomic) return false;
986
+ return option.resource === void 0 || option.resource === context.resourceUrl;
987
+ }
988
+ function buildX402IdempotencyKey(paymentRequired, option, now = Date.now()) {
989
+ const bucket = Math.floor(now / X402_IDEMPOTENCY_BUCKET_MS);
990
+ const material = [
991
+ paymentRequired.resource.url,
992
+ paymentRequired.resource.description ?? "",
993
+ option.payTo.toLowerCase(),
994
+ option.asset.toLowerCase(),
995
+ x402AuthorizationAmount(option),
996
+ option.network,
997
+ bucket
998
+ ].join("|");
999
+ return `x402:${crypto.createHash("sha256").update(material).digest("hex").slice(0, 16)}`;
1000
+ }
1001
+ function encodePaymentProof(receipt) {
1002
+ const payload = {
1003
+ x402Version: 2,
1004
+ resource: receipt.resourceUrl ? { url: receipt.resourceUrl } : void 0,
1005
+ accepted: receipt.accepted,
1006
+ payload: {
1007
+ type: "haven_tx_hash",
1008
+ txHash: receipt.txHash,
1009
+ paymentId: receipt.paymentId,
1010
+ settledVia: "haven",
1011
+ payer: receipt.payer,
1012
+ chainId: receipt.chainId
1013
+ }
1014
+ };
1015
+ return encodeBase64Json(payload);
1016
+ }
1017
+ function resolveTokenFromAddress(address, network) {
1018
+ const lower = address.toLowerCase();
1019
+ if (network && network in NETWORK_TOKENS) {
1020
+ return NETWORK_TOKENS[network][lower] ?? null;
1021
+ }
1022
+ return ALL_TOKENS[lower] ?? null;
1023
+ }
1024
+ function stableStringify2(value) {
1025
+ if (value === null || typeof value !== "object") {
1026
+ const primitive = JSON.stringify(value);
1027
+ return primitive === void 0 ? "undefined" : primitive;
1028
+ }
1029
+ if (value instanceof Date) return JSON.stringify(value.toISOString());
1030
+ if (Array.isArray(value)) return `[${value.map((item) => stableStringify2(item)).join(",")}]`;
1031
+ const object = value;
1032
+ return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${stableStringify2(object[key])}`).join(",")}}`;
1033
+ }
1034
+ var DEFAULT_BASE_URL = "http://localhost:3001";
1035
+ var DEFAULT_REQUEST_TIMEOUT = 3e4;
1036
+ var HavenApiTransport = class {
1037
+ apiKey;
1038
+ baseUrl;
1039
+ requestTimeout;
1040
+ defaultHeaders;
1041
+ requestContext = new async_hooks.AsyncLocalStorage();
1042
+ constructor(config) {
1043
+ this.apiKey = config.apiKey;
1044
+ this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
1045
+ this.requestTimeout = config.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT;
1046
+ this.defaultHeaders = { ...config.defaultHeaders ?? {} };
1047
+ }
1048
+ /** Run `fn` with extra headers scoped to its asynchronous Haven API work. */
1049
+ withRequestContext(headers, fn) {
1050
+ return this.requestContext.run({ headers: { ...headers } }, fn);
1051
+ }
1052
+ async post(path, body) {
1053
+ return this.request("POST", path, body);
1054
+ }
1055
+ async get(path) {
1056
+ return this.request("GET", path);
1057
+ }
1058
+ async request(method, path, body) {
1059
+ const url = `${this.baseUrl}${path}`;
1060
+ const controller = new AbortController();
1061
+ const timeout = setTimeout(() => controller.abort(), this.requestTimeout);
1062
+ try {
1063
+ const contextHeaders = this.requestContext.getStore()?.headers ?? {};
1064
+ const res = await fetch(url, {
1065
+ method,
1066
+ headers: {
1067
+ "Content-Type": "application/json",
1068
+ "Authorization": `Bearer ${this.apiKey}`,
1069
+ ...this.defaultHeaders,
1070
+ ...contextHeaders
1071
+ },
1072
+ body: body ? JSON.stringify(body) : void 0,
1073
+ signal: controller.signal
1074
+ });
1075
+ const data = await res.json();
1076
+ if (!res.ok) {
1077
+ const record = data;
1078
+ const errorText = typeof record.error === "string" ? record.error : void 0;
1079
+ const rawDetails = record.details ?? record.detail;
1080
+ const detailsText = typeof rawDetails === "string" ? rawDetails : rawDetails != null ? JSON.stringify(rawDetails) : void 0;
1081
+ const message = errorText && detailsText ? `${errorText}: ${detailsText}` : errorText ?? detailsText ?? "API request failed";
1082
+ throw new HavenApiError(message, res.status, data);
1083
+ }
1084
+ return data;
1085
+ } catch (err) {
1086
+ if (err instanceof HavenApiError) throw err;
1087
+ if (err instanceof Error && err.name === "AbortError") {
1088
+ throw new HavenApiError(`Request to ${path} timed out`, 408);
1089
+ }
1090
+ throw new HavenApiError(
1091
+ `Request to ${path} failed: ${err instanceof Error ? err.message : String(err)}`,
1092
+ 0
1093
+ );
1094
+ } finally {
1095
+ clearTimeout(timeout);
1096
+ }
1097
+ }
1098
+ };
1099
+
1100
+ // src/payment-mappers.ts
1101
+ function mapPaymentResult(raw, buildExplorerUrl2) {
1102
+ return {
1103
+ paymentId: raw.payment_id,
1104
+ status: raw.status,
1105
+ token: raw.token,
1106
+ amount: raw.amount,
1107
+ to: raw.to,
1108
+ txHash: raw.tx_hash,
1109
+ errorMessage: raw.error_message,
1110
+ explorerUrl: raw.explorer_url ?? (raw.tx_hash ? buildExplorerUrl2(raw.chain_id, raw.tx_hash) : null),
1111
+ fee: raw.fee ? {
1112
+ amount: raw.fee.amount,
1113
+ token: raw.fee.token,
1114
+ basisPoints: raw.fee.basis_points,
1115
+ applied: raw.fee.applied
1116
+ } : null,
1117
+ createdAt: raw.created_at,
1118
+ signedAt: raw.signed_at,
1119
+ submittedAt: raw.submitted_at,
1120
+ confirmedAt: raw.confirmed_at,
1121
+ expiresAt: raw.expires_at
1122
+ };
1123
+ }
1124
+ function mapPaymentStatusResult(raw) {
1125
+ return {
1126
+ paymentId: raw.payment_id,
1127
+ kind: raw.kind,
1128
+ rail: raw.rail,
1129
+ status: raw.status,
1130
+ phase: raw.phase,
1131
+ nextAction: raw.next_action,
1132
+ amount: raw.amount,
1133
+ token: raw.token,
1134
+ resourceUrl: raw.resource_url,
1135
+ merchantAddress: raw.merchant_address,
1136
+ payerAddress: raw.payer_address ?? null,
1137
+ txHash: raw.tx_hash,
1138
+ expiresAt: raw.expires_at,
1139
+ chainId: raw.chain_id,
1140
+ message: raw.message,
1141
+ fee: raw.fee ? {
1142
+ amount: raw.fee.amount,
1143
+ token: raw.fee.token,
1144
+ basisPoints: raw.fee.basis_points,
1145
+ applied: raw.fee.applied
1146
+ } : null,
1147
+ amountAtomic: raw.amount_atomic ?? raw.x402?.amount_atomic ?? null,
1148
+ asset: raw.asset ?? raw.x402?.asset ?? null,
1149
+ network: raw.network ?? raw.x402?.network ?? null,
1150
+ description: raw.description ?? raw.x402?.description ?? null,
1151
+ idempotencyKey: raw.idempotency_key ?? raw.x402?.idempotency_key ?? null,
1152
+ x402: raw.x402 ? {
1153
+ amountAtomic: raw.x402.amount_atomic ?? raw.amount_atomic ?? null,
1154
+ asset: raw.x402.asset ?? raw.asset ?? null,
1155
+ network: raw.x402.network ?? raw.network ?? null,
1156
+ resourceUrl: raw.x402.resource_url ?? raw.resource_url,
1157
+ merchantAddress: raw.x402.merchant_address ?? raw.merchant_address,
1158
+ description: raw.x402.description ?? raw.description ?? null,
1159
+ idempotencyKey: raw.x402.idempotency_key ?? raw.idempotency_key ?? null
1160
+ } : void 0
1161
+ };
1162
+ }
1163
+ function mapPaymentReceipt(raw) {
1164
+ const receipt = {
1165
+ id: raw.id,
1166
+ paymentId: raw.payment_id,
1167
+ rail: raw.rail,
1168
+ proofStatus: raw.proof_status,
1169
+ txHash: raw.tx_hash,
1170
+ chainId: raw.chain_id,
1171
+ resourceUrl: raw.resource_url,
1172
+ merchantAddress: raw.merchant_address,
1173
+ payerAddress: raw.payer_address,
1174
+ settlementAddress: raw.settlement_address,
1175
+ tokenSymbol: raw.token_symbol,
1176
+ tokenAddress: raw.token_address,
1177
+ amountRaw: raw.amount_raw,
1178
+ amount: raw.amount_human,
1179
+ challengeId: raw.challenge_id,
1180
+ idempotencyKey: raw.idempotency_key,
1181
+ challengePayload: raw.challenge_payload,
1182
+ selectedPayment: raw.selected_payment,
1183
+ paymentProofHeaderName: raw.payment_proof_header_name,
1184
+ protocolReceiptHeaderName: raw.protocol_receipt_header_name,
1185
+ protocolReceiptPayload: raw.protocol_receipt_payload,
1186
+ merchantStatus: raw.merchant_status,
1187
+ confirmedAt: raw.confirmed_at,
1188
+ createdAt: raw.created_at,
1189
+ updatedAt: raw.updated_at
1190
+ };
1191
+ if ("payment_intent_id" in raw) {
1192
+ receipt.paymentIntentId = raw.payment_intent_id ?? null;
1193
+ }
1194
+ if ("approval_request_id" in raw) {
1195
+ receipt.approvalRequestId = raw.approval_request_id ?? null;
1196
+ }
1197
+ return receipt;
1198
+ }
1199
+
1200
+ // src/payment-state.ts
1201
+ var PAYMENT_STATE_STATUS_CODES = {
1202
+ pending: 202,
1203
+ pending_approval: 202,
1204
+ approved: 202,
1205
+ proposed: 202,
1206
+ executed: 200,
1207
+ pending_signature: 409,
1208
+ submitted: 409,
1209
+ expired: 410,
1210
+ failed: 502,
1211
+ rejected: 409
1212
+ };
1213
+ function paymentStateStatusCode(status, fallback = 502) {
1214
+ return PAYMENT_STATE_STATUS_CODES[status] ?? fallback;
1215
+ }
1216
+ function phaseForStatus(status) {
1217
+ if (status === "pending_signature") return AgentPaymentPhase.AgentSignatureRequired;
1218
+ if (status === "submitted") return AgentPaymentPhase.PaymentSubmitted;
1219
+ if (status === "confirmed") return AgentPaymentPhase.PaymentConfirmed;
1220
+ if (status === "pending" || status === "pending_approval") return AgentPaymentPhase.UserApprovalRequired;
1221
+ if (status === "approved") return AgentPaymentPhase.UserExecutionRequired;
1222
+ if (status === "proposed") return AgentPaymentPhase.WaitingForAdditionalApprovals;
1223
+ if (status === "executed") return AgentPaymentPhase.FundingSent;
1224
+ if (status === "rejected") return AgentPaymentPhase.Rejected;
1225
+ if (status === "expired") return AgentPaymentPhase.Expired;
1226
+ if (status === "failed") return AgentPaymentPhase.Failed;
1227
+ return null;
1228
+ }
1229
+ function nextActionForStatus(status) {
1230
+ if (status === "pending_signature") return AgentPaymentNextAction.SignAndSubmitPayment;
1231
+ if (status === "submitted") return AgentPaymentNextAction.CheckStatusLater;
1232
+ if (status === "confirmed") return AgentPaymentNextAction.None;
1233
+ if (status === "pending" || status === "pending_approval") return AgentPaymentNextAction.StopAndTellUser;
1234
+ if (status === "approved") return AgentPaymentNextAction.StopAndTellUser;
1235
+ if (status === "proposed") return AgentPaymentNextAction.StopAndTellUser;
1236
+ if (status === "executed") return AgentPaymentNextAction.StopAndTellUser;
1237
+ if (status === "rejected") return AgentPaymentNextAction.StopAndTellUser;
1238
+ if (status === "expired") return AgentPaymentNextAction.RequestAgainIfUserStillWantsIt;
1239
+ if (status === "failed") return AgentPaymentNextAction.StopAndTellUser;
1240
+ return null;
1241
+ }
1242
+ function messageForState(label, status, paymentId, nextAction) {
1243
+ if (status === "pending" || status === "pending_approval") {
1244
+ return `${label} is not payable: it is outside the agent's on-chain budget and no approval is pending (payment_id: ${paymentId}). Ask the user to grant or raise the budget in Haven.`;
1245
+ }
1246
+ if (status === "approved") {
1247
+ return `This payment carries a retired status ("approved") that no live Haven rail produces (payment_id: ${paymentId}). Nothing is waiting to be completed \u2014 tell the user to review this payment in Haven.`;
1248
+ }
1249
+ if (status === "executed") {
1250
+ return `This payment carries a retired status ("executed") that no live Haven rail produces (payment_id: ${paymentId}). Do not retry it \u2014 tell the user to review this payment in Haven.`;
1251
+ }
1252
+ if (status === "rejected") {
1253
+ return `The user rejected this payment request (payment_id: ${paymentId}).`;
1254
+ }
1255
+ if (status === "expired") {
1256
+ return `This payment request expired (payment_id: ${paymentId}).`;
1257
+ }
1258
+ return `${label} is ${status}; next_action=${nextAction} (payment_id: ${paymentId}).`;
1259
+ }
1260
+ function paymentStateFromRaw(label, raw) {
1261
+ if (!raw.payment_id || !raw.status) return null;
1262
+ const phase = raw.phase ?? phaseForStatus(raw.status);
1263
+ const nextAction = raw.next_action ?? nextActionForStatus(raw.status);
1264
+ if (!phase || !nextAction) return null;
1265
+ const amount = raw.amount ?? raw.requested ?? "";
1266
+ const token = raw.token ?? "";
1267
+ const message = raw.message ?? raw.error ?? messageForState(label, raw.status, raw.payment_id, nextAction);
1268
+ return {
1269
+ paymentId: raw.payment_id,
1270
+ kind: raw.kind === "payment_intent" ? "payment_intent" : "approval_request",
1271
+ rail: raw.rail ?? "direct",
1272
+ status: raw.status === "pending" ? "pending_approval" : raw.status,
1273
+ phase,
1274
+ nextAction,
1275
+ amount,
1276
+ token,
1277
+ resourceUrl: raw.resource_url ?? null,
1278
+ merchantAddress: raw.merchant_address ?? raw.merchant_to ?? null,
1279
+ txHash: raw.tx_hash ?? null,
1280
+ expiresAt: raw.expires_at ?? "",
1281
+ chainId: raw.chain_id ?? 0,
1282
+ message,
1283
+ amountAtomic: raw.amount_atomic ?? raw.x402?.amount_atomic ?? raw.mpp?.amount_atomic ?? null,
1284
+ asset: raw.asset ?? raw.x402?.asset ?? raw.mpp?.asset ?? null,
1285
+ network: raw.network ?? raw.x402?.network ?? raw.mpp?.network ?? null,
1286
+ description: raw.description ?? raw.x402?.description ?? raw.mpp?.description ?? null,
1287
+ idempotencyKey: raw.idempotency_key ?? raw.x402?.idempotency_key ?? raw.mpp?.idempotency_key ?? null,
1288
+ x402: raw.x402 ? {
1289
+ amountAtomic: raw.x402.amount_atomic ?? raw.amount_atomic ?? null,
1290
+ asset: raw.x402.asset ?? raw.asset ?? null,
1291
+ network: raw.x402.network ?? raw.network ?? null,
1292
+ resourceUrl: raw.x402.resource_url ?? raw.resource_url ?? null,
1293
+ merchantAddress: raw.x402.merchant_address ?? raw.merchant_address ?? raw.merchant_to ?? null,
1294
+ description: raw.x402.description ?? raw.description ?? null,
1295
+ idempotencyKey: raw.x402.idempotency_key ?? raw.idempotency_key ?? null
1296
+ } : void 0,
1297
+ mpp: raw.mpp ? {
1298
+ amountAtomic: raw.mpp.amount_atomic ?? raw.amount_atomic ?? null,
1299
+ asset: raw.mpp.asset ?? raw.asset ?? null,
1300
+ network: raw.mpp.network ?? raw.network ?? null,
1301
+ resourceUrl: raw.mpp.resource_url ?? raw.resource_url ?? null,
1302
+ merchantAddress: raw.mpp.merchant_address ?? raw.merchant_address ?? raw.merchant_to ?? null,
1303
+ description: raw.mpp.description ?? raw.description ?? null,
1304
+ idempotencyKey: raw.mpp.idempotency_key ?? raw.idempotency_key ?? null,
1305
+ challengeId: raw.mpp.challenge_id ?? raw.challenge_id ?? null
1306
+ } : void 0
1307
+ };
1308
+ }
1309
+ function throwPaymentStateError(label, raw) {
1310
+ const statusCode = paymentStateStatusCode(raw.status);
1311
+ const state = paymentStateFromRaw(label, raw);
1312
+ if (state) {
1313
+ throw new HavenPaymentStateError(state.message, statusCode, state, raw);
1314
+ }
1315
+ if (raw.status === "pending_approval") {
1316
+ throw new HavenApiError(
1317
+ `${label} exceeds the agent's on-chain budget and was declined; no approval is pending and none will arrive (payment_id: ${raw.payment_id}).`,
1318
+ statusCode,
1319
+ raw
1320
+ );
1321
+ }
1322
+ if (raw.status === "expired") {
1323
+ throw new HavenApiError(
1324
+ `${label} expired before it could be completed (payment_id: ${raw.payment_id}).`,
1325
+ statusCode,
1326
+ raw
1327
+ );
1328
+ }
1329
+ const paymentId = raw.payment_id ? ` (payment_id: ${raw.payment_id})` : "";
1330
+ const message = raw.error ?? `${label} ${raw.status}${paymentId}`;
1331
+ throw new HavenApiError(message, statusCode, raw);
1332
+ }
1333
+
1334
+ // src/mcp-merchant-transport.ts
1335
+ var DEFAULT_MERCHANT_TIMEOUT = 3e5;
1336
+ var MCP_NOTIFICATION_TIMEOUT = 1e4;
1337
+ var MCP_PROTOCOL_VERSION = "2025-06-18";
1338
+ var MCP_ACCEPT = "application/json, text/event-stream";
1339
+ var MCP_CLIENT_INFO = { name: "haven-sdk", version: "1" };
1340
+ var McpMerchantTransport = class {
1341
+ merchantTimeout;
1342
+ fetchImpl;
1343
+ requestId = 0;
1344
+ constructor(options = {}) {
1345
+ this.merchantTimeout = options.merchantTimeout ?? DEFAULT_MERCHANT_TIMEOUT;
1346
+ this.fetchImpl = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
1347
+ }
1348
+ /** Fetch a merchant with a settlement-sized timeout and caller cancellation. */
1349
+ async fetch(url, init = {}, timeoutMs = this.merchantTimeout) {
1350
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
1351
+ const signal = init.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal;
1352
+ try {
1353
+ return await this.fetchImpl(url, { ...init, signal });
1354
+ } catch (err) {
1355
+ if (timeoutSignal.aborted) {
1356
+ throw new MerchantTimeoutError(`Merchant request timed out after ${timeoutMs}ms: ${url}`);
1357
+ }
1358
+ throw err;
1359
+ }
1360
+ }
1361
+ /**
1362
+ * Establish an MCP Streamable-HTTP session. Any handshake failure degrades
1363
+ * to `undefined`, allowing the caller to fall back to plain x402.
1364
+ */
1365
+ async initialize(url, init, wallet) {
1366
+ try {
1367
+ const headers = new Headers(init?.headers);
1368
+ headers.set("Content-Type", "application/json");
1369
+ headers.set("Accept", MCP_ACCEPT);
1370
+ if (wallet && !headers.has("x402-wallet")) headers.set("x402-wallet", wallet);
1371
+ const response = await this.fetch(url, {
1372
+ method: "POST",
1373
+ headers,
1374
+ body: JSON.stringify({
1375
+ jsonrpc: "2.0",
1376
+ id: ++this.requestId,
1377
+ method: "initialize",
1378
+ params: {
1379
+ protocolVersion: MCP_PROTOCOL_VERSION,
1380
+ capabilities: {},
1381
+ clientInfo: MCP_CLIENT_INFO
1382
+ }
1383
+ })
1384
+ });
1385
+ if (!response.ok) return void 0;
1386
+ const sessionId = response.headers.get("mcp-session-id");
1387
+ if (!sessionId) return void 0;
1388
+ const message = await this.readMessage(response);
1389
+ if (message && "error" in message) return void 0;
1390
+ await this.notifyInitialized(url, init, sessionId, wallet);
1391
+ return sessionId;
1392
+ } catch {
1393
+ return void 0;
1394
+ }
1395
+ }
1396
+ /** Add the MCP session and response-content negotiation headers. */
1397
+ withSessionHeaders(init, sessionId) {
1398
+ const headers = new Headers(init?.headers);
1399
+ headers.set("mcp-session-id", sessionId);
1400
+ headers.set("Accept", MCP_ACCEPT);
1401
+ return { ...init, headers };
1402
+ }
1403
+ /** Read one JSON-RPC message from a JSON or SSE response without consuming it. */
1404
+ async readMessage(response) {
1405
+ let text;
1406
+ try {
1407
+ text = await response.clone().text();
1408
+ } catch {
1409
+ return void 0;
1410
+ }
1411
+ if ((response.headers.get("content-type") ?? "").includes("text/event-stream")) {
1412
+ return selectJsonRpcResult(parseSseJsonRpcMessages(text));
1413
+ }
1414
+ try {
1415
+ return JSON.parse(text);
1416
+ } catch {
1417
+ return void 0;
1418
+ }
1419
+ }
1420
+ /**
1421
+ * Collapse an MCP SSE response to the JSON-RPC result. Non-SSE and
1422
+ * unparseable responses pass through with their original body untouched.
1423
+ */
1424
+ async surfaceResult(response) {
1425
+ if (!(response.headers.get("content-type") ?? "").includes("text/event-stream")) {
1426
+ return response;
1427
+ }
1428
+ let text;
1429
+ try {
1430
+ text = await response.clone().text();
1431
+ } catch {
1432
+ return response;
1433
+ }
1434
+ const message = selectJsonRpcResult(parseSseJsonRpcMessages(text));
1435
+ if (!message) return response;
1436
+ const body = "result" in message ? message.result : message;
1437
+ const headers = new Headers(response.headers);
1438
+ headers.set("content-type", "application/json");
1439
+ headers.delete("content-length");
1440
+ headers.delete("mcp-session-id");
1441
+ return new Response(JSON.stringify(body), {
1442
+ status: response.status,
1443
+ statusText: response.statusText,
1444
+ headers
1445
+ });
1446
+ }
1447
+ /** Detect MCP transport from the URL or Coinbase Bazaar extension. */
1448
+ async detect(url, paymentRequired, response) {
1449
+ if (isMcpUrl(url)) return { handshakeRequired: true, source: "path" };
1450
+ if (paymentRequired.extensions?.bazaar != null) {
1451
+ return { handshakeRequired: true, source: "bazaar" };
1452
+ }
1453
+ if (await responseHasBazaarExtension(response)) {
1454
+ return { handshakeRequired: true, source: "bazaar" };
1455
+ }
1456
+ return void 0;
1457
+ }
1458
+ /** Identify the conventional Streamable-HTTP MCP path without probing it. */
1459
+ isMcpUrl(url) {
1460
+ return isMcpUrl(url);
1461
+ }
1462
+ /** Detect Bazaar metadata without consuming the merchant response. */
1463
+ hasBazaarExtension(response) {
1464
+ return responseHasBazaarExtension(response);
1465
+ }
1466
+ /**
1467
+ * Deliver an already-signed x402 header without changing the caller body.
1468
+ *
1469
+ * #2289: x402 v2 reads `PAYMENT-SIGNATURE`; v1 reads `X-PAYMENT`. Sending
1470
+ * only the legacy name meant a strict v2 merchant never saw the header —
1471
+ * indistinguishable, from the merchant's side, from sending no header at
1472
+ * all, while on the EIP-3009 bridge the funding leg had already moved the
1473
+ * money.
1474
+ *
1475
+ * #2341: WHICH names go on is per-payload, not always both — see
1476
+ * `x402PaymentHeaderNamesFor`. Both for EIP-3009; `PAYMENT-SIGNATURE` alone
1477
+ * for erc7710, whose header carries a whole delegation chain and answered
1478
+ * HTTP 431 when duplicated. The decision is made here rather than by the
1479
+ * caller so every path inherits it, and it is read from the payload rather
1480
+ * than passed in, because a flag a caller supplies is a flag a caller can
1481
+ * get wrong.
1482
+ *
1483
+ * Always `set`, never `append`, so a stale header on the caller's `init` is
1484
+ * replaced rather than added to — a merchant that reads the first of two
1485
+ * values would otherwise verify a superseded authorization. The name NOT
1486
+ * being sent is deleted for the same reason: on erc7710 a stale `X-PAYMENT`
1487
+ * left in place would be a superseded authorization we chose not to
1488
+ * overwrite, which is worse than the duplicate this change removes.
1489
+ */
1490
+ async deliverPayment(url, init, paymentHeader) {
1491
+ const headers = new Headers(init?.headers);
1492
+ const send = x402PaymentHeaderNamesFor(paymentHeader);
1493
+ for (const name of X402_PAYMENT_HEADER_NAMES) {
1494
+ if (send.includes(name)) headers.set(name, paymentHeader);
1495
+ else headers.delete(name);
1496
+ }
1497
+ return this.fetch(url, { ...init, headers });
1498
+ }
1499
+ async notifyInitialized(url, init, sessionId, wallet) {
1500
+ try {
1501
+ const headers = new Headers(init?.headers);
1502
+ headers.set("Content-Type", "application/json");
1503
+ headers.set("Accept", MCP_ACCEPT);
1504
+ headers.set("mcp-session-id", sessionId);
1505
+ if (wallet && !headers.has("x402-wallet")) headers.set("x402-wallet", wallet);
1506
+ await this.fetch(
1507
+ url,
1508
+ {
1509
+ method: "POST",
1510
+ headers,
1511
+ body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" })
1512
+ },
1513
+ MCP_NOTIFICATION_TIMEOUT
1514
+ );
1515
+ } catch {
1516
+ }
1517
+ }
1518
+ };
1519
+ async function captureMerchantResponse(response) {
1520
+ const merchant_body = await response.text().catch(() => "");
1521
+ return {
1522
+ merchant_status: response.status,
1523
+ merchant_status_text: response.statusText,
1524
+ merchant_headers: Object.fromEntries(response.headers.entries()),
1525
+ merchant_body
1526
+ };
1527
+ }
1528
+ function isMcpUrl(url) {
1529
+ try {
1530
+ return new URL(url).pathname.replace(/\/+$/, "").endsWith("/mcp");
1531
+ } catch {
1532
+ return /\/mcp(?:[/?#]|$)/.test(url);
1533
+ }
1534
+ }
1535
+ async function responseHasBazaarExtension(response) {
1536
+ try {
1537
+ const body = await response.clone().json();
1538
+ return body?.extensions?.bazaar != null;
1539
+ } catch {
1540
+ return false;
1541
+ }
1542
+ }
1543
+ function parseSseJsonRpcMessages(text) {
1544
+ const messages = [];
1545
+ let dataLines = [];
1546
+ const flush = () => {
1547
+ if (dataLines.length === 0) return;
1548
+ try {
1549
+ messages.push(JSON.parse(dataLines.join("\n")));
1550
+ } catch {
1551
+ }
1552
+ dataLines = [];
1553
+ };
1554
+ for (const line of text.split(/\r?\n/)) {
1555
+ if (line === "") {
1556
+ flush();
1557
+ continue;
1558
+ }
1559
+ if (line.startsWith("data:")) dataLines.push(line.slice(5).replace(/^ /, ""));
1560
+ }
1561
+ flush();
1562
+ return messages;
1563
+ }
1564
+ function selectJsonRpcResult(messages) {
1565
+ return messages.find((message) => "result" in message || "error" in message) ?? messages.at(-1);
1566
+ }
1567
+ var RECEIPT_VERSION = "haven-receipt-1";
1568
+ function defaultRecover(hash, signature) {
1569
+ return ethers.ethers.recoverAddress(hash, signature);
1570
+ }
1571
+ function verifyPaymentReceipt(receipt, recover = defaultRecover) {
1572
+ const { delegate, signHash: signHash2, signature } = receipt.authorization;
1573
+ if (!signature) return { verified: false, reason: "missing_signature" };
1574
+ let recovered;
1575
+ try {
1576
+ recovered = recover(signHash2, signature);
1577
+ } catch {
1578
+ return { verified: false, reason: "bad_signature" };
1579
+ }
1580
+ if (recovered.toLowerCase() !== delegate.toLowerCase()) {
1581
+ return { verified: false, reason: "signer_mismatch", recoveredSigner: recovered };
1582
+ }
1583
+ return { verified: true, recoveredSigner: recovered };
1584
+ }
1585
+
1586
+ // src/account-reads.ts
1587
+ function safeBigInt(value) {
1588
+ try {
1589
+ return BigInt(value);
1590
+ } catch {
1591
+ return 0n;
1592
+ }
1593
+ }
1594
+ function formatAtomicAmount(atomic, decimals) {
1595
+ if (atomic < 0n) return "0.0";
1596
+ const value = atomic.toString().padStart(decimals + 1, "0");
1597
+ const whole = value.slice(0, value.length - decimals) || "0";
1598
+ const fraction = value.slice(value.length - decimals).replace(/0+$/, "") || "0";
1599
+ return `${whole}.${fraction}`;
1600
+ }
1601
+ function deriveReadiness(status, allowances) {
1602
+ if (status !== "active") return "revoked";
1603
+ return allowances.some((allowance) => safeBigInt(allowance.remainingAtomic) > 0n) ? "ready" : "needs_approval";
1604
+ }
1605
+ var AccountReads = class {
1606
+ transport;
1607
+ getPaymentStatus;
1608
+ agentInFlight = null;
1609
+ constructor(options) {
1610
+ this.transport = options.transport;
1611
+ this.getPaymentStatus = options.getPaymentStatus;
1612
+ }
1613
+ async getAgent() {
1614
+ if (this.agentInFlight) return this.agentInFlight;
1615
+ const request = this.fetchAgent();
1616
+ this.agentInFlight = request;
1617
+ request.finally(() => {
1618
+ this.agentInFlight = null;
1619
+ }).catch(() => {
1620
+ });
1621
+ return request;
1622
+ }
1623
+ async getAgentSummary() {
1624
+ const [agent, allowanceSummary] = await Promise.all([this.getAgent(), this.getAllowances()]);
1625
+ const allowances = allowanceSummary.allowances.map((allowance) => {
1626
+ const token = resolveTokenFromAddress(allowance.tokenAddress);
1627
+ const remainingDisplay = token ? `${formatAtomicAmount(safeBigInt(allowance.onchain.remaining), token.decimals)} ${allowance.tokenSymbol}` : `${allowance.onchain.remaining} ${allowance.tokenSymbol} (atomic; unknown decimals)`;
1628
+ return {
1629
+ tokenSymbol: allowance.tokenSymbol,
1630
+ remainingAtomic: allowance.onchain.remaining,
1631
+ remainingDisplay,
1632
+ configuredAmount: allowance.configuredAmount,
1633
+ resetPeriodMin: allowance.resetPeriodMin,
1634
+ isResetPending: allowance.onchain.isResetPending
1635
+ };
1636
+ });
1637
+ const readiness = deriveReadiness(agent.status, allowances);
1638
+ return { ...agent, readiness, spend_authority_readiness: readiness, allowances };
1639
+ }
1640
+ async getAllowances() {
1641
+ const raw = await this.transport.get("/machine-payments/allowances");
1642
+ return {
1643
+ agentId: raw.agent_id,
1644
+ safeAddress: raw.safe_address,
1645
+ delegateAddress: raw.delegate_address,
1646
+ chainId: raw.chain_id,
1647
+ allowances: raw.allowances.map((allowance) => ({
1648
+ id: allowance.id,
1649
+ tokenAddress: allowance.token_address,
1650
+ tokenSymbol: allowance.token_symbol,
1651
+ configuredAmount: allowance.configured_amount,
1652
+ resetPeriodMin: allowance.reset_period_min,
1653
+ onchain: {
1654
+ amount: allowance.onchain.amount,
1655
+ spent: allowance.onchain.spent,
1656
+ remaining: allowance.onchain.remaining,
1657
+ effectiveSpent: allowance.onchain.effective_spent,
1658
+ resetTimeMin: allowance.onchain.reset_time_min,
1659
+ lastResetMin: allowance.onchain.last_reset_min,
1660
+ nonce: allowance.onchain.nonce,
1661
+ isResetPending: allowance.onchain.is_reset_pending,
1662
+ remainingIsFromChain: allowance.onchain.remaining_is_from_chain
1663
+ }
1664
+ }))
1665
+ };
1666
+ }
1667
+ async getPostPurchaseAllowanceSummary(paymentId) {
1668
+ const unavailable = (detail, payment2 = null) => ({
1669
+ payment: payment2,
1670
+ allowance: null,
1671
+ warnings: [{
1672
+ code: AgentPaymentWarningCode.AllowanceCheckUnavailable,
1673
+ message: `Could not read the post-purchase allowance/budget for payment ${paymentId} (${detail}). The payment itself succeeded \u2014 the on-chain policy remains the actual spend gate; this only affects the remaining-budget figure reported here.`
1674
+ }]
1675
+ });
1676
+ const [statusResult, agentResult, allowanceResult] = await Promise.allSettled([
1677
+ this.getPaymentStatus(paymentId),
1678
+ this.getAgent(),
1679
+ this.getAllowances()
1680
+ ]);
1681
+ if (statusResult.status === "rejected") {
1682
+ return unavailable(statusResult.reason instanceof Error ? statusResult.reason.message : String(statusResult.reason));
1683
+ }
1684
+ const payment = statusResult.value;
1685
+ if (agentResult.status === "rejected") {
1686
+ return unavailable(agentResult.reason instanceof Error ? agentResult.reason.message : String(agentResult.reason), payment);
1687
+ }
1688
+ if (allowanceResult.status === "rejected") {
1689
+ return unavailable(allowanceResult.reason instanceof Error ? allowanceResult.reason.message : String(allowanceResult.reason), payment);
1690
+ }
1691
+ try {
1692
+ const tokenAddress = payment.asset ?? payment.x402?.asset ?? null;
1693
+ if (!tokenAddress) return unavailable("the settled payment does not carry a resolvable token address", payment);
1694
+ const match = allowanceResult.value.allowances.find(
1695
+ (allowance) => allowance.tokenAddress.toLowerCase() === tokenAddress.toLowerCase()
1696
+ );
1697
+ if (!match) return unavailable("no allowance/budget row matches the settled token", payment);
1698
+ const token = resolveTokenFromAddress(match.tokenAddress);
1699
+ const remainingDisplay = token ? `${formatAtomicAmount(safeBigInt(match.onchain.remaining), token.decimals)} ${match.tokenSymbol}` : void 0;
1700
+ const rail = agentResult.value.executionRail;
1701
+ return {
1702
+ payment,
1703
+ allowance: {
1704
+ rail,
1705
+ remaining_atomic: match.onchain.remaining,
1706
+ ...remainingDisplay ? { remaining_display: remainingDisplay } : {},
1707
+ token_symbol: match.tokenSymbol,
1708
+ token_address: match.tokenAddress,
1709
+ reset_period: match.resetPeriodMin,
1710
+ source: rail === "delegation" ? "active_delegations" : "allowance_module"
1711
+ },
1712
+ warnings: []
1713
+ };
1714
+ } catch (error) {
1715
+ return unavailable(error instanceof Error ? error.message : String(error));
1716
+ }
1717
+ }
1718
+ async listReceipts(options = {}) {
1719
+ const query = options.limit ? `?limit=${encodeURIComponent(String(options.limit))}` : "";
1720
+ const raw = await this.transport.get(`/machine-payments/receipts${query}`);
1721
+ return raw.receipts.map(mapPaymentReceipt);
1722
+ }
1723
+ async getReceipt(paymentId) {
1724
+ const { receipt } = await this.transport.get(`/payments/${paymentId}/receipt`);
1725
+ return { receipt, verification: verifyPaymentReceipt(receipt) };
1726
+ }
1727
+ async fetchAgent() {
1728
+ const raw = await this.transport.get("/machine-payments/agent");
1729
+ return {
1730
+ id: raw.id,
1731
+ name: raw.name,
1732
+ status: raw.status,
1733
+ safeAddress: raw.safe_address,
1734
+ delegateAddress: raw.delegate_address,
1735
+ chainId: raw.chain_id,
1736
+ executionRail: raw.execution_rail === "delegation" ? "delegation" : "legacy"
1737
+ };
1738
+ }
1739
+ };
1740
+ function createJsonRpcProvider(url) {
1741
+ return new ethers.ethers.JsonRpcProvider(url);
1742
+ }
1743
+ function createWallet(privateKey, provider) {
1744
+ return new ethers.ethers.Wallet(privateKey, provider);
1745
+ }
1746
+ function createErc20Contract(address, abi, runner) {
1747
+ return new ethers.ethers.Contract(address, abi, runner);
1748
+ }
1749
+
1750
+ // src/delegate-sweep.ts
1751
+ function isWaitTimeout(err) {
1752
+ return err?.code === "TIMEOUT";
1753
+ }
1754
+ async function waitForSweepTx(tx) {
1755
+ let receipt;
1756
+ try {
1757
+ receipt = await tx.wait(1, DEFAULT_CONFIRMATION_TIMEOUT_MS);
1758
+ } catch (err) {
1759
+ if (!isWaitTimeout(err)) throw err;
1760
+ return { txHash: tx.hash, confirmation: "unconfirmed" };
1761
+ }
1762
+ if (!receipt) return { txHash: tx.hash, confirmation: "unconfirmed" };
1763
+ return { txHash: receipt.hash, confirmation: "confirmed" };
1764
+ }
1765
+ var DelegateSweepApi = class {
1766
+ constructor(options) {
1767
+ this.options = options;
1768
+ }
1769
+ options;
1770
+ async sweepDelegate() {
1771
+ if (!this.options.delegateKey) throw new HavenSigningError("delegateKey is required for sweepDelegate.");
1772
+ const agent = await this.options.getAgent();
1773
+ if (!agent.delegateAddress) throw new HavenApiError("Agent has no delegate address.", 422);
1774
+ const rpcUrl = this.options.chainRpcs[agent.chainId];
1775
+ if (!rpcUrl) throw new HavenApiError(`chainRpcs[${agent.chainId}] must be configured to sweep the delegate wallet.`, 422);
1776
+ const provider = createJsonRpcProvider(rpcUrl);
1777
+ const wallet = createWallet(this.options.delegateKey, provider);
1778
+ const transfers = [];
1779
+ if (isSweepableChain(agent.chainId)) {
1780
+ const contract = createErc20Contract(sweepUsdcAddress(agent.chainId), ["function balanceOf(address) view returns (uint256)", "function transfer(address to, uint256 amount) returns (bool)"], wallet);
1781
+ const balance2 = await contract.balanceOf(agent.delegateAddress);
1782
+ if (balance2 > 0n) {
1783
+ const tx = await contract.transfer(agent.safeAddress, balance2);
1784
+ const { txHash, confirmation } = await waitForSweepTx(tx);
1785
+ transfers.push({ asset: "USDC", amount: format(balance2, 6), amountAtomic: balance2.toString(), txHash, explorerUrl: this.options.buildExplorerUrl(agent.chainId, txHash), confirmation });
1786
+ }
1787
+ }
1788
+ const balance = await provider.getBalance(agent.delegateAddress);
1789
+ if (balance > 0n) {
1790
+ const fee = await provider.getFeeData();
1791
+ const send = balance - (fee.maxFeePerGas ?? fee.gasPrice ?? 1000000n) * 21000n * 2n;
1792
+ if (send > 0n) {
1793
+ const tx = await wallet.sendTransaction({ to: agent.safeAddress, value: send });
1794
+ const { txHash, confirmation } = await waitForSweepTx(tx);
1795
+ transfers.push({ asset: "ETH", amount: format(send, 18), amountAtomic: send.toString(), txHash, explorerUrl: this.options.buildExplorerUrl(agent.chainId, txHash), confirmation });
1796
+ }
1797
+ }
1798
+ return { fromAddress: agent.delegateAddress, toAddress: agent.safeAddress, chainId: agent.chainId, transfers, unconfirmed: transfers.some((t) => t.confirmation === "unconfirmed") };
1799
+ }
1800
+ prepareSweep() {
1801
+ return this.options.transport.post("/machine-payments/sweep/prepare", {});
1802
+ }
1803
+ submitSweep(authorization, signature) {
1804
+ return this.options.transport.post("/machine-payments/sweep/submit", { authorization, signature });
1805
+ }
1806
+ };
1807
+ function format(value, decimals) {
1808
+ const raw = value.toString().padStart(decimals + 1, "0");
1809
+ return `${raw.slice(0, -decimals) || "0"}.${raw.slice(-decimals).replace(/0+$/, "") || "0"}`;
1810
+ }
1811
+
1812
+ // src/x402-protocol.ts
1813
+ var CHAIN_EXPLORER_TX = {
1814
+ 100: "https://gnosisscan.io/tx",
1815
+ 8453: "https://basescan.org/tx"
1816
+ };
1817
+ function buildExplorerUrl(chainId, txHash) {
1818
+ const base = CHAIN_EXPLORER_TX[chainId ?? 8453] ?? CHAIN_EXPLORER_TX[8453];
1819
+ return `${base}/${txHash}`;
1820
+ }
1821
+ function explorerUrlOrEmpty(chainId, txHash) {
1822
+ return txHash ? buildExplorerUrl(chainId, txHash) : "";
1823
+ }
1824
+ function chainIdFromNetwork(network) {
1825
+ if (network === "base") return 8453;
1826
+ if (network === "base-sepolia") return 84532;
1827
+ if (!network?.startsWith("eip155:")) return void 0;
1828
+ const chainId = Number(network.slice("eip155:".length));
1829
+ return Number.isFinite(chainId) ? chainId : void 0;
1830
+ }
1831
+ function chainIdOrNull(network) {
1832
+ return chainIdFromNetwork(network) ?? null;
1833
+ }
1834
+ function sameAddress3(a, b) {
1835
+ return Boolean(a && b && a.toLowerCase() === b.toLowerCase());
1836
+ }
1837
+ function decimalFromUsdcAtomic(value) {
1838
+ const amount = BigInt(value);
1839
+ const whole = amount / 1000000n;
1840
+ const fraction = (amount % 1000000n).toString().padStart(6, "0").replace(/0+$/, "");
1841
+ return fraction ? `${whole}.${fraction}` : whole.toString();
1842
+ }
1843
+ function normalizeDecimal(value) {
1844
+ if (!value.includes(".")) return value.replace(/^0+(?=\d)/, "") || "0";
1845
+ const [whole, fraction = ""] = value.split(".");
1846
+ const normalizedWhole = whole.replace(/^0+(?=\d)/, "") || "0";
1847
+ const normalizedFraction = fraction.replace(/0+$/, "");
1848
+ return normalizedFraction ? `${normalizedWhole}.${normalizedFraction}` : normalizedWhole;
1849
+ }
1850
+ function x402PayerAddress(delegateAddress, x402Wallet) {
1851
+ return delegateAddress ?? x402Wallet;
1852
+ }
1853
+ function withX402Wallet(init, wallet) {
1854
+ if (!wallet) return init;
1855
+ const headers = new Headers(init?.headers);
1856
+ if (!headers.has("x402-wallet")) {
1857
+ headers.set("x402-wallet", wallet);
1858
+ }
1859
+ return {
1860
+ ...init,
1861
+ headers
1862
+ };
1863
+ }
1864
+ function snapshotRequestBody(body) {
1865
+ if (body == null) return void 0;
1866
+ if (typeof body === "string") return body;
1867
+ if (body instanceof URLSearchParams) return body.toString();
1868
+ throw new HavenApiError(
1869
+ "Quote helpers can only capture resumable request bodies that are strings or URLSearchParams. For streams, blobs, or binary bodies, preserve the original request yourself and call the matching resume method with fresh init.",
1870
+ 400
1871
+ );
1872
+ }
1873
+ function snapshotX402Request(url, init) {
1874
+ return {
1875
+ url,
1876
+ method: init?.method ?? "GET",
1877
+ headers: Array.from(new Headers(init?.headers).entries()),
1878
+ body: snapshotRequestBody(init?.body)
1879
+ };
1880
+ }
1881
+ function requestInitFromSnapshot(request) {
1882
+ return {
1883
+ method: request.method,
1884
+ headers: request.headers,
1885
+ body: request.body
1886
+ };
1887
+ }
1888
+ function noCompatiblePaymentOptionError(accepts) {
1889
+ const erc7710Only = selectErc7710PaymentOption(accepts) !== null;
1890
+ return new HavenApiError(
1891
+ "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)." : ""),
1892
+ 400
1893
+ );
1894
+ }
1895
+ function buildX402Quote(paymentRequired, request, idempotencyKey, mcpTransport) {
1896
+ const standard = selectStandardPaymentOption(paymentRequired.accepts);
1897
+ const option = standard ?? selectErc7710PaymentOption(paymentRequired.accepts);
1898
+ if (!option) {
1899
+ throw noCompatiblePaymentOptionError(paymentRequired.accepts);
1900
+ }
1901
+ const token = resolveTokenFromAddress(option.asset, option.network);
1902
+ return {
1903
+ rail: "x402",
1904
+ idempotencyKey: idempotencyKey ?? buildX402IdempotencyKey(paymentRequired, option),
1905
+ paymentRequired,
1906
+ accepted: option,
1907
+ acceptedScheme: standard ? "standard" : "erc7710",
1908
+ request,
1909
+ ...mcpTransport ? { mcpTransport } : {},
1910
+ resourceUrl: paymentRequired.resource.url,
1911
+ description: paymentRequired.resource.description ?? option.description ?? null,
1912
+ mimeType: paymentRequired.resource.mimeType ?? option.mimeType ?? null,
1913
+ amountAtomic: x402AuthorizationAmount(option),
1914
+ amount: decimalFromUsdcAtomic(x402AuthorizationAmount(option)),
1915
+ token: token?.symbol ?? "USDC",
1916
+ // #1351: null when the asset is unrecognised on this network — the
1917
+ // `token` fallback above is a LABEL, not evidence of 6 decimals, and a
1918
+ // human-denominated cap must fail closed rather than convert against a
1919
+ // guess. Same resolution as `token`, so the two never disagree.
1920
+ decimals: token?.decimals ?? null,
1921
+ asset: option.asset,
1922
+ network: option.network,
1923
+ chainId: chainIdOrNull(option.network),
1924
+ merchantAddress: option.payTo,
1925
+ maxTimeoutSeconds: option.maxTimeoutSeconds
1926
+ };
1927
+ }
1928
+ function buildX402Receipt(input) {
1929
+ const fundingExplorerUrl = input.explorerUrl || explorerUrlOrEmpty(input.chainId, input.txHash);
1930
+ return {
1931
+ success: true,
1932
+ paymentId: input.paymentId,
1933
+ txHash: input.txHash,
1934
+ token: input.token,
1935
+ amount: input.amount,
1936
+ to: input.to,
1937
+ resourceUrl: input.resourceUrl,
1938
+ explorerUrl: input.explorerUrl,
1939
+ accepted: input.accepted,
1940
+ paymentHeader: input.paymentHeader,
1941
+ merchantTo: input.merchantTo ?? input.accepted.payTo,
1942
+ payer: input.payer,
1943
+ chainId: input.chainId,
1944
+ haven: {
1945
+ paymentId: input.paymentId,
1946
+ fundingTxHash: input.txHash,
1947
+ fundingExplorerUrl
1948
+ },
1949
+ merchant: {
1950
+ payTo: input.merchantTo ?? input.accepted.payTo
1951
+ },
1952
+ x402: {
1953
+ amount: x402AuthorizationAmount(input.accepted),
1954
+ token: input.token,
1955
+ network: input.accepted.network,
1956
+ asset: input.accepted.asset,
1957
+ resource: input.accepted.resource ?? input.resourceUrl
1958
+ }
1959
+ };
1960
+ }
1961
+ function buildX402ResumeState(input) {
1962
+ const token = resolveTokenFromAddress(input.accepted.asset, input.accepted.network);
1963
+ return {
1964
+ rail: "x402",
1965
+ paymentId: input.paymentId,
1966
+ idempotencyKey: input.idempotencyKey,
1967
+ paymentRequired: input.paymentRequired,
1968
+ accepted: input.accepted,
1969
+ url: input.request?.url ?? input.paymentRequired.resource.url,
1970
+ request: input.request,
1971
+ resourceUrl: input.paymentRequired.resource.url,
1972
+ description: input.paymentRequired.resource.description ?? input.accepted.description ?? null,
1973
+ amountAtomic: x402AuthorizationAmount(input.accepted),
1974
+ amount: decimalFromUsdcAtomic(x402AuthorizationAmount(input.accepted)),
1975
+ token: token?.symbol ?? "USDC",
1976
+ asset: input.accepted.asset,
1977
+ network: input.accepted.network,
1978
+ chainId: chainIdOrNull(input.accepted.network),
1979
+ merchantAddress: input.accepted.payTo
1980
+ };
1981
+ }
1982
+ function attachX402ResumeState(err, paymentRequired, accepted, idempotencyKey, request) {
1983
+ if (!(err instanceof HavenPaymentStateError)) return;
1984
+ if (err.state.rail !== "x402") return;
1985
+ err.resumeState = buildX402ResumeState({
1986
+ paymentId: err.state.paymentId,
1987
+ paymentRequired,
1988
+ accepted,
1989
+ idempotencyKey,
1990
+ request
1991
+ });
1992
+ }
1993
+ function attachResumeState(err, input) {
1994
+ attachX402ResumeState(
1995
+ err,
1996
+ input.paymentRequired,
1997
+ input.accepted,
1998
+ input.idempotencyKey,
1999
+ input.request
2000
+ );
2001
+ }
2002
+ function assertCanResumeX402(status, paymentRequired, option) {
2003
+ if (status.rail !== "x402") {
2004
+ throw new HavenPaymentStateError(
2005
+ `Payment ${status.paymentId} is ${status.rail}, not x402.`,
2006
+ 409,
2007
+ status
2008
+ );
2009
+ }
2010
+ if (status.nextAction !== AgentPaymentNextAction.RetryOriginalX402Request) {
2011
+ throw new HavenPaymentStateError(status.message, paymentStateStatusCode(status.status, 409), status);
2012
+ }
2013
+ if (!status.txHash) {
2014
+ throw new HavenApiError(
2015
+ `x402 payment ${status.paymentId} is ready to retry but has no Haven transaction hash.`,
2016
+ 502,
2017
+ status,
2018
+ status.paymentId
2019
+ );
2020
+ }
2021
+ if (status.resourceUrl && status.resourceUrl !== paymentRequired.resource.url) {
2022
+ throw new HavenApiError(
2023
+ "x402 resume request does not match the approved resource URL.",
2024
+ 409,
2025
+ { status, paymentRequired },
2026
+ status.paymentId
2027
+ );
2028
+ }
2029
+ if (status.merchantAddress && !sameAddress3(status.merchantAddress, option.payTo)) {
2030
+ throw new HavenApiError(
2031
+ "x402 resume request does not match the approved merchant.",
2032
+ 409,
2033
+ { status, selectedPayment: option },
2034
+ status.paymentId
2035
+ );
2036
+ }
2037
+ const optionChainId = chainIdFromNetwork(option.network);
2038
+ if (status.chainId && optionChainId && status.chainId !== optionChainId) {
2039
+ throw new HavenApiError(
2040
+ "x402 resume request does not match the approved network.",
2041
+ 409,
2042
+ { status, selectedPayment: option },
2043
+ status.paymentId
2044
+ );
2045
+ }
2046
+ if (status.token && status.token !== "USDC") {
2047
+ throw new HavenApiError(
2048
+ "x402 resume request does not match the approved token.",
2049
+ 409,
2050
+ { status, selectedPayment: option },
2051
+ status.paymentId
2052
+ );
2053
+ }
2054
+ const approvedAmount = status.amount ? normalizeDecimal(status.amount) : "";
2055
+ const requestedAmount = normalizeDecimal(decimalFromUsdcAtomic(x402AuthorizationAmount(option)));
2056
+ if (approvedAmount && approvedAmount !== requestedAmount) {
2057
+ throw new HavenApiError(
2058
+ "x402 resume request does not match the approved amount.",
2059
+ 409,
2060
+ { status, selectedPayment: option },
2061
+ status.paymentId
2062
+ );
2063
+ }
2064
+ }
2065
+ var X402FundingLeg = class {
2066
+ delegateKey;
2067
+ delegateAddress;
2068
+ x402Wallet;
2069
+ chainRpcs;
2070
+ post;
2071
+ signForData;
2072
+ assertSignableAuthorizationState;
2073
+ /**
2074
+ * Receipts keyed by idempotency key, held only as long as the underlying
2075
+ * EIP-3009 authorization is valid. The cache belongs to this module rather
2076
+ * than to the facade because its expiry is read out of the authorization
2077
+ * header itself — a 3009 artifact.
2078
+ */
2079
+ receiptCache = /* @__PURE__ */ new Map();
2080
+ constructor(options) {
2081
+ this.delegateKey = options.delegateKey;
2082
+ this.delegateAddress = options.delegateAddress;
2083
+ this.x402Wallet = options.x402Wallet;
2084
+ this.chainRpcs = options.chainRpcs;
2085
+ this.post = options.post;
2086
+ this.signForData = options.signForData;
2087
+ this.assertSignableAuthorizationState = options.assertSignableAuthorizationState;
2088
+ }
2089
+ // ── Receipt cache ────────────────────────────────────────────────
2090
+ /** A still-valid cached receipt for this key, or undefined. */
2091
+ cachedReceipt(idempotencyKey) {
2092
+ const cached = this.receiptCache.get(idempotencyKey);
2093
+ if (cached && cached.expiresAt > Date.now()) return cached.receipt;
2094
+ return void 0;
2095
+ }
2096
+ cacheReceipt(idempotencyKey, paymentHeader, receipt) {
2097
+ const expiresAt = getPaymentHeaderValidBefore(paymentHeader);
2098
+ if (expiresAt > Date.now()) {
2099
+ this.receiptCache.set(idempotencyKey, { expiresAt, receipt });
2100
+ }
2101
+ }
2102
+ // ── Authorization ────────────────────────────────────────────────
2103
+ async authorize(paymentRequired, option, idempotencyKey) {
2104
+ const raw = await this.post("/x402", {
2105
+ url: paymentRequired.resource.url,
2106
+ payTo: this.delegateAddress,
2107
+ merchantPayTo: option.payTo,
2108
+ amount: x402AuthorizationAmount(option),
2109
+ asset: option.asset,
2110
+ network: option.network,
2111
+ description: paymentRequired.resource.description,
2112
+ idempotencyKey,
2113
+ // #1360: same explicit funding-leg declaration as createX402Intent —
2114
+ // this local-key path derives payTo from the key (never stale), but the
2115
+ // declaration keeps both writers of the 3009 shape loud-by-default.
2116
+ settlementScheme: "eip3009"
2117
+ });
2118
+ const state = paymentStateFromRaw("x402 payment", raw);
2119
+ const executedReplay = raw.success && raw.tx_hash ? "idempotency-collision" : state?.nextAction === AgentPaymentNextAction.RetryOriginalX402Request ? "approval-resume" : null;
2120
+ if (executedReplay) {
2121
+ const canFund = await this.delegateCanFund(
2122
+ raw.chain_id ?? state?.chainId ?? chainIdFromNetwork(option.network),
2123
+ option.asset,
2124
+ x402AuthorizationAmount(option)
2125
+ );
2126
+ const refuse = executedReplay === "idempotency-collision" ? canFund !== true : canFund === false;
2127
+ if (refuse) {
2128
+ const settledReceipt = state && executedReplay === "approval-resume" ? this.receiptFromStatus(paymentRequired, option, void 0, state) : this.receiptFromAuthorization(paymentRequired, option, void 0, raw);
2129
+ throw new X402AlreadySettledError(
2130
+ canFund === false ? "This x402 payment already settled \u2014 the delegate no longer holds the funds to authorize it again. To buy the same item a second time, pass a distinct `idempotencyKey`; the synthesised key intentionally collapses repeat calls for the same product within a 5-minute window so a retried request cannot pay twice." : "This x402 payment already settled, and whether the delegate can still fund a new authorization could not be verified (no `chainRpcs` entry for this chain). Refusing rather than issue an authorization that may be unfundable. To buy the same item a second time, pass a distinct `idempotencyKey`; to finish an interrupted payment, resume it by `paymentId`.",
2131
+ settledReceipt,
2132
+ canFund === false ? "settled" : "unverifiable"
2133
+ );
2134
+ }
2135
+ }
2136
+ const paymentHeader = await this.createPaymentHeader(paymentRequired, option);
2137
+ if (raw.success && raw.tx_hash) {
2138
+ const receipt2 = this.receiptFromAuthorization(paymentRequired, option, paymentHeader, raw);
2139
+ this.cacheReceipt(idempotencyKey, paymentHeader, receipt2);
2140
+ return receipt2;
2141
+ }
2142
+ if (state?.nextAction === AgentPaymentNextAction.RetryOriginalX402Request) {
2143
+ const receipt2 = this.receiptFromStatus(paymentRequired, option, paymentHeader, state);
2144
+ this.cacheReceipt(idempotencyKey, paymentHeader, receipt2);
2145
+ return receipt2;
2146
+ }
2147
+ this.assertSignableAuthorizationState("x402 payment", raw);
2148
+ if (!raw.sign_data?.hash) {
2149
+ throw new HavenApiError("No sign_hash returned from x402/authorize", 500, raw);
2150
+ }
2151
+ const sig = await this.signForData(raw.sign_data);
2152
+ const execResult = await this.post(
2153
+ `/payments/${raw.payment_id}/sign`,
2154
+ { signature: sig }
2155
+ );
2156
+ if (execResult.status !== "confirmed") {
2157
+ throwPaymentStateError("x402 payment", execResult);
2158
+ }
2159
+ await this.waitForFundingTx(
2160
+ execResult.tx_hash,
2161
+ execResult.chain_id ?? chainIdFromNetwork(option.network)
2162
+ );
2163
+ const receipt = this.receiptFromAuthorization(paymentRequired, option, paymentHeader, raw, execResult);
2164
+ this.cacheReceipt(idempotencyKey, paymentHeader, receipt);
2165
+ return receipt;
2166
+ }
2167
+ // ── Header minting ───────────────────────────────────────────────
2168
+ async createPaymentHeader(paymentRequired, option) {
2169
+ if (!this.delegateKey) {
2170
+ throw new HavenSigningError("delegateKey is required to sign x402 payment headers.");
2171
+ }
2172
+ const account = accounts.privateKeyToAccount(this.delegateKey);
2173
+ const requirements = toStandardPaymentRequirements(paymentRequired, option);
2174
+ const header = await schemes.exact.evm.createPaymentHeader(
2175
+ account,
2176
+ paymentRequired.x402Version,
2177
+ requirements
2178
+ );
2179
+ if (paymentRequired.x402Version < 2) return header;
2180
+ const payment = decodeBase64Json(header);
2181
+ return encodeBase64Json(x402V2PaymentEnvelope(paymentRequired, option, payment.payload));
2182
+ }
2183
+ // ── Receipt mapping ──────────────────────────────────────────────
2184
+ receiptFromAuthorization(paymentRequired, option, paymentHeader, raw, execResult) {
2185
+ const txHash = execResult?.tx_hash ?? raw.tx_hash ?? "";
2186
+ const chainId = execResult?.chain_id ?? raw.chain_id ?? chainIdFromNetwork(option.network);
2187
+ const token = execResult?.token ?? raw.token ?? "USDC";
2188
+ const amount = execResult?.amount ?? raw.amount ?? decimalFromUsdcAtomic(x402AuthorizationAmount(option));
2189
+ const to = execResult?.to ?? raw.to ?? this.delegateAddress ?? "";
2190
+ const explorerUrl = execResult?.explorer_url ?? raw.explorer_url ?? explorerUrlOrEmpty(chainId, txHash);
2191
+ const merchantTo = execResult?.merchant_to ?? raw.merchant_to ?? option.payTo;
2192
+ const payer = raw.payer ?? raw.safe_address ?? raw.sign_data?.components.safe;
2193
+ return buildX402Receipt({
2194
+ paymentId: raw.payment_id,
2195
+ txHash,
2196
+ token,
2197
+ amount,
2198
+ to,
2199
+ resourceUrl: paymentRequired.resource.url,
2200
+ explorerUrl,
2201
+ accepted: option,
2202
+ paymentHeader,
2203
+ merchantTo,
2204
+ payer,
2205
+ chainId
2206
+ });
2207
+ }
2208
+ receiptFromStatus(paymentRequired, option, paymentHeader, status) {
2209
+ if (!status.txHash) {
2210
+ throw new HavenApiError(
2211
+ `x402 payment ${status.paymentId} is ready to retry but has no Haven transaction hash.`,
2212
+ 502,
2213
+ status,
2214
+ status.paymentId
2215
+ );
2216
+ }
2217
+ return buildX402Receipt({
2218
+ paymentId: status.paymentId,
2219
+ txHash: status.txHash,
2220
+ token: status.token || "USDC",
2221
+ amount: status.amount || decimalFromUsdcAtomic(x402AuthorizationAmount(option)),
2222
+ to: this.delegateAddress ?? "",
2223
+ resourceUrl: paymentRequired.resource.url,
2224
+ explorerUrl: explorerUrlOrEmpty(status.chainId, status.txHash),
2225
+ accepted: option,
2226
+ paymentHeader,
2227
+ merchantTo: status.merchantAddress ?? option.payTo,
2228
+ payer: this.x402Wallet,
2229
+ chainId: status.chainId || chainIdFromNetwork(option.network)
2230
+ });
2231
+ }
2232
+ // ── On-chain reads ───────────────────────────────────────────────
2233
+ /**
2234
+ * Wait for a funding tx to be mined with ≥1 confirmation before the
2235
+ * merchant retry, eliminating the race where the merchant's
2236
+ * `balanceOf(delegate)` runs before the funding block propagates.
2237
+ *
2238
+ * Skipped when `chainRpcs` does not include the chain; in that case Haven's
2239
+ * backend has already confirmed on-chain submission and callers accept the
2240
+ * small propagation window as a trade-off for not configuring an RPC URL.
2241
+ */
2242
+ async waitForFundingTx(txHash, chainId, timeoutMs = 3e4) {
2243
+ if (!txHash || !chainId) return;
2244
+ const rpcUrl = this.chainRpcs[chainId];
2245
+ if (!rpcUrl) return;
2246
+ const provider = createJsonRpcProvider(rpcUrl);
2247
+ const onChainReceipt = await provider.waitForTransaction(txHash, 1, timeoutMs);
2248
+ if (!onChainReceipt || onChainReceipt.status !== 1) {
2249
+ throw new HavenApiError(
2250
+ "Funding tx did not confirm on-chain within the timeout window.",
2251
+ 500,
2252
+ { txHash, chainId }
2253
+ );
2254
+ }
2255
+ }
2256
+ /**
2257
+ * Can the delegate EOA still fund an authorization for `amountAtomic`?
2258
+ *
2259
+ * #1521: the only question that separates a legitimate resume (funding
2260
+ * confirmed, merchant never paid — the delegate still holds the money) from
2261
+ * a replayed settled payment (funding confirmed, merchant paid, delegate
2262
+ * spent). The intent's own `status: 'confirmed'` is identical in both.
2263
+ *
2264
+ * The balance is asked of the CHAIN rather than of Haven's bookkeeping on
2265
+ * purpose: the merchant-settlement evidence record is written by this SDK
2266
+ * *after* the merchant call, so a client that dies between the two leaves
2267
+ * the backend believing the merchant was never paid — the exact case the
2268
+ * discriminator has to get right. The chain cannot be behind in that way.
2269
+ *
2270
+ * Returns `null` — never a guess — when `chainRpcs` has no entry for the
2271
+ * chain or the read fails. Callers must treat that as "unverifiable", not
2272
+ * as "funded".
2273
+ */
2274
+ async delegateCanFund(chainId, tokenAddress, amountAtomic, timeoutMs = 1e4) {
2275
+ if (!chainId || !this.delegateAddress) return null;
2276
+ const rpcUrl = this.chainRpcs[chainId];
2277
+ if (!rpcUrl) return null;
2278
+ try {
2279
+ const provider = createJsonRpcProvider(rpcUrl);
2280
+ const token = createErc20Contract(
2281
+ tokenAddress,
2282
+ ["function balanceOf(address) view returns (uint256)"],
2283
+ provider
2284
+ );
2285
+ const balance = await Promise.race([
2286
+ token.balanceOf(this.delegateAddress),
2287
+ new Promise((resolve) => setTimeout(() => resolve(null), timeoutMs).unref?.())
2288
+ ]);
2289
+ if (balance === null) return null;
2290
+ return balance >= BigInt(amountAtomic);
2291
+ } catch {
2292
+ return null;
2293
+ }
2294
+ }
2295
+ };
2296
+ function getPaymentHeaderValidBefore(paymentHeader) {
2297
+ try {
2298
+ const payment = decodeBase64Json(
2299
+ paymentHeader
2300
+ );
2301
+ const payload = payment.payload;
2302
+ const validBeforeSeconds = Number(payload.authorization?.validBefore);
2303
+ if (Number.isFinite(validBeforeSeconds)) return validBeforeSeconds * 1e3;
2304
+ } catch {
2305
+ }
2306
+ return 0;
2307
+ }
2308
+
2309
+ // src/x402-erc7710.ts
2310
+ var X402Erc7710 = class {
2311
+ delegateKey;
2312
+ post;
2313
+ signForData;
2314
+ getAgent;
2315
+ constructor(options) {
2316
+ this.delegateKey = options.delegateKey;
2317
+ this.post = options.post;
2318
+ this.signForData = options.signForData;
2319
+ this.getAgent = options.getAgent;
2320
+ }
2321
+ /**
2322
+ * Pay a merchant through **erc7710 direct settlement** (#1454, epic #1450).
2323
+ *
2324
+ * The whole point of this path is what it does NOT do. There is no funding
2325
+ * leg: the merchant redeems a delegation chain and pulls from the treasury
2326
+ * directly, so the delegate EOA never holds the money, no sweep can strand
2327
+ * it, and the #713 reconciliation class does not apply. It is also why this
2328
+ * method is SMALLER than the 3009 path — the backend assembles the merchant
2329
+ * `X-PAYMENT` header in `assembleSettlementPayload`, so the SDK builds no
2330
+ * header locally.
2331
+ *
2332
+ * authorize (payTo = the MERCHANT) → sign the child → settle → header
2333
+ *
2334
+ * The caller then retries the merchant with that header. **Nothing has
2335
+ * settled when this returns** — that is why it does not return an
2336
+ * `X402Receipt`.
2337
+ *
2338
+ * Requires a delegation-rail account. The backend enforces that at the
2339
+ * rail seam — a non-delegation account gets the #1986 retired-rail 410 from
2340
+ * `POST /x402/authorize` whatever scheme it asks for (#2245) — and so does
2341
+ * this method, before building a request the backend would only reject: an
2342
+ * error a client can explain is worth more than a refusal it has to decode.
2343
+ *
2344
+ * **MCP callers must pass `options.resourceUrl`.** An in-band MCP 402
2345
+ * challenge frequently carries no `resource` object at all, so
2346
+ * `paymentRequired.resource?.url` is undefined and the backend answers
2347
+ * "Valid url is required". The QA scenario this path was ported from falls
2348
+ * back to the request URL for exactly that reason — the SDK cannot, because
2349
+ * it never saw the request. Pass it.
2350
+ */
2351
+ async settle(paymentRequired, options = {}) {
2352
+ if (!this.delegateKey) {
2353
+ throw new HavenSigningError(
2354
+ "delegateKey is required for x402 payments. Pass it in the HavenClient config."
2355
+ );
2356
+ }
2357
+ const prepared = await this.prepare(paymentRequired, options);
2358
+ const signature = await this.signForData(prepared.signData);
2359
+ const paymentHeader = await this.submit(prepared.paymentId, signature);
2360
+ return { ...prepared.settlement, paymentHeader };
2361
+ }
2362
+ /**
2363
+ * The AUTHORIZE half of erc7710 settlement (#1456): select the scheme, build
2364
+ * the request, and return the child to be signed — without signing it.
2365
+ *
2366
+ * Split out because the hosted topology cannot use `settleX402Erc7710()`:
2367
+ * that method signs in-process with `delegateKey`, and hosted Haven does not
2368
+ * have one and must not. The hosted MCP server drives these two halves with
2369
+ * the LOCAL signer in between, so the key stays where it belongs and the
2370
+ * request shaping stays in one place rather than being reimplemented.
2371
+ */
2372
+ async prepare(paymentRequired, options = {}) {
2373
+ const delegationRail = options.delegationRail ?? (await this.getAgent()).executionRail === "delegation";
2374
+ if (!delegationRail) {
2375
+ throw new HavenApiError(
2376
+ "erc7710 settlement requires a delegation-rail account; this one is not on it. Use authorizeX402() for the standard EIP-3009 path.",
2377
+ 400
2378
+ );
2379
+ }
2380
+ const selection = selectX402SettlementScheme(paymentRequired.accepts, { delegationRail });
2381
+ if (!selection || selection.scheme !== "erc7710") {
2382
+ throw new HavenApiError(
2383
+ "This merchant does not advertise an erc7710 settlement option (no accepts[] entry carries extra.assetTransferMethod: 'erc7710'). Use authorizeX402() for the standard EIP-3009 path.",
2384
+ 400
2385
+ );
2386
+ }
2387
+ const option = selection.option;
2388
+ const merchantPayTo = option.payTo;
2389
+ const amountAtomic = x402AuthorizationAmount(option);
2390
+ const raw = await this.post("/x402", {
2391
+ url: options.resourceUrl ?? paymentRequired.resource?.url,
2392
+ // #2373: the full 402 challenge, persisted verbatim by the backend
2393
+ // (#1355) so the settle handoff can echo its resource/extensions into
2394
+ // the X-PAYMENT envelope (#2361). This scheme decomposes the challenge
2395
+ // into the fields below for AUTHORITY; the stored copy exists for the
2396
+ // echo, which cannot be reconstructed from the decomposition — omitting
2397
+ // it is how every erc7710 payment failed a merchant that enforces the
2398
+ // spec's extensions-echo MUST. Same ≤64KB guard and omission behaviour
2399
+ // as the 3009 path (client.ts): an oversized challenge omits the field
2400
+ // rather than failing the payment, and the settle echo then omits too.
2401
+ ...new TextEncoder().encode(JSON.stringify(paymentRequired)).length <= 65536 ? { paymentRequired } : {},
2402
+ // payTo = the MERCHANT is what selects direct settlement server-side.
2403
+ // The explicit settlementScheme must AGREE with that shape (#1360) —
2404
+ // disagreement is a 400 by design, so that a stale delegate address
2405
+ // becomes a loud mismatch instead of a silent reroute to the 3009 leg.
2406
+ payTo: merchantPayTo,
2407
+ settlementScheme: "erc7710",
2408
+ // #2041: sent only when the caller supplied one, so an omitting caller's
2409
+ // request body is byte-identical to the pre-#2041 shape.
2410
+ ...options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {},
2411
+ amount: amountAtomic,
2412
+ asset: option.asset,
2413
+ network: option.network,
2414
+ // The v2 header echoes the accepted entry field-for-field, so the quoted
2415
+ // timeout must round-trip or the merchant rejects the echo (#1064).
2416
+ maxTimeoutSeconds: option.maxTimeoutSeconds,
2417
+ // #1058: forward the advertised facilitators verbatim — the child becomes
2418
+ // redeemable ONLY by them. `null` here means the merchant advertised none
2419
+ // (or an empty array, which the backend 400s on), so the field is OMITTED
2420
+ // rather than sent empty. See x402FacilitatorAddresses.
2421
+ ...selection.facilitatorAddresses ? { facilitatorAddresses: selection.facilitatorAddresses } : {},
2422
+ // #1307/#1547: persisted so the settle leg can rehydrate the merchant
2423
+ // call by payment_id on this scheme too, not only on the 3009 bridge.
2424
+ ...options.mcpCallContext ? { mcpCallContext: options.mcpCallContext } : {}
2425
+ });
2426
+ if (!raw.payment_id) {
2427
+ throw new HavenApiError("No payment_id returned from x402/authorize", 500, raw);
2428
+ }
2429
+ const signData = raw.sign_data;
2430
+ if (signData?.signature_scheme !== "eip712_delegation" || !signData.typed_data) {
2431
+ throw new HavenApiError(
2432
+ `x402/authorize did not return an erc7710 settlement child (signature_scheme was ${JSON.stringify(signData?.signature_scheme)}). Refusing to sign a payload this path did not ask for.`,
2433
+ 500,
2434
+ raw
2435
+ );
2436
+ }
2437
+ return {
2438
+ paymentId: raw.payment_id,
2439
+ signData,
2440
+ settlement: {
2441
+ paymentId: raw.payment_id,
2442
+ merchantPayTo,
2443
+ amountAtomic,
2444
+ asset: option.asset,
2445
+ network: option.network,
2446
+ facilitatorAddresses: selection.facilitatorAddresses
2447
+ }
2448
+ };
2449
+ }
2450
+ /**
2451
+ * The SETTLE half (#1456): exchange the signed child for the merchant header.
2452
+ *
2453
+ * The SDK builds no header on this path — the backend assembles the MetaMask
2454
+ * erc7710 payload in `assembleSettlementPayload`. Whoever produced the
2455
+ * signature (an in-process delegate key, or the local edge signer over the
2456
+ * hosted boundary) is irrelevant here.
2457
+ */
2458
+ async submit(paymentId, signature) {
2459
+ const settled = await this.post(
2460
+ `/x402/${paymentId}/settle`,
2461
+ { signature }
2462
+ );
2463
+ if (!settled.payment_header) {
2464
+ throw new HavenApiError(
2465
+ "x402 settle returned no payment_header \u2014 the merchant cannot be retried.",
2466
+ 500,
2467
+ settled
2468
+ );
2469
+ }
2470
+ return settled.payment_header;
2471
+ }
2472
+ };
2473
+
2474
+ // src/tool-adapter.ts
2475
+ function toolX402PaymentRequired(input) {
2476
+ return {
2477
+ x402Version: 2,
2478
+ resource: { url: input.url, description: input.description },
2479
+ accepts: [
2480
+ {
2481
+ scheme: "exact",
2482
+ network: input.network,
2483
+ amount: input.amount,
2484
+ asset: input.asset,
2485
+ payTo: input.payTo,
2486
+ maxTimeoutSeconds: 30
2487
+ }
2488
+ ]
2489
+ };
2490
+ }
2491
+ function x402ToolReceipt(receipt) {
2492
+ return {
2493
+ success: true,
2494
+ payment_id: receipt.paymentId,
2495
+ tx_hash: receipt.txHash,
2496
+ token: receipt.token,
2497
+ amount: receipt.amount,
2498
+ to: receipt.to,
2499
+ resource_url: receipt.resourceUrl,
2500
+ explorer_url: receipt.explorerUrl,
2501
+ payment_header: receipt.paymentHeader,
2502
+ merchant_to: receipt.merchantTo,
2503
+ payer: receipt.payer,
2504
+ chain_id: receipt.chainId,
2505
+ haven: receipt.haven,
2506
+ merchant: receipt.merchant,
2507
+ x402: receipt.x402
2508
+ };
2509
+ }
2510
+ function toolError(err) {
2511
+ if (err instanceof HavenPaymentStateError) {
2512
+ return {
2513
+ success: false,
2514
+ payment_id: err.state.paymentId,
2515
+ kind: err.state.kind,
2516
+ rail: err.state.rail,
2517
+ status: err.state.status,
2518
+ phase: err.state.phase,
2519
+ next_action: err.state.nextAction,
2520
+ tx_hash: err.state.txHash,
2521
+ token: err.state.token,
2522
+ amount: err.state.amount,
2523
+ resource_url: err.state.resourceUrl,
2524
+ merchant_address: err.state.merchantAddress,
2525
+ amount_atomic: err.state.amountAtomic,
2526
+ asset: err.state.asset,
2527
+ network: err.state.network,
2528
+ description: err.state.description,
2529
+ idempotency_key: err.state.idempotencyKey,
2530
+ x402: err.state.x402 ? {
2531
+ amount_atomic: err.state.x402.amountAtomic,
2532
+ asset: err.state.x402.asset,
2533
+ network: err.state.x402.network,
2534
+ resource_url: err.state.x402.resourceUrl,
2535
+ merchant_address: err.state.x402.merchantAddress,
2536
+ description: err.state.x402.description,
2537
+ idempotency_key: err.state.x402.idempotencyKey
2538
+ } : void 0,
2539
+ mpp: err.state.mpp ? {
2540
+ amount_atomic: err.state.mpp.amountAtomic,
2541
+ asset: err.state.mpp.asset,
2542
+ network: err.state.mpp.network,
2543
+ resource_url: err.state.mpp.resourceUrl,
2544
+ merchant_address: err.state.mpp.merchantAddress,
2545
+ description: err.state.mpp.description,
2546
+ idempotency_key: err.state.mpp.idempotencyKey,
2547
+ challenge_id: err.state.mpp.challengeId
2548
+ } : void 0,
2549
+ resume_state: err.resumeState,
2550
+ expires_at: err.state.expiresAt,
2551
+ chain_id: err.state.chainId,
2552
+ message: err.state.message,
2553
+ error: err.message
2554
+ };
2555
+ }
2556
+ if (err instanceof HavenApiError) {
2557
+ return {
2558
+ success: false,
2559
+ status_code: err.statusCode,
2560
+ error: err.message,
2561
+ body: err.body
2562
+ };
2563
+ }
2564
+ return {
2565
+ success: false,
2566
+ error: err instanceof Error ? err.message : String(err)
2567
+ };
2568
+ }
2569
+
2570
+ // src/merchant-completion.ts
2571
+ var MERCHANT_BODY_SNIPPET_LIMIT = 1e3;
2572
+ var EVIDENCE_RETRY_DELAYS_MS = [1e3, 2e3, 4e3];
2573
+ var EVIDENCE_RETRYABLE_STATUS = 503;
2574
+ var MerchantCompletion = class {
2575
+ post;
2576
+ merchantTransport;
2577
+ getPaymentStatus;
2578
+ getAgent;
2579
+ delegateAddress;
2580
+ x402Wallet;
2581
+ sleep;
2582
+ constructor(options) {
2583
+ this.sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
2584
+ this.post = options.post;
2585
+ this.merchantTransport = options.merchantTransport;
2586
+ this.getPaymentStatus = options.getPaymentStatus;
2587
+ this.getAgent = options.getAgent;
2588
+ this.delegateAddress = options.delegateAddress;
2589
+ this.x402Wallet = options.x402Wallet;
2590
+ }
2591
+ async retryRequest(url, initialInit, paymentRequired, receipt) {
2592
+ if (!receipt.accepted) {
2593
+ throw new HavenApiError("No accepted x402 option was recorded for payment retry", 500);
2594
+ }
2595
+ if (!receipt.paymentHeader) {
2596
+ throw new HavenApiError("No x402 payment header was returned for payment retry", 500);
2597
+ }
2598
+ const retryResponse = await this.merchantTransport.deliverPayment(
2599
+ url,
2600
+ initialInit,
2601
+ receipt.paymentHeader
2602
+ );
2603
+ if (!retryResponse.ok) {
2604
+ const merchant = await captureMerchantResponse(retryResponse);
2605
+ await this.recordRetryRejected({
2606
+ rail: "x402",
2607
+ paymentId: receipt.paymentId,
2608
+ txHash: receipt.txHash,
2609
+ resourceUrl: receipt.resourceUrl,
2610
+ merchant,
2611
+ details: {
2612
+ merchant_to: receipt.merchantTo,
2613
+ delegate_to: receipt.to
2614
+ }
2615
+ });
2616
+ throw new HavenApiError(
2617
+ "x402 retry failed after Haven funded the delegate wallet; reconciliation may be required.",
2618
+ merchant.merchant_status,
2619
+ {
2620
+ marker: "x402_retry_rejected_after_funding",
2621
+ payment_id: receipt.paymentId,
2622
+ tx_hash: receipt.txHash,
2623
+ resource_url: receipt.resourceUrl,
2624
+ merchant_to: receipt.merchantTo,
2625
+ delegate_to: receipt.to,
2626
+ ...merchant
2627
+ }
2628
+ );
2629
+ }
2630
+ const merchantSettlement = parseMerchantSettlement(retryResponse.headers.get("PAYMENT-RESPONSE"));
2631
+ if (receipt.merchant && merchantSettlement.settlementTxHash) {
2632
+ receipt.merchant.settlementTxHash = merchantSettlement.settlementTxHash;
2633
+ receipt.merchant.settlementExplorerUrl = buildExplorerUrl(
2634
+ receipt.chainId,
2635
+ merchantSettlement.settlementTxHash
2636
+ );
2637
+ }
2638
+ await this.reportEvidence({
2639
+ paymentId: receipt.paymentId,
2640
+ rail: "x402",
2641
+ txHash: receipt.txHash,
2642
+ resourceUrl: receipt.resourceUrl,
2643
+ merchantStatus: retryResponse.status,
2644
+ challengePayload: paymentRequired,
2645
+ selectedPayment: receipt.accepted,
2646
+ paymentProofHeaderName: x402PaymentHeaderNamesSent(receipt.paymentHeader),
2647
+ paymentProofHeader: receipt.paymentHeader,
2648
+ protocolReceiptHeaderName: "PAYMENT-RESPONSE",
2649
+ protocolReceiptHeader: retryResponse.headers.get("PAYMENT-RESPONSE") ?? void 0
2650
+ });
2651
+ await this.reportMerchantReceipt(receipt.paymentId, retryResponse);
2652
+ return retryResponse;
2653
+ }
2654
+ /**
2655
+ * #956: capture the merchant's OWN receipt when the paid response carries
2656
+ * one, and report it to Haven so the reporting feed can attach it next to
2657
+ * the Haven-generated payment evidence (#498). Two supported signals on the
2658
+ * paid response:
2659
+ *
2660
+ * x-receipt-json: base64-encoded JSON receipt document (inline)
2661
+ * x-receipt-url: https URL to the receipt document (reference)
2662
+ *
2663
+ * Strictly best-effort: absence is the normal case, and no failure here may
2664
+ * ever affect the completed payment — the response is already paid for.
2665
+ */
2666
+ async reportMerchantReceipt(paymentId, response) {
2667
+ try {
2668
+ const inlineB64 = response.headers.get("x-receipt-json");
2669
+ const url = response.headers.get("x-receipt-url");
2670
+ if (!inlineB64 && !url) return;
2671
+ let body = null;
2672
+ if (inlineB64) {
2673
+ if (inlineB64.length > Math.ceil(64 * 1024 * 4 / 3)) return;
2674
+ const decoded = JSON.parse(Buffer.from(inlineB64, "base64").toString("utf8"));
2675
+ if (decoded && typeof decoded === "object") body = { json: decoded };
2676
+ } else if (url && url.startsWith("https://") && url.length <= 2048) {
2677
+ body = { url };
2678
+ }
2679
+ if (!body) return;
2680
+ await this.post(`/machine-payments/${paymentId}/merchant-receipt`, body);
2681
+ } catch {
2682
+ }
2683
+ }
2684
+ async resolveCompletionContext(input) {
2685
+ const status = await this.getPaymentStatus(input.paymentId);
2686
+ if (status.rail !== "x402") {
2687
+ throw new HavenPaymentStateError(
2688
+ `Payment ${status.paymentId} is ${status.rail}, not x402.`,
2689
+ 409,
2690
+ status
2691
+ );
2692
+ }
2693
+ const readyForMerchantCompletion = input.noFundingLeg ? status.kind === "payment_intent" && status.status === "submitted" : status.nextAction === AgentPaymentNextAction.RetryOriginalX402Request || status.kind === "payment_intent" && status.status === "confirmed" && status.phase === AgentPaymentPhase.PaymentConfirmed && status.nextAction === AgentPaymentNextAction.None;
2694
+ if (!readyForMerchantCompletion) {
2695
+ throw new HavenPaymentStateError(status.message, paymentStateStatusCode(status.status, 409), status);
2696
+ }
2697
+ if (!input.noFundingLeg && !status.txHash) {
2698
+ throw new HavenApiError(
2699
+ `x402 payment ${status.paymentId} is ready for merchant completion but has no Haven transaction hash.`,
2700
+ 502,
2701
+ status,
2702
+ status.paymentId
2703
+ );
2704
+ }
2705
+ const approvedResourceUrl = status.resourceUrl ?? status.x402?.resourceUrl ?? null;
2706
+ if (approvedResourceUrl && approvedResourceUrl !== input.url) {
2707
+ throw new HavenApiError(
2708
+ "x402 merchant completion does not match the approved resource URL.",
2709
+ 409,
2710
+ { status, url: input.url },
2711
+ status.paymentId
2712
+ );
2713
+ }
2714
+ return {
2715
+ paymentId: status.paymentId,
2716
+ txHash: status.txHash,
2717
+ resourceUrl: approvedResourceUrl ?? input.url,
2718
+ merchantAddress: status.merchantAddress ?? status.x402?.merchantAddress ?? null
2719
+ };
2720
+ }
2721
+ async resolveWalletForMerchantCall() {
2722
+ const localWallet = x402PayerAddress(this.delegateAddress, this.x402Wallet);
2723
+ if (localWallet) return localWallet;
2724
+ try {
2725
+ const agent = await this.getAgent();
2726
+ return agent.delegateAddress ?? void 0;
2727
+ } catch {
2728
+ return void 0;
2729
+ }
2730
+ }
2731
+ // #1328: authorizeMachinePayment / authorizeMppDemoPayment / resumeAuthorizedMpp
2732
+ // / resumeMppPayment / fetchWithMachinePayment / retryMppRequest (the
2733
+ // MACHINE-PAYMENT-CHALLENGE / mpp_demo client surface) are retired — the
2734
+ // backend's POST /machine-payments/authorize refuses unconditionally now,
2735
+ // and MACHINE-PAYMENT-CHALLENGE was never produced by any other Haven
2736
+ // surface. Use the x402 flow (authorizeX402 / fetch / quoteX402 / payX402Quote)
2737
+ // for agent-to-merchant payments.
2738
+ async recordRetryRejected(input) {
2739
+ try {
2740
+ await this.post("/machine-payments/reconciliation-events", {
2741
+ paymentId: input.paymentId,
2742
+ rail: input.rail,
2743
+ eventType: "merchant_retry_rejected_after_payment",
2744
+ txHash: input.txHash,
2745
+ reason: `Merchant returned HTTP ${input.merchant.merchant_status} after Haven payment confirmation`,
2746
+ details: {
2747
+ resource_url: input.resourceUrl,
2748
+ retry_status: input.merchant.merchant_status,
2749
+ retry_body: input.merchant.merchant_body.slice(0, MERCHANT_BODY_SNIPPET_LIMIT) || null,
2750
+ ...input.details
2751
+ }
2752
+ });
2753
+ } catch {
2754
+ }
2755
+ }
2756
+ /**
2757
+ * #2292: record what a merchant said to a retry **Haven did not make**.
2758
+ *
2759
+ * On the plain-HTTP x402 path Haven tells the agent to call the merchant
2760
+ * itself — that is the keyless design, not an oversight — so the two writes
2761
+ * above were reachable only from `completeX402MerchantCall`, where Haven IS
2762
+ * the caller. A manual retry had nowhere to put its outcome, which left
2763
+ * `intentStateFor`'s merchant-rejected branch dead on the one flow Haven
2764
+ * prescribes and made the 15-minute grace window the only route to
2765
+ * `funded_but_unsettled`.
2766
+ *
2767
+ * Three properties distinguish this from `recordRetryRejected` /
2768
+ * `reportEvidence`, and each is deliberate:
2769
+ *
2770
+ * 1. **It does not swallow.** Those two are bookkeeping hung off a call
2771
+ * whose outcome is already decided, so an exception there would turn a
2772
+ * completed payment into a reported failure. Here the report IS the
2773
+ * caller's request; silently dropping it would recreate the exact
2774
+ * unobservability #2292 exists to remove.
2775
+ * 2. **The anchor is server-side.** `txHash` and `resourceUrl` come from
2776
+ * the payment's own Haven record, never from the reporter — so a report
2777
+ * cannot be pointed at a different transaction or a different resource,
2778
+ * and it can never CONFIRM an intent (an erc7710 intent has no Haven
2779
+ * tx hash and is refused here rather than completed from a supplied one,
2780
+ * which is #2092's verified seam and stays its own path).
2781
+ * 3. **It is evidence, never authority.** Haven does not and must not check
2782
+ * the claim: verifying it would mean calling the merchant, which is the
2783
+ * property this whole path exists to preserve. What bounds a false
2784
+ * report is scope — the backend routes resolve the payment
2785
+ * `WHERE agent_id = $`, so a caller can only ever describe its own
2786
+ * payment — plus the fact that nothing financial keys off the claim:
2787
+ * the sweep is balance-driven, the intent's status/amount/recipient are
2788
+ * untouched, and a false `accepted` runs the server's own on-chain
2789
+ * residue check, which re-flags stranded funds independently.
2790
+ */
2791
+ async reportMerchantOutcome(input) {
2792
+ if (!Number.isInteger(input.merchantStatus) || input.merchantStatus < 100 || input.merchantStatus > 599) {
2793
+ throw new HavenApiError(
2794
+ `merchant_status must be an integer HTTP status from 100 to 599 (received ${input.merchantStatus}).`,
2795
+ 400,
2796
+ void 0,
2797
+ input.paymentId
2798
+ );
2799
+ }
2800
+ const looksAccepted = input.merchantStatus >= 200 && input.merchantStatus < 300;
2801
+ if (looksAccepted !== (input.outcome === "accepted")) {
2802
+ throw new HavenApiError(
2803
+ `outcome "${input.outcome}" contradicts merchant_status ${input.merchantStatus}: report "accepted" only for a 2xx and "rejected" only for a non-2xx.`,
2804
+ 400,
2805
+ void 0,
2806
+ input.paymentId
2807
+ );
2808
+ }
2809
+ const status = await this.getPaymentStatus(input.paymentId);
2810
+ if (status.rail !== "x402") {
2811
+ throw new HavenPaymentStateError(
2812
+ `Payment ${status.paymentId} is ${status.rail}, not x402 \u2014 there is no merchant retry to report.`,
2813
+ 409,
2814
+ status
2815
+ );
2816
+ }
2817
+ if (status.status !== "confirmed" || !status.txHash) {
2818
+ throw new HavenPaymentStateError(
2819
+ `Payment ${status.paymentId} has no confirmed Haven funding transaction to anchor a merchant report to (status ${status.status}). ${status.message}`,
2820
+ paymentStateStatusCode(status.status, 409),
2821
+ status
2822
+ );
2823
+ }
2824
+ const resourceUrl = status.resourceUrl ?? status.x402?.resourceUrl ?? null;
2825
+ if (!resourceUrl) {
2826
+ throw new HavenApiError(
2827
+ `Payment ${status.paymentId} has no recorded resource URL, so a merchant report cannot be stored.`,
2828
+ 409,
2829
+ status,
2830
+ status.paymentId
2831
+ );
2832
+ }
2833
+ const txHash = status.txHash;
2834
+ if (input.outcome === "rejected") {
2835
+ await this.post("/machine-payments/reconciliation-events", {
2836
+ paymentId: status.paymentId,
2837
+ rail: "x402",
2838
+ eventType: "merchant_retry_rejected_after_payment",
2839
+ txHash,
2840
+ reason: `Agent-reported: merchant returned HTTP ${input.merchantStatus} to a manual retry after Haven payment confirmation`,
2841
+ details: {
2842
+ resource_url: resourceUrl,
2843
+ retry_status: input.merchantStatus,
2844
+ retry_body: input.merchantBody?.slice(0, MERCHANT_BODY_SNIPPET_LIMIT) || null,
2845
+ reported_by: "agent_manual_retry"
2846
+ }
2847
+ });
2848
+ return { paymentId: status.paymentId, outcome: "rejected", txHash, resourceUrl, recorded: "reconciliation_event" };
2849
+ }
2850
+ await this.post("/machine-payments/evidence", {
2851
+ paymentId: status.paymentId,
2852
+ rail: "x402",
2853
+ txHash,
2854
+ resourceUrl,
2855
+ merchantStatus: input.merchantStatus
2856
+ });
2857
+ return { paymentId: status.paymentId, outcome: "accepted", txHash, resourceUrl, recorded: "evidence" };
2858
+ }
2859
+ async reportEvidence(input) {
2860
+ const body = {
2861
+ paymentId: input.paymentId,
2862
+ rail: input.rail,
2863
+ txHash: input.txHash,
2864
+ resourceUrl: input.resourceUrl,
2865
+ merchantStatus: input.merchantStatus,
2866
+ challengePayload: input.challengePayload,
2867
+ selectedPayment: input.selectedPayment,
2868
+ paymentProofHeaderName: input.paymentProofHeaderName,
2869
+ paymentProofHeader: input.paymentProofHeader,
2870
+ protocolReceiptHeaderName: input.protocolReceiptHeaderName,
2871
+ protocolReceiptHeader: input.protocolReceiptHeader,
2872
+ protocolReceiptPayload: input.protocolReceiptHeader ? parseProtocolReceiptHeader(input.protocolReceiptHeader) : void 0
2873
+ };
2874
+ for (let attempt = 0; ; attempt += 1) {
2875
+ try {
2876
+ await this.post("/machine-payments/evidence", body);
2877
+ return;
2878
+ } catch (err) {
2879
+ const retryable = err instanceof HavenApiError && err.statusCode === EVIDENCE_RETRYABLE_STATUS;
2880
+ if (!retryable || attempt >= EVIDENCE_RETRY_DELAYS_MS.length) return;
2881
+ await this.sleep(EVIDENCE_RETRY_DELAYS_MS[attempt]);
2882
+ }
2883
+ }
2884
+ }
2885
+ };
2886
+ function parseMerchantSettlement(header) {
2887
+ if (!header) return {};
2888
+ const parsed = parseProtocolReceiptHeader(header);
2889
+ const tx = typeof parsed?.transaction === "string" ? parsed.transaction : typeof parsed?.txHash === "string" ? parsed.txHash : typeof parsed?.tx_hash === "string" ? parsed.tx_hash : null;
2890
+ return { settlementTxHash: tx };
2891
+ }
2892
+ function parseProtocolReceiptHeader(value) {
2893
+ try {
2894
+ return decodeBase64Json(value);
2895
+ } catch {
2896
+ try {
2897
+ return JSON.parse(value);
2898
+ } catch {
2899
+ return void 0;
2900
+ }
2901
+ }
2902
+ }
2903
+
2904
+ // src/client.ts
2905
+ var DEFAULT_POLLING_INTERVAL = 3e3;
2906
+ function x402TypedDataDigest(typedData) {
2907
+ if (!typedData || typeof typedData !== "object") return void 0;
2908
+ try {
2909
+ return viem.hashTypedData(typedData);
2910
+ } catch (err) {
2911
+ throw new HavenSigningError(
2912
+ `The x402 funding intent carried a sign_data.typed_data that is not a valid EIP-712 payload (needs domain, types, primaryType, message), so its digest cannot be derived and no signer could accept it. Underlying error: ${err instanceof Error ? err.message : String(err)}`
2913
+ );
2914
+ }
2915
+ }
2916
+ function mapCatalogEntry(entry) {
2917
+ return {
2918
+ id: entry.id,
2919
+ name: entry.name,
2920
+ description: entry.description,
2921
+ category: entry.category,
2922
+ resourceUrl: entry.resource_url,
2923
+ rail: entry.rail,
2924
+ protocol: entry.protocol,
2925
+ toolName: entry.tool_name,
2926
+ toolArguments: entry.tool_arguments ?? null,
2927
+ priceDisplay: entry.price_display,
2928
+ priceAtomic: entry.price_atomic,
2929
+ asset: entry.asset,
2930
+ network: entry.network,
2931
+ status: entry.status,
2932
+ verifiedAt: entry.verified_at,
2933
+ source: entry.source,
2934
+ domainVerified: entry.domain_verified,
2935
+ verifiedPayable: entry.verified_payable
2936
+ };
2937
+ }
2938
+ var HavenClient = class {
2939
+ delegateKey;
2940
+ havenApi;
2941
+ accountReads;
2942
+ delegateSweep;
2943
+ x402Wallet;
2944
+ merchantTransport;
2945
+ confirmationTimeout;
2946
+ pollingInterval;
2947
+ chainRpcs;
2948
+ inFlightX402 = /* @__PURE__ */ new Map();
2949
+ /**
2950
+ * The EIP-3009 funding-leg lifecycle (#1618). The facade holds a reference
2951
+ * and delegates; it does not reimplement any of it.
2952
+ */
2953
+ fundingLeg;
2954
+ /**
2955
+ * The erc7710 direct-settlement lifecycle (#1619). Separate from the funding
2956
+ * leg on purpose: this scheme has no funding leg to share.
2957
+ */
2958
+ erc7710;
2959
+ /**
2960
+ * Merchant delivery and the evidence trail behind it (#1620). Scheme-neutral
2961
+ * on purpose — both settlement schemes finish through the same door.
2962
+ */
2963
+ merchantCompletion;
2964
+ /** Delegate address derived from the private key (if provided) */
2965
+ delegateAddress;
2966
+ constructor(config) {
2967
+ this.delegateKey = config.delegateKey;
2968
+ this.havenApi = new HavenApiTransport(config);
2969
+ this.accountReads = new AccountReads({
2970
+ transport: this.havenApi,
2971
+ getPaymentStatus: (paymentId) => this.getPaymentStatus(paymentId)
2972
+ });
2973
+ this.delegateSweep = new DelegateSweepApi({
2974
+ transport: this.havenApi,
2975
+ delegateKey: config.delegateKey,
2976
+ chainRpcs: config.chainRpcs ?? {},
2977
+ getAgent: () => this.getAgent(),
2978
+ buildExplorerUrl: (chainId, hash) => buildExplorerUrl(chainId, hash)
2979
+ });
2980
+ this.x402Wallet = config.x402Wallet;
2981
+ this.merchantTransport = new McpMerchantTransport({ merchantTimeout: config.merchantTimeout });
2982
+ this.confirmationTimeout = config.confirmationTimeout ?? DEFAULT_CONFIRMATION_TIMEOUT_MS;
2983
+ this.pollingInterval = config.pollingInterval ?? DEFAULT_POLLING_INTERVAL;
2984
+ this.chainRpcs = config.chainRpcs ?? {};
2985
+ if (this.delegateKey) {
2986
+ this.delegateAddress = addressFromKey(this.delegateKey);
2987
+ }
2988
+ this.fundingLeg = new X402FundingLeg({
2989
+ delegateKey: this.delegateKey,
2990
+ delegateAddress: this.delegateAddress,
2991
+ x402Wallet: this.x402Wallet,
2992
+ chainRpcs: this.chainRpcs,
2993
+ post: (path, body) => this.post(path, body),
2994
+ signForData: (signData) => this.signForData(signData),
2995
+ assertSignableAuthorizationState: (label, raw) => this.throwIfNonSignableAuthorizationState(label, raw)
2996
+ });
2997
+ this.merchantCompletion = new MerchantCompletion({
2998
+ post: (path, body) => this.post(path, body),
2999
+ merchantTransport: this.merchantTransport,
3000
+ getPaymentStatus: (paymentId) => this.getPaymentStatus(paymentId),
3001
+ getAgent: () => this.getAgent(),
3002
+ delegateAddress: this.delegateAddress,
3003
+ x402Wallet: this.x402Wallet
3004
+ });
3005
+ this.erc7710 = new X402Erc7710({
3006
+ delegateKey: this.delegateKey,
3007
+ post: (path, body) => this.post(path, body),
3008
+ signForData: (signData) => this.signForData(signData),
3009
+ getAgent: () => this.getAgent()
3010
+ });
3011
+ }
3012
+ /**
3013
+ * Run `fn` with extra Haven-API headers scoped to the async work it
3014
+ * performs. Used by the MCP server to tag every Haven API request that
3015
+ * a single tool dispatch makes with `X-Haven-MCP-Tool: <name>` so the
3016
+ * backend can write an audit-log row attributing the call.
3017
+ *
3018
+ * The headers are held in an `AsyncLocalStorage` so overlapping
3019
+ * dispatches do not leak headers into each other's requests. The store
3020
+ * inherits across `await` boundaries, so any Haven API call made while
3021
+ * `fn` is awaiting will pick up the right headers.
3022
+ *
3023
+ * Has no effect on outbound merchant requests (x402 / MPP) — those
3024
+ * never go through the internal `request<T>` path that reads the
3025
+ * context.
3026
+ */
3027
+ withRequestContext(headers, fn) {
3028
+ return this.havenApi.withRequestContext(headers, fn);
3029
+ }
3030
+ // ── High-Level API ───────────────────────────────────────────────
3031
+ /**
3032
+ * Send a payment in one call.
3033
+ *
3034
+ * Creates the intent, signs the hash, submits the signature,
3035
+ * and polls until confirmed (or throws on failure/timeout).
3036
+ *
3037
+ * Requires `delegateKey` to be set in the client config.
3038
+ */
3039
+ async pay(request) {
3040
+ if (!this.delegateKey) {
3041
+ throw new HavenSigningError(
3042
+ "Cannot use pay() without a delegateKey. Use createIntent() + submitSignature() for manual signing."
3043
+ );
3044
+ }
3045
+ const intent = await this.createIntent(request);
3046
+ const signature = this.sign(intent.signData.hash);
3047
+ await this.submitSignature(intent.paymentId, signature);
3048
+ return this.waitForConfirmation(intent.paymentId);
3049
+ }
3050
+ // ── Step-by-Step API ─────────────────────────────────────────────
3051
+ /**
3052
+ * Step 1: Create a payment intent.
3053
+ *
3054
+ * Returns the intent with the hash to sign.
3055
+ */
3056
+ async createIntent(request) {
3057
+ const raw = await this.post("/payments", {
3058
+ token: request.token,
3059
+ amount: request.amount,
3060
+ to: request.to,
3061
+ ...request.idempotencyKey ? { idempotency_key: request.idempotencyKey } : {}
3062
+ });
3063
+ if (raw.status === "pending_approval") {
3064
+ throwPaymentStateError("Payment", raw);
3065
+ }
3066
+ return {
3067
+ paymentId: raw.payment_id,
3068
+ status: "pending_signature",
3069
+ expiresAt: raw.expires_at,
3070
+ signData: raw.sign_data
3071
+ };
3072
+ }
3073
+ /**
3074
+ * Keyless x402 construct.
3075
+ *
3076
+ * The non-custodial half of an x402 payment: posts the funding request to
3077
+ * `/x402` and returns the unsigned funding hash plus the data the caller
3078
+ * needs to build and sign the EIP-3009 merchant header itself. Crucially it
3079
+ * does **not** sign — neither the funding hash nor the merchant header — so
3080
+ * it works without a `delegateKey`. Both delegate signatures happen on the
3081
+ * machine that holds the key (the edge); the hosted MCP server relays only.
3082
+ *
3083
+ * Use this from the hosted, keyless server. The all-in-one `authorizeX402`
3084
+ * remains for local clients that hold the key.
3085
+ *
3086
+ * Throws (via the shared payment-state path) when the amount exceeds the
3087
+ * on-chain allowance — there is nothing to sign until the user approves.
3088
+ */
3089
+ async createX402Intent(paymentRequired, options = {}) {
3090
+ const option = selectStandardPaymentOption(paymentRequired.accepts);
3091
+ if (!option) {
3092
+ throw noCompatiblePaymentOptionError(paymentRequired.accepts);
3093
+ }
3094
+ const fundingTo = options.delegateAddress ?? (await this.getAgent()).delegateAddress;
3095
+ if (!fundingTo) {
3096
+ throw new HavenApiError("Authenticated agent has no delegate address registered.", 502);
3097
+ }
3098
+ const idempotencyKey = options.idempotencyKey ?? buildX402IdempotencyKey(paymentRequired, option);
3099
+ const raw = await this.post("/x402", {
3100
+ url: paymentRequired.resource.url,
3101
+ payTo: fundingTo,
3102
+ merchantPayTo: option.payTo,
3103
+ // #1360: this path ALWAYS means the EIP-3009 funding leg (payTo is the
3104
+ // agent's own delegate EOA). Saying so explicitly turns a stale/rotated
3105
+ // delegate address into the backend's LOUD shape-mismatch 400 instead
3106
+ // of a silent reroute to the erc7710 settlement branch (the #1358
3107
+ // review's open-budget misroute). Legacy-rail backends ignore the field.
3108
+ settlementScheme: "eip3009",
3109
+ amount: x402AuthorizationAmount(option),
3110
+ asset: option.asset,
3111
+ network: option.network,
3112
+ description: paymentRequired.resource.description,
3113
+ idempotencyKey,
3114
+ // #1307: persisted so the settle leg can rehydrate it by payment_id.
3115
+ ...options.mcpCallContext ? { mcpCallContext: options.mcpCallContext } : {},
3116
+ // #1355: persisted so the SIGN leg can rehydrate it by payment_id — the
3117
+ // local signer's context fetch then carries the 402 PaymentRequired and
3118
+ // the agent passes only payment_id. Bounded: the backend rejects >64KB,
3119
+ // so an oversized blob is omitted here (signer falls back to the
3120
+ // caller-supplied copy) rather than failing the intent.
3121
+ ...new TextEncoder().encode(JSON.stringify(paymentRequired)).length <= 65536 ? { paymentRequired } : {}
3122
+ });
3123
+ if (raw.status !== "pending_signature") {
3124
+ throwPaymentStateError("x402 payment", raw);
3125
+ }
3126
+ if (!raw.sign_data?.hash) {
3127
+ throw new HavenApiError("No sign_hash returned from x402/authorize", 500, raw);
3128
+ }
3129
+ if (raw.sign_data.signature_scheme !== void 0 && !raw.sign_data.typed_data) {
3130
+ throw new HavenSigningError(
3131
+ `This account's x402 funding intent declares signature scheme '${raw.sign_data.signature_scheme}' but carried no typed_data to sign. Refusing to fall back to the bare hash \u2014 the account would reject that signature on-chain. This is a backend contract violation; report it rather than working around it.`
3132
+ );
3133
+ }
3134
+ if (!raw.x402_expected_auth) {
3135
+ throw new HavenApiError("No x402 expected-context binding returned from x402/authorize", 500, raw);
3136
+ }
3137
+ return {
3138
+ paymentId: raw.payment_id,
3139
+ idempotencyKey,
3140
+ status: "pending_signature",
3141
+ expiresAt: raw.expires_at,
3142
+ signData: raw.sign_data,
3143
+ accepted: option,
3144
+ resourceUrl: paymentRequired.resource.url,
3145
+ merchantTo: raw.merchant_to ?? option.payTo,
3146
+ amountAtomic: x402AuthorizationAmount(option),
3147
+ asset: option.asset,
3148
+ network: option.network,
3149
+ expectedAuth: raw.x402_expected_auth,
3150
+ payerDelegate: raw.payer_delegate,
3151
+ payerAgentId: raw.payer_agent_id,
3152
+ // #1138: the digest the delegation-rail expected context commits to.
3153
+ // Re-derived locally, exactly like every other context field the edge
3154
+ // signer is handed (amount, merchantTo, …) — none of them are trusted
3155
+ // because they arrived, they are trusted because the reconstructed
3156
+ // message has to match Haven's signature over it. A typed_data altered in
3157
+ // transit therefore fails message equality and is refused, and the signer
3158
+ // re-derives this digest a second time from the payload it actually signs.
3159
+ expectedTypedDataHash: x402TypedDataDigest(raw.sign_data.typed_data),
3160
+ fundingTo
3161
+ };
3162
+ }
3163
+ /**
3164
+ * Step 2: Sign a hash with the delegate key.
3165
+ *
3166
+ * Returns the 65-byte signature (0x-prefixed).
3167
+ * Requires `delegateKey` to be set in the client config.
3168
+ */
3169
+ sign(hash) {
3170
+ if (!this.delegateKey) {
3171
+ throw new HavenSigningError(
3172
+ "Cannot sign without a delegateKey. Pass the private key in HavenClient config, or sign externally."
3173
+ );
3174
+ }
3175
+ const signature = signHash(this.delegateKey, hash);
3176
+ if (!verifySignature(hash, signature, this.delegateAddress)) {
3177
+ throw new HavenSigningError(
3178
+ "Local signature verification failed \u2014 recovered address does not match delegate key."
3179
+ );
3180
+ }
3181
+ return signature;
3182
+ }
3183
+ /**
3184
+ * Sign a payment's `sign_data` with the correct scheme for its rail.
3185
+ *
3186
+ * Dispatching on the server-provided scheme means a caller never has to
3187
+ * know which rail an account is on; an unknown scheme is a hard error,
3188
+ * never a guessed signature. The session rail's 'eip191_userop' is retired
3189
+ * (#834) — the backend refuses those intents with HTTP 410 before any
3190
+ * sign_data reaches a client, so encountering it here is a hard error too.
3191
+ */
3192
+ async signForData(signData) {
3193
+ if (!this.delegateKey) {
3194
+ throw new HavenSigningError(
3195
+ "Cannot sign without a delegateKey. Pass the private key in HavenClient config, or sign externally."
3196
+ );
3197
+ }
3198
+ const scheme = signData.signature_scheme;
3199
+ if (scheme === "eip191_userop") {
3200
+ throw new HavenSigningError(
3201
+ "The session rail is retired \u2014 'eip191_userop' intents can no longer be signed. Re-onboard the account on the delegation rail."
3202
+ );
3203
+ }
3204
+ if (scheme === "eip712_userop") {
3205
+ if (!signData.typed_data) {
3206
+ throw new HavenSigningError(
3207
+ "sign_data.signature_scheme is eip712_userop but typed_data is missing \u2014 refusing to sign the bare hash (the account would reject it)."
3208
+ );
3209
+ }
3210
+ return signUserOpTypedDataForDelegation(this.delegateKey, signData.typed_data);
3211
+ }
3212
+ if (scheme === "eip712_delegation") {
3213
+ if (!signData.typed_data) {
3214
+ throw new HavenSigningError(
3215
+ "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)."
3216
+ );
3217
+ }
3218
+ return signSettlementDelegationTypedData(this.delegateKey, signData.typed_data);
3219
+ }
3220
+ if (scheme === void 0) {
3221
+ return signHash(this.delegateKey, signData.hash);
3222
+ }
3223
+ throw new HavenSigningError(
3224
+ `Unknown sign_data.signature_scheme '${scheme}' \u2014 refusing to guess a signing scheme. Update @haven_ai/sdk.`
3225
+ );
3226
+ }
3227
+ /**
3228
+ * Step 3: Submit a signature to execute the payment.
3229
+ *
3230
+ * The signature can come from `client.sign()` or from external signing.
3231
+ */
3232
+ async submitSignature(paymentId, signature) {
3233
+ const raw = await this.post(
3234
+ `/payments/${paymentId}/sign`,
3235
+ { signature }
3236
+ );
3237
+ return {
3238
+ status: raw.status,
3239
+ txHash: raw.tx_hash
3240
+ };
3241
+ }
3242
+ /**
3243
+ * Get the current status of a payment.
3244
+ */
3245
+ async getPayment(paymentId) {
3246
+ const raw = await this.get(`/payments/${paymentId}`);
3247
+ return mapPaymentResult(raw, buildExplorerUrl);
3248
+ }
3249
+ /**
3250
+ * Get agent-actionable status for a payment intent or approval request.
3251
+ *
3252
+ * Use this for IDs returned by agent tools and machine-payment/x402 flows.
3253
+ * `getPayment()` remains available for payment-intent-only integrations.
3254
+ */
3255
+ async getPaymentStatus(paymentId) {
3256
+ const raw = await this.get(`/machine-payments/${paymentId}/status`);
3257
+ return mapPaymentStatusResult(raw);
3258
+ }
3259
+ /**
3260
+ * Get the agent identity tied to this API key.
3261
+ */
3262
+ async getAgent() {
3263
+ return this.accountReads.getAgent();
3264
+ }
3265
+ /**
3266
+ * One-shot "am I ready?" bootstrap: identity + live spend authority + a
3267
+ * readiness signal, in a single call. Folds {@link getAgent} and
3268
+ * {@link getAllowances} together and derives a {@link HavenAgentReadiness}
3269
+ * so an agent can answer "who am I and can I pay right now" at session start
3270
+ * without two round trips and manual assembly.
3271
+ */
3272
+ async getAgentSummary() {
3273
+ return this.accountReads.getAgentSummary();
3274
+ }
3275
+ /**
3276
+ * Sweep stranded USDC and ETH from the delegate EOA back to the originating Safe.
3277
+ *
3278
+ * The delegate key held by this client signs and submits the transfer transactions
3279
+ * directly — Haven's backend never handles the key or constructs signed txs
3280
+ * (CASP/MiCA Red Line #2). Funds always go to the Safe linked to this agent.
3281
+ *
3282
+ * Requires `chainRpcs` to be set for the agent's chain in `HavenClientConfig`.
3283
+ */
3284
+ async sweepDelegate() {
3285
+ return this.delegateSweep.sweepDelegate();
3286
+ }
3287
+ /**
3288
+ * Hosted (keyless) split-signer sweep — step 1 of 2.
3289
+ *
3290
+ * Asks the backend to build a gasless EIP-3009 sweep authorization for the
3291
+ * delegate's stranded USDC. Returns `nothing_stranded` when the delegate is
3292
+ * empty, otherwise an `authorization` + Haven `expected_auth` to hand to the
3293
+ * edge signer's `haven_sign_sweep_delegate`. No key is required on this client.
3294
+ */
3295
+ async prepareSweep() {
3296
+ return this.delegateSweep.prepareSweep();
3297
+ }
3298
+ /**
3299
+ * Hosted (keyless) split-signer sweep — step 2 of 2.
3300
+ *
3301
+ * Relays the delegate-signed authorization. The Haven relayer submits the
3302
+ * on-chain `transferWithAuthorization` and pays gas; this client never holds
3303
+ * the key.
3304
+ */
3305
+ async submitSweep(authorization, signature) {
3306
+ return this.delegateSweep.submitSweep(authorization, signature);
3307
+ }
3308
+ /**
3309
+ * Get configured and on-chain allowances for the authenticated agent.
3310
+ */
3311
+ async getAllowances() {
3312
+ return this.accountReads.getAllowances();
3313
+ }
3314
+ /**
3315
+ * Post-purchase allowance/budget summary for a settled payment (#1310).
3316
+ *
3317
+ * Reuses the EXACT rail-aware read path {@link getAllowances} / #1306's
3318
+ * catalog-purchase preflight `allowance` block use — `GET
3319
+ * /machine-payments/allowances`, with delegation-rail values coming from
3320
+ * the #1090 `deriveDelegationBudgets`-backed enforcer read, never
3321
+ * `agent_allowances` — so this can never disagree with
3322
+ * {@link getAllowances} for the same fixture. The settled token is
3323
+ * resolved from {@link getPaymentStatus} so callers pass only
3324
+ * `paymentId`, never a second haven_get_agent-style round trip.
3325
+ *
3326
+ * NEVER throws: any failed read (status lookup, agent lookup, or the
3327
+ * allowance/budget lookup itself) degrades to `{ allowance: null,
3328
+ * warnings: [ALLOWANCE_CHECK_UNAVAILABLE] }` rather than converting a
3329
+ * successful settlement into a failure — the on-chain policy remains the
3330
+ * actual spend gate regardless of whether this report can be produced.
3331
+ *
3332
+ * Freshness caveat (#1319): the delegation rail's on-chain enforcer read
3333
+ * can silently fall back to the optimistic full period budget without
3334
+ * throwing when the RPC read itself fails (#1145's fund-safe design,
3335
+ * unchanged here). {@link getAllowances}'s `onchain.remainingIsFromChain`
3336
+ * now carries that provenance on the wire, and the #1306 catalog-purchase
3337
+ * preflight (`haven_prepare_catalog_purchase`) surfaces it as a warning —
3338
+ * this summary does not (yet). `remaining_atomic` here reflects the last
3339
+ * successful chain read, not a guaranteed-live one, and callers should not
3340
+ * phrase it as guaranteed-fresh.
3341
+ */
3342
+ async getPostPurchaseAllowanceSummary(paymentId) {
3343
+ return this.accountReads.getPostPurchaseAllowanceSummary(paymentId);
3344
+ }
3345
+ /**
3346
+ * `haven_get_payment_status` convenience: fetch status and, for a
3347
+ * genuinely SETTLED x402 payment, attach the same post-purchase
3348
+ * allowance/budget summary a settle response carries.
3349
+ *
3350
+ * #1310/#1311 parity: this is the ONE home for logic that was duplicated
3351
+ * verbatim in `packages/mcp-server/src/tools.ts` and `packages/mcp/src/tools.ts`
3352
+ * (both hosted and local `haven_get_payment_status` handlers) — extracted
3353
+ * here because both packages already depend on `@haven_ai/sdk` and call
3354
+ * methods on a `HavenClient` instance, so this needed no new dependency
3355
+ * edge. `funded_but_unsettled` is deliberately excluded: that phase means
3356
+ * the merchant did NOT accept the retry. Every other phase/rail returns
3357
+ * the status untouched.
3358
+ */
3359
+ async getPaymentStatusWithPostPurchaseAllowance(paymentId) {
3360
+ const status = await this.getPaymentStatus(paymentId);
3361
+ if (status.rail === AgentPaymentRail.X402 && status.phase === AgentPaymentPhase.PaymentConfirmed) {
3362
+ const { allowance, warnings } = await this.getPostPurchaseAllowanceSummary(paymentId);
3363
+ return { ...status, allowance, ...warnings.length > 0 ? { warnings } : {} };
3364
+ }
3365
+ return status;
3366
+ }
3367
+ /**
3368
+ * Discover payable services from Haven's merchant catalog (epic #1717).
3369
+ *
3370
+ * Read-only: returns catalog entries (price, rail, protocol) so an agent
3371
+ * can choose a service and pay it with the regular payment tools in the
3372
+ * same session. Never creates payments or signatures.
3373
+ */
3374
+ async discoverTools(options = {}) {
3375
+ const params = new URLSearchParams();
3376
+ if (options.category) params.set("category", options.category);
3377
+ if (options.search !== void 0) params.set("search", options.search);
3378
+ if (options.rail) params.set("rail", options.rail);
3379
+ const query = params.size > 0 ? `?${params.toString()}` : "";
3380
+ const raw = await this.get(`/catalog${query}`);
3381
+ let entries = raw.entries.map(mapCatalogEntry);
3382
+ if (options.verified === "verified") entries = entries.filter((e) => e.source === "ingestion");
3383
+ if (options.verified === "operator") entries = entries.filter((e) => e.source === "operator");
3384
+ return entries;
3385
+ }
3386
+ /**
3387
+ * Submit a merchant's payable (x402/MCP) endpoint to the Verified Payable
3388
+ * Directory (epic #1717, #1716). Queue-only: writes a submission row and
3389
+ * returns the id + verify_token. The request path makes no outbound
3390
+ * request; domain-ownership proof and the read-only quote probe run later
3391
+ * on the leader-locked monitor. Ownership proof is ALWAYS required before
3392
+ * any listing — this method cannot skip it. `website` is a honeypot field
3393
+ * that bots fill; leave it unset.
3394
+ */
3395
+ async submitCatalogEntry(resourceUrl, options = {}) {
3396
+ const accepted = await this.post("/catalog/submit", {
3397
+ resource_url: resourceUrl,
3398
+ ...options.website ? { website: options.website } : {}
3399
+ });
3400
+ return {
3401
+ id: accepted.id,
3402
+ verifyToken: accepted.verify_token,
3403
+ status: accepted.status
3404
+ };
3405
+ }
3406
+ /**
3407
+ * Fetch one submission's coarse status by id (epic #1717, #1716). Public
3408
+ * and read-only. While the submission can still prove ownership the
3409
+ * response carries the exact well-known / DNS-TXT `instructions`; the
3410
+ * verify token is never returned here.
3411
+ */
3412
+ async getCatalogSubmissionStatus(id) {
3413
+ return this.get(`/catalog/submit/${encodeURIComponent(id)}`);
3414
+ }
3415
+ /**
3416
+ * Fetch one curated catalog entry by id (#1306).
3417
+ *
3418
+ * Chain-scoped for free by the backend's SQL when the client is
3419
+ * agent-authenticated (#1299): an unknown id and an id curated for a
3420
+ * DIFFERENT chain than this agent's both 404 identically — this method does
3421
+ * not (and must not) re-filter by chain in JS. Read-only, like
3422
+ * {@link discoverTools}.
3423
+ */
3424
+ async getCatalogEntry(id) {
3425
+ const raw = await this.get(`/catalog/${encodeURIComponent(id)}`);
3426
+ return mapCatalogEntry(raw);
3427
+ }
3428
+ /**
3429
+ * List recent machine-payment receipts/evidence for bookkeeping.
3430
+ */
3431
+ async listReceipts(options = {}) {
3432
+ return this.accountReads.listReceipts(options);
3433
+ }
3434
+ /**
3435
+ * Fetch the verifiable receipt bundle for a settled payment and verify it
3436
+ * locally. The server's own verification is ignored — the receipt is verified
3437
+ * here (independently of Haven) by recovering the signer from the
3438
+ * authorisation, so the result is trustworthy even if the backend lied.
3439
+ */
3440
+ async getReceipt(paymentId) {
3441
+ return this.accountReads.getReceipt(paymentId);
3442
+ }
3443
+ /**
3444
+ * Rehydrate the x402 resume-state bundle for a payment id (#1328: the MPP
3445
+ * resume-state variant retired along with the rest of the mpp_demo surface).
3446
+ *
3447
+ * The server returns stored protocol context only. The client still signs the
3448
+ * merchant proof locally when resumeX402Payment() runs.
3449
+ */
3450
+ async getResumeState(paymentId) {
3451
+ return this.get(`/payments/${paymentId}/resume_state`);
3452
+ }
3453
+ /**
3454
+ * Poll until a payment reaches a terminal status (confirmed, failed, expired).
3455
+ */
3456
+ async waitForConfirmation(paymentId) {
3457
+ const deadline = Date.now() + this.confirmationTimeout;
3458
+ while (Date.now() < deadline) {
3459
+ const result = await this.getPayment(paymentId);
3460
+ if (result.status === "confirmed" || result.status === "failed" || result.status === "expired") {
3461
+ return result;
3462
+ }
3463
+ await sleep(this.pollingInterval);
3464
+ }
3465
+ throw new HavenTimeoutError(paymentId);
3466
+ }
3467
+ // ── x402 Protocol Support ────────────────────────────────────────
3468
+ /**
3469
+ * Authorize an x402 payment.
3470
+ *
3471
+ * Takes the parsed PaymentRequired from a 402 response, selects a compatible
3472
+ * option, funds the delegate wallet through Haven, and returns the standard
3473
+ * x402 header that the merchant can verify and settle.
3474
+ *
3475
+ * Requires `delegateKey` to be set in the client config.
3476
+ */
3477
+ async authorizeX402(paymentRequired, options = {}) {
3478
+ if (!this.delegateKey) {
3479
+ throw new HavenSigningError(
3480
+ "delegateKey is required for x402 payments. Pass it in the HavenClient config."
3481
+ );
3482
+ }
3483
+ if (!this.delegateAddress) {
3484
+ throw new HavenSigningError("delegateAddress could not be derived from delegateKey.");
3485
+ }
3486
+ const option = selectStandardPaymentOption(paymentRequired.accepts);
3487
+ if (!option) {
3488
+ throw noCompatiblePaymentOptionError(paymentRequired.accepts);
3489
+ }
3490
+ const idempotencyKey = options.idempotencyKey ?? buildX402IdempotencyKey(paymentRequired, option);
3491
+ const cached = this.fundingLeg.cachedReceipt(idempotencyKey);
3492
+ if (cached) return cached;
3493
+ const inFlight = this.inFlightX402.get(idempotencyKey);
3494
+ if (inFlight) return inFlight;
3495
+ const promise = this.fundingLeg.authorize(paymentRequired, option, idempotencyKey);
3496
+ this.inFlightX402.set(idempotencyKey, promise);
3497
+ try {
3498
+ return await promise;
3499
+ } catch (err) {
3500
+ attachResumeState(err, {
3501
+ paymentRequired,
3502
+ accepted: option,
3503
+ idempotencyKey
3504
+ });
3505
+ throw err;
3506
+ } finally {
3507
+ this.inFlightX402.delete(idempotencyKey);
3508
+ }
3509
+ }
3510
+ /**
3511
+ * Probe a paid endpoint and return its x402 quote without creating a Haven
3512
+ * payment or approval request.
3513
+ */
3514
+ async quoteX402(url, init, options = {}) {
3515
+ const initialInit = withX402Wallet(init, x402PayerAddress(this.delegateAddress, this.x402Wallet));
3516
+ const request = snapshotX402Request(url, initialInit);
3517
+ const response = await this.merchantTransport.fetch(url, initialInit);
3518
+ if (response.status !== 402) {
3519
+ throw new X402UnexpectedStatusError(
3520
+ `Expected an x402 quote response with HTTP 402, got HTTP ${response.status}.`,
3521
+ response.status || 400
3522
+ );
3523
+ }
3524
+ if (response.headers.get("MACHINE-PAYMENT-CHALLENGE")) {
3525
+ throw new HavenApiError("quoteX402 only supports standard x402 Payment Required responses.", 400);
3526
+ }
3527
+ const paymentRequired = await parsePaymentRequiredResponse(response);
3528
+ const mcpTransport = await this.merchantTransport.detect(url, paymentRequired, response);
3529
+ return buildX402Quote(paymentRequired, request, options.idempotencyKey, mcpTransport);
3530
+ }
3531
+ /**
3532
+ * Probe an MCP tool for its x402 quote without creating a payment.
3533
+ *
3534
+ * Unlike the generic {@link quoteX402} helper, this completes the
3535
+ * Streamable-HTTP MCP lifecycle before sending the unpaid `tools/call`.
3536
+ * Hosted MCP uses this path while remaining keyless: it resolves only the
3537
+ * agent's public delegate address for `x402-wallet`; signing remains local.
3538
+ * It refuses before the quote when the merchant does not establish a session;
3539
+ * callers that need a plain x402 endpoint must use {@link quoteX402}.
3540
+ */
3541
+ async quoteMcpX402(url, init, options = {}) {
3542
+ const wallet = await this.merchantCompletion.resolveWalletForMerchantCall();
3543
+ const sessionId = await this.merchantTransport.initialize(url, init, wallet);
3544
+ if (!sessionId) {
3545
+ throw new HavenApiError(
3546
+ "The merchant did not establish an MCP session before the x402 quote. No payment was created.",
3547
+ 502,
3548
+ { mcpSessionNotEstablished: true }
3549
+ );
3550
+ }
3551
+ let requestInit = withX402Wallet(init, wallet);
3552
+ requestInit = this.merchantTransport.withSessionHeaders(requestInit, sessionId);
3553
+ const quote = await this.quoteX402(url, requestInit, options);
3554
+ return {
3555
+ ...quote,
3556
+ mcpTransport: quote.mcpTransport ?? { handshakeRequired: true, source: "path" }
3557
+ };
3558
+ }
3559
+ /**
3560
+ * Pay a previously inspected x402 quote and retry the exact captured request.
3561
+ */
3562
+ async payX402Quote(quote, options = {}) {
3563
+ const idempotencyKey = options.idempotencyKey ?? quote.idempotencyKey;
3564
+ try {
3565
+ const receipt = await this.authorizeX402(quote.paymentRequired, { idempotencyKey });
3566
+ return this.merchantCompletion.retryRequest(
3567
+ quote.request.url,
3568
+ requestInitFromSnapshot(quote.request),
3569
+ quote.paymentRequired,
3570
+ receipt
3571
+ );
3572
+ } catch (err) {
3573
+ attachResumeState(err, {
3574
+ paymentRequired: quote.paymentRequired,
3575
+ accepted: quote.accepted,
3576
+ idempotencyKey,
3577
+ request: quote.request
3578
+ });
3579
+ throw err;
3580
+ }
3581
+ }
3582
+ /**
3583
+ * Pay a merchant through **erc7710 direct settlement** (#1454, epic #1450).
3584
+ *
3585
+ * **Nothing has settled when this returns** — that is why it does not return
3586
+ * an `X402Receipt`; the caller still has to retry the merchant with the
3587
+ * header. **MCP callers must pass `options.resourceUrl`**, because an in-band
3588
+ * MCP 402 challenge frequently carries no `resource` object at all.
3589
+ *
3590
+ * Both caveats, and why this scheme has no funding leg, are explained where
3591
+ * the lifecycle lives: `x402-erc7710.ts` (#1619).
3592
+ */
3593
+ async settleX402Erc7710(paymentRequired, options = {}) {
3594
+ return this.erc7710.settle(paymentRequired, options);
3595
+ }
3596
+ /**
3597
+ * The AUTHORIZE half of erc7710 settlement (#1456): select the scheme, build
3598
+ * the request, and return the child to be signed — without signing it.
3599
+ *
3600
+ * Split out because the hosted topology cannot use `settleX402Erc7710()`:
3601
+ * that method signs in-process with `delegateKey`, and hosted Haven does not
3602
+ * have one and must not.
3603
+ */
3604
+ async prepareX402Erc7710(paymentRequired, options = {}) {
3605
+ return this.erc7710.prepare(paymentRequired, options);
3606
+ }
3607
+ /**
3608
+ * The SETTLE half (#1456): exchange the signed child for the merchant header.
3609
+ *
3610
+ * The SDK builds no header on this path — the backend assembles the MetaMask
3611
+ * erc7710 payload. Whoever produced the signature (an in-process delegate
3612
+ * key, or the local edge signer over the hosted boundary) is irrelevant.
3613
+ */
3614
+ async submitX402Erc7710(paymentId, signature) {
3615
+ return this.erc7710.submit(paymentId, signature);
3616
+ }
3617
+ async resumeAuthorizedX402(input) {
3618
+ if (!this.delegateKey) {
3619
+ throw new HavenSigningError(
3620
+ "delegateKey is required for x402 payments. Pass it in the HavenClient config."
3621
+ );
3622
+ }
3623
+ if (!this.delegateAddress) {
3624
+ throw new HavenSigningError("delegateAddress could not be derived from delegateKey.");
3625
+ }
3626
+ const option = selectStandardPaymentOption(input.paymentRequired.accepts);
3627
+ if (!option) {
3628
+ throw noCompatiblePaymentOptionError(input.paymentRequired.accepts);
3629
+ }
3630
+ const idempotencyKey = input.idempotencyKey ?? buildX402IdempotencyKey(input.paymentRequired, option);
3631
+ const cached = this.fundingLeg.cachedReceipt(idempotencyKey);
3632
+ if (cached) return cached;
3633
+ const status = await this.getPaymentStatus(input.paymentId);
3634
+ assertCanResumeX402(status, input.paymentRequired, option);
3635
+ const canFund = await this.fundingLeg.delegateCanFund(
3636
+ status.chainId ?? chainIdFromNetwork(option.network),
3637
+ option.asset,
3638
+ x402AuthorizationAmount(option)
3639
+ );
3640
+ if (canFund === false) {
3641
+ throw new X402AlreadySettledError(
3642
+ `x402 payment ${status.paymentId} has already settled \u2014 the delegate no longer holds the funds to authorize it again, so there is nothing left to resume.`,
3643
+ this.fundingLeg.receiptFromStatus(input.paymentRequired, option, void 0, status),
3644
+ "settled"
3645
+ );
3646
+ }
3647
+ const paymentHeader = await this.fundingLeg.createPaymentHeader(input.paymentRequired, option);
3648
+ const receipt = this.fundingLeg.receiptFromStatus(input.paymentRequired, option, paymentHeader, status);
3649
+ this.fundingLeg.cacheReceipt(idempotencyKey, paymentHeader, receipt);
3650
+ return receipt;
3651
+ }
3652
+ async resumeX402Payment(input) {
3653
+ const inputInit = "init" in input ? input.init : void 0;
3654
+ const initialInit = withX402Wallet(
3655
+ inputInit ?? (input.request ? requestInitFromSnapshot(input.request) : void 0),
3656
+ x402PayerAddress(this.delegateAddress, this.x402Wallet)
3657
+ );
3658
+ let paymentRequired = input.paymentRequired;
3659
+ const url = input.url ?? input.request?.url;
3660
+ if (!paymentRequired) {
3661
+ if (!url) {
3662
+ throw new HavenApiError("x402 resume requires the original URL or a captured request snapshot.", 400);
3663
+ }
3664
+ const response = await this.merchantTransport.fetch(url, initialInit);
3665
+ if (response.status !== 402) {
3666
+ throw new HavenApiError("Expected the original x402 request to return HTTP 402 before resuming.", 400);
3667
+ }
3668
+ paymentRequired = await parsePaymentRequiredResponse(response);
3669
+ }
3670
+ const receipt = await this.resumeAuthorizedX402({
3671
+ paymentId: input.paymentId,
3672
+ paymentRequired,
3673
+ idempotencyKey: input.idempotencyKey
3674
+ });
3675
+ return this.merchantCompletion.retryRequest(url ?? paymentRequired.resource.url, initialInit, paymentRequired, receipt);
3676
+ }
3677
+ /**
3678
+ * Fetch wrapper that automatically handles HTTP 402 responses.
3679
+ *
3680
+ * Works like the standard `fetch()` but intercepts 402 responses,
3681
+ * pays via x402 through Haven, and retries the request.
3682
+ *
3683
+ * ```ts
3684
+ * const response = await haven.fetch('https://paid-api.com/data')
3685
+ * const data = await response.json()
3686
+ * ```
3687
+ *
3688
+ * **MCP-over-x402 auto-handshake (issue #315):** when the endpoint is
3689
+ * MCP-shaped — the URL path ends in `/mcp`, or the 402 body carries a
3690
+ * Coinbase Bazaar `extensions.bazaar` block — the SDK runs the MCP
3691
+ * `initialize` handshake, threads the resulting `mcp-session-id`,
3692
+ * `Accept: application/json, text/event-stream`, and `x402-wallet` headers
3693
+ * through every request, and collapses SSE responses to the JSON-RPC
3694
+ * `result`. The caller just passes `(url, { body })` and never sees the
3695
+ * protocol plumbing. A non-MCP server (handshake error / no session id)
3696
+ * falls back to standard x402 behaviour.
3697
+ *
3698
+ * Requires `delegateKey` to be set in the client config.
3699
+ */
3700
+ async fetch(url, init, options = {}) {
3701
+ let mcpSessionId;
3702
+ if (this.merchantTransport.isMcpUrl(url)) {
3703
+ mcpSessionId = await this.merchantTransport.initialize(url, init);
3704
+ }
3705
+ let requestInit = withX402Wallet(init, x402PayerAddress(this.delegateAddress, this.x402Wallet));
3706
+ if (mcpSessionId) requestInit = this.merchantTransport.withSessionHeaders(requestInit, mcpSessionId);
3707
+ const response = await this.merchantTransport.fetch(url, requestInit);
3708
+ if (response.status !== 402) {
3709
+ return mcpSessionId ? this.merchantTransport.surfaceResult(response) : response;
3710
+ }
3711
+ let paymentRequired;
3712
+ try {
3713
+ paymentRequired = await parsePaymentRequiredResponse(response);
3714
+ } catch {
3715
+ return response;
3716
+ }
3717
+ if (!mcpSessionId && await this.merchantTransport.hasBazaarExtension(response)) {
3718
+ mcpSessionId = await this.merchantTransport.initialize(url, init);
3719
+ if (mcpSessionId) requestInit = this.merchantTransport.withSessionHeaders(requestInit, mcpSessionId);
3720
+ }
3721
+ const request = snapshotX402Request(url, requestInit);
3722
+ const option = selectStandardPaymentOption(paymentRequired.accepts);
3723
+ const idempotencyKey = options.idempotencyKey ?? (option ? buildX402IdempotencyKey(paymentRequired, option) : void 0);
3724
+ let receipt;
3725
+ try {
3726
+ receipt = await this.authorizeX402(paymentRequired, options);
3727
+ } catch (err) {
3728
+ if (option && idempotencyKey) {
3729
+ attachResumeState(err, {
3730
+ paymentRequired,
3731
+ accepted: option,
3732
+ idempotencyKey,
3733
+ request
3734
+ });
3735
+ }
3736
+ throw err;
3737
+ }
3738
+ const retryResponse = await this.merchantCompletion.retryRequest(url, requestInit, paymentRequired, receipt);
3739
+ return mcpSessionId ? this.merchantTransport.surfaceResult(retryResponse) : retryResponse;
3740
+ }
3741
+ /**
3742
+ * Deliver an already-signed x402 payment header to the merchant and return
3743
+ * the merchant's response. Used by the hosted MCP server to complete the
3744
+ * merchant leg of an MCP tool payment after the edge signer has built the
3745
+ * merchant payment header.
3746
+ *
3747
+ * Custody note: this never needs the delegate key. It relays a signed,
3748
+ * amount/merchant/nonce-bound EIP-3009 authorization the edge signer already
3749
+ * produced — the hosted server cannot mint or reuse signing authority.
3750
+ *
3751
+ * When the URL is MCP-shaped (`/mcp` path) or the quote-time transport context
3752
+ * says the merchant was Bazaar-discoverable, runs a fresh `initialize`
3753
+ * handshake (the quote-time session is gone once funding confirms; the x402
3754
+ * challenge is stateless w.r.t. the MCP session, so a fresh session is
3755
+ * accepted), threads the session + wallet headers, sets the x402 payment
3756
+ * header under the names that scheme requires (#2341), and
3757
+ * collapses an SSE JSON-RPC response to its `result`.
3758
+ */
3759
+ /**
3760
+ * Wait for a payment's Safe→delegate funding tx to reach ≥1 on-chain
3761
+ * confirmation. The hosted x402 completion path MUST call this after funding
3762
+ * and before delivering the merchant payment header, so the merchant's
3763
+ * balanceOf(delegate) / transferWithAuthorization verification sees the funded
3764
+ * balance — otherwise it rejects with "Payment verification failed". The
3765
+ * SDK's local path already does this (see `X402FundingLeg.authorize`); the hosted
3766
+ * split flow regressed when the 5→3 collapse removed the incidental
3767
+ * inter-call latency that used to mask it.
3768
+ *
3769
+ * **NOT a no-op when the funding tx hash is absent** (#1508). The WAIT is
3770
+ * skipped without a hash or a chain RPC, but the `GET /payments/:id` read
3771
+ * below runs UNCONDITIONALLY — it is how the fallback hash and the chainId
3772
+ * are obtained. That distinction is load-bearing: this method must never be
3773
+ * called on a scheme with no funding leg, because the read itself fails once
3774
+ * the intent reaches a status the backend maps to a non-2xx (`submitted` is a
3775
+ * 409), turning a settled payment into a reported error. The previous wording
3776
+ * here said "No-op when the funding tx hash ... is unavailable", and the
3777
+ * hosted erc7710 path was written against that promise — see
3778
+ * `deliverMerchantPayment`'s `noFundingLeg` option.
3779
+ */
3780
+ async ensureFundingConfirmed(paymentId, fundingTxHash) {
3781
+ const status = await this.getPaymentStatus(paymentId);
3782
+ await this.fundingLeg.waitForFundingTx(fundingTxHash ?? status.txHash ?? void 0, status.chainId);
3783
+ }
3784
+ async completeX402MerchantCall(input) {
3785
+ const evidenceContext = await this.merchantCompletion.resolveCompletionContext({
3786
+ paymentId: input.paymentId,
3787
+ url: input.url,
3788
+ noFundingLeg: input.noFundingLeg === true
3789
+ });
3790
+ const fundingTxHash = evidenceContext.txHash;
3791
+ const shouldHandshakeMcp = this.merchantTransport.isMcpUrl(input.url) || input.mcpTransport?.handshakeRequired === true;
3792
+ const x402Wallet = shouldHandshakeMcp ? await this.merchantCompletion.resolveWalletForMerchantCall() : x402PayerAddress(this.delegateAddress, this.x402Wallet);
3793
+ let mcpSessionId;
3794
+ if (shouldHandshakeMcp) {
3795
+ mcpSessionId = await this.merchantTransport.initialize(input.url, input.init, x402Wallet);
3796
+ }
3797
+ let requestInit = withX402Wallet(input.init, x402Wallet) ?? {};
3798
+ if (mcpSessionId) requestInit = this.merchantTransport.withSessionHeaders(requestInit, mcpSessionId);
3799
+ const response = await this.merchantTransport.deliverPayment(input.url, requestInit, input.paymentHeader);
3800
+ const surfaced = mcpSessionId ? await this.merchantTransport.surfaceResult(response) : response;
3801
+ const protocolReceiptHeader = surfaced.headers.get("PAYMENT-RESPONSE") ?? void 0;
3802
+ const settlement = parseMerchantSettlement(protocolReceiptHeader ?? null);
3803
+ const text = await surfaced.text();
3804
+ let body;
3805
+ try {
3806
+ body = text ? JSON.parse(text) : null;
3807
+ } catch {
3808
+ body = text;
3809
+ }
3810
+ if (!surfaced.ok) {
3811
+ if (!input.noFundingLeg && fundingTxHash) {
3812
+ await this.merchantCompletion.recordRetryRejected({
3813
+ rail: "x402",
3814
+ paymentId: evidenceContext.paymentId,
3815
+ txHash: fundingTxHash,
3816
+ resourceUrl: evidenceContext.resourceUrl,
3817
+ merchant: {
3818
+ merchant_status: surfaced.status,
3819
+ merchant_status_text: surfaced.statusText,
3820
+ merchant_headers: Object.fromEntries(surfaced.headers.entries()),
3821
+ merchant_body: text
3822
+ },
3823
+ details: {
3824
+ merchant_to: evidenceContext.merchantAddress
3825
+ }
3826
+ });
3827
+ }
3828
+ } else {
3829
+ const evidenceTxHash = input.noFundingLeg ? settlement.settlementTxHash ?? void 0 : fundingTxHash ?? void 0;
3830
+ if (evidenceTxHash) {
3831
+ await this.merchantCompletion.reportEvidence({
3832
+ paymentId: evidenceContext.paymentId,
3833
+ rail: "x402",
3834
+ txHash: evidenceTxHash,
3835
+ resourceUrl: evidenceContext.resourceUrl,
3836
+ merchantStatus: surfaced.status,
3837
+ paymentProofHeaderName: x402PaymentHeaderNamesSent(input.paymentHeader),
3838
+ paymentProofHeader: input.paymentHeader,
3839
+ protocolReceiptHeaderName: protocolReceiptHeader ? "PAYMENT-RESPONSE" : void 0,
3840
+ protocolReceiptHeader
3841
+ });
3842
+ }
3843
+ await this.merchantCompletion.reportMerchantReceipt(evidenceContext.paymentId, surfaced);
3844
+ }
3845
+ return {
3846
+ status: surfaced.status,
3847
+ ok: surfaced.ok,
3848
+ body,
3849
+ settlementTxHash: settlement.settlementTxHash ?? void 0
3850
+ };
3851
+ }
3852
+ /**
3853
+ * #2292: report the outcome of a merchant retry the AGENT performed.
3854
+ *
3855
+ * The hosted `haven_complete_mcp_tool` / `completeX402MerchantCall` path is
3856
+ * for merchants Haven calls itself. On the plain-HTTP x402 path Haven never
3857
+ * talks to the merchant, so the outcome of that retry had no way back —
3858
+ * see `MerchantCompletion.reportMerchantOutcome` for what is verified about
3859
+ * a caller-asserted report and what deliberately is not.
3860
+ */
3861
+ async reportX402MerchantOutcome(input) {
3862
+ return await this.merchantCompletion.reportMerchantOutcome(input);
3863
+ }
3864
+ /**
3865
+ * GET /x402/:id/merchant-call-context — the settle-leg twin of #1263's
3866
+ * sign-context fetch (#1307). Re-serves the stored merchant MCP-tool call
3867
+ * context (merchant_url, tool_name, arguments, mcp_transport) recorded at
3868
+ * quote time, so `haven_settle_mcp_tool` / `haven_complete_mcp_tool` can
3869
+ * omit those fields and let Haven rehydrate them by payment_id instead of
3870
+ * the caller re-threading them. Throws `HavenApiError` (404 unknown/foreign
3871
+ * payment_id, 409 no stored context, 410 expired) — the caller decides the
3872
+ * fallback (re-send the full context explicitly).
3873
+ */
3874
+ async getX402MerchantCallContext(paymentId) {
3875
+ const raw = await this.get(
3876
+ `/x402/${paymentId}/merchant-call-context`
3877
+ );
3878
+ return {
3879
+ paymentId: raw.payment_id,
3880
+ merchantUrl: raw.merchant_url,
3881
+ toolName: raw.tool_name,
3882
+ arguments: raw.arguments ?? {},
3883
+ ...raw.mcp_transport ? {
3884
+ mcpTransport: {
3885
+ handshakeRequired: raw.mcp_transport.handshake_required,
3886
+ source: raw.mcp_transport.source
3887
+ }
3888
+ } : {}
3889
+ };
3890
+ }
3891
+ /**
3892
+ * Wait for a funding tx to be mined with ≥1 confirmation before the
3893
+ * merchant retry, eliminating the race where the merchant's
3894
+ * `balanceOf(delegate)` runs before the funding block propagates.
3895
+ *
3896
+ * Skipped when `chainRpcs` does not include the chain; in that case Haven's
3897
+ * backend has already confirmed on-chain submission and callers accept the
3898
+ * small propagation window as a trade-off for not configuring an RPC URL.
3899
+ */
3900
+ throwIfNonSignableAuthorizationState(label, raw) {
3901
+ if (raw.status === "pending_signature") return;
3902
+ throwPaymentStateError(label, raw);
3903
+ }
3904
+ // ── Tool Execution (for agent frameworks) ────────────────────────
3905
+ /**
3906
+ * Execute a tool call by name and input.
3907
+ *
3908
+ * Designed to plug directly into agent tool-call handlers:
3909
+ *
3910
+ * ```ts
3911
+ * if (block.type === 'tool_use') {
3912
+ * const result = await haven.executeTool(block.name, block.input)
3913
+ * // send result back to the model
3914
+ * }
3915
+ * ```
3916
+ */
3917
+ async executeTool(toolName, input) {
3918
+ if (toolName === "make_payment") {
3919
+ const { token, amount, to } = input;
3920
+ try {
3921
+ const result = await this.pay({ token, amount, to });
3922
+ return {
3923
+ success: result.status === "confirmed",
3924
+ payment_id: result.paymentId,
3925
+ status: result.status,
3926
+ tx_hash: result.txHash,
3927
+ token: result.token,
3928
+ amount: result.amount,
3929
+ to: result.to,
3930
+ explorer_url: result.explorerUrl,
3931
+ error: result.errorMessage
3932
+ };
3933
+ } catch (err) {
3934
+ return toolError(err);
3935
+ }
3936
+ }
3937
+ if (toolName === "authorize_x402_payment") {
3938
+ const { url, payTo, amount, asset, network, description, idempotencyKey } = input;
3939
+ try {
3940
+ const receipt = await this.authorizeX402(
3941
+ toolX402PaymentRequired({ url, payTo, amount, asset, network, description }),
3942
+ { idempotencyKey }
3943
+ );
3944
+ return x402ToolReceipt(receipt);
3945
+ } catch (err) {
3946
+ return toolError(err);
3947
+ }
3948
+ }
3949
+ if (toolName === "resume_x402_payment") {
3950
+ const { payment_id, url, payTo, amount, asset, network, description, idempotencyKey } = input;
3951
+ try {
3952
+ const receipt = await this.resumeAuthorizedX402({
3953
+ paymentId: payment_id,
3954
+ paymentRequired: toolX402PaymentRequired({ url, payTo, amount, asset, network, description }),
3955
+ idempotencyKey
3956
+ });
3957
+ return x402ToolReceipt(receipt);
3958
+ } catch (err) {
3959
+ return toolError(err);
3960
+ }
3961
+ }
3962
+ if (toolName === "get_payment_status") {
3963
+ const { payment_id } = input;
3964
+ const result = await this.getPaymentStatus(payment_id);
3965
+ return {
3966
+ payment_id: result.paymentId,
3967
+ kind: result.kind,
3968
+ rail: result.rail,
3969
+ status: result.status,
3970
+ phase: result.phase,
3971
+ next_action: result.nextAction,
3972
+ tx_hash: result.txHash,
3973
+ token: result.token,
3974
+ amount: result.amount,
3975
+ resource_url: result.resourceUrl,
3976
+ merchant_address: result.merchantAddress,
3977
+ amount_atomic: result.amountAtomic,
3978
+ asset: result.asset,
3979
+ network: result.network,
3980
+ description: result.description,
3981
+ idempotency_key: result.idempotencyKey,
3982
+ x402: result.x402,
3983
+ mpp: result.mpp,
3984
+ expires_at: result.expiresAt,
3985
+ chain_id: result.chainId,
3986
+ message: result.message
3987
+ };
3988
+ }
3989
+ if (toolName === "get_allowances") {
3990
+ return { ...await this.getAllowances() };
3991
+ }
3992
+ throw new Error(`Unknown tool: ${toolName}`);
3993
+ }
3994
+ // ── HTTP Helpers ─────────────────────────────────────────────────
3995
+ async post(path, body) {
3996
+ return this.havenApi.post(path, body);
3997
+ }
3998
+ async get(path) {
3999
+ return this.havenApi.get(path);
4000
+ }
4001
+ };
4002
+ function sleep(ms) {
4003
+ return new Promise((resolve) => setTimeout(resolve, ms));
4004
+ }
4005
+
4006
+ // src/tool-descriptions.ts
4007
+ function composeDescription(d) {
4008
+ return [d.summary, d.selectionGuidance, d.behavior, d.nextActionGuidance].filter(Boolean).join(" ");
4009
+ }
4010
+ var toolDescriptions = {
4011
+ quoteX402: {
4012
+ summary: "Inspect an HTTP 402 x402 paid resource without creating a Haven payment, signature, approval, or on-chain transaction.",
4013
+ behavior: "Probes the merchant directly and parses the 402 response. Pure read-only client behavior \u2014 Haven is not contacted.",
4014
+ nextActionGuidance: "On success the returned quote is the input to haven_pay_x402_quote. Do not call the merchant again \u2014 Haven re-uses the captured request when paying."
4015
+ },
4016
+ payX402: {
4017
+ summary: "Pay an inspected x402 quote. The delegate key signs locally; Haven only validates and relays signed, on-chain-constrained payment transactions.",
4018
+ selectionGuidance: "Do not use this for read-only allowance, budget, spend-limit, remaining-amount, reset-period, or what-can-I-spend questions; use the allowance lookup tool instead.",
4019
+ behavior: "Signs the payment locally 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.",
4020
+ nextActionGuidance: "Preserve the returned resume_state \u2014 it 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_safe_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."
4021
+ },
4022
+ payX402OneShot: {
4023
+ summary: "Fetch an x402 paid HTTP resource in a single call. Handles the full probe -> pay -> retry round trip and returns the merchant response.",
4024
+ 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.",
4025
+ 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.",
4026
+ 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_safe_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."
4027
+ },
4028
+ resumeX402: {
4029
+ summary: "Resume an x402 payment whose Haven-side authorization already succeeded but whose merchant retry did not complete.",
4030
+ 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.",
4031
+ 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."
4032
+ },
4033
+ // #1328: quoteMpp / payMpp / resumeMpp (the mpp_demo challenge/quote/resume
4034
+ // fragments) are retired along with the client surface they described —
4035
+ // MACHINE-PAYMENT-CHALLENGE was never produced by anything besides the now
4036
+ // deleted `/demo/mpp/*` route. Use the x402 fragments above instead.
4037
+ getPaymentStatus: {
4038
+ summary: "Fetch structured Haven payment status, including phase and nextAction taxonomy for agent recovery.",
4039
+ behavior: "Accepts a payment intent id and returns the full state taxonomy (phase, nextAction, rail, amount, merchant, resource url, idempotency key, message).",
4040
+ nextActionGuidance: ""
4041
+ },
4042
+ getResumeState: {
4043
+ summary: "Rehydrate stored x402 resume_state by payment_id.",
4044
+ behavior: "Returns the x402 context the agent originally received when the payment was authorized, reconstructed from Haven's database. This is context only; signing still happens locally when a resume tool is called.",
4045
+ nextActionGuidance: ""
4046
+ },
4047
+ getAgent: {
4048
+ 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.",
4049
+ 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.",
4050
+ 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, safeAddress, delegateAddress, chainId) are unchanged from before.`,
4051
+ nextActionGuidance: ""
4052
+ },
4053
+ getAllowances: {
4054
+ summary: "Return configured and on-chain allowance state for the authenticated agent. On-chain allowance is the real spend gate.",
4055
+ 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.",
4056
+ 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.",
4057
+ nextActionGuidance: ""
4058
+ },
4059
+ listReceipts: {
4060
+ summary: "List recent machine-payment receipts and evidence for bookkeeping.",
4061
+ 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.",
4062
+ behavior: "Returns the agent's recent machine-payment receipts ordered by recency. Proof header values are not returned.",
4063
+ nextActionGuidance: ""
4064
+ },
4065
+ verifyReceipt: {
4066
+ summary: "Verify a payment receipt offline \u2014 confirm the agent authorised the transfer.",
4067
+ selectionGuidance: "Use this to check a receipt you already hold; it needs no network and does not trust Haven. Use the history tool to fetch receipts in the first place.",
4068
+ behavior: "Recovers the signer from the receipt authorisation and confirms it matches the agent delegate. Returns verified true/false with the recovered signer or a reason. Pure and local \u2014 no backend call.",
4069
+ nextActionGuidance: ""
4070
+ },
4071
+ payMcpTool: {
4072
+ summary: "Call a named tool on an MCP merchant that requires an x402 payment, handling the full initialize \u2192 pay \u2192 retry round trip in one call.",
4073
+ selectionGuidance: "Use this when the agent wants to call a specific tool on an MCP merchant (e.g. Soundside, Coinbase Bazaar) and payment is required. Prefer this over haven_pay_x402 when you know the merchant_url and tool_name \u2014 it builds the JSON-RPC envelope internally. Use haven_pay_x402 for arbitrary HTTP resources. Do NOT use for read-only allowance or budget questions \u2014 use haven_get_allowances.",
4074
+ behavior: "Builds the JSON-RPC tools/call envelope, runs the MCP Streamable-HTTP initialize handshake automatically (if the endpoint is MCP-shaped), pays any HTTP 402 x402 challenge against the agent's on-chain budget delegation, and retries the request, returning the JSON-RPC result (the actual merchant output) on success. Amounts within the remaining on-chain budget execute automatically; anything outside it is declined before any money moves \u2014 follow the response's nextAction when present.",
4075
+ 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."
4076
+ },
4077
+ discoverTools: {
4078
+ 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.",
4079
+ 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 to show only self-submitted directory entries that passed domain-ownership proof and a live quote probe \u2014 never treat those 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.",
4080
+ 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.",
4081
+ 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.`
4082
+ },
4083
+ submitCatalogEntry: {
4084
+ summary: "Submit a merchant's payable (x402/MCP) endpoint to Haven's Verified Payable Directory for verification and listing.",
4085
+ selectionGuidance: "Use this when a merchant or seller asks to be listed in the directory, or when you have discovered a payable endpoint and want it registered. The submission is queue-only: it books a spot and returns a verify_token. The seller must then prove control of the domain (a well-known line or DNS TXT record); only after that plus a live quote probe does the entry become listed. Do NOT use to pay \u2014 check the returned status with getCatalogSubmissionStatus instead.",
4086
+ behavior: "Sends the https resource_url to Haven's public submission endpoint. The request path makes no outbound request to the merchant. Returns id + verify_token + status; the verify_token is shown exactly once. Ownership proof is always required later and cannot be skipped from the agent side. The website field is a honeypot for bots \u2014 leave it unset.",
4087
+ nextActionGuidance: "Give the verify_token and the well-known instructions (from getCatalogSubmissionStatus) to the merchant so they can publish the proof line, then poll the submission status until it reaches verified_payable or failed."
4088
+ },
4089
+ sweep_delegate: {
4090
+ summary: "Sweep stranded USDC and/or ETH from the delegate wallet back to the originating Safe.",
4091
+ 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.",
4092
+ 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 Safe (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 Safe) 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.`,
4093
+ 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.'
4094
+ },
4095
+ send: {
4096
+ summary: "Send ETH or USDC directly from the agent's Haven wallet to a recipient address.",
4097
+ 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.",
4098
+ 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.",
4099
+ 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."
4100
+ }
4101
+ };
4102
+
4103
+ // src/tools.ts
4104
+ var makePaymentSchema = {
4105
+ type: "object",
4106
+ properties: {
4107
+ token: {
4108
+ type: "string",
4109
+ description: "Token to send. Gnosis Chain: EURe, USDC.e, xDAI. Base: USDC, ETH."
4110
+ },
4111
+ amount: {
4112
+ type: "string",
4113
+ description: 'Amount to send as a decimal string, e.g. "5.00"'
4114
+ },
4115
+ to: {
4116
+ type: "string",
4117
+ description: "Recipient Ethereum address (0x...)"
4118
+ },
4119
+ reason: {
4120
+ type: "string",
4121
+ description: "Brief reason for this payment (for audit trail)"
4122
+ }
4123
+ },
4124
+ required: ["token", "amount", "to", "reason"]
4125
+ };
4126
+ var getPaymentStatusSchema = {
4127
+ type: "object",
4128
+ properties: {
4129
+ payment_id: {
4130
+ type: "string",
4131
+ description: "The payment ID returned from make_payment"
4132
+ }
4133
+ },
4134
+ required: ["payment_id"]
4135
+ };
4136
+ var getAllowancesSchema = {
4137
+ type: "object",
4138
+ properties: {},
4139
+ required: []
4140
+ };
4141
+ var authorizeX402Schema = {
4142
+ type: "object",
4143
+ properties: {
4144
+ url: {
4145
+ type: "string",
4146
+ description: "The URL that returned HTTP 402"
4147
+ },
4148
+ payTo: {
4149
+ type: "string",
4150
+ description: "Payment recipient address from the 402 response"
4151
+ },
4152
+ amount: {
4153
+ type: "string",
4154
+ description: 'Payment amount in atomic units (e.g. "1000000" for 1 USDC)'
4155
+ },
4156
+ asset: {
4157
+ type: "string",
4158
+ description: "Token contract address from the 402 response"
4159
+ },
4160
+ network: {
4161
+ type: "string",
4162
+ description: 'CAIP-2 chain ID. "eip155:100" for Gnosis Chain, "eip155:8453" for Base.'
4163
+ },
4164
+ description: {
4165
+ type: "string",
4166
+ description: "Description of the resource being paid for"
4167
+ },
4168
+ idempotencyKey: {
4169
+ type: "string",
4170
+ description: "Stable caller-supplied key for this user intent. Reuse it when resuming the same payment."
4171
+ }
4172
+ },
4173
+ required: ["url", "payTo", "amount", "asset", "network"]
4174
+ };
4175
+ var resumeX402Schema = {
4176
+ type: "object",
4177
+ properties: {
4178
+ payment_id: {
4179
+ type: "string",
4180
+ description: "The payment ID returned by authorize_x402_payment."
4181
+ },
4182
+ url: {
4183
+ type: "string",
4184
+ description: "The original URL that returned HTTP 402."
4185
+ },
4186
+ payTo: {
4187
+ type: "string",
4188
+ description: "Payment recipient address from the original 402 response."
4189
+ },
4190
+ amount: {
4191
+ type: "string",
4192
+ description: "Payment amount in atomic units from the original 402 response."
4193
+ },
4194
+ asset: {
4195
+ type: "string",
4196
+ description: "Token contract address from the original 402 response."
4197
+ },
4198
+ network: {
4199
+ type: "string",
4200
+ description: "CAIP-2 chain ID or x402 network from the original 402 response."
4201
+ },
4202
+ description: {
4203
+ type: "string",
4204
+ description: "Description of the resource being paid for."
4205
+ },
4206
+ idempotencyKey: {
4207
+ type: "string",
4208
+ description: "Stable caller-supplied key used for the original authorization."
4209
+ }
4210
+ },
4211
+ required: ["payment_id", "url", "payTo", "amount", "asset", "network"]
4212
+ };
4213
+ var MAKE_PAYMENT_DESCRIPTION = "Request and sign a payment from the user-controlled account within its on-chain budget. For read-only allowance, budget, spend-limit, remaining-amount, or reset-period questions, use get_allowances instead of making a payment. Haven authenticates the agent and relays the signed transaction that redeems the agent budget delegation; it does not hold keys or control funds. Gnosis Chain tokens: EURe, USDC.e, xDAI. Base tokens: USDC, ETH.";
4214
+ var GET_STATUS_DESCRIPTION = toolDescriptions.getPaymentStatus.summary + " Accepts payment intent IDs. Returns the current status, phase, next_action, transaction hash if available, and payment details.";
4215
+ var GET_ALLOWANCES_DESCRIPTION = composeDescription(toolDescriptions.getAllowances);
4216
+ var AUTHORIZE_X402_DESCRIPTION = composeDescription(toolDescriptions.payX402) + " In this SDK tool set, the allowance lookup tool is get_allowances. When a paid API returns x402 payment requirements, use this tool to sign with the agent-owned delegate key; funding, when the scheme needs it, is redeemed from the agent budget delegation and is bounded by it. Haven relays signed transactions only; the agent key authorizes payment and on-chain limits enforce spend. A payment outside the on-chain budget is declined before any money moves \u2014 report the decline and ask the user to raise the budget in Haven; do not loop retries and do not wait for an approval, because none is queued. Preserve the original merchant/MCP session and x402 details. On a manual HTTP retry always set PAYMENT-SIGNATURE (x402 v2) to the returned payment_header; a strict v2 merchant reads only that name. Also set X-PAYMENT (v1) on the EIP-3009 funding path for legacy merchants, but NEVER on erc7710 \u2014 that header carries a delegation chain and duplicating it is refused with HTTP 431.";
4217
+ var RESUME_X402_DESCRIPTION = toolDescriptions.resumeX402.summary + " Only call this after get_payment_status reports nextAction=retry_original_x402_request \u2014 that means Haven's funding leg confirmed but no merchant response was ever recorded (typically a crash between funding and the merchant retry). Any other nextAction reports a conflict instead of retrying \u2014 do not call this speculatively, and do not pay again.";
4218
+ var SWEEP_DELEGATE_DESCRIPTION = composeDescription(toolDescriptions.sweep_delegate);
4219
+ var sweepDelegateSchema = {
4220
+ type: "object",
4221
+ properties: {},
4222
+ required: []
4223
+ };
4224
+ function claudeTools() {
4225
+ return [
4226
+ {
4227
+ name: "make_payment",
4228
+ description: MAKE_PAYMENT_DESCRIPTION,
4229
+ input_schema: makePaymentSchema
4230
+ },
4231
+ {
4232
+ name: "get_payment_status",
4233
+ description: GET_STATUS_DESCRIPTION,
4234
+ input_schema: getPaymentStatusSchema
4235
+ },
4236
+ {
4237
+ name: "get_allowances",
4238
+ description: GET_ALLOWANCES_DESCRIPTION,
4239
+ input_schema: getAllowancesSchema
4240
+ },
4241
+ {
4242
+ name: "authorize_x402_payment",
4243
+ description: AUTHORIZE_X402_DESCRIPTION,
4244
+ input_schema: authorizeX402Schema
4245
+ },
4246
+ {
4247
+ name: "resume_x402_payment",
4248
+ description: RESUME_X402_DESCRIPTION,
4249
+ input_schema: resumeX402Schema
4250
+ },
4251
+ {
4252
+ name: "haven_sweep_delegate",
4253
+ description: SWEEP_DELEGATE_DESCRIPTION,
4254
+ input_schema: sweepDelegateSchema
4255
+ }
4256
+ ];
4257
+ }
4258
+ function openaiTools() {
4259
+ return [
4260
+ {
4261
+ type: "function",
4262
+ function: {
4263
+ name: "make_payment",
4264
+ description: MAKE_PAYMENT_DESCRIPTION,
4265
+ parameters: makePaymentSchema
4266
+ }
4267
+ },
4268
+ {
4269
+ type: "function",
4270
+ function: {
4271
+ name: "get_payment_status",
4272
+ description: GET_STATUS_DESCRIPTION,
4273
+ parameters: getPaymentStatusSchema
4274
+ }
4275
+ },
4276
+ {
4277
+ type: "function",
4278
+ function: {
4279
+ name: "get_allowances",
4280
+ description: GET_ALLOWANCES_DESCRIPTION,
4281
+ parameters: getAllowancesSchema
4282
+ }
4283
+ },
4284
+ {
4285
+ type: "function",
4286
+ function: {
4287
+ name: "authorize_x402_payment",
4288
+ description: AUTHORIZE_X402_DESCRIPTION,
4289
+ parameters: authorizeX402Schema
4290
+ }
4291
+ },
4292
+ {
4293
+ type: "function",
4294
+ function: {
4295
+ name: "resume_x402_payment",
4296
+ description: RESUME_X402_DESCRIPTION,
4297
+ parameters: resumeX402Schema
4298
+ }
4299
+ },
4300
+ {
4301
+ type: "function",
4302
+ function: {
4303
+ name: "haven_sweep_delegate",
4304
+ description: SWEEP_DELEGATE_DESCRIPTION,
4305
+ parameters: sweepDelegateSchema
4306
+ }
4307
+ }
4308
+ ];
4309
+ }
4310
+ var havenTools = {
4311
+ /** Tool definitions in Anthropic/Claude format */
4312
+ claude: claudeTools,
4313
+ /** Tool definitions in OpenAI function-calling format */
4314
+ openai: openaiTools
4315
+ };
4316
+
4317
+ // src/skill-content.ts
4318
+ var HAVEN_SKILL_MD = `---
4319
+ name: haven-pay
4320
+ description: Pay for things from the user's Haven wallet within their agent rules. Use when the user asks to send, pay, tip, or transfer crypto \u2014 or when a request hits an HTTP 402 (x402) paywall.
4321
+ ---
4322
+
4323
+ # Haven: pay from a Haven wallet
4324
+
4325
+ This skill lets the agent make payments from the user's Haven wallet through
4326
+ the Haven MCP tools. Every payment is checked against the agent's on-chain
4327
+ budget before money moves; a payment above the remaining budget is declined \u2014
4328
+ nothing is paid past the rules the user set.
4329
+
4330
+ Hosted tools run in the \`mcp__haven__\` namespace. Local signing tools run in
4331
+ the \`mcp__haven-signer__\` namespace and keep the delegate key on this machine.
4332
+ That namespacing is Claude-family; other runtimes name the servers by their
4333
+ own config keys (Codex: \`haven\`, \`haven_signer\`). Tool results carry the
4334
+ exact next step (\`next_action\`, \`next_tool\`, \`next_arguments\`, plus the
4335
+ runtime-neutral \`next_tool_server\` + \`next_tool_name\` \u2014 the bare tool name
4336
+ on that logical server, whatever your runtime calls it).
4337
+ Follow those fields first; the prose below is fallback and orientation, not
4338
+ the source of truth.
4339
+
4340
+ ## When to use this skill
4341
+
4342
+ - The user asks to send money, pay someone, tip, donate, or transfer tokens.
4343
+ - A request returns HTTP 402 (x402): use the Haven pay tools to settle it,
4344
+ then retry the original request.
4345
+
4346
+ ## Identity and budget
4347
+
4348
+ Do not guess the wallet address, network, or budget.
4349
+
4350
+ For instant orientation at the start of a session, read the non-secret
4351
+ \`agent.json\` the connector wrote to your Haven credential directory (typically
4352
+ \`~/.haven/agents/<agent-id>/agent.json\` \u2014 if you don't know the agent id, list
4353
+ \`~/.haven/agents/\` to find the folder). It
4354
+ holds your agent id, Haven wallet address, network, and *configured* per-token
4355
+ budget, and contains no keys \u2014 the fastest way to answer "who am I and what may
4356
+ I spend" with no round trip. If that file is absent (some setups don't write
4357
+ it), use the tools below instead.
4358
+
4359
+ Before any payment, confirm the *live remaining* budget with the tools \u2014
4360
+ \`agent.json\` shows the configured budget, not what is left after recent
4361
+ spending:
4362
+
4363
+ - \`mcp__haven__haven_get_agent\` \u2014 the recommended first call: identity
4364
+ (wallet, network) plus \`spend_authority_readiness\` (\`ready\` / \`needs_approval\` /
4365
+ \`revoked\`) and live remaining per-token allowance, in one shot. That signal
4366
+ covers hosted identity and on-chain spend authority only \u2014 it cannot see the
4367
+ local signer; the signer is verified by calling any signer tool.
4368
+ - \`mcp__haven__haven_get_allowances\` \u2014 detailed per-token breakdown
4369
+ (configured, spent, reset window) when you need more than the summary.
4370
+
4371
+ Budgets reset on a period the user chose. If a payment exceeds the remaining
4372
+ budget it is declined before any money moves \u2014 tell the user; they can raise
4373
+ the budget in the Haven dashboard, or wait for the period reset.
4374
+
4375
+ ## Paying
4376
+
4377
+ **Catalog purchases \u2014 the primary path for MCP merchants:**
4378
+
4379
+ 1. \`mcp__haven__haven_discover_tools\` to find a payable service and its
4380
+ \`catalog_id\`.
4381
+ 2. If the user needs the live price before authorizing a cap, call
4382
+ \`mcp__haven__haven_quote_catalog_purchase\` with \`catalog_id\`. It is
4383
+ read-only and informational only: it never reserves a price or creates a
4384
+ payment. Tell the user its \`amount\` / \`amount_atomic\`, then choose a cap.
4385
+ 3. \`mcp__haven__haven_prepare_catalog_purchase\` with \`catalog_id\` and a
4386
+ spending cap. A cap is REQUIRED on this tool and is best practice on every
4387
+ paid call below too \u2014 it caps what the LIVE merchant quote may charge,
4388
+ checked before any funding intent is created. Write it the way the user
4389
+ said it: \`max_amount_human\` is whole tokens, so "no more than 1 USDC" is
4390
+ \`max_amount_human: "1"\`. (\`max_amount\` is the atomic-unit form, where
4391
+ "1" means 0.000001 USDC \u2014 do not convert by hand, and never send both.)
4392
+ 4. Then FOLLOW THE RESPONSE'S GUIDANCE FIELDS: \`next_action\`, \`next_tool\`,
4393
+ and \`next_arguments\` name the exact next call \u2014 act on those first; the
4394
+ prose in this section is fallback and debugging detail. If the catalog
4395
+ entry is missing or degraded, the response instead names
4396
+ \`mcp__haven__haven_pay_mcp_tool\` (merchant URL, tool name, arguments) as
4397
+ the manual fallback.
4398
+
4399
+ **Signing:** \`mcp__haven-signer__haven_sign_x402\` with \`payment_id\` ONLY \u2014
4400
+ the local signer fetches the exact signing bytes AND \`payment_required\`
4401
+ itself, so never relay \`typed_data\` or the 402 blob yourself. If the signer
4402
+ reports its fetched context carried no \`payment_required\` (older backend),
4403
+ re-call with \`payment_required\` added verbatim. Fallback for an older signer
4404
+ or backend: re-run the quote/prepare tool with the SAME \`idempotency_key\`
4405
+ plus \`include_signing_payload=true\`, then pass \`payload_hash\`,
4406
+ \`x402_expected\` (the nested \`x402.expected\` object), and
4407
+ \`typed_data\`/\`typed_data_b64\` through unchanged.
4408
+
4409
+ **Settle:** \`mcp__haven__haven_settle_mcp_tool\` with \`payment_id\`,
4410
+ \`signature\`, and \`payment_header\` ONLY \u2014 Haven rehydrates the merchant call
4411
+ context (\`merchant_url\`, \`tool_name\`, \`arguments\`, \`mcp_transport\`)
4412
+ server-side from \`payment_id\`. Pass those four fields explicitly only as a
4413
+ version-skew fallback when Haven has no stored context for the id \u2014 both or
4414
+ none together, never just one. If the settle result carries \`settled: false\`,
4415
+ funding has not confirmed \u2014 follow the result's guidance fields and check
4416
+ status later, do not re-pay.
4417
+
4418
+ Step-by-step alternative (also key-safe; for an older signer or backend, or
4419
+ when you already have a merchant URL and tool name instead of a
4420
+ \`catalog_id\`): if the user needs the live price before choosing a cap, first
4421
+ call \`mcp__haven__haven_quote_mcp_tool\` with that merchant URL, tool name,
4422
+ and arguments. It is informational only; then call
4423
+ \`mcp__haven__haven_pay_mcp_tool\` with the same inputs and the explicit cap.
4424
+ The paid call always obtains a fresh quote before it creates any intent. Then
4425
+ continue \`mcp__haven__haven_pay_mcp_tool\` \u2192
4426
+ \`mcp__haven-signer__haven_sign\` \u2192 \`mcp__haven__haven_submit\` \u2192
4427
+ \`mcp__haven-signer__haven_x402_sign_header\` \u2192
4428
+ \`mcp__haven__haven_complete_mcp_tool\`. Call that last step with
4429
+ \`payment_id\` and the signer's \`payment_header\` ONLY. It does not take
4430
+ \`payment_required\`: Haven rehydrates the merchant call context
4431
+ (\`merchant_url\`, \`tool_name\`, \`arguments\`, \`mcp_transport\`) and the
4432
+ 402 server-side from \`payment_id\`, exactly as at settle. Pass that context
4433
+ explicitly only as a version-skew fallback when Haven has no stored context
4434
+ for the id \u2014 \`merchant_url\` and \`tool_name\` both or none together, never
4435
+ just one.
4436
+ The returned \`expires_at\` is the signing window; if a tool returns
4437
+ \`PAYMENT_WINDOW_EXPIRED\`, re-run the same quote/prepare tool with the same
4438
+ \`idempotency_key\`. Do not call the merchant yourself \u2014 Haven completes the
4439
+ merchant leg for you.
4440
+
4441
+ **Direct transfer / non-MCP paywall:** \`mcp__haven__haven_pay\` with
4442
+ \`to\`, \`amount\`, and \`token\` for a plain transfer. For an arbitrary,
4443
+ non-MCP x402 paywall: \`mcp__haven__haven_quote_x402\` to get a quote, then
4444
+ \`mcp__haven__haven_pay_x402_quote\` \u2014 follow the result's guidance fields
4445
+ first and sign in the local Haven signer. On THIS path Haven does not talk to
4446
+ the merchant: \`mcp__haven-signer__haven_sign_x402\` returns both
4447
+ \`signature\` and \`payment_header\`; relay \`signature\` with
4448
+ \`mcp__haven__haven_submit\`, then retry the paywalled URL yourself with
4449
+ \`payment_header\`. Do not pass that call's \`x402_binding\` to
4450
+ \`mcp__haven-signer__haven_x402_sign_header\` \u2014 the one-shot already spent it
4451
+ building the header, so the call can only refuse. Then tell Haven what the
4452
+ merchant answered: \`mcp__haven__haven_report_x402_outcome\` with the
4453
+ \`payment_id\`, \`outcome\` (\`"accepted"\` for a 2xx, else \`"rejected"\`)
4454
+ and the \`merchant_status\` you got. Because Haven never contacted that
4455
+ merchant, this is the only way it can learn the purchase failed \u2014 without it a
4456
+ failed purchase reads as complete for fifteen minutes. (The SDK's own
4457
+ \`haven_pay_x402\` tool does perform the merchant retry itself; that tool is
4458
+ not part of the hosted MCP surface.) If the process
4459
+ crashes after payment, a later \`mcp__haven__haven_get_payment_status\` call
4460
+ may report \`nextAction: 'retry_original_x402_request'\` \u2014 only then call
4461
+ \`mcp__haven__haven_resume_x402_payment\` with the preserved resume state or
4462
+ payment id, instead of paying again.
4463
+
4464
+ **Catalog tool arguments:** when \`haven_discover_tools\` returns
4465
+ \`tool_arguments\`, pass that object unchanged as the pay tool's
4466
+ \`arguments\` field (for example
4467
+ \`tool_arguments: { "tier": "50gb" }\` -> \`arguments: { "tier": "50gb" }\`).
4468
+
4469
+ **Prices:** show the user the live price from a read-only quote or the pay-tool
4470
+ result, never a catalog price. \`haven_discover_tools\` prices are indicative
4471
+ (\`price_is_indicative\`) and can be stale. A read-only quote is informational
4472
+ only and does not reserve a price; the later paid call re-quotes and enforces
4473
+ the cap. The pay-tool result's \`amount\` / \`amount_atomic\` is the merchant's
4474
+ own quoted price for that call \u2014 a ceiling the merchant settles at or below \u2014
4475
+ so present it as the most the user will pay. It is a price, not an approval:
4476
+ the payment goes through only if it also fits the cap you set and the on-chain
4477
+ budget the user signed, which is enforced on-chain rather than by Haven.
4478
+
4479
+ **Status:** \`mcp__haven__haven_get_payment_status\` with a \`payment_id\` to
4480
+ check on in-flight payments. Do not poll in a tight loop.
4481
+
4482
+ ## Declines and stop signals
4483
+
4484
+ - A payment outside the agent's rules \u2014 above the remaining budget, wrong
4485
+ recipient, or expired budget \u2014 is declined before any money moves. Nothing
4486
+ is queued; tell the user, who can raise the budget in Haven.
4487
+ - \`safe_to_continue: false\` on a guidance block is a stop signal in
4488
+ machine-readable form: stop and involve the user before calling anything
4489
+ else for this payment.
4490
+ - Never ask the user for private keys. Signing happens only in the local Haven
4491
+ signer; the hosted Haven tools never receive the signing key. If a tool
4492
+ reports a missing or invalid credential, tell the user to re-run the Haven
4493
+ setup command.
4494
+
4495
+ ## Failure handling
4496
+
4497
+ Haven tool failures are shaped like \`{ success: false, code, message, ... }\`
4498
+ or older \`{ error, status, details? }\` responses. Branch on \`code\` when
4499
+ present and surface \`message\` or \`error\` verbatim. Common cases:
4500
+
4501
+ - \`insufficient_funds\`: the Haven wallet doesn't hold enough of that token.
4502
+ Suggest the user add funds in the Haven dashboard.
4503
+ - \`PRICE_EXCEEDS_MAX\`: the live merchant price exceeded your cap. No funds
4504
+ moved; ask the user before retrying with a higher one.
4505
+ - \`AMBIGUOUS_MAX_AMOUNT\`: you sent both \`max_amount\` and
4506
+ \`max_amount_human\`. Nothing was contacted or spent \u2014 re-send with exactly
4507
+ one (\`max_amount_human\` for a cap the user stated in tokens).
4508
+ - \`MAX_AMOUNT_UNCONVERTIBLE\`: \`max_amount_human\` does not fit this quote's
4509
+ asset \u2014 unknown decimals, or more decimal places than the asset supports.
4510
+ Round the cap, or send an exact atomic \`max_amount\`.
4511
+ - \`PAYMENT_WINDOW_EXPIRED\`: re-run the quote/prepare tool with the same
4512
+ \`idempotency_key\`, then sign the fresh payload.
4513
+ - \`MERCHANT_REJECTED_AFTER_FUNDING\`: the merchant refused the paid retry.
4514
+ Stop-and-sweep \u2014 stop retrying the merchant and use
4515
+ \`mcp__haven__haven_sweep_delegate\` to recover stranded delegate funds.
4516
+ - \`MERCHANT_UNRESPONSIVE_AFTER_FUNDING\`: funding confirmed on-chain, but the
4517
+ merchant never answered the paid retry. This is NOT proof of rejection \u2014 the
4518
+ merchant may still settle late. Verify-then-sweep, never a blind sweep:
4519
+ check \`mcp__haven__haven_get_payment_status\`, retry
4520
+ \`mcp__haven__haven_complete_mcp_tool\` ONCE, and only sweep with
4521
+ \`mcp__haven__haven_sweep_delegate\` if no settlement appears.
4522
+ - Budget exceeded: tell the user how much remains (from
4523
+ \`mcp__haven__haven_get_allowances\`) and that they can raise the budget in
4524
+ Haven.
4525
+
4526
+ ## Reporting after a purchase
4527
+
4528
+ A settled \`mcp__haven__haven_settle_mcp_tool\` response carries
4529
+ \`agent_summary.purchase_summary\` and the remaining post-purchase allowance
4530
+ in \`allowance\` \u2014 report the product, Haven-derived payment/transaction
4531
+ fields, and what is left from those fields directly. \`result\` is optional
4532
+ raw merchant evidence; never use it to decide whether the purchase was paid.
4533
+ Do not call \`haven_get_agent\` or \`haven_get_allowances\` again just to
4534
+ report a purchase you already made.
4535
+
4536
+ ## Revoke
4537
+
4538
+ If this agent's credential may have leaked, tell the user to pause or revoke
4539
+ the agent in the Haven dashboard under Agents. New requests stop immediately
4540
+ for that credential.
4541
+ `;
4542
+ var SKILL_FOLDER_NAME = "haven-pay";
4543
+ var HAVEN_SKILL_BODY_MD = HAVEN_SKILL_MD.replace(/^---\n[\s\S]*?\n---\n+/, "");
4544
+
4545
+ // src/node-version.ts
4546
+ var HAVEN_MINIMUM_NODE_VERSION = "22.0.0";
4547
+ function parseNodeVersion(value) {
4548
+ const match = value.trim().match(/^v?(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
4549
+ if (!match) return [0, 0, 0];
4550
+ return [Number(match[1] ?? 0), Number(match[2] ?? 0), Number(match[3] ?? 0)];
4551
+ }
4552
+ function compareNodeVersions(left, right) {
4553
+ const leftParts = parseNodeVersion(left);
4554
+ const rightParts = parseNodeVersion(right);
4555
+ for (let i = 0; i < 3; i += 1) {
4556
+ if (leftParts[i] !== rightParts[i]) return leftParts[i] > rightParts[i] ? 1 : -1;
4557
+ }
4558
+ return 0;
4559
+ }
4560
+ function isSupportedNodeVersion(nodeVersion = process.versions.node, minimumNodeVersion = HAVEN_MINIMUM_NODE_VERSION) {
4561
+ return compareNodeVersions(nodeVersion, minimumNodeVersion) >= 0;
4562
+ }
4563
+ function unsupportedNodeVersionMessage(options) {
4564
+ const nodeVersion = options.nodeVersion ?? process.versions.node;
4565
+ const minimum = options.minimumNodeVersion ?? HAVEN_MINIMUM_NODE_VERSION;
4566
+ const lines = [
4567
+ `${options.subject} requires Node.js >=${minimum}, but this is Node.js ${nodeVersion}.`,
4568
+ "",
4569
+ "Upgrade Node, then try again:",
4570
+ ` nvm install ${major(minimum)} && nvm use ${major(minimum)} (nvm)`,
4571
+ ` fnm install ${major(minimum)} && fnm use ${major(minimum)} (fnm)`,
4572
+ ` volta install node@${major(minimum)} (volta)`,
4573
+ ` or download Node ${major(minimum)} from https://nodejs.org`,
4574
+ "",
4575
+ "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."
4576
+ ];
4577
+ if (options.retryHint) lines.push("", options.retryHint);
4578
+ return lines.join("\n");
4579
+ }
4580
+ function major(version) {
4581
+ return String(parseNodeVersion(version)[0]);
4582
+ }
4583
+
4584
+ // src/merchant-discovery.ts
4585
+ var MERCHANT_DISCOVERY_PATHS = ["/.well-known/haven-demo-merchant", "/"];
4586
+ var DISCOVERY_MAX_BYTES = 64 * 1024;
4587
+ async function discoverMerchantMcpUrl(inputUrl) {
4588
+ let input;
4589
+ try {
4590
+ input = new URL(inputUrl);
4591
+ } catch {
4592
+ return null;
4593
+ }
4594
+ for (const path of MERCHANT_DISCOVERY_PATHS) {
4595
+ try {
4596
+ const res = await globalThis.fetch(`${input.origin}${path}`, {
4597
+ method: "GET",
4598
+ headers: { accept: "application/json" },
4599
+ redirect: "error",
4600
+ signal: AbortSignal.timeout(5e3)
4601
+ });
4602
+ if (!res.ok) continue;
4603
+ const contentLength = Number(res.headers.get("content-length") ?? 0);
4604
+ if (contentLength > DISCOVERY_MAX_BYTES) continue;
4605
+ const text = await res.text();
4606
+ if (text.length > DISCOVERY_MAX_BYTES) continue;
4607
+ const doc = JSON.parse(text);
4608
+ if (typeof doc.mcp_url !== "string") continue;
4609
+ const resolved = new URL(doc.mcp_url);
4610
+ if (resolved.origin !== input.origin) continue;
4611
+ return resolved.toString();
4612
+ } catch {
4613
+ continue;
4614
+ }
4615
+ }
4616
+ return null;
4617
+ }
4618
+ function sameUrl(a, b) {
4619
+ try {
4620
+ const ua = new URL(a);
4621
+ const ub = new URL(b);
4622
+ return ua.origin === ub.origin && ua.pathname.replace(/\/+$/, "") === ub.pathname.replace(/\/+$/, "");
4623
+ } catch {
4624
+ return false;
4625
+ }
4626
+ }
4627
+
4628
+ exports.AGENT_PAYMENT_FAILURE_CODE_VALUES = AGENT_PAYMENT_FAILURE_CODE_VALUES;
4629
+ exports.AGENT_PAYMENT_NEXT_ACTION_VALUES = AGENT_PAYMENT_NEXT_ACTION_VALUES;
4630
+ exports.AGENT_PAYMENT_PHASE_VALUES = AGENT_PAYMENT_PHASE_VALUES;
4631
+ exports.AGENT_PAYMENT_RAIL_VALUES = AGENT_PAYMENT_RAIL_VALUES;
4632
+ exports.AgentPaymentFailureCode = AgentPaymentFailureCode;
4633
+ exports.AgentPaymentFailureCodeDescriptions = AgentPaymentFailureCodeDescriptions;
4634
+ exports.AgentPaymentFailureCodeSchema = AgentPaymentFailureCodeSchema;
4635
+ exports.AgentPaymentNextAction = AgentPaymentNextAction;
4636
+ exports.AgentPaymentNextActionDescriptions = AgentPaymentNextActionDescriptions;
4637
+ exports.AgentPaymentNextActionSchema = AgentPaymentNextActionSchema;
4638
+ exports.AgentPaymentPhase = AgentPaymentPhase;
4639
+ exports.AgentPaymentPhaseDescriptions = AgentPaymentPhaseDescriptions;
4640
+ exports.AgentPaymentPhaseSchema = AgentPaymentPhaseSchema;
4641
+ exports.AgentPaymentRail = AgentPaymentRail;
4642
+ exports.AgentPaymentRailDescriptions = AgentPaymentRailDescriptions;
4643
+ exports.AgentPaymentRailSchema = AgentPaymentRailSchema;
4644
+ exports.AgentPaymentWarningCode = AgentPaymentWarningCode;
4645
+ exports.CONNECTOR_PACKAGE_NAME = CONNECTOR_PACKAGE_NAME;
4646
+ exports.DEFAULT_CONFIRMATION_TIMEOUT_MS = DEFAULT_CONFIRMATION_TIMEOUT_MS;
4647
+ exports.DISCOVERY_MAX_BYTES = DISCOVERY_MAX_BYTES;
4648
+ exports.ERC7710_ASSET_TRANSFER_METHOD = ERC7710_ASSET_TRANSFER_METHOD;
4649
+ exports.HAVEN_CONNECTOR_CHANNEL = HAVEN_CONNECTOR_CHANNEL;
4650
+ exports.HAVEN_MINIMUM_NODE_VERSION = HAVEN_MINIMUM_NODE_VERSION;
4651
+ exports.HAVEN_SKILL_BODY_MD = HAVEN_SKILL_BODY_MD;
4652
+ exports.HAVEN_SKILL_MD = HAVEN_SKILL_MD;
4653
+ exports.HavenApiError = HavenApiError;
4654
+ exports.HavenClient = HavenClient;
4655
+ exports.HavenError = HavenError;
4656
+ exports.HavenPaymentStateError = HavenPaymentStateError;
4657
+ exports.HavenSigningError = HavenSigningError;
4658
+ exports.HavenTimeoutError = HavenTimeoutError;
4659
+ exports.HavenUnsupportedSignerVersionError = HavenUnsupportedSignerVersionError;
4660
+ exports.MERCHANT_DISCOVERY_PATHS = MERCHANT_DISCOVERY_PATHS;
4661
+ exports.MerchantTimeoutError = MerchantTimeoutError;
4662
+ exports.RECEIPT_VERSION = RECEIPT_VERSION;
4663
+ exports.SIGNER_UPDATE_FALLBACK = SIGNER_UPDATE_FALLBACK;
4664
+ exports.SKILL_FOLDER_NAME = SKILL_FOLDER_NAME;
4665
+ exports.SWEEP_BASE_CHAIN_ID = SWEEP_BASE_CHAIN_ID;
4666
+ exports.SWEEP_BASE_SEPOLIA_CHAIN_ID = SWEEP_BASE_SEPOLIA_CHAIN_ID;
4667
+ exports.SWEEP_BASE_SEPOLIA_USDC_ADDRESS = SWEEP_BASE_SEPOLIA_USDC_ADDRESS;
4668
+ exports.SWEEP_BASE_USDC_ADDRESS = SWEEP_BASE_USDC_ADDRESS;
4669
+ exports.SignerRefusalCode = SignerRefusalCode;
4670
+ exports.TRANSFER_WITH_AUTHORIZATION_TYPES = TRANSFER_WITH_AUTHORIZATION_TYPES;
4671
+ exports.X402AlreadySettledError = X402AlreadySettledError;
4672
+ exports.X402PaymentHeaderValidationError = X402PaymentHeaderValidationError;
4673
+ exports.X402UnexpectedStatusError = X402UnexpectedStatusError;
4674
+ exports.X402_LEGACY_PAYMENT_HEADER_NAME = X402_LEGACY_PAYMENT_HEADER_NAME;
4675
+ exports.X402_MAX_AUTHORIZATION_WINDOW_SECONDS = X402_MAX_AUTHORIZATION_WINDOW_SECONDS;
4676
+ exports.X402_PAYMENT_HEADER_NAME = X402_PAYMENT_HEADER_NAME;
4677
+ exports.X402_PAYMENT_HEADER_NAMES_SENT = X402_PAYMENT_HEADER_NAMES_SENT;
4678
+ exports.X402_PAYMENT_REQUIRED_HEADER_NAME = X402_PAYMENT_REQUIRED_HEADER_NAME;
4679
+ exports.X402_PAYMENT_RESPONSE_HEADER_NAME = X402_PAYMENT_RESPONSE_HEADER_NAME;
4680
+ exports.X402_SETTLEMENT_FORWARD_MARGIN_SECONDS = X402_SETTLEMENT_FORWARD_MARGIN_SECONDS;
4681
+ exports.addressFromKey = addressFromKey;
4682
+ exports.buildSweepAuthorizationMessage = buildSweepAuthorizationMessage;
4683
+ exports.buildSweepTypedData = buildSweepTypedData;
4684
+ exports.buildX402ExpectedMessage = buildX402ExpectedMessage;
4685
+ exports.compareNodeVersions = compareNodeVersions;
4686
+ exports.composeDescription = composeDescription;
4687
+ exports.connectorRerunCommand = connectorRerunCommand;
4688
+ exports.connectorSpec = connectorSpec;
4689
+ exports.decodeBase64Json = decodeBase64Json;
4690
+ exports.decodeBase64Utf8 = decodeBase64Utf8;
4691
+ exports.discoverMerchantMcpUrl = discoverMerchantMcpUrl;
4692
+ exports.encodeBase64Json = encodeBase64Json;
4693
+ exports.encodeBase64Utf8 = encodeBase64Utf8;
4694
+ exports.encodePaymentProof = encodePaymentProof;
4695
+ exports.havenTools = havenTools;
4696
+ exports.isConnectorChannel = isConnectorChannel;
4697
+ exports.isErc7710Option = isErc7710Option;
4698
+ exports.isSupportedNodeVersion = isSupportedNodeVersion;
4699
+ exports.isSweepableChain = isSweepableChain;
4700
+ exports.normalizePaymentRequired = normalizePaymentRequired;
4701
+ exports.parsePaymentRequired = parsePaymentRequired;
4702
+ exports.parsePaymentRequiredResponse = parsePaymentRequiredResponse;
4703
+ exports.resolveConnectorChannel = resolveConnectorChannel;
4704
+ exports.resolveTokenFromAddress = resolveTokenFromAddress;
4705
+ exports.sameUrl = sameUrl;
4706
+ exports.selectErc7710PaymentOption = selectErc7710PaymentOption;
4707
+ exports.selectPaymentOption = selectPaymentOption;
4708
+ exports.selectStandardPaymentOption = selectStandardPaymentOption;
4709
+ exports.selectX402SettlementScheme = selectX402SettlementScheme;
4710
+ exports.signHash = signHash;
4711
+ exports.signUserOpTypedDataForDelegation = signUserOpTypedDataForDelegation;
4712
+ exports.signerUpdateFallback = signerUpdateFallback;
4713
+ exports.sweepUsdcAddress = sweepUsdcAddress;
4714
+ exports.sweepUsdcDomain = sweepUsdcDomain;
4715
+ exports.toStandardPaymentRequirements = toStandardPaymentRequirements;
4716
+ exports.toolDescriptions = toolDescriptions;
4717
+ exports.unsupportedNodeVersionMessage = unsupportedNodeVersionMessage;
4718
+ exports.validateStandardX402PaymentHeader = validateStandardX402PaymentHeader;
4719
+ exports.verifyPaymentReceipt = verifyPaymentReceipt;
4720
+ exports.verifySignature = verifySignature;
4721
+ exports.x402AssetTransferMethod = x402AssetTransferMethod;
4722
+ exports.x402AuthorizationAmount = x402AuthorizationAmount;
4723
+ exports.x402FacilitatorAddresses = x402FacilitatorAddresses;
4724
+ exports.x402V2PaymentEnvelope = x402V2PaymentEnvelope;
4725
+ //# sourceMappingURL=index.cjs.map
4726
+ //# sourceMappingURL=index.cjs.map