SDKs
Install the ln.bot SDK for TypeScript, Python, Go, Rust or C#, create an invoice and pay an address, then add L402 for Express or ASP.NET Core.
The five SDKs share one shape: a client made from a key, and a wallet handle made from a wallet ID (ln.wallet("wal_...")). Every wallet call, from invoices to L402, goes through the handle. Amounts are integers in sats, the unit this site writes as ₿.
Keys and wallet IDs
lnbot init prints both: the wallet ID (wal_...) on the Wallet: line, and the user key (uk_...), which it also saves to ~/.config/lnbot/config.json. Or register with the SDK itself:
import { LnBot } from "@lnbot/sdk";
const account = await new LnBot().register(); // no key needed
console.log(account.primaryKey); // uk_..., shown once
console.log(account.recoveryPassphrase); // 12 words, shown once
const ln = new LnBot({ apiKey: account.primaryKey });
const wallet = await ln.wallets.create();
console.log(wallet.walletId, wallet.address); // wal_... x4k7m2@ln.bot
// A key that reaches only this wallet (one per wallet)
const { key } = await ln.wallet(wallet.walletId).key.create();
console.log(key); // wk_..., shown onceA user key reaches every wallet on the account. A wallet key reaches one, which suits a service or an agent that should touch only that wallet. Either kind works on every wallet call, the event streams (invoices.watch, payments.watch, events.stream) included.
A payment can come back with status pending or processing while it's in flight. Payments between ln.bot wallets settle within about half a second; ask again with payments.get(number) until the status is settled or failed. One that can't be routed yet stays pending and is retried, for 24 hours at most, or until its invoice expires. An invoice that shows expired can still turn settled if it was paid just before it expired, so read it again before treating it as unpaid.
Fees and pricing
Payments between ln.bot wallets are free. A payment out to Lightning pays a service fee of 0.25%, at least ₿1, and the network fee: 0.25% plus routing. Leave maxFee out: the server caps the network fee at 1% of the amount, at least ₿20, and holds only its quote plus a small buffer while the payment is in flight.
To show what a payment costs before sending it, price it. Resolve returns networkFeeEstimate, serviceFee and total, and internal when the recipient is an ln.bot wallet. Asked about a Lightning address or LNURL without an amount, it returns maxSendable, the most the wallet can send there now after fees; pay exactly that, with no maxFee, to send everything.
| SDK | Price a payment | Most the wallet can send |
|---|---|---|
| TypeScript | w.payments.resolve({ target, amount }) |
w.payments.resolve({ target }), then maxSendable |
| Python | w.payments.resolve(target=..., amount=...) |
w.payments.resolve(target=...), then max_sendable |
| Go | w.Payments.ResolveWithAmount(ctx, target, amount) |
w.Payments.Resolve(ctx, target), then MaxSendable |
| Rust | w.payments().resolve_with_amount(target, amount) |
w.payments().resolve(target), then max_sendable |
| C# | w.Payments.ResolveAsync(target, amount) |
w.Payments.ResolveAsync(target), then MaxSendable |
Pricing takes version 1.1.0 of each SDK. A BOLT11 invoice is priced at its own amount without one. Pricing a Lightning address or LNURL fetches an invoice from the recipient, so call it once the amount is settled.
TypeScript
npm install @lnbot/sdkimport { LnBot } from "@lnbot/sdk";
const ln = new LnBot({ apiKey: process.env.LNBOT_API_KEY! });
const w = ln.wallet("wal_...");
// Receive: an invoice anyone can pay from a Lightning wallet
const invoice = await w.invoices.create({ amount: 1000, memo: "Report #42" });
console.log(invoice.bolt11);
// Pay another agent's address (an address needs an amount)
const payment = await w.payments.create({ target: "r7vq3np@ln.bot", amount: 500 });
console.log(payment.status); // "settled", or "processing" while in flight
// Check the balance
const { available } = await w.get();
console.log(available);Errors are typed (BadRequestError, UnauthorizedError, NotFoundError and others; see errors). Node 18 or later, Bun or Deno.
Python
pip install lnbotfrom lnbot import LnBot
ln = LnBot(api_key="uk_...") # or leave it out and set LNBOT_API_KEY
w = ln.wallet("wal_...")
# Receive: an invoice anyone can pay from a Lightning wallet
invoice = w.invoices.create(amount=1000, memo="Report #42")
print(invoice.bolt11)
# Pay another agent's address (an address needs an amount)
payment = w.payments.create(target="r7vq3np@ln.bot", amount=500)
print(payment.status)
# Check the balance
print(w.get().available)Arguments are keyword-only and snake_case. AsyncLnBot has the same methods for await. Python 3.10 or later.
Go
go get github.com/lnbotdev/go-sdkpackage main
import (
"context"
"fmt"
"log"
lnbot "github.com/lnbotdev/go-sdk"
)
func main() {
ctx := context.Background()
ln := lnbot.New("uk_...")
w := ln.Wallet("wal_...")
// Receive: an invoice anyone can pay from a Lightning wallet
invoice, err := w.Invoices.Create(ctx, &lnbot.CreateInvoiceParams{
Amount: 1000,
Memo: lnbot.Ptr("Report #42"),
})
if err != nil {
log.Fatal(err)
}
fmt.Println(invoice.Bolt11)
// Pay another agent's address (an address needs an amount)
payment, err := w.Payments.Create(ctx, &lnbot.CreatePaymentParams{
Target: "r7vq3np@ln.bot",
Amount: lnbot.Ptr(int64(500)),
})
if err != nil {
log.Fatal(err)
}
fmt.Println(payment.Status)
// Check the balance
wallet, err := w.Get(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Println(wallet.Available)
}Optional fields are pointers; lnbot.Ptr makes one. Errors are *lnbot.BadRequestError and the rest, which errors.As can match.
Rust
The SDK is async and leaves the runtime to you, so add tokio too:
cargo add lnbot tokio --features tokio/macros,tokio/rt-multi-threaduse lnbot::{CreateInvoiceRequest, CreatePaymentRequest, LnBot};
#[tokio::main]
async fn main() -> Result<(), lnbot::LnBotError> {
let ln = LnBot::new("uk_...");
let w = ln.wallet("wal_...");
// Receive: an invoice anyone can pay from a Lightning wallet
let invoice = w
.invoices()
.create(&CreateInvoiceRequest::new(1000).memo("Report #42"))
.await?;
println!("{}", invoice.bolt11);
// Pay another agent's address (an address needs an amount)
let payment = w
.payments()
.create(&CreatePaymentRequest::new("r7vq3np@ln.bot").amount(500))
.await?;
println!("{:?}", payment.status);
// Check the balance
let wallet = w.get().await?;
println!("{}", wallet.available);
Ok(())
}C#
dotnet add package LnBotusing LnBot;
using LnBot.Models;
using var client = new LnBotClient("uk_...");
var w = client.Wallet("wal_...");
// Receive: an invoice anyone can pay from a Lightning wallet
var invoice = await w.Invoices.CreateAsync(new CreateInvoiceRequest
{
Amount = 1000,
Memo = "Report #42",
});
Console.WriteLine(invoice.Bolt11);
// Pay another agent's address (an address needs an amount)
var payment = await w.Payments.CreateAsync(new CreatePaymentRequest
{
Target = "r7vq3np@ln.bot",
Amount = 500,
});
Console.WriteLine(payment.Status);
// Check the balance
var wallet = await w.GetAsync();
Console.WriteLine(wallet.Available);.NET 8 or later. In ASP.NET Core, register one LnBotClient as a singleton.
L402 libraries
The L402 libraries charge per request on your own routes and pay the L402 challenges other APIs send: @lnbot/l402 for Express, and LnBot.L402 with LnBot.L402.AspNetCore for .NET. They use the L402 endpoints: for each paid request, the paywall asks ln.bot to verify the token (one HTTPS call), and ln.bot mints each new challenge. This section describes version 1.1.0.
By default a token pays for one request. It works once, only on the route and at the price it was issued for, and it expires after an hour unless you set expirySeconds. The paywall remembers spent tokens in the process's memory; if your API runs on several instances, give them a shared store, or a token spent on one instance works again on another. reuse: true (Reuse = true in C#) makes a token a pass that works until it expires, as every token did in 1.0; set expirySeconds with it, or the token never expires.
The clients read the price from the invoice itself, not from what the server says, and refuse an invoice with no amount, one above maxPrice (default ₿1,000), or one that would go past budgetSats for the budgetPeriod. The budget counts invoice amounts, not fees, and lives in memory, so it starts from zero when the process restarts. To cap what an agent can spend in total, fund its wallet with only that much.
TypeScript: Express.js
npm install @lnbot/l402@lnbot/sdk and express are peer dependencies, which npm installs alongside, and @lnbot/l402 re-exports LnBot, so one import line covers both. The server:
import express from "express";
import { l402, LnBot } from "@lnbot/l402";
const app = express();
const ln = new LnBot({ apiKey: process.env.LNBOT_API_KEY! });
// ₿10 per request on everything under /api/premium
app.use("/api/premium", l402.paywall(ln, { walletId: "wal_...", price: 10, description: "API access" }));
app.get("/api/premium/data", (req, res) => {
res.json({ data: "premium content", paid: req.l402?.price });
});
app.listen(3000);Handlers see req.l402, with paymentHash, caveats and price. To share spent tokens across instances, pass spentStore: any object whose claim(paymentHash, ttlSeconds) resolves true only for the first caller (Redis SET NX EX does it). The client:
import { l402, LnBot } from "@lnbot/l402";
const ln = new LnBot({ apiKey: process.env.LNBOT_API_KEY! });
// Pays a 402 if the invoice asks for ₿100 or less, up to ₿50,000 a day
const api = l402.client(ln, {
walletId: "wal_...",
maxPrice: 100,
budgetSats: 50_000,
budgetPeriod: "day",
});
const data = await api.get("https://api.example.com/premium/data");
console.log(data);api.fetch(url, init) works like fetch, and post, put, patch and delete sit beside get. A price above maxPrice or past the budget throws L402BudgetExceededError, a failed payment throws L402PaymentFailedError, and both extend L402Error.
C#: ASP.NET Core
dotnet add package LnBot.L402.AspNetCoreThe paywall and the client both take the LnBotClient from dependency injection, so register it before Build():
using LnBot;
using LnBot.L402;
var builder = WebApplication.CreateBuilder(args);
// The paywall and the client both take the LnBotClient from DI
builder.Services.AddSingleton(new LnBotClient("uk_..."));
// Client: an HttpClient that pays a 402 if the invoice asks for ₿100 or less
builder.Services.AddHttpClient("paid-apis")
.AddL402Handler(new L402ClientOptions { WalletId = "wal_...", MaxPrice = 100 });
var app = builder.Build();
// Server: ₿10 per request on everything under /api/premium
app.UseL402Paywall("/api/premium", new L402Options { WalletId = "wal_...", Price = 10 });
app.MapGet("/api/premium/data", (HttpContext context) =>
Results.Ok(new { data = "premium content", paymentHash = context.GetL402()?.PaymentHash }));
// Or one endpoint at a time
app.MapGet("/api/report", () => Results.Ok(new { report = "..." }))
.AddEndpointFilter(new L402EndpointFilter("wal_...", 25));
app.Run();On a controller action, [L402(WalletId = "wal_...", Price = 50)] does the same. L402Options also takes ExpirySeconds, Reuse, Caveats and PriceFactory for per-request prices. To share spent tokens across instances, register an ISpentTokenStore in DI. A client outside ASP.NET Core needs only LnBot.L402.
Python, Go and Rust
There is no L402 middleware for these. Their SDKs make the same three calls the libraries do: create a challenge (w.l402.create_challenge(amount=10) in Python, w.L402.CreateChallenge in Go, w.l402().create_challenge(...) in Rust), then verify a token and pay a challenge (verify and pay, or Verify and Pay in Go). Verification is stateless, so a paywall you build yourself has to remember spent payment hashes if each payment should buy one request.