Integration with LND (Lightning Network Daemon)
Lightning Network solves specific Bitcoin problem: on-chain transactions cost money and require confirmations. LND (Lightning Network Daemon from Lightning Labs) is the most widespread LN node implementation. LND integration is needed when you want to accept instant Bitcoin payments with fees in few satoshis, or build app on top of Lightning. This is not "connect to API" — it's running and maintaining payment infrastructure.
What LND is and how it works
LND is a software Lightning Network node. Requires:
- Synchronized Bitcoin node (Bitcoind or
neutrinolight mode) - Open payment channels with network partners
- Liquidity management: your side of channel needs funds for outgoing payments, opposite side for incoming
Payment channels are 2-of-2 multisig contracts on Bitcoin L1. LND manages channel state off-chain, publishing to blockchain only on open and close.
Invoice-based payments. Receiver creates invoice (BOLT-11 ticket), payer pays it. Invoice contains payment hash — HTLC mechanism guarantees atomicity.
Connecting to LND: gRPC vs REST
LND provides two APIs: gRPC (main, complete) and REST (wrapper). For production — gRPC.
Authentication through TLS certificate + macaroon (capability-based token):
import * as grpc from '@grpc/grpc-js';
import * as protoLoader from '@grpc/proto-loader';
import fs from 'fs';
const TLS_CERT = fs.readFileSync('/home/bitcoin/.lnd/tls.cert');
const MACAROON = fs.readFileSync('/home/bitcoin/.lnd/data/chain/bitcoin/mainnet/admin.macaroon');
// Create channel with TLS + macaroon
const sslCreds = grpc.credentials.createSsl(TLS_CERT);
const macaroonCreds = grpc.credentials.createFromMetadataGenerator((_, callback) => {
const metadata = new grpc.Metadata();
metadata.add('macaroon', MACAROON.toString('hex'));
callback(null, metadata);
});
const credentials = grpc.credentials.combineChannelCredentials(sslCreds, macaroonCreds);
const packageDef = protoLoader.loadSync('rpc.proto', { keepCase: true });
const lnrpc = grpc.loadPackageDefinition(packageDef) as any;
const lightning = new lnrpc.lnrpc.Lightning('localhost:10009', credentials);
Macaroon is not just a token, it's capability-based authorization. You can create invoice.macaroon (only invoice creation), readonly.macaroon (read-only), custom with IP and time restrictions. Don't give admin.macaroon to apps — only minimum necessary permissions.
Main operations
Creating invoice (receiving payment)
function addInvoice(amountSats: number, memo: string): Promise<Invoice> {
return new Promise((resolve, reject) => {
lightning.AddInvoice({
value: amountSats, // amount in satoshis
memo, // description (visible to payer)
expiry: 3600, // validity period in seconds
}, (err: any, response: any) => {
if (err) reject(err);
else resolve({
paymentRequest: response.payment_request, // BOLT-11 string
rHash: response.r_hash.toString('hex'), // payment hash
addIndex: response.add_index.toString(),
});
});
});
}
BOLT-11 string starts with lnbc (mainnet) or lntb (testnet). This is what user scans with wallet.
Tracking incoming payments
Two approaches:
Polling — LookupInvoice by r_hash. Simple but not optimal.
Streaming subscriptions — SubscribeInvoices streams all updates in real-time:
function subscribeInvoices(onSettled: (invoice: SettledInvoice) => void) {
const stream = lightning.SubscribeInvoices({
settle_index: 0, // from start, or from specific index for catch-up
});
stream.on('data', (invoice: any) => {
if (invoice.state === 1) { // SETTLED = paid
onSettled({
rHash: invoice.r_hash.toString('hex'),
amountPaidSats: Number(invoice.amt_paid_sat),
settledAt: Number(invoice.settle_date),
memo: invoice.memo,
});
}
});
stream.on('error', (err: Error) => {
// Reconnect logic
setTimeout(() => subscribeInvoices(onSettled), 5000);
});
}
Important: persist settle_index. On app restart subscribe from last processed settle_index, or you'll miss payments received during downtime.
Outgoing payments
async function sendPayment(paymentRequest: string): Promise<string> {
return new Promise((resolve, reject) => {
const routerStub = new lnrpc.routerrpc.Router('localhost:10009', credentials);
const stream = routerStub.SendPaymentV2({
payment_request: paymentRequest,
timeout_seconds: 60,
fee_limit_sat: 100, // max fee in satoshis
max_parts: 4, // MPP: split into multiple parts if needed
});
stream.on('data', (payment: any) => {
if (payment.status === 2) { // SUCCEEDED
resolve(payment.payment_preimage.toString('hex'));
} else if (payment.status === 3) { // FAILED
reject(new Error(`Payment failed: ${payment.failure_reason}`));
}
});
});
}
SendPaymentV2 (router RPC) is preferable to old SendPayment — supports MPP (Multi-Path Payments), better error handling.
Liquidity management
This is ongoing task that never ends. Problems:
Inbound liquidity. To receive payments you need liquidity on partner's channel side. New node often can't receive payments. Solutions:
- Lightning Service Providers (Bitrefill Thor, Loop In, Amboss Magma) — paid inbound rental
- Open channel to partner: ask partner to open channel to you
Channel rebalancing. Over time channels become "skewed" — all funds on one side. lnd loop out — submarine swap for rebalancing: exits Lightning funds to on-chain, redistributes. Used automatically by tools like charge-lnd or bos (Balance of Satoshis).
Fee policy. For routing other payments through your node, base_fee + fee_rate charged. Proper fee setup affects routing efficiency.
LNURL and wallet integration
LNURL is protocol extensions on top of LN. Key types:
LNURL-pay — user scans QR, wallet automatically requests invoice of needed amount. Don't need to know amount beforehand:
// Backend endpoint for LNURL-pay
app.get('/.well-known/lnurlp/:username', async (req, res) => {
res.json({
callback: `https://yourdomain.com/lnurlp/${req.params.username}/pay`,
maxSendable: 100_000_000, // msats
minSendable: 1_000,
metadata: JSON.stringify([['text/plain', `Pay ${req.params.username}`]]),
tag: 'payRequest',
});
});
app.get('/lnurlp/:username/pay', async (req, res) => {
const { amount } = req.query; // in msats
const invoice = await createInvoice(Number(amount) / 1000);
res.json({ pr: invoice.paymentRequest, routes: [] });
});
Lightning Address — human-readable address like [email protected]. Implemented through LNURL-pay endpoint at /.well-known/lnurlp/{username}.
LNURL-withdraw — allows user to receive funds through LN. Use: payouts, cashback.
What's included in integration
Standard LND integration: setting up or connecting to existing LND node, gRPC client with TLS + macaroon auth, invoice creation, subscription to incoming payments with persistent settle_index, outgoing payment handling, LNURL-pay endpoint (if needed), basic error handling and reconnect logic.
Operational part (channel management, liquidity) — separate question, depends on payment flow scale.
Timeline: 1-2 weeks for backend integration into existing application.







