Skip to content

Commit 88c77f4

Browse files
ralyodioclaude
andauthored
Charge AI training crawlers for access (@profullstack/x402-gateway) (#234)
Training crawlers (GPTBot, ClaudeBot, CCBot, meta-externalagent, Bytespider, Applebot-Extended) get 402 Payment Required with an x402 offer, or the sales page at /crawl, and a paid pass opens the site for a day. People, search engines and retrieval crawlers pass through untouched. robots.txt is now generated from the same lists. Claude-Session: https://claude.ai/code/session_01YafYxayh7Gqe5MWNNQMev2 Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 7fd8fde commit 88c77f4

7 files changed

Lines changed: 60 additions & 19 deletions

File tree

‎.env.example‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,9 @@ GITHUB_APP_SLUG=
114114
# Playwright owner-flow e2e. Email defaults to anthony+riotcoder@profullstack.com.
115115
PLAYWRIGHT_AUTH_EMAIL=
116116
PLAYWRIGHT_AUTH_PASSWORD=
117+
118+
# Crawl gateway (@profullstack/x402-gateway): AI training crawlers pay $1/day over
119+
# x402. A SCOPED CoinPay key (payments:create) and the EVM address that receives
120+
# the USDC. Unset = crawlers still get 402, nothing sold.
121+
COINPAY_X402_KEY=
122+
CRAWL_PAY_TO=

‎app/robots.ts‎

Lines changed: 0 additions & 19 deletions
This file was deleted.

‎app/robots.txt/route.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { robotsRoute } from "@profullstack/x402-gateway/next";
2+
import { gateway } from "@/lib/crawl-gateway";
3+
4+
// Generated from the same crawler lists the gateway enforces: training
5+
// crawlers are refused everywhere but /crawl (where they can buy a pass),
6+
// retrieval crawlers are named as welcome, everyone else gets the rules below.
7+
export const GET = robotsRoute(gateway, {
8+
disallow: ["/api/"],
9+
});

‎lib/crawl-gateway.ts‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { createGateway } from "@profullstack/x402-gateway";
2+
import { x402Proxy } from "@profullstack/x402-gateway/next";
3+
4+
/**
5+
* Sells crawl access to AI training crawlers (GPTBot, ClaudeBot, CCBot,
6+
* meta-externalagent, Bytespider, Applebot-Extended, ...) by the day over
7+
* x402, settled by CoinPay in USDC. People, Googlebot and the retrieval
8+
* crawlers behind AI search pass through untouched.
9+
*
10+
* Runs inside the middleware, so nothing here may import Node-only modules.
11+
* The env is read through a non-literal key on purpose: Next inlines
12+
* `process.env.NAME` at build time, and these are runtime secrets. Without
13+
* COINPAY_X402_KEY and CRAWL_PAY_TO the gateway still answers training
14+
* crawlers with 402, just with an empty offer.
15+
*/
16+
const env = (name: string) => process.env[name];
17+
18+
export const gateway = createGateway({
19+
siteUrl: env("SITE_URL") || env("NEXT_PUBLIC_SITE_URL") || "https://crawlproof.com",
20+
siteName: "CrawlProof",
21+
coinpay: { apiKey: env("COINPAY_X402_KEY") },
22+
payTo: env("CRAWL_PAY_TO"),
23+
contact: "mailto:support@crawlproof.com",
24+
});
25+
26+
/** Resolves to a Response for a refused crawler, or undefined to carry on. */
27+
export const gate = x402Proxy(gateway);

‎package-lock.json‎

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"@profullstack/referrals": "^0.1.0",
3030
"@profullstack/stack": "^0.1.3",
3131
"@profullstack/x402-client": "^0.2.0",
32+
"@profullstack/x402-gateway": "^0.1.0",
3233
"@supabase/ssr": "^0.10.3",
3334
"@supabase/supabase-js": "^2.105.4",
3435
"@types/nodemailer": "^8.0.0",

‎proxy.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
1+
import { gate } from "@/lib/crawl-gateway";
12
import { NextResponse, type NextRequest } from "next/server";
23
import { createServerClient, type CookieOptions } from "@supabase/ssr";
34
import { trackReferralCode } from "@profullstack/stack/referrals";
45

56
type Cookie = { name: string; value: string; options?: CookieOptions };
67

78
export async function proxy(request: NextRequest) {
9+
// Crawl gateway first: AI training crawlers get 402 Payment Required (or the
10+
// sales page at /crawl) unless they present a paid pass. People, Googlebot
11+
// and retrieval crawlers fall through to everything below.
12+
const answer = await gate(request);
13+
if (answer) return answer;
14+
815
// 308 redirect www.crawlproof.com -> crawlproof.com (preserves method + body).
916
const host = request.headers.get("host") ?? "";
1017
if (host.toLowerCase().startsWith("www.")) {

0 commit comments

Comments
 (0)