agent402-tollbooth 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,118 @@
1
+ # agent402-tollbooth
2
+
3
+ **Open-source, self-hostable x402 "pay-per-crawl". Put it in front of any site or
4
+ API: humans browse free, AI crawlers and agents pay per request** — either in
5
+ USDC over the [x402 protocol](https://x402.org), or for free by solving a
6
+ proof-of-work. No Cloudflare, no Stripe, no Merchant-of-Record, no signup.
7
+
8
+ The big platforms ([Cloudflare](https://stackoverflow.blog/2026/02/26/how-pay-per-crawl-is-reshaping-data-monetization/),
9
+ Stack Overflow) shipped pay-per-crawl as a closed, fiat, you-must-be-on-our-CDN
10
+ feature. This is the open, crypto-native, run-it-yourself version — built on the
11
+ same hardened 402 + proof-of-work machinery as [Agent402](https://github.com/MikeyPetrillo/Agent402).
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm install agent402-tollbooth
17
+ ```
18
+
19
+ ## Use it as Express middleware
20
+
21
+ ```js
22
+ import express from "express";
23
+ import { createTollbooth } from "agent402-tollbooth";
24
+
25
+ const app = express();
26
+
27
+ // Humans pass through; known AI crawlers get 402 and must pay or solve a PoW.
28
+ app.use(createTollbooth({ price: "$0.002" }));
29
+
30
+ app.get("/article", (_req, res) => res.send("…your content…"));
31
+ app.listen(3000);
32
+ ```
33
+
34
+ ```bash
35
+ curl -A "Mozilla/5.0" localhost:3000/article # human -> 200, free
36
+ curl -A "ClaudeBot/1.0" localhost:3000/article # bot -> 402 Payment Required
37
+ ```
38
+
39
+ The 402 body advertises both rails:
40
+
41
+ ```jsonc
42
+ {
43
+ "error": "Payment Required",
44
+ "message": "…humans browse free; bots pay in USDC via x402 or by solving a proof-of-work.",
45
+ "accepts": [{ "scheme": "exact", "network": "base", "maxAmountRequired": "$0.002", "asset": "USDC", "payTo": "0x…", "resource": "/article" }],
46
+ "proofOfWork": { "algorithm": "sha256", "challenge": "…", "difficulty": 18, "token": "…", "rule": "Find a nonce so sha256(challenge+\":\"+nonce) has >= 18 leading zero bits; resend with header X-Pow-Solution: <token>:<nonce>" }
47
+ }
48
+ ```
49
+
50
+ A crawler that can't (or won't) pay USDC solves the puzzle and retries with
51
+ `X-Pow-Solution: <token>:<nonce>` — sub-second of CPU, single-use, bound to that
52
+ exact URL.
53
+
54
+ ## Use it as a reverse proxy (any language/framework)
55
+
56
+ Point it at your existing site — no code changes there:
57
+
58
+ ```bash
59
+ TOLLBOOTH_UPSTREAM=https://your-site.com \
60
+ TOLLBOOTH_PAYTO=0xYourWallet \
61
+ npx agent402-tollbooth # listens on :4021, proxies humans free, charges bots
62
+ ```
63
+
64
+ ## Accepting USDC (x402)
65
+
66
+ The proof-of-work rail works with **zero config**. To also settle real USDC,
67
+ set `payTo` and supply `verifyX402` — wire it to the standard, audited x402
68
+ server stack (`@x402/express` / your facilitator) rather than reinventing
69
+ settlement:
70
+
71
+ ```js
72
+ import { paymentMiddleware } from "x402-express"; // or @x402/express
73
+ const x402 = paymentMiddleware(/* your wallet + facilitator config */);
74
+
75
+ app.use(createTollbooth({
76
+ payTo: "0xYourWallet",
77
+ network: "base",
78
+ // Reuse the standard middleware to verify the X-PAYMENT header:
79
+ verifyX402: (req) => new Promise((resolve) =>
80
+ x402(req, { setHeader() {}, status() { return this; }, json() { resolve(false); } }, () => resolve(true))),
81
+ }));
82
+ ```
83
+
84
+ (PoW is checked first, so an agent without a wallet always has a free path.)
85
+
86
+ ## Configuration
87
+
88
+ | Option | Default | What |
89
+ |---|---|---|
90
+ | `price` | `"$0.001"` | Advertised price per request (x402 quote) |
91
+ | `payTo` | – | Wallet address; set to advertise a USDC x402 quote |
92
+ | `network` | `"base"` | x402 network |
93
+ | `pow` | `true` | Enable the free proof-of-work rail |
94
+ | `powDifficulty` | `18` | PoW difficulty in leading zero bits (~0.1–0.5s of CPU) |
95
+ | `botUserAgents` | `AI_BOTS` | User-agents to charge (AI/LLM crawlers; search indexers excluded) |
96
+ | `charge(req)` | UA match | Custom predicate for "should this client pay?" |
97
+ | `free(req)` | – | Custom force-allow predicate (wins over `charge`) |
98
+ | `verifyX402(req, reqs)` | – | Async USDC settlement check (return `true` to allow) |
99
+ | `resourceBaseUrl` | `""` | Absolute base used for the `resource` field / PoW binding |
100
+
101
+ Environment variables: `TOLLBOOTH_UPSTREAM`, `TOLLBOOTH_PAYTO`, `TOLLBOOTH_PRICE`,
102
+ `TOLLBOOTH_NETWORK`, `TOLLBOOTH_POW_BITS`, `TOLLBOOTH_SECRET`, `PORT`.
103
+
104
+ ## How it decides who pays
105
+
106
+ By default it charges requests whose `User-Agent` matches a known **AI/LLM
107
+ crawler** (GPTBot, ClaudeBot, CCBot, PerplexityBot, Bytespider, Google-Extended,
108
+ Amazonbot, …). Classic search indexers (Googlebot, Bingbot) are intentionally
109
+ **not** charged so your SEO indexing stays free. Override with `botUserAgents`,
110
+ or take full control with a `charge(req)` predicate (e.g. charge anything
111
+ without a browser-like `Accept` header, or charge everyone on `/api/*`).
112
+
113
+ ## Notes
114
+
115
+ - Proof-of-work tokens are HMAC-signed, expiry-checked, single-use, and bound to
116
+ the exact resource — a solution for `/a` can't be replayed on `/b` or reused.
117
+ - Set a stable `TOLLBOOTH_SECRET` in production so tokens survive restarts.
118
+ - MIT licensed. Part of [Agent402](https://github.com/MikeyPetrillo/Agent402).
package/bots.js ADDED
@@ -0,0 +1,30 @@
1
+ // Default set of AI / LLM crawler and agent user-agents to charge.
2
+ //
3
+ // Deliberately AI-focused: traditional search indexers (Googlebot, Bingbot,
4
+ // DuckDuckBot) are NOT here — you almost always want classic SEO indexing to
5
+ // stay free. Override the whole list with `botUserAgents`, or supply your own
6
+ // `charge(req)` predicate for full control.
7
+ export const AI_BOTS = [
8
+ "GPTBot", "OAI-SearchBot", "ChatGPT-User",
9
+ "ClaudeBot", "Claude-Web", "anthropic-ai", "Claude-User",
10
+ "PerplexityBot", "Perplexity-User",
11
+ "CCBot", "Bytespider", "Google-Extended", "Amazonbot",
12
+ "cohere-ai", "Meta-ExternalAgent", "Meta-ExternalFetcher",
13
+ "Applebot-Extended", "Diffbot", "Omgilibot", "ImagesiftBot",
14
+ "YouBot", "Timpibot", "DuckAssistBot", "PetalBot",
15
+ "FriendlyCrawler", "AI2Bot", "Scrapy", "python-requests",
16
+ ];
17
+
18
+ /**
19
+ * Build a fast case-insensitive substring matcher over a user-agent list.
20
+ * @param {string[]} list
21
+ * @returns {(userAgent?: string) => boolean}
22
+ */
23
+ export function makeBotMatcher(list = AI_BOTS) {
24
+ const needles = list.map((s) => s.toLowerCase());
25
+ return (userAgent = "") => {
26
+ const ua = String(userAgent).toLowerCase();
27
+ if (!ua) return false;
28
+ return needles.some((n) => ua.includes(n));
29
+ };
30
+ }
package/example.js ADDED
@@ -0,0 +1,29 @@
1
+ // Runnable demo: a tiny "content site" sitting behind the tollbooth.
2
+ // Humans read it free; AI crawlers get a 402 and must pay (USDC via x402) or
3
+ // solve a proof-of-work.
4
+ //
5
+ // node tollbooth/example.js
6
+ // curl -A "Mozilla/5.0" localhost:4021/article # human -> 200
7
+ // curl -A "ClaudeBot/1.0" localhost:4021/article # bot -> 402
8
+ import express from "express";
9
+ import { createTollbooth } from "./index.js";
10
+
11
+ const app = express();
12
+
13
+ app.use(createTollbooth({
14
+ price: "$0.002",
15
+ // Set payTo to advertise a USDC x402 quote (and wire `verifyX402` to settle).
16
+ payTo: process.env.TOLLBOOTH_PAYTO || undefined,
17
+ // Humans pass free; AI crawlers (GPTBot/ClaudeBot/CCBot/PerplexityBot/…) pay.
18
+ }));
19
+
20
+ app.use((_req, res) =>
21
+ res.type("html").send("<h1>Premium content</h1><p>Humans read this for free. An AI crawler paid (or solved a proof-of-work) to reach this page.</p>")
22
+ );
23
+
24
+ const port = Number(process.env.PORT) || 4021;
25
+ app.listen(port, () => {
26
+ console.log(`Demo site behind the tollbooth → http://localhost:${port}`);
27
+ console.log(` human: curl -A "Mozilla/5.0" localhost:${port}/article`);
28
+ console.log(` bot: curl -A "ClaudeBot/1.0" localhost:${port}/article`);
29
+ });
package/index.js ADDED
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env node
2
+ // agent402-tollbooth — an open-source, self-hostable x402 "pay-per-crawl" gate.
3
+ //
4
+ // Put it in front of any site or API: human visitors pass through free, while
5
+ // AI crawlers / agents must pay per request — either in USDC over the x402
6
+ // protocol, or for free by solving a proof-of-work (no wallet, no signup, no
7
+ // Stripe, no Cloudflare). Use it two ways:
8
+ //
9
+ // 1. Express middleware: app.use(createTollbooth({ ... }))
10
+ // 2. Reverse proxy (CLI): TOLLBOOTH_UPSTREAM=https://your-site.com npx agent402-tollbooth
11
+ //
12
+ // The proof-of-work rail works out of the box with zero configuration. To also
13
+ // accept USDC, set `payTo` and supply `verifyX402` (wire it to the standard
14
+ // x402 server middleware / your facilitator — see README).
15
+ import { fileURLToPath } from "node:url";
16
+ import { createPow } from "./pow.js";
17
+ import { makeBotMatcher, AI_BOTS } from "./bots.js";
18
+
19
+ export { AI_BOTS, makeBotMatcher } from "./bots.js";
20
+ export { createPow, leadingZeroBits } from "./pow.js";
21
+
22
+ /**
23
+ * Create the tollbooth Express middleware.
24
+ * @param {object} [config]
25
+ * @param {string} [config.price="$0.001"] advertised price per request
26
+ * @param {string|null} [config.payTo] wallet for the x402 quote (enables USDC rail)
27
+ * @param {string} [config.network="base"] x402 network
28
+ * @param {string} [config.asset="USDC"]
29
+ * @param {boolean} [config.pow=true] enable the free proof-of-work rail
30
+ * @param {number} [config.powDifficulty] PoW difficulty in leading zero bits
31
+ * @param {string[]} [config.botUserAgents] user-agents to charge (default: AI_BOTS)
32
+ * @param {(req)=>boolean} [config.charge] custom "should this client pay?" predicate
33
+ * @param {(req)=>boolean} [config.free] custom force-allow predicate (wins over charge)
34
+ * @param {(req, requirements)=>boolean|Promise<boolean>} [config.verifyX402] USDC settlement check
35
+ * @param {string} [config.resourceBaseUrl] absolute base for the x402 `resource`/PoW binding
36
+ * @param {string} [config.message] human-readable note included in the 402
37
+ */
38
+ export function createTollbooth(config = {}) {
39
+ const {
40
+ price = process.env.TOLLBOOTH_PRICE || "$0.001",
41
+ payTo = process.env.TOLLBOOTH_PAYTO || null,
42
+ network = process.env.TOLLBOOTH_NETWORK || "base",
43
+ asset = "USDC",
44
+ pow = true,
45
+ powDifficulty,
46
+ powSecret,
47
+ botUserAgents = AI_BOTS,
48
+ charge,
49
+ free,
50
+ verifyX402,
51
+ resourceBaseUrl = process.env.TOLLBOOTH_RESOURCE_BASE || "",
52
+ 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.",
53
+ } = config;
54
+
55
+ const isBot = makeBotMatcher(botUserAgents);
56
+ const powEngine = pow ? createPow({ difficulty: powDifficulty, secret: powSecret }) : null;
57
+
58
+ const shouldCharge = (req) => {
59
+ if (typeof free === "function" && free(req)) return false;
60
+ if (typeof charge === "function") return Boolean(charge(req));
61
+ return isBot(req.headers["user-agent"] || "");
62
+ };
63
+ const resourceOf = (req) =>
64
+ (resourceBaseUrl ? resourceBaseUrl.replace(/\/$/, "") : "") + (req.originalUrl || req.url || "/");
65
+
66
+ function tollbooth(req, res, next) {
67
+ if (!shouldCharge(req)) return next();
68
+ const resource = resourceOf(req);
69
+
70
+ const send402 = (extra = {}) => {
71
+ const body = {
72
+ error: "Payment Required",
73
+ message,
74
+ accepts: payTo
75
+ ? [{ scheme: "exact", network, maxAmountRequired: String(price), asset, payTo, resource }]
76
+ : [],
77
+ ...extra,
78
+ };
79
+ if (powEngine) body.proofOfWork = powEngine.challenge(resource);
80
+ res.status(402).json(body);
81
+ };
82
+
83
+ // Free rail: proof-of-work.
84
+ const powHeader = req.headers["x-pow-solution"];
85
+ if (powEngine && powHeader) {
86
+ const r = powEngine.verify(powHeader, resource);
87
+ if (r.ok) { res.setHeader("X-Tollbooth-Paid", "pow"); return next(); }
88
+ res.setHeader("X-Pow-Error", r.reason);
89
+ }
90
+
91
+ // Paid rail: x402 (USDC). Settlement verification is operator-supplied so
92
+ // we reuse the standard, audited x402 stack rather than reinvent it.
93
+ const payHeader = req.headers["x-payment"] || req.headers["payment-signature"];
94
+ if (payTo && typeof verifyX402 === "function" && payHeader) {
95
+ return Promise.resolve(verifyX402(req, { price, network, asset, payTo, resource }))
96
+ .then((ok) => {
97
+ if (ok) { res.setHeader("X-Tollbooth-Paid", "x402"); return next(); }
98
+ send402();
99
+ })
100
+ .catch(() => { res.setHeader("X-Tollbooth-Error", "x402-verify-failed"); send402(); });
101
+ }
102
+
103
+ return send402();
104
+ }
105
+
106
+ tollbooth.shouldCharge = shouldCharge;
107
+ tollbooth.pow = powEngine;
108
+ return tollbooth;
109
+ }
110
+
111
+ /** Minimal reverse proxy: forward a request to `upstream`, streaming-safe-ish. */
112
+ export function createProxy(upstream) {
113
+ return async (req, res) => {
114
+ try {
115
+ const target = new URL(req.originalUrl || req.url, upstream);
116
+ const headers = {};
117
+ for (const [k, v] of Object.entries(req.headers)) {
118
+ const lk = k.toLowerCase();
119
+ if (lk === "host" || lk === "connection" || lk === "content-length") continue;
120
+ headers[k] = v;
121
+ }
122
+ const method = req.method;
123
+ const body = method === "GET" || method === "HEAD" ? undefined : await readBody(req);
124
+ const up = await fetch(target, { method, headers, body, redirect: "manual" });
125
+ res.status(up.status);
126
+ up.headers.forEach((val, key) => {
127
+ const lk = key.toLowerCase();
128
+ // fetch already decoded the body; drop hop-by-hop / length/encoding headers.
129
+ if (["content-encoding", "content-length", "transfer-encoding", "connection"].includes(lk)) return;
130
+ res.setHeader(key, val);
131
+ });
132
+ res.send(Buffer.from(await up.arrayBuffer()));
133
+ } catch (e) {
134
+ res.status(502).json({ error: `tollbooth proxy failed: ${e.message}` });
135
+ }
136
+ };
137
+ }
138
+
139
+ function readBody(req, cap = 10 * 1024 * 1024) {
140
+ return new Promise((resolve, reject) => {
141
+ const chunks = [];
142
+ let n = 0;
143
+ req.on("data", (c) => {
144
+ n += c.length;
145
+ if (n > cap) { reject(new Error("request body too large")); req.destroy(); return; }
146
+ chunks.push(c);
147
+ });
148
+ req.on("end", () => resolve(Buffer.concat(chunks)));
149
+ req.on("error", reject);
150
+ });
151
+ }
152
+
153
+ async function startCli() {
154
+ const upstream = process.env.TOLLBOOTH_UPSTREAM;
155
+ const port = Number(process.env.PORT) || 4021;
156
+ const { default: express } = await import("express");
157
+ const app = express();
158
+ const gate = createTollbooth({ resourceBaseUrl: process.env.TOLLBOOTH_RESOURCE_BASE || upstream || "" });
159
+ app.use(gate);
160
+ if (upstream) {
161
+ app.use(createProxy(upstream));
162
+ } else {
163
+ app.use((_req, res) => res.json({ ok: true, note: "Bare tollbooth gate (no TOLLBOOTH_UPSTREAM set). Clients that reach here paid or solved a proof-of-work." }));
164
+ }
165
+ app.listen(port, () => {
166
+ const rails = `${gate.pow ? "proof-of-work" : ""}${process.env.TOLLBOOTH_PAYTO ? (gate.pow ? " + x402(USDC)" : "x402(USDC)") : ""}`;
167
+ console.log(`agent402-tollbooth listening on :${port} — charging AI bots via ${rails || "proof-of-work"}`);
168
+ if (upstream) console.log(` proxying → ${upstream}`);
169
+ });
170
+ }
171
+
172
+ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) startCli();
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "agent402-tollbooth",
3
+ "version": "0.1.0",
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
+ "type": "module",
6
+ "main": "index.js",
7
+ "bin": {
8
+ "agent402-tollbooth": "index.js"
9
+ },
10
+ "exports": {
11
+ ".": "./index.js"
12
+ },
13
+ "files": [
14
+ "index.js",
15
+ "bots.js",
16
+ "pow.js",
17
+ "example.js",
18
+ "README.md"
19
+ ],
20
+ "engines": {
21
+ "node": ">=20"
22
+ },
23
+ "scripts": {
24
+ "test": "node test.js",
25
+ "example": "node example.js"
26
+ },
27
+ "dependencies": {
28
+ "express": "^5.0.0"
29
+ },
30
+ "keywords": [
31
+ "x402",
32
+ "pay-per-crawl",
33
+ "ai-bots",
34
+ "ai-crawler",
35
+ "bot-monetization",
36
+ "http-402",
37
+ "402-payment-required",
38
+ "proof-of-work",
39
+ "usdc",
40
+ "reverse-proxy",
41
+ "express-middleware",
42
+ "self-hosted",
43
+ "mcp",
44
+ "ai-agents"
45
+ ],
46
+ "author": "Mikey Petrillo (https://github.com/MikeyPetrillo)",
47
+ "license": "MIT",
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "https://github.com/MikeyPetrillo/Agent402.git",
51
+ "directory": "tollbooth"
52
+ },
53
+ "homepage": "https://github.com/MikeyPetrillo/Agent402/tree/main/tollbooth"
54
+ }
package/pow.js ADDED
@@ -0,0 +1,78 @@
1
+ // Self-contained proof-of-work for the tollbooth: a no-wallet payment rail.
2
+ // A client without USDC proves it spent real CPU instead — the same scheme
3
+ // Agent402 uses, packaged standalone so the tollbooth has zero crypto deps.
4
+ //
5
+ // Tokens are HMAC-signed by this process (stateless to issue), bound to the
6
+ // exact resource they were minted for, expiry-checked, and single-use (a solved
7
+ // token is remembered until it expires). Solving costs the caller CPU; issuing
8
+ // and verifying cost us one hash + one HMAC.
9
+ import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
10
+
11
+ const TTL_MS = 5 * 60 * 1000;
12
+
13
+ /** Count leading zero bits of a buffer (the proof-of-work difficulty metric). */
14
+ export function leadingZeroBits(buf) {
15
+ let bits = 0;
16
+ for (const b of buf) {
17
+ if (b === 0) { bits += 8; continue; }
18
+ bits += Math.clz32(b) - 24;
19
+ break;
20
+ }
21
+ return bits;
22
+ }
23
+
24
+ export function createPow({
25
+ secret = process.env.TOLLBOOTH_SECRET || randomBytes(32).toString("hex"),
26
+ difficulty = Number(process.env.TOLLBOOTH_POW_BITS) || 18,
27
+ ttlMs = TTL_MS,
28
+ } = {}) {
29
+ const used = new Map(); // token -> expiry(ms)
30
+ const sweep = setInterval(() => {
31
+ const now = Date.now();
32
+ for (const [t, exp] of used) if (exp < now) used.delete(t);
33
+ }, 60_000);
34
+ sweep.unref?.();
35
+
36
+ const sign = (payload) => createHmac("sha256", secret).update(payload).digest("base64url");
37
+
38
+ /** Mint a signed, resource-scoped challenge. */
39
+ function challenge(resource) {
40
+ const chal = randomBytes(16).toString("hex");
41
+ const exp = Date.now() + ttlMs;
42
+ const payload = `${chal}.${exp}.${difficulty}.${resource}`;
43
+ const token = `${payload}.${sign(payload)}`;
44
+ return {
45
+ algorithm: "sha256",
46
+ challenge: chal,
47
+ difficulty,
48
+ expires: exp,
49
+ token,
50
+ rule: `Find an integer nonce so sha256("${chal}:" + nonce) has >= ${difficulty} leading zero bits, then resend the request with header X-Pow-Solution: ${token}:<nonce>`,
51
+ };
52
+ }
53
+
54
+ /** Verify a "<token>:<nonce>" solution for a given resource. */
55
+ function verify(headerValue, resource) {
56
+ if (!headerValue || typeof headerValue !== "string") return { ok: false, reason: "missing solution" };
57
+ const cut = headerValue.lastIndexOf(":");
58
+ if (cut < 0) return { ok: false, reason: "malformed solution" };
59
+ const token = headerValue.slice(0, cut);
60
+ const nonce = headerValue.slice(cut + 1);
61
+ const parts = token.split(".");
62
+ if (parts.length !== 5) return { ok: false, reason: "malformed token" };
63
+ const [chal, expStr, diffStr, res, sig] = parts;
64
+ const expected = sign(`${chal}.${expStr}.${diffStr}.${res}`);
65
+ const a = Buffer.from(sig);
66
+ const b = Buffer.from(expected);
67
+ if (a.length !== b.length || !timingSafeEqual(a, b)) return { ok: false, reason: "bad signature" };
68
+ if (res !== resource) return { ok: false, reason: "wrong resource" };
69
+ if (Date.now() > Number(expStr)) return { ok: false, reason: "expired" };
70
+ if (used.has(token)) return { ok: false, reason: "already used" };
71
+ const hash = createHash("sha256").update(`${chal}:${nonce}`).digest();
72
+ if (leadingZeroBits(hash) < Number(diffStr)) return { ok: false, reason: "insufficient work" };
73
+ used.set(token, Number(expStr));
74
+ return { ok: true };
75
+ }
76
+
77
+ return { challenge, verify, difficulty };
78
+ }