About That — brokerage seat model (scoping)
Status: DESIGN (not built). The real product behind the $399 brokerage tier.
Front door shipped 2026-07-17 (migration 515 — a landing + interest capture +
Slack notify; brokerages onboarded hands-on for now). This scopes the self-serve
team product so it's ready to build once the org-design forks are decided.
Companion: product_tasks "About That brokerage seat model".
The promise (what $399 sells)
Your whole team, on every listing — each in their own voice. One account.
Three capabilities behind that, none built today:
- Team — a brokerage account with N agents under one subscription.
- Per-agent voice — each agent records their OWN consented clone.
- Per-listing routing — a listing page renders in the listing agent's voice, not the account owner's (today an embed has exactly one voice).
Today's reality (the gap)
- An About That embed belongs to one
user_id, resolves one voice (resolveVoice→ the owner's clone or a narrator). One snippet = one voice. - The
about_that_brokerageplan_key exists + is paywall-recognized, but grants nothing beyond whatagentdoes — there's no team, no seat, no routing. - Voice clones are per-user (
getActiveVoice(userId)), consent-gated.
The forks to decide (owner input needed)
A. Org model
- A1 — Reuse
user_id+ abrokerage_idgrouping (lightweight): abrokeragesrow +brokerage_members(user_id, role). Each agent keeps their own HJ account + their own clone; the brokerage links them + holds the sub. - A2 — A full org/workspace primitive (heavier, reusable elsewhere). Lean A1 — smaller, and agents already have accounts + clones.
B. Per-listing → agent resolution (the crux)
How does a listing page tell the render which agent to voice?
- B1 —
data-agenton the embed snippet: one brokerage embed; the listing template passes the listing agent's id/slug per page (data-embed=… data-agent="jane-doe"). The loader forwards it; the render resolves that agent's voice if they're a member of this brokerage (the security gate — a brokerage can only voice its own consented agents). - B2 — URL-pattern mapping (agent inferred from the listing URL) — brittle.
- B3 — Per-agent embeds (each agent their own embed, brokerage just bills) — no "one snippet" magic, but trivial; a viable Phase 1. Lean B3 for Phase 1 (bill-together, N embeds), B1 for Phase 2 (the real magic).
C. Billing across seats
- Flat $399 for the team (current pricing) vs per-seat. Flat is simpler and
matches the page. The sub lives on the brokerage; membership gates who can
create agent-preset embeds under it (reuse
isAboutThatSubscriberat the brokerage level).
D. Consent + voice safety (non-negotiable)
A brokerage can voice an agent ONLY with that agent's own consented clone
(JQ_VOICE_MIN_CONSENT_VERSION). No borrowing, no cloning-on-behalf. The
per-listing resolver MUST verify (agent ∈ brokerage) AND (agent has consented
clone) or fall back to narrator — never voice someone without their consent.
Phased plan
- Phase 0 (done): front door — landing +
about_that_brokerage_leads+ Slack notify; hands-on onboarding. - Phase 1 — team + bill-together (B3) — SHIPPED (migration 516):
brokerages+brokerage_members; owner names the brokerage + invites agents by email (accept link, token-authed); an agent accepts (binds their account);isBrokerageCoveredextendsrequiresAboutThatPlanso an active member's own agent-preset embed renders under the brokerage's oneabout_that_brokeragesub. Dashboard/dashboard/about-that/brokerage(name / invite / members / remove) + accept page/about-that/brokerage/accept?token=. Delivers "our whole team is on About That, one invoice." What's NOT here: the render is still per-agent embeds — one snippet per agent, not one brokerage snippet resolving the listing agent (that's Phase 2 / B1). - Phase 2 — one snippet, right agent (B1) — SHIPPED (migration 517):
data-agent="<slug>"on the brokerage embed → the loader forwards it → the render resolves the member agent's voice viaresolveAgentVoice(gate:resolveAgentUserIdconfirms the slug is an ACTIVE member of THIS embed owner's brokerage, then a consented-clone check; null → falls back to the embed's own voice, never voices a non-member/unconsented). Per-agentagent_slug(from the email localpart, per-brokerage unique) shown on the brokerage dashboard. Agent-specific voiceKey → renditions cache per-agent. Threaded through the on-tap render AND eager-render. This is the headline promise: one snippet on the listing template, each listing in its agent's voice. - Phase 3 — polish: per-agent analytics, brokerage brand controls, seat management UX.
Going paid — Stripe setup (the $399 brokerage tier)
The billing rails are built + wired; this is the one-time Stripe config to start charging. General flow: ABOUT_THAT_PAID_LAUNCH.md (agent + brokerage share it). Brokerage-specific below.
Step 1 — create the two recurring Prices in Stripe (Dashboard → Products):
- About That — Agent · $79/month · recurring → copy its Price ID
(
price_…, NOT the product idprod_…). - About That — Brokerage · $399/month · recurring → copy its Price ID.
- Use live-mode prices for real charges (test-mode for a dry run — the webhook + secret must match the same mode).
Step 2 — set the price env vars on the backend (Railway):
STRIPE_PRICE_ABOUT_THAT_AGENT=price_… # the $79 Agent price id
STRIPE_PRICE_ABOUT_THAT_BROKERAGE=price_… # the $399 Brokerage price id
Until set, POST /api/payment/about-that/checkout returns 503 for that plan
(the checkout endpoint reads these directly). Also confirm the base Stripe env
is present: STRIPE_SECRET_KEY + STRIPE_WEBHOOK_SECRET.
Step 3 — confirm the webhook points at POST /api/payment/webhook and is
subscribed to checkout.session.completed + customer.subscription.*. No new
code: the webhook maps the Price ID → plan_key via priceToPlanKey
(routes/payment.ts) and upserts a
subscriptions row (about_that_agent / about_that_brokerage). The
STRIPE_WEBHOOK_SECRET must be the signing secret for THIS endpoint (live vs
test must match Step 1).
Step 4 — flip the master switch (only after Steps 1–3):
NEXT_PUBLIC_ABOUT_THAT_PAID_MODE=true
Redeploy. Now agent-preset embeds require a plan; brokerage members are covered
by the owner's $399 sub via isBrokerageCovered.
Where the brokerage owner clicks subscribe: the "Subscribe — $399/mo" button
on /dashboard/about-that/brokerage (it POSTs /api/payment/about-that/checkout {plan:'brokerage'} → Stripe Checkout → success returns to the dashboard). This
is the entry for an owner who manages a team but has no agent embed of their own.
The generic paywall CTA on /dashboard/about-that (agent + brokerage) still
works too.
Test the whole loop (Stripe test mode):
- Set the two
price_…(test-mode) +STRIPE_WEBHOOK_SECRET(test) + flag on a staging deploy. - As a brokerage owner: name the brokerage → Subscribe ($399) → complete with
card
4242 4242 4242 4242. - Verify a
subscriptionsrow landed withplan_key='about_that_brokerage',status='active'. - Invite an agent → they accept → they create an agent-preset embed → its render
is covered (no 402), proving
isBrokerageCovered. GET /api/about-that/billing/statusshows the owner subscribed; a covered member showsbrokerage_covered:true.
Comping a brokerage (design-partner / pilot — no charge): insert the sub row directly (mirrors the agent comp in the paid-launch guide):
insert into subscriptions (user_id, plan_key, status, current_period_end, cancel_at_period_end)
values ('<brokerage-owner-uuid>', 'about_that_brokerage', 'active', now() + interval '90 days', true);
The owner's whole active team is then covered for 90 days.
Gotchas:
NEXT_PUBLIC_ABOUT_THAT_PAID_MODEis a build-timeNEXT_PUBLIC_*var — set it where the frontend build reads it and redeploy (a runtime-only set won't take on the client).- Test-mode Price IDs + a live webhook secret (or vice-versa) = silent non-activation. Keep all of Steps 1–3 in the same Stripe mode.
- The $399 covers the team via membership — you do not create a Stripe
seat/quantity per agent in Phase 1 (flat price;
quantity: 1).
Reuse map (what's already built)
- Voice clone + consent:
getActiveVoice/JQ_VOICE_MIN_CONSENT_VERSION(lovio). - Paywall/subscription:
about-that-billing.ts(extend the check to brokerage-membership). - Embed + preset + render:
about-that.ts(resolveVoicebecomes agent-aware in Phase 2). - Invite/notify: the
notifyBrokerageLead+ welcome-drip / Resend rails. - Partner-provisioning primitives (#1332) if a brokerage's site provisions via QuickSites — the app-acts-for-owner pattern generalizes.
Not in scope here
Pricing changes (flat $399 stands), a generic org system beyond brokerages, and QuickSites brokerage-site integration (a later crosstalk thread if it comes up).