Solutions/L402

L402

Sell access to API endpoints, data, or compute with Lightning payments. L402 lets a client pay an invoice and use the proof of payment with an access token. Your Phoenixd node receives the payment; your application or middleware decides what that token can access.

Start with one paid endpoint and a price in satoshis. Choose whether a payment buys a single request, prepaid usage, or access for a fixed period.

Connect your node

Use an L402 integration with a Phoenixd backend, such as toll-booth. Its Phoenixd adapter creates invoices and checks payments through your node's HTTP API.

You need:

  • A running node and its HTTPS endpoint from the Nodana dashboard.
  • A Phoenixd API password. See the Phoenixd API guide for authentication.
  • An existing API to protect and a price for each paid route.

Keep the node password in your backend's secret configuration. An L402 client receives the invoice and access token, without needing your node credentials.

Put a payment gate in front of your API

This Node.js example uses Express and toll-booth to proxy an existing API. Requests to /api/query cost 100 sats each, deducted from the client's prepaid balance.

Install the packages in your backend service:

npm install express @forgesworn/toll-booth

Set PHOENIXD_URL, PHOENIXD_PASSWORD, and UPSTREAM_URL in the service's environment. UPSTREAM_URL is the base URL of the API being protected. Generate a signing key with openssl rand -hex 32 and save it as L402_ROOT_KEY in your secret configuration. Keep the same signing key across service restarts so existing credentials remain usable.

With those settings in place, save this as server.mjs:

import express from "express";
import { Booth } from "@forgesworn/toll-booth";
import { phoenixdBackend } from "@forgesworn/toll-booth/backends/phoenixd";

const app = express();
app.use(express.json());

const booth = new Booth({
  adapter: "express",
  backend: phoenixdBackend({
    url: process.env.PHOENIXD_URL,
    password: process.env.PHOENIXD_PASSWORD,
  }),
  upstream: process.env.UPSTREAM_URL,
  rootKey: process.env.L402_ROOT_KEY,
  pricing: { "/api/query": 100 },
  defaultInvoiceAmount: 1000,
  freeTier: { requestsPerDay: 0 },
});

app.get("/invoice-status/:paymentHash", booth.invoiceStatusHandler);
app.post("/create-invoice", booth.createInvoiceHandler);
app.use("/", booth.middleware);

app.listen(3000);

Run the service with node server.mjs and expose it through your HTTPS endpoint. A default invoice tops up 1,000 sats of credit, enough for ten 100-sat requests. This is the middleware's prepaid usage model; the invoice amount and request price are separate settings. Add each paid route to the pricing table; unlisted routes are passed through without a charge.

Keep the middleware's database on persistent storage, and make the paid upstream accessible only through the payment gate. Follow the middleware's deployment guidance when configuring your service.

Test paid access

Request a protected route without credentials:

curl -i https://api.example.com/api/query

Expect HTTP 402 with an L402 payment challenge. Use an L402-capable client to pay and retry. The client must retain both the access token and the payment preimage returned by its Lightning wallet.

Check that the paid request reaches your upstream, that usage is charged at the configured price, and that exhausted credit requires another payment. Also check that direct requests to the upstream cannot bypass the gate.

Set access limits and expiry

For a custom L402 integration, bind the access token to the invoice's payment hash and set its permitted routes, usage allowance, or access expiry. Validate the token and payment proof before serving a protected resource. Use the integration's supported settings to enforce the access policy you advertise to customers.

Invoice expiry is the deadline for paying. Access expiry is the deadline for using the paid credential. Set them deliberately: paying an invoice need not mean a client can use your API indefinitely.

Troubleshoot

  • Invoices fail to appear: check the node URL, API password, and node status. Confirm your backend can reach the node.
  • Payment succeeds but access is refused: check that the client sends both the token and preimage, that its credit or access has not expired, and that the signing key matches the one used to issue the token.
  • Paid credit disappears after a restart: check that the middleware's database is on persistent storage and that the signing key was retained.

Keep tokens and payment preimages out of logs: together they grant access to the paid service.

Learn more

On this page