원클릭으로
원클릭으로
Setting up a Bitcoin Lightning node on Ubuntu using litd (Lightning Terminal). Default path: Neutrino-backed single node (fast, no bitcoind needed). Also covers bitcoind-backed routing nodes and the remote signer architecture for production.
Reference for litd (Lightning Terminal) gRPC API in Go: managing accounts, baking macaroons, listing payments, and creating LNC sessions.
Working guide for LND + Taproot Assets gRPC in Go — authentication, asset amounts, channel data, pagination, and common gotchas discovered building a real wallet.
Navigation guide for the lnd (Lightning Network Daemon) Go codebase — where to find things, key patterns, and how subsystems connect.
Build and configure the MCP server for Lightning Node Connect (LNC). Connects AI assistants to lnd nodes via encrypted WebSocket tunnels using pairing phrases — no direct network access or TLS certs needed. Read-only by default (18 tools for querying node state, channels, payments, invoices, peers, on-chain data).
Set up an lnd remote signer container that holds private keys separately from the agent. Exports a credentials bundle (accounts JSON, TLS cert, admin macaroon) for watch-only litd nodes. Container-first with Docker, native fallback. Use when firewalling private key material from AI agents.
| name | lnc-app |
| description | Guide for building a Lightning Node Connect (LNC) web application using lnc-web |
The user wants guidance on building an LNC-powered web application. Use the knowledge below to produce clear, accurate advice or code.
Lightning Node Connect lets a browser-based app communicate with an LND node without exposing any ports. The connection is end-to-end encrypted and routed through a mailbox proxy server. The user generates a pairing phrase (a BIP39-style mnemonic) in Lightning Terminal (litd) that encodes the cryptographic key material for the session. The client derives its keys from this phrase, connects to the mailbox, and performs a Noise protocol handshake with the node.
npm install @lightninglabs/lnc-web
The package ships a prebuilt UMD bundle. Import it as a default import:
import LNC from '@lightninglabs/lnc-web';
Vite config — tell Vite to pre-bundle it (converts UMD to ESM):
// vite.config.js
export default {
optimizeDeps: {
include: ['@lightninglabs/lnc-web'],
},
};
lnc-web stores everything it needs to reconnect in window.localStorage, namespaced to avoid conflicts. The credential store holds:
| Field | Description |
|---|---|
pairingPhrase | The original mnemonic — one-time use only |
serverHost | host:port of the mailbox proxy (no protocol prefix) |
localKey | Client's private key, generated on first connect |
remoteKey | Node's static public key, received on first connect |
password | Not read/written by LNC itself — exposed for your convenience |
isPaired is a read-only getter that returns true when localKey and remoteKey are both stored, meaning the session can reconnect without the pairing phrase.
The pairing phrase encodes cryptographic key material only — it does not encode the mailbox server address. The serverHost must be provided separately and is stored in the credential store after first connection.
The credential store encrypts localKey and remoteKey at rest using the password. The password is applied transparently inside the getter/setter — you never call encrypt/decrypt yourself. Set it before connecting:
lnc.credentials.password = 'user-chosen-password';
Pass pairingPhrase and password in the constructor (not as properties afterwards — the WASM module must be initialised with them):
const lnc = new LNC({
namespace: 'my-app', // isolates localStorage keys
pairingPhrase: phrase, // mnemonic from litcli
password: 'local-password',
});
// Override the mailbox if the user is not using the default
lnc.credentials.serverHost = 'mailbox.terminal.lightning.today:443';
await lnc.connect();
// IMPORTANT: clear the stored pairing phrase after success.
// On reconnect lnc-web would otherwise try to pair again with a
// one-time-use phrase and get "stream not found" from the mailbox.
lnc.credentials.pairingPhrase = '';
When isPaired is true the stored localKey/remoteKey and serverHost are all that is needed. Do not pass pairingPhrase — leave it out of the constructor entirely and clear it explicitly before connecting as a safety measure:
const lnc = new LNC({ namespace: 'my-app' });
lnc.credentials.password = 'local-password';
lnc.credentials.pairingPhrase = ''; // ensure it is never reused
await lnc.connect();
After connect() resolves, check lnc.isConnected. If it is false, read lnc.status for a human-readable reason (e.g. "Session Not Found", "Wallet Locked"). Reset the cached instance and throw so the UI can surface the message:
if (!lnc.isConnected) {
const reason = lnc.status || 'Unknown error';
lnc = null; // force a fresh instance on next attempt
throw new Error(reason);
}
Use a throwaway instance to read isPaired — do not cache the result or the instance, as you do not yet have the proxy or password:
function hasPairedCredentials() {
try {
return new LNC({ namespace: 'my-app' }).credentials.isPaired;
} catch {
return false;
}
}
lnc.disconnect();
lnc.credentials.clear(); // wipes localStorage
lnc = null;
| Setting | Default | Notes |
|---|---|---|
| Proxy server | mailbox.terminal.lightning.today:443 | Show on pairing screen only — stored in credentials, not needed again |
| Local password | — | Required; encrypts keys in localStorage |
| Pairing phrase | — | One-time use; cleared after first connect |
The proxy field should be shown only on the first-time pairing screen, not on the returning-user login screen (the stored value is used automatically). Pass the value via lnc.credentials.serverHost after constructing the instance, without a protocol prefix (wss:// is added internally by lnc-web).
All LND services are available under lnc.lnd.*. Calls are async and return plain JS objects.
const info = await lnc.lnd.lightning.getInfo();
console.log(info.alias, info.numActiveChannels);
const resp = await lnc.lnd.lightning.addInvoice({
value: '5000', // satoshis as string
memo: 'Coffee',
expiry: '300', // seconds as string
});
const { paymentRequest, rHash } = resp;
// paymentRequest is the BOLT11 string — encode it as a QR code
// rHash is the payment hash bytes — keep it to poll for settlement
Permission note:
addInvoiceis a write operation. A purereadonlyLNC session will reject it. The session must have invoice write permission.
Pass rHash back exactly as lnc-web returned it — do not attempt to convert it to hex manually:
const invoice = await lnc.lnd.lightning.lookupInvoice({ rHash: resp.rHash });
if (invoice.settled) {
// payment received
}
Poll on a timer; cancel when settled, timed out, or the user cancels:
function pollInvoice(rHash, timeoutMs, onPaid, onExpired) {
const deadline = Date.now() + timeoutMs;
let timer;
async function tick() {
if (Date.now() >= deadline) return onExpired();
const inv = await lnc.lnd.lightning.lookupInvoice({ rHash });
if (inv.settled) return onPaid(inv);
timer = setTimeout(tick, 2000);
}
timer = setTimeout(tick, 2000);
return { cancel: () => clearTimeout(timer) };
}
const resp = await lnc.lnd.lightning.listInvoices({
reversed: true, // newest first from LND
numMaxInvoices: '10',
});
const invoices = resp.invoices ?? [];
lnc.lnd.lightning.subscribeInvoices(
{},
(invoice) => {
if (invoice.settled) console.log('Paid:', invoice.paymentRequest);
},
(err) => console.error('Stream error:', err),
);
const wallet = await lnc.lnd.lightning.walletBalance();
const channel = await lnc.lnd.lightning.channelBalance();
Boot
└─ hasPairedCredentials()?
├─ No → Show pairing screen (phrase + password + proxy)
│ → pair() → clear pairingPhrase → show settings → main screen
└─ Yes → Show login screen (password only)
→ login() → main screen
On logout: disconnect() + credentials.clear() + redirect to pairing screen.