Reposition the product. Gate the door. Ship the panel.
A full rebuild of the public site and the auth/billing surface around it. Reframes browser-recon from "scraper tool" to "AI agent for scraping reconnaissance." Adds a waitlist gate, tier-based credits, and an admin panel that grants access and curates the marketing surface.
Status
DRAFT
Version
v1
Identity
Terminal · ultraviolet
Stripe
deferred → v2
Target
2026-06-15
verdict
Ship a single redesign that does four things at once: re-frames the product as an AI agent (not a scraper), gates new signups behind a waitlist with an optional tweet jump-line, replaces magic-link auto-create with ACTIVE-only login, and introduces four paid tiers with monthly-expiring credits and tier-gated report retention.
V1 is admin-granted credits — no card processing during beta. Stripe checkout is wired in V2 once the first cohort has shaken out the pricing surface. The admin panel ships with waitlist queue management, tweet template CRUD, and a live theme accent switcher stored in app_config.
01 — rationale
Three things are wrong with the current surface.
The product has matured faster than the public framing around it. The site sells a 2025 scraper-tool; the product behaves like a 2026 agent. Closing that gap unlocks pricing power, audience clarity, and a clean place to put the waitlist.
// problem 01
Anybody can sign up
Magic-link auto-creates a user. No vetting, no demand-shaping, no way to batch onboarding. We get drive-by users who never come back, and no story for "scarcity."
// problem 02
The product is mis-framed
The current copy reads as a scraping toolkit. The actual value is an autonomous reconnaissance pass over a target site. Same product, but the words don't match what users get.
// problem 03
No paid path
No tiers, no credit packs, no retention model. Every report is kept forever or thrown away — there's no shape to monetise against. Free users cost us blob storage forever.
The redesign fixes all three with a single shipped artefact: a new landing + pricing + user-guide trio, a waitlist gate in front of the magic-link flow, four monthly tiers with tier-gated retention, and an admin panel to operate the queue, the templates, and the theme.
02 — positioning
What we say. What we don't.
Every line on the public surface is a deliberate choice between marketing pull and operational risk. The table below is the source of truth — copywriters work from this, not from earlier drafts.
Say
Don't say
AI agent for scraping reconnaissance.
Scraper. We're not — our users build them. We're the recon pass before they do.
Measurement-first. Real test requests through real proxies.
"Powered by Claude / OpenAI / Grok." We may swap providers any week; locking into a brand badge is a future-tax.
Tested against Walmart, Staples, Target, Airbnb, Ticketmaster, CoinMarketCap.
Vague volume claims ("thousands of sites scanned"). We have six real ones — use the names.
TLS 1.3 in transit. AWS KMS (AES-256) at rest. Sensitive values scrubbed before storage.
"End-to-end encryption." Materially false — the server has plaintext access during validation. Saying this is legally regulated marketing language.
Runnable starter code. Drop into your stack.
Pipeline diagrams. The orchestrator / sweeper / synthesis flow is our IP. Don't draw it.
Invite-only beta · N spots remaining.
Fake user counts ("Trusted by 10,000+ developers"). Bad risk-reward; trust-precedent loss is permanent.
A two-minute browse is enough.
"Zero-config" / "automatic" — overstates. The user still picks an intent template.
What the agent does: detect anti-bot, validate library × proxy, project cost, generate starter code.
How the agent does it. The chain of LLM calls, the prompts, the scoring rubric — internal only.
One bold line for the hero, one for the deck, the rest of the page does the work. The hero does not claim numbers we can't back up. The "tested against" strip is the credibility load-bearer in V1 — it's all real, all named, all from the existing browser-recon verification set.
03 — site map
Seven public routes. Three gated. One admin.
Everything below /admin is staff-only (existing auth pattern). /dashboard and /r/<slug>/edit require waitlist_status = ACTIVE. Everything else is open.
Dedicated pricing page — same tiers as the landing block but with FAQ + retention timeline + comparison table.
/user-guide
Single long page — install, first scan, what's in a report, what we don't save, security model, FAQ.
/waitlist
Two purposes: post-signup confirmation + the "still on the list" page shown to WAITLIST users who try to log in. Hosts the per-user tweet-submission form.
/login
Magic-link request form. POST checks status and either sends the link, shows the still-waitlisted page, or creates a new WAITLIST row.
/r/<slug>
Public report (existing).
/dashboard
ACTIVE users only — credit balance, recent scans, tier, expiry dates.
/admin/*
Staff-only — waitlist queue, user CRUD, templates, config, KPIs.
04 — waitlist
Two states. One queue. One optional shortcut.
The waitlist is the entire gate. Status is binary: WAITLIST or ACTIVE. Tweet submission is a priority hint to the admin queue, not a separate state. Admins decide who gets in; the tweet just changes the order.
Signup paths
Waitlist form on the landing page or /pricing — email only.
Login attempt with an unknown email — silently creates a WAITLIST row and shows the "you're in line" page.
Tweet submission page (linked from the welcome-to-waitlist email + visible on /waitlist) — captures a tweet URL on the existing WAITLIST row.
Tweet jump-line — operational
The tweet is optional. Forced advocacy gets fewer + worse tweets than voluntary. The admin queue surfaces the tweet URL next to each entry — admin verifies the post manually and approves the user. The DB shape is intentionally flat: one nullable tweet_url column on the user row, no separate "verification" table.
What makes the admin's job fast: the queue page shows the tweet rendered inline (via X's embed/oEmbed) and a [approve] button that flips the status and triggers the welcome email. Single click. No mental tax.
05 — auth flip
Magic link fires for ACTIVE only.
Today, any email gets a magic link and an auto-created user. After this change, the magic link is gated on waitlist_status = ACTIVE. Unknown emails get added to the waitlist silently — the user sees the same "you're in line" page either way.
Existing users
Every user that exists at migration time is set to waitlist_status = ACTIVE. Nobody currently using the product loses access. The migration is one UPDATE statement (see section 11).
Edge cases
Case
Behaviour
WAITLIST user clicks an old magic link
Token validation succeeds → but session creation rejects because status != ACTIVE → redirect to /waitlist.
ACTIVE user removed by admin
Status flipped back to WAITLIST. Existing sessions invalidated. User shown the still-on-list page on next request.
Same email submitted twice on /waitlist
Upsert. Tweet URL updates if newly supplied; created_at doesn't change.
User on /login with unknown email
Silent WAITLIST row + same "in line" page as known WAITLIST. No "user not found" leak.
06 — pricing tiers
Four tiers. One trial. One enterprise call.
Tester is a once-per-lifetime first-month trial — enforced server-side, not on the honour system. Beginner, Pro, and Pro Max are the recurring tiers. Enterprise is a "talk to us" call with a real human; no self-serve quote tool in V1.
User can buy Tester only if users.tester_used_at IS NULL. After purchase, that timestamp is set and never cleared.
2 free scans
Granted on transition WAITLIST → ACTIVE, independent of tier purchase. Land before any paid plan.
Tier change
V1: admin sets users.tier. V2: self-serve. No proration in V1 — admins choose effective date.
Trial → paid
A Tester user upgrading to any paid tier immediately ends the trial; remaining trial credits expire on tier change.
07 — credits + retention
Credits expire. Reports do too — at different speeds.
Credits reset every billing month. No rollover, no banking. Reports stay live for the duration of the user's tier — long enough that re-scanning is rare, short enough that we don't carry blob storage forever.
Credit lifecycle
Granted at start of billing month (or at activation, for the 2 free scans).
Decremented by 1 per scan started. Re-scan from an expired report = same 1 credit.
Expired at end of billing month. The next month's grant overwrites; it does not accumulate.
Report retention by tier
Tester
2 weeks
Beginner
1 month
Pro
2 months
Pro Max
3 months
What "expired" means
Past scans.expires_at, the report page renders a stripped-down placeholder: scan metadata visible (domain, date, intent), report body hidden, and a [Scan again — 1 credit] CTA. Clicking starts a fresh scan from scratch — not a re-render of cached analysis. Costs 1 credit.
The DOWNLOAD report button is commented out in V1. Reasons: keeps users coming back to our domain (analytics + retention), avoids the IP-leak of shipping a portable report file, simplifies the V1 surface. Re-enable later if a paying user asks.
Expiry computation
At scan-complete time: scans.expires_at = scans.completed_at + tier_retention(scans.user_id). The retention is read from the user's tier at completion time and frozen onto the row. If the user later downgrades, existing reports keep their original expiry; only new scans pick up the shorter retention.
08 — v1 billing
Admin grants. Stripe waits.
V1 ships without self-serve checkout. Payments are handled out-of-band; admins flip a tier on the user record and credits land. The Stripe webhook code in the repo stays dormant — no checkout sessions, no price catalog, no plan upgrade flow on the customer side.
Why deferred
// reason 01
Tighter feedback loop
First-cohort pricing will move. Iterating in Stripe (price IDs, upgrade rules, proration) costs an order of magnitude more than iterating in our own admin panel.
// reason 02
Less surface to secure
No PAN data, no PCI footprint, no card-disputes risk in beta. Stripe Checkout is hosted and low-risk, but it still adds a webhook attack surface we don't need until we have buyers.
// reason 03
Manual = personal
Each early-buyer conversation is intel. A Stripe form is silent; an email exchange tells us who they are, what they're scraping, why our pricing felt right or wrong.
V1 collection
Pricing page CTA reads "Join the waitlist" — not "Buy now." A user who reaches out for a paid tier emails us (Enterprise CTA already points to mailto). Admin verifies payment via whichever channel makes sense (invoice, bank transfer, Stripe Payment Link generated ad-hoc), then sets users.tier and grants credits.
V2 outline (next phase)
What V2 adds on top of V1
1. Stripe Checkout sessions, one per tier (price IDs in app_config).
2. Webhook receiver wired to the existing handler stub in browser_recon_server/stripe/.
3. /account/billing page — current tier, next renewal, change tier, cancel.
4. Tester-gate enforcement at checkout (Stripe metadata + server check).
5. Proration: downgrade at end of period; upgrade immediate w/ proration credit.
6. Invoice emails (Stripe-hosted) + receipt PDFs.
7. Dunning: 3 retry attempts, then auto-downgrade to read-only.
09 — user guide
One long page. Nine sections.
The user guide is the single source of truth for "how do I use this thing" and "what does this thing do with my data." Everything a paying user needs to read at least once lives here.
§
Section
What's in it
1
Install
One pipx command. Supported Python (3.11+). What gets installed locally vs server-side.
2
First scan
Walk through recon scan <url>: intent template, browsing, Ctrl+C, what happens next.
Section §4 is the load-bearing section of the user guide for trust. It must be specific. Vague claims ("we strip sensitive data") read as marketing; named fields read as engineering.
Field type
Treatment
Why
Cookie values
Stripped before long-term storage; cookie names kept.
Names are non-secret and useful for scraping recon. Values authenticate.
Authorization header
Removed entirely.
Always a bearer token, always sensitive.
X-Api-Key
Removed entirely.
API keys are credentials.
X-Auth-Token
Removed entirely.
Convention varies, but the name is reserved for auth.
X-CSRF-Token
Removed entirely.
Pairs with a session — both removed.
JWT-shaped strings in bodies
Replaced with <redacted-jwt>.
Regex matches eyJ… three-segment base64.
Bearer-token shaped strings
Replaced with <redacted-token>.
Heuristic match on long opaque alphanumeric blobs in known auth-y contexts.
Session cookies (specific names)
Removed entirely (names + values).
Named matches against the common list: sessionid, JSESSIONID, PHPSESSID, connect.sid, etc.
Security framing — what to write
# /user-guide §5Encryption in transit: All traffic between your machine and our servers
uses TLS 1.3.
Encryption at rest: Captures are stored in AWS S3, encrypted with
AWS KMS (AES-256).
What's scrubbed: Cookie values, Authorization headers, X-Api-Key,
X-Auth-Token, X-CSRF-Token, JWT-shaped strings,
and named session cookies are stripped before
long-term storage. (Full list above.)
What we do see: We need plaintext access during validation
— that's how the agent confirms a library/proxy
combination actually works against the target.
We do NOT claim end-to-end encryption.
Who can read it: browser-recon staff, when a support case
requires it, with audit logging.
Deletion: Captures auto-delete at report expiry. The
report itself is also deleted at expiry.
10 — admin panel
Six routes. One operator.
The admin panel is for one human (us, at first). Optimise for keystroke economy, not enterprise RBAC. Everything below sits under /admin/*, gated on users.is_staff = true.
Route
What it does
Hot actions
/admin/waitlist
Queue of WAITLIST users, newest first. Inline tweet preview when tweet_url present.
approvedeferreject
/admin/users
Filterable user list. Per-user: edit tier, adjust credits, force-expire, deactivate.
grant creditschange tier
/admin/scans
Recent scan activity across all users. Drill into orchestrator logs, view raw blob.
view logsretry
/admin/templates
CRUD on tweet templates. Each row: text body, position, active flag, last-used.
The theme picker shipped in the design D mockup becomes the canonical admin control. Same swatches, same custom hex input. On save, writes theme_accent_hex + theme_accent_name to app_config. The base Jinja template reads these at request time (with a 60s in-memory cache, invalidated on save) and injects --accent as a CSS custom property in :root.
All customer-facing pages (landing, pricing, user guide, waitlist, dashboard, report, login) pick up the change on next page load. The admin panel itself uses a fixed neutral accent so the operator can preview without committing.
The shape is intentionally flat. Status as a column on users (not a separate table). Tweet URL as a nullable column (not a join table). App config as a key/value singleton (not a typed config row). Less to maintain, faster to query.
-- alembic revision T55_v1_redesign --ALTER TABLE users
ADD COLUMN waitlist_status VARCHAR(16)NOT NULL DEFAULT'WAITLIST',
ADD COLUMN activated_at TIMESTAMPTZNULL,
ADD COLUMN tier VARCHAR(16)NULL, -- tester|beginner|pro|pro_max|enterpriseADD COLUMN tester_used_at TIMESTAMPTZNULL,
ADD COLUMN tweet_url TEXTNULL,
ADD COLUMN x_handle VARCHAR(32)NULL,
ADD COLUMN credits INTEGERNOT NULL DEFAULT 0,
ADD COLUMN credits_renew_at TIMESTAMPTZNULL;
CREATE INDEX ix_users_waitlist_status ON users (waitlist_status);
-- grandfather: every existing user is ACTIVE; new signups default to WAITLISTUPDATE users SET waitlist_status = 'ACTIVE', activated_at = NOW() WHERE id IS NOT NULL;
ALTER TABLE scans
ADD COLUMN expires_at TIMESTAMPTZNULL;
CREATE INDEX ix_scans_expires_at ON scans (expires_at) WHERE expires_at IS NOT NULL;
CREATE TABLE tweet_templates (
id UUIDPRIMARY KEY DEFAULT gen_random_uuid(),
body TEXTNOT NULL,
position INTEGERNOT NULL DEFAULT 0,
active BOOLEANNOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZNOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZNOT NULL DEFAULT NOW()
);
CREATE TABLE app_config (
key VARCHAR(64)PRIMARY KEY,
value TEXTNOT NULL,
updated_at TIMESTAMPTZNOT NULL DEFAULT NOW(),
updated_by UUIDREFERENCES users(id)
);
-- seed defaultsINSERT INTO app_config (key, value) VALUES
('theme_accent_hex', '#c77dff'),
('theme_accent_name', 'ultraviolet'),
('x_handle', 'browser_recon'),
('waitlist_visible_depth', '247');
Field-level decisions worth keeping
waitlist_status
VARCHAR not ENUM — easier to add states later without a type migration.
tier
Nullable. NULL means "no paid plan" — the 2 free scans on activation don't require a tier.
tester_used_at
Lives on users, not on subscriptions/orders. One-time-only is a user-level fact.
tweet_url
Plain text. No validation in DB — application layer rejects non-https/non-x.com URLs.
scans.expires_at
Frozen at scan-complete time. Downgrades don't retroactively expire old reports.
app_config
Singleton-row pattern via PK = key. Two-column simplicity beats a typed config row.
12 — email copy
Three emails. Plain text. No HTML.
Transactional emails on the waitlist surface are short, plain, and signed by a human. No CSS, no logo, no preview-text engineering. The signal "this came from a real product" lands harder when the email looks like one a developer would send.
Email 01 — Joined the waitlist
subject: You're on the browser-recon waitlist.
from: browser-recon <hello@browser-recon.com>
to: {{ email }}
You're #{{ position }} in line.
We let in a small batch every week, starting with whoever's been
waiting longest. If you want to jump ahead, post about us on X and
drop the link here:
{{ tweet_submit_url }}
Pick from one of these templates if you want a starting point:
{{ template_1 }}
{{ template_2 }}
{{ template_3 }}
We'll email you when you're in.
— Lazy Coder
browser-recon
Email 02 — Approved (admin grants access)
subject: You're in. 2 free scans waiting.
from: browser-recon <hello@browser-recon.com>
to: {{ email }}
You're in. Sign in here:
{{ login_url }}
You have 2 free scans on the house. Pick any site, browse it for
two minutes, get a report.
Install the CLI:
pipx install browser-recon
First scan:
recon scan https://www.target.com
The user guide is at {{ user_guide_url }} if you want the
walk-through. Anything weird, reply to this email — it goes
to a real inbox.
— Lazy Coder
browser-recon
Email 03 — Tweet received (auto, before approval)
subject: Got your tweet. We'll review it shortly.
from: browser-recon <hello@browser-recon.com>
to: {{ email }}
Thanks for posting — appreciate it.
We review tweets in a batch every weekday morning. You'll hear back
within 24 hours either way. If it doesn't go through (e.g. the
account is private, or the post was deleted), we'll email you and
you can resubmit.
— Lazy Coder
browser-recon
13 — build sequence
Ten PRs. In this order.
Each step is sized to ship in one PR. Skip nothing; the ordering matters because the auth flip in step 4 has to land after the schema migration in step 1, and the admin panel depends on the schema being live.
Read app_config, inject --accent at request time, 60s LRU cache. Verify on existing report pages — purple accent everywhere.
~ 0.5d
3
Public landing + pricing + user guide
Three Jinja templates wired into FastAPI. Identity from design D. Tweet templates pulled from tweet_templates; X handle from app_config. No JS frameworks.
~ 2d
4
Auth flip + /waitlist + /login
POST /auth/magic-link branches on status. Unknown email → silent WAITLIST + show /waitlist page. Existing users grandfathered in step 1. Tweet URL submission on /waitlist.
~ 1.5d
5
Transactional emails
Three plain-text templates (join, approve, tweet ack). Triggered from waitlist endpoint + admin approve action. Existing SES integration or equivalent.
/admin/config (theme + handle), /admin/templates (CRUD). Cache invalidation on save. Preset swatches mirror the design D picker.
~ 1d
8
Credits + retention enforcement
Credit-decrement on scan start. scans.expires_at set at completion. Report view checks expiry and renders the [Scan again] CTA past it. Comment out the DOWNLOAD button.
~ 1.5d
9
Dashboard
/dashboard for ACTIVE users — recent scans, credit balance + renewal, tier, retention per report. Reuse the design D mock as the template.
Sequencing rule: step 1 lands first because everything reads from new columns. Step 4 (auth flip) must land before step 6 (admin approve) so the [approve] button has something to flip. Step 8 (retention) and step 9 (dashboard) can swap order if useful. Estimated total: ~11.5 days for one engineer, single-threaded.
14 — risks & kill criteria
Four ways this goes wrong.
If any of these trigger, pause and re-scope before pushing further. None are likely; all are recoverable.
RISK · waitlist starves
Approvals can't keep up with signups
If we get a big drop and the queue grows faster than the admin's morning batch, the experience reads as "spam, not exclusive." Mitigation: increase weekly batch size, or add a "we'll be in touch this week" line to the join email.
RISK · tweet quality
Tweets that get in are bad ones
Templates are too on-the-nose, users post them verbatim, the feed looks astroturfed. Mitigation: keep templates loose ("starting points, not scripts"), reject low-quality submissions at admin review. Track tweet engagement.
RISK · paid users churn
2-week Tester retention is too short
Tester users hit expiry, find no value in re-scanning, churn. Mitigation: monitor the first cohort closely; if churn > 50%, extend Tester retention to 1 month and re-pitch.
RISK · pricing wrong
The whole price ladder is off
$5 / $10 / $20 / $60 was set by gut. If we discover that buyers anchor on per-scan pricing instead of per-month, the tier shape changes. Mitigation: V1 is admin-granted on purpose — we can flex pricing per conversation before Stripe goes live.
Kill criteria — when to stop
If > 30% of approved users never run a scan within 7 days of activation, the onboarding is broken — stop building new tiers, fix the first-scan path.
If < 5% of WAITLIST users post a tweet, the jump-line incentive is too weak — strengthen the templates, or accept that the waitlist runs entirely on admin pace.
If support load > 1hr / day on the admin's time for the first 30 days, the admin panel is missing affordances — pause feature work and add them.