From fdd7027037346cb7521f704cd9bcbb018ffa02b9 Mon Sep 17 00:00:00 2001 From: Michael Bumann Date: Mon, 9 Feb 2026 22:25:28 +0100 Subject: [PATCH 01/18] Add library selection decision tree, runtime indicators, and unit conversion warning to SKILL.md - Add decision table mapping scenarios to libraries and runtime environments - Add explicit warnings about Bitcoin Connect being browser-only - Add cross-library unit conversion guidance (millisats vs sats) - Helps agents pick the right library immediately and avoid subtle amount bugs --- SKILL.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/SKILL.md b/SKILL.md index 8702191..08a9cfe 100644 --- a/SKILL.md +++ b/SKILL.md @@ -17,6 +17,28 @@ Use this skill to understand how to build apps that require bitcoin lightning wa - [Lightning Tools: Request invoices from a lightning address, parse BOLT-11 invoices, verify a preimage for a BOLT-11 invoice, LNURL-Verify, do bitcoin <-> fiat conversions](./references/lightning-tools/lightning-tools.md) - [Bitcoin Connect: Browser-only UI components for connecting wallets and accepting payments in React, Vue, or pure HTML web apps](./references/bitcoin-connect/bitcoin-connect.md) +## Which library to use + +| Scenario | Library | Runtime | +|---|---|---| +| Backend / server-side / console app wallet operations (send, receive, balance, invoices, notifications) | NWC Client (`@getalby/sdk`) | Node.js, Deno, Bun, Browser | +| Browser / frontend wallet connection UI and payment modals | Bitcoin Connect (`@getalby/bitcoin-connect`) | Browser only | +| Utility: parse invoices, lightning address lookups, fiat conversion, LNURL | Lightning Tools (`@getalby/lightning-tools`) | Node.js, Deno, Bun, Browser | +| Backend + Frontend in the same app | NWC Client (backend) + Bitcoin Connect (frontend) | Both | + +- **Do NOT use Bitcoin Connect in Node.js / server-side environments** — it requires a browser DOM. +- **Do NOT use NWC Client in the frontend if the goal is wallet connection UI** — use Bitcoin Connect instead, which provides the UI and manages the NWC connection for you. +- NWC Client and Lightning Tools can be freely combined in any environment. + +## ⚠️ Unit Warning + +NWC Client operates in **millisats** (1 sat = 1,000 millisats). +Lightning Tools and Bitcoin Connect/WebLN operate in **sats**. + +When combining libraries, always convert: +- NWC millisats → sats: `Math.floor(millisats / 1000)` +- sats → NWC millisats: `sats * 1000` + ## Prefer Typescript When the user says to use "JS" or "Javascript" or "NodeJS" or something similar, use typescript unless the user explicitly says to not use typescript or the project does not support it. From b353e395191d5b6df5ef294726256abee8c1edaa Mon Sep 17 00:00:00 2001 From: Michael Bumann Date: Mon, 9 Feb 2026 22:26:18 +0100 Subject: [PATCH 02/18] Add error handling guide for NWC Client - New error-handling.md with comprehensive error type reference table - Document all Nip47 error classes and when they occur - List common NIP-47 wallet error codes (INSUFFICIENT_BALANCE, QUOTA_EXCEEDED, etc.) - Include try/catch examples for payments and invoice creation - Add retry pattern for transient network/timeout errors - Link from nwc-client.md referenced files --- references/nwc-client/error-handling.md | 118 ++++++++++++++++++++++++ references/nwc-client/nwc-client.md | 1 + 2 files changed, 119 insertions(+) create mode 100644 references/nwc-client/error-handling.md diff --git a/references/nwc-client/error-handling.md b/references/nwc-client/error-handling.md new file mode 100644 index 0000000..17453aa --- /dev/null +++ b/references/nwc-client/error-handling.md @@ -0,0 +1,118 @@ +# Error Handling + +IMPORTANT: read the [typings](./nwc.d.ts) to better understand how this works. + +## Error Types + +All errors extend `Nip47Error`, which has a `code` and `message` property. + +| Error Class | When it occurs | +|---|---| +| `Nip47WalletError` | The wallet received the request but rejected it (e.g. insufficient balance, quota exceeded). Check the `.code` property for the specific reason. | +| `Nip47TimeoutError` | Base class for timeout errors (see below). | +| `Nip47PublishTimeoutError` | The request could not be published to the relay in time. | +| `Nip47ReplyTimeoutError` | The request was published but the wallet did not reply in time. | +| `Nip47NetworkError` | A network-level failure (e.g. relay unreachable, WebSocket dropped). | +| `Nip47PublishError` | The relay rejected the published event. | +| `Nip47ResponseDecodingError` | The wallet's response could not be decrypted or decoded. | +| `Nip47ResponseValidationError` | The wallet's response was decoded but failed validation. | +| `Nip47UnexpectedResponseError` | An unexpected response type was received. | +| `Nip47UnsupportedEncryptionError` | The wallet does not support a compatible encryption type. | + +## Common Wallet Error Codes (`Nip47WalletError`) + +These are returned by the wallet when it rejects a request: + +| Code | Description | +|---|---| +| `INSUFFICIENT_BALANCE` | The wallet does not have enough funds for this payment. | +| `QUOTA_EXCEEDED` | The app connection's budget has been exceeded. | +| `NOT_FOUND` | The requested invoice or transaction was not found. | +| `RATE_LIMITED` | Too many requests — the wallet is rate limiting. | +| `NOT_IMPLEMENTED` | The wallet does not support this method. | +| `INTERNAL` | An internal wallet error occurred. | +| `OTHER` | An unspecified error. | +| `RESTRICTED` | The app connection does not have permission for this method. | +| `UNAUTHORIZED` | The app connection is not authorized. | +| `PAYMENT_FAILED` | The payment could not be completed (e.g. no route found). | + +## Example: Handling Payment Errors + +```ts +import { NWCClient, Nip47WalletError, Nip47TimeoutError, Nip47NetworkError } from "@getalby/sdk/nwc"; + +try { + const response = await client.payInvoice({ invoice }); + console.log("Payment successful! Preimage:", response.preimage); +} catch (error) { + if (error instanceof Nip47WalletError) { + // The wallet received the request but rejected it + switch (error.code) { + case "INSUFFICIENT_BALANCE": + console.error("Not enough funds to complete this payment."); + break; + case "QUOTA_EXCEEDED": + console.error("Budget limit reached. Try again after the budget renews."); + break; + case "PAYMENT_FAILED": + console.error("Payment failed (e.g. no route to destination)."); + break; + case "RATE_LIMITED": + console.error("Too many requests. Try again later."); + break; + default: + console.error(`Wallet error [${error.code}]: ${error.message}`); + } + } else if (error instanceof Nip47TimeoutError) { + // Request timed out — the relay or wallet may be slow or unreachable + console.error("Request timed out. The wallet or relay may be temporarily unavailable."); + } else if (error instanceof Nip47NetworkError) { + // Network-level failure + console.error("Network error. Check relay connectivity."); + } else { + throw error; // Unexpected error, re-throw + } +} +``` + +## Example: Handling Invoice Creation Errors + +```ts +try { + const transaction = await client.makeInvoice({ + amount: 1000000, // 1000 sats in millisats + description: "Order #123", + }); + console.log("Invoice created:", transaction.invoice); +} catch (error) { + if (error instanceof Nip47WalletError) { + console.error(`Failed to create invoice [${error.code}]: ${error.message}`); + } else { + throw error; + } +} +``` + +## Retry Pattern for Transient Errors + +Network and timeout errors are often transient. Here is a simple retry wrapper: + +```ts +async function withRetry(fn: () => Promise, retries = 3, delayMs = 1000): Promise { + for (let i = 0; i < retries; i++) { + try { + return await fn(); + } catch (error) { + const isTransient = error instanceof Nip47TimeoutError || error instanceof Nip47NetworkError; + if (!isTransient || i === retries - 1) { + throw error; + } + await new Promise((r) => setTimeout(r, delayMs * (i + 1))); + } + } + throw new Error("Unreachable"); +} + +// Usage: +const response = await withRetry(() => client.payInvoice({ invoice })); +``` diff --git a/references/nwc-client/nwc-client.md b/references/nwc-client/nwc-client.md index ba49bbb..743387e 100644 --- a/references/nwc-client/nwc-client.md +++ b/references/nwc-client/nwc-client.md @@ -41,3 +41,4 @@ Make sure to read the [NWC Client typings](./nwc.d.ts) when using any of the bel - [subscribe to notifications of sent or received payments](./notifications.md) - [How to pay a BOLT-11 lightning invoice](pay-invoice.md) - [How to create, settle and cancel HOLD invoices for conditional payments](hold-invoices.md) +- [Error handling: error types, wallet error codes, and retry patterns](./error-handling.md) From a5ca2b503744b2ea7d1970c862839be77835777b Mon Sep 17 00:00:00 2001 From: Michael Bumann Date: Mon, 9 Feb 2026 22:27:10 +0100 Subject: [PATCH 03/18] Add common operations guide for NWC Client - New common-operations.md covering previously undocumented methods: getInfo, getBalance, makeInvoice, lookupInvoice, listTransactions, getBudget, signMessage, multiPayInvoice - All examples show proper millisat-to-sat conversion for display - Includes filtering transactions by type and time range - Shows how to access the lightning address from the client - Link from nwc-client.md referenced files --- references/nwc-client/common-operations.md | 139 +++++++++++++++++++++ references/nwc-client/nwc-client.md | 1 + 2 files changed, 140 insertions(+) create mode 100644 references/nwc-client/common-operations.md diff --git a/references/nwc-client/common-operations.md b/references/nwc-client/common-operations.md new file mode 100644 index 0000000..d510447 --- /dev/null +++ b/references/nwc-client/common-operations.md @@ -0,0 +1,139 @@ +# Common Operations + +IMPORTANT: read the [typings](./nwc.d.ts) to better understand how this works. + +## Get Wallet Info + +```ts +const info = await client.getInfo(); +console.log("Alias:", info.alias); +console.log("Network:", info.network); +console.log("Supported methods:", info.methods); +console.log("Lightning address:", info.lud16); // may be undefined +``` + +## Get Balance + +```ts +const { balance } = await client.getBalance(); +console.log("Balance:", Math.floor(balance / 1000), "sats"); // balance is in millisats +``` + +## Create an Invoice (Receive a Payment) + +```ts +const transaction = await client.makeInvoice({ + amount: 1000000, // 1000 sats in millisats + description: "Payment for order #123", +}); +console.log("Invoice:", transaction.invoice); +console.log("Payment hash:", transaction.payment_hash); +``` + +To wait for the invoice to be paid, use [notifications](./notifications.md). + +## Look Up an Invoice + +```ts +// Look up by payment hash +const transaction = await client.lookupInvoice({ + payment_hash: paymentHash, +}); +console.log("State:", transaction.state); // "settled", "pending", "failed", or "accepted" +console.log("Amount:", Math.floor(transaction.amount / 1000), "sats"); + +// Or look up by BOLT-11 invoice string +const transaction2 = await client.lookupInvoice({ + invoice: bolt11Invoice, +}); +``` + +## List Transactions + +```ts +// List recent settled transactions +const { transactions } = await client.listTransactions({ + limit: 10, +}); + +for (const tx of transactions) { + const amountSats = Math.floor(tx.amount / 1000); + console.log(`${tx.type} | ${amountSats} sats | ${tx.description || "(no description)"} | ${tx.state}`); +} +``` + +### Filter by type and time range + +```ts +// Only incoming payments in the last 24 hours +const oneDayAgo = Math.floor((Date.now() - 24 * 60 * 60 * 1000) / 1000); +const { transactions } = await client.listTransactions({ + type: "incoming", + from: oneDayAgo, + limit: 50, +}); +``` + +### Include pending/unpaid transactions + +```ts +const { transactions } = await client.listTransactions({ + unpaid: true, + limit: 20, +}); + +const pending = transactions.filter((tx) => tx.state === "pending"); +``` + +## Get Budget + +Check how much of the app connection's budget has been used: + +```ts +const budget = await client.getBudget(); + +if ("total_budget" in budget) { + const usedSats = Math.floor(budget.used_budget / 1000); + const totalSats = Math.floor(budget.total_budget / 1000); + console.log(`Budget: ${usedSats} / ${totalSats} sats used`); + if (budget.renews_at) { + console.log("Renews at:", new Date(budget.renews_at * 1000).toISOString()); + } +} else { + console.log("No budget restrictions on this connection."); +} +``` + +## Sign a Message + +```ts +const { signature, message } = await client.signMessage({ + message: "Proof of wallet ownership", +}); +console.log("Signature:", signature); +``` + +## Pay Multiple Invoices at Once + +```ts +const result = await client.multiPayInvoice({ + invoices: [ + { invoice: bolt11Invoice1, id: "payment-1" }, + { invoice: bolt11Invoice2, id: "payment-2" }, + ], +}); + +for (const paid of result.invoices) { + console.log(`Paid ${paid.dTag}: preimage ${paid.preimage}`); +} +``` + +## Access the Lightning Address + +The lightning address is available directly on the client if the NWC connection secret includes a `lud16` parameter: + +```ts +if (client.lud16) { + console.log("Lightning address:", client.lud16); +} +``` diff --git a/references/nwc-client/nwc-client.md b/references/nwc-client/nwc-client.md index 743387e..a17e795 100644 --- a/references/nwc-client/nwc-client.md +++ b/references/nwc-client/nwc-client.md @@ -38,6 +38,7 @@ const client = new NWCClient({ Make sure to read the [NWC Client typings](./nwc.d.ts) when using any of the below referenced files. +- [Common operations: getBalance, makeInvoice, getInfo, listTransactions, lookupInvoice, getBudget, signMessage, multiPayInvoice](./common-operations.md) - [subscribe to notifications of sent or received payments](./notifications.md) - [How to pay a BOLT-11 lightning invoice](pay-invoice.md) - [How to create, settle and cancel HOLD invoices for conditional payments](hold-invoices.md) From c1961f8d3de4dfeb27a2dafdc0547b792bb2cbd7 Mon Sep 17 00:00:00 2001 From: Michael Bumann Date: Mon, 9 Feb 2026 22:28:17 +0100 Subject: [PATCH 04/18] Scope out NWAClient and NWCWalletService as advanced use cases - Add note to nwc-client.md clarifying these are advanced classes - Prevents agents from hallucinating usage patterns for undocumented APIs - Directs agents to NWCClient for typical application development --- references/nwc-client/nwc-client.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/references/nwc-client/nwc-client.md b/references/nwc-client/nwc-client.md index a17e795..f214f16 100644 --- a/references/nwc-client/nwc-client.md +++ b/references/nwc-client/nwc-client.md @@ -43,3 +43,12 @@ Make sure to read the [NWC Client typings](./nwc.d.ts) when using any of the bel - [How to pay a BOLT-11 lightning invoice](pay-invoice.md) - [How to create, settle and cancel HOLD invoices for conditional payments](hold-invoices.md) - [Error handling: error types, wallet error codes, and retry patterns](./error-handling.md) + +## Advanced: NWAClient and NWCWalletService + +The typings also export `NWAClient` (Nostr Wallet Auth — for wallet-initiated connections) and `NWCWalletService` (for building a wallet service that *acts as* a NWC-compatible wallet provider). These are **advanced use cases** and should not be used unless the user explicitly asks to: + +- Build a wallet service / wallet provider (use `NWCWalletService`) +- Implement Nostr Wallet Auth connection flows (use `NWAClient`) + +For typical application development (sending/receiving payments, checking balances, etc.), use `NWCClient` as documented above. From c7c28a26d115861ef535ca1c55cf4ad73b348a11 Mon Sep 17 00:00:00 2001 From: Michael Bumann Date: Mon, 9 Feb 2026 22:28:58 +0100 Subject: [PATCH 05/18] Add resource cleanup patterns for NWCClient - Document client.close() for proper shutdown - Show unsubscribe + close pattern for notification listeners - Show SIGINT handler for graceful process exit - Prevents leaked WebSocket connections in server apps --- references/nwc-client/nwc-client.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/references/nwc-client/nwc-client.md b/references/nwc-client/nwc-client.md index f214f16..def6759 100644 --- a/references/nwc-client/nwc-client.md +++ b/references/nwc-client/nwc-client.md @@ -44,6 +44,27 @@ Make sure to read the [NWC Client typings](./nwc.d.ts) when using any of the bel - [How to create, settle and cancel HOLD invoices for conditional payments](hold-invoices.md) - [Error handling: error types, wallet error codes, and retry patterns](./error-handling.md) +## Cleanup + +Always close the client when your application exits to avoid leaked WebSocket connections: + +```ts +process.on("SIGINT", () => { + client.close(); + process.exit(); +}); +``` + +If you are using `subscribeNotifications`, unsubscribe before closing: + +```ts +const unsub = await client.subscribeNotifications(onNotification); + +// later, when shutting down: +unsub(); +client.close(); +``` + ## Advanced: NWAClient and NWCWalletService The typings also export `NWAClient` (Nostr Wallet Auth — for wallet-initiated connections) and `NWCWalletService` (for building a wallet service that *acts as* a NWC-compatible wallet provider). These are **advanced use cases** and should not be used unless the user explicitly asks to: From 74980b859be743e161eaf802b375e872cbfcc89a Mon Sep 17 00:00:00 2001 From: Michael Bumann Date: Mon, 9 Feb 2026 22:29:47 +0100 Subject: [PATCH 06/18] Add getFormattedFiatValue example and fix getFiatCurrencies bug in fiat.md - Add getFormattedFiatValue section for locale-formatted display strings - Fix incorrect 'fiat.getFiatCurrencies()' call to 'getFiatCurrencies()' - getFormattedFiatValue is the most user-friendly fiat function and was missing --- references/lightning-tools/fiat.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/references/lightning-tools/fiat.md b/references/lightning-tools/fiat.md index 5ee6481..ada831f 100644 --- a/references/lightning-tools/fiat.md +++ b/references/lightning-tools/fiat.md @@ -8,7 +8,7 @@ Useful to give the user ability to pick a currency, or verify if a fiat currency ```ts import { getFiatCurrencies } from "@getalby/lightning-tools/fiat"; -const fiatCurrencies = await fiat.getFiatCurrencies(); +const fiatCurrencies = await getFiatCurrencies(); ``` ## Fiat amount to Sats @@ -30,3 +30,17 @@ const fiatValue = await getFiatValue({ currency, }); ``` + +## Sats to Formatted Fiat String + +Returns a locale-formatted string like `"$1.23"` or `"€1,23"` — preferred for displaying to users: + +```ts +import { getFormattedFiatValue } from "@getalby/lightning-tools/fiat"; +const formatted = await getFormattedFiatValue({ + satoshi, + currency, // e.g. "USD" + locale, // e.g. "en-US" +}); +console.log(formatted); // e.g. "$1.23" +``` From 0d07ee4fcf25356567aad5688241ede7ae069957 Mon Sep 17 00:00:00 2001 From: Michael Bumann Date: Mon, 9 Feb 2026 22:30:27 +0100 Subject: [PATCH 07/18] Clarify import paths for Lightning Tools - Document the three subpath imports: /lnurl, /fiat, /bolt11 - Explicitly warn against importing from the package root - Prevents import resolution errors agents commonly produce --- references/lightning-tools/lightning-tools.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/references/lightning-tools/lightning-tools.md b/references/lightning-tools/lightning-tools.md index f17d5e1..f91ce95 100644 --- a/references/lightning-tools/lightning-tools.md +++ b/references/lightning-tools/lightning-tools.md @@ -4,6 +4,16 @@ Install the NPM package `@getalby/lightning-tools`. The latest version is 6.1.0. +## Imports + +Use subpath imports to import only what you need: + +- `@getalby/lightning-tools/lnurl` — `LightningAddress` and LNURL utilities +- `@getalby/lightning-tools/fiat` — Fiat currency conversion functions +- `@getalby/lightning-tools/bolt11` — `Invoice` class and `decodeInvoice` + +Do NOT import from the package root (e.g. `import { LightningAddress } from "@getalby/lightning-tools"`). Always use the subpath imports shown in the examples. + ## Units All referenced files in this folder operate in satoshis (sats). From 4652b49ebd64aa65d007d131b8bd062c85eed5b9 Mon Sep 17 00:00:00 2001 From: Michael Bumann Date: Mon, 9 Feb 2026 22:31:45 +0100 Subject: [PATCH 08/18] Add CORS proxy configuration guidance to lnurl.md - Explain default proxy behavior in browsers (CORS avoidance) - Show how to disable proxy for Node.js / server-side usage - Show how to provide a custom proxy URL - Prevents CORS-related failures in browser apps --- references/lightning-tools/lnurl.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/references/lightning-tools/lnurl.md b/references/lightning-tools/lnurl.md index eea4b89..2fec377 100644 --- a/references/lightning-tools/lnurl.md +++ b/references/lightning-tools/lnurl.md @@ -18,3 +18,23 @@ NOTE: not all lightning address providers support LNURL-Verify. ```ts const isPaid = await invoice.isPaid(); ``` + +## Proxy Configuration (Browser vs Node.js) + +In the browser, lightning address requests are subject to CORS restrictions. By default, `LightningAddress` routes requests through a proxy (`https://api.getalby.com/lnurl`) to avoid CORS errors. This works out of the box for browser apps. + +In Node.js / server-side environments, you can disable the proxy for direct requests: + +```ts +const ln = new LightningAddress("hello@getalby.com", { + proxy: false, +}); +``` + +You can also provide a custom proxy URL: + +```ts +const ln = new LightningAddress("hello@getalby.com", { + proxy: "https://my-proxy.example.com/lnurl", +}); +``` From db69040ca5876c4938d7dd6ade2c4b464263c450 Mon Sep 17 00:00:00 2001 From: Michael Bumann Date: Mon, 9 Feb 2026 22:34:03 +0100 Subject: [PATCH 09/18] Add cross-library recipe combining NWC Client + Lightning Tools MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New cross-library-recipe.md with full end-to-end example - Pattern: receive USD payment → convert fiat → create invoice → wait for payment → forward 90% to a lightning address - Exercises: fiat conversion, makeInvoice, subscribeNotifications, LightningAddress.requestInvoice, payInvoice, error handling, cleanup - Highlights unit conversion at every boundary between libraries - Link from SKILL.md --- SKILL.md | 4 ++ references/cross-library-recipe.md | 104 +++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 references/cross-library-recipe.md diff --git a/SKILL.md b/SKILL.md index 08a9cfe..1c076e8 100644 --- a/SKILL.md +++ b/SKILL.md @@ -66,6 +66,10 @@ Testing wallets should be used for [automated testing](./references/automated-te It is recommended to write tests so that the agent can test its own work and fix bugs itself without requiring human input. +## Cross-Library Recipe + +When combining NWC Client and Lightning Tools (e.g. fiat conversion + invoicing + forwarding payments), see the [cross-library recipe](./references/cross-library-recipe.md) for a full end-to-end example with proper unit conversion. + ## Production Wallet If they do not have a wallet yet [here are some options](./references/production-wallets.md) \ No newline at end of file diff --git a/references/cross-library-recipe.md b/references/cross-library-recipe.md new file mode 100644 index 0000000..92c93f3 --- /dev/null +++ b/references/cross-library-recipe.md @@ -0,0 +1,104 @@ +# Cross-Library Recipe: NWC Client + Lightning Tools + +This example combines NWC Client and Lightning Tools to build a common real-world pattern: + +> Receive a payment in USD, then forward 90% to a lightning address. + +## Libraries Used + +- **NWC Client** (`@getalby/sdk`) — create invoices, subscribe to payment notifications, send payments +- **Lightning Tools** (`@getalby/lightning-tools`) — convert fiat to sats, request invoices from a lightning address + +## ⚠️ Unit Conversion + +NWC Client uses **millisats**. Lightning Tools uses **sats**. This recipe converts between them: +- NWC → Lightning Tools: `Math.floor(millisats / 1000)` +- Lightning Tools → NWC: `sats * 1000` + +## Full Example + +IMPORTANT: read the [NWC Client typings](./nwc-client/nwc.d.ts) and [Lightning Tools typings](./lightning-tools/index.d.ts) to better understand how this works. + +```ts +import { NWCClient, Nip47WalletError } from "@getalby/sdk/nwc"; +import { getSatoshiValue } from "@getalby/lightning-tools/fiat"; +import { LightningAddress } from "@getalby/lightning-tools/lnurl"; + +const client = new NWCClient({ + nostrWalletConnectUrl: process.env.NWC_URL, +}); + +// Step 1: Convert $5 USD to sats using Lightning Tools +const amountSats = await getSatoshiValue({ amount: 5, currency: "USD" }); +console.log(`$5 USD = ${amountSats} sats`); + +// Step 2: Create an invoice using NWC Client (amount in millisats) +const amountMillisats = amountSats * 1000; +const transaction = await client.makeInvoice({ + amount: amountMillisats, + description: "Payment for service - $5 USD", +}); +console.log("Invoice created:", transaction.invoice); +console.log("Share this invoice with the payer."); + +// Step 3: Wait for the payment to arrive +const unsub = await client.subscribeNotifications(async (notification) => { + if (notification.notification_type !== "payment_received") { + return; + } + if (notification.notification.payment_hash !== transaction.payment_hash) { + return; + } + + const receivedMillisats = notification.notification.amount; + const receivedSats = Math.floor(receivedMillisats / 1000); + console.log(`Payment received: ${receivedSats} sats`); + + // Step 4: Calculate 90% to forward + const forwardSats = Math.floor(receivedSats * 0.9); + console.log(`Forwarding 90%: ${forwardSats} sats to recipient`); + + // Step 5: Request an invoice from a lightning address using Lightning Tools + const recipientAddress = new LightningAddress("hello@getalby.com", { + proxy: false, // server-side, no CORS proxy needed + }); + await recipientAddress.fetch(); + const recipientInvoice = await recipientAddress.requestInvoice({ + satoshi: forwardSats, + }); + + // Step 6: Pay the invoice using NWC Client + try { + const payResponse = await client.payInvoice({ + invoice: recipientInvoice.paymentRequest, + }); + console.log("Forwarded payment! Preimage:", payResponse.preimage); + } catch (error) { + if (error instanceof Nip47WalletError) { + console.error(`Payment failed [${error.code}]: ${error.message}`); + } else { + throw error; + } + } + + unsub(); + client.close(); +}); + +// Graceful shutdown +process.on("SIGINT", () => { + unsub(); + client.close(); + process.exit(); +}); +``` + +## Key Takeaways + +1. **Fiat conversion** happens via Lightning Tools (`getSatoshiValue`) which returns sats. +2. **Invoice creation** happens via NWC Client (`makeInvoice`) which expects millisats — so multiply by 1000. +3. **Notification amounts** from NWC Client are in millisats — divide by 1000 before passing to Lightning Tools. +4. **Lightning address invoice requests** happen via Lightning Tools (`requestInvoice`) which expects sats. +5. **Paying the invoice** happens via NWC Client (`payInvoice`) which takes a BOLT-11 string directly. +6. **Error handling** uses `Nip47WalletError` for wallet-level failures. +7. **Cleanup** always unsubscribes and closes the client. \ No newline at end of file From 8c5f621b1c5902d045f92c718e188c633d6eb411 Mon Sep 17 00:00:00 2001 From: Michael Bumann Date: Mon, 9 Feb 2026 22:43:05 +0100 Subject: [PATCH 10/18] Add browser-specific NWC Client patterns (init, cleanup, Bitcoin Connect integration) - Add browser initialization: user input, CDN/ESM, and Bitcoin Connect bridge - Show how to extract NWCClient from a Bitcoin Connect provider for advanced ops - Add browser cleanup: beforeunload, React useEffect pattern - Add long-lived Node.js process note for notification subscriptions - Separates Node.js vs browser patterns throughout --- references/nwc-client/nwc-client.md | 94 ++++++++++++++++++++++++++++- 1 file changed, 91 insertions(+), 3 deletions(-) diff --git a/references/nwc-client/nwc-client.md b/references/nwc-client/nwc-client.md index def6759..ebb9a08 100644 --- a/references/nwc-client/nwc-client.md +++ b/references/nwc-client/nwc-client.md @@ -25,15 +25,65 @@ When displaying to humans, please use satoshis (rounded to a whole value). ## Initialization +### Node.js / Backend + +```ts +import { NWCClient } from "@getalby/sdk/nwc"; + +const client = new NWCClient({ + nostrWalletConnectUrl: process.env.NWC_URL, +}); +``` + +### Browser (with bundler) + +In a browser, the NWC connection secret typically comes from user input, a URL parameter, or `localStorage` — never hardcoded: + ```ts import { NWCClient } from "@getalby/sdk/nwc"; -// or from e.g. https://esm.sh/@getalby/sdk@7.0.0 +// From user input (e.g. a text field or paste event) const client = new NWCClient({ - nostrWalletConnectUrl, + nostrWalletConnectUrl: userProvidedNwcUrl, }); ``` +### Browser (CDN, no build step) + +```html + +``` + +### Browser: Using NWC Client with Bitcoin Connect + +If the user connects their wallet via [Bitcoin Connect](../bitcoin-connect/bitcoin-connect.md), you can access the underlying NWC Client for advanced operations (e.g. notifications, hold invoices) that aren't available through WebLN alone: + +```ts +import { WebLNProviders, requestProvider } from "@getalby/bitcoin-connect"; +import { NWCClient } from "@getalby/sdk/nwc"; + +const provider = await requestProvider(); + +if (provider instanceof WebLNProviders.NostrWebLNProvider) { + // Get the NWC connection URL from the connected provider + const nwcUrl = provider.client.nostrWalletConnectUrl; + + // Create a dedicated NWCClient for advanced operations + const client = new NWCClient({ nostrWalletConnectUrl: nwcUrl }); + + // Now you can use notifications, hold invoices, etc. + const unsub = await client.subscribeNotifications((notification) => { + console.log("Payment notification:", notification); + }); +} +``` + ## Referenced files Make sure to read the [NWC Client typings](./nwc.d.ts) when using any of the below referenced files. @@ -46,6 +96,8 @@ Make sure to read the [NWC Client typings](./nwc.d.ts) when using any of the bel ## Cleanup +### Node.js + Always close the client when your application exits to avoid leaked WebSocket connections: ```ts @@ -65,6 +117,42 @@ unsub(); client.close(); ``` +### Browser + +In the browser, clean up on page unload or when the component unmounts: + +```ts +// On page unload +window.addEventListener("beforeunload", () => { + unsub?.(); + client.close(); +}); +``` + +In React, clean up in a `useEffect` return: + +```tsx +useEffect(() => { + const client = new NWCClient({ nostrWalletConnectUrl: nwcUrl }); + let unsub: (() => void) | undefined; + + client.subscribeNotifications((notification) => { + console.log("Notification:", notification); + }).then((unsubFn) => { + unsub = unsubFn; + }); + + return () => { + unsub?.(); + client.close(); + }; +}, [nwcUrl]); +``` + +### Long-Lived Node.js Processes + +When subscribing to notifications in a Node.js script, the process must stay alive for the subscription to work. The WebSocket connection kept open by `subscribeNotifications` will keep the Node.js event loop running automatically — no extra keep-alive code is needed. The process will stay alive as long as the subscription is active. Call `unsub()` and `client.close()` when you want the process to exit. + ## Advanced: NWAClient and NWCWalletService The typings also export `NWAClient` (Nostr Wallet Auth — for wallet-initiated connections) and `NWCWalletService` (for building a wallet service that *acts as* a NWC-compatible wallet provider). These are **advanced use cases** and should not be used unless the user explicitly asks to: @@ -72,4 +160,4 @@ The typings also export `NWAClient` (Nostr Wallet Auth — for wallet-initiated - Build a wallet service / wallet provider (use `NWCWalletService`) - Implement Nostr Wallet Auth connection flows (use `NWAClient`) -For typical application development (sending/receiving payments, checking balances, etc.), use `NWCClient` as documented above. +For typical application development (sending/receiving payments, checking balances, etc.), use `NWCClient` as documented above. \ No newline at end of file From 14cefab8d9ee7d4939f3c05e58345f772213380b Mon Sep 17 00:00:00 2001 From: Michael Bumann Date: Mon, 9 Feb 2026 22:43:42 +0100 Subject: [PATCH 11/18] Add SSR/SSG warning and init() placement guidance for Bitcoin Connect - Add SSR crash warning with framework-specific solutions: Next.js App Router, Next.js Pages Router, Nuxt 3, SvelteKit - Add general rule: never top-level import in server-rendered files - Add init() placement guidance for React, Vue, Next.js, plain HTML - Warn against calling init() multiple times or in render loops --- references/bitcoin-connect/bitcoin-connect.md | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/references/bitcoin-connect/bitcoin-connect.md b/references/bitcoin-connect/bitcoin-connect.md index 5da3580..c98adfd 100644 --- a/references/bitcoin-connect/bitcoin-connect.md +++ b/references/bitcoin-connect/bitcoin-connect.md @@ -35,6 +35,76 @@ or ``` +## ⚠️ SSR / SSG Warning (Next.js, Nuxt, SvelteKit, Remix, Astro) + +Bitcoin Connect requires a browser DOM and **will crash if imported on the server**. In frameworks that do server-side rendering or static site generation, you MUST use dynamic imports or client-only wrappers. + +### Next.js (App Router) + +Mark the component as client-only and use dynamic import: + +```tsx +"use client"; + +import dynamic from "next/dynamic"; +import { useEffect } from "react"; + +// Dynamic import — prevents server-side import of bitcoin-connect +const BitcoinConnectButton = dynamic( + () => import("@getalby/bitcoin-connect-react").then((mod) => { + // init() must be called once before using components + mod.init({ appName: "My App" }); + return { default: mod.Button }; + }), + { ssr: false } +); + +export default function WalletButton() { + return ; +} +``` + +### Next.js (Pages Router) + +```tsx +import dynamic from "next/dynamic"; + +const WalletButton = dynamic( + () => import("../components/WalletButton"), + { ssr: false } +); +``` + +### Nuxt 3 + +```vue + +``` + +### SvelteKit + +```svelte + +``` + +### General Rule + +If using any SSR framework, **never import `@getalby/bitcoin-connect` or `@getalby/bitcoin-connect-react` at the top level of a server-rendered file**. Always gate the import behind a browser/client check or dynamic import. + ## Key concepts - Web components for connecting Lightning wallets and enabling WebLN @@ -49,6 +119,15 @@ Unlike NWC, WebLN operates on sats, not millisats. (1000 millisats = 1 satoshi) ## Initialization +Call `init()` **once** when your app starts. Do NOT call it multiple times or conditionally inside render loops. + +**Where to call `init()`:** + +- **React (Vite / CRA):** in `main.tsx` or `App.tsx`, outside any component, at the top level +- **React (Next.js):** inside the dynamically imported client component (see SSR warning above) +- **Vue:** in `main.ts` before `createApp()` +- **Plain HTML:** in a `