Skip to content

Fix ad reporting and throttle crawlers through x402 gateway - #259

Merged
ralyodio merged 3 commits into
masterfrom
fix/ads-reporting-crawler-access
Sep 13, 2026
Merged

ralyodio merged 3 commits into
masterfrom
fix/ads-reporting-crawler-access

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

The Ads TUI showed billed clicks as all accepted clicks and mislabeled free clicks as invalid. API-token reports lacked auth.uid(), then fell back to slow lifetime views that could return incomplete HTTP-200 responses and zero domain rows.

This replaces that fallback with an owner-scoped, windowed reporting RPC over rollups and live records. Billed, free and rejected clicks remain separate; failed reads return retryable 503 without fabricated totals. The dashboard includes domain traffic/CoinPay attribution, potential scoring, loading/retry feedback and hqtui 0.6.2, and uses matching periods for targets and paid CPC.

Commercial crawlers, including Semrush, use the existing x402-gateway $1/day pass. Redis enforces 12 requests/minute/IP and 600/hour/crawler family even for paid passes, ahead of ad queries. Browser ad requests have a 60/minute/IP ceiling. Robots excludes ad redirects; authorized crawler redirects never write or charge ad clicks. Administrators can inspect separate aggregate crawler counters.

Validation: 2,366 unit/contract tests passed (9 optional integrations skipped), root typecheck and production webpack build passed. Disposable PostgreSQL tests cover owner isolation, execution permissions and click/date semantics; Redis tests cover concurrent admission, retry expiry and IP rotation. Those integration checks are now in CI. A rolled-back production validation exactly matched raw seven-day records; the new query took 132–394 ms for 7/30/365-day reports.

Deployment requires 20260913170000_ad_token_earnings.sql first. It has been applied to production with service-role-only execution. No historical click records were changed. See docs/crawler-access.md for policy and operations details.

@socket-security

socket-security Bot commented Sep 13, 2026

Copy link
Copy Markdown

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

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updated@​profullstack/​hqtui@​0.5.0 ⏵ 0.6.284 +1100100 +196 +1100

View full report

Comment thread lib/crawl-limits.ts
try {
const client = connection();
if (!client) throw new Error("unavailable");
const retryMs = Number(await client.eval(REQUEST_LIMIT_LUA, keys.length, ...keys, ...args));
redis = new Redis(`redis://${mapping}`, { maxRetriesPerRequest: 1, connectTimeout: 2000 });
const args = ['semrushbot:ad', 3600, 12, 60_000, 600, 3_600_000];
const keys = ['test:ip', 'test:family', 'test:metrics'];
const claim = () => redis.eval(REQUEST_LIMIT_LUA, keys.length, ...keys, ...args);
await claim();
assert.ok(await redis.pttl('test:ip') <= before, 'rejects must not extend the deadline');
await redis.set('test:family', '600', 'PX', 3_600_000);
const acrossIps = await redis.eval(REQUEST_LIMIT_LUA, 3, 'test:other-ip', 'test:family', 'test:metrics', ...args);
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

48 finding(s)

HIGH/CRITICAL: 2 | MEDIUM: 31 | LOW: 15

Severity Rule Location
HIGH tls-verification-disabled lib/onion.ts:48
HIGH secret-generic-credential lib/sp/platforms/facebook.ts:32
MEDIUM js-unescaped-html-sink app/(app)/dashboard/admin/email-broadcast/EmailBroadcastForm.tsx:125
MEDIUM js-unescaped-html-sink app/(app)/dashboard/projects/[id]/autoblog/articles/[articleId]/page.tsx:214
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:67
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:97
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:104
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:110
MEDIUM js-unescaped-html-sink app/(marketing)/recent/page.tsx:186
MEDIUM js-unescaped-html-sink app/(marketing)/recent/page.tsx:190
MEDIUM js-unescaped-html-sink app/c/[project]/[slug]/page.tsx:77
MEDIUM js-unescaped-html-sink app/c/[project]/page.tsx:57
MEDIUM js-unescaped-html-sink app/careers.js/route.ts:228
MEDIUM js-unescaped-html-sink app/careers.js/route.ts:285
MEDIUM js-unescaped-html-sink app/layout.tsx:129
MEDIUM js-open-redirect app/login/form.tsx:39
MEDIUM js-unescaped-html-sink app/r/[token]/page.tsx:176
MEDIUM js-open-redirect app/signup/form.tsx:43
MEDIUM js-open-redirect components/billing/buy-credits-modal.tsx:98
MEDIUM js-unescaped-html-sink components/json-ld.tsx:8
MEDIUM js-unescaped-html-sink components/report/markdown-view.tsx:15
MEDIUM js-unescaped-html-sink lib/careers/page-templates.ts:198
MEDIUM js-dynamic-code-execution lib/crawl-limits.ts:67
MEDIUM redos-nested-quantifier lib/emailMarkdown.ts:41
MEDIUM redos-nested-quantifier lib/emailMarkdown.ts:324
MEDIUM redos-nested-quantifier lib/lx/articleGen.ts:99
MEDIUM redos-nested-quantifier lib/tracker/agent-gate.ts:61
MEDIUM sh-remote-script-execution prober/deploy/provision.sh:30
MEDIUM sql-template-interpolation scripts/detect-slot-themes.ts:31
MEDIUM sql-template-interpolation scripts/purge-constructed-keywords.ts:163
MEDIUM sql-template-interpolation scripts/purge-offniche-keywords.ts:124
MEDIUM js-dynamic-code-execution scripts/test-crawl-limits.mjs:14
MEDIUM js-dynamic-code-execution scripts/test-crawl-limits.mjs:24
LOW secret-generic-credential app/(marketing)/docs/autoblog-webhook/page.tsx:145
LOW secret-generic-credential lib/sp/platforms/linkedin.ts:25
LOW js-dynamic-code-execution tests/careers-page-templates.test.ts:21
LOW js-dynamic-code-execution tests/careers-widget-script.test.ts:19
LOW js-dynamic-code-execution tests/careers-widget-script.test.ts:69
LOW js-dynamic-code-execution tests/contract/ad-visitor-id.test.ts:51
LOW js-dynamic-code-execution tests/contract/ad-visitor-id.test.ts:52
LOW js-dynamic-code-execution tests/contract/ads-click-cooldown-redis.test.ts:20
LOW js-dynamic-code-execution tests/contract/ads-click-cooldown-redis.test.ts:24
LOW js-dynamic-code-execution tests/contract/ads-click-cooldown-redis.test.ts:25
LOW js-dynamic-code-execution tests/contract/ads-click-cooldown-redis.test.ts:26
LOW js-dynamic-code-execution tests/contract/ads-click-cooldown-redis.test.ts:31
LOW js-dynamic-code-execution tests/contract/ads-click-cooldown-redis.test.ts:35
LOW secret-generic-credential tests/contract/posthog-integration.test.ts:13
LOW secret-generic-credential tests/lead-campaign.test.ts:16

Snippets are redacted; ThreatCrush never prints matched credential material.

expect(model.campaigns[0]).toMatchObject({ clicks: 14, impressions: 110, spentCents: 40 });
expect(model.slots[0]).toMatchObject({ clicks: 14, impressions: 110, earnedCents: 20 });
expect(model.daily).toHaveLength(7);
for (const [table, field, value] of filters.filter(([, field]) => field === "owner_id")) expect(value, table).toBe("owner");
@ralyodio
ralyodio merged commit 4b4a73f into master Sep 13, 2026
10 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.

2 participants