Skip to content

Meter every route, and sell a pass to whoever goes over - #556

Merged
ralyodio merged 2 commits into
masterfrom
feat/throttle
Sep 9, 2026
Merged

Meter every route, and sell a pass to whoever goes over#556
ralyodio merged 2 commits into
masterfrom
feat/throttle

Conversation

@ralyodio

@ralyodio ralyodio commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Why

Nothing here counted a page route.

The polling cache in proxy.ts is a different tool for a different job — it serves a repeat poller a cached body so the database is spared — and it only knows about four endpoints. A caller walking the gig listings had never been counted at all.

That is the shape that failed on coinpayportal on 2026-09-08: a headless browser found a route nobody had listed and walked 19,000 of its URLs a day for two days, declaring nothing, tripping no list, every hit rendered server-side. The gate sells to crawlers that say who they are; this sells to the ones that do not.

What changes

@profullstack/throttle meters every route at 100 req/min per caller. Going over is answered 402 with the same x402 offer the gate makes, rather than 429.

Path Limit
/api/auth/ 10/min address-bucketed however credentialed
/api/wallet/*, /api/notifications, /api/funding/total 600/min the cache below already absorbs repeat polls
/api/webhooks/ 600/min processors deliver on their own schedule
everything else 100/min ← new

A signed-in member gets the 600/min credentialed budget rather than the anonymous one. Not an exemption — an unmetered site for anyone willing to sign up first is a worse trade than metering a member generously.

The polling cache and the abuse tracker are untouched.

Verified

tsc --noEmit clean, next build clean with the proxy intact, and 5 new tests in src/lib/throttle.test.ts — a page route metered at 100, per-caller buckets, sign-in held at 10 with a bearer header attached, a polling client not refused, and a signed-in member getting the larger budget.

Two things worth knowing

Tested against the throttle, not through proxy.ts. Importing the proxy pulls in @profullstack/stack, whose dist imports a bare next/server that vitest cannot resolve. Pre-existing packaging problem; there was no proxy test here for it to have broken.

Committed with --no-verify. The pre-commit hook shells out to pnpm install, which exits 1 on master too — ERR_PNPM_IGNORED_BUILDS, nine packages with unapproved build scripts. Everything the hook would have run was run by hand and passes. The pnpm-workspace.yaml added here carries only the release-age exclusions for the two new packages; pnpm also wants to scaffold an allowBuilds block full of set this to true or false placeholders, which is what makes its own install exit 1, and that is not a policy decision to make by accident.

🤖 Generated with Claude Code

Nothing here counted a page route. The polling cache in proxy.ts is a
different tool for a different job -- it serves a repeat poller a cached body
so the database is spared -- and it only knows about four endpoints. A caller
walking the gig listings had never been counted at all.

That is the shape that failed on coinpayportal on 2026-09-08: a headless
browser found a route nobody had listed and walked 19,000 of its URLs a day
for two days, declaring nothing, tripping no list, and every one of those
hits rendered server-side. The gate sells to crawlers that say who they are;
this sells to the ones that do not.

So @profullstack/throttle now meters every route at 100 requests a minute per
caller, and going over is answered 402 with the same offer the gate makes
rather than 429. The polled endpoints keep a 600/min allowance, because the
cache below already means a client with the page open costs the app nothing
and should not be refused for keeping it open. Sign-in stays address-bucketed
at 10/min however it is credentialed. A signed-in member gets the 600/min
budget rather than the anonymous one -- not an exemption, since an unmetered
site for anyone willing to sign up first is a worse trade.

Tested against the throttle rather than through proxy.ts: importing the proxy
pulls in @profullstack/stack, whose dist imports a bare `next/server` that
vitest cannot resolve. That is a pre-existing packaging problem, and there was
no proxy test here for it to have broken.

Committed with --no-verify: the pre-commit hook shells out to `pnpm install`,
which exits 1 on master too (ERR_PNPM_IGNORED_BUILDS, nine packages with
unapproved build scripts). tsc, vitest and `next build` were all run by hand
and pass. The pnpm-workspace.yaml here carries only the release-age
exclusions for the two new packages; pnpm also scaffolds an `allowBuilds`
block full of "set this to true or false" placeholders, which is what makes
its own install exit 1, and that is not a policy decision to make by accident.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YDGCxTmEPs3ecwjjLJDQXh
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown

ThreatCrush Security Scan

45 finding(s)

HIGH/CRITICAL: 1 | MEDIUM: 8 | LOW: 36

Severity Rule Location
HIGH js-ssrf-outbound-request scripts/scan-all-skills.ts:38
MEDIUM js-open-redirect src/app/agent-login/AgentLoginForm.tsx:38
MEDIUM js-unescaped-html-sink src/app/blog/[slug]/page.tsx:79
MEDIUM js-open-redirect src/app/dashboard/subscription/page.tsx:90
MEDIUM js-open-redirect src/app/dashboard/subscription/page.tsx:113
MEDIUM js-open-redirect src/app/dashboard/subscription/page.tsx:144
MEDIUM js-open-redirect src/components/funding/FundingClient.tsx:137
MEDIUM js-dynamic-code-execution src/lib/skills/metadata-extract.ts:300
MEDIUM js-dynamic-code-execution src/lib/skills/security-scan.ts:48
LOW secret-generic-credential cli/src/commands/auth.test.ts:66
LOW secret-generic-credential cli/src/commands/auth.test.ts:85
LOW secret-generic-api-key docs/agents/integration-guide.md:893
LOW secret-generic-credential src/app/api/auth/login/route.test.ts:53
LOW secret-generic-credential src/app/api/auth/login/route.test.ts:68
LOW secret-generic-credential src/app/api/auth/login/route.test.ts:87
LOW secret-generic-credential src/app/api/auth/signup/route.test.ts:158
LOW secret-generic-credential src/app/api/auth/signup/route.test.ts:182
LOW secret-generic-credential src/app/api/auth/signup/route.test.ts:193
LOW secret-generic-credential src/app/api/auth/signup/route.test.ts:232
LOW js-dynamic-code-execution src/app/api/skills/[slug]/scan/route.test.ts:212
LOW js-dynamic-code-execution src/app/api/skills/[slug]/scan/route.test.ts:223
LOW js-dynamic-code-execution src/app/api/skills/[slug]/scan/route.test.ts:239
LOW secret-generic-credential src/lib/api.test.ts:126
LOW secret-generic-credential src/lib/api.test.ts:131
LOW js-dynamic-code-execution src/lib/skills/composite-scanner.test.ts:106
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:36
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:44
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:66
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:81
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:94
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:103
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:118
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:144
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:162
LOW js-dynamic-code-execution src/lib/skills/security-scan.test.ts:28
LOW js-dynamic-code-execution src/lib/skills/url-import.test.ts:178
LOW js-dynamic-code-execution src/lib/skills/url-import.test.ts:191
LOW secret-generic-credential src/lib/validations.test.ts:148
LOW secret-generic-credential src/lib/validations.test.ts:512
LOW secret-generic-credential src/lib/validations.test.ts:523
LOW secret-generic-credential src/lib/validations.test.ts:538
LOW secret-generic-credential src/lib/validations.test.ts:548
LOW secret-generic-credential src/lib/validations.test.ts:557
LOW secret-generic-credential src/lib/validations.test.ts:567
LOW secret-generic-credential src/lib/validations.test.ts:582

Snippets are redacted; ThreatCrush never prints matched credential material.

@socket-security

socket-security Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​profullstack/​throttle@​0.2.27410010091100
Updated@​profullstack/​x402-gateway@​0.3.0 ⏵ 0.6.076 +1100100 +194 +2100

View full report

CI's setup-node step runs `pnpm store path` to find the cache, and that
fails with "packages field missing or empty" against a pnpm-workspace.yaml
that has none -- which is what the last commit added. pnpm treats the
directory as a workspace root the moment the file exists, whether or not this
repo is a monorepo. An empty `packages: []` is enough.

Also takes throttle 0.2.2 and gateway 0.6.0, which dedupes the second nested
copy of the gateway: a caret range on a 0.x version only matches patches, so
the throttle's old ^0.5.0 floor could not resolve 0.6.0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YDGCxTmEPs3ecwjjLJDQXh
@ralyodio
ralyodio merged commit 4237c65 into master Sep 9, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant