Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
fdd7027
Add library selection decision tree, runtime indicators, and unit con…
bumi Feb 9, 2026
b353e39
Add error handling guide for NWC Client
bumi Feb 9, 2026
a5ca2b5
Add common operations guide for NWC Client
bumi Feb 9, 2026
c1961f8
Scope out NWAClient and NWCWalletService as advanced use cases
bumi Feb 9, 2026
c7c28a2
Add resource cleanup patterns for NWCClient
bumi Feb 9, 2026
74980b8
Add getFormattedFiatValue example and fix getFiatCurrencies bug in fi…
bumi Feb 9, 2026
0d07ee4
Clarify import paths for Lightning Tools
bumi Feb 9, 2026
4652b49
Add CORS proxy configuration guidance to lnurl.md
bumi Feb 9, 2026
db69040
Add cross-library recipe combining NWC Client + Lightning Tools
bumi Feb 9, 2026
8c5f621
Add browser-specific NWC Client patterns (init, cleanup, Bitcoin Conn…
bumi Feb 9, 2026
14cefab
Add SSR/SSG warning and init() placement guidance for Bitcoin Connect
bumi Feb 9, 2026
0a66386
Add Node.js project setup guidance (ESM, tsconfig, dependencies)
bumi Feb 9, 2026
72bca31
Add invoice expiration handling, decodeInvoice function, and expanded…
bumi Feb 9, 2026
c37bcd0
Add transaction metadata, amountless invoices, and payer data examples
bumi Feb 9, 2026
91ef744
Add LightningAddress metadata introspection, comments, and payer data
bumi Feb 9, 2026
2e8fb34
Add Nostr Zaps guide for Lightning Tools
bumi Feb 9, 2026
0f9a72b
Add comprehensive L402 guide — both client and server implementation
bumi Feb 9, 2026
03d768c
Improve skill docs: quickstart, security, notifications, budgets, tes…
bumi Feb 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 113 additions & 1 deletion SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,78 @@ 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`

## Node.js Project Setup
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is specific to Node.js maybe it can be referenced as a separate file


All packages in this skill are **ESM-only**. When creating a new Node.js project:

1. Set `"type": "module"` in `package.json`
2. For TypeScript, use the following minimal `tsconfig.json`:

```json
{
"compilerOptions": {
"target": "ES2022",
"module": "nodenext",
"moduleResolution": "nodenext",
"esModuleInterop": true,
"outDir": "dist",
"strict": true
}
}
```

3. Install dependencies based on what you need:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The individual tools already have installation instructions. I don't think we should add them here, and we shouldn't specify which package manager to use


```bash
# NWC Client (wallet operations)
npm install @getalby/sdk

# Lightning Tools (invoices, lightning addresses, fiat conversion)
npm install @getalby/lightning-tools

# Both (common for backend apps)
npm install @getalby/sdk @getalby/lightning-tools

# Bitcoin Connect (browser only — do NOT install for Node.js-only projects)
npm install @getalby/bitcoin-connect
# or for React specifically:
npm install @getalby/bitcoin-connect-react
```

4. If using TypeScript with Bitcoin Connect, also install WebLN types:

```bash
npm install -D @webbtc/webln-types
```

Then create a `webln-types.d.ts` file:

```ts
/// <reference types="@webbtc/webln-types" />
```

## 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.
Expand Down Expand Up @@ -44,6 +116,46 @@ 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)
If they do not have a wallet yet [here are some options](./references/production-wallets.md)

## Quickstart Decision Guide
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we are duplicating a lot of info here and putting it all in the root of the skill is not so great. We already link to individual reference files and describe what each reference file is for


- **Backend wallet ops (send/receive/balance/notifications)** → Use `NWCClient` (`@getalby/sdk`). Combine with Lightning Tools for fiat conversion and invoice parsing. Units: msats.
- **Browser wallet connection + payment UI** → Use Bitcoin Connect (`@getalby/bitcoin-connect` or `-react`). Units: sats. SSR frameworks must gate imports to the client.
- **Full-stack app** → Backend: `NWCClient` for wallet ops. Frontend: Bitcoin Connect for connect/pay UI. Shared utils: Lightning Tools for fiat, LNURL, invoice parsing.
- **Lightning address pay/receive utilities** → Use Lightning Tools `lnurl` APIs (sats). For payment, either WebLN (browser) or `NWCClient.payInvoice` (backend).
- **Fiat pricing** → Lightning Tools `fiat` APIs to convert fiat↔sats; multiply/divide by 1000 when handing amounts to/from `NWCClient`.
- **Zaps (Nostr-tied payments)** → Lightning Tools zap helpers + WebLN provider (browser) or `NWCClient.payInvoice` (backend).
- **L402 client** → Browser: `fetchWithL402` + Bitcoin Connect provider. Node: `fetchWithL402` + `NostrWebLNProvider` from `@getalby/sdk/webln`.
- **L402 server** → `NWCClient.makeInvoice` + macaroon verification (see `lightning-tools/l402.md`).
- **Testing** → Always prefer [testing wallets](./references/testing-wallets.md) and wire them into automated tests (Jest/Vitest/Playwright) per [automated testing](./references/automated-testing.md).

## NWC Secret Handling (Security)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is mostly duplicated (already in nwc-client.md) but could also be added to bitcoin connect (the browser specific stuff)


- Treat `nostrWalletConnectUrl` as a secret API key. Never log, print, or expose it.
- Backend: keep in environment variables (e.g., `NWC_URL`), never commit to source control, and redact in error messages.
- Browser: request from user input; keep only in memory unless the user explicitly opts into persistence. Do not bake into bundles or HTML.
- When wrapping errors, strip or mask the connection URL before surfacing to logs/telemetry.

## Invoice Safety & Common Pitfalls

- Always decode and check expiry before paying a BOLT-11 invoice; warn if expiry is under ~60 seconds.
- Units: `NWCClient` = **msats**; Lightning Tools/Bitcoin Connect/WebLN = **sats**. Convert carefully when mixing.
- Do not import Lightning Tools from the package root; always use subpath imports (e.g., `@getalby/lightning-tools/fiat`).
- Bitcoin Connect requires a browser DOM; never import it in SSR server code. Call `init()` exactly once on the client.
- Close resources: `unsub()` notifications and `client.close()` when shutting down long-lived processes.
- Handle permission/budget errors: on `QUOTA_EXCEEDED` or `RESTRICTED`, prompt for a new or expanded connection; on `RATE_LIMITED`, back off and retry later.

## Recipe Pointers

- End-to-end msats↔sats with invoicing and forwarding: [cross-library recipe](./references/cross-library-recipe.md).
- L402 client/server patterns: [L402 guide](./references/lightning-tools/l402.md).
- LNURL-pay, comments, payer data, and verify: [lnurl guide](./references/lightning-tools/lnurl.md).
- Invoice parsing/expiry/preimage verification: [invoice guide](./references/lightning-tools/invoice.md).
- Automated wallet creation for tests: [testing wallets](./references/testing-wallets.md) and [automated testing](./references/automated-testing.md).
52 changes: 35 additions & 17 deletions references/automated-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,31 +10,49 @@ Each test can create brand new wallet(s) as required to ensure reproducable resu

### Code Example

The below example allows for temporary networking errors.
The below example allows for temporary networking errors and is reusable as a fixture helper.

```ts
async function createTestWallet(retries = 3): Promise<{ nwcUrl: string; lightningAddress: string }> {
type TestWallet = { nwcUrl: string; lightningAddress: string };

async function createTestWallet({
retries = 4,
delayMs = 500,
balance = 10000,
}: {
retries?: number;
delayMs?: number;
balance?: number;
} = {}): Promise<TestWallet> {
let lastError: Error | undefined;
for (let i = 0; i < retries; i++) {
const response = await fetch("https://faucet.nwc.dev?balance=10000", { method: "POST" });
if (!response.ok) {
try {
const response = await fetch(`https://faucet.nwc.dev?balance=${balance}`, { method: "POST" });
if (!response.ok) {
throw new Error(`Faucet request failed: ${response.status} ${await response.text()}`);
}
const nwcUrl = (await response.text()).trim();
const lud16Match = nwcUrl.match(/lud16=([^&\s]+)/);
if (!lud16Match) {
throw new Error(`No lud16 found in NWC URL: ${nwcUrl}`);
}
const lightningAddress = decodeURIComponent(lud16Match[1]);
return { nwcUrl, lightningAddress };
} catch (error) {
lastError = error as Error;
if (i < retries - 1) {
await new Promise((r) => setTimeout(r, 1000));
continue;
await new Promise((r) => setTimeout(r, delayMs * (i + 1)));
}
throw new Error(`Faucet request failed: ${response.status} ${await response.text()}`);
}
const nwcUrl = (await response.text()).trim();
const lud16Match = nwcUrl.match(/lud16=([^&\s]+)/);
if (!lud16Match) {
throw new Error(`No lud16 found in NWC URL: ${nwcUrl}`);
}
const lightningAddress = decodeURIComponent(lud16Match[1]);
return { nwcUrl, lightningAddress };
}
throw new Error("Failed to create test wallet after retries");
throw lastError ?? new Error("Failed to create test wallet after retries");
}
```

## What to test
#### Fixture patterns (Jest/Vitest/Playwright)

Test both happy path and failure cases (payment failed with insufficient balance etc.)
- Create a fresh wallet per test (or per suite) to avoid state bleed and flakiness.
- Expose `nwcUrl` via env or in-memory fixtures; never log or print the secret.
- In Playwright E2E, pass `nwcUrl` to the app via query param or `page.evaluate` to set it in localStorage/sessionStorage as a setup step.
- Add a top-up helper for balance-sensitive flows: `POST https://faucet.nwc.dev/wallets/<username>/topup?amount=...`.
- Cover failure cases with real flows (insufficient balance, expired invoice, rate limit) and not just mocks.
79 changes: 79 additions & 0 deletions references/bitcoin-connect/bitcoin-connect.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,76 @@ or
</script>
```

## ⚠️ SSR / SSG Warning (Next.js, Nuxt, SvelteKit, Remix, Astro)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice additional docs. I would link to a separate md file to reduce the context for non-SSR apps (progressive disclosure)


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. Never import `@getalby/bitcoin-connect` or `@getalby/bitcoin-connect-react` in server code or shared modules that execute during SSR — gate imports to the client and dynamically load components.

### 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 <BitcoinConnectButton />;
}
```

### Next.js (Pages Router)

```tsx
import dynamic from "next/dynamic";

const WalletButton = dynamic(
() => import("../components/WalletButton"),
{ ssr: false }
);
```

### Nuxt 3

```vue
<template>
<ClientOnly>
<BitcoinConnectButton />
</ClientOnly>
</template>
```

### SvelteKit

```svelte
<script>
import { onMount } from "svelte";
import { browser } from "$app/environment";

let Button;
onMount(async () => {
const bc = await import("@getalby/bitcoin-connect");
bc.init({ appName: "My App" });
// use bc.launchModal(), bc.requestProvider(), etc.
});
</script>
```

### 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
Expand All @@ -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. If you have multiple entry points/components, centralize `init()` to a single client-only location to avoid duplicate initialization.

**Where to call `init()`:**

- **React (Vite / CRA):** in `main.tsx` or `App.tsx`, outside any component, at the top level
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use “top‑level” as a compound adjective.

Change “at the top level” → “at the top‑level” for standard hyphenation in technical docs.

✏️ Proposed fix
-- **React (Vite / CRA):** in `main.tsx` or `App.tsx`, outside any component, at the top level
+- **React (Vite / CRA):** in `main.tsx` or `App.tsx`, outside any component, at the top‑level
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- **React (Vite / CRA):** in `main.tsx` or `App.tsx`, outside any component, at the top level
- **React (Vite / CRA):** in `main.tsx` or `App.tsx`, outside any component, at the toplevel
🧰 Tools
🪛 LanguageTool

[uncategorized] ~126-~126: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ...App.tsx`, outside any component, at the top level - React (Next.js): inside the dynam...

(EN_COMPOUND_ADJECTIVE_INTERNAL)

🤖 Prompt for AI Agents
In `@references/bitcoin-connect/bitcoin-connect.md` at line 126, The phrase "at
the top level" in the React usage note should use the compound adjective form
"top‑level"; update the sentence under "**React (Vite / CRA):** in `main.tsx` or
`App.tsx`, outside any component, at the top level" to read "at the top‑level"
so it becomes "**React (Vite / CRA):** in `main.tsx` or `App.tsx`, outside any
component, at the top‑level", preserving the surrounding context and backtick
filenames.

- **React (Next.js):** inside the dynamically imported client component (see SSR warning above)
- **Vue:** in `main.ts` before `createApp()`
- **Plain HTML:** in a `<script type="module">` tag in the `<head>` or before your app code

```ts
import { init } from "@getalby/bitcoin-connect";

Expand Down
104 changes: 104 additions & 0 deletions references/cross-library-recipe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Cross-Library Recipe: NWC Client + Lightning Tools
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need this. AI is smart enough to figure out how to combine usage of the 2 packages.

If we think it is an issue maybe it's better to focus on the root problem that we have 2 separate npm packages


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.
Loading