lightning-toll 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +419 -0
- package/package.json +53 -0
- package/src/client/fetch.js +74 -0
- package/src/client/index.js +141 -0
- package/src/index.js +107 -0
- package/src/l402.js +79 -0
- package/src/macaroon.js +164 -0
- package/src/middleware.js +242 -0
- package/src/stats.js +103 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jeletor
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
# ⚡ lightning-toll
|
|
2
|
+
|
|
3
|
+
**Drop-in Express middleware that puts any API endpoint behind a Lightning paywall.** Consumers pay per request with Bitcoin Lightning — no API keys to manage, no billing system, no Stripe integration. Just `npm install lightning-toll`, wrap your routes, and start earning sats. Implements the [L402 protocol](https://docs.lightning.engineering/the-lightning-network/l402) with proper macaroon credentials.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install lightning-toll lightning-agent
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`express` is a peer dependency (use your existing Express app).
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
### Server (5 lines)
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
const express = require('express');
|
|
19
|
+
const { createToll } = require('lightning-toll');
|
|
20
|
+
|
|
21
|
+
const app = express();
|
|
22
|
+
const toll = createToll({ wallet: process.env.NWC_URL, secret: 'your-hmac-secret' });
|
|
23
|
+
|
|
24
|
+
app.get('/api/joke', toll({ sats: 5 }), (req, res) => res.json({ joke: '...' }));
|
|
25
|
+
app.listen(3000);
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Client (3 lines)
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
const { tollFetch } = require('lightning-toll/client');
|
|
32
|
+
const res = await tollFetch('https://api.example.com/joke', { wallet: process.env.NWC_URL });
|
|
33
|
+
const data = await res.json(); // Paid 5 sats automatically
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## How It Works — L402 Protocol
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
Client Server
|
|
40
|
+
| |
|
|
41
|
+
| GET /api/joke |
|
|
42
|
+
| ─────────────────────────────────> |
|
|
43
|
+
| |
|
|
44
|
+
| 402 Payment Required |
|
|
45
|
+
| WWW-Authenticate: L402 invoice="..",|
|
|
46
|
+
| macaroon=".." |
|
|
47
|
+
| <───────────────────────────────── |
|
|
48
|
+
| |
|
|
49
|
+
| [Pays Lightning invoice] |
|
|
50
|
+
| [Gets preimage as receipt] |
|
|
51
|
+
| |
|
|
52
|
+
| GET /api/joke |
|
|
53
|
+
| Authorization: L402 <mac>:<preimage>|
|
|
54
|
+
| ─────────────────────────────────> |
|
|
55
|
+
| |
|
|
56
|
+
| 200 OK { joke: "..." } |
|
|
57
|
+
| <───────────────────────────────── |
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
1. Client requests an endpoint without payment
|
|
61
|
+
2. Server returns **402 Payment Required** with a Lightning invoice and a macaroon
|
|
62
|
+
3. Client pays the invoice with any Lightning wallet
|
|
63
|
+
4. Client retries with `Authorization: L402 <macaroon>:<preimage>`
|
|
64
|
+
5. Server verifies the preimage matches the payment hash, checks the macaroon, and grants access
|
|
65
|
+
|
|
66
|
+
## API Reference
|
|
67
|
+
|
|
68
|
+
### `createToll(options)`
|
|
69
|
+
|
|
70
|
+
Creates a toll booth instance. Returns a `toll()` function for creating per-route middleware.
|
|
71
|
+
|
|
72
|
+
```js
|
|
73
|
+
const { createToll } = require('lightning-toll');
|
|
74
|
+
|
|
75
|
+
const toll = createToll({
|
|
76
|
+
// Required
|
|
77
|
+
wallet: process.env.NWC_URL, // NWC connection string OR lightning-agent wallet instance
|
|
78
|
+
secret: 'hmac-signing-secret', // Secret for macaroon HMAC signatures
|
|
79
|
+
|
|
80
|
+
// Optional
|
|
81
|
+
defaultSats: 10, // Default price if not set per-route (default: 10)
|
|
82
|
+
invoiceExpiry: 300, // Invoice expiry in seconds (default: 300 = 5 min)
|
|
83
|
+
macaroonExpiry: 3600, // How long a paid macaroon stays valid (default: 3600 = 1 hour)
|
|
84
|
+
bindEndpoint: true, // Bind macaroons to the specific endpoint (default: true)
|
|
85
|
+
bindMethod: true, // Bind macaroons to the HTTP method (default: true)
|
|
86
|
+
bindIp: false, // Bind macaroons to client IP (default: false)
|
|
87
|
+
|
|
88
|
+
// Callbacks
|
|
89
|
+
onPayment: (info) => {
|
|
90
|
+
console.log(`Paid: ${info.amountSats} sats for ${info.endpoint}`);
|
|
91
|
+
// info: { paymentHash, amountSats, endpoint, preimage, settledAt, clientId }
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
#### Using a wallet instance
|
|
97
|
+
|
|
98
|
+
You can pass an NWC URL string (and lightning-toll creates the wallet internally), or pass a pre-created `lightning-agent` wallet:
|
|
99
|
+
|
|
100
|
+
```js
|
|
101
|
+
const { createWallet } = require('lightning-agent');
|
|
102
|
+
const wallet = createWallet(process.env.NWC_URL);
|
|
103
|
+
|
|
104
|
+
const toll = createToll({ wallet, secret: 'my-secret' });
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### `toll(routeOptions)` — Route Middleware
|
|
108
|
+
|
|
109
|
+
```js
|
|
110
|
+
// Fixed price
|
|
111
|
+
app.get('/api/data', toll({ sats: 21 }), handler);
|
|
112
|
+
|
|
113
|
+
// Dynamic price based on request
|
|
114
|
+
app.get('/api/search', toll({
|
|
115
|
+
price: (req) => req.query.premium ? 50 : 10,
|
|
116
|
+
description: (req) => `Search: ${req.query.q}`
|
|
117
|
+
}), handler);
|
|
118
|
+
|
|
119
|
+
// Free tier + paid
|
|
120
|
+
app.get('/api/data', toll({
|
|
121
|
+
sats: 21,
|
|
122
|
+
freeRequests: 10, // Free requests per window per client
|
|
123
|
+
freeWindow: '1h' // Window duration: '30m', '1h', '1d', etc.
|
|
124
|
+
}), handler);
|
|
125
|
+
|
|
126
|
+
// Custom description
|
|
127
|
+
app.get('/api/ai', toll({
|
|
128
|
+
sats: 100,
|
|
129
|
+
description: 'AI inference — GPT-4 quality'
|
|
130
|
+
}), handler);
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
#### Route Options
|
|
134
|
+
|
|
135
|
+
| Option | Type | Description |
|
|
136
|
+
|--------|------|-------------|
|
|
137
|
+
| `sats` | `number` | Fixed price in satoshis |
|
|
138
|
+
| `price` | `(req) => number` | Dynamic pricing function |
|
|
139
|
+
| `description` | `string \| (req) => string` | Invoice description |
|
|
140
|
+
| `freeRequests` | `number` | Free requests per window per client |
|
|
141
|
+
| `freeWindow` | `string \| number` | Free tier window (`'1h'`, `'30m'`, `'1d'`, or milliseconds) |
|
|
142
|
+
|
|
143
|
+
### `req.toll` — Payment Info
|
|
144
|
+
|
|
145
|
+
After the middleware runs, `req.toll` is set on the request:
|
|
146
|
+
|
|
147
|
+
```js
|
|
148
|
+
app.get('/api/data', toll({ sats: 5 }), (req, res) => {
|
|
149
|
+
if (req.toll.paid) {
|
|
150
|
+
// Client paid with Lightning
|
|
151
|
+
console.log(req.toll.paymentHash);
|
|
152
|
+
console.log(req.toll.amountSats);
|
|
153
|
+
}
|
|
154
|
+
if (req.toll.free) {
|
|
155
|
+
// Client used a free tier request
|
|
156
|
+
}
|
|
157
|
+
res.json({ data: '...' });
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### `toll.dashboard()` — Stats Endpoint
|
|
162
|
+
|
|
163
|
+
```js
|
|
164
|
+
app.get('/api/stats', toll.dashboard());
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Returns JSON:
|
|
168
|
+
|
|
169
|
+
```json
|
|
170
|
+
{
|
|
171
|
+
"totalRevenue": 1250,
|
|
172
|
+
"totalRequests": 340,
|
|
173
|
+
"totalPaid": 125,
|
|
174
|
+
"uniquePayers": 42,
|
|
175
|
+
"endpoints": {
|
|
176
|
+
"/api/joke": { "revenue": 500, "requests": 100, "paid": 100, "free": 0 },
|
|
177
|
+
"/api/data": { "revenue": 750, "requests": 240, "paid": 25, "free": 215 }
|
|
178
|
+
},
|
|
179
|
+
"recentPayments": [
|
|
180
|
+
{
|
|
181
|
+
"endpoint": "/api/joke",
|
|
182
|
+
"amountSats": 5,
|
|
183
|
+
"payerId": "203.0.113.1",
|
|
184
|
+
"paymentHash": "abc123...",
|
|
185
|
+
"timestamp": 1706817600000
|
|
186
|
+
}
|
|
187
|
+
]
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Stats are in-memory by default. To persist them, read `toll.stats.toJSON()` periodically and restore on startup.
|
|
192
|
+
|
|
193
|
+
### `toll.stats` — Direct Stats Access
|
|
194
|
+
|
|
195
|
+
```js
|
|
196
|
+
const stats = toll.stats.toJSON();
|
|
197
|
+
console.log(`Total revenue: ${stats.totalRevenue} sats`);
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Client SDK
|
|
201
|
+
|
|
202
|
+
### `TollClient`
|
|
203
|
+
|
|
204
|
+
A client that automatically handles L402 payment flows:
|
|
205
|
+
|
|
206
|
+
```js
|
|
207
|
+
const { TollClient } = require('lightning-toll/client');
|
|
208
|
+
|
|
209
|
+
const client = new TollClient({
|
|
210
|
+
wallet: process.env.NWC_URL, // NWC URL or wallet instance
|
|
211
|
+
maxSats: 100, // Budget cap per request (default: 100)
|
|
212
|
+
autoRetry: true, // Auto-pay and retry on 402 (default: true)
|
|
213
|
+
headers: { // Default headers for all requests
|
|
214
|
+
'User-Agent': 'MyApp/1.0'
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// Transparent fetch — handles 402 automatically
|
|
219
|
+
const res = await client.fetch('https://api.example.com/joke');
|
|
220
|
+
const data = await res.json();
|
|
221
|
+
|
|
222
|
+
// Per-request budget override
|
|
223
|
+
const res2 = await client.fetch('https://api.example.com/expensive', {
|
|
224
|
+
maxSats: 500
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// Clean up
|
|
228
|
+
client.close();
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### `tollFetch(url, options)`
|
|
232
|
+
|
|
233
|
+
One-shot fetch with auto-payment — no client setup needed:
|
|
234
|
+
|
|
235
|
+
```js
|
|
236
|
+
const { tollFetch } = require('lightning-toll/client');
|
|
237
|
+
|
|
238
|
+
const res = await tollFetch('https://api.example.com/joke', {
|
|
239
|
+
wallet: process.env.NWC_URL,
|
|
240
|
+
maxSats: 50
|
|
241
|
+
});
|
|
242
|
+
const data = await res.json();
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
#### Options
|
|
246
|
+
|
|
247
|
+
| Option | Type | Default | Description |
|
|
248
|
+
|--------|------|---------|-------------|
|
|
249
|
+
| `wallet` | `string \| object` | required | NWC URL or wallet instance |
|
|
250
|
+
| `maxSats` | `number` | 50 | Max sats to auto-pay |
|
|
251
|
+
| `method` | `string` | `'GET'` | HTTP method |
|
|
252
|
+
| `headers` | `object` | `{}` | Request headers |
|
|
253
|
+
| `body` | `*` | - | Request body |
|
|
254
|
+
|
|
255
|
+
## Macaroon Caveats
|
|
256
|
+
|
|
257
|
+
Macaroons are bearer credentials with embedded restrictions (caveats). Each caveat narrows the scope of what the credential allows.
|
|
258
|
+
|
|
259
|
+
### Supported Caveats
|
|
260
|
+
|
|
261
|
+
| Caveat | Description | Default |
|
|
262
|
+
|--------|-------------|---------|
|
|
263
|
+
| `expires_at` | Unix timestamp — macaroon expires after this | Always set (based on `macaroonExpiry`) |
|
|
264
|
+
| `endpoint` | Path the macaroon is valid for | Set when `bindEndpoint: true` |
|
|
265
|
+
| `method` | HTTP method restriction | Set when `bindMethod: true` |
|
|
266
|
+
| `ip` | Client IP restriction | Set when `bindIp: true` |
|
|
267
|
+
|
|
268
|
+
### How Macaroons Work
|
|
269
|
+
|
|
270
|
+
```
|
|
271
|
+
1. Server creates macaroon:
|
|
272
|
+
HMAC(secret, paymentHash) → sig₁
|
|
273
|
+
HMAC(sig₁, "expires_at = 1706900000") → sig₂
|
|
274
|
+
HMAC(sig₂, "endpoint = /api/joke") → final_signature
|
|
275
|
+
|
|
276
|
+
2. Macaroon = { id: paymentHash, caveats: [...], signature: final_sig }
|
|
277
|
+
|
|
278
|
+
3. Verification: recompute the HMAC chain and compare signatures
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Macaroons use chained HMAC-SHA256. Each caveat is folded into the signature, making it impossible to remove caveats without invalidating the signature.
|
|
282
|
+
|
|
283
|
+
### Security Model
|
|
284
|
+
|
|
285
|
+
- **Payment binding:** The macaroon ID is the Lightning payment hash. The preimage (proof of payment) must match.
|
|
286
|
+
- **Caveat verification:** All caveats are checked against the current request context.
|
|
287
|
+
- **Timing-safe comparison:** Signature verification uses `crypto.timingSafeEqual`.
|
|
288
|
+
- **No replay:** Each preimage+macaroon combination is checked cryptographically. The preimage can only match one payment hash.
|
|
289
|
+
|
|
290
|
+
## Free Tier Configuration
|
|
291
|
+
|
|
292
|
+
Give users a taste before they pay:
|
|
293
|
+
|
|
294
|
+
```js
|
|
295
|
+
app.get('/api/data', toll({
|
|
296
|
+
sats: 21,
|
|
297
|
+
freeRequests: 10, // 10 free requests...
|
|
298
|
+
freeWindow: '1h' // ...per hour, per client IP
|
|
299
|
+
}), handler);
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Free tier tracking is per client IP by default. The window resets after the specified duration. Supported window formats:
|
|
303
|
+
|
|
304
|
+
- `'30s'` — 30 seconds
|
|
305
|
+
- `'5m'` — 5 minutes
|
|
306
|
+
- `'1h'` — 1 hour
|
|
307
|
+
- `'1d'` — 1 day
|
|
308
|
+
- `3600000` — milliseconds directly
|
|
309
|
+
|
|
310
|
+
## Dynamic Pricing
|
|
311
|
+
|
|
312
|
+
Price APIs based on request content:
|
|
313
|
+
|
|
314
|
+
```js
|
|
315
|
+
// Price by query complexity
|
|
316
|
+
app.get('/api/search', toll({
|
|
317
|
+
price: (req) => {
|
|
318
|
+
if (req.query.deep === 'true') return 50;
|
|
319
|
+
if (req.query.premium === 'true') return 20;
|
|
320
|
+
return 5;
|
|
321
|
+
}
|
|
322
|
+
}), handler);
|
|
323
|
+
|
|
324
|
+
// Price by content length
|
|
325
|
+
app.post('/api/translate', toll({
|
|
326
|
+
price: (req) => {
|
|
327
|
+
const chars = (req.body?.text || '').length;
|
|
328
|
+
return Math.max(1, Math.ceil(chars / 100)); // 1 sat per 100 chars
|
|
329
|
+
}
|
|
330
|
+
}), handler);
|
|
331
|
+
|
|
332
|
+
// Price by time of day (surge pricing)
|
|
333
|
+
app.get('/api/premium', toll({
|
|
334
|
+
price: (req) => {
|
|
335
|
+
const hour = new Date().getHours();
|
|
336
|
+
return hour >= 9 && hour <= 17 ? 50 : 10; // Peak vs off-peak
|
|
337
|
+
}
|
|
338
|
+
}), handler);
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
## 402 Response Format
|
|
342
|
+
|
|
343
|
+
When a client hits a toll-gated endpoint without payment:
|
|
344
|
+
|
|
345
|
+
```
|
|
346
|
+
HTTP/1.1 402 Payment Required
|
|
347
|
+
WWW-Authenticate: L402 invoice="lnbc50n1pj...", macaroon="eyJpZCI..."
|
|
348
|
+
Content-Type: application/json
|
|
349
|
+
|
|
350
|
+
{
|
|
351
|
+
"status": 402,
|
|
352
|
+
"message": "Payment Required",
|
|
353
|
+
"paymentHash": "a1b2c3d4...",
|
|
354
|
+
"invoice": "lnbc50n1pj...",
|
|
355
|
+
"macaroon": "eyJpZCI...",
|
|
356
|
+
"amountSats": 5,
|
|
357
|
+
"description": "Random joke",
|
|
358
|
+
"protocol": "L402",
|
|
359
|
+
"instructions": {
|
|
360
|
+
"step1": "Pay the Lightning invoice above",
|
|
361
|
+
"step2": "Get the preimage from the payment receipt",
|
|
362
|
+
"step3": "Retry the request with header: Authorization: L402 <macaroon>:<preimage>"
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
## Security Considerations
|
|
368
|
+
|
|
369
|
+
- **Use a strong secret.** The HMAC secret should be a random string of at least 32 characters. Use `crypto.randomBytes(32).toString('hex')`.
|
|
370
|
+
- **HTTPS in production.** Macaroons and preimages are bearer credentials — always use HTTPS.
|
|
371
|
+
- **Invoice expiry.** Default is 5 minutes. Shorter = safer, but gives users less time to pay.
|
|
372
|
+
- **Macaroon expiry.** Default is 1 hour. A paid macaroon can be reused until it expires.
|
|
373
|
+
- **IP binding.** Enable `bindIp: true` if you want macaroons tied to a specific client IP. Beware of NAT and proxies.
|
|
374
|
+
- **Rate limiting.** lightning-toll doesn't include rate limiting beyond the free tier. Use a proper rate limiter (like `express-rate-limit`) for DDoS protection.
|
|
375
|
+
- **Stats persistence.** Stats are in-memory by default and reset on restart. For production, periodically snapshot `toll.stats.toJSON()` to a database.
|
|
376
|
+
|
|
377
|
+
## Why Lightning Instead of API Keys?
|
|
378
|
+
|
|
379
|
+
| | API Keys / Stripe | lightning-toll |
|
|
380
|
+
|---|---|---|
|
|
381
|
+
| **Setup time** | Hours–days (Stripe onboarding, billing pages) | Minutes (`npm install` + 5 lines of code) |
|
|
382
|
+
| **User friction** | Sign up, enter credit card, wait for approval | Scan QR code, pay instantly |
|
|
383
|
+
| **Minimum viable payment** | $0.50+ (credit card minimums) | 1 sat (~$0.0005) — true micropayments |
|
|
384
|
+
| **Chargebacks** | Yes (costly) | No — Lightning payments are final |
|
|
385
|
+
| **KYC required** | Yes (for Stripe/PayPal) | No |
|
|
386
|
+
| **Geographic restrictions** | Yes | No — works globally, instantly |
|
|
387
|
+
| **Privacy** | Full identity required | Pseudonymous by default |
|
|
388
|
+
| **Settlement** | Days to weeks | Instant |
|
|
389
|
+
|
|
390
|
+
## Demo
|
|
391
|
+
|
|
392
|
+
Run the included demo server:
|
|
393
|
+
|
|
394
|
+
```bash
|
|
395
|
+
cd demo
|
|
396
|
+
npm install
|
|
397
|
+
NWC_URL="nostr+walletconnect://..." node server.js
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
Open `http://localhost:3402` for an interactive UI with:
|
|
401
|
+
- Multiple toll-gated endpoints at different price points
|
|
402
|
+
- "Try it" buttons showing the 402 response flow
|
|
403
|
+
- Live revenue dashboard
|
|
404
|
+
- Code examples
|
|
405
|
+
|
|
406
|
+
### Demo Endpoints
|
|
407
|
+
|
|
408
|
+
| Endpoint | Price | Description |
|
|
409
|
+
|----------|-------|-------------|
|
|
410
|
+
| `GET /api/joke` | 5 sats | Random programming joke |
|
|
411
|
+
| `GET /api/time` | 1 sat | Current server time |
|
|
412
|
+
| `POST /api/echo` | 1 sat/word | Echo text with dynamic pricing |
|
|
413
|
+
| `GET /api/fortune` | 10 sats | Bitcoin-themed fortune cookie |
|
|
414
|
+
| `GET /api/free-tier` | 21 sats (3 free/hr) | Free tier demo |
|
|
415
|
+
| `GET /api/stats` | Free | Revenue dashboard |
|
|
416
|
+
|
|
417
|
+
## License
|
|
418
|
+
|
|
419
|
+
MIT — [Jeletor](https://github.com/jeletor)
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lightning-toll",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Drop-in Express middleware for Lightning-gated API endpoints. L402 protocol, macaroons, auto-pay client — monetize any API with Bitcoin Lightning.",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.js",
|
|
8
|
+
"./client": "./src/client/index.js"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"lightning",
|
|
12
|
+
"bitcoin",
|
|
13
|
+
"l402",
|
|
14
|
+
"api",
|
|
15
|
+
"paywall",
|
|
16
|
+
"middleware",
|
|
17
|
+
"express",
|
|
18
|
+
"macaroon",
|
|
19
|
+
"micropayments",
|
|
20
|
+
"monetization",
|
|
21
|
+
"toll",
|
|
22
|
+
"402"
|
|
23
|
+
],
|
|
24
|
+
"author": "Jeletor",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "https://github.com/jeletor/lightning-toll.git"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/jeletor/lightning-toll#readme",
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/jeletor/lightning-toll/issues"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"lightning-agent": "^0.3.0"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"express": "^4.0.0 || ^5.0.0"
|
|
39
|
+
},
|
|
40
|
+
"peerDependenciesMeta": {
|
|
41
|
+
"express": {
|
|
42
|
+
"optional": true
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=16.0.0"
|
|
47
|
+
},
|
|
48
|
+
"files": [
|
|
49
|
+
"src/",
|
|
50
|
+
"README.md",
|
|
51
|
+
"LICENSE"
|
|
52
|
+
]
|
|
53
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { parseAuthorization } = require('../l402');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Auto-pay fetch wrapper.
|
|
7
|
+
* When a 402 response is received, automatically pays the Lightning invoice
|
|
8
|
+
* and retries the request with the L402 authorization header.
|
|
9
|
+
*
|
|
10
|
+
* @param {string} url - URL to fetch
|
|
11
|
+
* @param {object} [fetchOpts] - Standard fetch options
|
|
12
|
+
* @param {object} payOpts
|
|
13
|
+
* @param {object} payOpts.wallet - lightning-agent wallet instance
|
|
14
|
+
* @param {number} [payOpts.maxSats=100] - Maximum sats to pay per request
|
|
15
|
+
* @param {boolean} [payOpts.autoRetry=true] - Automatically pay and retry on 402
|
|
16
|
+
* @param {object} [payOpts.headers] - Additional headers
|
|
17
|
+
* @returns {Promise<Response>}
|
|
18
|
+
*/
|
|
19
|
+
async function autoPay(url, fetchOpts = {}, payOpts = {}) {
|
|
20
|
+
const wallet = payOpts.wallet;
|
|
21
|
+
if (!wallet) throw new Error('lightning-toll/client: wallet is required');
|
|
22
|
+
|
|
23
|
+
const maxSats = payOpts.maxSats || 100;
|
|
24
|
+
const autoRetry = payOpts.autoRetry !== false;
|
|
25
|
+
|
|
26
|
+
// Make the initial request
|
|
27
|
+
const mergedHeaders = { ...payOpts.headers, ...fetchOpts.headers };
|
|
28
|
+
const res = await fetch(url, { ...fetchOpts, headers: mergedHeaders });
|
|
29
|
+
|
|
30
|
+
// If not 402, return as-is
|
|
31
|
+
if (res.status !== 402) return res;
|
|
32
|
+
|
|
33
|
+
// If auto-retry is disabled, return the 402
|
|
34
|
+
if (!autoRetry) return res;
|
|
35
|
+
|
|
36
|
+
// Parse the 402 response
|
|
37
|
+
let body;
|
|
38
|
+
try {
|
|
39
|
+
body = await res.json();
|
|
40
|
+
} catch {
|
|
41
|
+
throw new Error('lightning-toll/client: Could not parse 402 response body');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!body.invoice) {
|
|
45
|
+
throw new Error('lightning-toll/client: 402 response missing invoice');
|
|
46
|
+
}
|
|
47
|
+
if (!body.macaroon) {
|
|
48
|
+
throw new Error('lightning-toll/client: 402 response missing macaroon');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Check budget
|
|
52
|
+
const amountSats = body.amountSats || 0;
|
|
53
|
+
if (amountSats > maxSats) {
|
|
54
|
+
throw new Error(`lightning-toll/client: Price ${amountSats} sats exceeds budget of ${maxSats} sats`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Pay the invoice
|
|
58
|
+
const payResult = await wallet.payInvoice(body.invoice);
|
|
59
|
+
if (!payResult || !payResult.preimage) {
|
|
60
|
+
throw new Error('lightning-toll/client: Payment failed — no preimage returned');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Retry with L402 authorization
|
|
64
|
+
const authHeader = `L402 ${body.macaroon}:${payResult.preimage}`;
|
|
65
|
+
const retryHeaders = {
|
|
66
|
+
...mergedHeaders,
|
|
67
|
+
Authorization: authHeader
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const retryRes = await fetch(url, { ...fetchOpts, headers: retryHeaders });
|
|
71
|
+
return retryRes;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = { autoPay };
|