Skip to content

About

The official TypeScript SDK for ln.bot - Bitcoin for AI Agents.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Repository files navigation

@lnbot/sdk

npm version npm downloads bundle size TypeScript License: MIT

The official TypeScript SDK for ln.bot — Bitcoin for AI Agents.

Give your AI agents, apps, and services access to Bitcoin over the Lightning Network. Create wallets, send and receive sats, and get real-time payment notifications — all in a few lines of code.

import { LnBot } from "@lnbot/sdk";

const client = new LnBot({ apiKey: "uk_..." });
const wallet = client.wallet("wal_...");

await wallet.payments.create({ target: "alice@ln.bot", amount: 1000 });

ln.bot also ships a Python SDK, Go SDK, Rust SDK, CLI, and MCP server.


Why ln.bot?

  • Built for agents — programmatic wallets, API keys, and idempotent payments designed for autonomous software
  • Instant settlement — payments arrive in seconds, not days
  • Lightning addresses — human-readable addresses like name@ln.bot
  • Real-time events — SSE streams and webhooks for payment notifications
  • Recovery built in — 12-word BIP-39 passphrase and passkey backup
  • Simple pricing — no monthly fees, pay only when you move sats off-network

Why this SDK?

  • Zero dependencies — native fetch (Node 18+, Bun, Deno, browsers)
  • Fully typed — every request, response, and error has TypeScript types
  • Tiny — under 10 KB minified + gzipped
  • Dual format — ESM + CJS with source maps and .d.ts declarations
  • Wallet-scoped — client.wallet("wal_...").invoices.create() — clean, explicit, no ambiguity

Install

npm install @lnbot/sdk
pnpm add @lnbot/sdk
yarn add @lnbot/sdk

Quick start

1. Register and create a wallet

import { LnBot } from "@lnbot/sdk";

// Register a new account (no auth needed)
const client = new LnBot();
const account = await client.register();

console.log(account.primaryKey);         // user key (uk_...)
console.log(account.recoveryPassphrase); // back this up!

// Create a wallet using your user key
const authed = new LnBot({ apiKey: account.primaryKey });
const wallet = await authed.wallets.create();

console.log(wallet.walletId); // wal_...
console.log(wallet.address);  // your Lightning address

2. Receive sats

const client = new LnBot({ apiKey: "uk_..." });
const w = client.wallet("wal_...");

const invoice = await w.invoices.create({
  amount: 1000,
  memo: "Payment for task #42",
});

console.log(invoice.bolt11); // share with the payer

3. Wait for payment (SSE)

const w = client.wallet("wal_...");

for await (const event of w.invoices.watch(invoice.number)) {
  if (event.event === "settled") {
    console.log("Paid!", event.data.preimage);
    break;
  }
}

4. Send sats

const w = client.wallet("wal_...");

// To a Lightning address
await w.payments.create({ target: "alice@ln.bot", amount: 500 });

// To a BOLT11 invoice
await w.payments.create({ target: "lnbc10u1p..." });

5. Check balance

const info = await client.wallet("wal_...").get();
console.log(`${info.available} sats available`);

Fees

Payments between ln.bot wallets are free. A payment out to Lightning pays a service fee of 0.25% (minimum 1 sat) and the network fee, which is the Lightning routing fee. Leave maxFee out: the server caps the network fee at 1% of the amount (minimum 20 sats) and holds only the estimate plus a small buffer while the payment is in flight. On success the wallet is debited amount + actualFee + serviceFee.

Price a payment before sending

const quote = await w.payments.resolve({ target: "alice@example.com", amount: 250000 });

console.log(quote.internal);           // true for an ln.bot wallet: no fees
console.log(quote.networkFeeEstimate); // 75
console.log(quote.serviceFee);         // 625
console.log(quote.total);              // 250700

if (quote.feeWarning) {
  // Sending without maxFee would be rejected: the fee is unusually high
  console.warn(quote.feeWarning);
}

A BOLT11 invoice carries its amount, so resolve({ target: "lnbc..." }) prices it as is. Pricing a Lightning address or LNURL fetches an invoice from the recipient, so call it once the amount is settled, not on every keystroke.

Send everything

Resolve a Lightning address or LNURL without an amount to get maxSendable, the most the wallet can send there right now after fees, and pay exactly that:

const { maxSendable } = await w.payments.resolve({ target: "alice@example.com" });

if (maxSendable) {
  await w.payments.create({ target: "alice@example.com", amount: maxSendable });
}

Payment stages

Every payment returned by create, get and list carries stage, statusDetail and retryUntil. stage says where the payment is, more precisely than status; statusDetail is a sentence to show your user as is.

const p = await w.payments.get(paymentNumber);

console.log(p.status);       // "pending"
console.log(p.stage);        // "retrying"
console.log(p.statusDetail); // "Not sent yet; retrying for a few minutes. ..."
console.log(p.retryUntil);   // "2026-10-25T14:43:53+00:00"
stage With status statusDetail
queued pending Waiting to be sent.
retrying pending Not sent yet; retrying for a few minutes. If it still can't be sent, it fails and the funds are released.
sending pending or processing Being sent. Between ln.bot wallets: Being transferred to the recipient's wallet.
in_flight processing Sent; the Lightning network is completing it, usually within seconds.
confirming processing Checking with the Lightning network whether it went out. The funds stay on hold until then.
on_hold processing Interrupted by an earlier version of the service and on hold until it is reviewed. The funds are held, not lost.
settled settled null
failed failed The failureReason
  • Fall back to status. More stages may be added, and PaymentStage accepts any string, so for a stage you don't know, go by status.
  • retryUntil is set only with stage: "retrying": if the payment still can't be sent by then, it fails within about a minute and the funds are released.
  • Both are in progress. A payment can go from processing back to pending when it is retried, so treat both as in progress.
  • Older API. The three fields are optional: an API without them leaves them undefined.
  • Where they appear. w.payments.watch() carries them too. Webhooks and w.events.stream() don't.

Authentication

ln.bot uses two key types:

  • User key (uk_...) — manages all wallets under your account. Use for creating wallets, listing wallets, rotating user keys.
  • Wallet key (wk_...) — scoped to a single wallet. Use it to give a service or an agent access to one wallet only.
// User key — full account access
const client = new LnBot({ apiKey: "uk_..." });
const w1 = client.wallet("wal_abc");
const w2 = client.wallet("wal_xyz");

// Wallet key — single wallet
const wkClient = new LnBot({ apiKey: "wk_..." });
const w = wkClient.wallet("current"); // "current" resolves to the key's wallet

Wallet keys

const w = client.wallet("wal_...");

// Create (one key per wallet)
const key = await w.key.create();
console.log(key.key); // wk_... — shown once

// Get metadata
const info = await w.key.get();

// Rotate (old key invalidated immediately)
const rotated = await w.key.rotate();

// Delete
await w.key.delete();

Addresses

Every wallet gets a Lightning address on creation. Create more or claim vanity addresses:

const w = client.wallet("wal_...");

const random = await w.addresses.create();
const vanity = await w.addresses.create({ address: "myagent" });
const all    = await w.addresses.list();

await w.addresses.delete("myagent@ln.bot");

// Transfer a vanity address to another wallet
await w.addresses.transfer("myagent@ln.bot", {
  targetWalletKey: "wk_other_wallet_key",
});

Transactions

Full history of credits and debits:

const txs = await client.wallet("wal_...").transactions.list({ limit: 20 });

for (const tx of txs) {
  console.log(`${tx.type} ${tx.amount} sats — balance: ${tx.balanceAfter}`);
}

Webhooks

Get notified when invoices are paid:

const w = client.wallet("wal_...");

const hook = await w.webhooks.create({
  url: "https://example.com/webhooks/lnbot",
});
// hook.secret — save this for signature verification. Only returned once.

const hooks = await w.webhooks.list();
await w.webhooks.delete(hook.id);

Public invoices (no auth)

Create invoices for any wallet or address without authentication:

const client = new LnBot(); // no API key needed

const inv = await client.invoices.createForWallet({
  walletId: "wal_...",
  amount: 100,
});

const inv2 = await client.invoices.createForAddress({
  address: "alice@ln.bot",
  amount: 100,
});

Service status

Whether ln.bot is working, measured about once a minute. Both calls are public: no API key is needed, and sending one does no harm.

const client = new LnBot(); // no API key needed

const now = await client.status();
console.log(now.status);                            // "operational" | "degraded" | "outage" | "unknown"
console.log(now.notice?.message);                   // a message from the operators, if any
console.log(now.components.sending.byStage);        // { in_flight: 1 }: payments being sent, by stage
console.log(now.components.node.latencyMs);         // null when the node didn't answer

const history = await client.statusHistory("7d");   // "24h" (default), "7d", "30d" or "90d"
for (const bucket of history.buckets) {
  if (bucket.samples === 0) continue;               // not measured: no data, not up or down
  const up = (bucket.samples - bucket.overall.degraded - bucket.overall.outage) / bucket.samples;
  console.log(bucket.at, up);
}
console.log(history.summary.uptime.overall.operational); // percent of measured time
  • status() covers the overall status and four parts: sending (payments out), receiving (payments in), node (the Lightning node) and webhooks (deliveries). unknown means nothing was measured for 5 minutes, and the numbers are as old as updatedAt.
  • statusHistory() returns buckets of 5 minutes (24h), an hour (7d), 6 hours (30d) or a day (90d), aligned to UTC and oldest first. The last one is still filling, so don't assume a count.
  • Every percentile, gauge and latency can be null. status, level, stage and range values are open strings: more may be added, so never fail on one you don't know.
  • Both calls share the API's limit for public routes, 10 requests a minute per IP. Over it they throw TooManyRequestsError (status 429).

User keys

const rotated = await client.keys.rotate(0); // 0 = primary, 1 = secondary
// rotated.key — new plaintext key, shown once

Backup & restore

Recovery passphrase

const backup = await client.backup.recovery();
// backup.passphrase — 12-word BIP-39, shown once

const restored = await client.restore.recovery({
  passphrase: "word1 word2 ... word12",
});
// restored.primaryKey, restored.secondaryKey — fresh keys

Passkey (WebAuthn)

const begin = await client.backup.passkeyBegin();
// pass begin.options to navigator.credentials.create()

await client.backup.passkeyComplete({
  sessionId: begin.sessionId,
  attestation: credential,
});

L402 paywalls

Monetize APIs with Lightning-native authentication:

const w = client.wallet("wal_...");

// Create a challenge (server side)
const challenge = await w.l402.createChallenge({
  amount: 100,
  description: "API access",
  expirySeconds: 3600,
});

// Pay the challenge (client side)
const other = otherClient.wallet("wal_...");
const result = await other.l402.pay({
  wwwAuthenticate: challenge.wwwAuthenticate,
});

// Verify a token (server side, stateless)
const { valid } = await w.l402.verify({
  authorization: result.authorization!,
});

SSE event streams

Watch a single invoice

for await (const event of w.invoices.watch(invoiceNumber, 120)) {
  // event.event: "settled" | "expired" | "timeout" (the wait ran out; data is the current state)
  // event.data: InvoiceResponse
}

An expired invoice is not always final: one paid just before it expired can still turn settled. Check it again with w.invoices.get(), or keep listening for invoice.settled on w.events.stream() or a webhook, before treating it as unpaid.

Watch a single payment

for await (const event of w.payments.watch(paymentNumber, 60)) {
  // event.event: "settled" | "failed" | "timeout" (the wait ran out; data is the current state)
  // event.data: PaymentResponse
}

A timeout event's data carries the payment's stage and statusDetail (payment stages); the stream doesn't send an event for each stage change. A payment that can't be sent fails quickly, and its funds are released at once: within about half a minute if the Lightning service refuses it, or after 10 minutes of automatic retries for a temporary problem (stage: "retrying", until retryUntil). If its invoice runs out first, it fails with failureReason set to Invoice expired before it could be paid. A payment stays processing longer only when its funds are already on the Lightning network: it settles or fails as soon as the network does, usually within seconds, and is never refunded while it could still be paid. Show failureReason as is; when it ends in "Please try again.", sending the payment again later can work.

Stream all wallet events

const controller = new AbortController();

for await (const event of w.events.stream(controller.signal)) {
  // event.event: "invoice.created" | "invoice.settled" | "payment.created" | ...
  console.log(event.event, event.data);
}

Event streams work with a user key or a wallet key. The API sends event and webhook payloads in snake_case (actual_fee, created_at); the SDK converts them to the camelCase fields used everywhere else, so event.data.actualFee works. A webhook handler reads the raw snake_case JSON.

Pagination

All list endpoints support cursor-based pagination:

const page1 = await w.invoices.list({ limit: 10 });

const page2 = await w.invoices.list({
  limit: 10,
  after: page1[page1.length - 1].number,
});

Works the same for w.payments.list() and w.transactions.list().

Error handling

All API errors throw typed exceptions:

import {
  LnBotError,
  BadRequestError,
  UnauthorizedError,
  ForbiddenError,
  NotFoundError,
  ConflictError,
  TooManyRequestsError,
} from "@lnbot/sdk";

try {
  await w.payments.create({ target: "invalid", amount: 100 });
} catch (err) {
  if (err instanceof BadRequestError) {
    // 400 — validation failed
  } else if (err instanceof UnauthorizedError) {
    // 401 — invalid or missing API key
  } else if (err instanceof ForbiddenError) {
    // 403 — key doesn't have access
  } else if (err instanceof NotFoundError) {
    // 404 — resource not found
  } else if (err instanceof ConflictError) {
    // 409 — duplicate or conflict
  } else if (err instanceof TooManyRequestsError) {
    // 429: public routes allow 10 requests a minute per IP
  } else if (err instanceof LnBotError) {
    // Other API error
    console.error(err.status, err.body);
  }
}

Configuration

import { LnBot } from "@lnbot/sdk";

const client = new LnBot({
  apiKey: "uk_...",                  // optional — not needed for register or public invoices
  baseUrl: "https://api.ln.bot",     // optional — this is the default
  fetch: customFetch,                // optional — bring your own fetch for testing or proxies
});

API reference

Top-level

Method Description
client.register() Create a new account (no auth)
client.me() Get current identity
client.status() Service status right now (no auth)
client.statusHistory(range?) Service status over 24h (default), 7d, 30d or 90d (no auth)
client.wallet(id) Get a wallet handle for scoped operations
client.wallets.create() Create a new wallet (user key)
client.wallets.list() List all wallets (user key)
client.keys.rotate(slot) Rotate user key (0 = primary, 1 = secondary)
client.invoices.createForWallet(req) Create invoice by wallet ID (no auth)
client.invoices.createForAddress(req) Create invoice by Lightning address (no auth)
client.backup.recovery() Generate 12-word recovery passphrase
client.restore.recovery(req) Restore with recovery passphrase

Wallet-scoped (client.wallet("wal_..."))

Method Description
w.get() Get wallet info and balance
w.update(req) Update wallet name

Wallet key (w.key)

Method Description
w.key.create() Create wallet key (max 1 per wallet)
w.key.get() Get key metadata
w.key.rotate() Rotate key (old key invalidated)
w.key.delete() Delete/revoke key

Invoices (w.invoices)

Method Description
w.invoices.create(req) Create a BOLT11 invoice
w.invoices.list(params?) List invoices
w.invoices.get(numberOrHash) Get invoice by number or payment hash
w.invoices.watch(numberOrHash, timeout?) SSE stream for invoice status

Payments (w.payments)

Method Description
w.payments.create(req) Send sats to address or invoice
w.payments.list(params?) List payments
w.payments.get(numberOrHash) Get payment by number or hash
w.payments.resolve(params) Inspect and price a target without sending ({ target, amount? })
w.payments.watch(numberOrHash, timeout?) SSE stream for payment status

Addresses (w.addresses)

Method Description
w.addresses.create(req?) Create random or vanity address
w.addresses.list() List all addresses
w.addresses.delete(address) Delete an address
w.addresses.transfer(address, req) Transfer vanity address to another wallet

Transactions (w.transactions)

Method Description
w.transactions.list(params?) List credit and debit transactions

Webhooks (w.webhooks)

Method Description
w.webhooks.create(req) Register a webhook endpoint (max 10)
w.webhooks.list() List all webhooks
w.webhooks.delete(id) Delete a webhook

L402 (w.l402)

Method Description
w.l402.createChallenge(req) Create L402 challenge (invoice + macaroon)
w.l402.verify(req) Verify L402 authorization token
w.l402.pay(req) Pay L402 challenge, get authorization header

Events (w.events)

Method Description
w.events.stream(signal?) SSE stream of all wallet events

Requirements

  • Node.js 18+, Bun, Deno, or any environment with a global fetch
  • Get your API key at ln.bot

Other interfaces

ln.bot isn't just an SDK — pick the interface that fits your stack:

  • CLI — lnbot pay lnbc1...f3q --amount 1000
  • MCP Server — Model Context Protocol for AI agents
  • REST API — direct HTTP calls

Links

Other SDKs

License

MIT

About

The official TypeScript SDK for ln.bot - Bitcoin for AI Agents.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages