Affiliate program
Built 2026-08-03. Beta testers become affiliate marketers who earn a recurring %
commission on the Family Wall Plus subscriptions they refer. Generic by plan_key
(launches on family_wall_plus); payouts are tracked here + paid manually (no Stripe Connect).
The loop
- Admin adds an affiliate at
/dashboard/admin/affiliates— by account email, with a code (auto-derived from the email if blank) and a % (default 30). Admin-created ⇒activeimmediately. Optional Comp Plus grandfathers them Family Wall Plus free (beta). - The affiliate shares their link —
/family-wall?ref=<code>— from their own dashboard at/dashboard/affiliate(link + share button + live stats + the FTC-disclosure reminder). - A referred family is attributed first-touch:
AcquisitionTrackercaptures?ref=<code>and posts it toPOST /api/affiliate/attribute, which write-once stampsprofiles.referred_by_affiliate_code(ignores unknown/inactive codes, self-referral, and already-attributed profiles). - Commission accrues on every payment. The
invoice.payment_succeededwebhook (initial + each renewal) resolves the plan from the invoice price and the payer from oursubscriptionsrow, thenrecordAffiliateCommissioninserts one row intoaffiliate_commissions(idempotent per Stripe invoice — redelivery-safe). Amount =round(gross × commission_pct / 100). - Admin pays out manually and clicks Mark paid — moves that affiliate's
pending/approvedcommissions topaid.
Data (migration 621)
affiliates— one per user:code(unique),status(pending/active/disabled),commission_pct,is_beta,notes.profiles.referred_by_affiliate_code— first-touch attribution (write-once).affiliate_commissions— the ledger: one row per referred payment (gross_cents,amount_cents,statuspending/approved/paid/reversed,stripe_invoice_idunique).
Service: affiliate.ts (AFFILIATE_PLANS allowlist — add a plan_key to extend). Routes: routes/affiliate.ts mounted /api/affiliate (POST /attribute, GET /me; super-admin GET /admin/list, POST /admin/create, PATCH /admin/:id, POST /admin/:id/mark-paid, POST /admin/:id/beta-comp).
Running it
- Apply migration 621 to prod.
- Redeploy the backend (new route + webhook branch).
- Confirm Stripe sends
invoice.payment_succeededto the webhook (it already handles it for coaching, so this is usually already on — check the Stripe dashboard webhook event list if commissions don't accrue on renewals). - Add your beta testers as affiliates; Comp Plus so they can experience the paid tier; hand them their
/dashboard/affiliatelink.
Compliance — not optional, and it's the honest thing
- FTC disclosure — BOTH sides. Affiliate-facing: affiliates must disclose they earn a commission when they share the link — the dashboard states this + gives a ready line. Referee-facing (added 2026-08-03, PH mesh catch): the person referred is also told — a dismissible
ReferralDisclosurebanner (components/ReferralDisclosure.tsx, mounted globally inlayout.tsx) fires when a?refcode is present: "you followed a friend's referral link; if you subscribe, they earn a small commission, never any extra cost to you." Affiliate-side and referee-side disclosure are separate requirements; disclosing to the affiliate does not disclose to the referee. Undisclosed affiliate promo is an FTC violation and off-brand. - Taxes / 1099. US affiliates paid ≥ $600 in a calendar year need a 1099-NEC, which means collecting a W-9 before you pay that much. The
affiliate_commissionsledger (sum ofpaidper affiliate per year) is your record; collecting W-9s + issuing 1099s is a manual/owner step the system does not automate. - Program terms. Before scaling past a handful of trusted testers, a short affiliate agreement (what's allowed, no spam/paid-search-on-brand/cookie-stuffing, clawback on refunds/chargebacks, payout cadence) is worth having. Not built — a legal/owner task.
- Clawbacks — wired.
charge.refunded→reverseCommissionForInvoiceauto-reverses the matching commission (by Stripe invoice id) so a refunded sale stops counting toward a payout. Only unpaid rows (pending/approved) flip toreversed; if the commission was already paid out, it's left intact and logged withalreadyPaid: true— you can't silently un-pay the affiliate, so that one is a manual clawback (deduct from their next payout). Chargebacks/disputes (charge.dispute.created) are not yet auto-reversed — handle manually.
Founders window (Aug–Sep 2026) — auto-fulfillment
A time-boxed promo on /family-wall: sign up in the window and the founder perks are auto-granted, no admin step. grantFounderPerks(userId) (affiliate.ts) fires fire-and-forget after POST /api/family/wall (first wall creation): comps Family Wall Plus (grandfather-style) + creates an active founder affiliate (auto code from the email local-part, collision-suffixed, commission_pct 30, is_beta). Idempotent (skips whichever they already have) + no-op outside inFoundersWindow() (hard-coded 2026-08-01 … 2026-10-01). Founders find their link at /dashboard/affiliate. Remove/refresh after Sept 2026 (the date guard makes it inert automatically, but pull the /family-wall band + this path when the promo ends).
Generalizing to all HJ offerings (roadmap)
The engine is already generic (AFFILIATE_PLANS allowlist, recordAffiliateCommission({ planKey, grossCents, … })); attribution is global (AcquisitionTracker captures ?ref on any page). Extending affiliate programs to every major HJ offering is mostly wiring, in three tiers of effort:
- Tier 1 — other subscription offerings — DONE (2026-08-03).
AFFILIATE_PLANSnow coversfamily_wall_plus,cornerstone,graphene_plus,write_cafe_pro,about_that_agent,about_that_brokerage(coaching excluded — it has its own coach split). They all accrue on the sameinvoice.payment_succeededbranch.AFFILIATE_OFFERINGS(label + path) drives the affiliate dashboard, which now shows a ref link per offering (Family Wall · Cornerstone · Graphene+ · write.cafe Pro · About That) — one code, promote whichever fits the audience. Per-offering commission % is still Tier 2 (today the rate is per-affiliate, flat across offerings). - Tier 2 — one-off purchases (real work). Persona Testing credit packs (
persona_test_credit_purchases), Drift coins, etc. accrue viacheckout.session.completed/ a purchase-recording path, not invoice renewals. GeneralizeAFFILIATE_PLANS→ anAFFILIATE_OFFERINGSmap ({ key, revenueType: 'recurring' | 'oneoff', pct }) and add arecordAffiliateCommissioncall in each one-off purchase path (idempotent per checkout-session/purchase id). - Tier 3 — offering-aware surfaces. Per-offering earnings breakdown on
/dashboard/affiliate, per-offering rate in admin, and an affiliate link that can target any offering's landing page (already works —?refis global). At this point an affiliate promotes the network, not one product.
The mesh version of this — one affiliate across HJ + QuickSites + PorchHearth — is a convention, not a shared service: crosstalk/contracts/affiliate-program.md. HJ's lean there is per-product first, adopt the shared-code + aggregated-owed convention only once a second product wants in.
Not built (deferred by design)
- Stripe Connect automated payouts (chosen against — manual is right for a few testers).
- Self-serve affiliate signup (invite-only in beta).
- Auto-reverse on chargeback/dispute (refunds ARE auto-reversed); per-affiliate payout history view; W-9/1099 tooling.