product

Persona testing as a service — the middle layer

Status: DESIGN (2026-07-28), Sandon-directed. The productization of persona testing: from "we test our own site + one partner" to any site owner can request persona testing for their own goals and get honest findings back. This doc is the plan; nothing here is built yet.

The engine already exists (persona-testing-core.ts: browse → extract → calibrate, first-person + absence-grounded, battle-tested against QuickSites). The middle layer is everything between a request and that engine: intake, authorization, the runner, and delivery.

Decisions made (2026-07-28)

  • Intake: verified self-serve — an owner proves they control the domain, then can request testing themselves.
  • Authorization: both — domain-ownership verification (proves it's their site) and a human approves the scope before any browse. Belt-and-suspenders, because pointing our browsers at a third-party site is the entire risk surface.
  • Delivery: a shareable report page — a per-request public link (/persona-report/<token>) that renders the findings, first-person and honestly scored. Doubles as the deliverable and the marketing artifact.

Why the caution is load-bearing

Testing your own site is safe. Testing someone else's on request opens real abuse vectors that the whole design exists to close:

  • DDoS-by-proxy — someone points our fleet at a victim URL. → rate limits + cost caps + robots.txt.
  • Testing a site they don't own — recon/harassment dressed as "testing." → domain-ownership verification + human approval.
  • Destructive actions — form submits, checkout, account creation on a stranger's site. → read-only enforcement (the existing browse constraints, hardened).
  • "Human testing" misrepresentation — the honesty rule travels: every finding is an AI-persona observation, labeled at delivery, never sold as human QA.

The safety spine (every request passes all of these): verified domain · human-approved scope · read-only only · robots.txt respected · rate-limited + per-request cost-capped · AI-persona labeled.

Architecture

 Owner ──▶ [1] Request form ──▶ persona_test_requests (status: pending_verification)
                                     │
           [2] Verify domain ────────┤  (meta tag / DNS TXT / /.well-known file)
                                     ▼  status: verified
           [3] Human approves scope ─┤  (super-admin reviews site + goals)
                                     ▼  status: approved
           [4] Runner ──────────────┤  reuses browseAndExtract, third-party-safe
              (personas browse the   │  → persona_test_findings (request_id)
               site with the goals)  ▼
           [5] Report page ──────────┘  /persona-report/<token>  (shareable, public)

1. Intake — persona_test_requests

{ id, owner_user_id (nullable — could be a logged-out requester), contact_email, site_url, goals[] (free-text goals the owner cares about), audience_hint (which persona archetype), status, verification_token, verification_method, report_token, created_at, approved_by, approved_at }.

Status lifecycle: pending_verification → verified → approved → running → delivered (+ rejected, failed).

2. Verification (net-new — the keystone)

On request, mint a random verification_token. The owner proves control via any one of:

  • Meta tag: <meta name="persona-testing-verification" content="TOKEN"> on the site root.
  • DNS TXT: persona-testing-verification=TOKEN.
  • Well-known file: /.well-known/persona-testing/TOKEN.

A POST /verify fetches the site/DNS and confirms. Verified domains are remembered (a persona_test_domains table keyed by host) so repeat requests skip re-verification. No existing HJ pattern for this — it's the main new build.

3. Approval

A super-admin sees verified requests in the cockpit (extends today's /dashboard/admin/persona-testing), reads the site + goals, and approves or rejects. Approval is where a human catches "verified but sketchy" (a competitor's site they happen to control, goals that push toward writes, etc.).

4. Runner (third-party-hardened)

Reuses browseAndExtract per goal, but adds third-party safety the self-test never needed:

  • robots.txt fetched + respected for our user-agent (net-new — the browse path has none today).
  • A distinct, honest user-agent identifying the persona-testing bot + a contact URL.
  • Per-request caps: max N browses, max total cost, on top of the existing 20-step / 25¢-per-browse cap.
  • Read-only, hardened: the goals themselves are constrained/validated to non-destructive intents (no "sign up", "buy", "submit") at approval time. Findings land in persona_test_findings (same shape as persona_self_findings, plus request_id).

5. Delivery — the shareable report (+ Slack / webhook)

A public page /persona-report/<report_token> (unguessable token, not enumerable) rendering the request's findings — first-person, per-goal, honestly scored (the same scoreboard: browses / frictions / severity). Mirrors /persona-testing's design. The owner can share it; it's also our best sales artifact ("here's what our personas found on your site").

Delivered where you work (net-new, an advertised differentiator). Per request, the owner can pick a notify_channel + notify_target: a Slack message to their team channel (an Incoming Webhook URL) or a generic webhook (POST of the report JSON to their own systems). Both are SSRF-guarded at intake AND at send time (assertSafePublicHost), https-only, best-effort (a failed ping never sinks the run), and stamped notified_at. Service: persona-test-notify.ts (sendReportNotification, notifyRequestDelivered) — the runner calls notifyRequestDelivered(requestId, summary) right after it delivers a report.

6. Monetization — credits, pay-per-use (built 2026-07-28)

Model decided: credits + a funnel-first free tier (a site owner who lets our personas browse their live site is a warm lead for the whole mesh; marginal cost of a browse is pennies, hard-capped by persona_browse_throttle).

  • 1 credit = one persona goal-browse. A report costs estimateCredits(goals) = goals × 1.
  • Free tier: FREE_MONTHLY_CREDITS = 10 granted every calendar month, idempotently (≈3 three-goal reports). No card.
  • Credit packs (Stripe Checkout, per-credit price falls with size): Small 25/$9, Medium 100/$29, Large 300/$69.
  • Ledger is the source of truthbalance = SUM(delta) over persona_test_credit_ledger; no mutable balance column. Debits are idempotent per request (uq_ptcl_report_run), monthly grants idempotent per owner-month (uq_ptcl_monthly_grant). Stripe grants use the same atomic pending→paid claim as drift-coin-purchases so at-least-once redelivery never double-credits.
  • Service: persona-test-credits.ts. Migration 599_persona_test_credits.sql. Webhook branch in routes/payment.ts (kind: 'persona_test_credit_purchase'). Routes: GET /api/persona-testing/credits (owner summary), GET /credits/packages (public price list), POST /credits/checkout.
  • The debit fires from the runner (Phase 4): call debitForReport(ownerUserId, requestId, goalCount) when a run actually starts; {ok:false, shortfall} routes the owner to buy a pack. Requesting + verifying stays free (funnel-first).

What we reuse

NeedReuse
Browse → first-person findingpersona-testing-core.ts (browseAndExtract, the calibrated extractor, absence-grounding)
Owner identity / keysthe partner-provisioning pattern (about-that-provisioning-core, coaching-provision)
Findings + curation UItoday's persona_self_findings + /dashboard/admin/persona-testing cockpit
Report page design/persona-testing (painterly hero, honest scoreboard, first-person cards)
Delivery-to-partner (later)the QS receiver model (crosstalk/contracts/persona-testing.md) as an optional 2nd delivery channel

Build order (each slice ships behind the super-admin gate first)

  1. Data modelpersona_test_requests, persona_test_domains, persona_test_findings (migration 598).
  2. Verification — token mint + POST /verify (meta/DNS/well-known) + instructions surface. persona-test-service.ts.
  3. Intake form — owner surface /dashboard/persona-testing: request form (site + goals + audience + contact + Slack/webhook delivery), credit balance + buy-pack, inline verification instructions + "Verify now". Entry points from the public case study ("Test your own site — free to start").
  4. Approval + runnerpersona-test-runner.ts: the cockpit /dashboard/admin/persona-testing shows a request queue (approve / reject / re-run); on approval the third-party-hardened runner (robots.txt respected, PERSONA_TEST_UA, inherited 20-step/25¢ caps + kill switch) debits credits once, runs one persona per goal, stores findings, delivers, and pings Slack/webhook.
  5. Report page/persona-report/<token> (shareable, noindex, token-gated public route GET /api/persona-testing/report/:token).
  6. Owner status view — the same /dashboard/persona-testing lists the requester's requests with a live status label + "Verify" / "View report" per row. On delivered, the report link is also emailed to the request's contact_email (or the owner's account email) via sendReportEmail in persona-test-notify.ts (Resend, best-effort, synthetic-address-safe) — alongside the Slack/webhook push.

All six phases shipped. The service is self-serve end-to-end: request → verify → approve → hardened browse (credits debited) → delivered report, pushed to Slack/webhook + emailed.

Open questions (not blockers for Phase 0 data model)

  • Business model: DECIDED 2026-07-28 — credits, pay-per-use, funnel-first free tier. See §6 Monetization. Built: ledger + free monthly grant + Stripe packs + notify (Slack/webhook) delivery. Remaining: the debit only bites once the Phase-4 runner calls debitForReport.
  • Logged-out requesters: allow anonymous requests (email-only) or require an HJ account? Recommend: require an account — ties verification + report ownership to a user, cuts abuse.
  • Goal safety: auto-validate goals against a non-destructive allowlist at intake, or rely on human approval? Recommend: both — a soft check at intake, the human as backstop.
  • Findings visibility: report public-by-token vs owner-only-until-they-share. Recommend: unguessable token, owner controls sharing.

Positioning

This is the capability the QuickSites test was proving out ([[project_persona_qs_testing]]), generalized into an app-neutral service ([[feedback_generalize_backend_apis]]). It's a natural top-of-funnel for the whole mesh: a site owner who lets our personas test their site is a warm lead for everything else — and the honesty discipline (a harness that can come out against you) is exactly the trust signal that makes it worth trying.

PERSONA TESTING SERVICE — Docs | HiveJournal