A payment gateway with no middleman you have to trust. Sats land straight in your own wallet, all it takes is one package and six calls.
A normal payment processor can throw you out over your age, your country or a line of business the regulator dislikes this season. Running your own Lightning node is the other extreme, because channels, liquidity and backups are a full-time job nobody wants before knowing whether the idea even breathes.
Thunder Bridge is the shortcut in between. The payer pays an invoice your own wallet issued, so the gateway never holds your sats and has nothing to take. Once the payment lands, your wallet releases the preimage, and that is the proof the money actually moved. Not an operator's promise, maths. 🧡
Nothing to run, nothing to back up. The wallet you already have is the whole infrastructure.
Sats go straight into your wallet. They never pass through the gateway, not even for a second.
Nobody holds your money, so there is no cut to take.
All of it rests on a Lightning address whose server speaks LUD-21, because that is what releases the preimage once someone pays. Blink does, and it takes two minutes to set up.
Leave the custodial version alone. Keys somebody else holds are somebody else's money. Blink also stops payments on custodial accounts after 31 August 2026, so it is a dead end in practice too. The non-custodial account runs on Spark, with an exit to the chain, and the name@blink.sv address stays the same.
The wallet has to speak LUD-21, otherwise there is nothing to prove the payment arrived. These were measured doing it, ranked by how much custody stays with you.
It does not work with Wallet of Satoshi, Strike, Cash App, ZBD, Primal, Fountain or LNbits. ZEUS Pay does serve verify but releases no preimage, which amounts to the same thing. Minibits and Ark do speak LUD-21, but they never pin the invoice to their metadata, so the gateway refuses them as well. Support belongs to the address domain rather than the app, so when in doubt, paste the address into the demo below. It is refused as the invoice is created, not after somebody has already paid.
Put in your address, pick an amount and scan the QR with a wallet. The invoice comes from your own wallet, the gateway is handed only its hash and a URL to poll, and your browser then checks the preimage itself.
This works better on a desktop: the screen shows the QR and you pay it with your phone. On a phone you have nothing to scan your own display with, so it takes two devices.
Leave it empty and the sats go to iamfatik@blink.sv. Most wallets refuse to pay their own invoice, so scan from a wallet other than the one behind the address.
sha256(preimage) has to be that payment hash, or it proves nothing
The client is on npm as thunder-bridge. It touches only fetch, crypto.subtle, URL and WebSocket, so it runs in a browser, on Node 22 and up, on Bun, on Deno and on Cloudflare Workers. The other half, thunder-bridge/server, takes the invoice from the wallet and needs node:dns for it, so it belongs on a server and will not run on Workers.
npm install thunder-bridge
import { ThunderBridge } from "thunder-bridge";
import { invoiceFrom, relayedVerifyUrl } from "thunder-bridge/server";
const gateway = new ThunderBridge("https://thunder-bridge-production.up.railway.app", {
secret: process.env.RAIL_SECRET,
});
const invoice = await invoiceFrom(["you@blink.sv"], 21_000);
const watched = await gateway.watchPayment({
paymentHash: invoice.paymentHash,
verifyUrl: await relayedVerifyUrl(
"https://your.site/verify/lightning",
{ url: invoice.verifyUrl, hash: invoice.paymentHash },
process.env.RAIL_SECRET,
),
expiresAt: Math.min(invoice.expiresAt, Math.floor(Date.now() / 1000) + 3600),
});
return Response.json({ id: watched.id, bolt11: invoice.bolt11 });import { ThunderBridge, invoiceToSvg } from "thunder-bridge";
const gateway = new ThunderBridge("https://thunder-bridge-production.up.railway.app");
const { id, bolt11 } = await (await fetch("/api/invoice", { method: "POST" })).json();
document.querySelector("#qr").innerHTML = invoiceToSvg(bolt11);
const settled = await gateway.waitForWatched(id);Your server resolves the address, the wallet issues the invoice, and the gateway is handed only a payment hash and a URL to poll. That URL is yours, not the wallet's: relayedVerifyUrl seals the wallet's into it, so the gateway does not even learn the host. Cap the expiry, it watches for three days at most and wallets write longer ones. The browser then waits on a socket with no secret at all, and waitForWatched returns paid only once the preimage hashes to that payment hash, otherwise it throws GatewayCheatError.
import {
answerWebhookChallengeRequest,
isProvablySettled,
parseSettlementRequest,
} from "thunder-bridge";
const signs = { publicKey: await gateway.webhookKey() };
export async function POST(request) {
const challenge = await answerWebhookChallengeRequest(request, signs);
if (challenge) return challenge;
const settled = await parseSettlementRequest(request, signs);
if (!settled) return new Response("bad signature", { status: 401 });
if (!isProvablySettled(settled)) return new Response("no preimage", { status: 202 });
unlock(settled.id);
return new Response("ok");
}The gateway signs every delivery with the key it publishes at /webhook-key, and there is no shared secret to register, so it holds nothing of yours. It challenges your URL before it takes the payment on and refuses it if nothing answers, so deploy the handler first and register it second. Delivery is at-least-once, so deduplicate on id.
Above is one payment from end to end. These three are not on this page, and they are the more interesting ones.
A trigger is a place rather than a payment. Every payment carrying the same secret belongs to it, so one QR on the wall and a process that reacts to each settled invoice. It holds a socket, so it belongs on something that stays up.
A whole lightning address as one fetch handler. A static QR points at your own domain instead of the gateway, nothing is stored, and it runs on Deno Deploy or Cloudflare Workers.
Asks which address on your list would take an amount and mints nothing doing it. A probe, not a promise, because whether a wallet issues a provable invoice is only knowable by asking for one.
The trigger and the address both have a working example in the repo, Deno Deploy for the address and the webhook, a watcher for the trigger.
The gateway and the example are both public on GitHub and deploy from the browser, no terminal and nothing to install.
The template starts the container from the repo, mounts the disk and generates the cluster key itself, so you fill in nothing. Just save that key once it is up, because without it you never get back into your own cluster.
Deploy the gatewayIt clones examples/deno-deploy into a repo of your own and deploys it, a paywall and a lightning address in one handler. You fill in one variable, LN_ADDRESSES, and the rest is generated at boot.
Deploy the exampleBoth then run on your account and your keys. The gateway behind the demo above is mine and it guarantees you nothing.
So it is clear what you are buying.