> ## Documentation Index
> Fetch the complete documentation index at: https://shinydapps-bd9fa40b.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Production Guide

> Deploy l402-kit APIs to production — performance, reliability, and scaling.

## Pre-launch checklist

Run through this before going live. Each item links to the relevant section below.

<Steps>
  <Step title="Replay protection configured for multi-instance">
    Default adapter is in-memory only. If you run more than one process (Gunicorn workers, PM2 cluster, Kubernetes), set `SUPABASE_URL` + `SUPABASE_ANON_KEY` or use `RedisReplayAdapter`. [Details →](#critical-replay-protection-in-production)
  </Step>

  <Step title="Environment variables in secrets manager">
    Never hardcode API keys. Use `.env` locally, secrets manager in prod. [Details →](#environment-variables)
  </Step>

  <Step title="Health check endpoint exists">
    Monitoring pings must not trigger invoice creation. Add a `/health` route before your paid routes. [Details →](#health-check-endpoint)
  </Step>

  <Step title="Error responses don't leak internals">
    Catch provider errors and return a clean 503 — not a stack trace. [Details →](#error-handling)
  </Step>

  <Step title="Price is intentional">
    `priceSats` should reflect real value. At 1 sat ≈ $0.0006, 100 sats ≈ $0.06 for a premium endpoint is reasonable. Don't set it to 0 by accident.
  </Step>

  <Step title="Uptime monitoring on the invoice endpoint">
    Monitor the 402 response (it's a normal response, not an error). Tools like UptimeRobot support custom status code expectations.
  </Step>

  <Step title="Rate limiting on invoice creation">
    Every unauthenticated request creates a Lightning invoice. Without rate limiting, an attacker can exhaust your provider's invoice quota for free. Add `express-rate-limit` before deploying publicly. [Details →](#rate-limiting)
  </Step>
</Steps>

***

## Critical: replay protection in production

<Warning>
  The default replay adapter is **in-memory only** — it resets on every process restart and does not work across multiple server instances. In production with more than one process (Gunicorn workers, Kubernetes pods, PM2 cluster), the same preimage can be accepted twice.

  **Fix:** Set `SUPABASE_URL` + `SUPABASE_ANON_KEY` in your environment. The middleware will automatically use Supabase as the replay store, which is shared across all instances.

  For Redis: pass a `RedisReplayAdapter` explicitly (see [TypeScript SDK](/sdk/typescript#replay-protection) or [Python SDK](/sdk/python#replay-protection)).
</Warning>

***

## Environment variables

Never hardcode keys. Always use environment variables:

```bash theme={null}
# .env (never commit this)
BLINK_API_KEY=blink_xxx
BLINK_WALLET_ID=your-wallet-id
SUPABASE_URL=https://xxx.supabase.co
SUPABASE_ANON_KEY=sb_publishable_xxx    # safe to expose to clients
SUPABASE_SERVICE_KEY=sb_secret_xxx      # server-side only — never expose to clients
```

```typescript theme={null}
import 'dotenv/config';
import { BlinkProvider } from 'l402-kit';

const blink = new BlinkProvider(
  process.env.BLINK_API_KEY!,
  process.env.BLINK_WALLET_ID!,
);
```

***

## Payment logging (Supabase)

Log every payment to Supabase for the VS Code extension dashboard and analytics:

```typescript theme={null}
import { createClient } from '@supabase/supabase-js';

const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_ANON_KEY!,
);

app.use(async (req, res, next) => {
  const original = res.json.bind(res);
  res.json = (body) => {
    // Log successful L402 payments (after middleware passes)
    if (req.l402Paid && req.l402Preimage) {
      supabase.from('payments').insert({
        endpoint: req.path,
        preimage: req.l402Preimage,
        amount_sats: req.l402AmountSats,
      }).then(() => {});
    }
    return original(body);
  };
  next();
});
```

***

## Cloudflare Workers deployment

l402-kit API runs on Cloudflare Workers (V8 isolates, zero cold start). The in-memory replay store resets per isolate — for high-traffic APIs, use Cloudflare KV or Durable Objects for replay protection.

```typescript theme={null}
// api/data.ts
import { l402 } from 'l402-kit';
import { BlinkProvider } from 'l402-kit';

const blink = new BlinkProvider(
  process.env.BLINK_API_KEY!,
  process.env.BLINK_WALLET_ID!,
);

export default async function handler(req, res) {
  await new Promise<void>((resolve, reject) => {
    l402({ priceSats: 10, lightning: blink })(req, res, (err) => {
      if (err) reject(err); else resolve();
    });
  });
  res.json({ data: 'premium content' });
}
```

***

## Docker

```dockerfile theme={null}
FROM node:20-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY dist ./dist
ENV NODE_ENV=production
EXPOSE 3000
CMD ["node", "dist/server.js"]
```

```yaml theme={null}
# docker-compose.yml
services:
  api:
    build: .
    environment:
      BLINK_API_KEY: ${BLINK_API_KEY}
      BLINK_WALLET_ID: ${BLINK_WALLET_ID}
    ports:
      - "3000:3000"
```

***

## User data deletion

Expose a deletion endpoint so users can permanently remove their data. The l402-kit backend includes `/api/delete-data` out of the box:

```http theme={null}
POST /api/delete-data
Content-Type: application/json

{ "lightningAddress": "user@blink.sv" }
```

```json theme={null}
// 200 OK
{ "deleted": { "payments": 42, "proAccess": true } }
```

The VS Code extension surfaces this as a **Danger Zone** panel at the bottom of the dashboard — users must type their Lightning address to confirm, then all payment history and Pro access are wiped. The operation uses the Supabase service key server-side; the anon key has no DELETE permission.

***

## Error handling

Catch provider errors before they surface as unformatted 500s:

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { l402, L402Error } from 'l402-kit';

  app.get('/api/data', l402({ priceSats: 10, lightning }), async (req, res) => {
    try {
      res.json({ data: await fetchData() });
    } catch (err) {
      if (err instanceof L402Error) {
        // Token invalid, expired, or already used — let the middleware handle it
        return res.status(err.status).json({ error: err.code });
      }
      console.error('[api/data]', err);
      res.status(503).json({ error: 'Service temporarily unavailable' });
    }
  });
  ```

  ```python Python theme={null}
  from l402kit import l402_required, L402Error
  from fastapi import Request
  from fastapi.responses import JSONResponse

  @app.exception_handler(L402Error)
  async def l402_exception_handler(request: Request, exc: L402Error):
      return JSONResponse(status_code=exc.status_code, content={"error": exc.code})

  @app.exception_handler(Exception)
  async def generic_exception_handler(request: Request, exc: Exception):
      return JSONResponse(status_code=503, content={"error": "Service temporarily unavailable"})
  ```
</CodeGroup>

**Rules of thumb:**

* Never return stack traces to clients — log them server-side.
* Provider timeouts (503) are transient — safe to retry with backoff.
* Token errors (401) are never transient — don't auto-retry, require a new invoice.
* Rate limit errors (429) — surface the `retryAfter` field to the caller.

See [Error Reference](/reference/errors) for the full list of structured error codes.

***

## Rate limiting

Every unauthenticated request triggers `createInvoice()` on your Lightning provider. Without rate limiting, anyone can flood your endpoint and exhaust your provider's API quota for free — even without paying a single sat.

Add [`express-rate-limit`](https://www.npmjs.com/package/express-rate-limit) before your L402 routes:

```bash theme={null}
npm install express-rate-limit
```

```typescript theme={null}
import rateLimit from "express-rate-limit";
import { l402 } from "l402-kit";

// Limit invoice creation: 30 unauthenticated requests/minute per IP
const invoiceLimit = rateLimit({
  windowMs: 60_000,
  max: 30,
  // Only apply to requests that don't already carry a valid L402 header
  skip: (req) => req.headers.authorization?.startsWith("L402 ") ?? false,
  message: { error: "Too many requests — slow down" },
});

app.get("/api/premium", invoiceLimit, l402({ priceSats: 10, lightning }), handler);
```

<Note>
  Already-paying clients are skipped by the `skip` function — the limit only applies to unauthenticated calls that trigger a new invoice. Legitimate payers are never throttled.
</Note>

For FastAPI:

```python theme={null}
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded

limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)

@app.get("/api/premium")
@limiter.limit("30/minute")
@l402_required(price_sats=10, lightning=lightning)
async def premium(request: Request):
    return {"data": "paid content"}
```

***

## Health check endpoint

Always add a free health check so monitoring tools don't trigger invoice creation:

```typescript theme={null}
app.get('/health', (req, res) => res.json({ ok: true, ts: Date.now() }));

// Paid endpoint
app.get('/api/data', l402({ priceSats: 10, lightning: blink }), handler);
```

***

## Monitoring

Key metrics to track:

* **402 response rate** — healthy baseline is high (most callers need to pay)
* **Payment verification rate** — ratio of paid vs unpaid calls
* **Provider latency** — `blink.createInvoice()` should be \< 500ms
* **Replay attempts** — spike indicates token reuse attacks

```typescript theme={null}
app.use((req, res, next) => {
  const start = Date.now();
  res.on('finish', () => {
    console.log(JSON.stringify({
      method: req.method,
      path: req.path,
      status: res.statusCode,
      ms: Date.now() - start,
      paid: res.statusCode !== 402,
    }));
  });
  next();
});
```

***

## Performance

* Token verification is **O(1)** — pure crypto, no DB, no network
* Invoice creation (402 path) calls your Lightning provider API — add a cache if you expect the same endpoint hit repeatedly before payment
* Replay store is in-memory `Set` — for multi-instance deployments, swap for Redis

```typescript theme={null}
// Redis replay store example
import { Redis } from 'ioredis';
const redis = new Redis(process.env.REDIS_URL!);

// Pass custom replay function to middleware
app.get('/api', l402({
  priceSats: 10,
  lightning: blink,
  checkReplay: async (preimage) => {
    const set = await redis.set(`l402:${preimage}`, '1', 'NX', 'EX', 86400);
    return set === 'OK';
  },
}), handler);
```
