x402-lite 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/README.md +167 -0
- package/index.js +270 -0
- package/package.json +20 -0
- package/test.js +105 -0
package/README.md
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# x402-lite
|
|
2
|
+
|
|
3
|
+
Lightweight x402 payment middleware for Express. **Zero crypto dependencies.**
|
|
4
|
+
|
|
5
|
+
The official `x402-express` package pulls in viem, Solana SDK, and CDP SDK (~100MB+ of node_modules). This package does the same job with **zero dependencies** by delegating verification and settlement to the [x402 facilitator](https://x402.org).
|
|
6
|
+
|
|
7
|
+
Perfect for Raspberry Pi, edge devices, serverless functions, and anywhere you want x402 payments without the dependency bloat.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install x402-lite express
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
const express = require("express");
|
|
19
|
+
const { paymentMiddleware } = require("x402-lite");
|
|
20
|
+
|
|
21
|
+
const app = express();
|
|
22
|
+
app.use(express.json());
|
|
23
|
+
|
|
24
|
+
// Health check (free)
|
|
25
|
+
app.get("/health", (req, res) => res.json({ status: "ok" }));
|
|
26
|
+
|
|
27
|
+
// Protect routes with x402 payments
|
|
28
|
+
app.use(paymentMiddleware("0xYourWalletAddress", {
|
|
29
|
+
"GET /api/data": {
|
|
30
|
+
price: "$0.01",
|
|
31
|
+
network: "base",
|
|
32
|
+
config: {
|
|
33
|
+
description: "Get some data",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
app.get("/api/data", (req, res) => {
|
|
39
|
+
res.json({ data: "You paid $0.01 for this!" });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
app.listen(3000);
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Test it:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# Should return 402 with payment requirements
|
|
49
|
+
curl -i http://localhost:3000/api/data
|
|
50
|
+
|
|
51
|
+
# Health check (free)
|
|
52
|
+
curl http://localhost:3000/health
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Why x402-lite?
|
|
56
|
+
|
|
57
|
+
| | x402-express | x402-lite |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| Dependencies | viem, @solana/kit, @coinbase/cdp-sdk, ... | **none** |
|
|
60
|
+
| node_modules size | ~100MB+ | **0 bytes** |
|
|
61
|
+
| Install time | minutes | **instant** |
|
|
62
|
+
| Works on Pi/edge | OOM kills likely | **yes** |
|
|
63
|
+
| EVM support | ✓ | ✓ |
|
|
64
|
+
| Solana support | ✓ | ✗ (EVM only) |
|
|
65
|
+
| Local verification | ✓ | ✗ (facilitator only) |
|
|
66
|
+
|
|
67
|
+
**Trade-off:** x402-lite requires network access to the facilitator for verification/settlement. If you need offline verification or Solana support, use the official package.
|
|
68
|
+
|
|
69
|
+
## API
|
|
70
|
+
|
|
71
|
+
### `paymentMiddleware(payTo, routes, options?)`
|
|
72
|
+
|
|
73
|
+
Creates Express middleware that enforces x402 payments.
|
|
74
|
+
|
|
75
|
+
**Parameters:**
|
|
76
|
+
|
|
77
|
+
| Param | Type | Description |
|
|
78
|
+
|---|---|---|
|
|
79
|
+
| `payTo` | `string` | Ethereum address to receive USDC payments |
|
|
80
|
+
| `routes` | `object` | Route config (see below) |
|
|
81
|
+
| `options.facilitatorUrl` | `string?` | Custom facilitator (default: `https://x402.org/facilitator`) |
|
|
82
|
+
|
|
83
|
+
**Route config:**
|
|
84
|
+
|
|
85
|
+
```js
|
|
86
|
+
{
|
|
87
|
+
// Simple — just a price (defaults to base network)
|
|
88
|
+
"GET /api/cheap": "$0.001",
|
|
89
|
+
|
|
90
|
+
// Full config
|
|
91
|
+
"POST /api/query": {
|
|
92
|
+
price: "$0.05",
|
|
93
|
+
network: "base", // "base" or "base-sepolia"
|
|
94
|
+
config: {
|
|
95
|
+
description: "What this endpoint does",
|
|
96
|
+
inputSchema: {
|
|
97
|
+
bodyType: "json",
|
|
98
|
+
bodyFields: {
|
|
99
|
+
query: { type: "string", description: "Search query" },
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
outputSchema: {
|
|
103
|
+
type: "object",
|
|
104
|
+
properties: {
|
|
105
|
+
result: { type: "string" },
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
// Wildcard — protects all routes under /api/
|
|
112
|
+
"GET /api/*": "$0.01",
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Route Matching
|
|
117
|
+
|
|
118
|
+
- `"GET /path"` — matches specific method + path
|
|
119
|
+
- `"/path"` — matches any method
|
|
120
|
+
- `"/path/*"` — wildcard, matches anything under `/path/`
|
|
121
|
+
- `"GET /users/:id"` — parameter matching
|
|
122
|
+
|
|
123
|
+
### Exported Utilities
|
|
124
|
+
|
|
125
|
+
```js
|
|
126
|
+
const { priceToAtomic, matchRoute, USDC_CONFIG } = require("x402-lite");
|
|
127
|
+
|
|
128
|
+
priceToAtomic("$0.01"); // "10000" (USDC has 6 decimals)
|
|
129
|
+
USDC_CONFIG["base"]; // { chainId: 8453, address: "0x833...", decimals: 6 }
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## How It Works
|
|
133
|
+
|
|
134
|
+
1. Client hits a protected endpoint without `X-PAYMENT` header
|
|
135
|
+
2. Server returns **HTTP 402** with payment requirements in `X-PAYMENT-REQUIRED` header and JSON body
|
|
136
|
+
3. Client signs a USDC payment and retries with `X-PAYMENT` header (base64 JSON)
|
|
137
|
+
4. Server forwards payment to the **facilitator** (`x402.org/facilitator`) for verification
|
|
138
|
+
5. If valid, server processes the request and calls facilitator to settle
|
|
139
|
+
6. Settlement response returned in `X-PAYMENT-RESPONSE` header
|
|
140
|
+
|
|
141
|
+
All crypto operations happen at the facilitator — your server never touches private keys or blockchain RPCs.
|
|
142
|
+
|
|
143
|
+
## Networks
|
|
144
|
+
|
|
145
|
+
| Network | Chain ID | USDC Address |
|
|
146
|
+
|---|---|---|
|
|
147
|
+
| `base` | 8453 | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` |
|
|
148
|
+
| `base-sepolia` | 84532 | `0x036CbD53842c5426634e7929541eC2318f3dCF7e` |
|
|
149
|
+
|
|
150
|
+
## Testing
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
node test.js
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Pricing Guidelines
|
|
157
|
+
|
|
158
|
+
| Use Case | Suggested Price |
|
|
159
|
+
|---|---|
|
|
160
|
+
| Simple data lookup | $0.001 – $0.01 |
|
|
161
|
+
| API proxy / enrichment | $0.01 – $0.10 |
|
|
162
|
+
| Compute-heavy query | $0.10 – $0.50 |
|
|
163
|
+
| AI inference | $0.05 – $1.00 |
|
|
164
|
+
|
|
165
|
+
## License
|
|
166
|
+
|
|
167
|
+
MIT
|
package/index.js
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* x402-lite — Lightweight x402 payment middleware for Express
|
|
3
|
+
*
|
|
4
|
+
* Zero crypto dependencies. Delegates all verification and settlement
|
|
5
|
+
* to the x402 facilitator service. Works on Raspberry Pi, edge devices,
|
|
6
|
+
* and resource-constrained environments.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* const { paymentMiddleware } = require("x402-lite");
|
|
10
|
+
* app.use(paymentMiddleware("0xYourAddress", {
|
|
11
|
+
* "POST /api/data": { price: "$0.01", network: "base" }
|
|
12
|
+
* }));
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const DEFAULT_FACILITATOR_URL = "https://x402.org/facilitator";
|
|
16
|
+
|
|
17
|
+
// USDC contract addresses per chain
|
|
18
|
+
const USDC_CONFIG = {
|
|
19
|
+
"base": {
|
|
20
|
+
chainId: 8453,
|
|
21
|
+
address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
22
|
+
name: "USD Coin",
|
|
23
|
+
decimals: 6,
|
|
24
|
+
},
|
|
25
|
+
"base-sepolia": {
|
|
26
|
+
chainId: 84532,
|
|
27
|
+
address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
|
|
28
|
+
name: "USDC",
|
|
29
|
+
decimals: 6,
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Parse a price string like "$0.01" or "0.01" into atomic USDC units (6 decimals)
|
|
35
|
+
*/
|
|
36
|
+
function priceToAtomic(price) {
|
|
37
|
+
if (typeof price === "number") return String(Math.round(price * 1e6));
|
|
38
|
+
const str = String(price).replace(/^\$/, "").trim();
|
|
39
|
+
const num = parseFloat(str);
|
|
40
|
+
if (isNaN(num)) throw new Error(`Invalid price: ${price}`);
|
|
41
|
+
return String(Math.round(num * 1e6));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Match a request to a route pattern
|
|
46
|
+
* Supports: "GET /path", "POST /path", "/path" (any method), "/path/*" (wildcard)
|
|
47
|
+
*/
|
|
48
|
+
function matchRoute(routes, method, path) {
|
|
49
|
+
for (const [pattern, config] of Object.entries(routes)) {
|
|
50
|
+
const parts = pattern.trim().split(/\s+/);
|
|
51
|
+
let routeMethod = null;
|
|
52
|
+
let routePath;
|
|
53
|
+
|
|
54
|
+
if (parts.length === 2) {
|
|
55
|
+
routeMethod = parts[0].toUpperCase();
|
|
56
|
+
routePath = parts[1];
|
|
57
|
+
} else {
|
|
58
|
+
routePath = parts[0];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Check method
|
|
62
|
+
if (routeMethod && routeMethod !== method.toUpperCase()) continue;
|
|
63
|
+
|
|
64
|
+
// Check path (simple glob: * matches anything)
|
|
65
|
+
if (routePath === path) return config;
|
|
66
|
+
if (routePath.endsWith("/*")) {
|
|
67
|
+
const prefix = routePath.slice(0, -2);
|
|
68
|
+
if (path === prefix || path.startsWith(prefix + "/")) return config;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Express-style :param matching
|
|
72
|
+
const routeSegments = routePath.split("/");
|
|
73
|
+
const pathSegments = path.split("/");
|
|
74
|
+
if (routeSegments.length === pathSegments.length) {
|
|
75
|
+
const match = routeSegments.every((seg, i) =>
|
|
76
|
+
seg.startsWith(":") || seg === pathSegments[i]
|
|
77
|
+
);
|
|
78
|
+
if (match) return config;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Normalize route config — accepts string price or full config object
|
|
86
|
+
*/
|
|
87
|
+
function normalizeConfig(config) {
|
|
88
|
+
if (typeof config === "string") {
|
|
89
|
+
return { price: config, network: "base" };
|
|
90
|
+
}
|
|
91
|
+
return config;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Build PaymentRequirements for a route
|
|
96
|
+
*/
|
|
97
|
+
function buildPaymentRequirements(payTo, routeConfig, req) {
|
|
98
|
+
const { price, network = "base", config: meta = {} } = normalizeConfig(routeConfig);
|
|
99
|
+
const usdc = USDC_CONFIG[network];
|
|
100
|
+
if (!usdc) throw new Error(`Unsupported network: ${network}`);
|
|
101
|
+
|
|
102
|
+
const maxAmountRequired = priceToAtomic(price);
|
|
103
|
+
const resource = `${req.protocol}://${req.headers.host}${req.path}`;
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
scheme: "exact",
|
|
107
|
+
network,
|
|
108
|
+
maxAmountRequired,
|
|
109
|
+
resource,
|
|
110
|
+
description: meta.description || "",
|
|
111
|
+
mimeType: meta.mimeType || "",
|
|
112
|
+
payTo,
|
|
113
|
+
maxTimeoutSeconds: meta.maxTimeoutSeconds || 60,
|
|
114
|
+
asset: usdc.address,
|
|
115
|
+
outputSchema: {
|
|
116
|
+
input: {
|
|
117
|
+
type: "http",
|
|
118
|
+
method: req.method.toUpperCase(),
|
|
119
|
+
discoverable: meta.discoverable !== false,
|
|
120
|
+
...(meta.inputSchema || {}),
|
|
121
|
+
},
|
|
122
|
+
output: meta.outputSchema || undefined,
|
|
123
|
+
},
|
|
124
|
+
extra: undefined,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Call the facilitator's verify endpoint
|
|
130
|
+
*/
|
|
131
|
+
async function verifyPayment(facilitatorUrl, paymentPayload, paymentRequirements) {
|
|
132
|
+
const res = await fetch(`${facilitatorUrl}/verify`, {
|
|
133
|
+
method: "POST",
|
|
134
|
+
headers: { "Content-Type": "application/json" },
|
|
135
|
+
body: JSON.stringify({
|
|
136
|
+
x402Version: paymentPayload.x402Version || 1,
|
|
137
|
+
paymentPayload,
|
|
138
|
+
paymentRequirements,
|
|
139
|
+
}),
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const data = await res.json();
|
|
143
|
+
if (res.status !== 200 || !data.isValid) {
|
|
144
|
+
return { valid: false, error: data };
|
|
145
|
+
}
|
|
146
|
+
return { valid: true, data };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Call the facilitator's settle endpoint
|
|
151
|
+
*/
|
|
152
|
+
async function settlePayment(facilitatorUrl, paymentPayload, paymentRequirements) {
|
|
153
|
+
try {
|
|
154
|
+
const res = await fetch(`${facilitatorUrl}/settle`, {
|
|
155
|
+
method: "POST",
|
|
156
|
+
headers: { "Content-Type": "application/json" },
|
|
157
|
+
body: JSON.stringify({
|
|
158
|
+
x402Version: paymentPayload.x402Version || 1,
|
|
159
|
+
paymentPayload,
|
|
160
|
+
paymentRequirements,
|
|
161
|
+
}),
|
|
162
|
+
});
|
|
163
|
+
const data = await res.json();
|
|
164
|
+
return data;
|
|
165
|
+
} catch (err) {
|
|
166
|
+
// Settlement failure shouldn't block the response — log it
|
|
167
|
+
console.error("[x402-lite] Settlement error:", err.message);
|
|
168
|
+
return { success: false, error: err.message };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Creates Express middleware that enforces x402 payments.
|
|
174
|
+
*
|
|
175
|
+
* @param {string} payTo - Ethereum address to receive payments
|
|
176
|
+
* @param {object} routes - Route config: { "METHOD /path": { price, network, config } }
|
|
177
|
+
* @param {object} [options] - Optional settings
|
|
178
|
+
* @param {string} [options.facilitatorUrl] - Custom facilitator URL (default: x402.org)
|
|
179
|
+
* @returns {function} Express middleware
|
|
180
|
+
*
|
|
181
|
+
* @example
|
|
182
|
+
* const { paymentMiddleware } = require("x402-lite");
|
|
183
|
+
*
|
|
184
|
+
* app.use(paymentMiddleware("0x1234...", {
|
|
185
|
+
* "GET /api/data": { price: "$0.01", network: "base" },
|
|
186
|
+
* "POST /api/query": {
|
|
187
|
+
* price: "$0.05",
|
|
188
|
+
* network: "base",
|
|
189
|
+
* config: {
|
|
190
|
+
* description: "Query the database",
|
|
191
|
+
* inputSchema: { bodyType: "json", bodyFields: { q: { type: "string" } } },
|
|
192
|
+
* outputSchema: { type: "object", properties: { result: { type: "string" } } },
|
|
193
|
+
* }
|
|
194
|
+
* }
|
|
195
|
+
* }));
|
|
196
|
+
*/
|
|
197
|
+
function paymentMiddleware(payTo, routes, options = {}) {
|
|
198
|
+
const facilitatorUrl = options.facilitatorUrl || DEFAULT_FACILITATOR_URL;
|
|
199
|
+
|
|
200
|
+
return async function x402Middleware(req, res, next) {
|
|
201
|
+
// Find matching route
|
|
202
|
+
const routeConfig = matchRoute(routes, req.method, req.path);
|
|
203
|
+
if (!routeConfig) return next(); // No payment required for this route
|
|
204
|
+
|
|
205
|
+
const paymentRequirements = buildPaymentRequirements(payTo, routeConfig, req);
|
|
206
|
+
|
|
207
|
+
// Check for payment header
|
|
208
|
+
const paymentHeader = req.headers["x-payment"];
|
|
209
|
+
|
|
210
|
+
if (!paymentHeader) {
|
|
211
|
+
// No payment — return 402
|
|
212
|
+
const encoded = Buffer.from(JSON.stringify([paymentRequirements])).toString("base64");
|
|
213
|
+
res.setHeader("X-PAYMENT-REQUIRED", encoded);
|
|
214
|
+
return res.status(402).json({
|
|
215
|
+
x402Version: 1,
|
|
216
|
+
error: "X-PAYMENT header is required",
|
|
217
|
+
accepts: [paymentRequirements],
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Parse payment payload
|
|
222
|
+
let paymentPayload;
|
|
223
|
+
try {
|
|
224
|
+
paymentPayload = JSON.parse(Buffer.from(paymentHeader, "base64").toString("utf8"));
|
|
225
|
+
} catch {
|
|
226
|
+
return res.status(400).json({ error: "Invalid X-PAYMENT header (not valid base64 JSON)" });
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Verify with facilitator
|
|
230
|
+
const verification = await verifyPayment(facilitatorUrl, paymentPayload, paymentRequirements);
|
|
231
|
+
if (!verification.valid) {
|
|
232
|
+
return res.status(402).json({
|
|
233
|
+
x402Version: 1,
|
|
234
|
+
error: "Payment verification failed",
|
|
235
|
+
details: verification.error,
|
|
236
|
+
accepts: [paymentRequirements],
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Payment verified — intercept the response to settle after
|
|
241
|
+
const originalJson = res.json.bind(res);
|
|
242
|
+
const originalSend = res.send.bind(res);
|
|
243
|
+
|
|
244
|
+
let settled = false;
|
|
245
|
+
const doSettle = async () => {
|
|
246
|
+
if (settled) return;
|
|
247
|
+
settled = true;
|
|
248
|
+
const settlement = await settlePayment(facilitatorUrl, paymentPayload, paymentRequirements);
|
|
249
|
+
if (settlement.transaction) {
|
|
250
|
+
// Add settlement info to response header
|
|
251
|
+
const encoded = Buffer.from(JSON.stringify(settlement)).toString("base64");
|
|
252
|
+
res.setHeader("X-PAYMENT-RESPONSE", encoded);
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
res.json = async function (body) {
|
|
257
|
+
await doSettle();
|
|
258
|
+
return originalJson(body);
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
res.send = async function (body) {
|
|
262
|
+
await doSettle();
|
|
263
|
+
return originalSend(body);
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
next();
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
module.exports = { paymentMiddleware, priceToAtomic, matchRoute, USDC_CONFIG };
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "x402-lite",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Lightweight x402 payment middleware for Express. Zero crypto dependencies — delegates verification and settlement to the facilitator. Perfect for Raspberry Pi, edge, and resource-constrained environments.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"keywords": ["x402", "payment", "middleware", "express", "lightweight", "edge", "raspberry-pi"],
|
|
7
|
+
"author": "Nexus <nexus@nexusagentorg.github.io>",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/NexusAgentOrg/x402-lite"
|
|
12
|
+
},
|
|
13
|
+
"peerDependencies": {
|
|
14
|
+
"express": ">=4.0.0"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {},
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=18.0.0"
|
|
19
|
+
}
|
|
20
|
+
}
|
package/test.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* x402-lite test suite — no external dependencies
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { paymentMiddleware, priceToAtomic, matchRoute, USDC_CONFIG } = require("./index.js");
|
|
6
|
+
|
|
7
|
+
let passed = 0;
|
|
8
|
+
let failed = 0;
|
|
9
|
+
|
|
10
|
+
function assert(condition, name) {
|
|
11
|
+
if (condition) {
|
|
12
|
+
console.log(`✓ ${name}`);
|
|
13
|
+
passed++;
|
|
14
|
+
} else {
|
|
15
|
+
console.log(`✗ ${name}`);
|
|
16
|
+
failed++;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// --- Unit tests ---
|
|
21
|
+
|
|
22
|
+
// priceToAtomic
|
|
23
|
+
assert(priceToAtomic("$0.01") === "10000", "priceToAtomic: $0.01 = 10000");
|
|
24
|
+
assert(priceToAtomic("$1.00") === "1000000", "priceToAtomic: $1.00 = 1000000");
|
|
25
|
+
assert(priceToAtomic("$0.001") === "1000", "priceToAtomic: $0.001 = 1000");
|
|
26
|
+
assert(priceToAtomic(0.05) === "50000", "priceToAtomic: 0.05 = 50000");
|
|
27
|
+
assert(priceToAtomic("0.10") === "100000", "priceToAtomic: 0.10 = 100000");
|
|
28
|
+
|
|
29
|
+
// matchRoute
|
|
30
|
+
const routes = {
|
|
31
|
+
"GET /api/data": { price: "$0.01" },
|
|
32
|
+
"POST /api/query": { price: "$0.05" },
|
|
33
|
+
"/api/any": { price: "$0.02" },
|
|
34
|
+
"GET /api/wild/*": { price: "$0.03" },
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
assert(matchRoute(routes, "GET", "/api/data") !== null, "matchRoute: exact GET match");
|
|
38
|
+
assert(matchRoute(routes, "POST", "/api/data") === null, "matchRoute: wrong method returns null");
|
|
39
|
+
assert(matchRoute(routes, "POST", "/api/query") !== null, "matchRoute: POST match");
|
|
40
|
+
assert(matchRoute(routes, "GET", "/api/any") !== null, "matchRoute: any method GET");
|
|
41
|
+
assert(matchRoute(routes, "DELETE", "/api/any") !== null, "matchRoute: any method DELETE");
|
|
42
|
+
assert(matchRoute(routes, "GET", "/api/wild/foo") !== null, "matchRoute: wildcard match");
|
|
43
|
+
assert(matchRoute(routes, "GET", "/api/wild/foo/bar") !== null, "matchRoute: deep wildcard");
|
|
44
|
+
assert(matchRoute(routes, "GET", "/unmatched") === null, "matchRoute: unmatched returns null");
|
|
45
|
+
|
|
46
|
+
// USDC_CONFIG
|
|
47
|
+
assert(USDC_CONFIG["base"].chainId === 8453, "USDC_CONFIG: base chainId");
|
|
48
|
+
assert(USDC_CONFIG["base"].decimals === 6, "USDC_CONFIG: base decimals");
|
|
49
|
+
assert(USDC_CONFIG["base-sepolia"].chainId === 84532, "USDC_CONFIG: base-sepolia chainId");
|
|
50
|
+
|
|
51
|
+
// --- Middleware integration test (mock) ---
|
|
52
|
+
|
|
53
|
+
function mockReq(method, path, headers = {}) {
|
|
54
|
+
return {
|
|
55
|
+
method,
|
|
56
|
+
path,
|
|
57
|
+
protocol: "https",
|
|
58
|
+
headers: { host: "example.com", ...headers },
|
|
59
|
+
header(name) { return this.headers[name.toLowerCase()]; },
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function mockRes() {
|
|
64
|
+
const res = {
|
|
65
|
+
statusCode: null,
|
|
66
|
+
headers: {},
|
|
67
|
+
body: null,
|
|
68
|
+
status(code) { res.statusCode = code; return res; },
|
|
69
|
+
json(data) { res.body = data; return res; },
|
|
70
|
+
send(data) { res.body = data; return res; },
|
|
71
|
+
setHeader(k, v) { res.headers[k] = v; },
|
|
72
|
+
};
|
|
73
|
+
return res;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Test: no payment → 402
|
|
77
|
+
(async () => {
|
|
78
|
+
const mw = paymentMiddleware("0x1234567890abcdef1234567890abcdef12345678", {
|
|
79
|
+
"GET /api/test": { price: "$0.01", network: "base" },
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const req = mockReq("GET", "/api/test");
|
|
83
|
+
const res = mockRes();
|
|
84
|
+
let nextCalled = false;
|
|
85
|
+
|
|
86
|
+
await mw(req, res, () => { nextCalled = true; });
|
|
87
|
+
|
|
88
|
+
assert(res.statusCode === 402, "middleware: returns 402 without payment");
|
|
89
|
+
assert(!nextCalled, "middleware: does not call next without payment");
|
|
90
|
+
assert(res.body && res.body.accepts, "middleware: 402 body includes accepts");
|
|
91
|
+
assert(res.body.accepts[0].scheme === "exact", "middleware: accepts scheme is exact");
|
|
92
|
+
assert(res.body.accepts[0].maxAmountRequired === "10000", "middleware: amount is 10000");
|
|
93
|
+
assert(res.headers["X-PAYMENT-REQUIRED"], "middleware: X-PAYMENT-REQUIRED header set");
|
|
94
|
+
|
|
95
|
+
// Test: unprotected route → next()
|
|
96
|
+
const req2 = mockReq("GET", "/health");
|
|
97
|
+
const res2 = mockRes();
|
|
98
|
+
let next2Called = false;
|
|
99
|
+
await mw(req2, res2, () => { next2Called = true; });
|
|
100
|
+
assert(next2Called, "middleware: unprotected route calls next()");
|
|
101
|
+
|
|
102
|
+
// Summary
|
|
103
|
+
console.log(`\n📊 Results: ${passed} passed, ${failed} failed`);
|
|
104
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
105
|
+
})();
|