# VYNDR Partner + Affiliate Plumbing (A1 Session 3) Everything here ships DISABLED / organic by default. No affiliate program is approved yet; nothing spends money and no link carries tracking params until an operator flips config. Data semantics apply to attribution too — the report endpoint returns zeros before it returns invented numbers. ## 1. Partner ref links A partner shares: ``` https://vyndr.app/?ref=CODE ``` - `CODE` — A-Z 0-9 dash underscore, max 32 chars, case-insensitive (canonicalized to UPPERCASE everywhere). - On first visit `PartnerRefCapture` (mounted in the layout) sets the first-party cookie `vyndr_ref` for 90 days (SameSite=Lax, Path=/). - Attribution is FIRST TOUCH: an existing cookie is never overwritten. - On email/password signup, `AuthContext.signUp` forwards the cookie value into Supabase signup metadata as `partner_ref` (`auth.users.raw_user_meta_data.partner_ref`). - Known limitation: OAuth signups (Google, etc.) do not carry signup metadata — those signups are not attributed yet. ## 2. Partner report endpoint (internal) ``` GET /api/partners/report/:code Header: x-internal-key: $VYNDR_INTERNAL_KEY ``` Response: ```json { "ok": true, "code": "CODE", "signups": 0, "conversions": 0, "mrr_attributed": 0 } ``` - `signups` — user_profiles rows with `partner_ref = CODE` - `conversions` — those currently `subscription_status = 'active'` on a paid tier - `mrr_attributed` — sum of the real monthly price per active profile (analyst 19.99 / 14.99 founder, desk 49.99 / 34.99 founder) ### TODO migration (documented, NOT run) `user_profiles` has no `partner_ref` column today, and signup metadata lives in `auth.users.raw_user_meta_data`, which PostgREST cannot query. Until the migration below is applied, the endpoint returns zeros with a `note`. Once applied, real numbers flow with no code change. ```sql -- TODO migration: 021_partner_ref.sql (do not run until reviewed) alter table public.user_profiles add column if not exists partner_ref text; create index if not exists idx_user_profiles_partner_ref on public.user_profiles(partner_ref) where partner_ref is not null; -- Copy the signup metadata field on profile creation -- (extend the existing handle_new_user trigger): create or replace function public.handle_new_user() returns trigger as $$ begin insert into public.user_profiles (id, email, partner_ref) values ( new.id, new.email, upper(nullif(new.raw_user_meta_data->>'partner_ref', '')) ) on conflict (id) do nothing; return new; end; $$ language plpgsql security definer; -- One-time backfill for signups that predate the column: update public.user_profiles p set partner_ref = upper(u.raw_user_meta_data->>'partner_ref') from auth.users u where u.id = p.id and p.partner_ref is null and nullif(u.raw_user_meta_data->>'partner_ref', '') is not null; ``` Note: migration 004 (`referral_codes` / `referral_conversions`) is the separate USER-referral system (codes owned by auth users). Partner codes are external strings and deliberately do not depend on those tables. ## 3. Stripe promo-code mapping convention One code, three systems, zero lookup tables: - The partner's ref code IS their Stripe promotion code, verbatim. Partner "HOOPSPOD" → `?ref=HOOPSPOD` link → Stripe promotion code `HOOPSPOD` on checkout. - Codes are created in Stripe as promotion codes on a shared partner coupon; set promotion code metadata `partner: ` so Stripe reporting can be cross-checked against `/api/partners/report/:code`. - Cookie-attributed signups and promo-code redemptions are reconciled by the shared code string — a signup that used the promo but arrived without the cookie still attributes through Stripe's side. - Founder codes (VYNDR, BETONBLK, `FOUNDER_CODE_EXPIRY` in stripeService) are NOT partner codes — do not issue a partner code that collides with a founder code. ## 4. Sportsbook affiliate config (BOOK IT deep links) - `web/src/lib/bookLinks.js` builds every sportsbook deep link (StatStrip BOOK IT + scan hand-off). Organic shape: `https://{host}/?search={player}`. - `web/src/lib/affiliateConfig.js` is the single flip point: per book `{ enabled: false, params: {} }`. Param shapes (Impact: irclickid / sharedid / wpsrc; Partnerize-style: btag / afid / siteid) are documented in that file. Empty param values are skipped — a half-filled config degrades to organic, never a fabricated id. - Every book anchor renders `rel="sponsored noopener noreferrer"` (`BOOK_LINK_REL`) — required for affiliate compliance, harmless organic. - BetRivers accepts an optional two-letter `state` for its state subdomain (`mi.betrivers.com`); anything else falls back to `www`.