PHP Lightning Address

Self-host you@yourdomain.com as a Bitcoin Lightning payment address. A small PHP library that implements LNURL-pay (LUD-06), serves any number of users from one domain, and issues invoices through your own node.

What it is

A Lightning Address looks like an email address and works like one for payments: anyone can type bob@example.com into a wallet and pay you, without QR codes, invoices pasted over chat, or a new invoice for every payment.

Under the hood it is just HTTPS. A wallet turns bob@example.com into a request to https://example.com/.well-known/lnurlp/bob, reads the payment parameters it gets back, and then asks the same server for a bolt11 invoice of the amount the payer chose. This library is that server, written in PHP.

Spec-compliant

Implements LNURL-pay (LUD-06), the format wallets already speak.

Multi-user

One deployment serves many usernames, each mapped to its own invoice backend and API key.

Backend-agnostic

LNbits ships today; other backends are an interface implementation away.

Small and typed

PHP 8.3+, Psalm errorLevel 1 and PHPStan level max on every commit.

Why self-host it

Anatomy of an address

Nothing about a Lightning Address is new protocol — it is a naming convention on top of plain HTTPS. The two halves of the address tell a wallet exactly which URL to open.

The address bob@example.com maps to https://example.com/.well-known/lnurlp/bob bob@example.com your domain https://example.com username → path /.well-known/lnurlp/bob Join the two and you have the URL the wallet opens — this library answers it.

How a payment happens

Two HTTP round trips, then a normal Lightning payment. Step through it:

Sequence diagram of the LNURL-pay flow between the payer's wallet, this library and your node Payer's wallet phone or browser Your domain this library Your node LNbits wallet GET /.well-known/lnurlp/bob pay params: min, max, callback GET /bob?amount=2000 POST /api/v1/payments bolt11 invoice { "pr": "lnbc20n1p…" } ⚡ pays the invoice over Lightning

    Steps 1, 2, 3 and 6 are what this library answers. Step 4 and 5 happen against your own node — the sats in step 7 travel from the payer straight to your wallet, never through the library.

    What travels on the wire

    The pay parameters (step 2), straight from a running instance:

    {
      "callback": "https://example.com",
      "maxSendable": 10000000000,
      "minSendable": 100000,
      "metadata": "[[\"text/plain\",\"Pay to bob@example.com\"], …]",
      "tag": "payRequest",
      "commentAllowed": false
    }

    And the invoice (step 6), with the bolt11 string under pr as LUD-06 requires:

    {
      "pr": "lnbc20n1p…",
      "status": "OK",
      "successAction": { "tag": "message", "message": "Payment received!" },
      "routes": [],
      "disposable": false,
      "error": null
    }

    Quick start

    Requires PHP 8.3 or newer and an LNbits wallet API key.

    composer require php-lightning/lnaddress
    
    cp lightning-config.dist.php lightning-config.php   # settings
    cp backends.dist.json backends.json                 # per-user invoice backends
    
    composer serve                                      # http://localhost:8080

    Settings — lightning-config.php

    use PhpLightning\Config\LightningConfig;
    
    return (new LightningConfig())
        ->setDomain('yourdomain.com')
        ->setReceiver('default-receiver')
        ->setDescriptionTemplate('Pay to %s')                 // %s = the lightning address
        ->setSuccessMessage('Thanks for the payment!')
        ->setSendableRange(min: 100_000, max: 10_000_000_000) // millisats
        ->setCallbackUrl('https://yourdomain.com')
        ->addBackendsFile(getcwd() . '/backends.json');

    Backends — backends.json

    {
      "bob":   { "type": "lnbits", "api_key": "abc...123", "api_endpoint": "http://localhost:5000" },
      "alice": { "type": "lnbits", "api_key": "def...456", "api_endpoint": "http://localhost:5000" }
    }

    Then point /.well-known/lnurlp/{username} at the app's /{username} route, over HTTPS, and the address is live.

    Under the hood

    Built on the Gacela framework as a small modular monolith. Each module exposes a facade; the domain layer is pure PHP that knows nothing about HTTP, which is what makes swapping the invoice backend — or calling the library directly instead of over HTTP — straightforward.

    public/index.php → Router → InvoiceRoutesPlugin
                                  ├─ CorsMiddleware            wallets call cross-origin
                                  ├─ InvoiceExceptionHandler   errors → LNURL error object
                                  └─ InvoiceController
                                       └─ InvoiceFacade
                                            ├─ CallbackUrl       → pay parameters
                                            └─ InvoiceGenerator  → bolt11 invoice
                                                 └─ LnbitsBackendInvoice → your node

    Using it as a library, without the HTTP layer:

    use Gacela\Framework\Gacela;
    use PhpLightning\Invoice\InvoiceFacade;
    
    Gacela::bootstrap(__DIR__);
    
    $facade = new InvoiceFacade();
    $payParams = $facade->getCallbackUrl('bob');
    $invoice = $facade->generateInvoice('bob', 2_000); // millisats

    Documentation

    GuideContents
    Getting started Install, configure, run locally, deploy behind a real domain
    Configuration Every setter, its default, and the backends file format
    HTTP API Routes, payloads, CORS, LNURL error objects, nginx rewrite
    Architecture Modules, layers, request flow, adding a backend
    Development Composer scripts, tests, static analysis, releasing

    FAQ

    Does the library hold my money?

    No. It asks your configured backend for an invoice and returns it. The payment settles between the payer's wallet and your node.

    Do I need LNbits?

    LNbits is the backend implemented today. Adding another means a case in the BackendType enum and an implementation of BackendInvoiceInterface — the rest of the code depends on the interface, not on LNbits.

    Can one domain serve several people?

    Yes. Every entry in backends.json is a username with its own endpoint and API key, so bob@ and alice@ can point at different wallets.

    Does it work in a browser wallet?

    Yes — every response carries permissive CORS headers and OPTIONS preflights are answered directly.

    What is not implemented?

    LNURL comments (LUD-12): commentAllowed is always false.