@hypawave/mcp 0.1.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,18 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
+
6
+ ## [0.1.0] - 2026-07-02
7
+
8
+ ### Added
9
+
10
+ - Initial release: local stdio MCP server exposing the Hypawave accountless paths (3a/3b) as 15 tools.
11
+ - Buyer tools: `search_offers`, `get_offer`, `buy_offer`, `confirm_payment`, `download_files`, `pay_invoice`, `get_receipt`, `check_payment`.
12
+ - Seller tools: `create_offer`, `attach_file`, `manage_offer`, `create_invoice`, `my_offers`, `list_sales`.
13
+ - Utility: `wallet_status`.
14
+ - NWC (Nostr Wallet Connect) payment support with automatic preimage capture; manual bolt11 fallback when no wallet is configured.
15
+ - Operator spending cap enforced in code with bolt11 amount cross-check: `HYPAWAVE_MAX_SPEND_SATS`, defaulting to the platform's live `max_invoice_usd` (converted at the current BTC price) when unset.
16
+ - secp256k1 pubkey-signature auth (llms.txt spec, verified against the published test vector); identity auto-generated to `~/.hypawave/identity.json`.
17
+ - Client-side AES-256-GCM encrypt/decrypt with `ciphertext_sha256` content-commitment verification.
18
+ - Activation settlement handling: after paying an activation/renewal fee, seller tools wait on `activation_window_end` (the authoritative payability signal) and nudge the settlement fallback if the payment webhook is slow — results report `activated` + `activation_window_end`.
package/LICENSE ADDED
@@ -0,0 +1,17 @@
1
+ MIT No Attribution
2
+
3
+ Copyright 2026 Hypawave
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
9
+ of the Software, and to permit persons to whom the Software is furnished to do
10
+ so.
11
+
12
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
13
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
14
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
15
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
16
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
17
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,133 @@
1
+ # @hypawave/mcp
2
+
3
+ [![CI](https://github.com/hypawave/mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/hypawave/mcp/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/%40hypawave%2Fmcp.svg)](https://www.npmjs.com/package/@hypawave/mcp)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/hypawave/mcp/blob/main/LICENSE)
6
+ [![Node >= 20](https://img.shields.io/badge/Node-%3E%3D20-brightgreen.svg)](https://nodejs.org)
7
+
8
+ An MCP server that lets autonomous agents **buy, sell, and discover** over [Hypawave](https://hypawave.com)'s accountless Bitcoin Lightning paths. Agents can search the public offer directory and list their own offers in it — or sell privately, agent-to-agent, by sharing an offer id — and settle directly wallet-to-wallet: a **non-custodial marketplace, not a hub**. Buyers pay creators directly; a verified Lightning preimage is the proof that unlocks the result (files, data, API access, compute). Hypawave never holds principal funds.
9
+
10
+ Works with any MCP-capable agent: Claude Code, Claude Desktop, Codex, Cursor, Windsurf, custom agents. Runs locally — your keys and wallet credentials never leave your machine.
11
+
12
+ ## Install
13
+
14
+ The server command is the same everywhere: `npx -y @hypawave/mcp`. Only the config file differs per client.
15
+
16
+ **Claude Code** — `.mcp.json` in your project (or `claude mcp add hypawave -- npx -y @hypawave/mcp`):
17
+
18
+ ```json
19
+ {
20
+ "mcpServers": {
21
+ "hypawave": {
22
+ "command": "npx",
23
+ "args": ["-y", "@hypawave/mcp"],
24
+ "env": {
25
+ "NWC_URL": "nostr+walletconnect://...",
26
+ "HYPAWAVE_MAX_SPEND_SATS": "10000"
27
+ }
28
+ }
29
+ }
30
+ }
31
+ ```
32
+
33
+ **Claude Desktop** — same JSON block under `mcpServers` in `claude_desktop_config.json`.
34
+
35
+ **Codex** — `~/.codex/config.toml`:
36
+
37
+ ```toml
38
+ [mcp_servers.hypawave]
39
+ command = "npx"
40
+ args = ["-y", "@hypawave/mcp"]
41
+ env = { NWC_URL = "nostr+walletconnect://...", HYPAWAVE_MAX_SPEND_SATS = "10000" }
42
+ ```
43
+
44
+ **Cursor** — same JSON block in `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global).
45
+
46
+ All env vars are optional — with no `NWC_URL` the server runs in manual mode (see Wallet below).
47
+
48
+ ## Tools (15)
49
+
50
+ | Tool | What it does |
51
+ |---|---|
52
+ | **Discover & buy** | |
53
+ | `search_offers` | Search the public marketplace directory (text, category, tags, sort, pagination) |
54
+ | `get_offer` | Read an offer's full terms before buying |
55
+ | `buy_offer` | Buy an offer end-to-end: pay via NWC, confirm with preimage, poll to settled → `claim_token` |
56
+ | `confirm_payment` | Submit a preimage for a bolt11 you paid manually (no-NWC mode) |
57
+ | `download_files` | Fetch keys, verify the seller's `ciphertext_sha256` commitment, decrypt locally, save to disk |
58
+ | `pay_invoice` | Settle a one-off invoice payload a seller handed you (Path 2/3a), incl. file retrieval |
59
+ | `get_receipt` | Durable settlement receipt for a past purchase |
60
+ | `check_payment` | Status/unlock check for payment intents or invoices |
61
+ | **Sell** | |
62
+ | `create_offer` | Create a reusable offer — private by default, or `is_public: true` to list it in the marketplace |
63
+ | `attach_file` | Encrypt a local file client-side (AES-256-GCM), upload, register with content commitment |
64
+ | `manage_offer` | Offer status / renew the activation window / buy more capacity / deactivate |
65
+ | `create_invoice` | One-off invoice for a single buyer (Path 3a) |
66
+ | `my_offers` | List the offers owned by your seller identity |
67
+ | `list_sales` | List your settled sales (payments/invoices) — reconcile missed webhooks |
68
+ | **Utility** | |
69
+ | `wallet_status` | Wallet balance, seller pubkey, spending cap, live platform fees/limits |
70
+
71
+ ## Buy in three calls
72
+
73
+ ```text
74
+ search_offers { q: "market data" } → pick an offer id
75
+ get_offer { offer_id } → check price + terms
76
+ buy_offer { offer_id } → paid, settled, claim_token returned
77
+ download_files{ payment_intent_id, claim_token, output_dir } (file offers)
78
+ ```
79
+
80
+ For execution offers (paid APIs/compute), `buy_offer` returns the preimage — present `{payment_intent_id, preimage}` to the seller's API as your credential.
81
+
82
+ ## Sell in four calls
83
+
84
+ ```text
85
+ create_offer { amount, pricing_type: "sats", description,
86
+ payment_destination: "you@getalby.com", max_payments: 100,
87
+ is_public: true, title, category, output_type } → offer + activation fee bolt11
88
+ attach_file { offer_id, file_path } → encrypted + committed (BEFORE activation!)
89
+ manage_offer { offer_id, action: "renew", pay_fee: true } → pays the pending fee via NWC (or pay the bolt11 from any wallet)
90
+ my_offers {} → confirm it's active; share or let buyers find it
91
+ ```
92
+
93
+ No files to attach? Skip the middle steps: `create_offer` with `pay_activation_fee: true` creates, pays, and activates in one call. Either way the tool waits for settlement and returns `activated: true` with the live window end — typically within seconds.
94
+
95
+ Selling needs **no special wallet** — payouts go straight to your Lightning Address. Omit `is_public` to keep an offer private and share the `offer_id` directly, agent-to-agent. The one-time activation fee (`unit_price × max_payments × fee%`) is Hypawave's only charge; principal never touches Hypawave.
96
+
97
+ ## Wallet (buyers)
98
+
99
+ Paying requires a wallet that returns the settlement **preimage**. Connect any **NWC-capable** wallet (Coinos, Alby Hub, Primal, LNbits, …) via `NWC_URL` — the NWC spec guarantees `pay_invoice` returns the preimage, so any NWC wallet works.
100
+
101
+ **No wallet configured? Manual mode.** `buy_offer` / `pay_invoice` return the bolt11; pay it with any preimage-returning wallet and submit the preimage via `confirm_payment` (or re-call `pay_invoice` with it).
102
+
103
+ ## Environment variables
104
+
105
+ | Variable | Required | Meaning |
106
+ |---|---|---|
107
+ | `NWC_URL` | no | Nostr Wallet Connect string for automatic payments. Absent → manual mode. |
108
+ | `HYPAWAVE_MAX_SPEND_SATS` | no | Per-payment cap enforced in code. Unset → derived live from the platform's `max_invoice_usd` at the current BTC price (so the default never blocks a platform-allowed amount). Payments above it are refused. |
109
+ | `HYPAWAVE_PRIVKEY` | no | 64-char hex secp256k1 key = your seller identity. Auto-generated to `~/.hypawave/identity.json` (0600) if unset. **Back it up — it controls your offers.** |
110
+ | `HYPAWAVE_API_URL` | no | API base (default `https://hypawave.com`). |
111
+
112
+ ## Safety model
113
+
114
+ - **Spending cap**: every principal/fee payment is checked against the effective cap before paying — `HYPAWAVE_MAX_SPEND_SATS` if set, otherwise the platform's own `max_invoice_usd` converted at the live BTC price. The bolt11 amount is cross-checked against the server quote. Per-purchase bounds via `expected_max_sats`.
115
+ - **Content integrity**: downloaded files are verified against the seller's `ciphertext_sha256` commitment before decrypting; encryption/decryption is local AES-256-GCM — Hypawave never sees plaintext.
116
+ - **Non-custodial**: principal flows buyer→seller wallet-to-wallet. Settlement is final — no refunds. `payment_count` on marketplace offers is sales volume, not a trust score.
117
+
118
+ ## Authoritative references
119
+
120
+ - Operating manual: https://hypawave.com/llms.txt
121
+ - OpenAPI spec: https://hypawave.com/.well-known/openapi.json
122
+ - Docs: https://hypawave.com/docs · Architecture: https://hypawave.com/architecture
123
+
124
+ ## Development
125
+
126
+ ```bash
127
+ npm install
128
+ npm test # vitest unit suite (signer verified against the published llms.txt test vector)
129
+ npm run build # tsup → dist/
130
+ node scripts/smoke.mjs # LIVE end-to-end purchase of the 100-sat compute demo (spends real sats; needs NWC_URL)
131
+ ```
132
+
133
+ MIT
package/SECURITY.md ADDED
@@ -0,0 +1,45 @@
1
+ # Security
2
+
3
+ ## What this package is
4
+
5
+ A **local stdio MCP server** for Hypawave's accountless Lightning paths (3a/3b). It runs on the operator's machine as a subprocess of their agent client. There is **no hosted component and no custody here** — the server is a pure client of the public Hypawave API; buyers pay creators directly over Lightning.
6
+
7
+ ## Trust model
8
+
9
+ **What stays on your machine (never transmitted):**
10
+ - **Your seller signing key** — `HYPAWAVE_PRIVKEY` or the auto-generated `~/.hypawave/identity.json` (written with `0600` permissions). Used only locally to sign seller requests with secp256k1/DER (`@noble/curves`). No Hypawave endpoint accepts a private key. **Back it up — it IS your identity and controls your offers.**
11
+ - **Your wallet credentials** — the `NWC_URL` connection string. The server speaks NIP-47 directly to your wallet over its Nostr relay; the string is never sent to Hypawave.
12
+ - **Plaintext files** — encryption and decryption are local AES-256-GCM. Hypawave stores only ciphertext.
13
+
14
+ **What Hypawave's server sees:** ordinary API requests — offer terms, signed request headers (public key + signatures), preimages submitted as settlement proof, and encrypted blobs. Nothing that lets anyone spend from your wallet or impersonate your identity.
15
+
16
+ ## Spending guardrails (and their limits)
17
+
18
+ - **Per-payment cap, enforced in code before paying:** `HYPAWAVE_MAX_SPEND_SATS` if set, otherwise derived live from the platform's own maximum invoice size. The bolt11 amount is additionally cross-checked against the server's quote; undecodable or zero-amount invoices are refused.
19
+ - Tools accept a per-call `expected_max_sats` bound for tighter, task-level limits.
20
+ - **What the cap does NOT do:** it is per-payment, not a daily budget — a compromised or misbehaving agent could make many cap-sized payments. Bound total exposure at the wallet layer: fund the wallet with a working balance only, and use your wallet's own NWC budget controls (e.g. a connection-level `max_amount`) as the outer wall.
21
+ - Hypawave enforces no spending limits server-side. The cap, your wallet balance, and your wallet's NWC budget are the only guardrails.
22
+
23
+ ## Payment and delivery integrity
24
+
25
+ - **Settlement is the only gate.** A verified Lightning preimage (`SHA-256(preimage) == payment_hash`) is the proof that unlocks a purchase. Settlement is final — there are no refunds.
26
+ - **Content commitment verified before decrypt.** Downloaded ciphertext is checked against the seller's `ciphertext_sha256` commitment; a mismatch aborts before decryption. Server-supplied filenames are sanitized before writing to disk.
27
+ - **`payment_count` on marketplace offers is settled-sales volume, not a trust score.** Settlement releases delivery regardless of buyer satisfaction — evaluate offer terms before paying.
28
+
29
+ ## Custodial-wallet tradeoff
30
+
31
+ The recommended buyer setup (a custodial NWC wallet such as Coinos) means the wallet provider holds those funds and can freeze or censor them. Keep only a small working balance there. Sellers are unaffected: payouts go directly to whatever Lightning Address you control.
32
+
33
+ ## Dependencies
34
+
35
+ Runtime dependencies are pinned, widely-used libraries: `@modelcontextprotocol/sdk` (MCP transport), `@getalby/sdk` (NIP-47 client), `@noble/curves`/`@noble/hashes` (audited cryptography), `zod`, `ws`. The only network destinations at runtime are the Hypawave API over HTTPS, your wallet's Nostr relay, and presigned storage URLs returned by the API.
36
+
37
+ ## Verifying
38
+
39
+ ```bash
40
+ npm test # 32 unit tests, including the signer against Hypawave's published llms.txt test vector
41
+ ```
42
+
43
+ ## Reporting a vulnerability
44
+
45
+ Email **security@hypawave.com** (or support@hypawave.com). Please do not open a public issue for security-sensitive reports. We aim to acknowledge within a few business days.
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/index.js ADDED
@@ -0,0 +1,958 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+
7
+ // src/tools/discover.ts
8
+ import { z } from "zod";
9
+
10
+ // src/config.ts
11
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
12
+ import { homedir } from "os";
13
+ import { join } from "path";
14
+ import { randomBytes } from "crypto";
15
+ import { secp256k1 } from "@noble/curves/secp256k1";
16
+ import { bytesToHex } from "@noble/hashes/utils";
17
+ var API_BASE = process.env.HYPAWAVE_API_URL || "https://hypawave.com";
18
+ var FALLBACK_SPEND_CAP_SATS = 5e4;
19
+ var KEY_DIR = join(homedir(), ".hypawave");
20
+ var KEY_FILE = join(KEY_DIR, "identity.json");
21
+ function getNwcUrl() {
22
+ return process.env.NWC_URL || process.env.HYPAWAVE_NWC_URL || void 0;
23
+ }
24
+ function getMaxSpendSatsEnv() {
25
+ const raw = process.env.HYPAWAVE_MAX_SPEND_SATS;
26
+ if (!raw) return null;
27
+ const n = Number(raw);
28
+ if (!Number.isFinite(n) || n <= 0) return null;
29
+ return Math.floor(n);
30
+ }
31
+ function getPrivKey() {
32
+ const env = process.env.HYPAWAVE_PRIVKEY;
33
+ if (env) {
34
+ if (!/^[0-9a-fA-F]{64}$/.test(env)) {
35
+ throw new Error("HYPAWAVE_PRIVKEY must be a 32-byte hex string (64 chars)");
36
+ }
37
+ return env.toLowerCase();
38
+ }
39
+ if (existsSync(KEY_FILE)) {
40
+ const saved = JSON.parse(readFileSync(KEY_FILE, "utf8"));
41
+ if (typeof saved.privkey === "string" && /^[0-9a-f]{64}$/.test(saved.privkey)) {
42
+ return saved.privkey;
43
+ }
44
+ throw new Error(`Corrupt identity file at ${KEY_FILE} \u2014 restore it or set $HYPAWAVE_PRIVKEY`);
45
+ }
46
+ const privkey = generatePrivKey();
47
+ mkdirSync(KEY_DIR, { recursive: true, mode: 448 });
48
+ writeFileSync(KEY_FILE, JSON.stringify({ privkey, created_at: (/* @__PURE__ */ new Date()).toISOString() }, null, 2), {
49
+ mode: 384
50
+ });
51
+ return privkey;
52
+ }
53
+ function getPubKey() {
54
+ return bytesToHex(secp256k1.getPublicKey(getPrivKey(), true));
55
+ }
56
+ function generatePrivKey() {
57
+ for (; ; ) {
58
+ const candidate = randomBytes(32);
59
+ try {
60
+ secp256k1.getPublicKey(candidate, true);
61
+ return bytesToHex(candidate);
62
+ } catch {
63
+ }
64
+ }
65
+ }
66
+
67
+ // src/signer.ts
68
+ import { createHash, randomBytes as randomBytes2 } from "crypto";
69
+ import { secp256k1 as secp256k12 } from "@noble/curves/secp256k1";
70
+ import { bytesToHex as bytesToHex2, hexToBytes } from "@noble/hashes/utils";
71
+ var sha256Hex = (input) => createHash("sha256").update(input).digest("hex");
72
+ function signRequest({
73
+ body,
74
+ privKey,
75
+ timestamp,
76
+ nonce
77
+ }) {
78
+ const pubKey = bytesToHex2(secp256k12.getPublicKey(privKey, true));
79
+ let fullBody = body;
80
+ let termsHash = null;
81
+ if (body) {
82
+ termsHash = sha256Hex(JSON.stringify(body));
83
+ const termsSig = secp256k12.sign(hexToBytes(termsHash), privKey, { lowS: true });
84
+ fullBody = { ...body, signed_payload_hash: termsHash, signature: termsSig.toDERHex() };
85
+ }
86
+ const bodyStr = fullBody ? JSON.stringify(fullBody) : "";
87
+ const bodyHash = sha256Hex(bodyStr);
88
+ const ts = timestamp ?? Math.floor(Date.now() / 1e3).toString();
89
+ const nce = nonce ?? randomBytes2(16).toString("hex");
90
+ const canonicalHash = sha256Hex(`${bodyHash}:${ts}:${nce}`);
91
+ const authSig = secp256k12.sign(hexToBytes(canonicalHash), privKey, { lowS: true });
92
+ return {
93
+ headers: {
94
+ "Content-Type": "application/json",
95
+ "x-pubkey": pubKey,
96
+ "x-signature": authSig.toDERHex(),
97
+ "x-signed-payload-hash": bodyHash,
98
+ "x-timestamp": ts,
99
+ "x-nonce": nce
100
+ },
101
+ body: bodyStr || void 0,
102
+ debug: { pubKey, termsHash, bodyHash, canonicalHash }
103
+ };
104
+ }
105
+
106
+ // src/api.ts
107
+ var HypawaveApiError = class extends Error {
108
+ constructor(status, code, message) {
109
+ super(message);
110
+ this.status = status;
111
+ this.code = code;
112
+ this.name = "HypawaveApiError";
113
+ }
114
+ status;
115
+ code;
116
+ };
117
+ async function hw(path, opts = {}) {
118
+ const method = opts.method ?? (opts.body !== void 0 ? "POST" : "GET");
119
+ const url = new URL(path, API_BASE);
120
+ for (const [k, v] of Object.entries(opts.query ?? {})) {
121
+ if (v !== void 0) url.searchParams.set(k, String(v));
122
+ }
123
+ let headers = { "Content-Type": "application/json" };
124
+ let bodyStr;
125
+ if (opts.signed) {
126
+ const signed = signRequest({ body: opts.body ?? null, privKey: getPrivKey() });
127
+ headers = signed.headers;
128
+ bodyStr = signed.body;
129
+ } else if (opts.body !== void 0 && opts.body !== null) {
130
+ bodyStr = JSON.stringify(opts.body);
131
+ }
132
+ const res = await fetch(url, { method, headers, body: bodyStr });
133
+ const text = await res.text();
134
+ let json;
135
+ try {
136
+ json = text ? JSON.parse(text) : void 0;
137
+ } catch {
138
+ json = void 0;
139
+ }
140
+ if (!res.ok) {
141
+ const code = json?.error || `http_${res.status}`;
142
+ const message = json?.message || text.slice(0, 300) || res.statusText;
143
+ throw new HypawaveApiError(res.status, code, message);
144
+ }
145
+ return json ?? {};
146
+ }
147
+ function isApiError(e, code) {
148
+ return e instanceof HypawaveApiError && (code === void 0 || e.code === code);
149
+ }
150
+
151
+ // src/util.ts
152
+ import { basename } from "path";
153
+
154
+ // src/bolt11.ts
155
+ var HRP_RE = /^ln(?:bcrt|tbs|tb|bc)(\d+)([munp]?)1/i;
156
+ var MULTIPLIER_SATS = {
157
+ // 1 BTC = 1e8 sats; m=1e-3 BTC, u=1e-6, n=1e-9, p=1e-12
158
+ "": 1e8,
159
+ m: 1e5,
160
+ u: 100,
161
+ n: 0.1,
162
+ p: 1e-4
163
+ };
164
+ function bolt11AmountSats(bolt11) {
165
+ const m = HRP_RE.exec(bolt11.trim());
166
+ if (!m) return null;
167
+ const value = Number(m[1]);
168
+ if (!Number.isFinite(value)) return null;
169
+ return Math.ceil(value * MULTIPLIER_SATS[m[2].toLowerCase()]);
170
+ }
171
+
172
+ // src/util.ts
173
+ var cachedDerivedCap = null;
174
+ var DERIVED_CAP_TTL_MS = 5 * 6e4;
175
+ async function getSpendCapSats() {
176
+ const envCap = getMaxSpendSatsEnv();
177
+ if (envCap !== null) return { cap: envCap, source: "HYPAWAVE_MAX_SPEND_SATS" };
178
+ if (cachedDerivedCap && Date.now() - cachedDerivedCap.fetchedAt < DERIVED_CAP_TTL_MS) {
179
+ return { cap: cachedDerivedCap.value, source: "platform max_invoice_usd (cached)" };
180
+ }
181
+ try {
182
+ const s = await hw("/api/public-settings");
183
+ if (s.max_invoice_usd && s.btc_usd_price && s.btc_usd_price > 0) {
184
+ const cap = Math.ceil(s.max_invoice_usd / s.btc_usd_price * 1e8);
185
+ cachedDerivedCap = { value: cap, fetchedAt: Date.now() };
186
+ return { cap, source: `platform max_invoice_usd ($${s.max_invoice_usd} @ $${s.btc_usd_price}/BTC)` };
187
+ }
188
+ } catch {
189
+ }
190
+ return { cap: FALLBACK_SPEND_CAP_SATS, source: "static fallback (platform settings unreachable)" };
191
+ }
192
+ async function assertWithinSpendCap(amountSats, context) {
193
+ if (amountSats === null) {
194
+ throw new Error(
195
+ `${context}: could not determine the invoice amount \u2014 refusing to auto-pay. Pay manually and use confirm_payment.`
196
+ );
197
+ }
198
+ const { cap, source } = await getSpendCapSats();
199
+ if (amountSats > cap) {
200
+ throw new Error(
201
+ `${context}: amount ${amountSats} sats exceeds the spending cap of ${cap} sats (${source}). Not paid. Raise HYPAWAVE_MAX_SPEND_SATS or pay manually and use confirm_payment.`
202
+ );
203
+ }
204
+ }
205
+ function effectiveAmountSats(bolt11, quotedSats) {
206
+ const decoded = bolt11AmountSats(bolt11);
207
+ if (decoded !== null && quotedSats !== void 0 && Math.abs(decoded - quotedSats) > 1) {
208
+ throw new Error(
209
+ `bolt11 amount (${decoded} sats) does not match the quoted amount (${quotedSats} sats) \u2014 refusing to pay`
210
+ );
211
+ }
212
+ return decoded ?? quotedSats ?? null;
213
+ }
214
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
215
+ async function pollUntil(fn, { timeoutMs = 6e4, intervalMs = 2500 } = {}) {
216
+ const deadline = Date.now() + timeoutMs;
217
+ for (; ; ) {
218
+ const result = await fn();
219
+ if (result !== null) return result;
220
+ if (Date.now() + intervalMs > deadline) return null;
221
+ await sleep(intervalMs);
222
+ }
223
+ }
224
+ function safeFilename(name, fallback) {
225
+ const base = basename(name || "").replace(/[\x00-\x1f\x7f]/g, "").trim();
226
+ if (!base || base === "." || base === "..") return fallback;
227
+ return base;
228
+ }
229
+ function jsonResult(data) {
230
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
231
+ }
232
+
233
+ // src/tools/discover.ts
234
+ function registerDiscoverTools(server2) {
235
+ server2.registerTool(
236
+ "search_offers",
237
+ {
238
+ title: "Search the Hypawave public offer directory",
239
+ description: "Browse opt-in public offers (data, APIs, compute, files) purchasable over Bitcoin Lightning. Returns id, title, category, tags, output_type, input_schema, price, and payment_count (settled-sales volume \u2014 NOT a trust or quality guarantee). Buy a result with buy_offer. Note: many offers are private (agent-to-agent by direct offer_id) and never appear here.",
240
+ inputSchema: {
241
+ q: z.string().optional().describe("Free-text search over title/description"),
242
+ category: z.enum(["data", "api", "compute", "media", "software", "access", "action", "other"]).optional(),
243
+ tags: z.string().optional().describe("Comma-separated tags; results must match all"),
244
+ sort: z.enum(["newest", "settled"]).optional().describe("Default newest"),
245
+ limit: z.number().int().min(1).max(50).optional(),
246
+ cursor: z.string().optional().describe("Pagination cursor from next_cursor (newest sort)"),
247
+ offset: z.number().int().min(0).optional().describe("Pagination offset (settled sort)")
248
+ }
249
+ },
250
+ async (args) => jsonResult(await hw("/api/offers/public", { query: { ...args } }))
251
+ );
252
+ server2.registerTool(
253
+ "get_offer",
254
+ {
255
+ title: "Read a Hypawave offer's terms",
256
+ description: "Fetch an offer's full terms before buying: amount, currency, pricing_type, description, creator_pubkey, status, file_count, remaining capacity (max_payments vs payment_count), and metadata. Always read and evaluate the terms before paying.",
257
+ inputSchema: {
258
+ offer_id: z.string().uuid().describe("The offer id")
259
+ }
260
+ },
261
+ async ({ offer_id }) => jsonResult(await hw(`/api/offers/${offer_id}`))
262
+ );
263
+ }
264
+
265
+ // src/tools/buy.ts
266
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
267
+ import { join as join2, resolve } from "path";
268
+ import { z as z2 } from "zod";
269
+
270
+ // src/crypto.ts
271
+ import { createCipheriv, createDecipheriv, createHash as createHash2, randomBytes as randomBytes3 } from "crypto";
272
+ function sha256HexOf(data) {
273
+ return createHash2("sha256").update(data).digest("hex");
274
+ }
275
+ function encryptFile(plaintext) {
276
+ const key = randomBytes3(32);
277
+ const iv = randomBytes3(12);
278
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
279
+ const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final(), cipher.getAuthTag()]);
280
+ return {
281
+ ciphertext,
282
+ keyB64: key.toString("base64"),
283
+ ivHex: iv.toString("hex"),
284
+ ciphertextSha256: sha256HexOf(ciphertext)
285
+ };
286
+ }
287
+ function decryptFile(ciphertext, keyB64, ivHex) {
288
+ if (ciphertext.length < 16) throw new Error("ciphertext too short \u2014 missing GCM auth tag");
289
+ const key = Buffer.from(keyB64, "base64");
290
+ if (key.length !== 32) throw new Error("encryption key must be 32 bytes (AES-256)");
291
+ const iv = Buffer.from(ivHex, "hex");
292
+ const tag = ciphertext.subarray(ciphertext.length - 16);
293
+ const data = ciphertext.subarray(0, ciphertext.length - 16);
294
+ const decipher = createDecipheriv("aes-256-gcm", key, iv);
295
+ decipher.setAuthTag(tag);
296
+ return Buffer.concat([decipher.update(data), decipher.final()]);
297
+ }
298
+ function verifyCommitment(ciphertext, expectedSha256) {
299
+ if (!expectedSha256) return;
300
+ const actual = sha256HexOf(ciphertext);
301
+ if (actual !== expectedSha256.toLowerCase()) {
302
+ throw new Error(
303
+ `content mismatch \u2014 downloaded bytes (sha256 ${actual}) do not match the seller's commitment (${expectedSha256}); aborting before decrypt`
304
+ );
305
+ }
306
+ }
307
+ function paymentHashFromPreimage(preimageHex) {
308
+ if (!/^[0-9a-fA-F]{64}$/.test(preimageHex)) {
309
+ throw new Error("preimage must be a 32-byte hex string (64 chars)");
310
+ }
311
+ return createHash2("sha256").update(Buffer.from(preimageHex, "hex")).digest("hex");
312
+ }
313
+
314
+ // src/nwc.ts
315
+ var clientPromise = null;
316
+ function nwcConfigured() {
317
+ return Boolean(getNwcUrl());
318
+ }
319
+ async function getClient() {
320
+ if (!clientPromise) {
321
+ clientPromise = (async () => {
322
+ const url = getNwcUrl();
323
+ if (!url) throw new Error("NWC_URL is not set \u2014 wallet payments unavailable (manual mode only)");
324
+ if (typeof globalThis.WebSocket === "undefined") {
325
+ const ws = await import("ws");
326
+ globalThis.WebSocket = ws.default;
327
+ }
328
+ const { nwc } = await import("@getalby/sdk");
329
+ return new nwc.NWCClient({ nostrWalletConnectUrl: url });
330
+ })();
331
+ clientPromise.catch(() => {
332
+ clientPromise = null;
333
+ });
334
+ }
335
+ return clientPromise;
336
+ }
337
+ async function payBolt11(bolt11) {
338
+ const client = await getClient();
339
+ const result = await client.payInvoice({ invoice: bolt11 });
340
+ if (!result?.preimage || !/^[0-9a-fA-F]{64}$/.test(result.preimage)) {
341
+ throw new Error(
342
+ "wallet paid (or attempted) but returned no valid preimage \u2014 cannot prove settlement; check the payment in your wallet before retrying"
343
+ );
344
+ }
345
+ return { preimage: result.preimage.toLowerCase() };
346
+ }
347
+ async function getBalanceSats() {
348
+ const client = await getClient();
349
+ const { balance } = await client.getBalance();
350
+ return Math.floor(balance / 1e3);
351
+ }
352
+
353
+ // src/tools/buy.ts
354
+ async function confirmAndClaim(paymentIntentId, preimage, payerSecret) {
355
+ const confirm = await hw(`/api/offers/payment-intent/${paymentIntentId}/confirm`, {
356
+ body: { preimage, payer_secret: payerSecret }
357
+ });
358
+ const settled = await pollUntil(async () => {
359
+ const s = await hw(`/api/offers/payment-intent/${paymentIntentId}/status`, {
360
+ query: { secret: payerSecret }
361
+ });
362
+ return s.status === "settled" ? s : null;
363
+ });
364
+ return { confirm, settled };
365
+ }
366
+ function registerBuyTools(server2) {
367
+ server2.registerTool(
368
+ "buy_offer",
369
+ {
370
+ title: "Buy a Hypawave offer (pay over Lightning)",
371
+ description: "Purchase an offer end-to-end. With an NWC wallet configured (NWC_URL): fetches a creator-direct bolt11, enforces the operator spending cap, pays it, submits the settlement preimage, and polls until settled \u2014 returns a claim_token for download_files (or, for execution offers, the preimage to present to the seller's API as your credential). Without NWC: returns the bolt11 + payer_secret + payment_intent_id; pay it with any preimage-returning wallet, then call confirm_payment. SPENDS REAL BITCOIN \u2014 read the offer terms with get_offer first. Settlement is final; no refunds.",
372
+ inputSchema: {
373
+ offer_id: z2.string().uuid(),
374
+ expected_max_sats: z2.number().int().positive().optional().describe(
375
+ "Refuse to pay if the quoted amount exceeds this (your own per-purchase bound, applied in addition to the operator cap)"
376
+ )
377
+ }
378
+ },
379
+ async ({ offer_id, expected_max_sats }) => {
380
+ const pay = await hw(`/api/offers/${offer_id}/pay`, { body: {} });
381
+ const amount = effectiveAmountSats(pay.bolt11, pay.locked_amount_sats);
382
+ if (expected_max_sats !== void 0 && amount !== null && amount > expected_max_sats) {
383
+ throw new Error(
384
+ `quoted amount ${amount} sats exceeds your expected_max_sats (${expected_max_sats}) \u2014 not paid`
385
+ );
386
+ }
387
+ if (!nwcConfigured()) {
388
+ return jsonResult({
389
+ mode: "manual",
390
+ message: "No NWC wallet configured. Pay this bolt11 with any wallet that returns the preimage, then call confirm_payment with {payment_intent_id, preimage, payer_secret}.",
391
+ payment_intent_id: pay.payment_intent_id,
392
+ bolt11: pay.bolt11,
393
+ amount_sats: amount,
394
+ payer_secret: pay.payer_secret,
395
+ expires_at: pay.expires_at
396
+ });
397
+ }
398
+ await assertWithinSpendCap(amount, `buy_offer ${offer_id}`);
399
+ const { preimage } = await payBolt11(pay.bolt11);
400
+ const { settled } = await confirmAndClaim(pay.payment_intent_id, preimage, pay.payer_secret);
401
+ return jsonResult({
402
+ ok: true,
403
+ settled: Boolean(settled),
404
+ payment_intent_id: pay.payment_intent_id,
405
+ amount_sats: amount,
406
+ claim_token: settled?.claim_token ?? null,
407
+ claim_token_expires_at: settled?.token_expires_at ?? null,
408
+ preimage,
409
+ payer_secret: pay.payer_secret,
410
+ next: !settled ? "Settlement not yet observed \u2014 poll confirm_payment or retry later with the same preimage (idempotent)." : settled.claim_token ? "File offer: call download_files with {payment_intent_id, claim_token}." : "Settled. No files on this offer (execution offer) \u2014 present {payment_intent_id, preimage} to the seller's API as your credential."
411
+ });
412
+ }
413
+ );
414
+ server2.registerTool(
415
+ "confirm_payment",
416
+ {
417
+ title: "Confirm an offer payment with a preimage (manual mode)",
418
+ description: "Submit the Lightning preimage as settlement proof for an offer purchase made outside NWC (you paid the bolt11 from buy_offer manually). Idempotent \u2014 safe to retry. Returns the claim_token once settled.",
419
+ inputSchema: {
420
+ payment_intent_id: z2.string().uuid(),
421
+ preimage: z2.string().regex(/^[0-9a-fA-F]{64}$/, "64-char hex preimage"),
422
+ payer_secret: z2.string().describe("payer_secret returned by buy_offer")
423
+ }
424
+ },
425
+ async ({ payment_intent_id, preimage, payer_secret }) => {
426
+ const { settled } = await confirmAndClaim(payment_intent_id, preimage.toLowerCase(), payer_secret);
427
+ return jsonResult({
428
+ ok: true,
429
+ settled: Boolean(settled),
430
+ claim_token: settled?.claim_token ?? null,
431
+ claim_token_expires_at: settled?.token_expires_at ?? null
432
+ });
433
+ }
434
+ );
435
+ server2.registerTool(
436
+ "download_files",
437
+ {
438
+ title: "Download and decrypt purchased offer files",
439
+ description: "After a settled purchase (buy_offer / confirm_payment returned a claim_token): fetches each file's key, downloads the encrypted blob, verifies it against the seller's ciphertext_sha256 commitment, decrypts (AES-256-GCM) locally, and writes plaintext files to output_dir. Returns the saved paths.",
440
+ inputSchema: {
441
+ payment_intent_id: z2.string().uuid(),
442
+ claim_token: z2.string(),
443
+ output_dir: z2.string().describe("Absolute directory to save decrypted files into (created if missing)")
444
+ }
445
+ },
446
+ async ({ payment_intent_id, claim_token, output_dir }) => {
447
+ const dir = resolve(output_dir);
448
+ mkdirSync2(dir, { recursive: true });
449
+ const { files } = await hw(
450
+ `/api/offers/payment-intent/${payment_intent_id}/file-key`,
451
+ { query: { claim_token } }
452
+ );
453
+ if (!files?.length) {
454
+ return jsonResult({ ok: true, files: [], message: "No files attached to this offer (execution-only offer?)" });
455
+ }
456
+ const saved = [];
457
+ for (const f of files) {
458
+ const { downloadUrl } = await hw(
459
+ `/api/offers/payment-intent/${payment_intent_id}/download-url`,
460
+ { body: { offer_file_id: f.offer_file_id, claim_token } }
461
+ );
462
+ const res = await fetch(downloadUrl);
463
+ if (!res.ok) throw new Error(`download failed for ${f.offer_file_id}: HTTP ${res.status}`);
464
+ const ciphertext = Buffer.from(await res.arrayBuffer());
465
+ verifyCommitment(ciphertext, f.ciphertext_sha256);
466
+ const plaintext = decryptFile(ciphertext, f.wrapped_key, f.iv_hex);
467
+ const path = join2(dir, safeFilename(f.filename, `${f.offer_file_id}.bin`));
468
+ writeFileSync2(path, plaintext);
469
+ saved.push({ path, bytes: plaintext.length, commitment_verified: Boolean(f.ciphertext_sha256) });
470
+ }
471
+ return jsonResult({ ok: true, files: saved });
472
+ }
473
+ );
474
+ }
475
+
476
+ // src/tools/invoice-buy.ts
477
+ import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
478
+ import { join as join3, resolve as resolve2 } from "path";
479
+ import { z as z3 } from "zod";
480
+ async function retrieveInvoiceFiles(invoiceId, token, outputDir) {
481
+ const dir = resolve2(outputDir);
482
+ mkdirSync3(dir, { recursive: true });
483
+ const { files } = await hw("/api/get-invoice-files", {
484
+ body: { invoice_ids: [invoiceId], token }
485
+ });
486
+ const records = Object.values(files ?? {}).flat();
487
+ const saved = [];
488
+ for (const f of records) {
489
+ const key = await hw("/api/get-key", {
490
+ query: { invoice_file_id: f.id, token }
491
+ });
492
+ const { downloadUrl } = await hw("/api/generate-download-url", {
493
+ body: { invoice_file_id: f.id, token }
494
+ });
495
+ const res = await fetch(downloadUrl);
496
+ if (!res.ok) throw new Error(`download failed for ${f.id}: HTTP ${res.status}`);
497
+ const ciphertext = Buffer.from(await res.arrayBuffer());
498
+ verifyCommitment(ciphertext, key.ciphertext_sha256);
499
+ const plaintext = decryptFile(ciphertext, key.encryption_key, key.iv_hex);
500
+ const path = join3(dir, safeFilename(f.file_name, `${f.id}.bin`));
501
+ writeFileSync3(path, plaintext);
502
+ saved.push({ path, bytes: plaintext.length, commitment_verified: Boolean(key.ciphertext_sha256) });
503
+ }
504
+ return saved;
505
+ }
506
+ function registerInvoiceBuyTools(server2) {
507
+ server2.registerTool(
508
+ "pay_invoice",
509
+ {
510
+ title: "Pay a Hypawave invoice payload (Path 2/3a buyer)",
511
+ description: "Settle a one-off Hypawave invoice a seller handed you (a payload with invoice_id + access_token). With NWC configured: fetches the creator-direct bolt11, enforces the spending cap, pays, confirms with the preimage, then downloads + verifies + decrypts any attached files to output_dir. Without NWC: returns the bolt11 \u2014 pay it manually, then re-call this tool with the preimage to confirm and fetch files. SPENDS REAL BITCOIN. Settlement is final; no refunds.",
512
+ inputSchema: {
513
+ invoice_id: z3.string().describe("Invoice id from the seller's payment payload"),
514
+ access_token: z3.string().describe("access_token from the seller's payment payload"),
515
+ preimage: z3.string().regex(/^[0-9a-fA-F]{64}$/).optional().describe("Only for manual mode: the preimage from paying the bolt11 yourself"),
516
+ output_dir: z3.string().optional().describe("Absolute directory for decrypted files (default: skip file retrieval)"),
517
+ expected_max_sats: z3.number().int().positive().optional().describe("Refuse if the bolt11 exceeds this")
518
+ }
519
+ },
520
+ async ({ invoice_id, access_token, preimage, output_dir, expected_max_sats }) => {
521
+ let settledPreimage = preimage?.toLowerCase();
522
+ if (!settledPreimage) {
523
+ const cb = await hw("/api/paystream-cb", {
524
+ query: { token: access_token }
525
+ });
526
+ const amount = effectiveAmountSats(cb.pr);
527
+ if (expected_max_sats !== void 0 && amount !== null && amount > expected_max_sats) {
528
+ throw new Error(`bolt11 amount ${amount} sats exceeds expected_max_sats (${expected_max_sats}) \u2014 not paid`);
529
+ }
530
+ if (!nwcConfigured()) {
531
+ return jsonResult({
532
+ mode: "manual",
533
+ message: "No NWC wallet configured. Pay this bolt11 with a preimage-returning wallet, then call pay_invoice again with the preimage.",
534
+ invoice_id,
535
+ bolt11: cb.pr,
536
+ amount_sats: amount,
537
+ terms_hash: cb.terms_hash
538
+ });
539
+ }
540
+ await assertWithinSpendCap(amount, `pay_invoice ${invoice_id}`);
541
+ settledPreimage = (await payBolt11(cb.pr)).preimage;
542
+ }
543
+ const paymentHash = paymentHashFromPreimage(settledPreimage);
544
+ const confirm = await hw(`/api/invoice/${invoice_id}/confirm`, {
545
+ body: { payment_hash: paymentHash, preimage: settledPreimage }
546
+ });
547
+ const files = output_dir ? await retrieveInvoiceFiles(invoice_id, access_token, output_dir) : void 0;
548
+ return jsonResult({
549
+ ok: true,
550
+ invoice_id,
551
+ confirm,
552
+ preimage: settledPreimage,
553
+ files: files ?? "not retrieved \u2014 pass output_dir to download and decrypt attached files"
554
+ });
555
+ }
556
+ );
557
+ }
558
+
559
+ // src/tools/sell.ts
560
+ import { readFileSync as readFileSync2 } from "fs";
561
+ import { basename as basename2 } from "path";
562
+ import { z as z4 } from "zod";
563
+ async function payFee(bolt11, feeSats, context) {
564
+ const amount = effectiveAmountSats(bolt11, feeSats);
565
+ await assertWithinSpendCap(amount, context);
566
+ await payBolt11(bolt11);
567
+ return amount;
568
+ }
569
+ async function waitForActivation(offerId) {
570
+ const windowEnd = async () => {
571
+ const offer = await hw(`/api/offers/${offerId}`);
572
+ const we2 = offer.activation_window_end ?? null;
573
+ return we2 && new Date(we2).getTime() > Date.now() ? we2 : null;
574
+ };
575
+ let we = await pollUntil(windowEnd, { timeoutMs: 2e4 });
576
+ if (!we) {
577
+ await hw(`/api/offers/${offerId}/pay`, { body: {} }).catch(() => void 0);
578
+ we = await pollUntil(windowEnd, { timeoutMs: 15e3 });
579
+ }
580
+ return { activated: Boolean(we), window_end: we };
581
+ }
582
+ function registerSellTools(server2) {
583
+ server2.registerTool(
584
+ "create_offer",
585
+ {
586
+ title: "Create a Hypawave offer (sell files/data/API/compute for Bitcoin)",
587
+ description: "Create a reusable Path 3b offer sold over Lightning. Payments go creator-direct to your payment_destination (a Lightning Address or LNURL-pay URL \u2014 any receiving wallet works; you need NO node and NO preimage support to sell). The offer is inert until you pay the returned activation fee bolt11 (fee = unit_price \xD7 max_payments \xD7 fee% \u2014 Hypawave's only charge; principal never touches Hypawave). Attach files with attach_file BEFORE the fee settles \u2014 content is sealed at activation. Set pay_activation_fee=true to pay it automatically via NWC. By default the offer is PRIVATE (share the offer_id directly, agent-to-agent). To list it in the public marketplace, set is_public=true with title, category, and output_type (immutable after creation).",
588
+ inputSchema: {
589
+ amount: z4.number().positive().describe("Price per sale, in sats (pricing_type=sats) or fiat units"),
590
+ pricing_type: z4.enum(["sats", "fiat"]),
591
+ currency: z4.string().optional().describe("Fiat currency code (e.g. USD) when pricing_type=fiat"),
592
+ description: z4.string().min(1).max(2e3),
593
+ payment_destination: z4.string().describe("YOUR payout destination: Lightning Address (name@domain) or LNURL-pay URL"),
594
+ max_payments: z4.number().int().positive().describe("Unlock capacity N \u2014 how many times the offer can be bought (fee basis; immutable, extend via manage_offer add_capacity)"),
595
+ activation_window: z4.string().optional().describe('Payability window, e.g. "30d" (default), bounds 1d\u2013365d'),
596
+ execution_webhook: z4.string().url().optional().describe("HTTPS endpoint POSTed the settlement proof (for selling execution instead of files)"),
597
+ metadata: z4.record(z4.unknown()).optional(),
598
+ is_public: z4.boolean().optional().describe("List in the public marketplace directory (default false = private)"),
599
+ title: z4.string().max(60).optional().describe("Required when is_public"),
600
+ category: z4.enum(["data", "api", "compute", "media", "software", "access", "action", "other"]).optional().describe("Required when is_public"),
601
+ output_type: z4.enum(["file", "link", "json", "text", "image", "video", "audio", "stream", "webhook"]).optional().describe("Required when is_public"),
602
+ tags: z4.array(z4.string()).max(5).optional(),
603
+ input_schema: z4.union([z4.string(), z4.record(z4.unknown())]).optional(),
604
+ pay_activation_fee: z4.boolean().optional().describe("Pay the activation fee automatically via NWC (default false). Attach files first if the offer has any!")
605
+ }
606
+ },
607
+ async ({ pay_activation_fee, ...body }) => {
608
+ const created = await hw("/api/offers", {
609
+ body: Object.fromEntries(Object.entries(body).filter(([, v]) => v !== void 0)),
610
+ signed: true
611
+ });
612
+ let feePaid = null;
613
+ let activation = null;
614
+ if (pay_activation_fee && created.activation?.fee_bolt11) {
615
+ if (!nwcConfigured()) throw new Error("pay_activation_fee=true but no NWC wallet configured");
616
+ feePaid = await payFee(
617
+ created.activation.fee_bolt11,
618
+ created.activation.fee_amount_sats,
619
+ `activation fee for offer ${created.offer_id}`
620
+ );
621
+ activation = await waitForActivation(created.offer_id);
622
+ }
623
+ return jsonResult({
624
+ ...created,
625
+ seller_pubkey: getPubKey(),
626
+ activation_fee_paid_sats: feePaid,
627
+ activated: activation?.activated ?? false,
628
+ activation_window_end: activation?.window_end ?? null,
629
+ next: !pay_activation_fee ? "Offer is INERT until the activation fee_bolt11 is paid (any wallet, no preimage needed). Attach files first via attach_file, then pay the fee." : activation?.activated ? "Offer is ACTIVE and payable. Share the offer_id (private) \u2014 public offers appear in search_offers." : "Fee paid but settlement not yet observed \u2014 check later with manage_offer action=status (activation_window_end set = active)."
630
+ });
631
+ }
632
+ );
633
+ server2.registerTool(
634
+ "attach_file",
635
+ {
636
+ title: "Encrypt and attach a local file to an offer or invoice",
637
+ description: "Encrypts a local file client-side (AES-256-GCM \u2014 Hypawave never sees plaintext), uploads the ciphertext, and registers the file + key with its ciphertext_sha256 content commitment. MUST run before the activation fee settles \u2014 content is sealed at activation. The presigned upload URL lasts 120s. Pass offer_id (Path 3b) or invoice_id (Path 3a), not both.",
638
+ inputSchema: {
639
+ offer_id: z4.string().uuid().optional(),
640
+ invoice_id: z4.string().optional(),
641
+ file_path: z4.string().describe("Absolute path of the plaintext file to sell"),
642
+ content_type: z4.string().optional().describe("MIME type (default application/octet-stream)")
643
+ }
644
+ },
645
+ async ({ offer_id, invoice_id, file_path, content_type }) => {
646
+ if (!offer_id === !invoice_id) throw new Error("pass exactly one of offer_id or invoice_id");
647
+ const plaintext = readFileSync2(file_path);
648
+ const fileName = basename2(file_path);
649
+ const mime = content_type || "application/octet-stream";
650
+ const enc = encryptFile(plaintext);
651
+ const { signedUrl, objectKey } = await hw(
652
+ "/api/offers/upload-url",
653
+ { body: { fileName, contentType: mime, fileSize: enc.ciphertext.length }, signed: true }
654
+ );
655
+ const put = await fetch(signedUrl, {
656
+ method: "PUT",
657
+ headers: { "Content-Type": mime },
658
+ body: new Uint8Array(enc.ciphertext)
659
+ });
660
+ if (!put.ok) throw new Error(`upload failed: HTTP ${put.status} (presigned URL expires after 120s \u2014 retry attach_file)`);
661
+ if (offer_id) {
662
+ const stored2 = await hw("/api/offers/store-file", {
663
+ body: {
664
+ offer_id,
665
+ storage_key: objectKey,
666
+ filename: fileName,
667
+ size: enc.ciphertext.length,
668
+ content_type: mime,
669
+ iv_hex: enc.ivHex,
670
+ ciphertext_sha256: enc.ciphertextSha256
671
+ },
672
+ signed: true
673
+ });
674
+ await hw("/api/offers/store-file-key", {
675
+ body: { offer_file_id: stored2.offer_file_id, wrapped_key: enc.keyB64 },
676
+ signed: true
677
+ });
678
+ return jsonResult({
679
+ ok: true,
680
+ offer_id,
681
+ offer_file_id: stored2.offer_file_id,
682
+ plaintext_bytes: plaintext.length,
683
+ ciphertext_sha256: enc.ciphertextSha256
684
+ });
685
+ }
686
+ const stored = await hw("/api/offers/store-invoice-file", {
687
+ body: {
688
+ invoice_id,
689
+ file_name: fileName,
690
+ encrypted_file_url: objectKey,
691
+ iv_hex: enc.ivHex,
692
+ size: enc.ciphertext.length,
693
+ ciphertext_sha256: enc.ciphertextSha256
694
+ },
695
+ signed: true
696
+ });
697
+ await hw("/api/offers/invoice-file-key", {
698
+ body: { invoice_file_id: stored.id, key_b64: enc.keyB64 },
699
+ signed: true
700
+ });
701
+ return jsonResult({
702
+ ok: true,
703
+ invoice_id,
704
+ invoice_file_id: stored.id,
705
+ plaintext_bytes: plaintext.length,
706
+ ciphertext_sha256: enc.ciphertextSha256
707
+ });
708
+ }
709
+ );
710
+ server2.registerTool(
711
+ "manage_offer",
712
+ {
713
+ title: "Manage an offer: status / renew / add capacity / deactivate",
714
+ description: "status: read the offer (activation state, payments sold vs max_payments, window end). renew: mint a fresh activation fee bolt11 after the window lapsed (402 offer_inactive on pay). add_capacity: buy M more unlock slots (returns a capacity fee bolt11). delete: deactivate the offer permanently. Fee bolt11s are paid automatically via NWC when pay_fee=true, otherwise returned for manual payment (any wallet).",
715
+ inputSchema: {
716
+ offer_id: z4.string().uuid(),
717
+ action: z4.enum(["status", "renew", "add_capacity", "delete"]),
718
+ add_capacity: z4.number().int().positive().optional().describe("Slots to add (action=add_capacity)"),
719
+ activation_window: z4.string().optional().describe('New window for renew, e.g. "30d"'),
720
+ pay_fee: z4.boolean().optional().describe("Pay the returned fee bolt11 automatically via NWC")
721
+ }
722
+ },
723
+ async ({ offer_id, action, add_capacity, activation_window, pay_fee }) => {
724
+ if (action === "status") {
725
+ return jsonResult(await hw(`/api/offers/${offer_id}`));
726
+ }
727
+ if (action === "delete") {
728
+ return jsonResult(await hw(`/api/offers/${offer_id}`, { method: "DELETE", body: null, signed: true }));
729
+ }
730
+ let result;
731
+ let fee;
732
+ if (action === "renew") {
733
+ try {
734
+ result = await hw(`/api/offers/${offer_id}/renew`, {
735
+ body: activation_window ? { activation_window } : {},
736
+ signed: true
737
+ });
738
+ fee = result.activation;
739
+ } catch (e) {
740
+ if (isApiError(e, "activation_not_needed")) {
741
+ return jsonResult({ ok: true, message: "activation window still live \u2014 no renewal needed", detail: e.message });
742
+ }
743
+ throw e;
744
+ }
745
+ } else {
746
+ if (!add_capacity) throw new Error("add_capacity (positive integer) is required for action=add_capacity");
747
+ result = await hw(`/api/offers/${offer_id}/add-capacity`, {
748
+ body: { add_capacity },
749
+ signed: true
750
+ });
751
+ fee = result.topup ?? result.activation;
752
+ }
753
+ let feePaid = null;
754
+ let activation = null;
755
+ if (pay_fee && fee?.fee_bolt11) {
756
+ if (!nwcConfigured()) throw new Error("pay_fee=true but no NWC wallet configured");
757
+ feePaid = await payFee(fee.fee_bolt11, fee.fee_amount_sats, `${action} fee for offer ${offer_id}`);
758
+ if (action === "renew") activation = await waitForActivation(offer_id);
759
+ }
760
+ return jsonResult({
761
+ ...result,
762
+ fee_paid_sats: feePaid,
763
+ ...activation ? { activated: activation.activated, activation_window_end: activation.window_end } : {}
764
+ });
765
+ }
766
+ );
767
+ server2.registerTool(
768
+ "create_invoice",
769
+ {
770
+ title: "Create a one-off Hypawave invoice (Path 3a seller)",
771
+ description: "Create a single-settlement invoice: one buyer pays once, creator-direct to your payment_destination. Returns the buyer payload (invoice_id + access_token \u2014 forward BOTH to the buyer, who settles it with pay_invoice) plus an activation fee bolt11 that must be paid before the invoice goes live. Attach a file first with attach_file(invoice_id=...) if selling a file \u2014 content seals at activation.",
772
+ inputSchema: {
773
+ amount: z4.number().positive(),
774
+ currency: z4.string().optional().describe("Default USD"),
775
+ description: z4.string().optional(),
776
+ payment_destination: z4.string().describe("YOUR Lightning Address or LNURL-pay URL"),
777
+ due_date: z4.string().describe("ISO date the invoice is due, e.g. 2026-07-31"),
778
+ client_email: z4.string().email().describe("Buyer contact email (required by the API)"),
779
+ client_first_name: z4.string(),
780
+ client_last_name: z4.string(),
781
+ company_name: z4.string().optional(),
782
+ expires_in: z4.enum(["1h", "24h", "7d"]).optional(),
783
+ execution_webhook: z4.string().url().optional(),
784
+ pay_activation_fee: z4.boolean().optional().describe("Pay the activation fee automatically via NWC (attach files first!)")
785
+ }
786
+ },
787
+ async ({ pay_activation_fee, ...body }) => {
788
+ const created = await hw(
789
+ "/api/offers/create-invoice",
790
+ { body: Object.fromEntries(Object.entries(body).filter(([, v]) => v !== void 0)), signed: true }
791
+ );
792
+ let feePaid = null;
793
+ if (pay_activation_fee && created.activation?.fee_bolt11) {
794
+ if (!nwcConfigured()) throw new Error("pay_activation_fee=true but no NWC wallet configured");
795
+ feePaid = await payFee(
796
+ created.activation.fee_bolt11,
797
+ created.activation.fee_amount_sats,
798
+ `activation fee for invoice ${created.invoice_id}`
799
+ );
800
+ }
801
+ return jsonResult({
802
+ ...created,
803
+ activation_fee_paid_sats: feePaid,
804
+ next: pay_activation_fee ? "Fee paid \u2014 once settled the invoice is live. Forward {invoice_id, access_token} to the buyer." : "Invoice is INERT until the activation fee_bolt11 is paid (any wallet). Attach files first if needed, pay the fee, then forward {invoice_id, access_token} to the buyer."
805
+ });
806
+ }
807
+ );
808
+ }
809
+
810
+ // src/tools/status.ts
811
+ import { z as z5 } from "zod";
812
+ function registerStatusTools(server2) {
813
+ server2.registerTool(
814
+ "my_offers",
815
+ {
816
+ title: "List your own offers (seller)",
817
+ description: "List all offers created by this server's seller identity (pubkey-signed). Shows each offer's status, capacity usage, and activation window \u2014 use manage_offer for details/renewal.",
818
+ inputSchema: {
819
+ status: z5.string().optional().describe("Filter by offer status")
820
+ }
821
+ },
822
+ async ({ status }) => jsonResult(await hw("/api/offers/list", { method: "GET", signed: true, query: { status } }))
823
+ );
824
+ server2.registerTool(
825
+ "list_sales",
826
+ {
827
+ title: "List your sales (seller reconciliation)",
828
+ description: "List settled/pending sales for this seller identity (pubkey-signed). kind=offers \u2192 Path 3b payment intents (via /api/offers/list-payments, filterable by offer_id); kind=invoices \u2192 Path 3a invoices (via /api/offers/list-invoices). Returns payment_hash/preimage per sale \u2014 the authoritative way to reconcile missed execution_webhook deliveries.",
829
+ inputSchema: {
830
+ kind: z5.enum(["offers", "invoices"]),
831
+ offer_id: z5.string().uuid().optional().describe("Filter to one offer (kind=offers only)"),
832
+ status: z5.string().optional(),
833
+ limit: z5.number().int().min(1).max(100).optional(),
834
+ offset: z5.number().int().min(0).optional()
835
+ }
836
+ },
837
+ async ({ kind, offer_id, status, limit, offset }) => {
838
+ const path = kind === "offers" ? "/api/offers/list-payments" : "/api/offers/list-invoices";
839
+ return jsonResult(
840
+ await hw(path, {
841
+ method: "GET",
842
+ signed: true,
843
+ query: { status, limit, offset, ...kind === "offers" ? { offer_id } : {} }
844
+ })
845
+ );
846
+ }
847
+ );
848
+ server2.registerTool(
849
+ "get_receipt",
850
+ {
851
+ title: "Fetch a settlement receipt for a past purchase",
852
+ description: "Retrieve the durable settlement record for a purchase you made. For an offer purchase (Path 3b) pass payment_intent_id + payer_secret (both returned by buy_offer). For an invoice (Path 2/3a) pass invoice_id + preimage (pay_invoice returned the preimage).",
853
+ inputSchema: {
854
+ payment_intent_id: z5.string().uuid().optional(),
855
+ payer_secret: z5.string().optional().describe("Required with payment_intent_id"),
856
+ invoice_id: z5.string().optional(),
857
+ preimage: z5.string().regex(/^[0-9a-fA-F]{64}$/).optional().describe("Required with invoice_id")
858
+ }
859
+ },
860
+ async ({ payment_intent_id, payer_secret, invoice_id, preimage }) => {
861
+ if (payment_intent_id) {
862
+ if (!payer_secret) throw new Error("payer_secret is required with payment_intent_id");
863
+ return jsonResult(
864
+ await hw(`/api/offers/payment-intent/${payment_intent_id}/receipt`, { query: { secret: payer_secret } })
865
+ );
866
+ }
867
+ if (invoice_id) {
868
+ if (!preimage) throw new Error("preimage is required with invoice_id");
869
+ return jsonResult(
870
+ await hw(`/api/invoice/${invoice_id}/receipt`, { query: { preimage: preimage.toLowerCase() } })
871
+ );
872
+ }
873
+ throw new Error("pass payment_intent_id+payer_secret (offer) or invoice_id+preimage (invoice)");
874
+ }
875
+ );
876
+ server2.registerTool(
877
+ "check_payment",
878
+ {
879
+ title: "Check settlement/unlock status of a purchase",
880
+ description: "Non-destructive status check. For an offer purchase (Path 3b) pass payment_intent_id + payer_secret \u2014 returns status and the claim_token once settled. For invoices (Path 2/3a) pass invoice_ids \u2014 returns unlock status per invoice.",
881
+ inputSchema: {
882
+ payment_intent_id: z5.string().uuid().optional(),
883
+ payer_secret: z5.string().optional().describe("Required with payment_intent_id"),
884
+ invoice_ids: z5.array(z5.string()).optional().describe("Invoice ids to check (Path 2/3a)")
885
+ }
886
+ },
887
+ async ({ payment_intent_id, payer_secret, invoice_ids }) => {
888
+ if (payment_intent_id) {
889
+ if (!payer_secret) throw new Error("payer_secret is required with payment_intent_id");
890
+ return jsonResult(
891
+ await hw(`/api/offers/payment-intent/${payment_intent_id}/status`, { query: { secret: payer_secret } })
892
+ );
893
+ }
894
+ if (invoice_ids?.length) {
895
+ return jsonResult(await hw("/api/get-unlock-status", { body: { invoice_ids } }));
896
+ }
897
+ throw new Error("pass payment_intent_id+payer_secret (offer) or invoice_ids (invoices)");
898
+ }
899
+ );
900
+ }
901
+
902
+ // src/tools/wallet.ts
903
+ function registerWalletTools(server2) {
904
+ server2.registerTool(
905
+ "wallet_status",
906
+ {
907
+ title: "Wallet, identity, and platform settings",
908
+ description: "Reports the operator wallet state (NWC configured? spendable balance in sats), your seller identity pubkey, the operator spending cap, and Hypawave's live public settings (fee_percent, min_fee_sats, limits, BTC price). Call this first to know whether payments can be made automatically and what fees to expect.",
909
+ inputSchema: {}
910
+ },
911
+ async () => {
912
+ const settings = await hw("/api/public-settings").catch((e) => ({ error: String(e) }));
913
+ let balance_sats = null;
914
+ let wallet_error;
915
+ if (nwcConfigured()) {
916
+ try {
917
+ balance_sats = await getBalanceSats();
918
+ } catch (e) {
919
+ wallet_error = e instanceof Error ? e.message : String(e);
920
+ }
921
+ }
922
+ let seller_pubkey = null;
923
+ let identity_error;
924
+ try {
925
+ seller_pubkey = getPubKey();
926
+ } catch (e) {
927
+ identity_error = e instanceof Error ? e.message : String(e);
928
+ }
929
+ return jsonResult({
930
+ wallet: {
931
+ nwc_configured: nwcConfigured(),
932
+ balance_sats,
933
+ ...wallet_error ? { error: wallet_error } : {},
934
+ mode: nwcConfigured() ? "automatic payments" : "manual mode \u2014 tools return bolt11s to pay externally"
935
+ },
936
+ spending_cap: await getSpendCapSats().catch((e) => ({ error: String(e) })),
937
+ seller_pubkey,
938
+ ...identity_error ? { identity_error } : {},
939
+ platform_settings: settings
940
+ });
941
+ }
942
+ );
943
+ }
944
+
945
+ // src/index.ts
946
+ var server = new McpServer({
947
+ name: "hypawave",
948
+ version: "0.1.0"
949
+ });
950
+ registerDiscoverTools(server);
951
+ registerBuyTools(server);
952
+ registerInvoiceBuyTools(server);
953
+ registerSellTools(server);
954
+ registerStatusTools(server);
955
+ registerWalletTools(server);
956
+ var transport = new StdioServerTransport();
957
+ await server.connect(transport);
958
+ console.error("hypawave-mcp ready (15 tools; NWC " + (process.env.NWC_URL || process.env.HYPAWAVE_NWC_URL ? "configured" : "not configured \u2014 manual mode") + ")");
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@hypawave/mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for Hypawave \u2014 agents buy, sell, and discover over Bitcoin Lightning: search the public offer marketplace, list offers in it or sell agent-to-agent, and settle non-custodially wallet-to-wallet. Verified preimage unlocks files, data, APIs, and compute.",
5
+ "type": "module",
6
+ "bin": {
7
+ "hypawave-mcp": "dist/index.js"
8
+ },
9
+ "main": "dist/index.js",
10
+ "types": "dist/index.d.ts",
11
+ "files": [
12
+ "dist",
13
+ "README.md",
14
+ "CHANGELOG.md",
15
+ "SECURITY.md",
16
+ "LICENSE"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsup src/index.ts --format esm --dts --clean",
20
+ "typecheck": "tsc --noEmit",
21
+ "test": "vitest run",
22
+ "test:watch": "vitest",
23
+ "prepublishOnly": "npm run build"
24
+ },
25
+ "keywords": [
26
+ "hypawave",
27
+ "mcp",
28
+ "mcp-server",
29
+ "model-context-protocol",
30
+ "lightning",
31
+ "lightning-network",
32
+ "bitcoin",
33
+ "bitcoin-payments",
34
+ "nwc",
35
+ "nostr-wallet-connect",
36
+ "non-custodial",
37
+ "agent",
38
+ "agent-payments",
39
+ "ai-agents",
40
+ "micropayments",
41
+ "preimage",
42
+ "bolt11",
43
+ "paywall",
44
+ "claude",
45
+ "claude-code"
46
+ ],
47
+ "author": "hypawave",
48
+ "license": "MIT",
49
+ "repository": {
50
+ "type": "git",
51
+ "url": "git+https://github.com/hypawave/mcp.git"
52
+ },
53
+ "homepage": "https://hypawave.com/docs",
54
+ "dependencies": {
55
+ "@getalby/sdk": "^5.1.0",
56
+ "@modelcontextprotocol/sdk": "^1.12.0",
57
+ "@noble/curves": "^1.6.0",
58
+ "@noble/hashes": "^1.5.0",
59
+ "ws": "^8.18.0",
60
+ "zod": "^3.24.0"
61
+ },
62
+ "devDependencies": {
63
+ "@types/node": "^22.0.0",
64
+ "@types/ws": "^8.5.0",
65
+ "tsup": "^8.0.0",
66
+ "typescript": "^5.0.0",
67
+ "vitest": "^3.0.0"
68
+ },
69
+ "engines": {
70
+ "node": ">=20"
71
+ }
72
+ }