@haven_ai/sdk 0.1.3 → 0.1.4

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.
@@ -0,0 +1,149 @@
1
+ import { readFile, writeFile } from 'node:fs/promises'
2
+ import {
3
+ HavenClient,
4
+ HavenPaymentStateError,
5
+ type X402ResumeState,
6
+ } from '@haven_ai/sdk'
7
+
8
+ const mcpUrl = process.env.MCP_URL
9
+ const apiKey = process.env.HAVEN_API_KEY
10
+ const delegateKey = process.env.HAVEN_DELEGATE_KEY
11
+ const baseUrl = process.env.HAVEN_API_URL
12
+ const maxUsd = Number(process.env.MAX_X402_USD ?? '0.05')
13
+ const resumeFile = process.env.HAVEN_X402_RESUME_FILE ?? '.haven-x402-resume.json'
14
+ const resumePaymentId = process.env.HAVEN_RESUME_PAYMENT_ID
15
+
16
+ if (!mcpUrl) throw new Error('MCP_URL is required')
17
+ if (!apiKey) throw new Error('HAVEN_API_KEY is required')
18
+ if (!delegateKey) throw new Error('HAVEN_DELEGATE_KEY is required')
19
+
20
+ const haven = new HavenClient({ apiKey, delegateKey, baseUrl })
21
+
22
+ async function initializeMcpSession(): Promise<string> {
23
+ const response = await fetch(mcpUrl!, {
24
+ method: 'POST',
25
+ headers: {
26
+ 'Content-Type': 'application/json',
27
+ Accept: 'application/json, text/event-stream',
28
+ },
29
+ body: JSON.stringify({
30
+ jsonrpc: '2.0',
31
+ id: 'init-1',
32
+ method: 'initialize',
33
+ params: {
34
+ protocolVersion: '2025-03-26',
35
+ capabilities: {},
36
+ clientInfo: {
37
+ name: 'haven-x402-mcp-example',
38
+ version: '1.0.0',
39
+ },
40
+ },
41
+ }),
42
+ })
43
+
44
+ const sessionId = response.headers.get('mcp-session-id')
45
+ if (!sessionId) {
46
+ throw new Error('MCP initialize response did not include mcp-session-id')
47
+ }
48
+
49
+ return sessionId
50
+ }
51
+
52
+ function paidToolCallInit(sessionId: string): RequestInit {
53
+ return {
54
+ method: 'POST',
55
+ headers: {
56
+ 'Content-Type': 'application/json',
57
+ Accept: 'application/json, text/event-stream',
58
+ 'mcp-session-id': sessionId,
59
+ },
60
+ body: JSON.stringify({
61
+ jsonrpc: '2.0',
62
+ id: 'paid-call-1',
63
+ method: 'tools/call',
64
+ params: {
65
+ name: process.env.MCP_TOOL ?? 'paid_tool',
66
+ arguments: process.env.MCP_TOOL_ARGS
67
+ ? JSON.parse(process.env.MCP_TOOL_ARGS)
68
+ : {},
69
+ },
70
+ }),
71
+ }
72
+ }
73
+
74
+ function capturedRequest(url: string, init: RequestInit): X402ResumeState['request'] {
75
+ return {
76
+ url,
77
+ method: init.method ?? 'GET',
78
+ headers: Array.from(new Headers(init.headers).entries()),
79
+ body: typeof init.body === 'string' ? init.body : undefined,
80
+ }
81
+ }
82
+
83
+ async function printResponse(response: Response): Promise<void> {
84
+ console.log('HTTP', response.status, response.statusText)
85
+ console.log(await response.text())
86
+ }
87
+
88
+ async function resumeFromSavedState(): Promise<void> {
89
+ const state = JSON.parse(await readFile(resumeFile, 'utf8')) as X402ResumeState
90
+ const response = await haven.resumeX402Payment(state)
91
+ await printResponse(response)
92
+ }
93
+
94
+ async function resumeFromPaymentId(paymentId: string): Promise<void> {
95
+ const state = await haven.getResumeState(paymentId)
96
+ if (state.rail !== 'x402') {
97
+ throw new Error(`Payment ${paymentId} is ${state.rail}; this example resumes x402 payments only.`)
98
+ }
99
+
100
+ const sessionId = await initializeMcpSession()
101
+ const request = paidToolCallInit(sessionId)
102
+ state.request = capturedRequest(mcpUrl!, request)
103
+ state.url = state.request.url
104
+
105
+ const response = await haven.resumeX402Payment(state)
106
+ await printResponse(response)
107
+ }
108
+
109
+ async function quoteAndPay(): Promise<void> {
110
+ const sessionId = await initializeMcpSession()
111
+ const request = paidToolCallInit(sessionId)
112
+ const idempotencyKey = process.env.HAVEN_X402_IDEMPOTENCY_KEY ?? `mcp:${sessionId}:paid-call-1`
113
+
114
+ const quote = await haven.quoteX402(mcpUrl!, request, { idempotencyKey })
115
+ console.log('Quote', {
116
+ amount: quote.amount,
117
+ token: quote.token,
118
+ network: quote.network,
119
+ merchantAddress: quote.merchantAddress,
120
+ resourceUrl: quote.resourceUrl,
121
+ })
122
+
123
+ if (Number(quote.amount) > maxUsd) {
124
+ throw new Error(`Quote ${quote.amount} ${quote.token} is above cap ${maxUsd}`)
125
+ }
126
+
127
+ try {
128
+ const response = await haven.payX402Quote(quote)
129
+ await printResponse(response)
130
+ } catch (err) {
131
+ if (err instanceof HavenPaymentStateError && err.resumeState) {
132
+ await writeFile(resumeFile, JSON.stringify(err.resumeState, null, 2))
133
+ console.error(`Waiting for user approval in Haven. Resume state saved to ${resumeFile}.`)
134
+ console.error(`After approval, rerun with HAVEN_RESUME_PAYMENT_ID=${err.resumeState.paymentId}.`)
135
+ console.error(`The local file fallback is still available with HAVEN_X402_RESUME=1.`)
136
+ return
137
+ }
138
+
139
+ throw err
140
+ }
141
+ }
142
+
143
+ if (resumePaymentId) {
144
+ await resumeFromPaymentId(resumePaymentId)
145
+ } else if (process.env.HAVEN_X402_RESUME === '1') {
146
+ await resumeFromSavedState()
147
+ } else {
148
+ await quoteAndPay()
149
+ }
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Minimal non-TypeScript x402 proof of concept for the Haven OpenAPI surface.
4
+
5
+ Requires:
6
+ pip install requests eth-keys
7
+
8
+ Environment:
9
+ HAVEN_API_KEY sk_agent_* from Haven
10
+ HAVEN_DELEGATE_KEY local agent delegate private key; never sent to Haven
11
+ HAVEN_API_URL default: https://havenbackend-production-8a00.up.railway.app
12
+ HAVEN_X402_URL default: $HAVEN_API_URL/demo/x402/data
13
+
14
+ This intentionally uses only documented HTTP endpoints:
15
+ GET /openapi.json
16
+ POST /x402/authorize
17
+ POST /payments/{id}/sign
18
+ """
19
+
20
+ import base64
21
+ import json
22
+ import os
23
+ import sys
24
+
25
+ import requests
26
+ from eth_keys import keys
27
+
28
+
29
+ API = os.environ.get("HAVEN_API_URL", "https://havenbackend-production-8a00.up.railway.app").rstrip("/")
30
+ PAID_URL = os.environ.get("HAVEN_X402_URL", f"{API}/demo/x402/data")
31
+ API_KEY = os.environ["HAVEN_API_KEY"]
32
+ DELEGATE_KEY = os.environ["HAVEN_DELEGATE_KEY"]
33
+
34
+
35
+ def b64_json(value):
36
+ return base64.b64encode(json.dumps(value).encode()).decode()
37
+
38
+
39
+ def decode_payment_required(response):
40
+ header = response.headers.get("PAYMENT-REQUIRED")
41
+ if not header:
42
+ raise RuntimeError(f"Expected PAYMENT-REQUIRED header, got HTTP {response.status_code}: {response.text}")
43
+ return json.loads(base64.b64decode(header).decode())
44
+
45
+
46
+ def post_haven(path, payload):
47
+ response = requests.post(
48
+ f"{API}{path}",
49
+ headers={"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"},
50
+ json=payload,
51
+ timeout=60,
52
+ )
53
+ body = response.json()
54
+ if response.status_code >= 400:
55
+ raise RuntimeError(f"{path} failed with HTTP {response.status_code}: {body}")
56
+ return body
57
+
58
+
59
+ def sign_raw_hash(sign_hash, private_key):
60
+ """Sign the raw Safe hash exactly as /payments/{id}/sign expects."""
61
+ key_hex = private_key[2:] if private_key.startswith("0x") else private_key
62
+ hash_hex = sign_hash[2:] if sign_hash.startswith("0x") else sign_hash
63
+ signature = keys.PrivateKey(bytes.fromhex(key_hex)).sign_msg_hash(bytes.fromhex(hash_hex))
64
+ v = signature.v + 27 if signature.v in (0, 1) else signature.v
65
+ return (
66
+ "0x"
67
+ + signature.r.to_bytes(32, "big").hex()
68
+ + signature.s.to_bytes(32, "big").hex()
69
+ + bytes([v]).hex()
70
+ )
71
+
72
+
73
+ spec = requests.get(f"{API}/openapi.json", timeout=30).json()
74
+ authorize_path = "/x402/authorize" if "/x402/authorize" in spec["paths"] else "/x402"
75
+
76
+ initial = requests.get(PAID_URL, timeout=30)
77
+ if initial.status_code != 402:
78
+ raise RuntimeError(f"Expected paid resource to return HTTP 402, got {initial.status_code}")
79
+
80
+ payment_required = decode_payment_required(initial)
81
+ accepted = payment_required["accepts"][0]
82
+
83
+ authorization = post_haven(authorize_path, {
84
+ "url": payment_required["resource"]["url"],
85
+ "payTo": accepted["payTo"],
86
+ "merchantPayTo": accepted["payTo"],
87
+ "amount": accepted.get("amount") or accepted["maxAmountRequired"],
88
+ "asset": accepted["asset"],
89
+ "network": accepted["network"],
90
+ "description": payment_required["resource"].get("description"),
91
+ "idempotencyKey": f"python-openapi:{payment_required['resource']['url']}",
92
+ })
93
+
94
+ if authorization.get("status") == "pending_approval":
95
+ print(json.dumps({
96
+ "payment_id": authorization["payment_id"],
97
+ "next_action": authorization.get("next_action"),
98
+ "message": authorization.get("message"),
99
+ }, indent=2))
100
+ sys.exit("Payment is waiting for approval in Haven. Re-run after approval and use /payments/{id}/resume_state.")
101
+
102
+ sign_hash = authorization["sign_data"]["hash"]
103
+ signature = sign_raw_hash(sign_hash, DELEGATE_KEY)
104
+
105
+ result = post_haven(f"/payments/{authorization['payment_id']}/sign", {"signature": signature})
106
+ tx_hash = result["tx_hash"]
107
+
108
+ paid = requests.get(PAID_URL, headers={"PAYMENT-SIGNATURE": b64_json({"txHash": tx_hash})}, timeout=30)
109
+ print(json.dumps({
110
+ "status": paid.status_code,
111
+ "payment_id": authorization["payment_id"],
112
+ "tx_hash": tx_hash,
113
+ "response": paid.json(),
114
+ }, indent=2))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haven_ai/sdk",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "TypeScript SDK for Haven — agent wallet infrastructure",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -20,7 +20,8 @@
20
20
  },
21
21
  "files": [
22
22
  "dist",
23
- "README.md"
23
+ "README.md",
24
+ "examples"
24
25
  ],
25
26
  "scripts": {
26
27
  "build": "tsup",
@@ -41,7 +42,7 @@
41
42
  "license": "MIT",
42
43
  "dependencies": {
43
44
  "ethers": "^6.13.0",
44
- "viem": "^2.48.11",
45
+ "viem": "^2.51.0",
45
46
  "x402": "^1.2.0"
46
47
  },
47
48
  "devDependencies": {