0996320bd1
Zero out-of-pocket; everything config-flip-ready but DISABLED/organic. - BOOK IT deep links: web/src/lib/bookLinks.js + affiliateConfig.js (all books enabled:false, Impact/Partnerize param shapes documented, empty params skipped). Wired into StatStrip BookItTeaser (real anchor now) + scan hand-off links. Every book anchor renders rel="sponsored noopener noreferrer" (BOOK_LINK_REL). - Best-price marker: slateAdapter.detectBestBook (only when >=2 books post the SAME line and prices differ — absent beats wrong) + subtle signal-green dot in StatStrip. Slate.groupByGame threads the grouped per-book rows (books[]) onto PropRowProp instead of discarding them. - Partner refs: ?ref=CODE -> vyndr_ref cookie (90d, first-touch, PartnerRefCapture in layout) -> signup metadata partner_ref -> internal GET /api/partners/report/:code (requireInternalAuth; honest zeros + note until the TODO migration in docs/PARTNERS.md adds user_profiles.partner_ref — NOT run). Stripe promo-code convention: partner code == promotion code, verbatim. - Tests: +41 (2398 -> 2439, 209 suites); bookItTeaser + vyndrCoreScreens invariants updated to the new (stronger) rel contract. Web build exit 0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
73 lines
4.0 KiB
Markdown
73 lines
4.0 KiB
Markdown
# 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 `<a>` 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.
|