Skip to content

Trending-topic targeting, and 90 days free with it - #248

Merged
ralyodio merged 1 commit into
masterfrom
feat/samebrain-trend-targeting
Sep 12, 2026
Merged

ralyodio merged 1 commit into
masterfrom
feat/samebrain-trend-targeting

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Run ads where the subject is what people are asking about right now, and give an advertiser who turns it on ninety days during which every click is billed at $0.00.

The signals come from SameBrain on chovy.com — what founders are asking to build this week. That endpoint did not exist; it is built in profullstack/chovy-ai#9 and this branch is the puller.

Targeting

Two halves have to agree before a campaign is preferred: its own subject has to be trending and the page being filled has to be about that subject. Either half alone is how an ad network ends up putting a crypto ad on a recipe blog — one is "this is in the news", the other is ordinary contextual targeting.

The page's subject is not new work: the autoblog already keeps a hand-checked subject list per site (lx_site.master_keywords), and that, the niche and the slot's niche are what get read.

It is a preference, not a rule. A match carries 4× its own bid weight into the same bid-weighted lottery, so it wins more fills and every other eligible campaign still rotates — the auction's hard requirement, tested.

The promo, and why it books as free

A campaign inside its ninety days competes for real placement (that is what premium delivery means; parked in the backfill pool it would never serve at all next to a paying advertiser) and books as tier='free', because nothing is charged.

So no spend and no publisher earnings are written for a payment that is not happening. That matters most here, where one account owns both sides of nearly every fill: a paid booking would read as revenue on the ROI dashboard. It is not revenue — it is a discount we gave ourselves.

Billing at zero is settled in resolveClick, not inside ad_charge_click: that function is the hottest SQL in the product and its migrations are applied by hand, so teaching it about promos would put a new failure mode in front of every click on the network.

Degrading

  • A trend list older than 36 hours stops steering delivery instead of targeting last week forever.
  • A failed pull leaves the stored list; ingestion failure is not an outage.
  • A missing table or column reads as "nobody is trending" — which is exactly how serving behaved yesterday. Covered by a test, because the migration is applied by hand and a deploy can lead it.
  • On a network where nobody has opted in, this costs one cached query a minute and nothing per fill.

Surfaces

  • GET /api/ads/v1/trends (bearer token) · POST /api/cron/ad-trends (cron secret, hourly)
  • POST /api/ads/v1/campaigns { trending_topics, topics }, PATCH the same
  • crawlproof ads trends, ads create --trending --topics=, ads trending <ref> on|off; promo days left on ads show and ads list
  • Dashboard: a checkbox on the new-campaign form, and the days remaining on the campaign page

$0.02

Stated once in lib/ads/pricing.ts and quoted from there by the dashboard, the CLI and the promo row. Known gap, recorded in that file: $0.02 is smaller than one credit (5¢) and nothing bills a fraction of a credit today, so after a promo ends a trending campaign bills at its ordinary bid until sub-credit metering exists.

Migration

supabase/migrations/20260911120000_ad_trend_targeting.sql is added, not applied. Apply it one file at a time via the Supabase MCP. Nothing in it backfills a row that serving reads, so before or after the deploy are both safe.

Secrets to set

SAMEBRAIN_URL and SAMEBRAIN_SECRET on the Railway service and in the vault (crawlproof-com--prod), the secret matching chovy.com's.

Tests

vitest run2174 passed, 7 skipped, 168 files, 29 of them new (topic matching, the promo window, billing at zero, and a serving contract test for delivery preference and promo tiering). tsc --noEmit clean. npm run lint fails on master too: it runs next lint, which Next 16 removed, and the repo has no eslint config or binary.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QuUkTrofSjQ15j79mRuy4f

@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

39 finding(s)

HIGH/CRITICAL: 2 | MEDIUM: 28 | LOW: 9

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 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
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 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.

Targeting on what chovy.com's SameBrain says founders are trying to build this
week, and the 90 days free that comes with turning it on.

Two halves have to agree before a campaign is preferred: its own subject has to
be trending AND the page being filled has to be about that subject. Either half
alone is how an ad network ends up putting a crypto ad on a recipe blog — one
is "this is in the news", the other is ordinary contextual targeting. The
page's subject is not new work: the autoblog already keeps a hand-checked
subject list per site (lx_site.master_keywords) and that is what is read.

It is a preference, not a rule. A match carries four times its own bid weight
into the same bid-weighted lottery, so it wins more fills and every other
eligible campaign still rotates — the auction's hard requirement.

The promo separates two questions that look like one. A campaign inside its
ninety days COMPETES for real placement (that is what premium delivery means;
parked in the backfill pool it would never serve at all next to a paying
advertiser) and BOOKS as tier='free', because nothing is charged. So no spend
and no publisher earnings are written for a payment that is not happening —
which matters most here, where one account owns both sides of nearly every fill
and a paid booking would read as revenue on the ROI dashboard. It is not
revenue. It is a discount we gave ourselves.

Billing at zero is settled in resolveClick rather than inside ad_charge_click:
that function is the hottest SQL in the product and its migrations are applied
by hand, so teaching it about promos would put a new failure mode in front of
every click on the network.

Degrading is the design everywhere it touches. A trend list older than 36 hours
stops steering delivery instead of targeting last week forever; a failed pull
leaves the stored list; a missing table or column — the migration is applied by
hand, so a deploy can lead it — reads as "nobody is trending", which is exactly
how serving behaved yesterday. On a network where nobody has opted in this
costs one cached query a minute and nothing per fill.

$0.02 CPC is stated once, in lib/ads/pricing.ts, and quoted from there by the
dashboard, the CLI and the promo row. Known gap recorded there: it is smaller
than one credit (5c), and nothing bills a fraction of a credit today, so after
a promo ends a trending campaign bills at its ordinary bid until sub-credit
metering exists.

The migration is added, not applied — apply it one file at a time.

SAMEBRAIN_URL and SAMEBRAIN_SECRET go in the vault and on the deployment, never
in a committed .env file.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QuUkTrofSjQ15j79mRuy4f
@ralyodio
ralyodio marked this pull request as ready for review September 12, 2026 11:31
@ralyodio
ralyodio force-pushed the feat/samebrain-trend-targeting branch from 9eabba0 to 75aa221 Compare September 12, 2026 11:31
@ralyodio
ralyodio merged commit d29bbff into master Sep 12, 2026
7 of 8 checks passed
@ralyodio
ralyodio deleted the feat/samebrain-trend-targeting branch September 12, 2026 11:31
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