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