Skip to content

Tell people which controls spend money before they press them - #29

Merged
ThinkingSpade merged 1 commit into
mainfrom
feat/disclose-paid-controls
Jul 31, 2026
Merged

Tell people which controls spend money before they press them#29
ThinkingSpade merged 1 commit into
mainfrom
feat/disclose-paid-controls

Conversation

@ThinkingSpade

Copy link
Copy Markdown
Owner

This app is careful about not spending without a click — useMeteredQuery
disables mount/focus/reconnect refetching so a restored page can't fire a paid
request. This closes the other half: six controls that do spend and didn't
say so.

Look up · up to 1 paid request
Run · 4 paid requests
Build brief · up to 4 paid requests
Fetch reviews · 1 paid request
Update keyword stats · uses credits
Analyze <domain> · may use credits      + "If not cached: est. $0.85."

One helper, not six suffixes

The wording encodes two facts that are easy to get wrong independently.

How much. A count of upstream requests where that's knowable, a measured
dollar figure where one exists, otherwise just "uses credits". Never an invented
number — shared/analysis-costs.ts already says why: showing a guessed price
immediately before spending someone's money is worse than showing none.
The
brand estimate reuses that file's constants and its markup rule rather than
hardcoding a figure.

Whether it might cost nothing. Most of these read a server cache first, so a
second press inside the window spends zero. Saying "1 paid request" there would
be a lie in the expensive-sounding direction — which teaches people to distrust
the labels that are accurate. Hence "up to" / "may", and hence the brand price
sitting beside the button as a conditional rather than on it as a promise.

Counts were verified against the actual fan-out, not assumed: Prompt Explorer
dedupes models before spending (new Set(...)), Local SEO caches a successful
profile for 12h, the brief caches 7 days over a 6h SERP read.

Two things deliberately unchanged

  • Small rank checks stay as they are. The header already shows a per-check
    estimate, and the <50-keyword threshold deliberately avoids modal fatigue.
  • No new blocking confirmations. The app reserves those for 50+ keyword
    checks and batch analysis. These six are deliberate submits, so an inline label
    is proportionate — a modal in front of a cheap, cached click trains people to
    dismiss modals.

Both came from the Codex audit, which correctly refuted one of its own
earlier items once it read RankTrackingDetailHeader.

A caught mistake worth recording

knip passed while meteredEstimateNote was exported, tested, and rendered
nowhere — the test's import satisfied the unused-export check. A green gate
was hiding the fact that the price never reached a user. It's rendered now, and
browser-verified showing If not cached: est. $0.85.

Verification

Browser-verified: four of the six labels render correctly (Local SEO, Prompt
Explorer, Content brief, Brand analyze). Fetch reviews and Update keyword
stats
need a business profile and rank data respectively.

7 new tests, 2,129 passing, pnpm ci:check clean.

🤖 Generated with Claude Code

This app is careful about not spending WITHOUT a click -- `useMeteredQuery`
disables mount/focus/reconnect refetching so a restored page cannot fire a
paid request. This closes the other half: six controls that DO spend and
did not say so.

    Look up · up to 1 paid request
    Run · 4 paid requests
    Build brief · up to 4 paid requests
    Fetch reviews · 1 paid request
    Update keyword stats · uses credits
    Analyze <domain> · may use credits     (+ "If not cached: est. $0.85.")

One `meteredActionLabel` helper rather than six hand-written suffixes,
because the wording encodes two facts that are easy to get wrong
independently.

**How much.** A count of upstream requests where that is knowable, a
MEASURED dollar figure where one exists, otherwise just "uses credits".
Never an invented number -- `shared/analysis-costs.ts` already says why:
showing a guessed price immediately before spending someone's money is
worse than showing none. The brand estimate reuses that file's constants
and its markup rule rather than hardcoding the figure Codex quoted.

**Whether it might cost nothing.** Most of these read a server cache first,
so a second press inside the window spends zero. Saying "1 paid request"
there would be a lie in the expensive-sounding direction, which teaches
people to distrust the labels that are accurate. Hence "up to" / "may", and
hence the brand price sitting beside the button as a conditional rather
than on it as a promise.

Counts were verified against the fan-out, not assumed: Prompt Explorer
dedupes models before spending (`new Set(...)`), Local SEO caches a
successful profile for 12h, the brief caches for 7 days over a 6h SERP
read.

Two things NOT changed, on Codex's recommendation and after checking:

* Small rank checks stay as they are. The header already shows a per-check
  estimate, and the <50-keyword threshold deliberately avoids modal fatigue.
* No new blocking confirmations. The app reserves those for 50+ keyword
  checks and batch analysis; these six are deliberate submits, so an inline
  label is proportionate. A modal in front of a cheap cached click trains
  people to dismiss modals.

A caught mistake worth recording: knip passed while `meteredEstimateNote`
was exported, tested, and rendered NOWHERE -- the test import satisfied the
unused-export check. A green gate was hiding the fact that the price never
reached a user. It is rendered now, and browser-verified showing
"If not cached: est. $0.85."

Browser-verified: four of the six labels render correctly. Fetch reviews
and Update keyword stats need a business profile and rank data respectively.

7 new tests, 2,129 passing, ci:check clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
flyrocketseo 3576e7e Jul 31 2026, 06:45 AM

@ThinkingSpade
ThinkingSpade merged commit 61b09e8 into main Jul 31, 2026
3 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