A provider-agnostic Lightning Network payment method for MPP (Machine Payments Protocol).
Note for Lightning developers: "MPP" here refers to Machine Payments Protocol, an open HTTP payment standard — not Multi-Path Payments (BOLT #4).
MPP lets any HTTP API accept payments using the standard 402 Payment Required flow. This SDK implements the Lightning payment method with a pluggable provider architecture — bring your own LND node, or swap in any Lightning implementation.
Client Server
│ │
│────── GET /resource ───────────>│
│ │
│<──── 402 + BOLT11 invoice ──────│ server generates invoice via provider
│ │
│ (pay invoice over Lightning) │
│ │
│─── GET /resource + preimage ───>│
│ │ server verifies sha256(preimage) == paymentHash
│<────── 200 + resource ──────────│
│ │
No external payment processor. No polling. No webhooks. The preimage is the proof of payment.
| Package | Description |
|---|---|
@ambosstech/lightning-mpp-sdk |
Core SDK — method definitions, provider interface, store, errors |
@ambosstech/lightning-mpp-adapter-lnd |
LND adapter — gRPC and REST transports |
@ambosstech/lightning-mpp-adapter-nwc |
NWC adapter — Nostr Wallet Connect (NIP-47) over relays |
@ambosstech/lightning-mpp-adapter-mock |
Mock adapter — for testing without a real Lightning node |
# Core + LND adapter
pnpm add @ambosstech/lightning-mpp-sdk @ambosstech/lightning-mpp-adapter-lnd mppx
# Or core + NWC adapter (Alby Hub, coinos, Primal, etc.)
pnpm add @ambosstech/lightning-mpp-sdk @ambosstech/lightning-mpp-adapter-nwc mppx
# Or core + mock for testing
pnpm add @ambosstech/lightning-mpp-sdk @ambosstech/lightning-mpp-adapter-mock mppxUses the Web-standard Request/Response API — works with Node.js, Cloudflare Workers, Next.js, Bun, Deno, and any other runtime.
import { Mppx } from "mppx";
import {
lightningChargeServer,
createMemoryStore,
} from "@ambosstech/lightning-mpp-sdk";
import { LndLightningProvider } from "@ambosstech/lightning-mpp-adapter-lnd";
// 1. Create a provider (LND in this example)
const provider = new LndLightningProvider({
transport: "rest",
url: "https://127.0.0.1:8080",
macaroon: process.env.LND_MACAROON!,
});
// 2. Create the server-side charge method
const chargeMethod = lightningChargeServer({
provider,
currency: "sat",
network: "mainnet",
});
// 3. Wire it into mppx
const mppx = Mppx.create({
methods: [chargeMethod],
secretKey: process.env.MPP_SECRET_KEY!,
});
// 4. Use in your request handler
export async function handler(request: Request): Promise<Response> {
const result = await mppx.charge({
amount: "100",
currency: "sat",
description: "Premium API access",
})(request);
if (result.status === 402) return result.challenge;
return result.withReceipt(Response.json({ data: "..." }));
}The MPP client intercepts 402 responses automatically — paying invoices and retrying with credentials before returning the final response.
import { Mppx } from "mppx";
import { lightningChargeClient } from "@ambosstech/lightning-mpp-sdk";
import { LndLightningProvider } from "@ambosstech/lightning-mpp-adapter-lnd";
const provider = new LndLightningProvider({
transport: "rest",
url: "https://127.0.0.1:8080",
macaroon: process.env.LND_MACAROON!,
});
const chargeClient = lightningChargeClient(provider, {
maxFeeSats: 100,
onProgress: (event) => console.log(event.type),
});
const mppx = Mppx.create({
polyfill: false,
methods: [chargeClient],
});
const response = await mppx.fetch("https://api.example.com/weather");
console.log(await response.json());Sessions let clients deposit a lump sum and make multiple requests, with per-request billing deducted from the balance. Supports mid-stream top-ups and refunds on close.
import { Mppx } from "mppx";
import {
lightningSessionServer,
createMemoryStore,
} from "@ambosstech/lightning-mpp-sdk";
import { LndLightningProvider } from "@ambosstech/lightning-mpp-adapter-lnd";
const provider = new LndLightningProvider({
transport: "rest",
url: "https://127.0.0.1:8080",
macaroon: process.env.LND_MACAROON!,
});
const sessionMethod = lightningSessionServer({
provider,
depositAmount: 300, // sats required upfront
idleTimeout: 300, // auto-close after 5 min inactivity
unitType: "chunk",
});
const mppx = Mppx.create({
methods: [sessionMethod],
secretKey: process.env.MPP_SECRET_KEY!,
});
export async function handler(request: Request): Promise<Response> {
const result = await mppx.session({
amount: "2", // 2 sats per chunk
currency: "sat",
description: "LLM stream",
})(request);
if (result.status === 402) return result.challenge;
// Use the built-in serve() for automatic per-chunk billing over SSE
return result.withReceipt(
sessionMethod.serve({
sessionId: "...", // from the credential
satsPerChunk: 2,
generate: myAsyncGenerator(),
timeoutMs: 60_000, // wait 60s for top-up before closing
}),
);
}The serve() method handles the full SSE lifecycle:
- Deducts
satsPerChunkfrom the session balance for each yielded value - Emits
payment-need-topupwhen balance is exhausted - Holds the connection open until the client tops up or timeout elapses
- Emits
payment-receiptand[DONE]on stream completion
import { Mppx } from "mppx";
import { lightningSessionClient } from "@ambosstech/lightning-mpp-sdk";
import { LndLightningProvider } from "@ambosstech/lightning-mpp-adapter-lnd";
const provider = new LndLightningProvider({
transport: "rest",
url: "https://127.0.0.1:8080",
macaroon: process.env.LND_MACAROON!,
});
const sessionClient = lightningSessionClient(provider, {
maxFeeSats: 100,
onProgress: (event) => console.log(event.type),
});
const mppx = Mppx.create({
polyfill: false,
methods: [sessionClient],
});
// First request opens the session and pays the deposit
const response1 = await mppx.fetch("https://api.example.com/generate");
// Subsequent requests reuse the session (bearer auth, no payment)
const response2 = await mppx.fetch("https://api.example.com/generate");
// Top up if balance is exhausted mid-stream
await sessionClient.topUp(mppx.fetch, "https://api.example.com/generate");
// Close the session and get a refund for unspent balance
const closeResponse = await sessionClient.close(
mppx.fetch,
"https://api.example.com/generate",
);
const { refundSats } = await closeResponse.json();The mock adapter lets you test the full payment flow without a real Lightning node:
import {
lightningChargeServer,
lightningChargeClient,
createMemoryStore,
} from "@ambosstech/lightning-mpp-sdk";
import { MockLightningProvider } from "@ambosstech/lightning-mpp-adapter-mock";
// Server-side mock (auto-settles invoices)
const serverProvider = new MockLightningProvider({ autoSettle: true });
const server = lightningChargeServer({ provider: serverProvider });
// Client-side mock (pays invoices instantly)
const clientProvider = new MockLightningProvider();
const client = lightningChargeClient(clientProvider);
// Test failure scenarios
const failProvider = new MockLightningProvider({ failOnPay: true });
const failClient = lightningChargeClient(failProvider);
// Simulate slow payments
const slowProvider = new MockLightningProvider({ paymentDelay: 2000 });| Option | Type | Default | Description |
|---|---|---|---|
autoSettle |
boolean |
true |
Auto-mark invoices as settled on lookup |
failOnPay |
boolean |
false |
Throw RouteNotFoundError on payment |
paymentDelay |
number |
0 |
Artificial delay in ms before payment resolves |
import { LndLightningProvider } from "@ambosstech/lightning-mpp-adapter-lnd";
import { readFileSync } from "node:fs";
const provider = new LndLightningProvider({
transport: "grpc",
host: "127.0.0.1:10009",
tlsCert: readFileSync("/path/to/tls.cert"),
macaroon: readFileSync("/path/to/admin.macaroon"),
});const provider = new LndLightningProvider({
transport: "rest",
url: "https://127.0.0.1:8080",
macaroon: process.env.LND_MACAROON!, // hex-encoded
fetch: customFetchWithTLS, // optional: custom fetch for TLS cert handling
});Connect to any Nostr Wallet Connect (NIP-47) compatible wallet using a connection string:
import { NwcLightningProvider } from "@ambosstech/lightning-mpp-adapter-nwc";
const provider = new NwcLightningProvider({
connectionString: "nostr+walletconnect://pubkey?relay=wss://relay.example.com&secret=hex",
timeoutSecs: 60, // optional, default 60
});
// Use like any other provider
const invoice = await provider.createInvoice({ amountSats: 1000, memo: "test" });
// Clean up when done
provider.close();The connection string is typically provided by your wallet (Alby Hub, coinos, Primal, etc.). It contains the wallet pubkey, relay URL, and client secret. All communication is encrypted with NIP-44.
Note: NWC does not support
maxFeeSats— fee limits are controlled by the wallet. If provided, it will be ignored with a warning.
Implement LightningProvider to add support for any Lightning node or wallet:
import type { LightningProvider } from "@ambosstech/lightning-mpp-sdk";
class MyCustomProvider implements LightningProvider {
async createInvoice(params: {
amountSats: number;
memo?: string;
expirySecs?: number;
}) {
// Return { bolt11: string, paymentHash: string }
}
async payInvoice(params: {
bolt11: string;
amountSats?: number; // required for 0-amount invoices (e.g. session refunds)
maxFeeSats?: number;
timeoutSecs?: number;
}) {
// Return { preimage: string }
}
async lookupInvoice(params: { paymentHash: string }) {
// Return { settled: boolean, preimage?: string, amountSats?: number }
}
}Session state and consume-once tracking use a KeyValueStore interface. The default is in-memory — swap it for Redis, Cloudflare KV, DynamoDB, etc. in production:
import type { KeyValueStore } from "@ambosstech/lightning-mpp-sdk";
const redisStore: KeyValueStore = {
async get<T>(key: string): Promise<T | undefined> {
const value = await redis.get(key);
return value ? JSON.parse(value) : undefined;
},
async put<T>(key: string, value: T): Promise<void> {
await redis.set(key, JSON.stringify(value));
},
};
const server = lightningChargeServer({ provider, store: redisStore });The SDK provides typed error classes for common Lightning failure modes:
import {
LightningError,
InsufficientBalanceError,
InvoiceExpiredError,
RouteNotFoundError,
PaymentTimeoutError,
ConnectionError,
AuthenticationError,
} from "@ambosstech/lightning-mpp-sdk";
try {
await provider.payInvoice({ bolt11: invoice });
} catch (error) {
if (error instanceof RouteNotFoundError) {
// No path to destination — retry later or use a different route
} else if (error instanceof PaymentTimeoutError) {
// Payment did not complete in time
} else if (error instanceof InsufficientBalanceError) {
// Not enough local balance to send
}
}| Error | Code | When |
|---|---|---|
InsufficientBalanceError |
INSUFFICIENT_BALANCE |
Not enough local balance |
InvoiceExpiredError |
INVOICE_EXPIRED |
Invoice TTL has elapsed |
RouteNotFoundError |
ROUTE_NOT_FOUND |
No route to destination |
PaymentTimeoutError |
PAYMENT_TIMEOUT |
Payment did not complete in time |
ConnectionError |
CONNECTION_ERROR |
Cannot reach the Lightning node |
AuthenticationError |
AUTHENTICATION_ERROR |
Invalid macaroon or credentials |
| Option | Type | Default | Description |
|---|---|---|---|
provider |
LightningProvider |
required | Lightning node adapter |
store |
KeyValueStore |
in-memory | For consume-once tracking |
currency |
string |
"sat" |
Currency code sent in challenges |
network |
string |
— | Network name sent in challenges (e.g. "mainnet") |
invoiceExpirySecs |
number |
3600 |
Invoice TTL in seconds |
| Option | Type | Default | Description |
|---|---|---|---|
maxFeeSats |
number |
— | Maximum routing fee |
onProgress |
(event) => void |
— | Progress callback (challenge, paying, paid) |
| Option | Type | Default | Description |
|---|---|---|---|
provider |
LightningProvider |
required | Lightning node adapter |
store |
KeyValueStore |
in-memory | For session state |
currency |
string |
"sat" |
Currency code |
depositAmount |
number |
amount * 20 |
Deposit size in sats |
unitType |
string |
— | Label for the priced unit (e.g. "token") |
idleTimeout |
number |
300 |
Idle timeout in seconds (0 to disable) |
Returns the method plus { deduct, waitForTopUp, serve }:
deduct(sessionId, sats)— Deducts from balance. Returnstrueon success,falseif insufficient.waitForTopUp(sessionId, timeoutMs?)— Waits for a top-up. Returnstrueif topped up,falseon timeout.serve({ sessionId, satsPerChunk, generate, timeoutMs? })— Returns an SSEResponsewith automatic per-chunk billing.
| Option | Type | Default | Description |
|---|---|---|---|
maxFeeSats |
number |
— | Maximum routing fee |
onProgress |
(event) => void |
— | Progress callback (opening, bearer, topping-up, topped-up) |
Returns the method plus { close, topUp, getSession, resetSession }:
topUp(fetch, url)— Pays a new deposit invoice to add balance.close(fetch, url)— Closes the session and triggers a refund.getSession()— Returns{ sessionId }ornull.resetSession()— Clears local state (for server-initiated closes).
- Node.js 22+
- pnpm 9+
git clone https://github.com/ambosstech/lightning-mpp-sdk.git
cd lightning-mpp-sdk
pnpm installpnpm build # Build all packages
pnpm test # Run all tests
pnpm test:watch # Run tests in watch mode
pnpm typecheck # TypeScript type checking
pnpm lint # ESLint
pnpm format # Prettierlightning-mpp-sdk/
├── packages/
│ ├── sdk/ # Core SDK — methods, provider interface, store, errors
│ │ └── src/
│ │ ├── methods/ # charge.ts, session.ts (client + server)
│ │ ├── session/ # SessionStateManager (low-level API)
│ │ ├── provider.ts # LightningProvider interface
│ │ ├── store.ts # KeyValueStore interface
│ │ ├── errors.ts # Typed error classes
│ │ └── preimage.ts # Crypto utilities (sha256 verification, hex/base64)
│ ├── adapter-lnd/ # LND adapter (gRPC + REST)
│ ├── adapter-nwc/ # NWC adapter (Nostr Wallet Connect)
│ └── adapter-mock/ # Mock adapter for testing
├── package.json # Workspace root
├── pnpm-workspace.yaml
├── turbo.json # Build orchestration
└── tsconfig.base.json # Shared TypeScript config
The Lightning payment method is defined in two IETF-formatted specifications within the HTTP Payment Authentication framework:
draft-lightning-charge-00— One-time BOLT11 invoice paymentsdraft-lightning-session-00— Prepaid sessions with per-unit billing and refund on close
MIT