Skip to main content

Installation

npm install l402-kit
Prérequis : Node.js 18+, Express 4+

Démarrage rapide

import express from 'express';
import { l402, BlinkProvider } from 'l402-kit';

const app = express();

// Les paiements vont directement à votre portefeuille Blink — 0% de frais
const lightning = new BlinkProvider(
  process.env.BLINK_API_KEY!,
  process.env.BLINK_WALLET_ID!,
);

app.get('/api/data', l402({ priceSats: 10, lightning }), (req, res) => {
  res.json({ data: 'premium content' });
});

app.listen(3000);

l402(options) — middleware

Retourne un RequestHandler Express qui impose le paiement L402 sur la route.

Options

OptionTypeDéfautDescription
priceSatsnumberrequisPrix par appel en satoshis
lightningLightningProviderrequisVotre backend Lightning
supabaseUrlstringenv SUPABASE_URLURL Supabase pour la journalisation des paiements
supabaseKeystringenv SUPABASE_ANON_KEYClé Supabase pour la journalisation des paiements
onPayment(token, amountSats) => voidCallback déclenché après chaque paiement vérifié
webhookUrlstringVotre endpoint pour recevoir les événements de paiement signés
webhookSecretstringSecret HMAC-SHA256 pour la signature des webhooks
replayAdapterReplayAdapteren mémoireBackend de protection contre la répétition (pluggable)

Comportement

RequêteRéponse
Pas d’en-tête Authorization402 Payment Required avec facture BOLT11 + macaroon
L402 <macaroon>:<preimage> validenext() — le gestionnaire s’exécute
Jeton invalide ou expiré401 Unauthorized
preimage rejoué401 Token already used

Corps de la réponse 402

{
  "error": "Payment Required",
  "invoice": "lnbc100n1p...",
  "macaroon": "eyJoYXNoIjoiYWJjMTIzIiwiZXhwIjoxNzAwMDAwMDAwfQ==",
  "priceSats": 10
}

En-tête WWW-Authenticate

WWW-Authenticate: L402 macaroon="eyJ...", invoice="lnbc..."

Fournisseurs

AlbyProvider — recommandé pour le mode souverain

Alby — portefeuille non-custodial. Vous contrôlez les clés.
import { AlbyProvider } from 'l402-kit';

// 1. Créez un Alby Hub sur hub.getalby.com (ou hébergez-le vous-même)
// 2. Paramètres → Jetons d'accès → créer un jeton (portées : invoices:create, invoices:read)
const lightning = new AlbyProvider(
  process.env.ALBY_ACCESS_TOKEN!, // Jeton d'accès Hub
  process.env.ALBY_HUB_URL!,      // ex. "https://your-name.getalby.com"
);

BTCPayProvider — auto-hébergé, zéro confiance

Gérez votre propre BTCPay Server. Souveraineté totale.
import { BTCPayProvider } from 'l402-kit';

const lightning = new BTCPayProvider(
  process.env.BTCPAY_URL!,         // https://your-btcpay.com
  process.env.BTCPAY_API_KEY!,     // clé API du magasin
  process.env.BTCPAY_STORE_ID!,    // ID du magasin
);

BlinkProvider — custodial, démarrage le plus simple

Blink — gratuit, sans KYC pour les petits montants.
import { BlinkProvider } from 'l402-kit';

const lightning = new BlinkProvider(
  process.env.BLINK_API_KEY!,    // dashboard.blink.sv → Clés API
  process.env.BLINK_WALLET_ID!,  // votre ID de portefeuille BTC
);

LNbitsProvider

Auto-hébergé ou legend.lnbits.com.
import { LNbitsProvider } from 'l402-kit';

const lightning = new LNbitsProvider(
  process.env.LNBITS_API_KEY!,
  'https://your-lnbits-instance.com', // optionnel, par défaut legend.lnbits.com
);

OpenNodeProvider

import { OpenNodeProvider } from 'l402-kit';

const lightning = new OpenNodeProvider(
  process.env.OPENNODE_API_KEY!,
  false, // testMode — définir à true pour le bac à sable
);

ManagedProvider — mode cloud (0,3% de frais)

l402kit.com héberge le nœud Lightning. Vous recevez 99,7% de chaque paiement. Activation explicite requise.
import { ManagedProvider } from 'l402-kit';

const lightning = ManagedProvider.fromAddress('you@yourdomain.com');

// Optionnel : inscription automatique dans le répertoire public des API
const lightning = ManagedProvider.fromAddress('you@yourdomain.com', {
  registerDirectory: {
    url: 'https://api.you.com/v1/data',
    name: 'My Data API',
    priceSats: 10,
    category: 'data',          // data | ai | finance | weather | compute | storage | other
    description: 'Optionnel',
  },
});
L’inscription s’effectue une seule fois au démarrage (fire-and-forget, les erreurs sont silencieuses). L’API apparaît sur l402kit.com/apis.json afin que les agents puissent la découvrir automatiquement.

Protection contre la répétition

Par défaut — en mémoire (développement)

Intégré. Se réinitialise au redémarrage. Convient aux déploiements mono-processus.
app.get('/api', l402({ priceSats: 10, lightning }), handler);

Redis (production — multi-instance)

import Redis from 'ioredis';
import { l402, RedisReplayAdapter } from 'l402-kit';

const replay = new RedisReplayAdapter(
  new Redis(process.env.REDIS_URL!),
  86400, // TTL en secondes (24h)
);

app.get('/api', l402({ priceSats: 10, lightning, replayAdapter: replay }), handler);
RedisReplayAdapter utilise SET key 1 NX EX ttl — atomique, sans condition de course.

Webhooks de paiement

Recevez un événement signé après chaque paiement.
import { l402, verifyWebhook } from 'l402-kit';

app.get('/api/data', l402({
  priceSats: 10,
  lightning,
  webhookUrl: 'https://yourapi.com/webhooks/l402',
  webhookSecret: process.env.L402_WEBHOOK_SECRET!,
}), handler);

// Récepteur de webhook
app.post('/webhooks/l402', express.raw({ type: '*/*' }), (req, res) => {
  const valid = verifyWebhook(
    process.env.L402_WEBHOOK_SECRET!,
    req.body.toString(),
    req.headers['l402-signature'] as string,
  );
  if (!valid) return res.status(401).end();

  const event = JSON.parse(req.body.toString());
  console.log('Payment:', event.data.paymentHash, event.data.amountSats);
  res.json({ ok: true });
});
Charge utile du webhook :
{
  "id": "evt_abc123",
  "type": "payment.received",
  "created": 1700000000,
  "data": {
    "endpoint": "/api/data",
    "amountSats": 10,
    "paymentHash": "sha256-of-preimage"
  }
}

Callback onPayment

Hook synchrone appelé après chaque paiement vérifié, avant next() :
app.get('/api/data', l402({
  priceSats: 10,
  lightning,
  onPayment: async ({ macaroon, preimage }, amountSats) => {
    await myAnalytics.track('payment', { amountSats });
  },
}), handler);

Journalisation des paiements avec Supabase

Définissez SUPABASE_URL + SUPABASE_ANON_KEY dans votre environnement pour journaliser les paiements automatiquement.
app.get('/api', l402({ priceSats: 10, lightning }), handler);
// SUPABASE_URL et SUPABASE_ANON_KEY sont lus depuis process.env automatiquement
Schéma de la table des paiements (payments) :
create table payments (
  id            uuid primary key default gen_random_uuid(),
  payment_hash  text unique not null,  -- SHA256(preimage) — sûr à stocker
  endpoint      text,
  amount_sats   integer,
  paid_at       timestamptz default now()
);
payment_hash stocke SHA256(preimage), et non le preimage brut. Le preimage est le secret de paiement Lightning de 32 octets — son hash est déjà public dans la facture BOLT11.

Utilitaires autonomes

import { verifyToken, parseToken, checkAndMarkPreimage } from 'l402-kit';

// Vérifier un jeton (retourne true/false)
const isValid = await verifyToken('eyJoYXNoIjoi...:deadbeef...');

// Analyser les parties du jeton
const { macaroon, preimage } = parseToken(token);

// Vérification personnalisée de répétition (retourne true = première utilisation, false = répétition)
const isFirstUse = await checkAndMarkPreimage(preimage);

Types

import type { L402Options, LightningProvider, Invoice, L402Token } from 'l402-kit';

interface L402Options {
  priceSats: number;
  lightning: LightningProvider;
  supabaseUrl?: string;
  supabaseKey?: string;
  onPayment?: (token: L402Token, amountSats: number) => void | Promise<void>;
  webhookUrl?: string;
  webhookSecret?: string;
  replayAdapter?: ReplayAdapter;
}

interface Invoice {
  paymentRequest: string;
  paymentHash: string;
  macaroon: string;
  amountSats: number;
  expiresAt: number;        // Unix ms
}

interface LightningProvider {
  createInvoice(amountSats: number): Promise<Invoice>;
  checkPayment(paymentHash: string): Promise<boolean>;
}

Timing de vérification

La vérification du jeton exécute SHA256(preimage) == paymentHash en mémoire — sous la milliseconde, sans appel réseau sur le chemin critique. Le ReplayAdapter en mémoire (par défaut) s’exécute également de manière synchrone. Si vous utilisez RedisReplayAdapter, comptez 5 à 50 ms d’aller-retour Redis par requête. Planifiez la capacité en conséquence pour les endpoints à haute fréquence.

Compatibilité x402 (en-tête X-Payment)

Le middleware accepte silencieusement l’en-tête X-Payment (utilisé par le protocole x402 de Coinbase) en plus de l’en-tête standard Authorization: L402 …. Les deux sont traités de manière identique — utile si vous souhaitez servir des clients parlant l’un ou l’autre protocole.
// Client utilisant l'en-tête x402 — géré de manière transparente
// X-Payment: <macaroon>:<preimage>
Aucune configuration requise ; cette fonctionnalité est toujours activée.

Guide de migration

v1.1 → v1.2

Renommez la colonne dans votre table payments :
ALTER TABLE payments RENAME COLUMN preimage TO payment_hash;