Skip to main content
Run lnurl-server beside your wallet backend to give your users LNURL-pay and Lightning addresses such as [email protected]. It can also receive Lightning payments into a user’s Arkade wallet while the wallet is offline. This page shows how to deploy the server, connect wallets, add Lightning addresses, and turn on offline receive. The commands target lnurl-server v0.4.0. If you run v0.3, back up its database before you upgrade. On its first start, v0.4.0 converts the database for SDK 0.5, and v0.3 can’t read it afterwards.

Prerequisites

  • Docker and openssl.
  • A domain for Lightning addresses (yourdomain.com below) and a hostname for the server (pay.yourdomain.com), both behind a reverse proxy with TLS.
  • For offline receive: a card for an intent-solver that serves Lightning receive, and the URL of the emulator that the solver uses.

Start the Server

The server stores wallet tokens in its database, encrypted with TOKEN_ENCRYPTION_KEY, a 32-byte key. Generate the key once and keep it with your other secrets: every restart must use the same key.
Check that it runs:
The server answers {"status":"ready",...}. Route https://pay.yourdomain.com to port 3000 through your reverse proxy. The proxy must set X-Forwarded-Proto: https: the server builds its callback URLs from it. Keep the admin port (3001) private: it has no authentication. The command above does not publish it.

Connect a Wallet

A wallet receives through a session: a server-sent events (SSE) stream that the server uses to ask the wallet for invoices. Open every session with the wallet’s own token: a random hex string that the wallet generates once and keeps. It must have an even number of characters, and at least 32. The server derives the session id from the token. The wallet then gets the same LNURL each time it reconnects, and its Lightning address reaches the session. A Lightning address registered from a session opened without a token reports the wallet as offline.
  1. Open the session with POST /lnurl/session and the JSON body { "token": "<wallet-token>" }. Keep the SSE response connected.
  2. Read sessionId and lnurl from the first event, session_created.
  3. Display the LNURL.
  4. On each invoice_request event, read amountMsat, in millisatoshis (divide by 1000 for sats). Create an invoice for that amount with the swap client, as in LNURL and Lightning Addresses. Post { "pr": "<bolt11>" } to /lnurl/session/<sessionId>/invoice with Authorization: Bearer <wallet-token>, within 30 seconds. To decline, post { "error": "<reason>" } instead, with a reason of at most 500 characters.
  5. When the invoice settles, post { "preimage": "<hex>" } to /lnurl/session/<sessionId>/settled with the same header. Close the SSE connection when the receive screen closes.
In TypeScript, @arkade-os/lnurl-client makes these calls for you: openSession, then answerInvoice or rejectInvoice for each request, then reportSettled. It can also derive the token from the wallet key, so the wallet does not have to store it. To check the stream, open a session from a terminal:
The first event is session_created, with the session’s lnurl.

Add a Lightning Address

BOOTSTRAP_DOMAIN creates the domain on first start. Route https://yourdomain.com/.well-known/lnurlp/* to the server, and keep the Host header: the server picks the domain from it. Then register a name with the wallet’s token:
The server answers with "lightningAddress": "[email protected]". While the wallet’s session is connected, each payment to the address sends an invoice_request to the wallet.

Receive While the Wallet Is Offline

When the wallet’s session is down, lnurl-server quotes the receive route itself and claims the payment for the wallet. Keep OFFLINE_SELF_CLAIM on. lnurl-server claims each funded lockup itself, through the covenant’s claim leaf. The claim needs no key. The Arkade operator and the emulator sign the leaf, it is gated on the preimage lnurl-server already holds, and it can pay only the wallet’s address. An unclaimed lockup goes back to the solver at refund_locktime, and the payer is refunded. OFFLINE_EMULATOR_URL must be the solver’s emulator, the EMULATOR_URL the solver runs with, because the covenant commits to its key. Save the solver’s card in solver-cards.json, as a JSON array. The server also reads the published solver registry for the operator’s network, and can use any solver there whose emulator matches OFFLINE_EMULATOR_URL. To use only the card you saved, add -e SOLVER_REGISTRY_URLS= to the command below. Recreate the container with the offline settings:
Replace https://emulator.yoursolver.com with the solver’s emulator URL. /readyz answers ready even when no card can serve Lightning receive. Check components.solverDiscovery in its response: ok is true when at least one card can. Register the Arkade address and the claim public key that the wallet owns, for each Lightning address:
claimPublicKey is a compressed public key: 02 or 03 followed by 64 hex characters. For each payment, the service generates the preimage, selects the solver, returns its hold invoice, and tracks settlement. The claim pays only the Arkade address committed in the covenant. A wallet can register a public key branch instead, so that each payment gets its own Arkade address and claim key. An address in this HD mode never uses the wallet’s session, cannot go back to a single Arkade address, and leaves the wallet to restore and watch every payout. See HD receiving.
Test one funded receive before production. Keep lnurl-server and its SQLite database running until every accepted invoice settles or expires, because lnurl-server claims each lockup.

lnurl-server operations

Production Compose file, reverse proxy, backups, restore, and upgrades.