Skip to content

Ads: autobid by default, paper-money auction, bid history - #253

Merged
ralyodio merged 1 commit into
masterfrom
worktree-ads-autobid
Sep 13, 2026
Merged

ralyodio merged 1 commit into
masterfrom
worktree-ads-autobid

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

What

Every campaign on crawlproof.com is self-deal, so every fill books free tier and the free tier weighted every campaign at 1. Nobody bid. This turns bidding on for all 314 campaigns with paper money, so the auction, the pacing and the charge path run end to end with real numbers and no real funds.

  • Autobid on by default. New autobid column, true for every existing and new campaign. lib/ads/autobid.ts is a pure pacing controller: behind daily pace raises the bid, ahead lowers it, capped so the budget buys at least five clicks, held when already twice the market median. The worker runs it hourly and at boot (runAutobidSweep), so every campaign has a seed bid within a minute of deploy.
  • Paper auction. The free tier now runs the same bid-weighted lottery as the paid tier, weighted by bid and gated by a paper daily budget. Each free-tier click records what it would have cost via ad_paper_charge, which refuses any click that billed. Impressions record the winning bid. No credit balance is ever debited; the paid tier and ad_charge_click are untouched.
  • Bid history. ad_bids logs every decision with the signals behind it. The campaign page gets a bid vs. clicks chart (impressions as bars, tracker-attributed visits when the destination runs the tracker) and a decision log. The API's stats.bid carries the same; crawlproof ads show prints it.
  • UI. The create form has no bid field. The edit form shows an Autobid switch with the current bid and reason; a typed bid appears only with the switch off. Campaign list shows the live bid.
  • API/CLI. PATCH { autobid }; a bare bid_credits turns autobid off. crawlproof ads bid <ref> auto|<credits>.

Schema

supabase/migrations/20260913120000_ad_autobid.sql is already applied to prod (ywcizjsgrcmhgyplldac). All columns are additive with defaults. ad_paper_charge, ad_autobid_activity and ad_campaign_bid_history were each exercised in a rolled-back transaction; the ledger is at zero.

Checks

  • tsc --noEmit clean, vitest run 174 files / 2279 tests green, next build exit 0.
  • New tests: tests/ads-autobid.test.ts (controller rules, paper weights, history carry-forward, sweep writes) and tests/ads-autobid-requests.test.ts.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PDJmt1CPi1fAkpD4FXRSHu

Every campaign on the network is self-deal, so serveAd demoted every fill to
the free tier and the free tier weighted everything at 1: nobody bid, and the
auction, pacing and charge path had never run end to end with real numbers.

- `autobid` on ad_campaigns, default true for every row. A pure pacing
  controller (lib/ads/autobid.ts) recomputes each bid hourly from the worker:
  behind daily pace raises it, ahead lowers it, capped by what the daily
  budget covers, held when already well above the market. The bid field is
  gone from the create form; the edit form shows an Autobid switch and the
  current bid, with a typed bid only when the switch is off.
- The free tier is a paper auction: the same bid-weighted lottery, weighted by
  the paper bid and gated by a paper daily budget. Every free-tier click
  records what it would have cost (ad_clicks.paper_cents, campaign paper
  counters) through a new ad_paper_charge RPC that refuses any billed click.
  No credit balance is ever debited; the paid tier is untouched.
- ad_bids logs every decision with its signals. The campaign page charts bid
  against impressions, clicks and tracker-attributed visits, with the last
  decisions and their reasons underneath. GET /api/ads/v1/campaigns/[id]
  carries the same under stats.bid; PATCH takes `autobid`; a bare bid_credits
  turns autobid off. CLI: `crawlproof ads bid <ref> auto|<credits>`.

Migration 20260913120000_ad_autobid.sql is applied to prod (ref
ywcizjsgrcmhgyplldac) and every function was exercised in a rolled-back
transaction. The worker seeds every campaign's first bid at boot.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PDJmt1CPi1fAkpD4FXRSHu
@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.

@ralyodio
ralyodio merged commit 782cca8 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.

1 participant