A learning project that demonstrates how an online store can create Bitcoin Lightning invoices, display them as QR codes, detect settlement, and update a customer's checkout in real time.
The project is being built step by step to understand the infrastructure behind a non-custodial Bitcoin payment gateway. It is not production-ready and should not currently be used to process customer money.
Create invoice → show QR → pay from a Lightning wallet → detect LND settlement → update the checkout over WebSockets.
More screenshots can be added as the project grows:
Recommended screenshots:
| Filename | What it should show |
|---|---|
storefront.png |
The shoe product, $20 total, and Pay with Bitcoin button |
lightning-checkout.png |
QR code, lnbcrt... request, amount, and countdown |
polar-network.png |
Bitcoin Core, Merchant LND, Customer LND, and their channels |
polar-payment.png |
Customer node paying the Merchant invoice |
payment-received.png |
React's successful Payment received state |
The storefront and Lightning checkout screenshots are included. After adding
any remaining files, move their image markup outside the <!-- and -->
comment lines to display them on GitHub.
- A customer selects a product in the React storefront.
- React requests an invoice from the Node.js API.
- The API asks the merchant's LND node to create a BOLT11 invoice.
- The invoice is rendered as a QR code with a 15-minute countdown.
- The backend monitors the invoice state in LND.
- When LND reports
SETTLED, the backend marks the orderPAID. - A WebSocket message updates the React checkout immediately.
This project currently implements a Bitcoin Lightning checkout for receiving customer payments.
Customer Lightning wallet
│
│ pays a BOLT11 invoice
▼
Merchant LND wallet
- Receive incoming Bitcoin payments over the Lightning Network.
- Create a unique BOLT11 invoice for an e-commerce order.
- Convert the
$20product price to30,000 satsusing a fixed learning-only rate. - Display the Lightning invoice as a QR code and copyable payment request.
- Show a 15-minute invoice-expiration countdown.
- Check the invoice state through the Merchant LND node.
- Accept
SETTLEDas the only successful-payment state. - Push the paid state to React using WebSockets.
- Demonstrate Merchant inbound and Customer outbound channel liquidity on regtest.
- Switch LND connectivity between regtest and mainnet using environment configuration.
- Merchant payouts to customers, suppliers, or other wallets.
- Customer refunds.
- Sending Bitcoin from the Merchant wallet.
- On-chain Bitcoin checkout using
bc1...orbcrt1...addresses. - Withdrawals from LND to cold storage or an exchange.
- Automatic conversion between Bitcoin and fiat currency.
- Bank, card, stablecoin, or other cryptocurrency payments.
- Production merchant accounts, balances, or settlement reports.
Receiving and spending require different security permissions. The backend uses
LND's restricted invoice.macaroon, which can manage invoices but is not given
general permission to spend the Merchant's funds. A payout system would require
separate authorization, approval rules, spending limits, idempotency, audit
logs, destination validation, and stronger operational security.
The accurate current project description is:
A non-custodial Bitcoin Lightning checkout gateway for receiving customer payments, developed and tested end to end on regtest with a network-configurable LND integration.
React storefront
│
│ HTTP + WebSocket
▼
Node.js / Express API
│
│ HTTPS + invoice macaroon
▼
Merchant LND node
│
▼
Bitcoin + Lightning network
sequenceDiagram
actor Shopper as Customer
participant UI as React Checkout
participant API as Express API
participant Merchant as Merchant LND
participant Wallet as Customer LND
Shopper->>UI: Pay with Bitcoin
UI->>API: POST /api/v1/invoices
API->>Merchant: AddInvoice (30,000 sats)
Merchant-->>API: lnbcrt... BOLT11 invoice
API-->>UI: QR + payment request
Shopper->>Wallet: Pay invoice
Wallet->>Merchant: Lightning payment
Merchant-->>API: Invoice state = SETTLED
API-->>UI: WebSocket status = PAID
UI-->>Shopper: Payment received
- React 19 and Vite
- Node.js and Express
- WebSockets (
ws) - LND REST API
- BOLT11 Lightning invoices
- QR-code generation
- Node.js test runner
- Polar and Docker for local regtest development
PostgreSQL, merchant webhooks, live BTC/USD pricing, authentication, and payment auditing are planned later steps.
Use the official project documentation when installing or updating the tools:
| Tool | Why it is needed | Official documentation |
|---|---|---|
| Node.js and npm | Runs the React build tools and Express API | Download Node.js |
| Docker Engine | Runs Polar's Bitcoin and Lightning containers | Install Docker Engine |
| Docker Compose | Manages the containers created by Polar | Install Docker Compose |
| Polar | Creates the private regtest network and managed nodes | Polar website and Polar releases |
| Bitcoin Core | Provides the regtest Bitcoin blockchain | Bitcoin Core downloads |
LND and lncli |
Creates wallets, channels, invoices, and payments | LND installation guide |
| LND REST API | Defines the invoice endpoints used by the backend | LND API reference |
| React | Implements the customer checkout | React documentation |
| Vite | Runs and builds the React frontend | Vite guide |
For the recommended regtest setup, install Node.js, Docker, Docker Compose, and Polar. Polar downloads and runs its own Bitcoin Core and LND Docker images when the network starts, so separate Bitcoin Core and LND installations are not required for the free regtest demonstration. The standalone LND instructions in this README document the optional unfunded mainnet learning experiment.
LND (Lightning Network Daemon) is used as both the Lightning node and wallet. The free regtest environment uses two separate LND wallets:
- Merchant LND: creates invoices and receives store payments.
- Customer LND: represents a shopper and pays the merchant invoice.
The customer node opens a regtest channel toward the merchant node. This gives the customer outbound liquidity and the merchant inbound liquidity without using valuable bitcoin.
Wallet seeds, passwords, TLS private keys, macaroons, databases, and node data must never be committed to Git.
Regtest is a private Bitcoin network created locally with Polar. Its addresses, keys, signatures, blocks, Lightning channels, invoices, and settlements use the real Bitcoin and Lightning protocols, but its coins have no monetary value.
- Bitcoin addresses start with
bcrt1.... - Lightning invoices start with
lnbcrt.... - Blocks and coins are generated locally.
- Complete payments can be tested without purchasing bitcoin.
The regtest environment is running with Merchant and Customer LND nodes and two
active private channels. A completed settlement test will be documented here
after the first lnbcrt... invoice is paid from Customer to Merchant.
An LND v0.20.0-beta mainnet node was installed, checksum-verified, synchronized, and connected to the backend successfully. It has not been funded, no channels have been opened, and no mainnet payment has been performed.
Mainnet addresses and invoices are real and can hold monetary value. Mainnet is kept unfunded while the project is under development.
| Regtest | Mainnet |
|---|---|
| Free local coins | Valuable BTC |
bcrt1... addresses |
bc1... addresses |
lnbcrt... invoices |
lnbc... invoices |
| Locally generated blocks | Publicly mined blocks |
| Safe for development | Financial loss is possible |
Regtest coins, addresses, invoices, and channels cannot be transferred to or used on mainnet.
- Responsive React product checkout
- Express invoice API
- LND REST client with TLS verification
- Restricted invoice-macaroon authentication
- BOLT11 QR-code generation
- Invoice expiration countdown
- LND settlement polling
- WebSocket status updates
- Mainnet LND installation and synchronization
- Network-selectable LND configuration
- Safe unit tests using an injected fake payment service
- Start the two-node Polar regtest network
- Fund the Customer with free regtest coins
- Open private Customer-to-Merchant channels
- Complete the first regtest Lightning payment
- Store invoices in PostgreSQL
- Add signed merchant webhooks
- Replace the fixed exchange rate with a live provider
- Add authentication, idempotency, logging, and deployment hardening
This section records the project in the order it was built. Commands are shown so another developer can reproduce the work. Paths containing wallet credentials are examples only and must be adjusted locally.
e-commercepayment/
├── backend/
│ ├── lnd-client.js
│ ├── server.js
│ └── server.test.js
├── frontend/
│ ├── public/images/
│ └── src/
│ ├── App.jsx
│ ├── main.jsx
│ └── styles.css
├── docs/
├── scripts/
├── package.json
└── vite.config.js
Install the Node.js dependencies:
npm installThe first backend used an in-memory simulated invoice provider. This allowed the API, countdown, WebSocket flow, duplicate-payment protection, and React states to be tested before connecting any Bitcoin infrastructure.
The initial plain browser prototype was replaced with React 19 and Vite. The checkout now includes:
- A product page for an example shoe.
- A $20 order total.
- A Bitcoin payment modal.
- A generated QR image.
- The full Lightning payment request.
- The internal invoice ID.
- Copy buttons and a 15-minute timer.
- Pending, paid, expired, loading, and error states.
Run both development services:
npm run devCreate a production frontend build:
npm run buildThe official LND v0.20.0-beta Linux archive and checksum manifest were downloaded from the Lightning Network Daemon GitHub release:
mkdir -p .tools/downloads .tools/lnd
curl --fail --location \
--output .tools/downloads/manifest-v0.20.0-beta.txt \
https://github.com/lightningnetwork/lnd/releases/download/v0.20.0-beta/manifest-v0.20.0-beta.txt
curl --fail --location \
--output .tools/downloads/lnd-linux-amd64-v0.20.0-beta.tar.gz \
https://github.com/lightningnetwork/lnd/releases/download/v0.20.0-beta/lnd-linux-amd64-v0.20.0-beta.tar.gzVerify the archive before extracting it:
grep 'lnd-linux-amd64-v0.20.0-beta.tar.gz' \
.tools/downloads/manifest-v0.20.0-beta.txt
sha256sum .tools/downloads/lnd-linux-amd64-v0.20.0-beta.tar.gzThe expected and calculated SHA-256 value was:
88c43d138bb2fb38ccc806da3a2d2a6845cd6a0d6a25b8a3f9ba047a73533557
Extract and inspect the version:
tar -xzf .tools/downloads/lnd-linux-amd64-v0.20.0-beta.tar.gz \
--strip-components=1 \
-C .tools/lnd
./.tools/lnd/lnd --version
./.tools/lnd/lncli --versionThe project contains helper scripts that consistently point LND and lncli at
the project-specific data directory:
./scripts/start-lnd.sh
./scripts/lncli.sh create
./scripts/lncli.sh unlock
./scripts/lncli.sh getinfoWallet creation is interactive. The wallet password and 24-word recovery seed were never stored in source code or documentation.
LND was configured for mainnet with its Neutrino light client. The mainnet fee estimator required this configuration:
fee.url=https://nodes.lightning.computer/fees/v1/btc-fee-estimates.jsonSynchronization was monitored with:
./scripts/lncli.sh getinfo | \
grep -E 'block_height|best_header_timestamp|num_peers|synced'The completed node reported:
synced_to_chain: true
synced_to_graph: true
network: mainnet
The mainnet wallet remained at zero balance, and no mainnet channel or payment was created. This validated LND installation and backend connectivity without risking money.
The backend now authenticates using LND's restricted invoice.macaroon and
verifies the node's TLS certificate. It does not use admin.macaroon.
The backend calls:
POST /v1/invoices Create a BOLT11 invoice
GET /v1/invoice/{payment_hash} Check its LND state
GET /v1/invoices Verify LND connectivity
Invoice states are polled every two seconds. Only LND's SETTLED state changes
the order to PAID; the old simulated-payment endpoint and button were removed.
The browser receives payment updates through:
/api/v1/invoices/{invoice_id}/ws
QR images are served as PNG files from:
GET /api/v1/invoices/{invoice_id}/qr
Kali Linux provided Compose under the docker-compose package name:
sudo apt update
sudo apt install docker.io docker-compose
sudo systemctl enable --now docker
sudo usermod -aG docker "$USER"
newgrp dockerVerify the installation:
docker version
docker compose version
docker run --rm hello-worldVersions used while building this project:
Docker Engine: 28.5.2
Docker Compose: 2.40.3
Polar v4.0.0 was downloaded from its official GitHub release:
curl --fail --location \
--output /tmp/polar-linux-amd64-v4.0.0.deb \
https://github.com/jamaljsr/polar/releases/download/v4.0.0/polar-linux-amd64-v4.0.0.deb
sha256sum /tmp/polar-linux-amd64-v4.0.0.debThe calculated checksum matched GitHub's published asset digest:
227609c57bcc639c6e06ff16344c3c57e0d9d466f52e605be120378fcdb17d40
Install and open Polar:
sudo apt install /tmp/polar-linux-amd64-v4.0.0.deb
polarThe Polar network is named ecommerce-regtest and contains:
Bitcoin Core 30.0
├── Merchant — LND 0.20.0-beta
└── Customer — LND 0.20.0-beta
No Core Lightning, Eclair, Taproot Assets, or Terminal nodes are required for this project.
The Customer received free regtest funds through Polar. The deposit form used
1,000,000 sats per test deposit; these coins have no value:
Customer test-deposit amount: 1,000,000 regtest sats
Merchant deposit: 0 sats
Private channels were opened in this direction:
Customer ──500,000-sat private channel──▶ Merchant
The Customer is the channel initiator, so it receives outbound capacity. The Merchant receives inbound capacity and can accept checkout payments. Two test channels were opened during development; this is harmless because all balances exist only on regtest.
Observed channel state:
Active channels: 2
Capacity per channel: 500,000 sats
Customer local/outbound: approximately 993,060 sats total
Merchant remote/inbound: approximately 993,060 sats total
Create the ignored environment file:
touch .envPopulate it with the Merchant values shown in Polar's Connect tab:
PORT=3000
LND_NETWORK=regtest
LND_REST_HOST=<merchant-lnd-host>
LND_REST_PORT=<merchant-rest-port>
LND_TLS_PATH=<absolute-path-to-Merchant-tls.cert>
LND_MACAROON_PATH=<absolute-path-to-Merchant-regtest-invoice.macaroon>Verify the selected network through GET /api/v1/lnd/health before creating
an invoice.
Expected result:
{"connected":true,"network":"regtest"}Never pay an invoice beginning with lnbc during this free exercise. A valid
regtest Lightning invoice begins with lnbcrt.
Run the application while the Polar network remains active:
npm run devThen:
- Open the address printed by Vite after the application starts.
- Select Pay with Bitcoin.
- Confirm the payment request begins with
lnbcrt. - Copy the entire Lightning payment request—not the internal UUID invoice ID.
- In Polar, open Customer → Payments → Pay Invoice.
- Paste the
lnbcrt...request and approve the free regtest payment. - Merchant LND reports
SETTLED. - The backend changes the order from
PENDINGtoPAID. - WebSocket pushes the update to React.
- The checkout displays Payment received.
Until step 6 is performed successfully, the first full settlement remains an open checklist item rather than a claimed result.
npm test
npm run buildCurrent automated coverage verifies:
- Invoice creation and retrieval.
- Request validation.
- QR PNG delivery.
- Settlement reported by an injected payment provider.
- LND health response.
The automated suite injects a fake payment service and cannot move mainnet BTC.
This means an older development process is still running:
sudo fuser -v 3000/tcp 5173/tcp
kill <PID>
npm run devRun only one copy of npm run dev.
Port 5173 is already occupied. Stop the older Vite process and restart rather
than using two frontends connected to different backend instances.
Stop immediately and do not pay the invoice. Confirm .env contains:
LND_NETWORK=regtestRestart the backend and verify /api/v1/lnd/health before continuing.
LND is not running or exited during startup. Inspect its log:
tail -n 30 .lnd-data/logs/bitcoin/mainnet/lnd.logThe project originally returned a large embedded data: URL. It now serves the
QR through a normal PNG API endpoint, which is more reliable across browsers.
Requirements:
- Node.js 22 or newer
- npm
- A configured LND node, or Polar for free regtest payments
npm installCreate the ignored environment file:
touch .envSet the LND REST port, TLS certificate path, invoice macaroon path, and network
in .env. Never commit the populated file.
Start React and Express together:
npm run devFollow docs/REGTEST_SETUP.md to install Docker and Polar, create the Merchant and Customer LND nodes, fund them with free regtest coins, and open the test channel.
Before creating an invoice, call GET /api/v1/lnd/health and verify that the
backend is connected to regtest.
Expected response:
{"connected":true,"network":"regtest"}Stop if the response says mainnet during a free regtest exercise.
npm test
npm run buildThe automated tests use a fake injected payment service. They never create a mainnet invoice and never move bitcoin.
- Invoice records are stored in memory and disappear after a backend restart.
- The temporary rate is fixed at
1 USD = 1,500 sats. - There is no merchant authentication.
- There is no PostgreSQL database or webhook retry queue yet.
- The mainnet node has no channels or inbound liquidity.
- The application has not completed a real-value mainnet payment.
Never commit or share:
- The 24-word wallet seed
- Wallet passwords
admin.macaroonorinvoice.macaroontls.key.env- LND wallet databases
- Polar node-data directories
Anyone who obtains powerful LND credentials or wallet recovery material may be able to control or steal funds. Use regtest until the complete system has been tested and reviewed.

