Back to Learn

Lightning Address: What It Is and How to Get One

What a Lightning address is, how a wallet turns name@domain into an invoice with LNURL-pay, where to get one, and how to serve addresses on your own domain.

Updated October 23, 2026View as Markdown
  • Lightning address
  • LNURL
  • Lightning network
  • Bitcoin
  • Payments

The worst part of receiving bitcoin used to be telling someone where to send it.

On-chain, you hand over 42 characters like bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh and hope they get copied right. On Lightning it was worse. An invoice is longer still, it's meant for one payment, and it expires: an hour after it's made, unless it says otherwise. To be paid twice you make a second invoice, and a payment link in a profile bio is dead before most people see it.

A Lightning address looks like an email address instead:

x4k7m2@ln.bot

Share it once, and any wallet that supports Lightning addresses can pay it, any amount, any time. It works in a bio, on a website, in a README, or in an agent's tool output.

How it works

Lightning addresses come from outside the Lightning protocol. They're defined by LUD-16, a convention on top of LNURL-pay (LUD-06), and the work is done by a small web service on the address's domain: it hands out a fresh invoice for every payment.

When a wallet is asked to pay x4k7m2@ln.bot:

  1. It splits the address and requests https://ln.bot/.well-known/lnurlp/x4k7m2.

  2. The server answers with what it accepts. Here is what ln.bot's handler returns:

    {
      "tag": "payRequest",
      "callback": "https://ln.bot/.well-known/lnurlp/x4k7m2/callback",
      "minSendable": 1000,
      "maxSendable": 1000000000,
      "metadata": "[[\"text/plain\",\"Sats for x4k7m2 on ln.bot\"],[\"text/identifier\",\"x4k7m2@ln.bot\"]]",
      "commentAllowed": 256
    }
    json

    The amounts are in millisatoshis, so this address takes ₿1 to ₿1,000,000 per payment. metadata is a JSON array sent as a string: LUD-06 requires a text/plain description, and LUD-16 adds a text/identifier entry naming the address. commentAllowed (LUD-12) is how many characters of note the payer may attach.

  3. The wallet shows the domain and the description, and the payer picks an amount within the bounds.

  4. The wallet makes a second GET, to the callback, with the amount in millisatoshis: …/x4k7m2/callback?amount=1000000 for ₿1,000, plus &comment=… if the payer wrote one.

  5. The server creates an invoice for that amount and returns it as {"pr": "lnbc10u1p...", "routes": []}.

  6. The wallet checks that the invoice is for exactly the amount it asked for, then pays it.

Sender's wallet                          ln.bot
      │                                     │
      │  GET /.well-known/lnurlp/x4k7m2 ───▶│
      │◀── { callback, minSendable, … } ────│
      │                                     │
      │  GET callback?amount=1000000 ──────▶│
      │◀── { pr: "lnbc10u1p…", routes } ────│
      │                                     │
      │  pays the invoice over Lightning    │

Two HTTPS requests, then an ordinary Lightning payment. Because the invoice is made per payment, the address never expires, the sender chooses the amount, and every payment gets its own payment hash. Errors come back as {"status": "ERROR", "reason": "..."}, and LNURL clients read that body rather than the HTTP status code (LUD-01).

The client side fits in a function:

// What a wallet does with an address (LUD-16, then LUD-06)
async function invoiceFor(address: string, sats: number): Promise<string> {
  const [name, domain] = address.split("@");
  const res = await fetch(`https://${domain}/.well-known/lnurlp/${name}`);
  const payRequest = await res.json();
  if (payRequest.status === "ERROR") throw new Error(payRequest.reason);

  const msat = sats * 1000;
  if (msat < payRequest.minSendable || msat > payRequest.maxSendable) {
    throw new Error("Amount out of range");
  }

  const callback = new URL(payRequest.callback); // may already have a query
  callback.searchParams.set("amount", String(msat));
  const result = await (await fetch(callback)).json();
  if (result.status === "ERROR") throw new Error(result.reason);

  return result.pr; // BOLT 11: check that its amount is msat before paying
}
typescript

LUD-16 also defines tags: x4k7m2+order1042@ln.bot pays the same wallet, and ln.bot saves the tag on the invoice as its reference, so a single address can tell orders apart. If you pay from an ln.bot wallet, payments.create() does both requests for you:

import { LnBot } from "@lnbot/sdk";

const w = new LnBot({ apiKey: "uk_..." }).wallet("wal_...");

// Paying an address: you pick the amount; ln.bot makes both LNURL
// requests and pays the invoice
await w.payments.create({ target: "r7vq3np@ln.bot", amount: 500 });
typescript

Where to get one

Many Lightning wallets give you an address when you sign up; lightningaddress.com lists providers including Wallet of Satoshi, Strike, Blink, Alby and Coinos. If you already use one of them, look on its receive screen.

On ln.bot, every wallet gets a random address like x4k7m2@ln.bot when it's created. A name you choose costs a one-time fee from the wallet's balance: ₿100 for five characters or more, ₿1,000 for four, ₿10,000 for three.

import { LnBot } from "@lnbot/sdk";

const ln = new LnBot({ apiKey: "uk_..." });

// Every wallet is created with a random address, free
const { walletId, address } = await ln.wallets.create();
console.log(address); // "x4k7m2@ln.bot"

// A name you choose is paid once from the wallet's balance (fund it first):
// ₿100 for five characters or more, ₿1,000 for four, ₿10,000 for three
const w = ln.wallet(walletId);
const vanity = await w.addresses.create({ address: "weatherapi" });
console.log(vanity.cost); // 100
typescript

From the CLI it's lnbot address buy weatherapi, and agents on MCP have the buy_address tool. A bought name can be moved to another wallet later (lnbot address transfer); a random one can't.

Your domain, your address

An address on someone else's domain lasts as long as that provider keeps serving it. For a business, pay@yourdomain.com keeps the address yours even if you change where the money goes.

The server side is two routes. Here is a minimal LNURL-pay server in Express, modeled on ln.bot's own address handler. ln.bot creates the invoices, so there is no node to run, and the server holds no API key:

import express from "express";
import { LnBot } from "@lnbot/sdk";

const DOMAIN = "example.com";
// Each name on your domain, and the ln.bot wallet it pays into
const WALLETS: Record<string, string> = { pay: "wal_..." };
const MIN_SENDABLE = 1_000;         // millisatoshis (₿1)
const MAX_SENDABLE = 1_000_000_000; // ₿1,000,000, as on ln.bot's own addresses

const ln = new LnBot(); // no API key: requesting a wallet's invoice needs none
const app = express();

// LUD-01: browser-based wallets can only call you with this header
app.use((_req, res, next) => {
  res.set("Access-Control-Allow-Origin", "*");
  next();
});

const metadata = (name: string) =>
  JSON.stringify([
    ["text/plain", `Payment to ${name}@${DOMAIN}`],
    ["text/identifier", `${name}@${DOMAIN}`], // required by LUD-16
  ]);

const unknown = { status: "ERROR", reason: "Unknown name" };

// Step 1: a wallet paying pay@example.com asks what you accept
app.get("/.well-known/lnurlp/:name", (req, res) => {
  const name = req.params.name.toLowerCase();
  if (!WALLETS[name]) return res.status(404).json(unknown);

  res.json({
    tag: "payRequest",
    callback: `https://${DOMAIN}/.well-known/lnurlp/${name}/callback`,
    minSendable: MIN_SENDABLE,
    maxSendable: MAX_SENDABLE,
    metadata: metadata(name),
    commentAllowed: 256, // LUD-12: the payer may attach a note
  });
});

// Step 2: GET <callback>?amount=<millisatoshis>[&comment=...] gets an invoice
app.get("/.well-known/lnurlp/:name/callback", async (req, res) => {
  const walletId = WALLETS[req.params.name.toLowerCase()];
  if (!walletId) return res.status(404).json(unknown);

  const msat = Number(req.query.amount);
  if (!Number.isInteger(msat) || msat < MIN_SENDABLE || msat > MAX_SENDABLE) {
    return res.status(400).json({ status: "ERROR", reason: "Amount out of range" });
  }
  // ln.bot invoices are in whole sats, and the wallet checks that the invoice
  // is for exactly the amount it asked for: refuse what can't match, never round
  if (msat % 1000 !== 0) {
    const reason = "Amount must be a whole number of sats";
    return res.status(400).json({ status: "ERROR", reason });
  }

  const { comment } = req.query;
  const note = typeof comment === "string" ? comment.slice(0, 256) : undefined;

  try {
    const invoice = await ln.invoices.createForWallet({
      walletId,
      amount: msat / 1000,
      comment: note,
    });
    res.json({ pr: invoice.bolt11, routes: [] });
  } catch {
    res.status(502).json({ status: "ERROR", reason: "Invoice creation failed" });
  }
});

app.listen(3000);
typescript

Serve it over HTTPS at your domain's root and pay@example.com works in any wallet that pays Lightning addresses. Three details decide whether it does:

  • The text/identifier entry has to name the address the wallet resolved. LUD-16 requires it.
  • Wallets send millisatoshis, and an ln.bot invoice is for a whole number of ₿. Rounding ₿1.5 down to ₿1 would produce an invoice the wallet refuses at step 6, so the callback says no to amounts it can't match exactly. Keep minSendable and maxSendable multiples of 1,000.
  • ln.invoices.createForWallet() needs only the wallet ID, which keeps a server facing the internet free of secrets. ln.bot rate-limits that call by IP address. A server with heavy traffic can create invoices with w.invoices.create() and a key instead, but there are no receive-only keys, so that key could also spend.

Receiving in code

A payment to an ln.bot address settles as an invoice in the wallet, so you can react to it like any other, on the wallet's live event stream:

import { LnBot } from "@lnbot/sdk";

const w = new LnBot({ apiKey: "uk_..." }).wallet("wal_...");
for await (const event of w.events.stream()) {
  if (event.event === "invoice.settled") {
    // reference holds the +tag, if the payer used one
    console.log(event.data.amount, event.data.reference);
  }
}
typescript

A backend that shouldn't hold a connection open can register a webhook instead (lnbot webhook create --url https://example.com/hooks/lnbot), and ln.bot will POST to it when payments arrive. Wallet as a service compares the two for platforms with many wallets.

The privacy tradeoff

Worth understanding before you post an address everywhere.

Paying x4k7m2@ln.bot starts with two HTTPS requests to ln.bot. The host of an address sees every payment made to it: the amount, the time, any comment, and the IP address that asked for the invoice (the sender's device, or the sender's wallet provider when it makes the requests on their behalf). It doesn't learn who the sender is unless the comment or the wallet gives that away.

A custodial host sees more, because it also receives the money: ln.bot, Wallet of Satoshi or Strike sees every payment to the address and the balance they add up to. Hosting the LNURL server yourself, as above, keeps the requests on your server, but when ln.bot creates the invoices, ln.bot still sees each payment. The invoice itself names the node that receives it, so payers learn which provider hosts you.

An address is also a permanent public identifier, and the host can link every payment made to it. It's the deal email offers: easy to share, and the provider that runs it handles everything sent to it. For payments that shouldn't be linked to each other, send a fresh invoice over a private channel such as Signal.

BOLT 12 and BIP 353

Two newer standards cover the same ground without an LNURL server. BOLT 12 offers joined the Lightning specification in September 2024: an offer (lno1...) is a static code that the payer's node uses to request an invoice over Lightning itself, through onion messages, and with blinded paths it can do so without learning the recipient's node. BIP 353, whose status in the bitcoin/bips repository is Complete, puts payment instructions such as an offer in a DNSSEC-signed TXT record at user.user._bitcoin-payment.domain, written ₿user@domain.

The two kinds of name look alike on purpose. BIP 353 keeps the user@domain form for compatibility with existing names, and describes itself as extending and absorbing Lightning Address, partly because a DNS lookup usually doesn't reveal the sender's IP address to the recipient's web server. Phoenix for Android 2.3.3 (July 2024) pays BIP 353 addresses, and given an email-style address it tries DNS first and falls back to LNURL. ln.bot's addresses are LNURL-pay addresses. Before you rely on offers, check that the wallets your payers use can pay them.

FAQ

What is a Lightning address?

An email-style identifier such as x4k7m2@ln.bot that any wallet supporting Lightning addresses can pay, any amount, any time. Behind it, the domain's web server follows LNURL-pay (LUD-16 on top of LUD-06): the paying wallet asks it for an invoice for the chosen amount, then pays that invoice.

Is a Lightning address the same as a Bitcoin address?

No. A Bitcoin address receives on-chain transactions, which wait for block confirmations. A Lightning address points to a web server that issues Lightning invoices, and payments to it settle over Lightning.

How do I get a Lightning address?

Many Lightning wallets give you one, among them Wallet of Satoshi, Strike, Blink and Alby. On ln.bot every wallet gets a random address such as x4k7m2@ln.bot when it is created, and a name you choose costs a one-time ₿100 for five characters or more, ₿1,000 for four or ₿10,000 for three. You can also serve addresses on your own domain.

Can I use my own domain for a Lightning address?

Yes. Serve two HTTPS routes under /.well-known/lnurlp/: one that describes what the address accepts, and a callback that returns an invoice for the amount the wallet asks for. The server can get its invoices from ln.bot, so there is no Lightning node to run.

Are Lightning addresses private?

Less private than invoices. The host of an address sees every payment to it: the amount, the time, any comment and the IP address that requested the invoice. A custodial host also holds the money. Payments to one address are linked to each other; for payments that shouldn't be, share a fresh invoice privately.