Skip to main content

L402-Requests (.NET)

Three lines of C#. Paid APIs just work.

using L402Requests;

using var client = new L402HttpClient();
var response = await client.GetAsync("https://api.example.com/paid-resource");
Console.WriteLine(await response.Content.ReadAsStringAsync());

That's the entire integration. No payment logic. No invoice parsing. No retry code. No protocol knowledge required.

Behind the scenes, L402Requests detects the 402 challenge, pays the Lightning invoice from your wallet, caches the credential, and retries the request. You get back a normal HttpResponseMessage. The API just worked — and it got paid.

Install

dotnet add package L402Requests

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                L402Requests               L402 Server            Lightning
──────── ──────────── ─────────── ─────────
│ │ │ │
│── GetAsync("/data") ────▶│ │ │
│ │──── GET /data ──────────▶│ │
│ │◀── 402 + invoice + mac ──│ │
│ │ │ │
│ │ check budget │ │
│ │ extract amount │ │
│ │ │ │
│ │──── pay invoice ────────────────────────────── ▶│
│ │◀─── preimage ───────────────────────────────── │
│ │ │ │
│ │── GET /data ────────────▶│ │
│ │ Authorization: L402 │ │
│ │◀──── 200 + data ────────│ │
│◀── 200 + data ──────────│ │ │
  1. You make an HTTP request — client.GetAsync(url)
  2. If the server returns 200, the response comes back as-is
  3. 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}
  4. 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:

PriorityWalletEnvironment VariablesPreimageNotes
1LNDLND_REST_HOST + LND_MACAROON_HEXYesRequires running a node
2NWCNWC_CONNECTION_STRINGYesCoinOS, CLINK compatible
3StrikeSTRIKE_API_KEYYesNo infrastructure required
4OpenNodeOPENNODE_API_KEYLimitedNo preimage support
Recommended: Strike

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"
export LND_TLS_CERT_PATH="/path/to/tls.cert" # optional

NWC (Nostr Wallet Connect)

export NWC_CONNECTION_STRING="nostr+walletconnect://pubkey?relay=wss://relay&secret=hex"

OpenNode

export OPENNODE_API_KEY="your-opennode-key"
OpenNode L402 Limitation

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.

WalletReturns PreimageL402 WorksNotes
LNDYesYesRequires running a node
NWC (CoinOS)YesYesFree, easy setup
NWC (CLINK)YesYesNostr users
StrikeYesYesEasy setup, no infrastructure
Alby HubYesYesSelf-custody, NWC compatible
PrimalNoNoDirect payments only
OpenNodeNoNoDirect 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:

using L402Requests;
using L402Requests.Wallets;

using var client = new L402HttpClient(new StrikeWallet("your-api-key"));
var response = await client.GetAsync("https://api.example.com/paid-resource");

Budget Controls

Safety is built in. Budgets are enabled by default so you can't accidentally overspend:

using var client = new L402HttpClient(new L402Options
{
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 = ["api.example.com"], // Optional domain allowlist
});

If a payment would exceed any limit, BudgetExceededException is raised before the payment is attempted — no sats leave your wallet.

To disable budgets entirely:

using var client = new L402HttpClient(new L402Options { BudgetEnabled = false }); // Not recommended

Default Limits

LimitDefaultDescription
MaxSatsPerRequest1,000 satsRejects any single invoice above this
MaxSatsPerHour10,000 satsRolling 1-hour window
MaxSatsPerDay50,000 satsRolling 24-hour window

Domain Allowlist

Restrict payments to specific domains:

using var client = new L402HttpClient(new L402Options
{
AllowedDomains = ["api.example.com", "store.lightningenable.com"],
});

Any request to a domain not in the list will raise DomainNotAllowedException before attempting payment.

DI / HttpClientFactory

For ASP.NET Core and services using dependency injection:

// In Program.cs
builder.Services.AddL402HttpClient("myapi", options =>
{
options.MaxSatsPerRequest = 500;
options.MaxSatsPerHour = 5000;
options.AllowedDomains = ["api.example.com"];
});

// In consuming class
public class MyService(IHttpClientFactory factory)
{
public async Task<string> GetPaidData()
{
var client = factory.CreateClient("myapi");
var response = await client.GetAsync("https://api.example.com/paid-resource");
return await response.Content.ReadAsStringAsync();
}
}

Spending Introspection

Track every payment made during a session:

using var client = new L402HttpClient();
await client.GetAsync("https://api.example.com/data");
await client.GetAsync("https://api.example.com/more-data");

// Inspect spending
Console.WriteLine($"Total: {client.SpendingLog.TotalSpent()} sats");
Console.WriteLine($"Last hour: {client.SpendingLog.SpentLastHour()} sats");
Console.WriteLine($"Today: {client.SpendingLog.SpentToday()} sats");
Console.WriteLine($"By domain: {string.Join(", ", client.SpendingLog.ByDomain())}");

// Export as JSON for auditing
Console.WriteLine(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.

using var client = new L402HttpClient(new L402Options
{
CacheMaxSize = 256, // Maximum cached credentials
CacheTtlSeconds = 3600.0, // 1 hour TTL
});

Error Handling

using L402Requests;

using var client = new L402HttpClient();
try
{
var response = await client.GetAsync("https://api.example.com/paid-resource");
}
catch (BudgetExceededException e)
{
Console.WriteLine($"Over budget: {e.LimitType} limit is {e.LimitSats} sats");
}
catch (PaymentFailedException e)
{
Console.WriteLine($"Payment failed: {e.Reason}");
}
catch (NoWalletException)
{
Console.WriteLine("No wallet configured — set STRIKE_API_KEY or other wallet env vars");
}
ExceptionWhen
BudgetExceededExceptionPayment would exceed a budget limit
PaymentFailedExceptionLightning payment failed (routing, timeout, etc.)
InvoiceExpiredExceptionInvoice expired before payment
NoWalletExceptionNo wallet env vars detected
DomainNotAllowedExceptionDomain not in AllowedDomains
ChallengeParseExceptionMalformed L402 challenge header

Example: Lightning Enable Store

Access the Lightning Enable Store — a live L402 commerce demo.

Budget Configuration Required

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.

US Shipping Only

The Lightning Enable Store currently ships to US addresses only.

The store uses a two-step L402 flow designed for physical goods commerce:

using System.Net.Http.Json;
using L402Requests;

// Step 1: Browse catalog (free, no payment)
using var client = new L402HttpClient(new L402Options
{
MaxSatsPerRequest = 50000,
});

var catalog = await client.GetAsync("https://store.lightningenable.com/api/store/catalog");
var catalogJson = await catalog.Content.ReadAsStringAsync();
Console.WriteLine(catalogJson);

// Step 2: Checkout (returns 402 — L402Requests pays the invoice automatically)
var checkoutContent = JsonContent.Create(new
{
items = new[] { new { productId = 2, quantity = 1, size = "L", color = "Black" } }
});
var checkout = await client.PostAsync(
"https://store.lightningenable.com/api/store/checkout", checkoutContent);

// Payment was made — retrieve credentials from the spending log
var record = client.SpendingLog.Records[^1];
Console.WriteLine($"Paid {record.AmountSats} sats, preimage: {record.Preimage}");

API Reference

L402HttpClient

new L402HttpClient()                              // Auto-detect wallet, default options
new L402HttpClient(IWallet wallet) // Explicit wallet
new L402HttpClient(L402Options options) // Custom options
new L402HttpClient(IWallet? wallet, L402Options? options) // Both

Methods: .GetAsync(), .PostAsync(), .PutAsync(), .PatchAsync(), .DeleteAsync(), .SendAsync()

Properties:

  • .SpendingLogSpendingLog instance for payment history

L402Options

new L402Options
{
MaxSatsPerRequest = 1000, // Default: 1000
MaxSatsPerHour = 10000, // Default: 10000
MaxSatsPerDay = 50000, // Default: 50000
AllowedDomains = null, // null = all domains
Wallet = null, // null = auto-detect
BudgetEnabled = true, // false to disable all limits
CacheMaxSize = 256, // LRU cache size
CacheTtlSeconds = 3600.0, // null to disable expiration
}

ServiceCollectionExtensions

services.AddL402HttpClient("name", options => { ... });
services.AddL402HttpClient<TClient>(options => { ... });

Wallet Classes

  • StrikeWallet(string apiKey)
  • LndWallet(string host, string macaroonHex, string? tlsCertPath = null)
  • NwcWallet(string connectionString, TimeSpan? timeout = null)
  • OpenNodeWallet(string apiKey)

Also Available

  • Python: l402-requests — same "three lines of code" experience for Python
  • TypeScript: l402-requests — same "three lines of code" experience for TypeScript/Node.js

Source Code

GitHub Repository (MIT License)