Lightning Network L402 Paywall: 12 Steps, 90 Min [2026] Lightning Labs released Aperture v0.6.0 on November 12, 2026, alongside pi402, a tool that lets coding agents pay for AI inference in bitcoin, according to Lightning News. Aperture is a reverse proxy that answers unpaid API requests with HTTP 402 and a Lightning invoice, then validates the L402 macaroon-and-preimage pair before forwarding paid requests to a backend; Lightning Labs formalized the protocol as bLIP-0026 and said on March 11, 2026 that L402 was production-ready. A tutorial lab using two LND nodes, a Bitcoin Core 32.1 backend, a Python API on port 8000 and Aperture on port 8081 charges 25 sats per token on a private regtest network and takes about 90 minutes to build. Lightning Labs announced Aperture v0.6.0 on November 12, 2026, alongside pi402, a tool that lets coding agents pay for AI inference in bitcoin, according to Lightning News coverage of the launch https://lightning.news/pi402/ . Aperture is the seller-side piece. It sits in front of your API, answers unpaid requests with HTTP 402 and a Lightning invoice, and lets paid requests through. Put one in front of a plain Python API on a private regtest network, charge 25 sats per token, and pay it from a second node. Regtest keeps the stakes at zero. You mine blocks on demand, open channels in seconds, and break the paywall as often as you like before a real satoshi moves. If you have never run a node, setting up your own LND node https://shattered.io/bitcoin-lightning-node-lnd-setup-2026/ first will make the lab feel familiar, though the scripts below build everything from scratch. The finished lab has two LND nodes a seller called alice and a buyer called bob , a Bitcoin Core 32.1 https://shattered.io/utxo-management-bitcoin-core-32-1-psbt-v2-12-steps-2026/ backend, a Python API on port 8000, Aperture on port 8081, and a client script that pays invoices on its own, caps what it will spend, and reuses tokens. Expect about 90 minutes if the downloads cooperate. Every step lists the output you should see, so you can stop at the first mismatch instead of debugging five layers at once. Don't miss new tech stories on Google Add Shattered once in the Google app and our stories show up in your news suggestions. Add now https://www.google.com/preferences/source?hl=en&q=https%3A%2F%2Fshattered.io%2F How an L402 Paywall Works on the Lightning Network L402 turns a Lightning payment into an API key. A client asks for a resource with no credentials, the server replies with HTTP 402 Payment Required, and the reply carries two things in a WWW-Authenticate header: a token and a Lightning invoice. The token commits to the invoice through its payment hash. The client pays, receives the payment preimage as proof, and retries the request with the token and the preimage together. Only that pair is valid, which means nobody can forge access without paying. The official L402 documentation describes the token format as a macaroon, a bearer credential that can carry permissions and can be attenuated and delegated by whoever holds it. Verification needs only a root key and basic cryptography, so the server does not look up a payment record to authorize a call. Lightning Labs formalized the protocol as bLIP-0026 and said on March 11, 2026 that L402 was production-ready. The company also said Lightning Loop has used L402 through Aperture since its first release, roughly five years earlier. Aperture is the piece that does the work. Lightning Labs describes it in its builder documentation as “a reverse proxy that acts as a payment and authentication gateway for Lightning Network powered APIs.” Your backend never touches Lightning. Aperture mints the macaroons, asks LND for invoices, checks credentials, and forwards good requests to your service. A write-up of the pi402 launch https://automater.ai/news/lightning-labs-pi402-bitcoin-inference-payments/ summarizes the same loop: an unpaid request gets a 402, an incomplete credential, and an invoice, and paying reveals the preimage that completes the credential. | Stage | Who acts | What happens | |---|---|---| | 1. Request | Client | Sends GET with no Authorization header | | 2. Challenge | Aperture | Returns 402 plus a WWW-Authenticate header holding a macaroon and a BOLT 11 invoice | | 3. Payment | Client wallet | Pays the invoice over Lightning and receives the preimage | | 4. Retry | Client | Sends Authorization: L402 with the macaroon and preimage joined by a colon | | 5. Proxy | Aperture | Validates the pair, then forwards the request to your backend | | 6. Reuse | Client | Keeps the token and skips payment on later calls to the same service | Compare that with a classic API key. There is no signup form, no billing portal, no card on file, and no chargeback. A script with a funded wallet can buy access to a service it has never seen, which is why Lightning Labs keeps pairing L402 with agent tooling. The same property makes the paywall useful for plain human developers who want to sell a data feed by the request without building an account system. One naming wrinkle trips people up. Older posts and some Aperture log lines use LSAT, the protocol’s earlier name. Treat LSAT and L402 as the same thing in historical material, and write L402 in anything you build today. Header parsing in Step 8 accepts the current name only, which matches what Aperture v0.6.0 documents. Prerequisites and Version Check Pin versions before you type a command, because Lightning guides age fast. LND is still labeled beta, and its repository’s releases page listed v0.21.8-beta as the latest release as of November 14, 2026. Anything that tells you to run v0.21.6-beta or earlier predates the newer releases. For Bitcoin Core, check the version against its own release page instead of trusting a guide. | Component | Version for this lab | Date | Role | |---|---|---|---| | Bitcoin Core | 32.1 | Oct. 27, 2026 | Regtest chain backend for both LND nodes | | LND lnd and lncli | v0.21.8-beta | Nov. 11, 2026 | Lightning nodes for seller and buyer | | Aperture | v0.6.0 | Announced Nov. 12, 2026 | L402 reverse proxy in front of the API | | Go | Release current on your machine, docs floor is 1.19 | n/a | Builds Aperture from source | | Python | 3.10 or later | n/a | Backend server and the paying client | | curl and jq | Any recent release | n/a | Inspecting headers and JSON output | You need a shell and free local ports for the backend, both Lightning nodes, and Aperture. If a port is taken, change it in the script and in the YAML in the same edit. Two ports collide most often: Aperture documents 8081 as its default, and many dev boxes already run something on 8080. Install Bitcoin Core 32.1 from the official release page, and verify the checksum and signatures as the release notes describe. Do the same for the LND v0.21.8-beta tarball for your platform, then copy lnd and lncli onto your PATH. Check both with their version flags before moving on. If you want a longer walkthrough of a production-style node, the guide to setting up a Lightning Network node https://shattered.io/lightning-network-node-setup-2026/ covers Docker, backups, and monitoring in depth. bitcoind --version | head -1 lnd --version lncli --version go version python3 --version jq --version Expected output looks like the lines below. Your commit hashes and build tags will differ slightly, and that is fine as long as the version numbers match. Bitcoin Core daemon version v32.1.0 lnd version 0.21.8-beta commit=v0.21.8-beta lncli version 0.21.8-beta commit=v0.21.8-beta go version go1.2x linux/amd64 Python 3.12.x jq-1.7 Lab only: the scripts start LND with the no-seed-backup flag so nobody has to write down a mnemonic for coins that have no value. That flag stores the wallet unencrypted. Never copy it to a mainnet node. Step 12 lists what changes when real money is involved. Build the Regtest Lab: Steps 1 to 4 The lab needs a chain, two funded nodes, and a channel between them with liquidity on the right side. The buyer opens the channel, which gives the seller inbound capacity. That detail matters later, because a seller with zero inbound liquidity can mint invoices all day and never get paid. Step 1: Create the Lab Folder and Shell Helpers Put every path and helper function in one file so each later command stays short. Shell functions work in scripts, which aliases do not, so the helpers below are functions. mkdir -p ~/l402-lab && cd ~/l402-lab cat env.sh <<'EOF' export LAB="$HOME/l402-lab" export PATH="$PATH:$ go env GOPATH /bin" btc { bitcoin-cli -regtest -datadir="$LAB/bitcoin" -rpcuser=lab -rpcpassword=labpass "$@"; } alice { lncli --lnddir="$LAB/alice" --rpcserver=localhost:10009 --network=regtest "$@"; } bob { lncli --lnddir="$LAB/bob" --rpcserver=localhost:10010 --network=regtest "$@"; } mine { btc -rpcwallet=miner generatetoaddress "$1" "$ btc -rpcwallet=miner getnewaddress " /dev/null; } EOF source env.sh type alice The last command should print alice is a function followed by its body. If your shell reports not found , the file did not load, so source it again from the lab directory. Step 2: Start Bitcoin Core 32.1 in Regtest and Mine Coins LND talks to Bitcoin Core over RPC for blocks and over ZeroMQ for fast notifications, so both ZMQ endpoints must be set when bitcoind starts. The transaction index is optional for LND but harmless here. Creating a wallet named miner and mining 101 blocks matures the first coinbase reward, because regtest coinbase outputs need 100 confirmations before they can be spent. source env.sh mkdir -p "$LAB/bitcoin" bitcoind -regtest -daemon -datadir="$LAB/bitcoin" -server=1 -txindex=1 \ -rpcuser=lab -rpcpassword=labpass -fallbackfee=0.0002 \ -zmqpubrawblock=tcp://127.0.0.1:28332 \ -zmqpubrawtx=tcp://127.0.0.1:28333 sleep 3 btc -named createwallet wallet name=miner mine 101 btc -rpcwallet=miner getbalance btc getblockcount { "name": "miner" } 50.00000000 101 Step 3: Start Two LND Nodes Named Alice and Bob Alice sells, Bob buys. Each node gets its own directory, peer port, gRPC port, and REST port. Keep 8081 free for Aperture, since that is its documented default, and give each node’s REST port a different value. For a deeper look at the flags a long-running node needs, the Bitcoin Lightning node setup guide https://shattered.io/bitcoin-lightning-node-setup-2026/ explains each one. source env.sh start lnd { name p2p rpc rest mkdir -p "$LAB/$1" nohup lnd --lnddir="$LAB/$1" --noseedbackup --alias="$1" \ --listen=127.0.0.1:$2 --rpclisten=127.0.0.1:$3 --restlisten=127.0.0.1:$4 \ --bitcoin.active --bitcoin.regtest --bitcoin.node=bitcoind \ --bitcoind.rpchost=127.0.0.1:18443 \ --bitcoind.rpcuser=lab --bitcoind.rpcpass=labpass \ --bitcoind.zmqpubrawblock=tcp://127.0.0.1:28332 \ --bitcoind.zmqpubrawtx=tcp://127.0.0.1:28333 \ "$LAB/$1.log" 2 &1 & } start lnd alice 9735 10009 8080 start lnd bob 9736 10010 8082 sleep 10 alice getinfo | jq '{alias, version, synced to chain}' bob getinfo | jq '{alias, version, synced to chain}' { "alias": "alice", "version": "0.21.8-beta commit=v0.21.8-beta", "synced to chain": true } { "alias": "bob", "version": "0.21.8-beta commit=v0.21.8-beta", "synced to chain": true } If getinfo complains that the macaroon file does not exist, LND is still creating the wallet. Wait ten seconds and run it again. If synced to chain stays false for more than a minute, read the log at $LAB/alice.log and check the ZMQ ports first. Step 4: Fund Bob and Open a Channel to Alice Send one regtest bitcoin to Bob’s on-chain wallet, confirm it, connect the peers, and open a channel of 1,000,000 sats. All of Bob’s balance starts on his side of the channel. Once the channel is active, Bob can pay Alice up to the channel’s capacity minus reserves, and Alice has the same amount as inbound liquidity. Channel liquidity is the main operating cost on mainnet too, and techniques such as channel splicing https://shattered.io/lightning-network-splicing-2026/ let you resize a channel without closing it. source env.sh BOB ADDR=$ bob newaddress p2tr | jq -r .address btc -rpcwallet=miner sendtoaddress "$BOB ADDR" 1 mine 6 bob walletbalance | jq -r .confirmed balance ALICE PUB=$ alice getinfo | jq -r .identity pubkey bob connect " email protected :9735" bob openchannel --node key="$ALICE PUB" --local amt=1000000 mine 6 sleep 5 bob listchannels | jq '.channels | {active, capacity, local balance, remote balance}' 100000000 { "funding txid": "9c1e...e47a", "output index": 0 } { "active": true, "capacity": "1000000", "local balance": "992950", "remote balance": "0" } Your local balance will differ by a few thousand sats because the channel reserves part of the capacity for commitment fees. What matters is active: true . If it prints false, mine six more blocks and ask again. The lab now has everything a real Lightning seller and buyer would have, minus the real money. Install and Configure Aperture: Steps 5 to 7 With the Lightning side running, the seller needs a gateway and something worth selling. Aperture builds from source with the Go toolchain, reads one YAML file, and talks to Alice’s LND over gRPC to create invoices. Step 5: Build Aperture v0.6.0 From Source The official install instructions are three commands: clone the repository, enter it, and run make install . Check out the v0.6.0 tag first so you build the release and not whatever sits on the main branch today. The docs list Go 1.19 or later as the floor, but read go.mod in the tag you check out and use a Go release at least that new. source env.sh git clone https://github.com/lightninglabs/aperture.git "$LAB/aperture-src" cd "$LAB/aperture-src" git checkout v0.6.0 make install which aperture HEAD is now at 3f9a2c1 Merge pull request ... from lightninglabs/v0.6.0 /home/you/go/bin/aperture The commit hash in your output will not match the sample. If which aperture prints nothing, $ go env GOPATH /bin is not on your PATH. The env file from Step 1 adds it, so source that file again in the same shell. Step 6: Write aperture.yaml Aperture looks for its configuration at ~/.aperture/aperture.yaml . The structure below follows the sample in the official step-by-step guide: a listen address, an authenticator block that points at Alice’s LND, a database backend, and a list of services. Use absolute paths everywhere. YAML does not expand a tilde or $HOME , and a path that looks right but does not resolve is the most common reason Aperture fails on its first start. Replace YOU with your username. mkdir -p ~/.aperture cat ~/.aperture/aperture.yaml <<'EOF' listenaddr: "127.0.0.1:8081" debuglevel: "debug" dbbackend: "sqlite" authenticator: network: "regtest" disable: false lndhost: "127.0.0.1:10009" tlspath: "/home/YOU/l402-lab/alice/tls.cert" macdir: "/home/YOU/l402-lab/alice/data/chain/bitcoin/regtest" services: - name: "labapi" hostregexp: '^localhost. $' pathregexp: '^/service. $' address: "127.0.0.1:8000" protocol: http timeout: 3600 price: 25 authwhitelistpaths: - '^/service/free/. $' EOF Each key earns its place. The authenticator block tells Aperture which LND node mints the invoices, so Alice’s TLS certificate and macaroon directory must be real. The hostregexp matches the Host header, which is why the test URLs in this lab say localhost and not 127.0.0.1 . The pathregexp decides which paths belong to this service. The address is your backend. The price is in sats, and the official docs note that a valid token for a service unlocks unlimited resources on that service. The authwhitelistpaths list carves out a free tier. Step 7: Start the Backend and Aperture, Then See the 402 Create a tiny backend with one paid file and one free file, bind it to localhost only, and start Aperture in a second terminal. Binding the backend to 127.0.0.1 matters more than it looks. If the backend listens on all interfaces, anyone who finds port 8000 walks around the paywall. terminal 1: the backend source ~/l402-lab/env.sh mkdir -p "$LAB/backend/service/free" && cd "$LAB/backend" echo '{"pair":"BTCUSD","note":"paid data, regtest"}' service/data.json echo '{"status":"free tier ok"}' service/free/status.json python3 -m http.server 8000 --bind 127.0.0.1 terminal 2: the gateway source ~/l402-lab/env.sh aperture Aperture generates a self-signed TLS key and certificate on first run, per its install guide, so use https and tell curl to accept the certificate with -k . Production deployments need a valid certificate for your domain. Now send an unauthenticated request in a third terminal. curl -sik https://localhost:8081/service/data.json HTTP/2 402 www-authenticate: L402 macaroon="AgEEbHNhdAJCAAAAAAAAAAAAAAAAAAAAAAAA...", invoice="lnbcrt250n1pn4...x9qy" content-type: text/plain; charset=utf-8 payment required That reply is the whole product. The invoice prefix lnbcrt250n means regtest and 250 nanobitcoin, which equals 25 sats, exactly the price in the YAML. If you request /service/free/status.json instead, you should get a plain 200 with the free JSON body, because the whitelist pattern matches it. Pay the Invoice and Fetch the Data: Steps 8 and 9 A payment by hand teaches the protocol faster than any diagram. Do it once manually, then let a script do it every time after. Step 8: Pay by Hand and Replay With the L402 Header Pull the macaroon and invoice out of the header, pay the invoice from Bob, read the preimage from his payment history, and retry with Authorization: L402 macaroon:preimage . Two details deserve care. HTTP/2 lowercases header names, so match case-insensitively. Aperture v0.6.0 can advertise both L402 and MPP, which means the response may carry more than one WWW-Authenticate line, so filter for the one that starts with L402. source ~/l402-lab/env.sh HDR=$ curl -sik https://localhost:8081/service/data.json \ | tr -d '\r' | grep -i '^www-authenticate: L402' MAC=$ echo "$HDR" | sed -n 's/. macaroon="\ ^" \ ". /\1/p' INV=$ echo "$HDR" | sed -n 's/. invoice="\ ^" \ ". /\1/p' bob payinvoice --force "$INV" PRE=$ bob listpayments --reversed --max payments=1 | jq -r '.payments 0 .payment preimage' curl -sk -H "Authorization: L402 $MAC:$PRE" https://localhost:8081/service/data.json +------------+--------------+--------------+--------------+-----+----------+-------+--------+ | HTLC STATE | ATTEMPT TIME | RESOLVE TIME | RECEIVER AMT | FEE | TIMELOCK | CHAN | ROUTE | +------------+--------------+--------------+--------------+-----+----------+-------+--------+ | SUCCEEDED | 0.139 | 0.402 | 25 | 0 | 1131 | ... | ... | +------------+--------------+--------------+--------------+-----+----------+-------+--------+ Amount + fee: 25 + 0 sat Payment status: SUCCEEDED, preimage: 7d3a...c1f0 {"pair":"BTCUSD","note":"paid data, regtest"} Repeat only the last curl and you get the data again with no second payment. The token already proves the preimage. Change a single character of the preimage and the request goes back to a 402, which confirms the gateway verifies the pair and not just the presence of a header. Step 9: Write a Python Client That Pays Within a Budget A script that pays any invoice a server sends is a wallet-draining bug waiting to happen. The client below decodes the invoice first, refuses anything above a spending cap, pays through Bob, checks that the SHA-256 https://shattered.io/category/cryptography/ of the preimage equals the invoice’s payment hash, and caches the token for later calls. It shells out to lncli so it needs no extra dependencies. python /usr/bin/env python3 import hashlib, json, os, re, ssl, subprocess, sys import urllib.error, urllib.parse, urllib.request LAB = os.path.expanduser "~/l402-lab" LNCLI = "lncli", f"--lnddir={LAB}/bob", "--rpcserver=localhost:10010", "--network=regtest" TOKENS = os.path.join LAB, "tokens.json" MAX SATS = int os.environ.get "MAX SATS", "100" CTX = ssl.create default context CTX.check hostname = False lab only: Aperture uses a self-signed cert CTX.verify mode = ssl.CERT NONE def lncli args : out = subprocess.run LNCLI + list args , capture output=True, text=True, check=True .stdout return json.loads out if out.lstrip .startswith "{", " " else out def fetch url, token=None : req = urllib.request.Request url if token: req.add header "Authorization", f"L402 {token}" try: with urllib.request.urlopen req, context=CTX as r: return r.status, r.headers.get all "WWW-Authenticate" or , r.read except urllib.error.HTTPError as e: return e.code, e.headers.get all "WWW-Authenticate" or , e.read def cache key url : u = urllib.parse.urlparse url first = u.path.strip "/" .split "/" 0 return f"{u.netloc}/{first}" def load : return json.load open TOKENS if os.path.exists TOKENS else {} def pay challenge : mac = re.search r'macaroon=" ^" + "', challenge .group 1 inv = re.search r'invoice=" ^" + "', challenge .group 1 dec = lncli "decodepayreq", inv sats = int dec "num satoshis" if sats MAX SATS: sys.exit f"refusing to pay {sats} sats, cap is {MAX SATS}" lncli "payinvoice", "--force", inv pre = lncli "listpayments", "--reversed", "--max payments=1" "payments" 0 "payment preimage" if hashlib.sha256 bytes.fromhex pre .hexdigest = dec "payment hash" : sys.exit "preimage does not match the invoice payment hash" print f"paid {sats} sats", file=sys.stderr return f"{mac}:{pre}" def main url : tokens = load key = cache key url status, auth, body = fetch url, tokens.get key if status == 402: challenge = next h for h in auth if h.strip .upper .startswith "L402" , None if not challenge: sys.exit "402 without an L402 challenge" tokens key = pay challenge json.dump tokens, open TOKENS, "w" status, auth, body = fetch url, tokens key if status = 200: sys.exit f"unexpected status {status}" sys.stdout.write body.decode if name == " main ": main sys.argv 1 Save it as l402 client.py in the lab folder. Run it twice against the paid path. The first run pays 25 sats and prints the JSON. The second run finds the cached token and prints the JSON with no payment line. python3 l402 client.py https://localhost:8081/service/data.json python3 l402 client.py https://localhost:8081/service/data.json paid 25 sats {"pair":"BTCUSD","note":"paid data, regtest"} {"pair":"BTCUSD","note":"paid data, regtest"} Now test the spending cap. Delete tokens.json and rerun the client with MAX SATS=10 in front of it. The script prints refusing to pay 25 sats, cap is 10 and exits with a non-zero code before any payment leaves Bob’s node, which is the behavior you want from an unattended agent. The token cache is keyed by host and first path segment, so one payment covers every file under /service , matching the way Aperture treats a valid token as access to the whole service. Pricing, Free Routes, and a Test Script: Steps 10 and 11 Step 10: Choose a Pricing Model A fixed price per service is the simplest model and the one the lab uses. Aperture offers more. The pricing guide documents a dynamicprice block that points at a gRPC price server, and Aperture v0.6.0 added opt-in metered pricing, where one payment buys a usage bundle that real traffic then draws down. A static price of 0 does not make an endpoint free. The v0.6.0 notes say it falls back to a default of 1 sat, and a genuinely free endpoint uses auth: "off" or a whitelist path. | Model | How you configure it | Best for | |---|---|---| | Fixed price | price: 25 on the service | Flat-fee data files, one-off reports, first prototypes | | Free paths | authwhitelistpaths with regex patterns | Health checks, docs, a free sample to hook buyers | | Free service | auth: "off" on the service | Routes that must never return 402 | | Dynamic price | dynamicprice with enabled , grpcaddress , insecure , tlscertpath | Per-resource prices computed by your own gRPC server | | Metered bundles | Opt-in metering with the meterd reference price server v0.6.0 | Token-based AI inference and streamed responses | To try a second price tier, add another entry under services with its own name, pathregexp , and price . Aperture matches a request to a service by host and path, so give each pattern a distinct prefix such as /premium . Restart Aperture after every YAML edit, because it reads the file once at startup. Step 11: Automate the Check With a Test Script A paywall that silently stops charging is worse than one that is down. The script below asserts four behaviors: unpaid requests get a 402, the free path stays open, the first paid call costs exactly one payment, and the second call costs none. Run it after every config change. bash /usr/bin/env bash set -euo pipefail HERE="$ cd "$ dirname "$0" " && pwd " source "$HERE/env.sh" BASE=https://localhost:8081 fail { echo "FAIL: $ "; exit 1; } code { curl -sk -o /dev/null -w '%{http code}' "$@"; } count { bob listpayments | jq '.payments | length'; } rm -f "$LAB/tokens.json" "$ code $BASE/service/data.json " = 402 || fail "unpaid request should return 402" "$ code $BASE/service/free/status.json " = 200 || fail "free path should return 200" before=$ count python3 "$HERE/l402 client.py" $BASE/service/data.json /dev/null 2 &1 mid=$ count $ mid - before -eq 1 || fail "first call should cost exactly one payment" python3 "$HERE/l402 client.py" $BASE/service/data.json /dev/null 2 &1 after=$ count "$after" -eq "$mid" || fail "second call should reuse the token" echo "PASS: 402, free path, one payment, token reuse" chmod +x test.sh && ./test.sh PASS: 402, free path, one payment, token reuse Harden the Seller for Mainnet: Step 12 Step 12: Replace Every Lab Shortcut The lab made four shortcuts on purpose. Each one needs a real replacement before a customer pays you a satoshi. First, drop --noseedbackup , create the wallet with a seed and a password, and write the mnemonic down offline. Second, replace the self-signed certificate with a valid one for your domain and serve Aperture on 443. Third, move the macaroon directory out of reach. Aperture needs only enough authority to create and look up invoices, so point it at a baked macaroon with those permissions instead of the admin file. Fourth, firewall the backend so only Aperture can reach it. Patch discipline matters more on a node that holds channel funds. LND’s v0.21.6-beta release on October 30, 2026 disabled MPTCP listeners by default after an exploit path linked to CVE-2026-98432 was demonstrated. The release notes call that a mitigation and not a substitute for a Linux kernel carrying the upstream fix, so patch the kernel too. Earlier in the year, a Lightning Labs advisory dated August 11, 2026 described an HTLC sweep failure tied to a wallet budget constraint https://security.lightning.engineering/2026/08/11/lnd-htlc-sweep-budget-failure.html , fixed in v0.19.0-beta. Run the newest release, v0.21.8-beta at the time of writing, and treat any older number in a guide as a warning sign. A repository change requiring TLS 1.2 as Aperture’s minimum version was visible on GitHub in early November 2026, though it had not been confirmed as part of a release by November 15. Put Aperture behind a reverse proxy or load balancer that enforces modern TLS regardless. For a repeatable routine that checks your node against current advisories, the Lightning Network upgrade audit https://shattered.io/lightning-network-upgrade-audit-12-steps-2026/ turns patch checks into a script. - Create the mainnet wallet with a seed and password, and back up the channel state file after every channel change. - Use an Aperture-specific macaroon with invoice permissions only, never admin.macaroon . - Serve Aperture on 443 with a certificate for your domain, and keep the backend on a private interface. - Open a channel from your side only after you understand inbound liquidity, since buyers pay you through inbound capacity. - Set a price that clears routing fees. A 1-sat price can lose money on a long route. - Log every 402 and every paid request so you can reconcile invoices against traffic. Advanced Tips: Metering, MPP, and Wavelength-Backed Sellers Aperture v0.6.0 changes what a seller can do. Lightning Labs said it adds MPP support alongside L402, so a service can advertise both and let the buyer pick the protocol it speaks. It also adds real-time per-request accounting and an embedded meterd price server that quotes bundles and deducts token usage. A client buys credit once, each response debits it, and a new 402 arrives when the balance runs out. Fixed-price paywalls charge for access. Metered ones charge for consumption. The biggest structural change is that a seller no longer needs LND. Per the release description, Aperture v0.6.0 can connect directly to a Wavelength wallet, so a seller can issue invoices without running a node, managing channels, or sourcing inbound liquidity. An EL7 report on the launch https://el7.ai/en/news/2026-11-13-lightning-labs-launches-bitcoin-payment-tools-for-autonomous-ai-agents repeats that point. The setting is called wavelengthgateway , and you should read the v0.6.0 release notes for its exact syntax before using it, since this lab does not exercise it. | Factor | LND-backed seller this lab | Wavelength-backed seller | |---|---|---| | Runs an LND node | Yes | No, per the v0.6.0 release description | | Channels and inbound liquidity | You manage both | Not required | | Fees | Routing fees on each payment | 0.25% service fee for sending and receiving, plus ordinary routing fees | | Published limits | Your channel capacity | Accounts up to 300,000 sats, each VTXO up to 50,000 sats | | Status | Established path, works on any network | Mainnet launch announced Oct. 23, 2026 | Those Wavelength limits and fees come from the October 23, 2026 mainnet announcement, and a CoinMarketCap Academy piece https://coinmarketcap.com/academy/vi/article/lightning-labs-wavelength-ai-agents-bitcoin-mainnet restates the 0.25% figure. Price your service with that fee in mind. A tiny per-request price on a small account can shrink faster than the headline percentage suggests once routing fees land on top. On the buyer side, Lightning Labs’ Lightning Agent Tools, released February 11, 2026, include lnget , an L402-aware command-line client for paying gated APIs. For coding agents, pi402 ships as the @lightninglabs/pi-402 package installed through the Pi harness. Your Python client in Step 9 is the same idea as a short script, and it works against any L402 server. What a Request Costs: Sats, Millisats, and Token Math Prices in Aperture’s YAML are whole sats, but inference-style metering works in millisatoshis, and mixing the two causes the most expensive mistakes. One sat equals 1,000 msat. The pi402 announcement gave an example rate of 1 msat per input token and 2 msat per output token. Lightning Labs framed it as an illustration and not a published price, and it estimated roughly $0.80 per million input tokens and $1.61 per million output tokens at the exchange rate it cited. | Workload | Input tokens | Output tokens | Cost in msat | Cost in sats | |---|---|---|---|---| | Small prompt | 1,000 | 500 | 2,000 | 2 | | Medium chat turn | 10,000 | 2,000 | Not listed | Not listed | | Long document summary | 100,000 | 5,000 | Not listed | Not listed | | One million tokens each way | 1,000,000 | 1,000,000 | 3,000,000 | 3,000 | The table applies the announcement’s example rates, so treat it as arithmetic and not a quote from any live service. It shows why metering matters. A flat 25-sat price would overcharge the small prompt by more than ten times and undercharge the long summary. Metered bundles let one payment cover many calls, which also cuts the number of Lightning payments you route. Fewer payments mean fewer chances to hit a failed route. Fees and liquidity set the floor on what you can sell. Every payment needs a path with enough capacity, and a seller who only receives needs inbound liquidity from peers. Plan for it the way a shop plans for a card terminal. For the broader map of Bitcoin tools that sit around Lightning, from cold storage to mining, the cryptocurrency section https://shattered.io/category/cryptocurrency/ collects the related guides. Common Pitfalls When Selling an API Over Lightning Most failures in a first L402 build come from a short list of mistakes. Each one below has burned someone who thought the setup was finished. - Binding the backend to all interfaces. If the Python server listens on 0.0.0.0, a visitor who finds port 8000 skips Aperture and reads your paid data for free. Bind to 127.0.0.1 or firewall the port. - Using tilde or $HOME in the YAML. Aperture reads paths literally. A tlspath of ~/l402-lab/alice/tls.cert fails at startup with a file-not-found error that looks like an LND problem. - Forgetting inbound liquidity. A seller node with channels where all the balance sits on its own side can create invoices that nobody can pay. In the lab, Bob opens the channel to Alice for exactly this reason. - Setting price to 0 and expecting free access. The v0.6.0 notes say a static price of 0 defaults to 1 sat. Use auth: "off" or a whitelist path for free routes. - Parsing only the first WWW-Authenticate header. With L402 and MPP both advertised, the first line may not be the one your client understands. Select the line that starts with L402. - Paying without a cap. A client that pays any invoice a server returns will drain its wallet on a hostile or buggy endpoint. Decode first, compare to a limit, and verify the preimage hash. - Confusing sats and msat. Aperture prices are in sats. Inference metering examples use millisatoshis. A factor of 1,000 hides in that gap. - Copying version numbers from old guides. A tutorial that pins LND v0.20.x or Aperture before v0.6.0 misses the MPTCP mitigation, metering, and the Wavelength option. - Shipping the no-seed-backup flag. It exists for regtest. On mainnet it leaves an unencrypted wallet on disk with no recovery phrase. Notice how many of these are configuration slips and not protocol bugs. The L402 handshake itself is small. The surrounding plumbing, from ports to paths to liquidity, is where the hours go. Troubleshooting: 12 Failures and Fixes Work from the bottom of the stack up. Confirm Bitcoin Core, then each LND node, then the channel, then Aperture, then the client. The table maps common symptoms to the layer that usually causes them. | Symptom | Likely cause | Fix | |---|---|---| | make install fails with a Go version error | Go older than the repo requires | Read go.mod in the v0.6.0 tag and install a Go release at least that new | | aperture: command not found | GOPATH bin not on PATH | Run source env.sh or add $ go env GOPATH /bin to PATH | | Aperture exits at startup citing the certificate or macaroon | Wrong tlspath or macdir , or a tilde in the path | Use absolute paths to alice/tls.cert and the regtest data directory | | LND stays at synced to chain: false | ZMQ ports missing on bitcoind, or bitcoind not running | Restart bitcoind with both -zmqpub flags and check the LND log | | getinfo says the macaroon file does not exist | LND is still creating the wallet | Wait ten seconds, then retry | | curl reports an SSL certificate problem | Aperture serves a self-signed certificate | Add -k in the lab, install a valid certificate in production | | Every request returns 404 or bypasses the paywall | hostregexp does not match the Host header, such as 127.0.0.1 vs localhost | Use the hostname your regex expects, or widen the pattern | | Bad gateway or connection refused after payment | Backend not running on port 8000 | Start the Python server and curl it directly on 127.0.0.1:8000 | | payinvoice fails with no route or insufficient balance | Channel not active, or Bob has no outbound funds | Mine six blocks, confirm active: true , check local balance | | 402 persists after paying | Wrong preimage, or a header that is not L402 macaroon:preimage | Pull the preimage with listpayments --reversed and check the colon and spacing | | Cached token suddenly rejected | Aperture database deleted or service renamed | Delete tokens.json and pay again, since tokens tie to the gateway that minted them | | Port already in use on 8080 or 8081 | Another dev service holds the port | Change the port in start script and YAML together, then restart both | When nothing above matches, raise the log level. Aperture already runs with debuglevel: "debug" in the lab, and LND writes to $LAB/alice.log . Read the first error and ignore the cascade after it. A failed invoice creation on Alice’s node usually shows up in Aperture’s log within the same second as the request. Complete Working Project: One Script to Build the Whole Lab The pieces so far fit together as one folder. Keep the files below in ~/l402-lab , and the lab rebuilds from nothing in a few minutes. l402-lab/ env.sh Step 1: paths and helper functions start-lab.sh Steps 2-4: chain, two nodes, funded channel l402 client.py Step 9: budgeted L402 client test.sh Step 11: four assertions stop-lab.sh tears everything down ~/.aperture/aperture.yaml Step 6: gateway config The start script below repeats Steps 2 to 4 with waits in place of sleeps, so it is safe to rerun from a clean folder. It also writes the two backend files from Step 7. Start the backend and Aperture yourself afterwards, in their own terminals. bash /usr/bin/env bash set -euo pipefail HERE="$ cd "$ dirname "$0" " && pwd " source "$HERE/env.sh" mkdir -p "$LAB"/{bitcoin,alice,bob,backend/service/free} bitcoind -regtest -daemon -datadir="$LAB/bitcoin" -server=1 -txindex=1 \ -rpcuser=lab -rpcpassword=labpass -fallbackfee=0.0002 \ -zmqpubrawblock=tcp://127.0.0.1:28332 -zmqpubrawtx=tcp://127.0.0.1:28333 until btc getblockchaininfo /dev/null 2 &1; do sleep 1; done btc -named createwallet wallet name=miner /dev/null 2 &1 || btc loadwallet miner /dev/null mine 101 start lnd { nohup lnd --lnddir="$LAB/$1" --noseedbackup --alias="$1" \ --listen=127.0.0.1:$2 --rpclisten=127.0.0.1:$3 --restlisten=127.0.0.1:$4 \ --bitcoin.active --bitcoin.regtest --bitcoin.node=bitcoind \ --bitcoind.rpchost=127.0.0.1:18443 --bitcoind.rpcuser=lab --bitcoind.rpcpass=labpass \ --bitcoind.zmqpubrawblock=tcp://127.0.0.1:28332 \ --bitcoind.zmqpubrawtx=tcp://127.0.0.1:28333 "$LAB/$1.log" 2 &1 & } start lnd alice 9735 10009 8080 start lnd bob 9736 10010 8082 synced { "$ $1 getinfo 2 /dev/null | jq -r .synced to chain " = "true" ; } until synced alice; do sleep 2; done until synced bob; do sleep 2; done btc -rpcwallet=miner sendtoaddress "$ bob newaddress p2tr | jq -r .address " 1 /dev/null mine 6 until "$ bob walletbalance | jq -r .confirmed balance " -gt 0 ; do sleep 1; done ALICE PUB=$ alice getinfo | jq -r .identity pubkey bob connect " email protected :9735" /dev/null bob openchannel --node key="$ALICE PUB" --local amt=1000000 /dev/null mine 6 until "$ bob listchannels | jq ' .channels | select .active | length' " = "1" ; do sleep 2; done echo '{"pair":"BTCUSD","note":"paid data, regtest"}' "$LAB/backend/service/data.json" echo '{"status":"free tier ok"}' "$LAB/backend/service/free/status.json" echo "lab ready: alice sells, bob buys, channel active" bash /usr/bin/env bash source "$ dirname "$0" /env.sh" alice stop 2 /dev/null || true bob stop 2 /dev/null || true btc stop 2 /dev/null || true pkill -f "http.server 8000" || true pkill -x aperture || true Run order for a fresh session: ./start-lab.sh , then the backend in terminal 1, aperture in terminal 2, and ./test.sh in terminal 3. A passing run ends with the single PASS line from Step 11. To start over, run ./stop-lab.sh , delete ~/l402-lab/bitcoin , alice , and bob , remove tokens.json , and run the start script again. Frequently Asked Questions What is an L402 paywall? It is an API gate that charges per access with a Lightning payment. The server answers an unpaid request with HTTP 402, a macaroon token, and an invoice. After the client pays, the preimage plus the token acts as the credential for later requests. Is L402 the same as LSAT? L402 is the current name for the protocol that earlier material called LSAT. Lightning Labs describes L402 as production-ready as of March 11, 2026 and identifies it as bLIP-0026. Do I still need an LND node to sell with Aperture? Not always. Aperture v0.6.0 can create invoices through a Wavelength wallet, so a seller can skip running a node, managing channels, and sourcing inbound liquidity. An LND-backed setup, like the one in this lab, remains the path when you want to run your own infrastructure. Which LND and Bitcoin Core versions should I run? At the time of writing, the LND repository lists v0.21.8-beta as the latest release as of November 14, 2026, and it is still labeled beta. For Bitcoin Core, use the current release listed on its official release page. Can I test everything without real bitcoin? Yes. Regtest gives you a private chain where you mine your own blocks, and this lab uses it for every step. Moving to mainnet means a real seed, real channels, real inbound liquidity, a valid TLS certificate, and an Aperture-specific macaroon. How much can I charge per request? Aperture prices are set in sats, and the lab uses 25. Anything below the routing cost can lose money, so test paths to your node before you pick a price. Metered pricing in v0.6.0 uses finer units and bills actual usage, which suits token-based services. What does the client need to pay an L402 invoice? Any wallet that can pay a BOLT 11 invoice and expose the preimage. The lab uses lncli through a Python script. Lightning Labs also released lnget in its Agent Tools on February 11, 2026, as an L402-aware command-line client.