product

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:

  1. Team — a brokerage account with N agents under one subscription.
  2. Per-agent voice — each agent records their OWN consented clone.
  3. 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_brokerage plan_key exists + is paywall-recognized, but grants nothing beyond what agent does — 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 + a brokerage_id grouping (lightweight): a brokerages row + 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-agent on 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 isAboutThatSubscriber at the brokerage level).

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); isBrokerageCovered extends requiresAboutThatPlan so an active member's own agent-preset embed renders under the brokerage's one about_that_brokerage sub. 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 via resolveAgentVoice (gate: resolveAgentUserId confirms 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-agent agent_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 id prod_…).
  • 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):

  1. Set the two price_… (test-mode) + STRIPE_WEBHOOK_SECRET (test) + flag on a staging deploy.
  2. As a brokerage owner: name the brokerage → Subscribe ($399) → complete with card 4242 4242 4242 4242.
  3. Verify a subscriptions row landed with plan_key='about_that_brokerage', status='active'.
  4. Invite an agent → they accept → they create an agent-preset embed → its render is covered (no 402), proving isBrokerageCovered.
  5. GET /api/about-that/billing/status shows the owner subscribed; a covered member shows brokerage_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_MODE is a build-time NEXT_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 (resolveVoice becomes 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).

ABOUT THAT BROKERAGE SEAT MODEL — Docs | HiveJournal