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.

Updated View as Markdown

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 once
typescript

A 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/sdk
bash
import { 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);
typescript

Errors are typed (BadRequestError, UnauthorizedError, NotFoundError and others; see errors). Node 18 or later, Bun or Deno.

Python

pip install lnbot
bash
from 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)
python

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-sdk
bash
package 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)
}
go

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-thread
bash
use 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(())
}
rust

C#

dotnet add package LnBot
bash
using 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);
csharp

.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
bash

@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);
typescript

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);
typescript

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.AspNetCore
bash

The 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();
csharp

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.