tollgate-sdk
Drop-in AI bot paywall for Express, Next.js, Fastify, and Cloudflare Workers. Provide your Solana wallet address — USDC payments land there directly. No API key, no signup, no custodian.
Overview
Tollgate intercepts HTTP requests at the middleware layer. When it detects an AI bot, it returns HTTP 402 with an x402 payment envelope — the bot's wallet address, price in USDC, and a signed challenge token. Human visitors pass through with zero overhead.
On retry with a valid X-PAYMENT header, the SDK verifies the on-chain USDC transfer via Solana RPC and unlocks content. Replay protection is enforced via Supabase.
Installation
npm install tollgate-sdkZero Solana dependencies on your server — the SDK handles all on-chain verification through the hosted facilitator.
Supabase setup (required)
Run the schema in your Supabase SQL editor once. This creates the payments table and replay-protection cache.
create table if not exists public.payments (
id bigserial primary key,
tx text not null unique,
wallet_address text,
bot_name text,
path text,
lamports bigint,
timestamp timestamptz not null default now()
);
create table if not exists public.verified_tx_cache (
tx text primary key,
cached_at timestamptz not null default now()
);# .env
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_SERVICE_ROLE_KEY=your-service-role-keyQuick Start
The only required config is your Solana wallet address. Everything else has a sensible default.
Express
import express from "express";
import { createPaywall } from "tollgate-sdk";
import { expressMiddleware } from "tollgate-sdk/express";
const paywall = createPaywall({
walletAddress: process.env.SOLANA_WALLET_ADDRESS,
network: "mainnet-beta",
protect: ["/*"], // gate all routes
basePriceMicroUsdc: 1_000, // $0.001 per crawl
});
const app = express();
app.use(expressMiddleware(paywall));
// req.paywallPayment is set when a bot paid successfully
app.get("/articles/:slug", (req, res) => {
res.json({
content: "Your article...",
payment: req.paywallPayment ?? null,
});
});Next.js (App Router)
Use paywallMiddleware in middleware.ts to gate routes at the edge, before any route handler runs.
// middleware.ts
import { createPaywall } from "tollgate-sdk";
import { paywallMiddleware } from "tollgate-sdk/nextjs";
const paywall = createPaywall({
walletAddress: process.env.SOLANA_WALLET_ADDRESS!,
basePriceMicroUsdc: 1_000,
protect: ["/articles/*", "/blog/*"],
});
export default paywallMiddleware(paywall);
export const config = { matcher: ["/articles/:path*", "/blog/:path*"] };For App Router route handlers, use withRouteHandler:
// app/articles/[slug]/route.ts
import { withRouteHandler } from "tollgate-sdk/nextjs";
import { paywall } from "@/lib/paywall"; // shared instance
export const GET = withRouteHandler(paywall, async (req) =>
Response.json({ content: "Your article..." })
);Fastify
import Fastify from "fastify";
import { createPaywall } from "tollgate-sdk";
import { fastifyPlugin } from "tollgate-sdk/fastify";
const paywall = createPaywall({
walletAddress: process.env.SOLANA_WALLET_ADDRESS,
basePriceMicroUsdc: 1_000,
});
const app = Fastify();
await app.register(fastifyPlugin, { paywall });
app.get("/articles/:slug", async (req, reply) => {
return { content: "Your article..." };
});Cloudflare Workers
import { createPaywall } from "tollgate-sdk";
import { cloudflareHandler } from "tollgate-sdk/cloudflare";
export default {
async fetch(request, env) {
const paywall = createPaywall({
walletAddress: env.SOLANA_WALLET_ADDRESS,
basePriceMicroUsdc: 1_000,
});
return cloudflareHandler(paywall, request, async () =>
new Response(JSON.stringify({ content: "Your article..." }), {
headers: { "Content-Type": "application/json" },
})
);
},
};Configuration
Pass these options to createPaywall({ ... }).
| Option | Default | Description |
|---|---|---|
walletAddress | required | Your Solana wallet address. USDC payments land at its ATA. |
network | "devnet" | Solana network — "devnet" or "mainnet-beta". |
protect | ["/*"] | Path globs or RegExp patterns to gate. Use ["/*"] to protect all routes. |
basePriceMicroUsdc | 1000 | Price per crawl in micro-USDC. 1000 = $0.001. 1_000_000 = $1.00. |
botScoreThreshold | 70 | Composite score threshold for bot classification. Lower = stricter. |
allowList | [] | UA patterns that always pass as humans, e.g. [{pattern: /Googlebot/i}]. |
failOpen | false | If true, allow bots through when the facilitator is unreachable. |
onDetection(d) | — | Hook called with the bot detection result on every classified request. |
apiUrl | hosted | Override the facilitator URL for self-hosting. |
timeoutMs | 8000 | Network timeout for facilitator calls in milliseconds. |
Path matching
The protect option accepts strings or RegExp. String patterns support * suffix wildcards.
protect: ["/*"] // all routes
protect: ["/articles/*"] // prefix match
protect: ["/blog/*", "/docs/*"] // multiple prefixes
protect: [/^/api//] // RegExpUsing req.paywallPayment
After a verified payment, Express and Fastify attach the payment receipt to the request object.
app.get("/article", (req, res) => {
console.log(req.paywallPayment);
// {
// signature: "3jK9xZ...", // Solana tx signature
// payer: "AgentWallet...",
// received: 1000 // micro-USDC received
// }
});Bot Detection
Detection runs entirely in-process — no network call, zero overhead for human visitors. A composite score across four signals determines whether a request is classified as a bot.
| Signal | Examples | Score |
|---|---|---|
| User-Agent match | GPTBot, ClaudeBot, PerplexityBot, CCBot, Scrapy, python-requests | 55–90 pts |
| Missing browser headers | accept-language, sec-fetch-site, sec-ch-ua absent | 12 pts each |
| No Accept: text/html | Scripts rarely request HTML | 15 pts |
| Datacenter IP | AWS, GCP, Azure, Cloudflare CIDR ranges | 30 pts |
| Reverse DNS verified | Real Googlebot/ClaudeBot resolve to known hostnames | Labelled, not blocked |
Score ≥ 70 → bot (gated). Score 40–69 → suspicious (passed through by default). Score < 40 → human. Tune the threshold with botScoreThreshold.
Using the onDetection hook
const paywall = createPaywall({
walletAddress: process.env.SOLANA_WALLET_ADDRESS,
protect: ["/*"],
onDetection: (d) => {
console.log({
isBot: d.isBot,
botName: d.botName, // "GPTBot", "ClaudeBot", etc.
score: d.score, // composite score
signals: d.signals, // ["ua:GPTBot(90)", "headers:suspicious(36)", ...]
ip: d.ip,
});
},
});Dynamic Pricing
The base price is multiplied by signals from the bot type and content being requested. The final price is returned in the 402 envelope so agents can decide whether to pay before committing.
final_price = base × bot_multiplier × content_affinity × exclusivity_mod| Bot / crawler | Multiplier | Rationale |
|---|---|---|
| CCBot (Common Crawl) | 2.8× | Training data — highest commercial value |
| GPTBot, ClaudeBot | 2.5× | LLM training / inference |
| MetaAI, CohereBot | 2.6–2.7× | Training data |
| PerplexityBot | 2.0× | Answer engine |
| Googlebot, Bingbot | 1.0× | Search index — don't over-price |
| Unknown | 1.5× | Conservative default |
Content type is detected from path patterns (/blog/ = prose,/data/ = dataset) and body signals (code blocks, tables). The 402 response body includes the full score breakdown so agents can inspect it.
Dashboard & Auth
The dashboard shows payments received, top bots, and top paid pages — scoped to your wallet. Authentication uses Sign-In With Solana: you sign a server-issued message, no password required.
Step 1 — Request a nonce
curl -X POST https://ai-paywall-production-f453.up.railway.app/v1/auth/nonce \
-H "Content-Type: application/json" \
-d '{"walletAddress": "YourSolanaWallet..."}'
# Response:
# {
# "token": "<opaque-token>",
# "message": "ai-paywall.dev wants you to sign in...",
# "expiresAt": "2026-05-12T12:05:00.000Z"
# }Step 2 — Sign the message
import nacl from "tweetnacl";
import bs58 from "bs58";
// message = the exact string from Step 1
const messageBytes = new TextEncoder().encode(message);
const signature = nacl.sign.detached(messageBytes, keypair.secretKey);
const signatureBase58 = bs58.encode(signature);Step 3 — Exchange for a session (valid 24h)
curl -X POST https://ai-paywall-production-f453.up.railway.app/v1/auth/verify \
-H "Content-Type: application/json" \
-d '{
"walletAddress": "YourSolanaWallet...",
"message": "<exact message from Step 1>",
"signature": "<base58 signature>",
"token": "<token from Step 1>"
}'
# Response: { "session": "<token>", "expiresAt": "..." }Step 4 — Fetch analytics
curl https://ai-paywall-production-f453.up.railway.app/v1/dashboard \
-H "Authorization: Bearer <session-token>"
# Response:
# {
# "wallet": { "address": "YourWallet..." },
# "total": 42,
# "total_lamports": 56000,
# "payments": [
# { "tx": "3jK9...", "botName": "GPTBot", "path": "/article", "lamports": 1000, "timestamp": "..." },
# ...
# ]
# }Check your treasury ATA
# Verify where payments will land before going live
curl "https://ai-paywall-production-f453.up.railway.app/v1/wallet/treasury?walletAddress=YourWallet&network=mainnet-beta"
# Response:
# {
# "walletAddress": "YourWallet...",
# "network": "mainnet-beta",
# "usdcMint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
# "treasuryAta": "7xKpT..."
# }Environment Variables
# Required
SOLANA_WALLET_ADDRESS=YourSolanaWallet...
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
# Recommended
SOLANA_NETWORK=mainnet-beta
SOLANA_RPC_URL=https://mainnet.helius-rpc.com/?api-key=...
# Auth secrets — generate fresh: openssl rand -hex 32
PAYWALL_CHALLENGE_SECRET=<32-byte-hex>
PAYWALL_AUTH_SECRET=<32-byte-hex>
PAYWALL_AUTH_DOMAIN=yourdomain.com
# Optional — enables auto-creation of USDC ATAs for new publisher wallets
FACILITATOR_FEE_PAYER_SECRET_KEY=[...keypair-json-array...].env to source control. Rotate PAYWALL_CHALLENGE_SECRET and PAYWALL_AUTH_SECRET before going to production.