@haven_ai/sdk 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @haven_ai/sdk
2
2
 
3
- TypeScript SDK for [Haven](https://github.com/d-hinders/Haven) — agent wallet infrastructure for the autonomous economy.
3
+ TypeScript SDK for [Haven](https://github.com/d-hinders/Haven-AI) — agent wallet infrastructure for the autonomous economy.
4
4
 
5
5
  Haven gives AI agents the ability to hold, send, and receive money within strict, user-defined guardrails. This SDK makes it trivial to integrate Haven payments into any agent.
6
6
 
@@ -32,6 +32,35 @@ console.log(result.txHash) // 0x...
32
32
  console.log(result.explorerUrl) // https://gnosisscan.io/tx/0x... (or basescan.org for Base)
33
33
  ```
34
34
 
35
+ ## Try it live — zero setup
36
+
37
+ Haven hosts a demo endpoint you can hit immediately after creating an agent:
38
+
39
+ ```typescript
40
+ import { HavenClient } from '@haven_ai/sdk'
41
+
42
+ const haven = new HavenClient({
43
+ apiKey: process.env.HAVEN_API_KEY!, // from Haven dashboard
44
+ delegateKey: process.env.DELEGATE_KEY!, // agent's delegate private key
45
+ baseUrl: 'https://havenbackend-production-8a00.up.railway.app', // hosted Haven, or your self-hosted URL
46
+ })
47
+
48
+ // haven.fetch handles 402 → pay → retry automatically
49
+ const response = await haven.fetch(
50
+ 'https://havenbackend-production-8a00.up.railway.app/demo/x402/data',
51
+ )
52
+ const data = await response.json()
53
+
54
+ console.log(data.message) // "You paid! Here's your demo data."
55
+ console.log(data.fact) // a fun fact about the agent economy
56
+ console.log(data.explorerUrl) // link to the on-chain payment tx
57
+ ```
58
+
59
+ Tell your agent:
60
+ > "Use Haven to fetch `https://havenbackend-production-8a00.up.railway.app/demo/x402/data` and show me what came back."
61
+
62
+ The agent will pay a tiny amount (~0.01 EURe on Gnosis Chain), receive the demo payload, and you'll see the payment in your Haven dashboard activity feed — no local server or extra config required.
63
+
35
64
  ## Supported Networks & Tokens
36
65
 
37
66
  | Network | CAIP-2 | Tokens |
@@ -63,7 +92,7 @@ const result = await haven.waitForConfirmation(intent.paymentId)
63
92
 
64
93
  ## x402 Protocol Support
65
94
 
66
- Haven natively supports the [x402](https://x402.org) payment protocol. When an API returns HTTP 402, Haven evaluates the payment against policy, executes from the Safe, and retries automatically:
95
+ Haven natively supports the [x402](https://x402.org) payment protocol. When an API returns HTTP 402, Haven evaluates the payment against policy, funds the agent delegate wallet from the Haven wallet, signs the merchant's standard x402 payment payload, and retries automatically:
67
96
 
68
97
  ```typescript
69
98
  // Automatic — fetch() intercepts 402, pays, and retries
@@ -71,17 +100,20 @@ const response = await haven.fetch('https://paid-api.example.com/data')
71
100
  const data = await response.json()
72
101
 
73
102
  // Manual — parse and authorize the 402 yourself
74
- import { parsePaymentRequired } from '@haven_ai/sdk'
103
+ import { parsePaymentRequiredResponse } from '@haven_ai/sdk'
75
104
 
76
105
  const apiResponse = await fetch('https://paid-api.example.com/data')
77
106
  if (apiResponse.status === 402) {
78
- const paymentRequired = parsePaymentRequired(apiResponse)
107
+ const paymentRequired = await parsePaymentRequiredResponse(apiResponse)
79
108
  const receipt = await haven.authorizeX402(paymentRequired)
109
+ // Retry with { 'X-PAYMENT': receipt.paymentHeader }
80
110
  console.log(receipt.explorerUrl)
81
111
  }
82
112
  ```
83
113
 
84
- Supported x402 networks: `eip155:100` (Gnosis Chain) and `eip155:8453` (Base).
114
+ Merchant-verified x402 retries use the official EIP-3009 `exact` scheme on Base USDC (`base` / `eip155:8453`) and send the payment as `X-PAYMENT`. Haven's older tx-hash proof helper remains exported for Haven-native integrations, but `haven.fetch()` does not send `PAYMENT-SIGNATURE`.
115
+
116
+ For standard x402, the `x402-wallet` identity is the agent delegate wallet, because that is the wallet that signs and settles the merchant payment. Integrations that scope access by Haven wallet/Safe address should use a Haven-native flow instead of standard merchant x402.
85
117
 
86
118
  ## AI Agent Integration
87
119
 
@@ -97,7 +129,7 @@ const haven = new HavenClient({ apiKey, delegateKey })
97
129
  const anthropic = new Anthropic()
98
130
 
99
131
  const response = await anthropic.messages.create({
100
- model: 'claude-opus-4-6',
132
+ model: 'claude-opus-4-7',
101
133
  tools: havenTools.claude(), // or havenTools.openai() for OpenAI
102
134
  messages: [{ role: 'user', content: 'Pay 5 EURe to 0xabc for API access' }],
103
135
  })
@@ -117,7 +149,7 @@ for (const block of response.content) {
117
149
  |------|-------------|
118
150
  | `make_payment` | Send a payment from the Haven-managed Safe wallet |
119
151
  | `get_payment_status` | Check the status of a previously initiated payment |
120
- | `authorize_x402_payment` | Pay for an HTTP 402 resource via the x402 protocol |
152
+ | `authorize_x402_payment` | Fund the agent wallet and return a payment header for an HTTP 402 resource |
121
153
 
122
154
  ## Configuration
123
155
 
@@ -126,12 +158,34 @@ const haven = new HavenClient({
126
158
  apiKey: 'sk_agent_xxx', // required — Haven agent API key
127
159
  delegateKey: '0x...', // optional — enables .pay() and .sign()
128
160
  baseUrl: 'http://localhost:3001', // default
161
+ x402Wallet: '0x...', // optional fallback when no delegate key is configured
129
162
  requestTimeout: 30000, // per-request timeout (ms)
130
163
  confirmationTimeout: 90000, // polling timeout (ms)
131
164
  pollingInterval: 3000, // polling interval (ms)
132
165
  })
133
166
  ```
134
167
 
168
+ ## Payments above the on-chain allowance
169
+
170
+ Haven's policy lives entirely on the Safe AllowanceModule (token, amount,
171
+ reset period). If an agent requests a payment above the remaining allowance,
172
+ Haven does **not** reject it — it returns HTTP 202 with `status: 'pending_approval'`
173
+ and queues it for the wallet owner to approve in the dashboard.
174
+
175
+ Surface that to the user: the payment isn't dead, it's waiting for a human to
176
+ sign off. Don't retry — the same request would just queue another approval.
177
+
178
+ ```typescript
179
+ try {
180
+ await haven.pay({ token: 'USDC', amount: '500', to: '0xabc...' })
181
+ } catch (err) {
182
+ if (err instanceof HavenApiError && err.statusCode === 202) {
183
+ // err.body.payment_id, err.body.remaining, err.body.requested
184
+ console.log('Queued for owner approval — visible in the Haven dashboard.')
185
+ }
186
+ }
187
+ ```
188
+
135
189
  ## Error Handling
136
190
 
137
191
  ```typescript
package/dist/index.cjs CHANGED
@@ -1,8 +1,11 @@
1
1
  'use strict';
2
2
 
3
+ var schemes = require('x402/schemes');
4
+ var accounts = require('viem/accounts');
3
5
  var ethers = require('ethers');
6
+ var crypto = require('crypto');
4
7
 
5
- // src/signer.ts
8
+ // src/client.ts
6
9
 
7
10
  // src/types.ts
8
11
  var HavenError = class extends Error {
@@ -72,11 +75,66 @@ function verifySignature(hash, signature, expectedAddress) {
72
75
  return false;
73
76
  }
74
77
  }
75
-
76
- // src/x402.ts
78
+ var BASE_USDC_ADDRESS = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913";
79
+ var X402_IDEMPOTENCY_BUCKET_MS = 3e5;
80
+ function decodeBase64Json(value, label) {
81
+ try {
82
+ return JSON.parse(atob(value));
83
+ } catch {
84
+ throw new Error(`Failed to decode ${label}`);
85
+ }
86
+ }
87
+ function normalizePaymentOption(value) {
88
+ const candidate = value;
89
+ if (!candidate || typeof candidate !== "object" || typeof candidate.scheme !== "string" || typeof candidate.network !== "string" || typeof candidate.asset !== "string" || typeof candidate.payTo !== "string") {
90
+ return null;
91
+ }
92
+ const amount = typeof candidate.amount === "string" ? candidate.amount : typeof candidate.maxAmountRequired === "string" ? candidate.maxAmountRequired : null;
93
+ if (!amount) return null;
94
+ return {
95
+ scheme: candidate.scheme,
96
+ network: candidate.network,
97
+ amount,
98
+ maxAmountRequired: candidate.maxAmountRequired,
99
+ resource: candidate.resource,
100
+ description: candidate.description,
101
+ mimeType: candidate.mimeType,
102
+ asset: candidate.asset,
103
+ payTo: candidate.payTo,
104
+ maxTimeoutSeconds: candidate.maxTimeoutSeconds ?? 30,
105
+ extra: candidate.extra
106
+ };
107
+ }
108
+ function normalizePaymentRequired(value) {
109
+ const candidate = value;
110
+ if (!candidate || typeof candidate !== "object" || typeof candidate.x402Version !== "number" || !Array.isArray(candidate.accepts)) {
111
+ return null;
112
+ }
113
+ const accepts = candidate.accepts.map((option) => normalizePaymentOption(option)).filter((option) => !!option);
114
+ if (accepts.length === 0) return null;
115
+ const first = accepts[0];
116
+ const resourceUrl = candidate.resource?.url ?? first.resource;
117
+ if (!resourceUrl) return null;
118
+ const resource = {
119
+ url: resourceUrl,
120
+ description: candidate.resource?.description ?? first.description,
121
+ mimeType: candidate.resource?.mimeType ?? first.mimeType
122
+ };
123
+ return {
124
+ x402Version: candidate.x402Version,
125
+ resource,
126
+ accepts,
127
+ error: candidate.error
128
+ };
129
+ }
77
130
  var SUPPORTED_X402_NETWORKS = {
78
131
  "eip155:100": "Gnosis Chain",
79
- "eip155:8453": "Base"
132
+ "eip155:8453": "Base",
133
+ "base": "Base"
134
+ };
135
+ var STANDARD_X402_NETWORKS = {
136
+ "eip155:8453": "base",
137
+ "base": "base"
80
138
  };
81
139
  var GNOSIS_TOKENS = {
82
140
  "0x0000000000000000000000000000000000000000": { symbol: "xDAI", decimals: 18 },
@@ -89,29 +147,41 @@ var BASE_TOKENS = {
89
147
  };
90
148
  var NETWORK_TOKENS = {
91
149
  "eip155:100": GNOSIS_TOKENS,
92
- "eip155:8453": BASE_TOKENS
150
+ "eip155:8453": BASE_TOKENS,
151
+ "base": BASE_TOKENS
93
152
  };
94
153
  function parsePaymentRequired(response) {
95
154
  const v2Header = response.headers.get("PAYMENT-REQUIRED");
96
155
  if (v2Header) {
97
- try {
98
- return JSON.parse(atob(v2Header));
99
- } catch {
100
- throw new Error("Failed to decode PAYMENT-REQUIRED header");
101
- }
156
+ const parsed = normalizePaymentRequired(
157
+ decodeBase64Json(v2Header, "PAYMENT-REQUIRED header")
158
+ );
159
+ if (parsed) return parsed;
102
160
  }
103
161
  const v1Header = response.headers.get("X-PAYMENT");
104
162
  if (v1Header) {
105
- try {
106
- return JSON.parse(atob(v1Header));
107
- } catch {
108
- throw new Error("Failed to decode X-PAYMENT header");
109
- }
163
+ const parsed = normalizePaymentRequired(
164
+ decodeBase64Json(v1Header, "X-PAYMENT header")
165
+ );
166
+ if (parsed) return parsed;
110
167
  }
111
168
  throw new Error(
112
169
  "No x402 payment headers found in 402 response. Expected PAYMENT-REQUIRED (v2) or X-PAYMENT (v1) header."
113
170
  );
114
171
  }
172
+ async function parsePaymentRequiredResponse(response) {
173
+ try {
174
+ return parsePaymentRequired(response);
175
+ } catch (headerErr) {
176
+ try {
177
+ const body = await response.clone().json();
178
+ const parsed = normalizePaymentRequired(body);
179
+ if (parsed) return parsed;
180
+ } catch {
181
+ }
182
+ throw headerErr;
183
+ }
184
+ }
115
185
  function selectPaymentOption(accepts) {
116
186
  if (!accepts || accepts.length === 0) return null;
117
187
  for (const opt of accepts) {
@@ -127,13 +197,61 @@ function selectPaymentOption(accepts) {
127
197
  }
128
198
  return null;
129
199
  }
200
+ function selectStandardPaymentOption(accepts) {
201
+ if (!accepts || accepts.length === 0) return null;
202
+ for (const opt of accepts) {
203
+ if (opt.scheme === "exact" && opt.network in STANDARD_X402_NETWORKS && opt.asset.toLowerCase() === BASE_USDC_ADDRESS) {
204
+ return opt;
205
+ }
206
+ }
207
+ return null;
208
+ }
209
+ function toStandardPaymentRequirements(paymentRequired, option) {
210
+ const network = STANDARD_X402_NETWORKS[option.network];
211
+ if (!network) {
212
+ throw new Error(`x402 exact payments are not supported on ${option.network}`);
213
+ }
214
+ if (option.scheme !== "exact") {
215
+ throw new Error(`Unsupported x402 scheme: ${option.scheme}`);
216
+ }
217
+ return {
218
+ scheme: "exact",
219
+ network,
220
+ maxAmountRequired: option.maxAmountRequired ?? option.amount,
221
+ resource: option.resource ?? paymentRequired.resource.url,
222
+ description: option.description ?? paymentRequired.resource.description ?? "Haven x402 payment",
223
+ mimeType: option.mimeType ?? paymentRequired.resource.mimeType ?? "application/octet-stream",
224
+ payTo: option.payTo,
225
+ asset: option.asset,
226
+ maxTimeoutSeconds: option.maxTimeoutSeconds,
227
+ extra: option.extra
228
+ };
229
+ }
230
+ function buildX402IdempotencyKey(paymentRequired, option, now = Date.now()) {
231
+ const bucket = Math.floor(now / X402_IDEMPOTENCY_BUCKET_MS);
232
+ const material = [
233
+ paymentRequired.resource.url,
234
+ paymentRequired.resource.description ?? "",
235
+ option.payTo.toLowerCase(),
236
+ option.asset.toLowerCase(),
237
+ option.amount,
238
+ option.network,
239
+ bucket
240
+ ].join("|");
241
+ return `x402:${crypto.createHash("sha256").update(material).digest("hex").slice(0, 16)}`;
242
+ }
130
243
  function encodePaymentProof(receipt) {
131
244
  const payload = {
132
245
  x402Version: 2,
246
+ resource: receipt.resourceUrl ? { url: receipt.resourceUrl } : void 0,
247
+ accepted: receipt.accepted,
133
248
  payload: {
249
+ type: "haven_tx_hash",
134
250
  txHash: receipt.txHash,
135
251
  paymentId: receipt.paymentId,
136
- settledVia: "haven"
252
+ settledVia: "haven",
253
+ payer: receipt.payer,
254
+ chainId: receipt.chainId
137
255
  }
138
256
  };
139
257
  return btoa(JSON.stringify(payload));
@@ -146,25 +264,35 @@ var CHAIN_EXPLORER_TX = {
146
264
  8453: "https://basescan.org/tx"
147
265
  };
148
266
  function buildExplorerUrl(chainId, txHash) {
149
- const base = CHAIN_EXPLORER_TX[chainId ?? 100] ?? CHAIN_EXPLORER_TX[100];
267
+ const base = CHAIN_EXPLORER_TX[chainId ?? 8453] ?? CHAIN_EXPLORER_TX[8453];
150
268
  return `${base}/${txHash}`;
151
269
  }
152
270
  var DEFAULT_REQUEST_TIMEOUT = 3e4;
153
271
  var DEFAULT_CONFIRMATION_TIMEOUT = 9e4;
154
272
  var DEFAULT_POLLING_INTERVAL = 3e3;
273
+ function chainIdFromNetwork(network) {
274
+ if (network === "base") return 8453;
275
+ if (!network?.startsWith("eip155:")) return void 0;
276
+ const chainId = Number(network.slice("eip155:".length));
277
+ return Number.isFinite(chainId) ? chainId : void 0;
278
+ }
155
279
  var HavenClient = class {
156
280
  apiKey;
157
281
  delegateKey;
158
282
  baseUrl;
283
+ x402Wallet;
159
284
  requestTimeout;
160
285
  confirmationTimeout;
161
286
  pollingInterval;
287
+ inFlightX402 = /* @__PURE__ */ new Map();
288
+ x402ReceiptCache = /* @__PURE__ */ new Map();
162
289
  /** Delegate address derived from the private key (if provided) */
163
290
  delegateAddress;
164
291
  constructor(config) {
165
292
  this.apiKey = config.apiKey;
166
293
  this.delegateKey = config.delegateKey;
167
294
  this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
295
+ this.x402Wallet = config.x402Wallet;
168
296
  this.requestTimeout = config.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT;
169
297
  this.confirmationTimeout = config.confirmationTimeout ?? DEFAULT_CONFIRMATION_TIMEOUT;
170
298
  this.pollingInterval = config.pollingInterval ?? DEFAULT_POLLING_INTERVAL;
@@ -204,6 +332,13 @@ var HavenClient = class {
204
332
  amount: request.amount,
205
333
  to: request.to
206
334
  });
335
+ if (raw.status === "pending_approval") {
336
+ throw new HavenApiError(
337
+ `Payment exceeds the on-chain allowance and was queued for owner approval (payment_id: ${raw.payment_id}).`,
338
+ 202,
339
+ raw
340
+ );
341
+ }
207
342
  return {
208
343
  paymentId: raw.payment_id,
209
344
  status: "pending_signature",
@@ -271,8 +406,9 @@ var HavenClient = class {
271
406
  /**
272
407
  * Authorize an x402 payment.
273
408
  *
274
- * Takes the parsed PaymentRequired from a 402 response, selects a
275
- * compatible payment option, signs and executes the payment through Haven.
409
+ * Takes the parsed PaymentRequired from a 402 response, selects a compatible
410
+ * option, funds the delegate wallet through Haven, and returns the standard
411
+ * x402 header that the merchant can verify and settle.
276
412
  *
277
413
  * Requires `delegateKey` to be set in the client config.
278
414
  */
@@ -282,23 +418,43 @@ var HavenClient = class {
282
418
  "delegateKey is required for x402 payments. Pass it in the HavenClient config."
283
419
  );
284
420
  }
285
- const option = selectPaymentOption(paymentRequired.accepts);
421
+ if (!this.delegateAddress) {
422
+ throw new HavenSigningError("delegateAddress could not be derived from delegateKey.");
423
+ }
424
+ const option = selectStandardPaymentOption(paymentRequired.accepts);
286
425
  if (!option) {
287
426
  throw new HavenApiError(
288
- "No compatible payment option found in x402 requirements. Haven supports Gnosis Chain (eip155:100) and Base (eip155:8453).",
427
+ "No compatible payment option found in x402 requirements. Haven supports standard x402 exact payments on Base USDC.",
289
428
  400
290
429
  );
291
430
  }
431
+ const idempotencyKey = buildX402IdempotencyKey(paymentRequired, option);
432
+ const cached = this.x402ReceiptCache.get(idempotencyKey);
433
+ if (cached && cached.expiresAt > Date.now()) return cached.receipt;
434
+ const inFlight = this.inFlightX402.get(idempotencyKey);
435
+ if (inFlight) return inFlight;
436
+ const promise = this.authorizeStandardX402(paymentRequired, option, idempotencyKey);
437
+ this.inFlightX402.set(idempotencyKey, promise);
438
+ try {
439
+ return await promise;
440
+ } finally {
441
+ this.inFlightX402.delete(idempotencyKey);
442
+ }
443
+ }
444
+ async authorizeStandardX402(paymentRequired, option, idempotencyKey) {
445
+ const paymentHeader = await this.createStandardX402Header(paymentRequired, option);
292
446
  const raw = await this.post("/x402", {
293
447
  url: paymentRequired.resource.url,
294
- payTo: option.payTo,
448
+ payTo: this.delegateAddress,
449
+ merchantPayTo: option.payTo,
295
450
  amount: option.amount,
296
451
  asset: option.asset,
297
452
  network: option.network,
298
- description: paymentRequired.resource.description
453
+ description: paymentRequired.resource.description,
454
+ idempotencyKey
299
455
  });
300
456
  if (raw.success && raw.tx_hash) {
301
- return {
457
+ const receipt2 = {
302
458
  success: true,
303
459
  paymentId: raw.payment_id,
304
460
  txHash: raw.tx_hash,
@@ -306,8 +462,15 @@ var HavenClient = class {
306
462
  amount: raw.amount ?? "",
307
463
  to: raw.to ?? "",
308
464
  resourceUrl: paymentRequired.resource.url,
309
- explorerUrl: raw.explorer_url ?? (raw.tx_hash ? buildExplorerUrl(raw.chain_id, raw.tx_hash) : "")
465
+ explorerUrl: raw.explorer_url ?? (raw.tx_hash ? buildExplorerUrl(raw.chain_id, raw.tx_hash) : ""),
466
+ accepted: option,
467
+ paymentHeader,
468
+ merchantTo: raw.merchant_to ?? option.payTo,
469
+ payer: raw.payer ?? raw.safe_address,
470
+ chainId: raw.chain_id ?? chainIdFromNetwork(option.network)
310
471
  };
472
+ this.cacheX402Receipt(idempotencyKey, paymentHeader, receipt2);
473
+ return receipt2;
311
474
  }
312
475
  if (!raw.sign_data?.hash) {
313
476
  throw new HavenApiError("No sign_hash returned from x402/authorize", 500, raw);
@@ -324,7 +487,7 @@ var HavenClient = class {
324
487
  execResult
325
488
  );
326
489
  }
327
- return {
490
+ const receipt = {
328
491
  success: true,
329
492
  paymentId: raw.payment_id,
330
493
  txHash: execResult.tx_hash ?? "",
@@ -332,8 +495,15 @@ var HavenClient = class {
332
495
  amount: execResult.amount ?? raw.amount ?? "",
333
496
  to: execResult.to ?? raw.to ?? "",
334
497
  resourceUrl: paymentRequired.resource.url,
335
- explorerUrl: execResult.explorer_url ?? (execResult.tx_hash ? buildExplorerUrl(execResult.chain_id, execResult.tx_hash) : "")
498
+ explorerUrl: execResult.explorer_url ?? (execResult.tx_hash ? buildExplorerUrl(execResult.chain_id, execResult.tx_hash) : ""),
499
+ accepted: option,
500
+ paymentHeader,
501
+ merchantTo: option.payTo,
502
+ payer: raw.payer ?? raw.safe_address ?? raw.sign_data?.components.safe,
503
+ chainId: execResult.chain_id ?? raw.chain_id ?? chainIdFromNetwork(option.network)
336
504
  };
505
+ this.cacheX402Receipt(idempotencyKey, paymentHeader, receipt);
506
+ return receipt;
337
507
  }
338
508
  /**
339
509
  * Fetch wrapper that automatically handles HTTP 402 responses.
@@ -349,21 +519,82 @@ var HavenClient = class {
349
519
  * Requires `delegateKey` to be set in the client config.
350
520
  */
351
521
  async fetch(url, init) {
352
- const response = await globalThis.fetch(url, init);
522
+ const initialInit = this.withX402Wallet(init, this.x402PayerAddress());
523
+ const response = await globalThis.fetch(url, initialInit);
353
524
  if (response.status !== 402) return response;
354
525
  let paymentRequired;
355
526
  try {
356
- paymentRequired = parsePaymentRequired(response);
527
+ paymentRequired = await parsePaymentRequiredResponse(response);
357
528
  } catch {
358
529
  return response;
359
530
  }
360
531
  const receipt = await this.authorizeX402(paymentRequired);
361
- const retryHeaders = new Headers(init?.headers);
362
- retryHeaders.set("PAYMENT-SIGNATURE", encodePaymentProof(receipt));
363
- return globalThis.fetch(url, {
364
- ...init,
532
+ if (!receipt.accepted) {
533
+ throw new HavenApiError("No accepted x402 option was recorded for payment retry", 500);
534
+ }
535
+ if (!receipt.paymentHeader) {
536
+ throw new HavenApiError("No x402 payment header was returned for payment retry", 500);
537
+ }
538
+ const retryHeaders = new Headers(initialInit?.headers);
539
+ retryHeaders.set("X-PAYMENT", receipt.paymentHeader);
540
+ const retryResponse = await globalThis.fetch(url, {
541
+ ...initialInit,
365
542
  headers: retryHeaders
366
543
  });
544
+ if (retryResponse.status === 402) {
545
+ throw new HavenApiError(
546
+ "x402 retry was rejected after Haven funded the delegate wallet; reconciliation may be required.",
547
+ 402,
548
+ {
549
+ marker: "x402_retry_rejected_after_funding",
550
+ payment_id: receipt.paymentId,
551
+ tx_hash: receipt.txHash,
552
+ resource_url: receipt.resourceUrl,
553
+ merchant_to: receipt.merchantTo,
554
+ delegate_to: receipt.to
555
+ }
556
+ );
557
+ }
558
+ return retryResponse;
559
+ }
560
+ async createStandardX402Header(paymentRequired, option) {
561
+ if (!this.delegateKey) {
562
+ throw new HavenSigningError("delegateKey is required to sign x402 payment headers.");
563
+ }
564
+ const account = accounts.privateKeyToAccount(this.delegateKey);
565
+ const requirements = toStandardPaymentRequirements(paymentRequired, option);
566
+ const header = await schemes.exact.evm.createPaymentHeader(
567
+ account,
568
+ paymentRequired.x402Version,
569
+ requirements
570
+ );
571
+ if (paymentRequired.x402Version < 2) return header;
572
+ const payment = decodeBase64Json2(header);
573
+ return btoa(JSON.stringify({
574
+ x402Version: paymentRequired.x402Version,
575
+ accepted: option,
576
+ payload: payment.payload
577
+ }));
578
+ }
579
+ cacheX402Receipt(idempotencyKey, paymentHeader, receipt) {
580
+ const expiresAt = getPaymentHeaderValidBefore(paymentHeader);
581
+ if (expiresAt > Date.now()) {
582
+ this.x402ReceiptCache.set(idempotencyKey, { expiresAt, receipt });
583
+ }
584
+ }
585
+ x402PayerAddress() {
586
+ return this.delegateAddress ?? this.x402Wallet;
587
+ }
588
+ withX402Wallet(init, wallet = this.x402PayerAddress()) {
589
+ if (!wallet) return init;
590
+ const headers = new Headers(init?.headers);
591
+ if (!headers.has("x402-wallet")) {
592
+ headers.set("x402-wallet", wallet);
593
+ }
594
+ return {
595
+ ...init,
596
+ headers
597
+ };
367
598
  }
368
599
  // ── Tool Execution (for agent frameworks) ────────────────────────
369
600
  /**
@@ -426,7 +657,11 @@ var HavenClient = class {
426
657
  amount: receipt.amount,
427
658
  to: receipt.to,
428
659
  resource_url: receipt.resourceUrl,
429
- explorer_url: receipt.explorerUrl
660
+ explorer_url: receipt.explorerUrl,
661
+ payment_header: receipt.paymentHeader,
662
+ merchant_to: receipt.merchantTo,
663
+ payer: receipt.payer,
664
+ chain_id: receipt.chainId
430
665
  };
431
666
  } catch (err) {
432
667
  return {
@@ -512,6 +747,21 @@ var HavenClient = class {
512
747
  function sleep(ms) {
513
748
  return new Promise((resolve) => setTimeout(resolve, ms));
514
749
  }
750
+ function getPaymentHeaderValidBefore(paymentHeader) {
751
+ try {
752
+ const payment = decodeBase64Json2(
753
+ paymentHeader
754
+ );
755
+ const payload = payment.payload;
756
+ const validBeforeSeconds = Number(payload.authorization?.validBefore);
757
+ if (Number.isFinite(validBeforeSeconds)) return validBeforeSeconds * 1e3;
758
+ } catch {
759
+ }
760
+ return 0;
761
+ }
762
+ function decodeBase64Json2(value) {
763
+ return JSON.parse(atob(value));
764
+ }
515
765
 
516
766
  // src/tools.ts
517
767
  var makePaymentSchema = {
@@ -578,7 +828,7 @@ var authorizeX402Schema = {
578
828
  };
579
829
  var MAKE_PAYMENT_DESCRIPTION = "Send a payment from the Haven-managed Safe wallet. The payment will be validated against the agent's on-chain spending policy. Gnosis Chain tokens: EURe, USDC.e, xDAI. Base tokens: USDC, ETH.";
580
830
  var GET_STATUS_DESCRIPTION = "Check the status of a previously initiated payment. Returns the current status, transaction hash (if confirmed), and payment details.";
581
- var AUTHORIZE_X402_DESCRIPTION = "Authorize payment for an HTTP 402 (Payment Required) response. When a paid API returns 402 with x402 payment requirements, use this tool to pay and get access. Haven evaluates the payment against policy and executes from the Safe wallet.";
831
+ var AUTHORIZE_X402_DESCRIPTION = "Authorize payment for an HTTP 402 (Payment Required) response. When a paid API returns 402 with x402 payment requirements, use this tool to fund the agent wallet and get a merchant payment header. Haven evaluates the payment against policy before moving funds from the Haven wallet. Use the returned payment_header as the X-PAYMENT header on the retry request.";
582
832
  function claudeTools() {
583
833
  return [
584
834
  {
@@ -642,6 +892,7 @@ exports.addressFromKey = addressFromKey;
642
892
  exports.encodePaymentProof = encodePaymentProof;
643
893
  exports.havenTools = havenTools;
644
894
  exports.parsePaymentRequired = parsePaymentRequired;
895
+ exports.parsePaymentRequiredResponse = parsePaymentRequiredResponse;
645
896
  exports.selectPaymentOption = selectPaymentOption;
646
897
  exports.signHash = signHash;
647
898
  exports.verifySignature = verifySignature;