Host your own Lightning Address Server (NodeJS)
Introduction
Lightning Addresses allow you to receive Bitcoin over Lightning using an email-like identifier (e.g. [email protected]). This tutorial shows you how to host your own Lightning Address server using a Voltage cloud node and LND’s REST API — eliminating the complexity of gRPC.
By leveraging Voltage’s REST endpoint and a simple Node.js server using Axios, you can generate invoices dynamically and serve a Lightning Address with minimal setup.
Lightning Address Spec:
Lightning Address is a part of the LNURL spec. In order to support Lightning address you must support the 3 specs mentioned below:
Why Use REST Instead of gRPC?
Using REST avoids the need to manage TLS certs, proto files, and gRPC libraries. Voltage’s cloud interface provides a clean HTTPS REST endpoint with macaroon-based auth, making it easy to call from any HTTP client. REST is ideal for lightweight server setups.
Quickstart Checklist
Before coding, ensure you have:
- Voltage LND Node: With REST API access and inbound liquidity.
- Domain Name with HTTPS: A public HTTPS domain is required for Lightning Address support.
- Node.js Environment: Install Node.js (v14+) and npm.
- LND REST Credentials:
- REST Host URL: e.g. https://<nodeid>.m.voltageapp.io:8080
- Macaroon (HEX): Export a hex-encoded macaroon with invoice permission.
- Environment Config: Create a .env file with the necessary values: LND_REST_HOST, LND_REST_MACAROON_HEX, and your DOMAIN. We’ll provide a template in the setup section.
Once you have all the above, the setup process will involve installing a few npm packages and writing the Node.js server code. Then you’ll run the server and test receiving a Lightning payment via your new Lightning Address. Let’s get started!
Setup and Configuration
1. Obtain LND REST URL and Macaroon
Log in to your Voltage account and navigate to your node’s Connect info. You should find the REST URL and admin macaroon (or invoice macaroon) available for download or copyvoltage.cloud. The REST URL will look something like:
Take note of this URL – we’ll use it as LND_REST_HOST in our config. Next, get the macaroon in hex format. If Voltage provides a base64 or binary macaroon file (e.g. admin.macaroon), convert it to hex by reading it in Node or using a tool. For example, in Node you can do:
const fs = require('fs');
const macaroonHex = fs.readFileSync('./admin.macaroon').toString('hex');In many cases, Voltage might display the hex directly. If you prefer a least-privilege approach, create an invoice-only macaroon. Using ThunderHub (or LND CLI), bake a macaroon with create invoices permission onlygithub.com. Copy the hex string of that macaroon for the next step.
2. Configure Environment Variables (.env)
In your project directory, create a file named .env and add the following variables:
# .env - Lightning Address Server config
LND_REST_HOST=https://<YOUR_LND_REST_URL>:8080
LND_REST_MACAROON_HEX=<YOUR_INVOICE_MACAROON_HEX>
DOMAIN=<YOUR_DOMAIN_NAME>Replace <YOUR_LND_REST_URL> with the host from step 1 (for Voltage this is the .voltageapp.io URL or an Onion address if applicable), <YOUR_INVOICE_MACAROON_HEX> with the hex-encoded macaroon, and <YOUR_DOMAIN_NAME> with your domain (e.g. mydomain.com) where this service will rungithub.com.
These values will be loaded by our Node.js server:
- LND_REST_HOST – Base URL for LND’s REST API (including https:// and port, usually 8080 for LND REST)github.com.
- LND_REST_MACAROON_HEX – Hex string of the macaroon that allows invoice creationgithub.com.
- DOMAIN – The domain name you’re using for the Lightning Address (used to construct callback URLs and metadata)github.com.
Note: If your LND node uses a self-signed certificate (default for LND), our code will disable strict TLS verification for simplicity. In production, consider properly trusting the TLS certificate or using a certificate signed by a known CA to avoid disabling verification.
3. Install Dependencies
Initialize a new Node.js project and install the required packages:
$ npm init -y
$ npm install express axios dotenvThese will give us:
- Express: to quickly set up HTTP server routes.
- Axios: to make HTTP requests to LND’s REST API.
- dotenv: to load the config from our .env file.
4. Plan the Server Routes
We’ll follow the LNURL-Pay flow, which requires:
- Discovery Endpoint Responds with payment metadata and a callback URL:
- Callback Endpoint Handles invoice creation. Requires amount in millisatoshis:
This separation aligns with LNURL best practices and adds stricter validation.
5. server.js – Lightning Address Server Example
Below is a complete Node.js server example implementing the above logic. This code uses REST calls (via Axios) to LND instead of gRPC. Create a file server.js and paste the following content:
require("dotenv").config();
const express = require("express");
const axios = require("axios");
const https = require("https");
const app = express();
const PORT = process.env.PORT || 3000;
// Load env variables
const { LND_REST_HOST, LND_REST_MACAROON_HEX, DOMAIN } = process.env;
console.log("LND_REST_HOST:", process.env.LND_REST_HOST);
// Create an Axios instance for LND REST API calls
const lightning = axios.create({
baseURL: LND_REST_HOST,
httpsAgent: new https.Agent({ rejectUnauthorized: false }),
headers: { "Grpc-Metadata-macaroon": LND_REST_MACAROON_HEX }, // Macaroon auth header:contentReference[oaicite:12]{index=12}
});
// Basic home route (optional)
app.get("/", (req, res) => {
res.send("⚡ Lightning Address server is running!");
});
// LNURLp/LN Address endpoint
app.get("/.well-known/lnurlp/:username", (req, res) => {
const username = req.params.username;
const metadata = [
["text/plain", `Payment to ${username}@${DOMAIN}`],
["text/identifier", `${username}@${DOMAIN}`],
];
return res.json({
tag: "payRequest",
callback: `https://${DOMAIN}/lnurl/callback/${username}`,
minSendable: 1000,
maxSendable: 1000000000,
metadata: JSON.stringify(metadata),
});
});
app.get("/lnurl/callback/:username", async (req, res) => {
const amountMsat = req.query.amount;
if (!amountMsat) {
return res
.status(400)
.json({ status: "ERROR", reason: "Missing 'amount' parameter" });
}
try {
const msat = parseInt(amountMsat, 10);
const satoshis = Math.floor(msat / 1000);
const response = await lightning.post("/v1/invoices", { value: satoshis });
const paymentRequest = response.data.payment_request;
return res.json({ pr: paymentRequest, routes: [] });
} catch (err) {
console.error("Invoice creation error:", err.response?.data || err);
return res
.status(500)
.json({ status: "ERROR", reason: "Failed to create invoice" });
}
});
// Start the server
app.listen(PORT, () => {
console.log(`✅ Lightning Address server is listening on port ${PORT}`);
});Let’s break down what this server does (Updated):
- It sets up two clearly separated routes to comply with the LNURL-Pay protocol:
- Discovery Endpoint: The route at /.well-known/lnurlp/:username responds with a payRequest object. This includes a callback URL (now pointing to a separate endpoint), minSendable, maxSendable, and metadata describing the payment target.
- Callback Endpoint: The route at /lnurl/callback/:username handles the actual invoice creation. It checks for the required amount parameter (in millisatoshis). If it’s missing, it returns an error as per LNURL spec. If present, it uses Axios to call LND’s REST API and returns a pr (Bolt11 invoice) and empty routes array.
- Axios is configured to talk to LND’s REST API using the LND_REST_HOST and macaroon from your .env file.
- The server listens on the specified port (3000 by default).
- TLS verification is disabled for simplicity when using self-signed certs (Voltage’s REST interface can be trusted as it uses a proper CA).
- The use of .env keeps your credentials secure and out of the codebase.=
Environment Tip: Keep your .env values secure and never commit them to Git. The macaroon is like a password to your node (in this case, it grants invoice creation rights), so treat it confidentially.
Running the Server
Start the Node server:
$ node server.js
✅ Lightning Address server is listening on port 3000Your Lightning Address service is now live. When a Lightning wallet queries https://<DOMAIN>/.well-known/lnurlp/<username>, it will receive the LNURL pay service response. Then, when it requests an invoice (by calling the callback URL with an amount), your server will create an invoice via the Voltage node and return it.
Testing Your Lightning Address (Updated Flow)
- Wallet queries LNURL Address
- https://<DOMAIN>/.well-known/lnurlp/<username>
- Receives metadata + callback URL
- Wallet sends amount to callback
- Example: https://<DOMAIN>/lnurl/callback/<username>?amount=10000
- Server generates invoice and responds with pr
You can test with:
curl https://<DOMAIN>/.well-known/lnurlp/alice
curl "https://<DOMAIN>/lnurl/callback/alice?amount=10000"Note: On Voltage, make sure your node’s IP Whitelist includes the IP of your server (or is disabled) so that API calls from your server to the node are allowedvoltage.cloud. A 403 error in the Axios request may indicate a missing whitelist entry for your server’s IP.
Differences from the gRPC Approach
Migrating from gRPC/ln-service to REST+Axios brings some changes worth noting:
- No gRPC Dependency: We removed the need for the gRPC interface and ln-service. Our example no longer loads TLS certificate files or uses grpc libraries. Everything is done with HTTP calls using Axios, which can simplify deployment and avoid compatibility issues (especially on platforms where installing gRPC is troublesome).
- Environment Variables: Instead of a socket address, cert, and macaroon file path, we now use a single LND_REST_HOST URL and a hex-encoded macaroongithub.com. This can be easier to configure, especially with cloud nodes. We no longer need to handle reading a cert file or configuring gRPC channels.
- TLS Handling: Voltage nodes already present a valid CA-signed certificate, so Axios’s default HTTPS validation works out of the box; no extra TLS configuration is required.
- Streaming and Listeners: gRPC allows streaming RPCs (for example, listening for invoices being paid). The REST API is request-response only. In this simple Lightning Address server, we don’t need streaming — we generate an invoice and that’s it. However, if you wanted the server to wait for the invoice to be paid (and maybe confirm to the user), you’d need to poll the /v1/invoice/<r_hash> endpoint or use WebSocket/subscription solutions. This is a bit less convenient than gRPC’s streaming (e.g., subscribeInvoices)lightning.engineering. In practice, LNURL-pay flows usually don’t require the server to confirm payment in real-time to the payer; the wallet takes care of notifying the user upon payment.
- Performance: For most use cases, the REST approach performs similarly for creating invoices. gRPC might have a slight edge in high-frequency scenarios or when sending large streams of data, but for on-demand invoice creation the difference is negligible. The trade-off is well worth the simplicity for a small self-hosted service.
- Code Simplicity: Our Node.js code is straightforward and uses common libraries. We avoided the intricacies of proto definitions and gRPC clients. This should make maintenance easier and the code more accessible to developers who aren’t familiar with gRPC.
Conclusion
By refactoring the Lightning Address server to use Voltage’s REST API, we achieved a simpler and more accessible setup while preserving full functionality. We retained the original structure (initial LNURL fetch and invoice generation callback) but now use standard HTTP calls to interface with LND. You can still receive payments directly into your own node, but with less overhead in your codebase.
To recap, you configured a Voltage LND node, set up a small Express server with Axios to create invoices, and served the Lightning Address protocol on your domain. You can now share an address like [email protected] and receive Lightning payments straight into your node 🎉.
Feel free to extend this example: you might add logging, support for multiple users, or even a simple frontend. For instance, some implementations include a static webpage or redirect for /<username> to show a donation page. You could also integrate a database if you want to track payments or generate dynamic success messages. The core logic, however, will remain the same – responding to LNURL requests and creating invoices.
Finally, remember to keep your macaroon safe and never expose it publicly. Happy hacking, and enjoy your self-hosted Lightning Address server! ⚡