# v2 brief — free creator beta (supersedes ../BRIEF.md)

Source of truth: `docs/superpowers/plans/2026-09-08-frontend-flow-baseline.md`, the API contract, `src/api/types.ts`. Everything below is derived from them. Where this brief and the plan disagree, the plan wins.

Product: **Who's Paying** (wordmark `ostrya.`). An invited Instagram creator in India types their handle and a beta code, waits a few minutes while we read their recent reels and the Meta Ad Library, and gets a free report: which brands fit their content, what their attention might be worth, and how to pitch. Mobile first, desktop carried over. Nothing is locked, blurred, paid, queued or reviewed by a human.

Every template is ONE HTML file that loads the sibling `fixture.js` (`<script src="fixture.js"></script>`) and reads `window.FIXTURE`. Inline CSS and vanilla JS otherwise, Google Fonts allowed, no other network. Do not modify `fixture.js`. Do not hardcode report numbers: render from the fixture so edge fixtures render correctly through the same code.

## Fixture map

`FIXTURE.reports.{main|partial|withheld|zero|few|lowconf}` are `FullReportBody` objects. `FIXTURE.scans.{pending|early|demand|matching|checking|contacts|done|unavailable|failed}` are `ScanProgress` objects. `FIXTURE.liveSequence` is an ordered list of `[progress, holdMs]` for an animated scan. `FIXTURE.facts` answers `GET /report/:id/facts` for the main report. `FIXTURE.preview` is a `PreviewAccepted`. `FIXTURE.labels` maps score components to creator-facing labels. `FIXTURE.stageRows` is the five-row grouping. Helpers: `FIXTURE.inr(n)` Indian grouping, `FIXTURE.rupees(paise)`, `FIXTURE.rowStatus(progress,row)`.

## Routes (hash-based app URLs, mirroring the proposed app)

| Route | Screen | Notes |
|---|---|---|
| `#/` | Entry | `?state=filled`, `?state=400`, `?state=403`, `?state=503`, `?state=500`, `?state=lost` render the inline states. |
| `#/scans/live` | Scan, animated | Plays `FIXTURE.liveSequence` as if polling every 2 s, then routes to `#/reports/main`. Under reduced motion: show `scans.done` and a "Open report" button instead. |
| `#/scans/early` `#/scans/demand` `#/scans/checking` `#/scans/contacts` | Scan, frozen states | Static render of that progress object. `checking` must show the active substage (verify) inside row 5; `contacts` shows enrich running with rows 1–4 done and narrate + verify done inside row 5. |
| `#/scans/reconnecting` | Scan, connection trouble | Render `scans.demand` as last known state with a reconnecting notice; polling paused; last-updated time; nothing invented. |
| `#/scans/failed` | Scan failed | `scans.failed`: demand failed. Offer "Start a new scan for @moneywithmeera" (goes to `#/?state=filled`) and "Back to start". |
| `#/scans/done` | Restored completed scan | Show completed rows and a primary "Open report" to `#/reports/main`. |
| `#/scans/unavailable` | Done, report unavailable | `scans.unavailable`: honest "Report unavailable" state, no empty report, no auto-rescan. |
| `#/scans/<anything else>` | Unknown link | 404 recovery: "We can't find that scan." + "Start a scan". |
| `#/reports/main` | Full report | The happy path. |
| `#/reports/main/brands/page:100281786832302` | Brand focus | Why, evidence, pitch, contact, copy actions. Every brand in a report gets this route. |
| `#/reports/partial` `#/reports/withheld` `#/reports/zero` `#/reports/few` `#/reports/lowconf` | Reduced results | Same report code, edge fixtures. |
| `#/reports/main?evidence=unavailable` | Evidence fetch failed | Opening evidence shows a load error inside the evidence surface; the report stays usable. |
| `#/reports/<anything else>` | Unknown link | Same 404 recovery. |
| `#/preview` | Optional comparison | Preview-first snapshot from `FIXTURE.preview` with an explicit "Find my brand matches" that starts a **new** full scan (route to `#/scans/live`). Label clearly that this starts a new scan. |

Prototype footer (fixed, small): `Prototype · fixture data` plus a "Screens" menu listing every route above so a reviewer can jump anywhere. Include a "Reduced motion" note if applicable.

## Entry `#/`

Hierarchy: what the creator will learn → handle → beta code → primary action.

- Wordmark `ostrya.`, product line "Who's Paying · India".
- H1 (working copy, keep): "See which brands fit your content, what to charge, and how to pitch them."
- Supporting copy: "We read your recent public reels and the Meta Ad Library's active ads in India, then match brands to your topics. No deal is promised." Do not promise a Match count, income or a deal.
- Handle input with `@` prefix, label "Instagram handle". Accept with or without `@`; trim and lowercase on submit. Do not call it account validation.
- Beta code field, clearly labelled "Beta code", with a one-line explanation "Invited creators got this from us." Placement is per template (inline, disclosure or second step) but it is always required to submit.
- Primary button: "Find my brand matches". Pending state: disabled, spinner, "Starting your scan". Never allow a second submit while pending.
- Reassurance line: "Public data only. No Instagram sign-in. Nothing is posted or sent for you."
- Timing: "This can take a few minutes." Never "About a minute", never a countdown.
- Sample: a single clearly labelled link "See a sample report" to `#/reports/main`. No example chips that start scans. No live ticker. If you show any market numbers on the entry screen they must be labelled "Sample data".
- Inline states: 400 "Enter your Instagram handle." (field-level); 403 "That beta code didn't match. Check the code we sent you." (field-level on the code); 503 "Scanning is paused while a data source reconnects. Try again in a few minutes."; 500 "The scan failed before it started. Try again."; `lost` "We lost the connection before the scan was confirmed. It may already be running. Wait a minute before starting another." with a secondary "Start another scan anyway". Entered values stay in the fields.
- Submit routes to `#/scans/live`.

## Scan `#/scans/:id`

Show the handle, the current activity, completed evidence summaries and a clear sign that work continues. Five grouped rows (from `FIXTURE.stageRows`) unless the template's assignment says nine. Row status via `FIXTURE.rowStatus`. Each done row shows its constituent stage summaries verbatim from `summary` (they are already Indian-grouped Facts, never prose). A running row shows the active stage name in plain words (profile "reading profile", posts "fetching reels", understand "reading topics", demand "searching the Ad Library", match "matching", price "pricing", narrate "writing your report", verify "checking citations", enrich "looking for contacts"). Handle `summary: null` cleanly.

Rules: no percentage progress bar, no ticking counters, no ETA, no fake timers advancing rows. A restrained "Started 09:12" and "Last update 09:13" is fine. Observed durations may be shown per done stage as "took 1m 14s". Long demand stage: copy "Searching the Ad Library can take a minute or two." A skeleton of the report below or beside is allowed but must not imply data exists yet.

Recovery: reconnecting notice keeps the last known rows; failed state is explicit and separate from reconnecting; done + reportAvailable → primary "Open report"; done + not available → "Report unavailable" with "Start a new scan" and the scan link kept visible; unknown id → 404 recovery.

## Report `#/reports/:id`

Order: A creator context → B rate card → C ranked brands → D brand focus (own route) → E methodology and evidence. Templates may reorder B and C if they say why in their header comment.

A. Handle, topics, vertical, "Scan finished 8 Sept 2026, 09:14" (from `scans.done.finishedAt`). NO display name, avatar or verified badge; they are not in the report. Stats: Followers; Median reel plays; "Plays per follower" = `reachPct` shown as `22.4%` with helper "median plays ÷ followers, can exceed 100%"; "Paid partnerships in fetched reels" = `paidDeals`; total posts shows "unknown" when `postCount` is null. Insight and gap are cited prose: render the text with an "Evidence" affordance that opens their `*FactIds`. If empty, render the honest empty state (see below), never client-written persuasion.

B. Rate card: three rows from `rateCard`: "Sponsored reel", "Licensed content", "Three-reel package", each `rupees(lo) – rupees(hi)`. Label "Estimate" when `isEstimate`. Basis line: "Based on your median 41,250 and top 2,38,900 plays at ₹1–₹2 per view for bfsi creators." computed from the fields. When `lowConfidence`: a visible note next to the amounts "Low confidence: we don't have a rate band for this content type yet, so this uses the default band." No "submit your deal" control.

C. Brands: heading "Brands that match your content" with the real count ("9 brands", "2 brands", or the zero state). Cards in returned order with rank, name, category (hide when null), score labelled "Match score" with a tooltip "Not a probability of closing a deal. Higher means more evidence of fit and activity.", matched topics, the four components using `FIXTURE.labels` (never "Audience overlap"), a contact state ("Contact found" / "Contact unavailable in this report"), and the why prose (or its empty state). Evidence chips like "14 active ads", "9 creator-made ads", "6 creators paid" come from resolving `evidenceIds` against `FIXTURE.facts`, not from flat fields. No "heaviest spender" badge. Card opens brand focus.

D. Brand focus: why + Evidence; pitch (full text) + "Copy pitch"; contact block with email, domain, source, confidence as "Provider confidence 91%", note "Not a verified address", "Copy email"; or "Contact unavailable in this report" with the explanation "This can mean no search ran, nothing suitable was found, or the provider failed." Copy actions give success and failure feedback (toast or inline). Empty pitch: "We couldn't verify this pitch." and no copy button. Pitch and contact states are independent. No "Find contact" button anywhere. Preserve scroll position and return focus on close.

E. Evidence surface: resolves Fact IDs to rows of `kind`, `value`, `stage` and source details, with a link when `sourceRows[].url` exists, otherwise the identifier. Optional raw JSON behind a disclosure, never first. Loading and error states leave the report usable. Methodology block shows `methodology`, provenance counts (`factCount`, `candidateCount`, `matchCount`) and the verification status in words.

Verification treatment: `verified` → no banner, methodology explains checks (never "100% accurate" or "human approved"). `partial` → one restrained report-level note "Some explanations couldn't be verified and are left blank." plus the slot-level empty state. `withheld` → prominent note near the top "We couldn't verify the summary for this report, so it's left out. Your stats, rates and brand matches are unaffected." while everything else renders. Slot empty state text: "We couldn't verify this explanation." / "We couldn't verify this pitch." Zero matches: "No matching brands found in this scan." + "We only list brands with active Ad Library ads in India that match your topics and show creator-partnership evidence. It doesn't mean no brand fits you." Keep stats and rate card. One or two Matches: real shortlist, no padding.

## Component floor (shadcn-like, hand-rolled)

Button (primary / secondary / ghost / destructive; loading), input with prefix and field error, badge, status pill (done / running / failed / pending, with text not just colour), skeleton, spinner, card, tooltip, dialog or drawer or sheet (per template), toast, copy-to-clipboard with feedback, disclosure/accordion, tabs where the template uses them, sticky elements that never cover the primary action on mobile, visible focus rings, `prefers-reduced-motion`, 360px responsive, large-text tolerant (test at 125% zoom), `aria-live="polite"` for stage changes only. Indian number grouping everywhere via `FIXTURE.inr`.

## Copy rules

Sentence case. Plain verbs on buttons. Estimates are called estimates; observations are called observations. No "→" appended to links. No uppercase-tracked eyebrow labels. Middle dots only inside the server summaries and the meta line under the handle. Never invent numbers not in the fixture. Never say "verified" about a contact or "approved" about a report.
