@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 +61 -7
- package/dist/index.cjs +286 -35
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +37 -3
- package/dist/index.d.ts +37 -3
- package/dist/index.js +286 -36
- package/dist/index.js.map +1 -1
- package/package.json +7 -3
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,
|
|
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 {
|
|
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 =
|
|
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
|
-
|
|
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-
|
|
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` |
|
|
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/
|
|
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
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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 ??
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
527
|
+
paymentRequired = await parsePaymentRequiredResponse(response);
|
|
357
528
|
} catch {
|
|
358
529
|
return response;
|
|
359
530
|
}
|
|
360
531
|
const receipt = await this.authorizeX402(paymentRequired);
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
|
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;
|