@hypawave/sdk 0.2.1 → 0.3.0

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/CHANGELOG.md ADDED
@@ -0,0 +1,30 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@hypawave/sdk` are documented here.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.3.0]
9
+
10
+ ### Added
11
+ - Automatic request retries with full-jitter exponential backoff, honoring the
12
+ `Retry-After` header on 429 / overload responses.
13
+ - `maxRetries` config option (default `3`; set `0` to disable retries).
14
+ - Adaptive backoff in `waitForSettlement` (ramps from `pollInterval` up to
15
+ `maxPollInterval`, default 2s → 20s) instead of a fixed-interval poll.
16
+
17
+ ### Changed
18
+ - Retry policy is gated by safety: every `GET` and the server-idempotent POSTs
19
+ (`confirmPayment`, `topup`, `getUnlockStatus`, `getOfferDownloadUrl`) retry on
20
+ 5xx and network errors; non-idempotent POSTs (e.g. `createInvoice`) retry only
21
+ on 429. This prevents duplicate side effects on retry.
22
+ - `timeout` now applies per attempt rather than to the whole call.
23
+ - Query parameters accept `string | number | boolean | undefined`; values are
24
+ serialized centrally and `undefined` is omitted.
25
+
26
+ ### Fixed
27
+ - Non-JSON responses (e.g. an HTML 502 from an overloaded proxy) now surface as
28
+ a clean `HypawaveAPIError` instead of throwing an opaque JSON parse error.
29
+ - A successful (2xx) response with a non-JSON body now throws instead of
30
+ returning a malformed success object.
package/README.md CHANGED
@@ -1,8 +1,9 @@
1
1
  # @hypawave/sdk
2
2
 
3
- **Lightning SDK for AI Agent Payments — non-custodial Bitcoin settlement with preimage proof**
3
+ **Bitcoin Lightning SDK for AI Agent Payments — non-custodial settlement with preimage-proof unlocks**
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/@hypawave/sdk.svg)](https://www.npmjs.com/package/@hypawave/sdk)
6
+ [![CI](https://github.com/hypawave/sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/hypawave/sdk/actions/workflows/ci.yml)
6
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/hypawave/sdk/blob/main/LICENSE)
7
8
  [![Node >= 18](https://img.shields.io/badge/Node-%3E%3D18-brightgreen.svg)](https://nodejs.org)
8
9
 
@@ -10,13 +11,19 @@
10
11
 
11
12
  > ⚠️ **Hypawave has no token and will never issue one.** Any "HYPA," "WAVE," airdrop, or token offer claiming to be from Hypawave is a scam. Hypawave is non-custodial Bitcoin Lightning settlement — buyers pay creators directly in sats. The protocol fee is the only economic primitive.
12
13
 
13
- TypeScript SDK for settlement-triggered Lightning execution. Fetch creator-direct Lightning invoices, confirm preimage proof, and trigger deterministic unlocks.
14
+ Hypawave is programmable settlement infrastructure for AI agents: a non-custodial Bitcoin Lightning protocol where verified payment unlocks API access, files, agent actions, and digital work.
15
+
16
+ This TypeScript SDK lets developers create creator-direct Lightning invoices, deliver payment payloads to payer agents, verify preimage proof, and trigger deterministic unlocks.
14
17
 
15
18
  **Payment is the authorization — confirmed settlement unconditionally unlocks access.**
16
19
 
17
20
  ## Why Hypawave
18
21
 
19
- AI agents need to pay and get access without accounts, custody, or manual delivery. Hypawave turns Lightning settlement proof into deterministic execution — the same preimage that proves payment also unlocks the file, the API call, or the webhook.
22
+ Hypawave lets developers charge for digital work with one primitive: verified Bitcoin Lightning settlement unlocks access.
23
+
24
+ Use it when an API, file, webhook, inference job, automation, or agent action should only run after payment. The SDK creates creator-direct Lightning invoices, packages payment instructions for payer agents, verifies preimage proof after payment, and returns the preimage your app gates execution on.
25
+
26
+ The important difference from a normal invoice flow is that payment proof and authorization are the same event. Once the payer submits a valid preimage, Hypawave can release encrypted files, return gated data, or trigger execution without manual reconciliation, payer accounts, or custody.
20
27
 
21
28
  This SDK covers **Path 2**: account-based agent flows. Accountless Paths 3a and 3b use raw HTTP via [llms.txt](https://hypawave.com/llms.txt) and the [OpenAPI spec](https://hypawave.com/.well-known/openapi.json). Requires programmable Lightning infrastructure (LND, CLN, Alby API, LNbits, NWC, etc.) that returns the preimage after payment.
22
29
 
@@ -47,6 +54,8 @@ npm install @hypawave/sdk
47
54
 
48
55
  ## Quick Start
49
56
 
57
+ This example shows the full settlement loop in one script for clarity. In production, the creator/developer usually creates the invoice, then sends either `payment_url` to a browser payer or `getPaymentPayload()` to a payer agent.
58
+
50
59
  ```typescript
51
60
  import { Hypawave } from "@hypawave/sdk";
52
61
 
@@ -59,7 +68,7 @@ const invoice = await pp.createInvoice({
59
68
  client_last_name: "Smith",
60
69
  amount: 5.00,
61
70
  due_date: "2026-12-31",
62
- payment_destination: "creator@getalby.com",
71
+ payment_destination: "creator@getalby.com", // optional; defaults to the API key owner's Lightning Address
63
72
  });
64
73
 
65
74
  console.log(invoice.invoice_id); // Use for confirmation + key retrieval
@@ -148,7 +157,8 @@ Both files point to two authoritative web sources so instructions stay fresh:
148
157
  |-----------|------|---------|-------------|
149
158
  | `apiKey` | `string` | — | API key (`sk_test_*` or `sk_live_*`) |
150
159
  | `baseUrl` | `string` | `https://hypawave.com` | API base URL |
151
- | `timeout` | `number` | `30000` | Request timeout in ms |
160
+ | `timeout` | `number` | `30000` | Request timeout in ms (per attempt) |
161
+ | `maxRetries` | `number` | `3` | Max automatic retries on 429 / retryable 5xx / network errors (`0` disables). Honors `Retry-After`. |
152
162
 
153
163
  ### `createInvoice(params)`
154
164
 
@@ -322,12 +332,15 @@ const { downloadUrl } = await pp.getOfferDownloadUrl(paymentIntentId, {
322
332
 
323
333
  ### `waitForSettlement(invoiceId, options?)`
324
334
 
325
- Poll until an invoice settles, fails, or expires.
335
+ Poll until an invoice settles, fails, or expires. Polling backs off
336
+ adaptively — the interval ramps from `pollInterval` up to `maxPollInterval`
337
+ (with jitter) so a busy backend isn't hit on a fixed cadence.
326
338
 
327
339
  ```typescript
328
340
  const result = await pp.waitForSettlement(invoiceId, {
329
- pollInterval: 2000, // ms between polls (default: 2000)
330
- timeout: 300000, // max wait time in ms (default: 300000)
341
+ pollInterval: 2000, // starting interval in ms (default: 2000)
342
+ maxPollInterval: 20000, // ceiling the interval ramps toward (default: 20000)
343
+ timeout: 300000, // max wait time in ms (default: 300000)
331
344
  });
332
345
 
333
346
  if (result.unlocked) {
@@ -369,11 +382,10 @@ const settings = await pp.getSettings();
369
382
  Additional methods available — see types for full signatures, or [openapi.json](https://hypawave.com/.well-known/openapi.json) for the complete API reference.
370
383
 
371
384
  - `listInvoices(params?)` — list invoices with filters and pagination
372
- - `getPayerReceipt(invoiceId, accessToken)` — receipt fetch using a payer access token (no API key needed)
385
+ - `getPayerReceipt(invoiceId, preimage)` — payer receipt fetch using the Lightning preimage as proof of payment (no API key needed)
373
386
  - `getUploadUrl(params)` — signed URL for encrypted file upload (creator side)
374
387
  - `storeFile(params)` — register an uploaded file against an invoice
375
388
  - `storeFileKey(params)` — register a file's encryption key against an invoice
376
- - `request(path, options)` — low-level escape hatch for direct API calls
377
389
 
378
390
  ## Error Handling
379
391
 
package/dist/index.d.mts CHANGED
@@ -2,6 +2,8 @@ interface HypawaveConfig {
2
2
  apiKey: string;
3
3
  baseUrl?: string;
4
4
  timeout?: number;
5
+ /** Max automatic retries on 429 / retryable 5xx / network errors. Default 3. Set 0 to disable. */
6
+ maxRetries?: number;
5
7
  }
6
8
  interface CreateInvoiceParams {
7
9
  client_email: string;
@@ -259,6 +261,7 @@ declare class Hypawave {
259
261
  private readonly apiKey;
260
262
  private readonly baseUrl;
261
263
  private readonly timeout;
264
+ private readonly maxRetries;
262
265
  constructor(config: HypawaveConfig);
263
266
  private request;
264
267
  createInvoice(params: CreateInvoiceParams): Promise<CreateInvoiceResponse>;
@@ -292,6 +295,7 @@ declare class Hypawave {
292
295
  getSettings(): Promise<PublicSettingsResponse>;
293
296
  waitForSettlement(invoiceId: number, options?: {
294
297
  pollInterval?: number;
298
+ maxPollInterval?: number;
295
299
  timeout?: number;
296
300
  }): Promise<UnlockStatusResponse["statuses"][string]>;
297
301
  payAndConfirm(params: PayAndConfirmParams): Promise<PayAndConfirmResult>;
package/dist/index.d.ts CHANGED
@@ -2,6 +2,8 @@ interface HypawaveConfig {
2
2
  apiKey: string;
3
3
  baseUrl?: string;
4
4
  timeout?: number;
5
+ /** Max automatic retries on 429 / retryable 5xx / network errors. Default 3. Set 0 to disable. */
6
+ maxRetries?: number;
5
7
  }
6
8
  interface CreateInvoiceParams {
7
9
  client_email: string;
@@ -259,6 +261,7 @@ declare class Hypawave {
259
261
  private readonly apiKey;
260
262
  private readonly baseUrl;
261
263
  private readonly timeout;
264
+ private readonly maxRetries;
262
265
  constructor(config: HypawaveConfig);
263
266
  private request;
264
267
  createInvoice(params: CreateInvoiceParams): Promise<CreateInvoiceResponse>;
@@ -292,6 +295,7 @@ declare class Hypawave {
292
295
  getSettings(): Promise<PublicSettingsResponse>;
293
296
  waitForSettlement(invoiceId: number, options?: {
294
297
  pollInterval?: number;
298
+ maxPollInterval?: number;
295
299
  timeout?: number;
296
300
  }): Promise<UnlockStatusResponse["statuses"][string]>;
297
301
  payAndConfirm(params: PayAndConfirmParams): Promise<PayAndConfirmResult>;
package/dist/index.js CHANGED
@@ -51,6 +51,20 @@ var HypawaveAPIError = class extends Error {
51
51
  // src/client.ts
52
52
  var DEFAULT_BASE_URL = "https://hypawave.com";
53
53
  var DEFAULT_TIMEOUT = 3e4;
54
+ var DEFAULT_MAX_RETRIES = 3;
55
+ var RETRY_BASE_MS = 500;
56
+ var RETRY_CAP_MS = 8e3;
57
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
58
+ function backoffDelay(attempt, retryAfter) {
59
+ if (retryAfter) {
60
+ const secs = Number(retryAfter);
61
+ if (Number.isFinite(secs)) return Math.max(0, secs * 1e3);
62
+ const date = Date.parse(retryAfter);
63
+ if (!Number.isNaN(date)) return Math.max(0, date - Date.now());
64
+ }
65
+ const exp = Math.min(RETRY_CAP_MS, RETRY_BASE_MS * 2 ** attempt);
66
+ return Math.random() * exp;
67
+ }
54
68
  async function sha256hex(hex) {
55
69
  const bytes = new Uint8Array(hex.match(/.{2}/g).map((b) => parseInt(b, 16)));
56
70
  const hash = await crypto.subtle.digest("SHA-256", bytes);
@@ -67,42 +81,71 @@ var Hypawave = class {
67
81
  this.apiKey = config.apiKey;
68
82
  this.baseUrl = (config.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, "");
69
83
  this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
84
+ this.maxRetries = Math.max(0, config.maxRetries ?? DEFAULT_MAX_RETRIES);
70
85
  }
71
- async request(method, path, body, query) {
86
+ async request(method, path, body, query, opts) {
72
87
  let url = `${this.baseUrl}${path}`;
73
88
  if (query) {
74
- const params = new URLSearchParams(query);
75
- url += `?${params.toString()}`;
76
- }
77
- const controller = new AbortController();
78
- const timer = setTimeout(() => controller.abort(), this.timeout);
79
- try {
80
- const res = await fetch(url, {
81
- method,
82
- headers: {
83
- Authorization: `Bearer ${this.apiKey}`,
84
- "Content-Type": "application/json",
85
- Accept: "application/json"
86
- },
87
- body: body ? JSON.stringify(body) : void 0,
88
- signal: controller.signal
89
- });
90
- const data = await res.json();
91
- if (!res.ok) {
92
- throw new HypawaveAPIError(res.status, data);
89
+ const params = new URLSearchParams();
90
+ for (const [k, v] of Object.entries(query)) {
91
+ if (v !== void 0) params.set(k, String(v));
93
92
  }
94
- return data;
95
- } catch (err) {
96
- if (err instanceof HypawaveAPIError) throw err;
97
- if (err instanceof DOMException && err.name === "AbortError") {
98
- throw new HypawaveAPIError(408, {
99
- error: "timeout",
100
- message: `Request timed out after ${this.timeout}ms`
93
+ const qs = params.toString();
94
+ if (qs) url += `?${qs}`;
95
+ }
96
+ const retrySafe = opts?.retrySafe ?? method === "GET";
97
+ for (let attempt = 0; ; attempt++) {
98
+ const controller = new AbortController();
99
+ const timer = setTimeout(() => controller.abort(), this.timeout);
100
+ try {
101
+ const res = await fetch(url, {
102
+ method,
103
+ headers: {
104
+ Authorization: `Bearer ${this.apiKey}`,
105
+ "Content-Type": "application/json",
106
+ Accept: "application/json"
107
+ },
108
+ body: body ? JSON.stringify(body) : void 0,
109
+ signal: controller.signal
101
110
  });
111
+ let data;
112
+ let parseFailed = false;
113
+ try {
114
+ data = await res.json();
115
+ } catch {
116
+ parseFailed = true;
117
+ data = res.ok ? { error: "invalid_response", message: "Server returned a successful status with a non-JSON body." } : { error: `http_${res.status}`, message: `Server returned status ${res.status} with a non-JSON body.` };
118
+ }
119
+ if (!res.ok) {
120
+ const retryable = res.status === 429 || res.status >= 500 && retrySafe;
121
+ if (retryable && attempt < this.maxRetries) {
122
+ clearTimeout(timer);
123
+ await sleep(backoffDelay(attempt, res.headers.get("retry-after")));
124
+ continue;
125
+ }
126
+ throw new HypawaveAPIError(res.status, data);
127
+ }
128
+ if (parseFailed) {
129
+ throw new HypawaveAPIError(res.status, data);
130
+ }
131
+ return data;
132
+ } catch (err) {
133
+ if (err instanceof HypawaveAPIError) throw err;
134
+ if (err instanceof DOMException && err.name === "AbortError") {
135
+ throw new HypawaveAPIError(408, {
136
+ error: "timeout",
137
+ message: `Request timed out after ${this.timeout}ms`
138
+ });
139
+ }
140
+ if (retrySafe && attempt < this.maxRetries) {
141
+ clearTimeout(timer);
142
+ await sleep(backoffDelay(attempt, null));
143
+ continue;
144
+ }
145
+ throw err;
146
+ } finally {
147
+ clearTimeout(timer);
102
148
  }
103
- throw err;
104
- } finally {
105
- clearTimeout(timer);
106
149
  }
107
150
  }
108
151
  async createInvoice(params) {
@@ -112,7 +155,9 @@ var Hypawave = class {
112
155
  return this.request("GET", "/api/paystream-cb", void 0, { token: accessToken });
113
156
  }
114
157
  async confirmPayment(invoiceId, params) {
115
- return this.request("POST", `/api/invoice/${invoiceId}/confirm`, params);
158
+ return this.request("POST", `/api/invoice/${invoiceId}/confirm`, params, void 0, {
159
+ retrySafe: true
160
+ });
116
161
  }
117
162
  getPaymentPayload(params, response) {
118
163
  return {
@@ -145,12 +190,16 @@ var Hypawave = class {
145
190
  * - duplicate_topup — a pending top-up invoice already exists
146
191
  */
147
192
  async topup(_params) {
148
- return this.request("POST", "/api/agent/topup", {});
193
+ return this.request("POST", "/api/agent/topup", {}, void 0, { retrySafe: true });
149
194
  }
150
195
  async getUnlockStatus(invoiceIds) {
151
- return this.request("POST", "/api/get-unlock-status", {
152
- invoice_ids: invoiceIds
153
- });
196
+ return this.request(
197
+ "POST",
198
+ "/api/get-unlock-status",
199
+ { invoice_ids: invoiceIds },
200
+ void 0,
201
+ { retrySafe: true }
202
+ );
154
203
  }
155
204
  async getKey(invoiceFileId, token) {
156
205
  const query = { invoice_file_id: invoiceFileId };
@@ -167,16 +216,11 @@ var Hypawave = class {
167
216
  return this.request("POST", "/api/agent/store-file-key", params);
168
217
  }
169
218
  async listInvoices(params) {
170
- const query = {};
171
- if (params?.limit !== void 0) query.limit = String(params.limit);
172
- if (params?.offset !== void 0) query.offset = String(params.offset);
173
- if (params?.status) query.status = params.status;
174
- return this.request(
175
- "GET",
176
- "/api/agent/list-invoices",
177
- void 0,
178
- Object.keys(query).length ? query : void 0
179
- );
219
+ return this.request("GET", "/api/agent/list-invoices", void 0, {
220
+ limit: params?.limit,
221
+ offset: params?.offset,
222
+ status: params?.status
223
+ });
180
224
  }
181
225
  async getInvoiceFiles(invoiceIds, token) {
182
226
  const body = { invoice_ids: invoiceIds };
@@ -192,12 +236,14 @@ var Hypawave = class {
192
236
  return this.request(
193
237
  "POST",
194
238
  `/api/offers/payment-intent/${paymentIntentId}/download-url`,
195
- params
239
+ params,
240
+ void 0,
241
+ { retrySafe: true }
196
242
  );
197
243
  }
198
244
  async getReceipt(invoiceId) {
199
245
  return this.request("GET", "/api/agent/receipt", void 0, {
200
- invoice_id: String(invoiceId)
246
+ invoice_id: invoiceId
201
247
  });
202
248
  }
203
249
  async getPayerReceipt(invoiceId, preimage) {
@@ -209,10 +255,11 @@ var Hypawave = class {
209
255
  return this.request("GET", "/api/public-settings");
210
256
  }
211
257
  async waitForSettlement(invoiceId, options) {
212
- const interval = options?.pollInterval ?? 2e3;
258
+ const base = options?.pollInterval ?? 2e3;
259
+ const maxInterval = options?.maxPollInterval ?? 2e4;
213
260
  const timeout = options?.timeout ?? 3e5;
214
261
  const start = Date.now();
215
- while (Date.now() - start < timeout) {
262
+ for (let attempt = 0; Date.now() - start < timeout; attempt++) {
216
263
  const status = await this.getUnlockStatus([invoiceId]);
217
264
  const entry = status.statuses?.[String(invoiceId)];
218
265
  if (entry?.unlocked) {
@@ -221,7 +268,9 @@ var Hypawave = class {
221
268
  if (entry?.status === "failed" || entry?.status === "expired") {
222
269
  return entry;
223
270
  }
224
- await new Promise((r) => setTimeout(r, interval));
271
+ const ceiling = Math.min(maxInterval, base * 2 ** attempt);
272
+ const wait = ceiling / 2 + Math.random() * (ceiling / 2);
273
+ await sleep(Math.min(wait, Math.max(0, start + timeout - Date.now())));
225
274
  }
226
275
  throw new HypawaveAPIError(408, {
227
276
  error: "settlement_timeout",
package/dist/index.mjs CHANGED
@@ -24,6 +24,20 @@ var HypawaveAPIError = class extends Error {
24
24
  // src/client.ts
25
25
  var DEFAULT_BASE_URL = "https://hypawave.com";
26
26
  var DEFAULT_TIMEOUT = 3e4;
27
+ var DEFAULT_MAX_RETRIES = 3;
28
+ var RETRY_BASE_MS = 500;
29
+ var RETRY_CAP_MS = 8e3;
30
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
31
+ function backoffDelay(attempt, retryAfter) {
32
+ if (retryAfter) {
33
+ const secs = Number(retryAfter);
34
+ if (Number.isFinite(secs)) return Math.max(0, secs * 1e3);
35
+ const date = Date.parse(retryAfter);
36
+ if (!Number.isNaN(date)) return Math.max(0, date - Date.now());
37
+ }
38
+ const exp = Math.min(RETRY_CAP_MS, RETRY_BASE_MS * 2 ** attempt);
39
+ return Math.random() * exp;
40
+ }
27
41
  async function sha256hex(hex) {
28
42
  const bytes = new Uint8Array(hex.match(/.{2}/g).map((b) => parseInt(b, 16)));
29
43
  const hash = await crypto.subtle.digest("SHA-256", bytes);
@@ -40,42 +54,71 @@ var Hypawave = class {
40
54
  this.apiKey = config.apiKey;
41
55
  this.baseUrl = (config.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, "");
42
56
  this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
57
+ this.maxRetries = Math.max(0, config.maxRetries ?? DEFAULT_MAX_RETRIES);
43
58
  }
44
- async request(method, path, body, query) {
59
+ async request(method, path, body, query, opts) {
45
60
  let url = `${this.baseUrl}${path}`;
46
61
  if (query) {
47
- const params = new URLSearchParams(query);
48
- url += `?${params.toString()}`;
49
- }
50
- const controller = new AbortController();
51
- const timer = setTimeout(() => controller.abort(), this.timeout);
52
- try {
53
- const res = await fetch(url, {
54
- method,
55
- headers: {
56
- Authorization: `Bearer ${this.apiKey}`,
57
- "Content-Type": "application/json",
58
- Accept: "application/json"
59
- },
60
- body: body ? JSON.stringify(body) : void 0,
61
- signal: controller.signal
62
- });
63
- const data = await res.json();
64
- if (!res.ok) {
65
- throw new HypawaveAPIError(res.status, data);
62
+ const params = new URLSearchParams();
63
+ for (const [k, v] of Object.entries(query)) {
64
+ if (v !== void 0) params.set(k, String(v));
66
65
  }
67
- return data;
68
- } catch (err) {
69
- if (err instanceof HypawaveAPIError) throw err;
70
- if (err instanceof DOMException && err.name === "AbortError") {
71
- throw new HypawaveAPIError(408, {
72
- error: "timeout",
73
- message: `Request timed out after ${this.timeout}ms`
66
+ const qs = params.toString();
67
+ if (qs) url += `?${qs}`;
68
+ }
69
+ const retrySafe = opts?.retrySafe ?? method === "GET";
70
+ for (let attempt = 0; ; attempt++) {
71
+ const controller = new AbortController();
72
+ const timer = setTimeout(() => controller.abort(), this.timeout);
73
+ try {
74
+ const res = await fetch(url, {
75
+ method,
76
+ headers: {
77
+ Authorization: `Bearer ${this.apiKey}`,
78
+ "Content-Type": "application/json",
79
+ Accept: "application/json"
80
+ },
81
+ body: body ? JSON.stringify(body) : void 0,
82
+ signal: controller.signal
74
83
  });
84
+ let data;
85
+ let parseFailed = false;
86
+ try {
87
+ data = await res.json();
88
+ } catch {
89
+ parseFailed = true;
90
+ data = res.ok ? { error: "invalid_response", message: "Server returned a successful status with a non-JSON body." } : { error: `http_${res.status}`, message: `Server returned status ${res.status} with a non-JSON body.` };
91
+ }
92
+ if (!res.ok) {
93
+ const retryable = res.status === 429 || res.status >= 500 && retrySafe;
94
+ if (retryable && attempt < this.maxRetries) {
95
+ clearTimeout(timer);
96
+ await sleep(backoffDelay(attempt, res.headers.get("retry-after")));
97
+ continue;
98
+ }
99
+ throw new HypawaveAPIError(res.status, data);
100
+ }
101
+ if (parseFailed) {
102
+ throw new HypawaveAPIError(res.status, data);
103
+ }
104
+ return data;
105
+ } catch (err) {
106
+ if (err instanceof HypawaveAPIError) throw err;
107
+ if (err instanceof DOMException && err.name === "AbortError") {
108
+ throw new HypawaveAPIError(408, {
109
+ error: "timeout",
110
+ message: `Request timed out after ${this.timeout}ms`
111
+ });
112
+ }
113
+ if (retrySafe && attempt < this.maxRetries) {
114
+ clearTimeout(timer);
115
+ await sleep(backoffDelay(attempt, null));
116
+ continue;
117
+ }
118
+ throw err;
119
+ } finally {
120
+ clearTimeout(timer);
75
121
  }
76
- throw err;
77
- } finally {
78
- clearTimeout(timer);
79
122
  }
80
123
  }
81
124
  async createInvoice(params) {
@@ -85,7 +128,9 @@ var Hypawave = class {
85
128
  return this.request("GET", "/api/paystream-cb", void 0, { token: accessToken });
86
129
  }
87
130
  async confirmPayment(invoiceId, params) {
88
- return this.request("POST", `/api/invoice/${invoiceId}/confirm`, params);
131
+ return this.request("POST", `/api/invoice/${invoiceId}/confirm`, params, void 0, {
132
+ retrySafe: true
133
+ });
89
134
  }
90
135
  getPaymentPayload(params, response) {
91
136
  return {
@@ -118,12 +163,16 @@ var Hypawave = class {
118
163
  * - duplicate_topup — a pending top-up invoice already exists
119
164
  */
120
165
  async topup(_params) {
121
- return this.request("POST", "/api/agent/topup", {});
166
+ return this.request("POST", "/api/agent/topup", {}, void 0, { retrySafe: true });
122
167
  }
123
168
  async getUnlockStatus(invoiceIds) {
124
- return this.request("POST", "/api/get-unlock-status", {
125
- invoice_ids: invoiceIds
126
- });
169
+ return this.request(
170
+ "POST",
171
+ "/api/get-unlock-status",
172
+ { invoice_ids: invoiceIds },
173
+ void 0,
174
+ { retrySafe: true }
175
+ );
127
176
  }
128
177
  async getKey(invoiceFileId, token) {
129
178
  const query = { invoice_file_id: invoiceFileId };
@@ -140,16 +189,11 @@ var Hypawave = class {
140
189
  return this.request("POST", "/api/agent/store-file-key", params);
141
190
  }
142
191
  async listInvoices(params) {
143
- const query = {};
144
- if (params?.limit !== void 0) query.limit = String(params.limit);
145
- if (params?.offset !== void 0) query.offset = String(params.offset);
146
- if (params?.status) query.status = params.status;
147
- return this.request(
148
- "GET",
149
- "/api/agent/list-invoices",
150
- void 0,
151
- Object.keys(query).length ? query : void 0
152
- );
192
+ return this.request("GET", "/api/agent/list-invoices", void 0, {
193
+ limit: params?.limit,
194
+ offset: params?.offset,
195
+ status: params?.status
196
+ });
153
197
  }
154
198
  async getInvoiceFiles(invoiceIds, token) {
155
199
  const body = { invoice_ids: invoiceIds };
@@ -165,12 +209,14 @@ var Hypawave = class {
165
209
  return this.request(
166
210
  "POST",
167
211
  `/api/offers/payment-intent/${paymentIntentId}/download-url`,
168
- params
212
+ params,
213
+ void 0,
214
+ { retrySafe: true }
169
215
  );
170
216
  }
171
217
  async getReceipt(invoiceId) {
172
218
  return this.request("GET", "/api/agent/receipt", void 0, {
173
- invoice_id: String(invoiceId)
219
+ invoice_id: invoiceId
174
220
  });
175
221
  }
176
222
  async getPayerReceipt(invoiceId, preimage) {
@@ -182,10 +228,11 @@ var Hypawave = class {
182
228
  return this.request("GET", "/api/public-settings");
183
229
  }
184
230
  async waitForSettlement(invoiceId, options) {
185
- const interval = options?.pollInterval ?? 2e3;
231
+ const base = options?.pollInterval ?? 2e3;
232
+ const maxInterval = options?.maxPollInterval ?? 2e4;
186
233
  const timeout = options?.timeout ?? 3e5;
187
234
  const start = Date.now();
188
- while (Date.now() - start < timeout) {
235
+ for (let attempt = 0; Date.now() - start < timeout; attempt++) {
189
236
  const status = await this.getUnlockStatus([invoiceId]);
190
237
  const entry = status.statuses?.[String(invoiceId)];
191
238
  if (entry?.unlocked) {
@@ -194,7 +241,9 @@ var Hypawave = class {
194
241
  if (entry?.status === "failed" || entry?.status === "expired") {
195
242
  return entry;
196
243
  }
197
- await new Promise((r) => setTimeout(r, interval));
244
+ const ceiling = Math.min(maxInterval, base * 2 ** attempt);
245
+ const wait = ceiling / 2 + Math.random() * (ceiling / 2);
246
+ await sleep(Math.min(wait, Math.max(0, start + timeout - Date.now())));
198
247
  }
199
248
  throw new HypawaveAPIError(408, {
200
249
  error: "settlement_timeout",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hypawave/sdk",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "TypeScript SDK for Lightning settlement, preimage proof, and execution unlocks for AI agents.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -17,10 +17,14 @@
17
17
  "skills",
18
18
  "AGENTS.md",
19
19
  "README.md",
20
+ "CHANGELOG.md",
20
21
  "LICENSE"
21
22
  ],
22
23
  "scripts": {
23
24
  "build": "tsup src/index.ts --format cjs,esm --dts --clean",
25
+ "typecheck": "tsc --noEmit",
26
+ "test": "vitest run",
27
+ "test:watch": "vitest",
24
28
  "prepublishOnly": "npm run build"
25
29
  },
26
30
  "keywords": [
@@ -65,7 +69,8 @@
65
69
  "homepage": "https://hypawave.com/docs",
66
70
  "devDependencies": {
67
71
  "tsup": "^8.0.0",
68
- "typescript": "^5.0.0"
72
+ "typescript": "^5.0.0",
73
+ "vitest": "^3.0.0"
69
74
  },
70
75
  "engines": {
71
76
  "node": ">=18"