Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"@profullstack/profile": "0.1.6",
"@profullstack/rssamplifier": "workspace:*",
"@profullstack/submit-feed": "^0.1.3",
"@profullstack/x402-gateway": "^0.13.1",
"@profullstack/x402-gateway": "^0.16.1",
"@rssamplifier/auth": "workspace:*",
"@rssamplifier/db": "workspace:*",
"@rssamplifier/feed": "workspace:*",
Expand Down
10 changes: 10 additions & 0 deletions apps/web/src/lib/crawl-gateway.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,16 @@ export const gateway = createGateway({
* charged like GPTBot: 402 and the offer, a hash instead of a render.
*/
chargeSpoofedBrowsers: true,
/*
* gateway 0.16 gives a suspect string 20 pages an hour per address before
* the 402. Not here, not everywhere: the rotation above never sent more
* than two requests from one address, so a per-address allowance would hand
* it the whole directory. Only the front door gets it, so a person or an
* agent fetching the homepage once reads it (2026-10-28: a one-shot fetch
* of the homepage got 402) while /topics, /authors and the API stay charged
* on sight. '/' alone is the homepage, not a prefix.
*/
suspectAllowance: { requests: 20, windowSeconds: 3600, paths: ['/', '/about', '/contact', '/advertise', '/submit', '/terms', '/privacy'] },
exempt,
/*
* What a pass actually buys, printed on the sales page and in every 402.
Expand Down
11 changes: 11 additions & 0 deletions apps/web/test/crawl-gateway.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,17 @@ test('a Chrome string that cannot answer like Chrome is charged', async () => {
assert.equal((await gate(request('/authors/somebody', UAS.rotation[2])))?.status, 402);
});

test('a one-shot fetch of the front door reads it; the directory stays charged on sight', async () => {
// 2026-10-28: an agent fetching the homepage once, no cookies, no JS, got
// 402. The front door gets gateway 0.16's per-address allowance; /topics,
// /authors and the API do not, or the rotation above walks back in.
for (const path of ['/', '/about', '/submit']) {
assert.equal(await gate(request(path, UAS.rotation[0], { 'x-forwarded-for': '203.0.113.50' })), undefined, path);
}
assert.equal((await gate(request('/topics/x', UAS.rotation[0], { 'x-forwarded-for': '203.0.113.50' })))?.status, 402);
assert.equal((await gate(request('/', UAS.gptbot)))?.status, 402);
});

test('the same string with the header a browser sends passes', async () => {
for (const ua of UAS.rotation) {
assert.equal(await gate(request('/topics/x', ua, { 'sec-fetch-mode': 'navigate' })), undefined, ua);
Expand Down
4 changes: 2 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading