L402-Requests (TypeScript)
Three lines of TypeScript. Paid APIs just work.
import { get } from 'l402-requests';
const response = await get("https://api.example.com/paid-resource");
console.log(await response.json());
That's the entire integration. No payment logic. No invoice parsing. No retry code. No protocol knowledge required.
Behind the scenes, l402-requests detects the 402 challenge, pays the Lightning invoice from your wallet, caches the credential, and retries the request. You get back a normal Response. The API just worked — and it got paid.
Install
npm install l402-requests
Set one environment variable for your wallet and you're done:
export STRIKE_API_KEY="your-strike-api-key"
That's it. Every L402-protected API you call will automatically get paid.
How It Works
You never see any of this — it happens automatically:
Your Code l402-requests L402 Server Lightning
───────── ───────────── ─────────── ─────────
│ │ │ │
│── GET /resource ──▶│ │ │
│ │── GET /resource ───────▶│ │
│ │◀── 402 + invoice + mac ─│ │
│ │ │ │
│ │ check budget │ │
│ │ extract amount │ │
│ │ │ │
│ │── pay invoice ──────────────────────────────▶│
│ │◀── preimage ────────────────────────────────│
│ │ │ │
│ │── GET /resource ────────▶│ │
│ │ Authorization: L402 │ │
│ │◀──── 200 + data ────────│ │
│◀── 200 + data ─────│ │ │
- You make an HTTP request —
get(url) - If the server returns 200, the response comes back as-is
- If the server returns 402 with an L402 challenge:
- The invoice is parsed automatically
- The amount is checked against your budget
- The invoice is paid via your Lightning wallet
- The request is retried with
Authorization: L402 {macaroon}:{preimage}
- Credentials are cached — subsequent requests to the same endpoint don't re-pay
Wallet Configuration
Set environment variables for your wallet. The library auto-detects in priority order:
| Priority | Wallet | Environment Variables | Preimage | Notes |
|---|---|---|---|---|
| 1 | LND | LND_REST_HOST + LND_MACAROON_HEX | Yes | Requires running a node |
| 2 | NWC | NWC_CONNECTION_STRING | Yes | CoinOS, CLINK compatible |
| 3 | Strike | STRIKE_API_KEY | Yes | No infrastructure required |
| 4 | OpenNode | OPENNODE_API_KEY | Limited | No preimage support |
Strike has full preimage support and requires no infrastructure. Set STRIKE_API_KEY and you're done.
Strike
export STRIKE_API_KEY="your-strike-api-key"
LND
export LND_REST_HOST="https://localhost:8080"
export LND_MACAROON_HEX="your-admin-macaroon-hex"
NWC (Nostr Wallet Connect)
NWC requires optional peer dependencies:
npm install @noble/secp256k1 ws
export NWC_CONNECTION_STRING="nostr+walletconnect://pubkey?relay=wss://relay&secret=hex"
OpenNode
export OPENNODE_API_KEY="your-opennode-key"
OpenNode does not return payment preimages, which means L402 credential construction will fail. For L402 use cases, use Strike, LND, or a compatible NWC wallet.
L402 Wallet Compatibility
L402 requires the payment preimage (proof of payment) to construct credentials. Not all wallets return it. If yours doesn't, payment succeeds but API access fails.
| Wallet | Returns Preimage | L402 Works | Notes |
|---|---|---|---|
| LND | Yes | Yes | Requires running a node |
| NWC (CoinOS) | Yes | Yes | Free, easy setup |
| NWC (CLINK) | Yes | Yes | Nostr users |
| Strike | Yes | Yes | Easy setup, no infrastructure |
| Alby Hub | Yes | Yes | Self-custody, NWC compatible |
| Primal | No | No | Direct payments only |
| OpenNode | No | No | Direct payments only |
For detailed wallet setup instructions, see the MCP Wallet Setup Guide.
Explicit Wallet
You can also pass a wallet directly instead of relying on auto-detection:
import { L402Client, StrikeWallet } from 'l402-requests';
const client = new L402Client({
wallet: new StrikeWallet("your-key"),
});
const response = await client.get("https://api.example.com/paid-resource");
Budget Controls
Safety is built in. Budgets are enabled by default so you can't accidentally overspend:
import { L402Client, BudgetController } from 'l402-requests';
const client = new L402Client({
budget: new BudgetController({
maxSatsPerRequest: 500, // Max per single payment (default: 1,000)
maxSatsPerHour: 5000, // Hourly rolling limit (default: 10,000)
maxSatsPerDay: 25000, // Daily rolling limit (default: 50,000)
allowedDomains: new Set(["api.example.com"]),
}),
});
If a payment would exceed any limit, BudgetExceededError is thrown before the payment is attempted — no sats leave your wallet.
To disable budgets entirely:
const client = new L402Client({ budget: null }); // Not recommended
Default Limits
| Limit | Default | Description |
|---|---|---|
maxSatsPerRequest | 1,000 sats | Rejects any single invoice above this |
maxSatsPerHour | 10,000 sats | Rolling 1-hour window |
maxSatsPerDay | 50,000 sats | Rolling 24-hour window |
Domain Allowlist
Restrict payments to specific domains:
const budget = new BudgetController({
allowedDomains: new Set(["api.example.com", "store.lightningenable.com"]),
});
Any request to a domain not in the list will raise DomainNotAllowedError before attempting payment.
Spending Introspection
Track every payment made during a session:
import { L402Client } from 'l402-requests';
const client = new L402Client();
await client.get("https://api.example.com/data");
await client.get("https://api.example.com/more-data");
// Inspect spending
console.log(`Total: ${client.spendingLog.totalSpent()} sats`);
console.log(`Last hour: ${client.spendingLog.spentLastHour()} sats`);
console.log(`Today: ${client.spendingLog.spentToday()} sats`);
console.log(`By domain:`, client.spendingLog.byDomain());
// Export as JSON for auditing
console.log(client.spendingLog.toJSON());
Credential Caching
L402 credentials are cached by (domain, path_prefix) so you don't re-pay for the same endpoint within a session. The cache uses an LRU eviction strategy with a default TTL of 1 hour.
import { L402Client, CredentialCache } from 'l402-requests';
const client = new L402Client({
credentialCache: new CredentialCache({
maxSize: 256, // Maximum cached credentials
defaultTtlMs: 3_600_000, // 1 hour TTL
}),
});
Error Handling
import { L402Client, BudgetExceededError, PaymentFailedError, NoWalletError } from 'l402-requests';
const client = new L402Client();
try {
const response = await client.get("https://api.example.com/paid-resource");
} catch (e) {
if (e instanceof BudgetExceededError) {
console.log(`Over budget: ${e.limitType} limit is ${e.limitSats} sats`);
} else if (e instanceof PaymentFailedError) {
console.log(`Payment failed: ${e.reason}`);
} else if (e instanceof NoWalletError) {
console.log("No wallet configured — set STRIKE_API_KEY or other wallet env vars");
}
}
| Exception | When |
|---|---|
BudgetExceededError | Payment would exceed a budget limit |
PaymentFailedError | Lightning payment failed (routing, timeout, etc.) |
InvoiceExpiredError | Invoice expired before payment |
NoWalletError | No wallet env vars detected |
DomainNotAllowedError | Domain not in allowedDomains |
ChallengeParseError | Malformed L402 challenge header |
Example: Lightning Enable Store
Access the Lightning Enable Store — a live L402 commerce demo.
Store products cost 25,000 - 45,000+ sats (including shipping). The default budget limit of 1,000 sats per request will reject these payments. You must increase maxSatsPerRequest before purchasing.
import { L402Client, BudgetController, parseChallenge } from 'l402-requests';
// Step 1: Browse catalog (free, no payment)
const client = new L402Client({
budget: new BudgetController({ maxSatsPerRequest: 50000 }),
});
const catalog = await client.get("https://store.lightningenable.com/api/store/catalog");
const products = (await catalog.json()).products;
for (const p of products) {
console.log(`[${p.id}] ${p.name} — ${p.priceSats} sats`);
}
// Step 2: Checkout (returns 402 — l402-requests pays the invoice automatically)
const checkout = await client.post(
"https://store.lightningenable.com/api/store/checkout",
{
body: JSON.stringify({
items: [{ productId: 2, quantity: 1, size: "L", color: "Black" }],
}),
headers: { "Content-Type": "application/json" },
},
);
// Step 3: Claim the order using the credential from the spending log
const record = client.spendingLog.records[client.spendingLog.records.length - 1];
const claim = await fetch("https://store.lightningenable.com/api/store/claim", {
method: "POST",
headers: {
"Authorization": `L402 ${record.preimage}`, // Use cached credential
"Content-Type": "application/json",
},
body: "{}",
});
const claimData = await claim.json();
console.log(`Claim URL: ${claimData.claimUrl}`);
API Reference
L402Client
new L402Client({
wallet?: Wallet,
budget?: BudgetController | null, // undefined = default, null = disabled
credentialCache?: CredentialCache,
fetchOptions?: RequestInit,
})
Methods: .get(), .post(), .put(), .delete(), .patch(), .head(), .fetch()
Properties:
.spendingLog—SpendingLoginstance for payment history
Module-Level Convenience Functions
import { get, post, put, del, patch } from 'l402-requests';
Uses a lazy singleton L402Client with default options.
BudgetController
new BudgetController({
maxSatsPerRequest?: number, // default: 1000
maxSatsPerHour?: number, // default: 10000
maxSatsPerDay?: number, // default: 50000
allowedDomains?: Set<string>, // default: null (all domains)
})
Wallet Classes
StrikeWallet(apiKey: string, baseUrl?: string)LndWallet(host: string, macaroonHex: string)NwcWallet(connectionString: string, timeout?: number)OpenNodeWallet(apiKey: string, baseUrl?: string)
Zero Dependencies
The core library has zero required dependencies. It uses Node.js 18+ built-in fetch() for HTTP requests. Only the NWC wallet adapter requires optional peer dependencies (@noble/secp256k1 and ws).
Also Available
- Python:
l402-requests— same "three lines of code" experience for Python - .NET:
L402Requests— same experience for .NET
Source Code
GitHub Repository (MIT License)