Skip to content

ads: refuse a click whose impression is too old for its audience - #264

Merged
ralyodio merged 1 commit into
masterfrom
worktree-ads-stale-impression-clicks
Sep 22, 2026
Merged

ralyodio merged 1 commit into
masterfrom
worktree-ads-stale-impression-clicks

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Why

/dashboard/ads?range=4h read 101% CTR on 2026-09-22. Nobody is gaming the network for money (charged and earned are both 0); it is the same distributed Singapore headless-Chrome crawler documented on coinpayportal's explorer, now crawling rssamplifier.com.

It fetches rssamplifier's feed documents with a plain Chrome user agent (so the server-side impression books as device=desktop), harvests the /a/<code> link out of the injected feed_item, and requests it a median of 54 hours later from a different rotating address. Every existing check passes: the impression exists, campaign and slot match, device is not a bot, the IP hash never repeats inside the 6h dedupe window. ad_charge_click takes the self-deal branch and writes it as a free-tier click, which the dashboard counts as delivery.

The ratio goes past 100% because the click lands in the 4h window while its impression was minted two days earlier, and ~90% of those impressions are duplicate=true, which the impressions denominator excludes while the numerator keeps the click.

Real money never moved, but the paper auction was being steered by it: 43,185 paper cents in one 4h window.

The evidence that picks the ceilings

30 days of ad_clicks joined to their impression, device <> 'bot':

cohort clicks median delay past 6h past 24h same IP as impression
browser impression, real visitor id 90 15 s 0 0 83
browser impression, any format, non-SG no visitor id ~40 < 3 min p90 0 0
terminal impression, non-SG 225 34 9
terminal impression, SG 629 626 625 0
feed-reader impression, non-SG 316 210 160
feed document fetched by a browser, SG 37,128 54 h 37,074 0

What changes

assessClickValidity now also refuses a cited impression that is older than its audience could plausibly still be looking at, keyed on the impression's device:

  • browser (desktop/mobile/tablet/unknown): 6h, the existing click window. A browser has nothing holding the item once the page is gone.
  • terminal: 24h. The MOTD is read at the next login.
  • feed: no ceiling. A reader keeps an unread item for as long as the subscriber leaves it.

Refused clicks get reason stale_impression and land in the existing invalid bucket (valid=false, tier='paid') that ad_slot_totals.invalid_clicks already reports, so they stay visible without counting as delivery or charging paper.

Effect on live data (30 days)

Removes 37,074 of 37,128 Singapore feed clicks and 625 of 629 Singapore terminal clicks. Costs 0 real browser clicks, 34 non-SG terminal clicks past 24h (most of which sit past 7 days and look like the same class), and no feed-reader clicks.

Tests

tests/contract/ads-click-validity.test.ts covers the crawler shape (browser impression, 54h later), the boundary just past 6h, a terminal at 12h and 2 days, a feed reader at 9 days, and null/garbage timestamps. Full suite: 194 files, 2,420 tests pass; tsc --noEmit clean.

No migration. Deploy is enough.

🤖 Generated with Claude Code

A distributed headless-browser crawler fetches rssamplifier.com's feed
documents with a plain Chrome user agent, harvests the /a/<code> link out
of the injected feed_item, and requests it a median of 54 hours later from
a different address. Every other check passed: the impression exists, the
campaign matches, the address never repeats inside the dedupe window. It
booked as free-tier delivery at ~1,100 clicks every four hours, and
/dashboard/ads?range=4h read a 101% CTR because those impressions sat two
days outside the window and were mostly flagged duplicate. Nothing billed,
but the paper auction was being steered by it.

Click validity now also refuses a cited impression that is older than its
audience could plausibly still be looking at: 6h when it was served to a
browser (real browser clicks over 30 days: p90 2.7 min, none past 6h),
24h for a terminal (non-crawler terminal clicks: 96% within a day), and no
ceiling for a feed reader, which keeps an unread item for as long as the
subscriber leaves it. Refused clicks land in the existing invalid bucket
that ad_slot_totals already reports, so they stay visible without counting
as delivery.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@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.

@ralyodio
ralyodio merged commit eb89a23 into master Sep 22, 2026
10 checks passed
ralyodio added a commit that referenced this pull request Sep 28, 2026
…#329)

#264 marked a click that trails its impression past the device ceiling as
invalid, but still wrote a row and still 302'd to the advertiser. That is the
reward the feed-link harvester keeps coming back for: it fetches feeds with a
Chrome UA, keeps the /a/<code> links, and replays them from fresh IPs days to
weeks later. Last 7 days on prod: 13,178 of 13,513 clicks were exactly that,
0 of them valid.

A stale click now gets a 410 with no link, no-store and noindex/nofollow, and
nothing is recorded or charged. Feed-reader impressions keep no ceiling, so a
subscriber opening an old item is untouched; no browser click in 30 days has
trailed its impression past 6h. Every other invalid click (duplicate,
cooldown, forged) is still recorded and redirected as before.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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