Skip to main content

Environment variables

Never hardcode keys. Always use environment variables:
# .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
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:
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,
        owner_address: process.env.LIGHTNING_ADDRESS,
      }).then(() => {});
    }
    return original(body);
  };
  next();
});

Vercel / Edge deployment

l402-kit works with Vercel Serverless Functions. Note: the in-memory replay store resets on cold starts. For high-traffic APIs, use Redis for replay protection.
// 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

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"]
# 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:
POST /api/delete-data
Content-Type: application/json

{ "lightningAddress": "user@blink.sv" }
// 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.

Health check endpoint

Always add a free health check so monitoring tools don’t trigger invoice creation:
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 latencyblink.createInvoice() should be < 500ms
  • Replay attempts — spike indicates token reuse attacks
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
// 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);