Lightning MPP SDK

March 20, 2026 · View on GitHub

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.

How it works

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.

Packages

PackageDescription
@ambosstech/lightning-mpp-sdkCore SDK — method definitions, provider interface, store, errors
@ambosstech/lightning-mpp-adapter-lndLND adapter — gRPC and REST transports
@ambosstech/lightning-mpp-adapter-nwcNWC adapter — Nostr Wallet Connect (NIP-47) over relays
@ambosstech/lightning-mpp-adapter-mockMock adapter — for testing without a real Lightning node

Quick start

Installation

# 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 mppx

Server — charge (one-time payment)

Uses 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: "..." }));
}

Client — charge (one-time payment)

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

Server — session (prepaid metered access)

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 satsPerChunk from the session balance for each yielded value
  • Emits payment-need-topup when balance is exhausted
  • Holds the connection open until the client tops up or timeout elapses
  • Emits payment-receipt and [DONE] on stream completion

Client — session

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

Testing with the mock adapter

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

Mock provider options

OptionTypeDefaultDescription
autoSettlebooleantrueAuto-mark invoices as settled on lookup
failOnPaybooleanfalseThrow RouteNotFoundError on payment
paymentDelaynumber0Artificial delay in ms before payment resolves

LND adapter configuration

gRPC transport

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"),
});

REST transport

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

NWC adapter configuration

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.

Provider interface

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 }
  }
}

Pluggable store

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

Error handling

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
  }
}
ErrorCodeWhen
InsufficientBalanceErrorINSUFFICIENT_BALANCENot enough local balance
InvoiceExpiredErrorINVOICE_EXPIREDInvoice TTL has elapsed
RouteNotFoundErrorROUTE_NOT_FOUNDNo route to destination
PaymentTimeoutErrorPAYMENT_TIMEOUTPayment did not complete in time
ConnectionErrorCONNECTION_ERRORCannot reach the Lightning node
AuthenticationErrorAUTHENTICATION_ERRORInvalid macaroon or credentials

API reference

Charge method

lightningChargeServer(options)

OptionTypeDefaultDescription
providerLightningProviderrequiredLightning node adapter
storeKeyValueStorein-memoryFor consume-once tracking
currencystring"sat"Currency code sent in challenges
networkstringNetwork name sent in challenges (e.g. "mainnet")
invoiceExpirySecsnumber3600Invoice TTL in seconds

lightningChargeClient(provider, options?)

OptionTypeDefaultDescription
maxFeeSatsnumberMaximum routing fee
onProgress(event) => voidProgress callback (challenge, paying, paid)

Session method

lightningSessionServer(options)

OptionTypeDefaultDescription
providerLightningProviderrequiredLightning node adapter
storeKeyValueStorein-memoryFor session state
currencystring"sat"Currency code
depositAmountnumberamount * 20Deposit size in sats
unitTypestringLabel for the priced unit (e.g. "token")
idleTimeoutnumber300Idle timeout in seconds (0 to disable)

Returns the method plus { deduct, waitForTopUp, serve }:

  • deduct(sessionId, sats) — Deducts from balance. Returns true on success, false if insufficient.
  • waitForTopUp(sessionId, timeoutMs?) — Waits for a top-up. Returns true if topped up, false on timeout.
  • serve({ sessionId, satsPerChunk, generate, timeoutMs? }) — Returns an SSE Response with automatic per-chunk billing.

lightningSessionClient(provider, options?)

OptionTypeDefaultDescription
maxFeeSatsnumberMaximum routing fee
onProgress(event) => voidProgress 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 } or null.
  • resetSession() — Clears local state (for server-initiated closes).

Development

Prerequisites

  • Node.js 22+
  • pnpm 9+

Setup

git clone https://github.com/ambosstech/lightning-mpp-sdk.git
cd lightning-mpp-sdk
pnpm install

Commands

pnpm 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         # Prettier

Project structure

lightning-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

Specifications

The Lightning payment method is defined in two IETF-formatted specifications within the HTTP Payment Authentication framework:

License

MIT