Skip to content

feat(leads): charge for lead generation, then sell it - #151

Merged
ralyodio merged 1 commit into
masterfrom
feat/lead-gen-billing-and-upsell
Jul 28, 2026
Merged

ralyodio merged 1 commit into
masterfrom
feat/lead-gen-billing-and-upsell

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

The thing that was actually broken

LEAD_RUN_CREDITS was defined and never read:

$ rg LEAD_RUN_CREDITS
lib/credits.ts:36:export const LEAD_RUN_CREDITS = 3;

One hit. The declaration. Every campaign tick and every manual search has been free, and the expensive part is not the model — it's the search calls behind contact lookup. The one-shot finder was the wider hole: it budgeted up to 100 paid lookups per click and charged nothing.

So the meter goes in before the sales page, not after it.

Charging a tick that doesn't know yet what it will do

Charged once, lazily, at the first stage that actually reaches for search or a model.

  • Up-front billing would cost 288 credits/day to leave an idle campaign switched on (one tick per 15 min).
  • The cron can't know what a tick will do until it looks — a prospect whose scan hasn't landed costs nothing to skip, so the gate sits after that check, not before the loop.
  • Charged but produced nothing → refunded. Some of it is genuinely spent by then; billing for a run with no output is the worse trade.
  • Out of credits → the campaign stops and says so in the run history, the only place a stalled campaign gets to explain itself.

The manual finder is priced by size

Per hundred leads asked for, and the search budget scales with it:

Asked for Credits Paid lookups
≤100 3 10
1,000 30 100

Sizing the budget by the request instead is exactly what let a 1,000-lead run buy 100 lookups for the price of 10.

The size picker now goes to 1,000 to match — it was still offering 5/10/25 against an action that already accepted 1k, so a directory page reported a fraction of itself and looked broken.

Pure pricing moved to lib/credits.ts beside the other prices rather than next to the deduction, so the form can show the cost before the click without pulling a database client into the browser bundle.

Then the selling

  • /lead-generation — the feature page
  • A card on the home module grid (ModuleCard grew an optional href; cards without one stay non-clickable rather than eight of nine looking live and doing nothing)
  • Footer link
  • A "what else credits buy" section on /pricing, which until now priced only scans

Two claims the page deliberately doesn't make: open tracking and automatic reply detection. Neither is built.

The guard reads the source

A unit test of manualRunPrice would have passed for the entire time the price was unreferenced. The bug was that nobody called it — so four tests assert the call sites exist.

Checks

  • tsc --noEmit clean · 966 tests pass (17 new) · build compiles, /lead-generation renders
  • Migration credits_spent on outreach_campaign_runs applied to prod

Heads-up: two active campaigns on the account start drawing down a 1,760-credit balance once this lands.

🤖 Generated with Claude Code

LEAD_RUN_CREDITS was defined and never read. Every campaign tick and every
manual search has been running on the house, and the expensive part is not
the model — it is the search calls behind contact lookup. The one-shot
finder was the wider hole: it budgeted up to a hundred paid lookups per
click and charged nothing for any of them.

So the meter goes in before the sales page, not after it.

A tick is charged once, lazily, at the first stage that actually reaches
for search or a model. Charging up front would bill 288 credits a day to
leave an idle campaign switched on, and the cron cannot know what a tick
will do until it looks — a prospect whose scan has not landed costs
nothing to skip. A run that is charged and produces nothing gives the
money back; some of it is genuinely spent by then, but billing for a run
with no output is the worse trade. Running out pauses the campaign and
says so in the run history, which is the only place a stalled campaign
gets to explain itself.

The manual finder is priced per hundred leads asked for, and the search
budget it buys scales with it. Sizing that budget by the request instead
would let a thousand-lead run buy a hundred lookups for the price of ten.
The size picker goes up to a thousand to match — it was still offering
5/10/25 against an action that already accepted 1k, so a directory page
reported a fraction of itself and looked broken.

The pure pricing lives in lib/credits.ts beside the other prices rather
than next to the deduction, so the form can show the cost before the click
without pulling a database client into the browser bundle.

Then the selling: a /lead-generation page, a card on the home grid, a
footer link, and a section on pricing for what credits buy besides scans.
Two things the page deliberately does not claim are open tracking and
automatic reply detection, because neither is built.

The regression guard reads the source. A unit test of the pricing function
would have passed for the entire time the price was unreferenced — the bug
was that nobody called it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

vu1nz Security Review

0 finding(s) in PR #?

No security issues found.

@ralyodio
ralyodio merged commit 479a955 into master Jul 28, 2026
8 checks passed
@ralyodio
ralyodio deleted the feat/lead-gen-billing-and-upsell branch July 28, 2026 11:12
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