@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/README.md +581 -0
- package/dist/index.cjs +4726 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +2808 -0
- package/dist/index.d.ts +2808 -0
- package/dist/index.js +4628 -0
- package/dist/index.js.map +1 -0
- package/examples/mcp-x402-sse.ts +149 -0
- package/examples/x402_openapi_python.py +119 -0
- package/package.json +67 -0
package/README.md
ADDED
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
# @haven_ai/sdk
|
|
2
|
+
|
|
3
|
+
TypeScript SDK for [Haven](https://github.com/d-hinders/Haven-AI) — agent wallet infrastructure for the autonomous economy.
|
|
4
|
+
|
|
5
|
+
Haven lets AI agents request and sign payments within strict, user-approved on-chain guardrails. This SDK makes it straightforward to integrate Haven payment requests into any agent without giving Haven custody of user or agent keys.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @haven_ai/sdk@alpha
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { HavenClient } from '@haven_ai/sdk'
|
|
17
|
+
|
|
18
|
+
const haven = new HavenClient({
|
|
19
|
+
apiKey: 'sk_agent_xxx', // from Haven dashboard
|
|
20
|
+
delegateKey: '0x...', // agent's delegate EOA private key
|
|
21
|
+
baseUrl: 'http://localhost:3001', // Haven API URL
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
// One-liner payment — handles intent, signing, submission, and confirmation
|
|
25
|
+
const result = await haven.pay({
|
|
26
|
+
token: 'EURe',
|
|
27
|
+
amount: '5.00',
|
|
28
|
+
to: '0xabc...',
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
console.log(result.txHash) // 0x...
|
|
32
|
+
console.log(result.explorerUrl) // https://gnosisscan.io/tx/0x... (or basescan.org for Base)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Pay for an x402 resource
|
|
36
|
+
|
|
37
|
+
Point `haven.fetch` at any HTTP resource gated behind `402 Payment Required` —
|
|
38
|
+
the SDK detects the 402, pays through Haven, and retries automatically:
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
import { HavenClient } from '@haven_ai/sdk'
|
|
42
|
+
|
|
43
|
+
const haven = new HavenClient({
|
|
44
|
+
apiKey: process.env.HAVEN_API_KEY!, // from Haven dashboard
|
|
45
|
+
delegateKey: process.env.DELEGATE_KEY!, // agent's delegate private key
|
|
46
|
+
baseUrl: 'https://havenbackend-production-8a00.up.railway.app', // hosted Haven, or your self-hosted URL
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
// haven.fetch handles 402 → pay → retry automatically
|
|
50
|
+
const response = await haven.fetch('https://your-x402-endpoint.example/resource')
|
|
51
|
+
const data = await response.json()
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The payment fits within the agent's on-chain budget — anything outside it is
|
|
55
|
+
declined before any money moves, never queued for you to approve later — and it
|
|
56
|
+
shows up in your Haven dashboard activity feed.
|
|
57
|
+
|
|
58
|
+
## Supported Networks & Tokens
|
|
59
|
+
|
|
60
|
+
| Network | CAIP-2 | Tokens |
|
|
61
|
+
|---------|--------|--------|
|
|
62
|
+
| Gnosis Chain | `eip155:100` | EURe, USDC.e, xDAI |
|
|
63
|
+
| Base | `eip155:8453` | USDC, ETH |
|
|
64
|
+
|
|
65
|
+
## Credential Lifecycle
|
|
66
|
+
|
|
67
|
+
- The Haven API key identifies the agent. It is not payment authority.
|
|
68
|
+
- The delegate key signs payment payloads locally. Haven's backend never receives it.
|
|
69
|
+
- The agent's on-chain budget delegation enforces the agent budget: budget, recipient and expiry are checked by audited caveat enforcers at redemption, not by an off-chain rules engine.
|
|
70
|
+
- `getAllowances()` / `get_allowances` is the right path for budget, remaining amount, reset period, or "what can I spend?" questions.
|
|
71
|
+
- If an API key is exposed or lost, rotate it from the Haven agent detail page. The new key is shown once and the old key stops working.
|
|
72
|
+
- If a delegate key is exposed or lost, a delegation-rail agent is **re-keyed** rather than replaced — same agent, new signing key, budget remainder carried. See [Replacing an agent's signing key](../../docs/product/agent-key-rotation.md).
|
|
73
|
+
|
|
74
|
+
## Step-by-Step API
|
|
75
|
+
|
|
76
|
+
Discovery and listing: `discoverTools({ verified?: 'any' | 'verified' | 'operator' })` returns the merged
|
|
77
|
+
catalog — operator-curated plus `verified_payable` directory entries (epic #1717), each with `source`,
|
|
78
|
+
`domainVerified` and `verifiedPayable`. `submitCatalogEntry(resourceUrl)` submits a merchant endpoint to
|
|
79
|
+
the Verified Payable Directory (queue-only; the seller still must prove domain ownership before listing),
|
|
80
|
+
and `getCatalogSubmissionStatus(id)` returns coarse status plus the ownership-proof instructions while
|
|
81
|
+
the submission can still prove ownership.
|
|
82
|
+
|
|
83
|
+
For agents that need control over each step (e.g., external signing):
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
// Step 1: Create a payment intent
|
|
87
|
+
const intent = await haven.createIntent({
|
|
88
|
+
token: 'USDC',
|
|
89
|
+
amount: '5.00',
|
|
90
|
+
to: '0xabc...',
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
// Step 2: Sign the hash (or sign externally)
|
|
94
|
+
const signature = haven.sign(intent.signData.hash)
|
|
95
|
+
|
|
96
|
+
// Step 3: Submit the signature
|
|
97
|
+
await haven.submitSignature(intent.paymentId, signature)
|
|
98
|
+
|
|
99
|
+
// Step 4: Wait for on-chain confirmation
|
|
100
|
+
const result = await haven.waitForConfirmation(intent.paymentId)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## x402 Protocol Support
|
|
104
|
+
|
|
105
|
+
Production merchant acceptance, facilitator, settlement, fiat, or acquiring functionality needs separate product and legal review under the repo's [CASP / MiCA guardrails](../../docs/regulatory/casp-risk-guardrails.md). The hosted x402 endpoint is an internal technical demo, not a merchant settlement product.
|
|
106
|
+
|
|
107
|
+
The SDK supports [x402](https://x402.org) client flows. When an API returns HTTP 402, the SDK evaluates the challenge against the agent's on-chain budget, uses the configured delegate key for the required signature, and retries automatically:
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
// Automatic — fetch() intercepts 402, pays, and retries.
|
|
111
|
+
// Use a stable idempotencyKey so one user intent stays one Haven payment.
|
|
112
|
+
const response = await haven.fetch(
|
|
113
|
+
'https://paid-api.example.com/data',
|
|
114
|
+
undefined,
|
|
115
|
+
{ idempotencyKey: 'paid-api-data-2026-05-22' },
|
|
116
|
+
)
|
|
117
|
+
const data = await response.json()
|
|
118
|
+
|
|
119
|
+
// Manual — parse and authorize the 402 yourself
|
|
120
|
+
import { parsePaymentRequiredResponse } from '@haven_ai/sdk'
|
|
121
|
+
|
|
122
|
+
const apiResponse = await fetch('https://paid-api.example.com/data')
|
|
123
|
+
if (apiResponse.status === 402) {
|
|
124
|
+
const paymentRequired = await parsePaymentRequiredResponse(apiResponse)
|
|
125
|
+
const receipt = await haven.authorizeX402(paymentRequired, {
|
|
126
|
+
idempotencyKey: 'paid-api-data-2026-05-22',
|
|
127
|
+
})
|
|
128
|
+
// Retry with { 'PAYMENT-SIGNATURE': receipt.paymentHeader }, and on the
|
|
129
|
+
// EIP-3009 bridge 'X-PAYMENT' too. NEVER both on erc7710 — see below.
|
|
130
|
+
console.log(receipt.explorerUrl)
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Idempotency: what the key guarantees, and what it costs
|
|
135
|
+
|
|
136
|
+
**Omit `idempotencyKey` and the SDK synthesises one** from the merchant's
|
|
137
|
+
resource URL and description, the payee, asset, amount, network, and a
|
|
138
|
+
**5-minute time bucket**. Every one of those inputs except the bucket
|
|
139
|
+
describes *the product*, so the guarantee is:
|
|
140
|
+
|
|
141
|
+
> Repeated calls for the same product, at the same price, within the same
|
|
142
|
+
> 5-minute bucket are **one payment**.
|
|
143
|
+
|
|
144
|
+
That is what makes a retried HTTP request safe by default — a dropped
|
|
145
|
+
connection or a re-run tool call cannot pay twice.
|
|
146
|
+
|
|
147
|
+
**The cost is the flip side of the same rule.** The SDK cannot tell a retry
|
|
148
|
+
from a *deliberate* second purchase of the same item: both re-fetch the 402
|
|
149
|
+
and produce identical key material. So a genuine second purchase inside the
|
|
150
|
+
window collapses onto the first payment. When that happens the SDK does not
|
|
151
|
+
hand you a fresh authorization — the funds are already spent, and any
|
|
152
|
+
authorization it minted would be unfundable. It throws
|
|
153
|
+
`X402AlreadySettledError`, carrying the original receipt:
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
import { X402AlreadySettledError } from '@haven_ai/sdk'
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
await haven.fetch('https://paid-api.example.com/data')
|
|
160
|
+
} catch (err) {
|
|
161
|
+
if (err instanceof X402AlreadySettledError) {
|
|
162
|
+
// The FIRST payment's receipt — real, settled funds.
|
|
163
|
+
console.log(err.receipt.paymentId, err.receipt.txHash)
|
|
164
|
+
// 'settled' — the delegate was checked on-chain and cannot fund again.
|
|
165
|
+
// 'unverifiable' — no chainRpcs entry for the chain, so it was not checked.
|
|
166
|
+
console.log(err.basis)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**To buy the same item twice, pass distinct keys** — that is the supported
|
|
172
|
+
way to say "this is a new purchase, not a retry":
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
await haven.fetch(url, init, { idempotencyKey: `vpn-renewal:${orderId}` })
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Configure `chainRpcs` for your chain. Without it the SDK cannot check the
|
|
179
|
+
delegate's balance, so an accidental key collision refuses on the weaker
|
|
180
|
+
`unverifiable` basis rather than risk issuing an authorization it cannot vouch
|
|
181
|
+
for.
|
|
182
|
+
|
|
183
|
+
**Resuming is not affected by that.** When you are following the documented
|
|
184
|
+
resume flow — re-calling after a funding leg confirms, or calling
|
|
185
|
+
`resumeAuthorizedX402({ paymentId })` — you named the payment, so an
|
|
186
|
+
unverifiable balance lets the resume proceed as before. Only a balance
|
|
187
|
+
verified *absent* refuses there. The stricter default applies solely to the
|
|
188
|
+
case where two purchases collided on a key you did not choose.
|
|
189
|
+
|
|
190
|
+
This applies to the **EIP-3009 funding-leg** scheme, which routes money
|
|
191
|
+
through the delegate EOA. **erc7710 direct settlement is unaffected**: it has
|
|
192
|
+
no funding leg and no delegate balance to exhaust.
|
|
193
|
+
|
|
194
|
+
For agents that need to inspect the price before paying, use the quote-first
|
|
195
|
+
path. `quoteX402()` probes the merchant and parses the HTTP 402 response, but it
|
|
196
|
+
does not create a Haven payment, signature, or on-chain transaction.
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
const quote = await haven.quoteX402(
|
|
200
|
+
'https://paid-api.example.com/data',
|
|
201
|
+
undefined,
|
|
202
|
+
{ idempotencyKey: 'paid-api-data-2026-05-22' },
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
if (Number(quote.amount) > 0.05) {
|
|
206
|
+
throw new Error(`Price ${quote.amount} ${quote.token} is above the user cap`)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const response = await haven.payX402Quote(quote)
|
|
210
|
+
const data = await response.json()
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Merchant-verified x402 retries use the official EIP-3009 `exact` scheme on Base USDC (`base` / `eip155:8453`). `haven.fetch()` sends the payment under `PAYMENT-SIGNATURE` (the x402 v2 name), and on the EIP-3009 bridge also under `X-PAYMENT` (v1), so a merchant on either version reads it. **On erc7710 it sends the v2 name ALONE**: that payload is always x402 v2, and its header carries a whole delegation chain, so duplicating it overflows the merchant's header limit and the request is refused with HTTP 431. If you build the retry yourself, follow the same rule. Haven's older tx-hash proof helper remains exported for Haven-native integrations; it is a different payload that happens to have shared the v2 name, and it is not what `haven.fetch()` sends.
|
|
214
|
+
|
|
215
|
+
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.
|
|
216
|
+
|
|
217
|
+
## AI Agent Integration
|
|
218
|
+
|
|
219
|
+
### Pre-built Tool Definitions
|
|
220
|
+
|
|
221
|
+
The SDK ships with ready-made tool schemas for Claude and OpenAI:
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
import { HavenClient, havenTools } from '@haven_ai/sdk'
|
|
225
|
+
import Anthropic from '@anthropic-ai/sdk'
|
|
226
|
+
|
|
227
|
+
const haven = new HavenClient({ apiKey, delegateKey })
|
|
228
|
+
const anthropic = new Anthropic()
|
|
229
|
+
|
|
230
|
+
const response = await anthropic.messages.create({
|
|
231
|
+
model: 'claude-opus-4-7',
|
|
232
|
+
tools: havenTools.claude(), // or havenTools.openai() for OpenAI
|
|
233
|
+
messages: [{ role: 'user', content: 'Pay 5 EURe to 0xabc for API access' }],
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
// Handle tool calls
|
|
237
|
+
for (const block of response.content) {
|
|
238
|
+
if (block.type === 'tool_use') {
|
|
239
|
+
const result = await haven.executeTool(block.name, block.input)
|
|
240
|
+
// send result back to the model
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Available Tools
|
|
246
|
+
|
|
247
|
+
| Tool | Description |
|
|
248
|
+
|------|-------------|
|
|
249
|
+
| `make_payment` | Request and sign a payment from the user-controlled account within its on-chain budget |
|
|
250
|
+
| `get_payment_status` | Check the status of a payment intent |
|
|
251
|
+
| `get_allowances` | Read configured and on-chain budget state, including spent and remaining budget |
|
|
252
|
+
| `authorize_x402_payment` | Authorize a policy-limited x402 payment and return a payment header for an HTTP 402 resource |
|
|
253
|
+
| `resume_x402_payment` | Resume an authorized x402 payment and return a merchant payment header without creating a duplicate payment |
|
|
254
|
+
|
|
255
|
+
Use `get_allowances` for allowance, budget, spend-limit, remaining amount, reset-period, or "what can I spend?" questions. Payment tools still require the agent-held delegate key and the on-chain budget delegation; the Haven API key identifies the agent but does not authorize spending by itself.
|
|
256
|
+
|
|
257
|
+
## Configuration
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
const haven = new HavenClient({
|
|
261
|
+
apiKey: 'sk_agent_xxx', // required — Haven agent API key
|
|
262
|
+
delegateKey: '0x...', // optional — enables .pay() and .sign()
|
|
263
|
+
baseUrl: 'http://localhost:3001', // default
|
|
264
|
+
x402Wallet: '0x...', // optional fallback when no delegate key is configured
|
|
265
|
+
requestTimeout: 30000, // per-request timeout (ms)
|
|
266
|
+
confirmationTimeout: 90000, // polling timeout (ms)
|
|
267
|
+
pollingInterval: 3000, // polling interval (ms)
|
|
268
|
+
})
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## OpenAPI
|
|
272
|
+
|
|
273
|
+
The backend serves an OpenAPI 3.1 contract at:
|
|
274
|
+
|
|
275
|
+
- Production: `https://havenbackend-production-8a00.up.railway.app/openapi.json`
|
|
276
|
+
- Local development: `http://localhost:3001/openapi.json`
|
|
277
|
+
|
|
278
|
+
The spec covers the agent-facing payment surface: agents, direct payments,
|
|
279
|
+
payment status, x402 authorization, resume-state rehydration, machine-payment
|
|
280
|
+
receipts, and transactions. `POST /machine-payments/authorize` (the legacy
|
|
281
|
+
internal MPP demo challenge flow) is retired — it now refuses unconditionally
|
|
282
|
+
with HTTP 410; use the x402 flow for agent-to-merchant payments. Its security
|
|
283
|
+
scheme is deliberate: the Haven API key identifies the agent, but payment
|
|
284
|
+
authority still requires an agent-held delegate signature and an on-chain
|
|
285
|
+
budget delegation.
|
|
286
|
+
|
|
287
|
+
## Agent payment state machine
|
|
288
|
+
|
|
289
|
+
Every payment state returned by Haven includes:
|
|
290
|
+
|
|
291
|
+
- `phase`: where the Haven-side payment currently is.
|
|
292
|
+
- `nextAction`: the stable action an agent should take next.
|
|
293
|
+
- `rail`: which payment rail produced the state. Categorical values (`direct`, `x402`, `mpp`) appear on resume-state discriminators; granular values (`mpp_demo`, `mpp_crypto`, `stripe_deposit`, `spt`) appear on response bodies. The `mpp` resume-state shape is a historical read only — the SDK no longer exposes a client method that acts on it (`mpp_demo` is retired, #1328).
|
|
294
|
+
- `message`: human-readable guidance for the same state.
|
|
295
|
+
|
|
296
|
+
The enum values and JSON Schema fragments are exported from `@haven_ai/sdk`:
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
import {
|
|
300
|
+
AgentPaymentNextAction,
|
|
301
|
+
AgentPaymentNextActionSchema,
|
|
302
|
+
AgentPaymentFailureCode,
|
|
303
|
+
AgentPaymentFailureCodeSchema,
|
|
304
|
+
AgentPaymentPhase,
|
|
305
|
+
AgentPaymentPhaseSchema,
|
|
306
|
+
AgentPaymentRail,
|
|
307
|
+
AgentPaymentRailSchema,
|
|
308
|
+
} from '@haven_ai/sdk'
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### Flow diagram
|
|
312
|
+
|
|
313
|
+
```text
|
|
314
|
+
┌─────────────────────────┐
|
|
315
|
+
│ agent_signature_required│
|
|
316
|
+
└──────────┬──────────────┘
|
|
317
|
+
│ sign_and_submit_payment
|
|
318
|
+
▼
|
|
319
|
+
┌─────────────────────────┐
|
|
320
|
+
│ payment_submitted │
|
|
321
|
+
└──────────┬──────────────┘
|
|
322
|
+
│ check_status_later
|
|
323
|
+
▼
|
|
324
|
+
┌─────────────────────────┐
|
|
325
|
+
│ payment_confirmed (✔) │
|
|
326
|
+
└─────────────────────────┘
|
|
327
|
+
|
|
328
|
+
(EIP-3009 bridge only. `funding_sent` is Haven's funding leg confirming —
|
|
329
|
+
value left the treasury and sits on the delegate EOA. `executed` is the
|
|
330
|
+
agent's own merchant retry succeeding; Haven has no phase for the merchant
|
|
331
|
+
leg itself. See the `retry_original_x402_request` row below.)
|
|
332
|
+
┌───────────────────────┐
|
|
333
|
+
│ funding_sent │
|
|
334
|
+
└──────────┬────────────┘
|
|
335
|
+
│ retry_original_x402_request (x402, after the
|
|
336
|
+
│ merchant-report grace window — #2145)
|
|
337
|
+
│ none (direct / erc7710)
|
|
338
|
+
▼
|
|
339
|
+
┌───────────────────────┐
|
|
340
|
+
│ executed (✔) │
|
|
341
|
+
└───────────────────────┘
|
|
342
|
+
|
|
343
|
+
Terminal from any non-confirmed phase:
|
|
344
|
+
rejected → stop_and_tell_user
|
|
345
|
+
failed → stop_and_tell_user
|
|
346
|
+
expired → request_again_if_user_still_wants_it
|
|
347
|
+
|
|
348
|
+
x402 tool-window failures:
|
|
349
|
+
expired funding/quote window → PAYMENT_WINDOW_EXPIRED → re-quote with same idempotency_key
|
|
350
|
+
merchant rejection after funding → MERCHANT_REJECTED_AFTER_FUNDING → haven_sweep_delegate
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### `phase` reference
|
|
354
|
+
|
|
355
|
+
| `phase` | Meaning | Terminal? |
|
|
356
|
+
|---------|---------|-----------|
|
|
357
|
+
| `agent_signature_required` | Haven prepared a payment intent; the agent must sign and submit. | no |
|
|
358
|
+
| `payment_submitted` | Haven received the signed payment; the agent should poll for confirmation. | no |
|
|
359
|
+
| `payment_confirmed` | Direct payment is confirmed on chain. | yes |
|
|
360
|
+
| `user_approval_required` | **No live rail produces it.** Described the retired Safe rail's approval queue; kept in the exported enum for wire compatibility only. A payment outside the budget is now declined outright — see [Payments outside the agent's budget](#payments-outside-the-agents-budget). | n/a |
|
|
361
|
+
| `user_execution_required` | **No live rail produces it.** Same retirement as above. | n/a |
|
|
362
|
+
| `waiting_for_additional_approvals` | **No live rail produces it.** Same retirement as above. | n/a |
|
|
363
|
+
| `funding_sent` | Haven funding leg landed; the agent can continue the merchant/protocol leg. Only the EIP-3009 bridge has a funding leg; erc7710 direct settlement has none. | no |
|
|
364
|
+
| `rejected` | The payment was rejected and cannot proceed. | yes |
|
|
365
|
+
| `expired` | Payment expired before completion. | yes |
|
|
366
|
+
| `failed` | Haven could not complete the payment. | yes |
|
|
367
|
+
|
|
368
|
+
The merchant settlement leg of x402 (and the MPP retry) is the agent's own request to the merchant — it does not have a Haven `phase`. The payment is `funding_sent` until the agent retries with the payment header (`PAYMENT-SIGNATURE`, plus `X-PAYMENT` on this bridged path) (x402) or the MPP proof header; from Haven's perspective the payment becomes `executed` only after the agent successfully resumes.
|
|
369
|
+
|
|
370
|
+
### `nextAction` reference
|
|
371
|
+
|
|
372
|
+
| `nextAction` | What the agent should do |
|
|
373
|
+
|--------------|--------------------------|
|
|
374
|
+
| `sign_and_submit_payment` | Sign with the delegate key and submit the payment to Haven. |
|
|
375
|
+
| `check_status_later` | Poll `getPaymentStatus(payment_id)` later. |
|
|
376
|
+
| `none` | Stop polling; no more action is needed for this payment id. |
|
|
377
|
+
| `wait_for_user_approval` | **No longer produced — nothing maps to it.** Retired with the Safe rail's approval queue; kept in the exported enum for wire compatibility. The SDK's own status mapping now answers `stop_and_tell_user` for the statuses that used to yield this. |
|
|
378
|
+
| `wait_for_user_to_complete_payment` | **No longer produced — nothing maps to it.** Same retirement as above. |
|
|
379
|
+
| `retry_original_x402_request` | Haven's funding leg confirmed but no merchant response was ever recorded — most often because the process crashed between the funding confirmation and the merchant retry (a 15-minute grace window applies before this fires; a client-reported merchant rejection instead yields `sweep_stranded_funds`). Call `resumeX402Payment()` with the preserved `resumeState`, or rehydrate it first with `getResumeState(payment_id)`. Do not start a new payment for the same purchase. |
|
|
380
|
+
| `stop_and_tell_user` | Stop retrying and tell the user the payment failed or was rejected. |
|
|
381
|
+
| `request_again_if_user_still_wants_it` | The request expired; ask again only if the user still wants the payment. |
|
|
382
|
+
| `payment_window_expired` | The x402 funding/quote window expired. Re-quote the same paid MCP tool call with the same `idempotency_key`, then sign the fresh `payload_hash`. |
|
|
383
|
+
| `sweep_stranded_funds` | A funding leg succeeded but the merchant/protocol leg did not settle. Stop retrying and use `haven_sweep_delegate` to recover stranded delegate funds. |
|
|
384
|
+
|
|
385
|
+
### Machine-readable recovery codes
|
|
386
|
+
|
|
387
|
+
Hosted MCP and signer tools also return stable `code` values on recoverable x402 failures:
|
|
388
|
+
|
|
389
|
+
| `code` | Meaning | Agent recovery |
|
|
390
|
+
|--------|---------|----------------|
|
|
391
|
+
| `PRICE_EXCEEDS_MAX` | The merchant-authoritative x402 price is above the caller's spending cap. No funding transfer was created. | Tell the user the live price exceeded the cap and retry only after they confirm a higher one. |
|
|
392
|
+
| `AMBIGUOUS_MAX_AMOUNT` | Both `max_amount` (atomic units) and `max_amount_human` (whole tokens) were sent 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. |
|
|
393
|
+
| `MAX_AMOUNT_UNCONVERTIBLE` | `max_amount_human` could not be converted against this quote's asset — its decimals are unknown to Haven, or the cap has more decimal places than the asset supports. Nothing was spent. | Round the cap to the asset's decimals, or re-send it as an exact atomic `max_amount`. |
|
|
394
|
+
| `PAYMENT_WINDOW_EXPIRED` | The funding/quote window closed before `haven_x402_sign_header`, `haven_submit`, or `haven_complete_mcp_tool` could finish. | Re-run `haven_pay_mcp_tool` with the same `idempotency_key`, then sign and complete the fresh quote. Payloads include `retry_with_new_quote: true`. |
|
|
395
|
+
| `MERCHANT_REJECTED_AFTER_FUNDING` | Haven's funding leg succeeded, but the merchant rejected the paid retry. | Stop retrying the merchant and call `haven_sweep_delegate` so the user can recover stranded delegate USDC. |
|
|
396
|
+
|
|
397
|
+
## Payments outside the agent's budget
|
|
398
|
+
|
|
399
|
+
Haven's policy is the agent's on-chain budget delegation — a period budget, an
|
|
400
|
+
optional recipient pin, and an expiry, each enforced by an audited caveat
|
|
401
|
+
enforcer at redemption. There is no off-chain rules engine and **no approval
|
|
402
|
+
queue**: the queue-and-approve path belonged to the retired Safe rail, which now
|
|
403
|
+
answers HTTP 410 at every agent-payment entry point.
|
|
404
|
+
|
|
405
|
+
If an agent requests a payment outside that policy, Haven **declines it before
|
|
406
|
+
any money moves** — during prepare, before anything is written and before the
|
|
407
|
+
agent is asked to sign. `POST /payments` answers `403` when no active delegation
|
|
408
|
+
authorizes that token and recipient, and `502` when the on-chain caveat check
|
|
409
|
+
rejects the amount, recipient or expiry; the x402 authorize path answers `403
|
|
410
|
+
delegation_budget_exceeded`. In every case the SDK raises `HavenApiError` and no
|
|
411
|
+
`payment_id` exists to poll.
|
|
412
|
+
|
|
413
|
+
Surface that to the user as a decline, not a wait: **nothing will arrive later.**
|
|
414
|
+
The fix is for the wallet owner to grant or raise the budget in Haven, after
|
|
415
|
+
which the agent can request the payment again. Do not retry in a loop, and do
|
|
416
|
+
not poll `getPaymentStatus()` hoping for an approval.
|
|
417
|
+
|
|
418
|
+
### Resuming an x402 payment
|
|
419
|
+
|
|
420
|
+
Resume was triggered by *funding confirmation*, not by an approval, and applied
|
|
421
|
+
only to the EIP-3009 bridge — erc7710 direct settlement has no funding leg and
|
|
422
|
+
nothing to resume.
|
|
423
|
+
|
|
424
|
+
> **Resume is reachable again (#2145).** If the agent process crashes after
|
|
425
|
+
> Haven's funding leg confirms but before the merchant retry is recorded, a
|
|
426
|
+
> later `getPaymentStatus(payment_id)` reports
|
|
427
|
+
> `nextAction: 'retry_original_x402_request'` — Haven's funding confirmed but
|
|
428
|
+
> the merchant has likely not been paid. Gate on that structured field, not on
|
|
429
|
+
> message prose: call `resumeX402Payment()` with the preserved `resumeState`, or
|
|
430
|
+
> rehydrate it first via `getResumeState(payment_id)`. Any other `nextAction`
|
|
431
|
+
> means the payment is not ready to resume — do not call it speculatively.
|
|
432
|
+
>
|
|
433
|
+
> Meanwhile: the `payX402*` helpers perform the merchant retry themselves, so
|
|
434
|
+
> the ordinary in-flight path never needs resume. Only reach for
|
|
435
|
+
> `resumeX402Payment()` after seeing the trigger on a later status check —
|
|
436
|
+
> never speculatively, and never as a substitute for a fresh payment.
|
|
437
|
+
|
|
438
|
+
When the agent used `quoteX402()` / `payX402Quote()`, the thrown
|
|
439
|
+
`HavenPaymentStateError` includes a serializable `resumeState`. Persist it with
|
|
440
|
+
the MCP session details and pass it back to `resumeX402Payment()`.
|
|
441
|
+
|
|
442
|
+
If the agent process restarts and only kept the `payment_id`, call
|
|
443
|
+
`getResumeState(payment_id)` to rehydrate the stored x402/MPP context from
|
|
444
|
+
Haven, then pass that state to the matching resume helper. For POST-based
|
|
445
|
+
merchant or MCP calls, rebuild the live request details before retrying; Haven
|
|
446
|
+
stores payment context, not the agent's local request stream.
|
|
447
|
+
|
|
448
|
+
```typescript
|
|
449
|
+
let resumeState
|
|
450
|
+
try {
|
|
451
|
+
await haven.payX402Quote(quote)
|
|
452
|
+
} catch (err) {
|
|
453
|
+
if (err instanceof HavenPaymentStateError && err.resumeState) {
|
|
454
|
+
resumeState = err.resumeState
|
|
455
|
+
console.log(err.paymentId, err.phase, err.nextAction)
|
|
456
|
+
console.log('Funding has not confirmed yet. Save resumeState and poll.')
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const status = await haven.getPaymentStatus('payment-id')
|
|
461
|
+
if (status.nextAction === AgentPaymentNextAction.RetryOriginalX402Request) {
|
|
462
|
+
resumeState ??= await haven.getResumeState(status.paymentId)
|
|
463
|
+
const response = await haven.resumeX402Payment(resumeState)
|
|
464
|
+
const data = await response.json()
|
|
465
|
+
}
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
Think of bridged x402 as two separate legs:
|
|
469
|
+
|
|
470
|
+
- Haven funding leg: the account funds the agent delegate wallet by redeeming
|
|
471
|
+
the budget delegation. Status fields such as `phase`, `nextAction`, and
|
|
472
|
+
`txHash` describe this leg. It is automatic and bounded by the budget — no
|
|
473
|
+
human step.
|
|
474
|
+
- Merchant x402 leg: after the funding leg is complete, the agent resumes the
|
|
475
|
+
same payment id and retries the original merchant request with the payment
|
|
476
|
+
header, under `PAYMENT-SIGNATURE` and — on this bridged path only —
|
|
477
|
+
`X-PAYMENT`.
|
|
478
|
+
Do not treat a new 402 probe or a new MCP session as a resume.
|
|
479
|
+
|
|
480
|
+
For manual HTTP stacks, use `resumeAuthorizedX402()` to get the merchant header
|
|
481
|
+
without retrying the request for you:
|
|
482
|
+
|
|
483
|
+
```typescript
|
|
484
|
+
const receipt = await haven.resumeAuthorizedX402({
|
|
485
|
+
paymentId: status.paymentId,
|
|
486
|
+
paymentRequired,
|
|
487
|
+
idempotencyKey: 'paid-api-data-2026-05-22',
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
await fetch('https://paid-api.example.com/data', {
|
|
491
|
+
headers: {
|
|
492
|
+
'PAYMENT-SIGNATURE': receipt.paymentHeader!,
|
|
493
|
+
// Bridged (EIP-3009) resume only. On erc7710 send the v2 name ALONE —
|
|
494
|
+
// that header carries a delegation chain and duplicating it is refused
|
|
495
|
+
// with HTTP 431.
|
|
496
|
+
'X-PAYMENT': receipt.paymentHeader!,
|
|
497
|
+
},
|
|
498
|
+
})
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
**When YOU make the retry, report the outcome (#2292).** `haven.fetch()` and
|
|
502
|
+
the `payX402*` helpers call the merchant themselves and write the evidence or
|
|
503
|
+
reconciliation record from what they observed. `resumeAuthorizedX402()` and the
|
|
504
|
+
raw MCP/SSE flow below deliberately do not — you hold the header and make the
|
|
505
|
+
call — so Haven cannot learn what happened unless you tell it:
|
|
506
|
+
|
|
507
|
+
```typescript
|
|
508
|
+
const response = await fetch('https://paid-api.example.com/data', {
|
|
509
|
+
headers: {
|
|
510
|
+
'PAYMENT-SIGNATURE': receipt.paymentHeader!,
|
|
511
|
+
// Bridged (EIP-3009) resume only — see the erc7710 note above.
|
|
512
|
+
'X-PAYMENT': receipt.paymentHeader!,
|
|
513
|
+
},
|
|
514
|
+
})
|
|
515
|
+
|
|
516
|
+
await haven.reportX402MerchantOutcome({
|
|
517
|
+
paymentId: status.paymentId,
|
|
518
|
+
outcome: response.ok ? 'accepted' : 'rejected',
|
|
519
|
+
merchantStatus: response.status,
|
|
520
|
+
})
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
A `rejected` report writes the same open `merchant_retry_rejected_after_payment`
|
|
524
|
+
reconciliation event the built-in retry writes, so the next
|
|
525
|
+
`getPaymentStatus()` answers `phase: funded_but_unsettled` /
|
|
526
|
+
`nextAction: sweep_stranded_funds` instead of reading as complete for the
|
|
527
|
+
fifteen-minute merchant-report grace window. An `accepted` report records the
|
|
528
|
+
merchant response so a delivered payment never enters that window at all.
|
|
529
|
+
|
|
530
|
+
It is evidence, not authority. The funding transaction hash and resource URL are
|
|
531
|
+
read from the payment's own record rather than taken from you — they are not
|
|
532
|
+
parameters — the call is scoped to your own agent's payments, and it changes no
|
|
533
|
+
amount, recipient or status. `outcome` must agree with `merchantStatus`
|
|
534
|
+
(`accepted` only for a 2xx), and an acceptance is terminal: a rejection reported
|
|
535
|
+
after a recorded merchant response is refused rather than re-flagging a
|
|
536
|
+
delivered payment as stranded.
|
|
537
|
+
|
|
538
|
+
For MCP/SSE x402 tools, keep the same MCP session and JSON-RPC payload where the
|
|
539
|
+
merchant requires it: initialize, retain `mcp-session-id`, send the original
|
|
540
|
+
`tools/call`, parse the 402 challenge, wait for the funding leg to confirm if
|
|
541
|
+
the payment is bridged, then resume with the same `payment_id` and retry the
|
|
542
|
+
original `tools/call`, setting BOTH `PAYMENT-SIGNATURE` (x402 v2) and `X-PAYMENT` (v1) to the header. Use a stable `idempotencyKey` for the
|
|
543
|
+
user intent so fresh merchant quotes or sessions do not become duplicate Haven
|
|
544
|
+
payments.
|
|
545
|
+
|
|
546
|
+
See [`examples/mcp-x402-sse.ts`](./examples/mcp-x402-sse.ts) for a complete
|
|
547
|
+
MCP flow with initialize, `mcp-session-id`, JSON-RPC `tools/call`, quote
|
|
548
|
+
inspection, saved resume state, and final retry.
|
|
549
|
+
|
|
550
|
+
## Error Handling
|
|
551
|
+
|
|
552
|
+
```typescript
|
|
553
|
+
import { HavenApiError, HavenPaymentStateError, HavenSigningError, HavenTimeoutError } from '@haven_ai/sdk'
|
|
554
|
+
|
|
555
|
+
try {
|
|
556
|
+
await haven.pay({ token: 'EURe', amount: '5.00', to: '0xabc...' })
|
|
557
|
+
} catch (err) {
|
|
558
|
+
if (err instanceof HavenPaymentStateError) {
|
|
559
|
+
console.log(err.paymentId, err.phase, err.nextAction)
|
|
560
|
+
}
|
|
561
|
+
if (err instanceof HavenApiError) {
|
|
562
|
+
console.log(err.statusCode, err.message) // API returned an error
|
|
563
|
+
}
|
|
564
|
+
if (err instanceof HavenSigningError) {
|
|
565
|
+
console.log(err.message) // Signing failed
|
|
566
|
+
}
|
|
567
|
+
if (err instanceof HavenTimeoutError) {
|
|
568
|
+
console.log(err.paymentId) // Confirmation timed out
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
`X402AlreadySettledError` extends `HavenApiError` (status 409) and is the one
|
|
574
|
+
error above that is **not** a failure to pay — it reports that the payment it
|
|
575
|
+
describes *succeeded*, earlier. Handle it before the generic `HavenApiError`
|
|
576
|
+
branch, and treat `err.receipt` as proof of purchase rather than retrying. See
|
|
577
|
+
[Idempotency](#idempotency-what-the-key-guarantees-and-what-it-costs).
|
|
578
|
+
|
|
579
|
+
## License
|
|
580
|
+
|
|
581
|
+
MIT
|