@hypawave/mcp 0.2.0 → 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 CHANGED
@@ -3,6 +3,14 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
+ ## [0.3.0] - 2026-07-22
7
+
8
+ ### Added
9
+
10
+ - **Operator funding options** — `setup_wallet {action:"funding_options", amount_sats?}` returns operator-facing funding instructions to present verbatim: an exact-amount Lightning invoice (payable from Cash App, Coinbase, or any Lightning wallet) or the wallet's Lightning address, plus an on-chain deposit address for exchanges without Lightning (e.g. Robinhood; 300-sat Coinos dust minimum surfaced). Hosted-wallet creation now returns the same funding block, and insufficient-balance NWC payment errors point the agent at the action.
11
+ - **Per-wallet NWC guidance** — `setup_wallet {action:"connect_own"}` without `nwc_url` now returns wallet-specific steps (Alby Hub, Coinos, Primal, LNbits, self-hosted node) for finding the connection string, instead of erroring; operator option 2 tells the agent to ask which wallet the operator uses.
12
+ - Coinos registration JWT (no expiry) is persisted in `~/.hypawave/wallet.json` (`token`) to mint funding invoices and on-chain addresses later; new `createFundingInvoice()` / `getOnchainAddress()` client helpers (`POST /invoice`, types `lightning` / `bitcoin`).
13
+
6
14
  ## [0.2.0] - 2026-07-06
7
15
 
8
16
  ### Added
package/README.md CHANGED
@@ -67,7 +67,7 @@ All env vars are optional — with no `NWC_URL` the server runs in manual mode (
67
67
  | `list_sales` | List your settled sales (payments/invoices) — reconcile missed webhooks |
68
68
  | **Utility** | |
69
69
  | `wallet_status` | Wallet balance, seller pubkey, spending cap, live platform fees/limits |
70
- | `setup_wallet` | One-time wallet setup: create a hosted Coinos wallet (with operator consent) or connect your own NWC string |
70
+ | `setup_wallet` | One-time wallet setup: create a hosted Coinos wallet (with operator consent) or connect your own NWC wallet (with per-wallet steps to find the string); also serves operator funding options (Lightning + on-chain) |
71
71
 
72
72
  ## Buy in three calls
73
73
 
@@ -101,7 +101,9 @@ Selling needs **no special wallet** — payouts go straight to your Lightning Ad
101
101
 
102
102
  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.
103
103
 
104
- **No wallet yet? `setup_wallet`.** With explicit operator consent it registers a fresh hosted wallet at coinos.io (custodial — keep only small amounts) and saves the credentials to `~/.hypawave/wallet.json` (0600, local only; Hypawave's servers never receive them — **back this file up: it holds the only copy**). Fund it by sending sats to the returned Lightning address. Or pass your own NWC string via `{action:"connect_own"}`. `NWC_URL`, when set, always wins over the wallet file.
104
+ **No wallet yet? `setup_wallet`.** With explicit operator consent it registers a fresh hosted wallet at coinos.io (custodial — keep only small amounts) and saves the credentials to `~/.hypawave/wallet.json` (0600, local only; Hypawave's servers never receive them — **back this file up: it holds the only copy**). Or `{action:"connect_own"}` connects a wallet you already use called without an NWC string it returns per-wallet steps (Alby Hub, Coinos, Primal, LNbits, self-hosted node) for finding it. `NWC_URL`, when set, always wins over the wallet file.
105
+
106
+ **Funding the wallet (the human's only job).** `setup_wallet {action:"funding_options", amount_sats?}` returns operator-facing instructions the agent presents verbatim, with two paths: **instant** — an exact-amount Lightning invoice (payable from Cash App, Coinbase, or any Lightning wallet) or the wallet's Lightning address; **on-chain** — a deposit address for exchanges without Lightning support (e.g. Robinhood; ~10–60 min, mining fees, 300-sat minimum — best for larger top-ups). Low-balance payment failures point the agent at this action automatically. No bitcoin at all? Any of those apps sells it.
105
107
 
106
108
  **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).
107
109
 
package/dist/index.js CHANGED
@@ -364,7 +364,18 @@ async function getClient() {
364
364
  }
365
365
  async function payBolt11(bolt11) {
366
366
  const client = await getClient();
367
- const result = await client.payInvoice({ invoice: bolt11 });
367
+ let result;
368
+ try {
369
+ result = await client.payInvoice({ invoice: bolt11 });
370
+ } catch (e) {
371
+ const msg = e instanceof Error ? e.message : String(e);
372
+ if (/insufficient|balance|not enough|exceeds.*budget|quota/i.test(msg)) {
373
+ throw new Error(
374
+ `${msg} \u2014 the agent wallet balance is too low. Call setup_wallet {action:"funding_options", amount_sats:<needed>} and present the funding options to your operator.`
375
+ );
376
+ }
377
+ throw e;
378
+ }
368
379
  if (!result?.preimage || !/^[0-9a-fA-F]{64}$/.test(result.preimage)) {
369
380
  throw new Error(
370
381
  "wallet paid (or attempted) but returned no valid preimage \u2014 cannot prove settlement; check the payment in your wallet before retrying"
@@ -876,19 +887,93 @@ async function createCoinosWallet() {
876
887
  provider: "coinos",
877
888
  username,
878
889
  password,
890
+ token,
879
891
  nwc_url: nwc,
880
892
  lightning_address: `${username}@${new URL(COINOS_API).host}`,
881
893
  created_at: (/* @__PURE__ */ new Date()).toISOString()
882
894
  };
883
895
  }
896
+ async function createFundingInvoice(token, amountSats) {
897
+ const inv = await coinos("/invoice", {
898
+ token,
899
+ body: { invoice: { type: "lightning", amount: Math.floor(amountSats) } }
900
+ });
901
+ if (!inv?.hash || !inv.hash.toLowerCase().startsWith("ln")) {
902
+ throw new Error("coinos returned no bolt11 for the funding invoice");
903
+ }
904
+ return inv.hash;
905
+ }
906
+ async function getOnchainAddress(token) {
907
+ const inv = await coinos("/invoice", {
908
+ token,
909
+ body: { invoice: { type: "bitcoin", amount: 0 } }
910
+ });
911
+ if (!inv?.hash || !/^(bc1|[13])[a-zA-Z0-9]{20,}$/.test(inv.hash)) {
912
+ throw new Error("coinos returned no on-chain deposit address");
913
+ }
914
+ return inv.hash;
915
+ }
916
+ async function getFundingOptions(amountSats) {
917
+ const wallet = readWalletFile();
918
+ if (!wallet || wallet.provider !== "coinos" || !wallet.token) {
919
+ return {
920
+ present_to_operator: [
921
+ "Fund the agent's Lightning wallet using its own receive/deposit flow (open the wallet app and create a receive invoice or address).",
922
+ "Instant options once you have an invoice: pay it from Cash App, Coinbase, or any Lightning wallet.",
923
+ wallet?.lightning_address ? `Or send sats to the wallet's Lightning address: ${wallet.lightning_address}` : ""
924
+ ].filter(Boolean).join("\n"),
925
+ ...wallet?.lightning_address ? { lightning_address: wallet.lightning_address } : {}
926
+ };
927
+ }
928
+ const result = { present_to_operator: "" };
929
+ const lines = [
930
+ amountSats ? `The agent wallet needs a top-up (~${amountSats} sats). Two ways to fund it:` : "Two ways to fund the agent wallet:"
931
+ ];
932
+ if (amountSats && amountSats >= 1) {
933
+ try {
934
+ result.lightning_invoice = await createFundingInvoice(wallet.token, amountSats);
935
+ } catch {
936
+ }
937
+ }
938
+ lines.push(
939
+ result.lightning_invoice ? `1. INSTANT \u2014 pay this Lightning invoice from Cash App, Coinbase, or any Lightning wallet (settles in seconds):
940
+ ${result.lightning_invoice}` : `1. INSTANT \u2014 send sats from any Lightning wallet to the agent's Lightning address: ${wallet.lightning_address}
941
+ (If your app needs an exact-amount invoice \u2014 Cash App, Coinbase \u2014 ask the agent for one: setup_wallet {action:'funding_options', amount_sats:N}.)`
942
+ );
943
+ result.lightning_address = wallet.lightning_address;
944
+ try {
945
+ result.onchain_address = await getOnchainAddress(wallet.token);
946
+ lines.push(
947
+ `2. FROM AN EXCHANGE WITHOUT LIGHTNING (e.g. Robinhood) \u2014 send BTC on-chain to:
948
+ ${result.onchain_address}
949
+ Arrives after confirmation (~10\u201360 min). Minimum 300 sats; mining fees apply, so best for larger top-ups.`
950
+ );
951
+ } catch {
952
+ lines.push(
953
+ "2. FROM AN EXCHANGE WITHOUT LIGHTNING (e.g. Robinhood) \u2014 an on-chain deposit address could not be fetched right now; log in at coinos.io to get one, or use the Lightning option."
954
+ );
955
+ }
956
+ lines.push("The agent will detect the funds and continue automatically.");
957
+ result.present_to_operator = lines.join("\n");
958
+ return result;
959
+ }
884
960
 
885
961
  // src/tools/setup-wallet.ts
886
962
  var OPERATOR_OPTIONS = [
887
963
  "To buy things automatically, your agent needs a Lightning wallet it can spend from. Three options:",
888
964
  "1. CREATE ONE NOW (recommended for getting started) \u2014 creates a hosted wallet at coinos.io (think prepaid card: funds are held by that service, so keep only small amounts, e.g. $10\u201320 worth of sats; the operator spending cap applies to every payment). Credentials are generated and stored only on this machine \u2014 Hypawave's servers never receive them.",
889
- "2. CONNECT YOUR OWN WALLET \u2014 if you already use an NWC-capable wallet (Alby Hub, Primal, LNbits, your own node), provide its NWC connection string; no account is created anywhere. Note: self-hosted nodes need channel liquidity even for tiny payments.",
965
+ "2. CONNECT YOUR OWN WALLET \u2014 if you already use an NWC-capable wallet (Alby Hub, Coinos, Primal, LNbits, your own node), tell the agent which one and it will walk you through copying its NWC connection string; no account is created anywhere. Note: self-hosted nodes need channel liquidity even for tiny payments.",
890
966
  "3. SKIP \u2014 purchases will return a Lightning invoice to pay manually from any wallet (you'll also need the preimage from your wallet to confirm \u2014 fine for testing, clunky as a routine)."
891
967
  ].join("\n");
968
+ var NWC_WALLET_GUIDE = [
969
+ "Ask the operator which wallet they use, then give them the matching steps to copy its NWC connection string (starts with nostr+walletconnect://):",
970
+ "- Alby Hub: App Store (or Settings) \u2192 Connections \u2192 Add connection \u2192 set a budget \u2192 copy the connection secret.",
971
+ "- Coinos (existing account): coinos.io \u2192 Settings \u2192 Nostr Wallet Connect (Apps) \u2192 create/copy the connection string.",
972
+ "- Primal: Wallet \u2192 Settings \u2192 Connected apps \u2192 New connection \u2192 copy the string.",
973
+ "- LNbits: enable the NWC Service extension on your wallet \u2192 create a connection \u2192 copy the pairing URL.",
974
+ "- Self-hosted node (LND/CLN): run an NWC bridge (e.g. Alby Hub connected to the node) and create a connection there. Reminder: the node needs outbound channel liquidity.",
975
+ "Then re-call setup_wallet with {action:'connect_own', nwc_url:'<the string>'}."
976
+ ].join("\n");
892
977
  var NWC_URI_RE = /^nostr\+walletconnect:\/\/[0-9a-f]{64}\?/i;
893
978
  async function tryBalance() {
894
979
  try {
@@ -906,15 +991,26 @@ function registerSetupWalletTools(server2) {
906
991
  "setup_wallet",
907
992
  {
908
993
  title: "Set up the agent's Lightning wallet",
909
- description: "One-time wallet setup so purchases can pay automatically. Call with no arguments first: it returns the operator-facing options \u2014 present them to the operator verbatim and let them choose; do not choose for them. Then call {action:'create_hosted', confirm:true} ONLY after the operator explicitly agreed (creates a hosted custodial wallet at coinos.io; credentials are stored locally in ~/.hypawave/wallet.json and never sent to Hypawave), or {action:'connect_own', nwc_url:'nostr+walletconnect://\u2026'} to use an existing NWC wallet. The NWC_URL env var, when set, always takes precedence over anything configured here.",
994
+ description: "One-time wallet setup so purchases can pay automatically. Call with no arguments first: it returns the operator-facing options \u2014 present them to the operator verbatim and let them choose; do not choose for them. Then call {action:'create_hosted', confirm:true} ONLY after the operator explicitly agreed (creates a hosted custodial wallet at coinos.io; credentials are stored locally in ~/.hypawave/wallet.json and never sent to Hypawave), or {action:'connect_own'} to use an existing NWC wallet \u2014 without nwc_url it returns per-wallet steps to help the operator find their connection string. Also: {action:'funding_options', amount_sats?} returns operator-facing funding instructions for the configured wallet (instant Lightning invoice/address + on-chain deposit address for exchanges without Lightning, e.g. Robinhood) \u2014 present them verbatim whenever the wallet needs sats. The NWC_URL env var, when set, always takes precedence over anything configured here.",
910
995
  inputSchema: {
911
- action: z5.enum(["create_hosted", "connect_own"]).optional().describe("Omit to get the options to present to the operator"),
996
+ action: z5.enum(["create_hosted", "connect_own", "funding_options"]).optional().describe("Omit to get the options to present to the operator"),
912
997
  confirm: z5.boolean().optional().describe("Required true for create_hosted \u2014 set only after the operator explicitly agreed"),
913
- nwc_url: z5.string().optional().describe("For connect_own: the wallet's NWC connection string")
998
+ nwc_url: z5.string().optional().describe("For connect_own: the wallet's NWC connection string"),
999
+ amount_sats: z5.number().int().positive().optional().describe("For funding_options: mint an exact-amount Lightning funding invoice for this many sats")
914
1000
  }
915
1001
  },
916
- async ({ action, confirm, nwc_url }) => {
1002
+ async ({ action, confirm, nwc_url, amount_sats }) => {
917
1003
  const envSet = Boolean(process.env.NWC_URL || process.env.HYPAWAVE_NWC_URL);
1004
+ if (action === "funding_options") {
1005
+ if (!getNwcUrl()) {
1006
+ return jsonResult({
1007
+ ok: false,
1008
+ message: "No wallet configured yet \u2014 run setup_wallet first, then request funding options."
1009
+ });
1010
+ }
1011
+ const funding2 = await getFundingOptions(amount_sats);
1012
+ return jsonResult({ ok: true, ...funding2 });
1013
+ }
918
1014
  if (!action) {
919
1015
  const configured = Boolean(getNwcUrl());
920
1016
  return jsonResult({
@@ -930,9 +1026,16 @@ function registerSetupWalletTools(server2) {
930
1026
  });
931
1027
  }
932
1028
  if (action === "connect_own") {
933
- if (!nwc_url || !NWC_URI_RE.test(nwc_url) || !/[?&]secret=[0-9a-f]{64}/i.test(nwc_url)) {
1029
+ if (!nwc_url) {
1030
+ return jsonResult({
1031
+ ok: false,
1032
+ needs_nwc_url: true,
1033
+ present_to_operator: NWC_WALLET_GUIDE
1034
+ });
1035
+ }
1036
+ if (!NWC_URI_RE.test(nwc_url) || !/[?&]secret=[0-9a-f]{64}/i.test(nwc_url)) {
934
1037
  throw new Error(
935
- "connect_own requires a valid NWC connection string: nostr+walletconnect://<64-hex-pubkey>?relay=\u2026&secret=<64-hex>"
1038
+ "connect_own requires a valid NWC connection string: nostr+walletconnect://<64-hex-pubkey>?relay=\u2026&secret=<64-hex>. Call {action:'connect_own'} without nwc_url for per-wallet steps to find it."
936
1039
  );
937
1040
  }
938
1041
  if (walletFileExists()) {
@@ -976,12 +1079,14 @@ function registerSetupWalletTools(server2) {
976
1079
  }
977
1080
  const wallet = await createCoinosWallet();
978
1081
  const path = saveWalletFile(wallet);
1082
+ const funding = await getFundingOptions().catch(() => null);
979
1083
  return jsonResult({
980
1084
  ok: true,
981
1085
  provider: "coinos",
982
1086
  lightning_address: wallet.lightning_address,
983
1087
  credentials_file: path,
984
- next: `Fund it by sending sats to ${wallet.lightning_address} (start small \u2014 5,000\u201350,000 sats). Then wallet_status shows the balance and purchases pay automatically under the spending cap.`,
1088
+ ...funding ? { funding } : {},
1089
+ next: `Fund it now (start small \u2014 5,000\u201350,000 sats): present the funding options above to the operator verbatim \u2014 instant via Lightning (Cash App, Coinbase, or any Lightning wallet), or on-chain from an exchange without Lightning (e.g. Robinhood; slower, mining fees). Then wallet_status shows the balance and purchases pay automatically under the spending cap.`,
985
1090
  important: `Custodial wallet \u2014 coinos.io holds the funds; keep only small amounts. ${path} contains the ONLY copy of the credentials: back it up, and never delete it while the wallet holds funds.`
986
1091
  });
987
1092
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hypawave/mcp",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "mcpName": "io.github.hypawave/mcp",
5
5
  "description": "MCP server for Hypawave — 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.",
6
6
  "type": "module",