A browser tool that decodes a BOLT11 Lightning invoice and shows a developer everything it encodes — with particular attention to Route Hints, and to invoices that are subtly wrong.
Live (once deployed): https://nepet.github.io/ln-invoice-decoder/ — not yet published; see Deploying.
Off-the-shelf BOLT11 decoders are written for payers: they throw on the first problem and give up. This tool is written for the developer staring at a wallet that says "no route" or "invalid invoice" with no further explanation. Paste the invoice, or scan its QR code, and every Tagged Field is decoded and laid out, alongside the Diagnostics the tool has to say about it — including the fields that decoded fine but are still wrong.
It is not a payer. It never asks you for anything a wallet would need to actually pay — no channel state, no balance, no signing key. It reads what the Payee put in the invoice and explains it.
Decoding happens entirely client-side. No request carries any part of the invoice you paste or scan, because the page makes no requests at all — once it has loaded, it never talks to anything again.
This is not a promise you have to take on trust — it is checkable:
-
Read the source. The whole tool ships as a single
index.htmlfile, every script and stylesheet inlined, and it is not minified: View Source shows you the same function names, control flow and layout assrc/, for the tool and for its three dependencies alike. The bundler concatenates and strips comments — for those, readsrc/— but it mangles nothing, so nothing is hiding behind a one-letter identifier. That costs about 170 KB over a minified build, which is the cheaper thing to spend here. -
Grep it yourself. The artifact contains no network primitive at all, not even an unreachable one inside a dependency:
grep -nE 'fetch\(|XMLHttpRequest|WebSocket|sendBeacon|EventSource|importScripts' index.htmlfinds nothing, on the very file you are running.
test/artifact.test.tsasserts the same thing on every build. -
Enforced, not just asserted. That
index.htmlcarries aContent-Security-Policybuilt from a hash of the page's own inline script and style, so the browser refuses any script it did not already hash — the enforcement does not depend on the tool's own code behaving.connect-src 'none'blocks fetch, XHR, WebSocket and beacons outright, anddefault-src 'none'means no subresource of any kind loads. What is permitted is deliberately narrow and local:img-src 'self' data: blob:andmedia-src 'self' blob:, which is what the QR scanner needs to read a camera frame or a screenshot you dropped on the page.scripts/verify-artifact.mjsfails the build if that CSP is missing or weakened, if anything in the page references an externalsrc,href, or CSSurl(), or if any of the network primitives above appears anywhere in it. It is wired intonpm run buildand into.github/workflows/deploy.ymlas the last step before a deploy, so a build that acquired one of them fails there rather than reaching the site. -
Try it yourself. Save the page (
File → Save Page As…once it's live, or runnpm run buildlocally and opendist/index.htmldirectly), turn off your network connection, and open the saved file. Paste an invoice. It still works — because it was never talking to anything.
What the CSP does not do is make the tool trustworthy on someone else's server: it is enforced by your browser against the file you loaded, so it is only worth as much as the file you checked. Save it, read it, and run it from disk if it matters to you.
If you share a link, the invoice travels in the URL fragment
(#lnbc1...), not the query string or path — see
ADR-0003 for why that
distinction matters: browsers never send the fragment to a server, but they do
send the path and query string, and GitHub Pages would log it.
See ADR-0002 for the
single-file, zero-network design this all rests on, and
CONTEXT.md for the domain language used throughout the tool
and this README.
A Route Hint (BOLT11's r Tagged Field) is an ordered list of Hops describing
a path toward the Payee for the invoice's final, unannounced channels. An
invoice can carry several, each an independent alternative — and each Hop's
public key is where that hop begins, never the Payee itself, which is
implied one step past the last Hop.
For each Route Hint, the tool shows every Hop's public key, its Short Channel ID, and its Hop Terms (base fee, proportional fee, CLTV delta), plus the Hint Cost: the total fee and total CLTV delta the whole hint adds to a payment. Hint Cost is derived by the tool, not read from the invoice — it is computed backwards from the Payee, because a Hop's proportional fee applies to everything it forwards, which includes the fees every Hop after it already added. Walking hop-by-hop from the front would get the fee wrong.
Hint Cost is only ever shown when it means something: an invoice with no Amount lets the payer choose how much to pay, so there is nothing to compute a fee against, and the Hint Cost section is hidden entirely rather than showing an estimate the invoice doesn't actually determine.
An Invoice usually carries no key for its Payee, so the Payee is recovered from the signature rather than checked against anything. Recovery succeeds for essentially any well-formed 64-byte signature: it yields a key, not a verdict. Tamper with one word of an invoice and re-checksum it, and this tool — like every other BOLT11 decoder — reports a perfectly good signature belonging to a different Payee.
So the tool never says "signature valid". It says Signature well-formed; payee recovered, and on any invoice without an n Tagged Field it adds an
Info Diagnostic saying exactly this, because the useful check is comparing the
recovered Payee against the one you were expecting. When an invoice does
carry n and it disagrees with the recovered key, that is a Warning.
Everything the tool has to say about an invoice — a decoding failure, a spec violation, or something merely worth noticing — is a Diagnostic, at one of three severities (Error, Warning, Info). Diagnostics that come from a specific source say so, and the tool distinguishes two of them:
- Spec Diagnostics cite a rule: a specific clause of the BOLT11 specification the invoice violates or that the tool is applying.
- Practice Diagnostics name implementations: something the invoice is fully legal per BOLT11, but that real wallets (LND, CLN, LDK, …) are named as actually rejecting or mishandling. These exist because "legal" and "will pay" are different questions, and the gap between them is exactly what sends a developer down a debugging rabbit hole.
(A third class of Info-level Diagnostics — an invoice expired, or carries no expiry so the default applies — is just the tool's own observation, with no rule or implementation to cite.)
Decoding never throws. Every stage — bech32, the 5-bit word stream, the tagged-field walk — returns whatever it managed to read plus the Diagnostics about it, so a broken invoice still shows everything the tool could recover, with the specific point of failure marked rather than a stack trace.
npm ci
npm run dev # local dev server
npm test # vitest
npm run typecheck # tsc --noEmit
npm run build # vite build → dist/index.html, CSP injected and verifiednpm run build produces exactly one file, dist/index.html (~355 KB —
unminified on purpose, see the claim),
and then runs scripts/verify-artifact.mjs against it — the same
check .github/workflows/deploy.yml is
configured to run in CI, after typecheck and test, before every deploy,
so a red test or a network-capable build is designed to never reach the live
site.
This repository is not yet published. .github/workflows/deploy.yml is
committed and ready — it runs typecheck, test, and build before
deploying — but it has not run yet, because the repository has no GitHub
remote. To publish it, the owner runs:
gh repo create nepet/ln-invoice-decoder --public --source=. --remote=origin --push
gh api -X POST repos/nepet/ln-invoice-decoder/pages -f build_type=workflowThat creates the repository, pushes this history, and switches Pages to build from the Actions workflow rather than a branch. The first push triggers the workflow; once it succeeds, the site above is live.
CONTEXT.md— the domain language: Invoice, Payee, Route Hint, Hop, Hint Cost, Tagged Field, Diagnostic, and how they relate.- ADR-0001 — why the decoder is hand-written rather than built on an existing library.
- ADR-0002 — the single-file, zero-network artifact.
- ADR-0003 — why the invoice lives in the URL fragment and never the query string.
MIT — see LICENSE.