@scriptmasterlabs/x402-paywall 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 ADDED
@@ -0,0 +1,40 @@
1
+ # @scriptmasterlabs/x402-paywall
2
+
3
+ **One line of code. AI agents pay. No Stripe. No API keys. No accounts.**
4
+
5
+ ```js
6
+ import { x402 } from '@scriptmasterlabs/x402-paywall'
7
+
8
+ app.use('/premium', x402({
9
+ price: '0.001',
10
+ payTo: '0xYourBaseAddress',
11
+ freeForHumans: true, // bots/agents pay; browsers free
12
+ }))
13
+ ```
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ npm i @scriptmasterlabs/x402-paywall
19
+ ```
20
+
21
+ ## What happens
22
+
23
+ 1. Agent hits your route → **HTTP 402** + machine-readable `accepts[]`
24
+ 2. Agent sends **0.001 USDC** on Base to `payTo`
25
+ 3. Agent retries with header: `X-PAYMENT-TX: 0x<txHash>`
26
+ 4. Middleware verifies the on-chain USDC transfer → your handler runs
27
+
28
+ ## CRAWLTOLL mode
29
+
30
+ `freeForHumans: true` → Chrome/Safari free; GPTBot/ClaudeBot/curl/mcp pay.
31
+
32
+ ## Live $0.001 snacks (already deployed)
33
+
34
+ See: https://www.scriptmasterlabs.com/x402-snacks.html
35
+
36
+ ## Related
37
+
38
+ - MCP drop-in: `npx @scriptmasterlabs/mcp-x402`
39
+ - SDK for tool authors: `@scriptmasterlabs/mcp-x402-sdk`
40
+ - CRAWLTOLL product page: https://www.scriptmasterlabs.com/crawltoll.html
@@ -0,0 +1,40 @@
1
+ import express from 'express';
2
+ import { x402 } from '../src/index.js';
3
+
4
+ const app = express();
5
+ const PAY_TO = process.env.PAY_TO || '0x4e14B249D9A4c9c9352D780eCEB508A8eB7a7700';
6
+
7
+ // Humans free, agents/bots pay $0.001 (CRAWLTOLL mode)
8
+ app.use(
9
+ '/premium',
10
+ x402({ price: '0.001', payTo: PAY_TO, freeForHumans: true, description: 'premium demo' }),
11
+ );
12
+
13
+ app.get('/premium/secret', (req, res) => {
14
+ res.json({
15
+ ok: true,
16
+ message: 'You paid (or you are human).',
17
+ payment: req.x402 || null,
18
+ });
19
+ });
20
+
21
+ // Always-paid API path
22
+ app.use('/api/paid', x402({ price: '0.001', payTo: PAY_TO }));
23
+ app.get('/api/paid/ping', (req, res) => {
24
+ res.json({ pong: true, tx: req.x402?.txHash });
25
+ });
26
+
27
+ app.get('/', (_req, res) => {
28
+ res.type('html').send(`<pre>
29
+ @scriptmasterlabs/x402-paywall demo
30
+
31
+ GET /premium/secret — humans free, bots 402
32
+ GET /api/paid/ping — always 402 until X-PAYMENT-TX
33
+
34
+ payTo: ${PAY_TO}
35
+ price: 0.001 USDC on Base
36
+ </pre>`);
37
+ });
38
+
39
+ const port = process.env.PORT || 8787;
40
+ app.listen(port, () => console.log(`x402 demo http://127.0.0.1:${port}`));
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@scriptmasterlabs/x402-paywall",
3
+ "version": "0.1.0",
4
+ "description": "One-line x402 paywall: app.use('/premium', x402({ price: '0.001', payTo: '0x...' })). Agents pay USDC on Base. Humans optional free.",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js"
9
+ },
10
+ "files": ["src", "README.md", "examples"],
11
+ "keywords": ["x402", "http-402", "paywall", "usdc", "base", "ai-agents", "middleware", "express", "micropayments"],
12
+ "author": "ScriptMasterLabs",
13
+ "license": "MIT",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/Timwal78/SML_Portfolio.git",
17
+ "directory": "packages/x402-paywall"
18
+ },
19
+ "engines": { "node": ">=18" }
20
+ }
package/src/index.js ADDED
@@ -0,0 +1,191 @@
1
+ /**
2
+ * @scriptmasterlabs/x402-paywall
3
+ *
4
+ * One line:
5
+ * app.use('/premium', x402({ price: '0.001', payTo: '0xYourAddress' }))
6
+ *
7
+ * Agents hit 402 → pay USDC on Base → retry with X-PAYMENT-TX (or X-PAYMENT).
8
+ * Set freeForHumans: true to toll bots/agents only (CRAWLTOLL mode).
9
+ */
10
+
11
+ const USDC_BASE = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
12
+ const NETWORK = 'eip155:8453';
13
+
14
+ const BOT_RE =
15
+ /GPTBot|ClaudeBot|Claude-Web|Anthropic|PerplexityBot|Google-Extended|Bytespider|CCBot|Amazonbot|Applebot-Extended|meta-externalagent|ChatGPT|OpenAI|mcp-client|x402|curl\/|python-requests|axios|Go-http-client|Java\/|httpx|aiohttp|node-fetch|undici|wget/i;
16
+
17
+ function toAmountAtomic(price) {
18
+ // price in USDC dollars → 6 decimals
19
+ const n = Number(price);
20
+ if (!Number.isFinite(n) || n <= 0) throw new Error('x402: price must be a positive number string');
21
+ return String(Math.round(n * 1e6));
22
+ }
23
+
24
+ function isHuman(req, freeForHumans) {
25
+ if (!freeForHumans) return false;
26
+ if (req.get('x-payment') || req.get('x-payment-tx')) return false; // paying client = agent path
27
+ const ua = req.get('user-agent') || '';
28
+ if (!ua) return false;
29
+ if (BOT_RE.test(ua)) return false;
30
+ // browser-ish
31
+ return /Mozilla|Chrome|Safari|Firefox|Edg/i.test(ua);
32
+ }
33
+
34
+ function buildAccepts({ price, payTo, description }) {
35
+ const amount = toAmountAtomic(price);
36
+ return [
37
+ {
38
+ scheme: 'exact',
39
+ network: NETWORK,
40
+ amount,
41
+ maxAmountRequired: amount,
42
+ asset: USDC_BASE,
43
+ payTo,
44
+ resource: description || 'x402-protected resource',
45
+ description: description || `Pay ${price} USDC on Base (x402). Retry with X-PAYMENT-TX: <txHash>`,
46
+ mimeType: 'application/json',
47
+ maxTimeoutSeconds: 120,
48
+ extra: {
49
+ name: 'USDC',
50
+ version: '2',
51
+ rail: 'sovereign-x-payment-tx',
52
+ chain: 'base',
53
+ },
54
+ },
55
+ ];
56
+ }
57
+
58
+ /**
59
+ * Verify a Base USDC transfer to payTo for at least `amount` atomic units.
60
+ * Uses public RPC + eth_getTransactionReceipt. No API key.
61
+ */
62
+ async function verifySovereignTx({ txHash, payTo, amountAtomic, rpcUrl }) {
63
+ if (!txHash || !/^0x[a-fA-F0-9]{64}$/.test(txHash)) {
64
+ return { ok: false, reason: 'invalid_tx_hash' };
65
+ }
66
+ const rpc = rpcUrl || process.env.BASE_RPC_URL || 'https://mainnet.base.org';
67
+ const body = {
68
+ jsonrpc: '2.0',
69
+ id: 1,
70
+ method: 'eth_getTransactionReceipt',
71
+ params: [txHash],
72
+ };
73
+ const res = await fetch(rpc, {
74
+ method: 'POST',
75
+ headers: { 'content-type': 'application/json' },
76
+ body: JSON.stringify(body),
77
+ });
78
+ const json = await res.json();
79
+ const receipt = json.result;
80
+ if (!receipt || receipt.status !== '0x1') {
81
+ return { ok: false, reason: 'tx_not_found_or_failed' };
82
+ }
83
+
84
+ // ERC-20 Transfer(address,address,uint256)
85
+ const TRANSFER_TOPIC =
86
+ '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';
87
+ const payToTopic = '0x' + payTo.toLowerCase().replace(/^0x/, '').padStart(64, '0');
88
+ const usdc = USDC_BASE.toLowerCase();
89
+ const need = BigInt(amountAtomic);
90
+
91
+ for (const log of receipt.logs || []) {
92
+ if ((log.address || '').toLowerCase() !== usdc) continue;
93
+ if (!log.topics || log.topics[0] !== TRANSFER_TOPIC) continue;
94
+ if ((log.topics[2] || '').toLowerCase() !== payToTopic) continue;
95
+ const value = BigInt(log.data || '0x0');
96
+ if (value >= need) {
97
+ return {
98
+ ok: true,
99
+ txHash,
100
+ from: '0x' + (log.topics[1] || '').slice(26),
101
+ amount: value.toString(),
102
+ };
103
+ }
104
+ }
105
+ return { ok: false, reason: 'no_matching_usdc_transfer' };
106
+ }
107
+
108
+ // simple in-memory replay guard (process-local)
109
+ const seenTx = new Map();
110
+ function rememberTx(tx) {
111
+ seenTx.set(tx, Date.now());
112
+ if (seenTx.size > 5000) {
113
+ const first = seenTx.keys().next().value;
114
+ seenTx.delete(first);
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Express / Connect middleware factory.
120
+ * @param {object} opts
121
+ * @param {string} opts.price - USDC amount as decimal string, e.g. '0.001'
122
+ * @param {string} opts.payTo - destination Base address (0x...)
123
+ * @param {boolean} [opts.freeForHumans=false] - CRAWLTOLL mode: browsers free, bots pay
124
+ * @param {string} [opts.description]
125
+ * @param {string} [opts.rpcUrl] - Base JSON-RPC
126
+ */
127
+ export function x402(opts = {}) {
128
+ const { price, payTo, freeForHumans = false, description, rpcUrl } = opts;
129
+ if (!price) throw new Error('x402({ price }) required');
130
+ if (!payTo || !/^0x[a-fA-F0-9]{40}$/.test(payTo)) {
131
+ throw new Error('x402({ payTo }) must be a 0x address');
132
+ }
133
+ const amountAtomic = toAmountAtomic(price);
134
+ const accepts = buildAccepts({ price, payTo, description });
135
+
136
+ return async function x402Middleware(req, res, next) {
137
+ try {
138
+ if (isHuman(req, freeForHumans)) return next();
139
+
140
+ const txHeader =
141
+ req.get('x-payment-tx') ||
142
+ req.get('X-PAYMENT-TX') ||
143
+ (req.get('x-payment') || '').replace(/^tx:/i, '');
144
+
145
+ if (txHeader) {
146
+ if (seenTx.has(txHeader)) {
147
+ // allow same tx only once per process (anti double-spend on replay)
148
+ // soft: still verify once then mark — if already seen, reject
149
+ res.status(402).json({
150
+ x402Version: 1,
151
+ error: 'tx_already_used',
152
+ accepts,
153
+ });
154
+ return;
155
+ }
156
+ const ver = await verifySovereignTx({
157
+ txHash: txHeader,
158
+ payTo,
159
+ amountAtomic,
160
+ rpcUrl,
161
+ });
162
+ if (ver.ok) {
163
+ rememberTx(txHeader);
164
+ req.x402 = ver;
165
+ res.setHeader('X-PAYMENT-RESPONSE', JSON.stringify({ success: true, tx: txHeader }));
166
+ return next();
167
+ }
168
+ res.status(402).json({
169
+ x402Version: 1,
170
+ error: ver.reason || 'payment_invalid',
171
+ accepts,
172
+ });
173
+ return;
174
+ }
175
+
176
+ // Challenge
177
+ res.setHeader('Content-Type', 'application/json');
178
+ res.setHeader('Cache-Control', 'no-store');
179
+ res.status(402).json({
180
+ x402Version: 1,
181
+ error: 'Payment Required',
182
+ accepts,
183
+ resource: req.originalUrl || req.url,
184
+ });
185
+ } catch (err) {
186
+ res.status(500).json({ error: 'x402_middleware_error', message: String(err?.message || err) });
187
+ }
188
+ };
189
+ }
190
+
191
+ export default x402;