agent402-tollbooth 0.1.0 → 0.1.2

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 CHANGED
@@ -10,6 +10,31 @@ Stack Overflow) shipped pay-per-crawl as a closed, fiat, you-must-be-on-our-CDN
10
10
  feature. This is the open, crypto-native, run-it-yourself version — built on the
11
11
  same hardened 402 + proof-of-work machinery as [Agent402](https://github.com/MikeyPetrillo/Agent402).
12
12
 
13
+ ## See it work (one command)
14
+
15
+ ```bash
16
+ npx agent402-tollbooth # then, in the repo: npm run --prefix tollbooth demo
17
+ ```
18
+
19
+ ```text
20
+ agent402-tollbooth — live pay-per-crawl demo
21
+
22
+ ① A human opens the page (normal browser)
23
+ → HTTP 200 FREE "📄 The Future of Machine Payments — full article text…"
24
+ Humans are never charged.
25
+
26
+ ② An AI crawler hits the same page (ClaudeBot)
27
+ → HTTP 402 Payment Required
28
+ pay with USDC: $0.002 USDC on base → 0x…
29
+ …or free with proof-of-work: a 18-bit sha256 puzzle
30
+
31
+ ③ The crawler has no wallet, so it spends CPU instead
32
+ solved in 0.32s (nonce=100208)
33
+ → HTTP 200 OK (paid via pow) "📄 The Future of Machine Payments — full article text…"
34
+
35
+ ✓ Pay-per-crawl, end to end — humans free, bots pay (USDC or compute).
36
+ ```
37
+
13
38
  ## Install
14
39
 
15
40
  ```bash
@@ -61,6 +86,41 @@ TOLLBOOTH_PAYTO=0xYourWallet \
61
86
  npx agent402-tollbooth # listens on :4021, proxies humans free, charges bots
62
87
  ```
63
88
 
89
+ ## Run on the edge (Cloudflare Workers, Next.js, Deno, Bun)
90
+
91
+ The same gate is also built on the Web Crypto + Fetch APIs (`edge.js`), so it runs
92
+ anywhere — no Node required. The gate returns a `402 Response` when the client
93
+ must pay, or `null` to let it through.
94
+
95
+ **Cloudflare Workers** (poetic: the open pay-per-crawl, on the incumbent's own platform). `wrangler.toml`:
96
+
97
+ ```toml
98
+ name = "tollbooth"
99
+ main = "node_modules/agent402-tollbooth/worker.js"
100
+ [vars]
101
+ TOLLBOOTH_UPSTREAM = "https://your-origin.example.com"
102
+ TOLLBOOTH_PAYTO = "0xYourWallet" # optional: advertise a USDC x402 quote
103
+ # wrangler secret put TOLLBOOTH_SECRET
104
+ # optional single-use replay store: [[kv_namespaces]] binding = "TOLLBOOTH_KV"
105
+ ```
106
+
107
+ **Next.js middleware** (`middleware.ts`):
108
+
109
+ ```ts
110
+ import { createEdgeTollbooth } from "agent402-tollbooth/edge";
111
+ const gate = createEdgeTollbooth({ secret: process.env.TOLLBOOTH_SECRET! });
112
+
113
+ export async function middleware(req: Request) {
114
+ return (await gate(req)) ?? NextResponse.next();
115
+ }
116
+ ```
117
+
118
+ **Any Fetch-API runtime** (Deno, Bun, custom): `const gate = createEdgeTollbooth({ secret }); const blocked = await gate(request); return blocked ?? fetch(request);`
119
+
120
+ > On the edge, pass a stable `secret` (PoW tokens are HMAC-signed). For
121
+ > single-use replay protection across stateless invocations, supply a `store`
122
+ > (e.g. a Cloudflare KV wrapper — the Worker entry wires this for you).
123
+
64
124
  ## Accepting USDC (x402)
65
125
 
66
126
  The proof-of-work rail works with **zero config**. To also settle real USDC,
package/demo.js ADDED
@@ -0,0 +1,58 @@
1
+ // Live, narrated pay-per-crawl demo. Spins up a "premium content" site behind
2
+ // the tollbooth, then shows a human getting it free and an AI crawler paying to
3
+ // get through (here: by solving a proof-of-work, since that needs no wallet).
4
+ //
5
+ // node demo.js (or: npm run demo)
6
+ import express from "express";
7
+ import { createHash } from "node:crypto";
8
+ import { createTollbooth } from "./index.js";
9
+
10
+ const c = { dim: "\x1b[2m", reset: "\x1b[0m", green: "\x1b[32m", yellow: "\x1b[33m", cyan: "\x1b[36m", bold: "\x1b[1m" };
11
+ const log = (s = "") => console.log(s);
12
+ const lz = (buf) => { let b = 0; for (const x of buf) { if (x === 0) { b += 8; continue; } b += Math.clz32(x) - 24; break; } return b; };
13
+
14
+ // A "premium content" site, fronted by the tollbooth.
15
+ const app = express();
16
+ app.use(createTollbooth({ price: "$0.002", payTo: "0xYourWalletHere000000000000000000000000", powDifficulty: 18 }));
17
+ app.use((_req, res) => res.send("📄 The Future of Machine Payments — full article text…"));
18
+ const server = app.listen(0);
19
+ const base = `http://localhost:${server.address().port}`;
20
+ const visit = (ua, extra = {}) => fetch(`${base}/article`, { headers: { "user-agent": ua, ...extra } });
21
+
22
+ const HUMAN = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36";
23
+ const CRAWLER = "Mozilla/5.0 (compatible; ClaudeBot/1.0; +claudebot@anthropic.com)";
24
+
25
+ try {
26
+ log(`${c.bold}agent402-tollbooth — live pay-per-crawl demo${c.reset}`);
27
+ log(`${c.dim}A premium-content site is now running behind the tollbooth.${c.reset}\n`);
28
+
29
+ // 1) A human browses — free.
30
+ log(`${c.cyan}① A human opens the page (normal browser)${c.reset}`);
31
+ let r = await visit(HUMAN);
32
+ log(` → HTTP ${r.status} ${c.green}FREE${c.reset} "${await r.text()}"`);
33
+ log(` ${c.dim}Humans are never charged.${c.reset}\n`);
34
+
35
+ // 2) An AI crawler hits the same page — 402.
36
+ log(`${c.cyan}② An AI crawler hits the same page (ClaudeBot)${c.reset}`);
37
+ r = await visit(CRAWLER);
38
+ const q = await r.json();
39
+ const a = q.accepts?.[0];
40
+ log(` → HTTP ${r.status} ${c.yellow}Payment Required${c.reset}`);
41
+ if (a) log(` ${c.dim}pay with USDC:${c.reset} ${a.maxAmountRequired} ${a.asset} on ${a.network} → ${a.payTo}`);
42
+ log(` ${c.dim}…or free with proof-of-work:${c.reset} a ${q.proofOfWork.difficulty}-bit sha256 puzzle\n`);
43
+
44
+ // 3) The crawler has no wallet, so it solves the proof-of-work.
45
+ log(`${c.cyan}③ The crawler has no wallet, so it spends CPU instead${c.reset}`);
46
+ const t0 = Date.now();
47
+ let nonce = 0;
48
+ while (lz(createHash("sha256").update(`${q.proofOfWork.challenge}:${nonce}`).digest()) < q.proofOfWork.difficulty) nonce++;
49
+ log(` ${c.dim}solved in ${((Date.now() - t0) / 1000).toFixed(2)}s (nonce=${nonce})${c.reset}`);
50
+
51
+ // 4) Retry with the solution — unlocked.
52
+ r = await visit(CRAWLER, { "x-pow-solution": `${q.proofOfWork.token}:${nonce}` });
53
+ log(` → HTTP ${r.status} ${c.green}OK${c.reset} (paid via ${r.headers.get("x-tollbooth-paid")}) "${await r.text()}"\n`);
54
+
55
+ log(`${c.bold}${c.green}✓ Pay-per-crawl, end to end${c.reset} — humans free, bots pay (USDC or compute). No Cloudflare, no Stripe, no signup.`);
56
+ } finally {
57
+ server.close();
58
+ }
package/edge.js ADDED
@@ -0,0 +1,144 @@
1
+ // Edge / runtime-agnostic tollbooth — the same pay-per-crawl gate, built on the
2
+ // Web Crypto + Fetch APIs so it runs anywhere: Cloudflare Workers, Next.js
3
+ // middleware (edge), Deno, Bun, and Node 20+. No node:* imports.
4
+ //
5
+ // const gate = createEdgeTollbooth({ secret: env.TOLLBOOTH_SECRET });
6
+ // const blocked = await gate(request); // Response(402) if must pay, else null
7
+ //
8
+ // See worker.js for a Cloudflare Worker entry, and the README for Next.js.
9
+ import { makeBotMatcher, AI_BOTS } from "./bots.js";
10
+
11
+ const te = new TextEncoder();
12
+
13
+ function b64url(buf) {
14
+ const bytes = new Uint8Array(buf);
15
+ let s = "";
16
+ for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);
17
+ return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
18
+ }
19
+ const toHex = (bytes) => [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
20
+ function leadingZeroBits(bytes) {
21
+ let n = 0;
22
+ for (const b of bytes) { if (b === 0) { n += 8; continue; } n += Math.clz32(b) - 24; break; }
23
+ return n;
24
+ }
25
+ async function hmac(secret, payload) {
26
+ const key = await crypto.subtle.importKey("raw", te.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
27
+ return b64url(await crypto.subtle.sign("HMAC", key, te.encode(payload)));
28
+ }
29
+ const sha256 = async (str) => new Uint8Array(await crypto.subtle.digest("SHA-256", te.encode(str)));
30
+ function constEq(a, b) {
31
+ if (a.length !== b.length) return false;
32
+ let r = 0;
33
+ for (let i = 0; i < a.length; i++) r |= a.charCodeAt(i) ^ b.charCodeAt(i);
34
+ return r === 0;
35
+ }
36
+
37
+ /**
38
+ * Web Crypto proof-of-work. `secret` is required (edge runtimes can't keep a
39
+ * random per-process secret across invocations). `store` is an optional
40
+ * single-use replay backend ({ has(token), add(token, expMs) }, both async) —
41
+ * e.g. a Cloudflare KV wrapper; defaults to in-isolate memory.
42
+ */
43
+ export function createEdgePow({ secret, difficulty = 18, ttlMs = 5 * 60 * 1000, store } = {}) {
44
+ if (!secret) throw new Error("createEdgePow requires a stable `secret` string");
45
+ const mem = new Map();
46
+ const used = store || {
47
+ has: async (k) => { const e = mem.get(k); if (e && e < Date.now()) { mem.delete(k); return false; } return mem.has(k); },
48
+ add: async (k, exp) => { mem.set(k, exp); },
49
+ };
50
+
51
+ async function challenge(resource) {
52
+ const rnd = new Uint8Array(16);
53
+ crypto.getRandomValues(rnd);
54
+ const chal = toHex(rnd);
55
+ const exp = Date.now() + ttlMs;
56
+ const payload = `${chal}.${exp}.${difficulty}.${resource}`;
57
+ const token = `${payload}.${await hmac(secret, payload)}`;
58
+ return {
59
+ algorithm: "sha256",
60
+ challenge: chal,
61
+ difficulty,
62
+ expires: exp,
63
+ token,
64
+ rule: `Find an integer nonce so sha256("${chal}:" + nonce) has >= ${difficulty} leading zero bits, then resend with header X-Pow-Solution: ${token}:<nonce>`,
65
+ };
66
+ }
67
+
68
+ async function verify(headerValue, resource) {
69
+ if (!headerValue || typeof headerValue !== "string") return { ok: false, reason: "missing solution" };
70
+ const cut = headerValue.lastIndexOf(":");
71
+ if (cut < 0) return { ok: false, reason: "malformed solution" };
72
+ const token = headerValue.slice(0, cut);
73
+ const nonce = headerValue.slice(cut + 1);
74
+ const parts = token.split(".");
75
+ if (parts.length !== 5) return { ok: false, reason: "malformed token" };
76
+ const [chal, expStr, diffStr, res, sig] = parts;
77
+ const expected = await hmac(secret, `${chal}.${expStr}.${diffStr}.${res}`);
78
+ if (!constEq(sig, expected)) return { ok: false, reason: "bad signature" };
79
+ if (res !== resource) return { ok: false, reason: "wrong resource" };
80
+ if (Date.now() > Number(expStr)) return { ok: false, reason: "expired" };
81
+ if (await used.has(token)) return { ok: false, reason: "already used" };
82
+ const h = await sha256(`${chal}:${nonce}`);
83
+ if (leadingZeroBits(h) < Number(diffStr)) return { ok: false, reason: "insufficient work" };
84
+ await used.add(token, Number(expStr));
85
+ return { ok: true };
86
+ }
87
+
88
+ return { challenge, verify, difficulty };
89
+ }
90
+
91
+ /**
92
+ * Create the edge gate. Returns `async (request) => Response | null`:
93
+ * a 402 `Response` when the client must pay, or `null` to allow the request
94
+ * through (proxy it, or `NextResponse.next()`).
95
+ */
96
+ export function createEdgeTollbooth(config = {}) {
97
+ const {
98
+ price = "$0.001",
99
+ payTo = null,
100
+ network = "base",
101
+ asset = "USDC",
102
+ pow = true,
103
+ powDifficulty,
104
+ secret,
105
+ store,
106
+ botUserAgents = AI_BOTS,
107
+ charge,
108
+ free,
109
+ resourceBaseUrl = "",
110
+ message = "This resource charges automated / AI clients per request. Humans browse free; bots pay in USDC via x402 or by solving a proof-of-work.",
111
+ } = config;
112
+
113
+ const isBot = makeBotMatcher(botUserAgents);
114
+ const powEngine = pow ? createEdgePow({ secret, difficulty: powDifficulty, store }) : null;
115
+
116
+ const shouldCharge = (request) => {
117
+ if (typeof free === "function" && free(request)) return false;
118
+ if (typeof charge === "function") return Boolean(charge(request));
119
+ return isBot(request.headers.get("user-agent") || "");
120
+ };
121
+
122
+ return async function gate(request) {
123
+ if (!shouldCharge(request)) return null;
124
+ const u = new URL(request.url);
125
+ const resource = (resourceBaseUrl ? resourceBaseUrl.replace(/\/$/, "") : "") + u.pathname + u.search;
126
+
127
+ const sol = request.headers.get("x-pow-solution");
128
+ if (powEngine && sol) {
129
+ const r = await powEngine.verify(sol, resource);
130
+ if (r.ok) return null;
131
+ }
132
+
133
+ const body = {
134
+ error: "Payment Required",
135
+ message,
136
+ accepts: payTo ? [{ scheme: "exact", network, maxAmountRequired: String(price), asset, payTo, resource }] : [],
137
+ };
138
+ if (powEngine) body.proofOfWork = await powEngine.challenge(resource);
139
+ return new Response(JSON.stringify(body), {
140
+ status: 402,
141
+ headers: { "content-type": "application/json", "x-tollbooth": "pay-per-crawl" },
142
+ });
143
+ };
144
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent402-tollbooth",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Open-source, self-hostable x402 pay-per-crawl tollbooth: charge AI bots per request (USDC via x402, or free proof-of-work) while humans browse free. Drop-in Express middleware or reverse proxy — no Cloudflare, no Stripe, no signup.",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -12,16 +12,20 @@
12
12
  },
13
13
  "files": [
14
14
  "index.js",
15
+ "edge.js",
16
+ "worker.js",
15
17
  "bots.js",
16
18
  "pow.js",
17
19
  "example.js",
20
+ "demo.js",
18
21
  "README.md"
19
22
  ],
20
23
  "engines": {
21
24
  "node": ">=20"
22
25
  },
23
26
  "scripts": {
24
- "test": "node test.js",
27
+ "test": "node test.js && node edge.test.js",
28
+ "demo": "node demo.js",
25
29
  "example": "node example.js"
26
30
  },
27
31
  "dependencies": {
@@ -39,6 +43,9 @@
39
43
  "usdc",
40
44
  "reverse-proxy",
41
45
  "express-middleware",
46
+ "cloudflare-workers",
47
+ "nextjs",
48
+ "edge",
42
49
  "self-hosted",
43
50
  "mcp",
44
51
  "ai-agents"
package/worker.js ADDED
@@ -0,0 +1,47 @@
1
+ // Cloudflare Worker entry for the tollbooth — open-source pay-per-crawl on the
2
+ // edge. Deploy in front of your origin: humans pass through, AI crawlers pay
3
+ // (USDC via x402, or free proof-of-work).
4
+ //
5
+ // wrangler.toml:
6
+ // name = "tollbooth"
7
+ // main = "node_modules/agent402-tollbooth/worker.js"
8
+ // [vars]
9
+ // TOLLBOOTH_UPSTREAM = "https://your-origin.example.com"
10
+ // TOLLBOOTH_PAYTO = "0xYourWallet" # optional (advertises USDC quote)
11
+ // # secret (required): wrangler secret put TOLLBOOTH_SECRET
12
+ // # optional single-use store: [[kv_namespaces]] binding = "TOLLBOOTH_KV"
13
+ import { createEdgeTollbooth } from "./edge.js";
14
+
15
+ const kvStore = (kv) => ({
16
+ has: async (k) => (await kv.get(k)) != null,
17
+ add: async (k, expMs) => kv.put(k, "1", { expiration: Math.ceil(expMs / 1000) }),
18
+ });
19
+
20
+ export default {
21
+ async fetch(request, env) {
22
+ if (!env.TOLLBOOTH_SECRET) {
23
+ return new Response("Tollbooth misconfigured: set TOLLBOOTH_SECRET (wrangler secret put TOLLBOOTH_SECRET)", { status: 500 });
24
+ }
25
+ const gate = createEdgeTollbooth({
26
+ secret: env.TOLLBOOTH_SECRET,
27
+ price: env.TOLLBOOTH_PRICE || "$0.001",
28
+ payTo: env.TOLLBOOTH_PAYTO || null,
29
+ network: env.TOLLBOOTH_NETWORK || "base",
30
+ powDifficulty: env.TOLLBOOTH_POW_BITS ? Number(env.TOLLBOOTH_POW_BITS) : undefined,
31
+ store: env.TOLLBOOTH_KV ? kvStore(env.TOLLBOOTH_KV) : undefined,
32
+ });
33
+
34
+ const blocked = await gate(request);
35
+ if (blocked) return blocked;
36
+
37
+ // Allowed → proxy to the origin.
38
+ const upstream = env.TOLLBOOTH_UPSTREAM;
39
+ if (!upstream) return new Response("Tollbooth: set TOLLBOOTH_UPSTREAM to your origin", { status: 500 });
40
+ const target = new URL(request.url);
41
+ const origin = new URL(upstream);
42
+ target.protocol = origin.protocol;
43
+ target.hostname = origin.hostname;
44
+ target.port = origin.port;
45
+ return fetch(new Request(target.toString(), request));
46
+ },
47
+ };