Native L402 Integration — ASP.NET Core
This walkthrough takes you from a vanilla ASP.NET Core app to charging Lightning payments per request in under 10 minutes. You'll install L402Server.AspNetCore, add one line of middleware + an attribute, and your existing endpoints become paid endpoints.
If you haven't picked an integration mode yet, start with the Native Integration overview to understand when Native mode is the right fit.
Prerequisites
- .NET 8.0 or higher
- An ASP.NET Core app
- A Lightning Enable merchant API key — generate at Dashboard → Settings → API Keys
- A payment provider (Strike or OpenNode) configured under Dashboard → Settings → Payment Provider
Install
dotnet add package L402Server.AspNetCore
(L402Server — the underlying SDK — is pulled in transitively. Both are MIT-licensed.)
30-second example
using L402Server.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddL402AspNetCore(opts =>
{
opts.ApiKey = builder.Configuration["LightningEnable:ApiKey"]!;
});
var app = builder.Build();
app.UseRouting();
app.UseL402();
app.MapControllers();
app.Run();
Then mark any controller action with [L402(PriceSats = N)]:
[ApiController]
[Route("api/premium")]
public class PremiumController : ControllerBase
{
[HttpGet("weather")]
[L402(PriceSats = 100)]
public IActionResult Weather() => Ok(new { temp = 72 });
}
That's the whole integration. The middleware:
- Reads
Authorization: L402 <macaroon>:<preimage>from each request - If the matched endpoint has
[L402]and no valid credential → mints a fresh challenge via Lightning Enable's hosted producer API and returns402 Payment Required - If a valid credential is present → executes the action
Endpoints without [L402] pass through ungated.
What the caller sees
Without payment
curl -i https://your-api.example/api/premium/weather
HTTP/1.1 402 Payment Required
Content-Type: application/json
WWW-Authenticate: L402 macaroon="AgELbWFjYXJvb24...", invoice="lnbc1u1p3..."
{
"error": "Payment Required",
"l402": {
"macaroon": "AgELbWFjYXJvb24...",
"invoice": "lnbc1u1p3...",
"amount_sats": 100,
"payment_hash": "abc123...",
"expires_at": "2026-05-12T01:00:00Z",
"resource": "/api/premium/weather"
}
}
With payment
curl -i https://your-api.example/api/premium/weather \
-H 'Authorization: L402 AgELbWFjYXJvb24...:deadbeef...'
HTTP/1.1 200 OK
Content-Type: application/json
{ "temp": 72 }
Pricing patterns
Per-route attributes (compile-time prices)
[HttpGet("forecast"), L402(PriceSats = 100)]
public IActionResult Forecast() => Ok(...);
[HttpGet("premium-llm"), L402(PriceSats = 500, Description = "GPT-4 backed")]
public IActionResult PremiumLlm() => Ok(...);
Global flat price (gates everything mounted under the middleware)
app.UseL402(opts => opts.DefaultPriceSats = 100);
Function-form pricing (variable per request)
app.UseL402(opts =>
{
opts.PriceSelector = ctx => ValueTask.FromResult(
ctx.Request.Query["model"] == "premium" ? 500 : 100);
});
Resolution order:
PriceSelector(if set, wins everywhere)[L402(PriceSats = N)]attribute on the matched endpointDefaultPriceSatson options- None → request passes through ungated
Configuration reference
L402AspNetCoreOptions:
| Option | Type | Default | Notes |
|---|---|---|---|
DefaultPriceSats | int? | null | Flat price applied when no [L402] attribute is present |
PriceSelector | Func<HttpContext, ValueTask<int>>? | null | Variable pricing per request — overrides attribute + default |
ResourceSelector | Func<HttpContext, string>? | HttpContext.Request.Path | Bound as a macaroon caveat |
DescriptionSelector | Func<HttpContext, string?>? | null | Shown in the payer's Lightning wallet |
IdempotencyKeySelector | Func<HttpContext, string?>? | null | Sends X-Idempotency-Key for retry-safe challenge minting |
OnInvalidToken | Func<HttpContext, VerificationResult, ValueTask>? | sends 401 | Custom handler |
Accessing the verified credential in your action
After a successful verification the middleware sets HttpContext.Items[L402HttpContextKeys.VerificationResult]:
[HttpGet("weather"), L402(PriceSats = 100)]
public IActionResult Weather(HttpContext ctx)
{
var result = (VerificationResult)ctx.Items[L402HttpContextKeys.VerificationResult]!;
_logger.LogInformation(
"Served {Resource} for {Sats} sats ({Hash})",
result.Resource, result.AmountSats, result.PaymentHash);
return Ok(new { temp = 72 });
}
Useful for usage logging, per-endpoint analytics, fraud detection.
Minimal API support
[L402] is a regular Attribute so it works on minimal-API metadata too:
app.MapGet("/api/premium/weather", () => new { temp = 72 })
.WithMetadata(new L402Attribute { PriceSats = 100 });
Using the SDK directly (without the middleware)
If you need to mint a challenge or verify a token outside the request pipeline — from a background service, a hosted worker, an HTTP handler in a non-ASP.NET context — use L402Server directly:
using L402Server;
var client = new L402ServerClient(new L402ServerOptions
{
ApiKey = Environment.GetEnvironmentVariable("LIGHTNING_ENABLE_API_KEY")!,
});
var challenge = await client.CreateChallengeAsync(new CreateChallengeRequest
{
Resource = "/api/x",
PriceSats = 100,
});
var verification = await client.VerifyTokenAsync(new VerifyTokenRequest
{
Macaroon = mac,
Preimage = pre,
});
Pipeline order
The middleware needs to be placed AFTER UseRouting() (so it can read [L402] attribute metadata from the matched endpoint) and BEFORE the endpoint executor (MapControllers, UseEndpoints, etc.):
app.UseRouting();
app.UseAuthentication(); // any other auth middleware
app.UseAuthorization();
app.UseL402(); // ← here
app.MapControllers();
If you put UseL402() before UseRouting() the middleware will see no matched endpoint and won't find [L402] attributes. If you put it after MapControllers() it'll never run.
Custom failure handling
Default behavior: invalid L402 token → 401 Unauthorized. Override with OnInvalidToken to send a fresh 402 instead:
app.UseL402(opts =>
{
opts.OnInvalidToken = async (ctx, failure) =>
{
ctx.Response.StatusCode = 402;
await ctx.Response.WriteAsJsonAsync(new
{
error = "Token rejected — please pay again",
details = failure.Error,
});
};
});
When OnInvalidToken is supplied, the middleware does NOT send the default 401 and does NOT continue to the next middleware — your callback is fully responsible for producing the response. Write a status code + body via ctx.Response, or redirect via ctx.Response.Redirect(...). The pipeline short-circuits after your callback returns. (The callback signature is Func<HttpContext, VerificationResult, ValueTask> — there's no next delegate passed in.)
Troubleshooting
502 Bad Gateway on every request
The middleware couldn't reach Lightning Enable. Check:
LIGHTNING_ENABLE_API_KEYis set and valid- No outbound firewall blocking
api.lightningenable.com - Subscription is active
Every request returns 402 even after payment
Behind a reverse proxy? Confirm the Authorization header is being forwarded to the ASP.NET Core app. In Azure App Service and most reverse-proxy setups this just works; some custom proxies strip the header.
403 Forbidden from upstream
L402 isn't enabled on your subscription plan. Check Dashboard → Settings → Plan — Native mode requires Agentic Commerce — Individual or Business.
[L402] attributes don't seem to work
Verify app.UseL402() is placed AFTER app.UseRouting() and BEFORE app.MapControllers() / app.UseEndpoints(). The middleware reads attribute metadata from the matched endpoint, which only exists after routing has run.
Source and license
Both packages are MIT-licensed open source:
Next steps
- Producer API reference — full HTTP surface
- Native Integration — Express — Node + Express version of this walkthrough
- Proxy setup walkthrough — if Proxy mode is a better fit