Skip to content

docs: outline the pricing V3 paywall move before building it - #29

Open
itsdylon wants to merge 3 commits into
mainfrom
docs/pricing-v3-outline
Open

docs: outline the pricing V3 paywall move before building it#29
itsdylon wants to merge 3 commits into
mainfrom
docs/pricing-v3-outline

Conversation

@itsdylon

Copy link
Copy Markdown
Owner

Outline only — no behaviour changes. Requested before implementation so the trade is explicit.

The measured picture

Fact Value
Users 14, all free
Median session 227 pages
Sessions ≤ 250 pages 39 of 76 (51%)
Sessions ≤ 500 pages 73 of 76 (96%)
Deep Match today $0.10/page for first 500 → median job ≈ $22.70

Two things the outline flags

1. The cap becomes a cost control, not a revenue lever. Once export is the paywall, the page cap only decides how much free compute you give away — so it should be set by worker capacity. But migration_sessions has no started_at/completed_at, so Deep Match wall-clock (the real cost driver — embeddings are ~$0.005 per 250-page job) cannot currently be measured. Any specific cap number is a guess until that exists.

Separately, 250 sits almost exactly on the median of 227, so half of all sessions land within ~10% of the line. Fragile place to draw a boundary unless "about half free" is the intent.

2. Jobs would run before payment, which inverts the worker's risk profile. Today requires_payment_unlock gates the run. Under V3 free jobs execute first and payment happens at export. At WORKER_MAX_CONCURRENT=2, a burst of free jobs delays paying customers, and there is no per-user ceiling on free compute. Queue priority and a free-run ceiling need to ship with the paywall move, not after it.

Watch is cheaper than it looks

gsc_traffic_baselines (migration 026) already snapshots each project's full traffic distribution at ingestion for every tier, and the Resend email infrastructure already exists. Watch mostly needs a scheduler, a baseline diff, and threshold logic.

Five open decisions

Cap number; export gated per-project or per-subscription; free-run ceiling; whether the existing $349/mo Agency plan survives alongside a $29/mo Watch; and sequencing.

🤖 Generated with Claude Code

Dylon Shattuck and others added 3 commits August 19, 2026 19:20
Free Deep Match under a page cap, paywall moves to export, Watch
subscription alongside. Grounded in measured usage rather than
assumption: median session is 227 pages and 51% of sessions are under
250, so the proposed cap lands almost exactly on the median.

Flags two things that have to ship with the change rather than after it.
Once the run is free, unpaid jobs execute before payment, so free work
can starve paying work at max_concurrent=2 and there is no per-user
ceiling on free compute.

Also notes that migration_sessions has no started_at/completed_at, so
Deep Match wall-clock — the actual cost driver — cannot currently be
measured, which makes any specific cap number a guess.
Co-Authored-By: Paperclip <noreply@paperclip.ing>

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 3 advisory finding(s) below merit a look before merge.

Formal verification. No changes could be formally verified in this run.


Graphify review — findings

Adds job-timing and queue-priority instrumentation ahead of the pricing V3 paywall move. Migration 027 adds started_at, completed_at, and a priority column (default 0, so ordering is unchanged until something sets it) plus partial indexes for claim order and duration reporting, and rebuilds claim_next_job to order by priority DESC, created_at ASC and stamp started_at/clear completed_at per attempt. In worker.py, release_lease now stamps completed_at only for terminal statuses in the new TERMINAL_JOB_STATUSES set (retries back to pending stay unstamped), and the REST claim fallback mirrors the RPC by ordering on priority and setting the timing fields itself.

Worth a look

  • Fallback claim uses non-atomic select-then-updatebackend/worker.py:399 · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Lease release is not scoped to the owning workerbackend/worker.py:475 · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Hot-table indexes are built with blocking CREATE INDEXdatabase/migrations/027_add_job_timing_and_priority.sql:35 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 100 functions depend on the 97 functions this change touches.

Health — this change adds coupling hotspots:

  • new: RedirxWorker — 16 callers, 9 callees

Verification — 100 functions in the blast radius were not formally verified this run (proofs are advisory here).

Health delta baseline: last indexed commit 23fff0b (diverged from this PR's base — delta is approximate).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 100 function(s) in the blast radius were not formally verified this run

Formal verification

Could not verify: Could not verify \_claim\_job\_fallback.

The verifier did not have enough to check \_claim\_job\_fallback, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: all 200 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly __init__ raised on the synthesized receiver args — names the real obstacle, not a sampling gap)

Could not verify: Could not verify release\_lease.

The verifier did not have enough to check release\_lease, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: all 33 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly __init__ raised on the synthesized receiver args — names the real obstacle, not a sampling gap)

· 1 more finding(s) on lines outside this diff (see the check run).

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