# A1 Session 3 — Affiliate + Partnership Plumbing ## Operating constraints - ZERO out-of-pocket. No affiliate program is approved yet — everything ships config-flip-ready but DISABLED / organic by default. - Data semantics: never fabricate market values. A "best price" claim renders only when genuinely multiple books post the same line — absent beats wrong. - No exclamation points in any copy (VOICE v1.1). ## 1. BOOK IT deep-link builder - `web/src/lib/bookLinks.js` (CommonJS, unit-testable) - `SUPPORTED_BOOKS`: draftkings, fanduel, betmgm, caesars, betrivers (hosts sourced from the existing SPORTSBOOKS list in scan/page.tsx + lib/books.js brand map). - `buildBookLink({ book, player, sport, state? }, config?) → { url, tracking } | null` - Unknown book → null (absent beats wrong). - Organic base: `https://{host}/?search={player}` (existing deep-link shape). - Affiliate layer: reads `web/src/lib/affiliateConfig.js` — per book `{ enabled: false, params: {} }` with Impact/Partnerize-style param shapes documented in comments (irclickid / sharedid / afid / btag). - `tracking: true` only when the book is enabled AND at least one non-empty param was appended. Disabled → clean organic link. - `BOOK_LINK_REL = 'sponsored noopener noreferrer'` — every book anchor renders this rel. - Wiring: - `StatStrip.tsx` BookItTeaser → real `` deep link (best-price book when known, else the prop's book, else DraftKings). Organic until config flips. - `scan/page.tsx` sportsbook hand-off anchors → `buildBookLink` + `BOOK_LINK_REL` (replaces the local SPORTSBOOKS/deepLink pair). ## 2. Best-line highlight (cheap version) - Finding (documented in the session report): the grouped odds proxy already ships per-book rows to the browser as `lines[]` per player+stat; `Slate.groupByGame`/`pickLine` collapse to ONE line and discard the rest. - Ship: thread `books` (the raw `lines[]`) onto each PropRowProp; `slateAdapter.detectBestBook(rows, side, refLine)` returns `{ book, odds }` ONLY when ≥2 books post the SAME line for the side and prices differ; `buildPlayerStripsFromProps` attaches `bestBook` + `book` to each strip prop; StatStrip renders a subtle signal-green dot (one meaning: best available price). ## 3. Partner ref system - (a) `web/src/lib/partnerRef.js` (CommonJS) — parse `?ref=CODE`, sanitize (A–Z 0–9 - _, ≤32, uppercased), first-party cookie `vyndr_ref` (90d, SameSite=Lax, Path=/), first-touch (never overwrites an existing cookie). `components/vyndr/PartnerRefCapture.tsx` mounted in the layout (GlobalHosts pattern). - (b) `src/routes/partners.js` — `GET /api/partners/report/:code` behind `requireInternalAuth` → `{ signups, conversions, mrr_attributed }` read from `user_profiles` filtered by `partner_ref`. The column does NOT exist yet: the endpoint degrades to zeros + a note, and the TODO migration is documented in docs/PARTNERS.md (NOT run). - (c) Signup: `AuthContext.signUp` forwards the `vyndr_ref` cookie into Supabase signup metadata as `partner_ref`. - (d) `docs/PARTNERS.md` — Stripe promo-code mapping convention + the TODO migration. ## Acceptance criteria - All book links organic by default; flipping `enabled` + params in affiliateConfig produces a tracked URL with the params appended. - Best-price dot never renders on single-book or unequal-line data. - `?ref=CODE` first visit sets `vyndr_ref` for 90 days; signup metadata carries `partner_ref`; report endpoint 401s without the internal key. - Full `npx jest` green; `cd web && npx next build` exit 0. ## Test plan - tests/unit/bookLinks.test.js — organic default, enabled param shapes, aliases, betrivers state, unknown book, rel constant + source assertions. - tests/unit/slateAdapterBestBook.test.js — detectBestBook honesty rules + strip attachment. - tests/unit/partnerRef.test.js — sanitize/parse/cookie/first-touch. - tests/unit/partnersRoute.test.js — report math + internal-auth mount.