feat: Feature 3.1 — Landing page + blog + Phase 3 specs
Next.js 14+ web app in web/ directory: - Landing page: Hero, How It Works, Features, 3-tier Pricing with founder badges, Footer with email capture - Blog system: MDX-powered, /blog index + /blog/[slug] pages, reading time, Open Graph tags, JSON-LD structured data - Auth pages: /login + /signup (Supabase Auth ready) - Design system: dark theme, grade colors (A/B/C/D), BetonBLK voice - 1 seed blog post: "How to Read Line Movement Like a Sharp" - Specs for 3.2 (Scan UI), 3.3 (Bet Tracker), 3.4 (Stripe) Build passes clean: 7 static pages generated. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
# Feature 3.1 — Landing Page
|
||||
|
||||
## Overview
|
||||
Next.js marketing site. Hero section, How It Works, 3-tier pricing with founder badges, email capture CTA, and a blog for SEO content. Deployed on Vercel. This is the first thing a visitor sees — it needs to convert. All copy uses BetonBLK voice: direct, confident, no fluff, speaks like a sharp bettor who respects your time.
|
||||
|
||||
## Dependencies
|
||||
- None (static marketing page, no backend calls)
|
||||
- Feature 3.4 (Stripe) links from pricing CTAs
|
||||
|
||||
## Tech
|
||||
- **Framework:** Next.js 14+ (App Router)
|
||||
- **Styling:** Tailwind CSS
|
||||
- **Deployment:** Vercel
|
||||
- **Directory:** `web/` in the betonblk repo (monorepo)
|
||||
|
||||
## Pages
|
||||
|
||||
### / (Home)
|
||||
Single-page scroll with sections:
|
||||
|
||||
**1. Hero**
|
||||
- Headline: "Stop guessing. Start grading."
|
||||
- Subheadline: "BetonBLK scans your parlay in seconds. AI-powered prop analysis across DraftKings, FanDuel, and BetMGM."
|
||||
- CTA button: "Scan Your First Parlay — Free" → links to /scan (Feature 3.2)
|
||||
- Background: dark, clean, sports-betting aesthetic. No stock photos.
|
||||
|
||||
**2. How It Works**
|
||||
3-step visual flow:
|
||||
1. "Paste your parlay" — illustration of leg input
|
||||
2. "Get your grade" — A/B/C/D grade card with color
|
||||
3. "See the edge" — reasoning breakdown preview
|
||||
|
||||
**3. Features Grid**
|
||||
- Prop Analysis: 6-step grading pipeline
|
||||
- Correlation Detection: flags conflicting legs
|
||||
- Line Movement: sharp money alerts
|
||||
- Bet Tracking: ROI and win rate over time
|
||||
- Kill Conditions: 6 hard checks before you bet
|
||||
|
||||
**4. Pricing**
|
||||
3-tier card layout:
|
||||
|
||||
| | Free | Analyst | Desk |
|
||||
|---|---|---|---|
|
||||
| Price | $0 | $19.99/mo | $49.99/mo |
|
||||
| Founder Price | — | $14.99/mo | $34.99/mo |
|
||||
| Scans | 5/month | Unlimited | Unlimited |
|
||||
| Line Movement | View | View + Alerts | View + Alerts + Priority |
|
||||
| Bet Tracking | No | Yes | Yes |
|
||||
| Cascade Alerts | No | Yes | Priority |
|
||||
| Performance Analytics | No | Basic | Full + Patterns |
|
||||
|
||||
Founder badge: "Founder Rate — Locked for Life" visual badge on Analyst/Desk cards.
|
||||
CTA: "Get Started" on Free, "Subscribe" on paid → links to Stripe checkout (Feature 3.4).
|
||||
|
||||
**5. Social Proof / Trust**
|
||||
- "Built by bettors, for bettors"
|
||||
- Number counters: props analyzed, parlays scanned (can be seeded/estimated)
|
||||
|
||||
**6. Footer**
|
||||
- Email capture: "Get early access + founder pricing"
|
||||
- Links: Terms, Privacy, Twitter/X, Discord (placeholder)
|
||||
|
||||
### /login
|
||||
Supabase Auth login page (email + password, or magic link).
|
||||
|
||||
### /signup
|
||||
Supabase Auth signup page → auto-creates user profile (trigger from Feature 1.4).
|
||||
|
||||
### /blog
|
||||
MDX-powered blog for SEO content. Static generation at build time.
|
||||
|
||||
**Content types:**
|
||||
- Betting strategy guides ("How to Read Line Movement Like a Sharp")
|
||||
- Prop analysis breakdowns ("Why PRA Props Are the Most Profitable Market")
|
||||
- Product updates ("New: Cascade Alerts When Your Player Gets Scratched")
|
||||
- Stat explainers ("What Back-to-Back Games Do to Over/Under Props")
|
||||
|
||||
**Implementation:**
|
||||
- MDX files in `content/blog/` directory
|
||||
- `@next/mdx` or `contentlayer` for parsing
|
||||
- Each post: frontmatter (title, date, slug, description, tags) + MDX body
|
||||
- `/blog` index page with post list, sorted by date
|
||||
- `/blog/[slug]` dynamic route for individual posts
|
||||
- SEO: Open Graph tags, JSON-LD structured data, sitemap.xml
|
||||
- Reading time estimate in post header
|
||||
|
||||
**Blog post frontmatter:**
|
||||
```mdx
|
||||
---
|
||||
title: "How to Read Line Movement Like a Sharp"
|
||||
date: "2026-03-22"
|
||||
slug: "line-movement-guide"
|
||||
description: "Line moves tell a story. Here's how to read it."
|
||||
tags: ["strategy", "line-movement", "sharp-money"]
|
||||
---
|
||||
```
|
||||
|
||||
**Blog component structure:**
|
||||
```
|
||||
web/
|
||||
├── app/
|
||||
│ └── blog/
|
||||
│ ├── page.tsx # Blog index
|
||||
│ └── [slug]/page.tsx # Individual post
|
||||
├── content/
|
||||
│ └── blog/
|
||||
│ ├── line-movement-guide.mdx
|
||||
│ └── pra-props-explained.mdx
|
||||
```
|
||||
|
||||
## BetonBLK Voice Guide
|
||||
All user-facing copy follows these rules:
|
||||
- **Direct.** No hedging. "This line is soft" not "This line might be worth considering."
|
||||
- **Confident.** The system did the work. Present findings with authority.
|
||||
- **Concise.** Bettors scan fast. Short sentences. No filler.
|
||||
- **Respectful.** Never condescending. Assume the reader knows the game.
|
||||
- **Honest.** If the edge is thin, say so. A D grade is "no edge — avoid" not "proceed with caution."
|
||||
|
||||
Examples:
|
||||
- Hero: "Stop guessing. Start grading."
|
||||
- Grade A: "Strong edge. Clear play."
|
||||
- Grade D: "No edge. Walk away."
|
||||
- Kill condition: "Back-to-back game. High-minute player. Historically underperforms."
|
||||
- Upgrade pitch: "You've got a good eye. Unlock unlimited scans."
|
||||
|
||||
## Component Structure
|
||||
```
|
||||
web/
|
||||
├── app/
|
||||
│ ├── layout.tsx # Root layout, fonts, metadata
|
||||
│ ├── page.tsx # Landing page (home)
|
||||
│ ├── login/page.tsx # Login
|
||||
│ ├── signup/page.tsx # Signup
|
||||
│ ├── scan/page.tsx # Feature 3.2
|
||||
│ ├── tracker/page.tsx # Feature 3.3
|
||||
│ ├── blog/
|
||||
│ │ ├── page.tsx # Blog index
|
||||
│ │ └── [slug]/page.tsx # Individual post
|
||||
│ └── api/ # Next.js API routes (proxy to backend if needed)
|
||||
├── components/
|
||||
│ ├── Hero.tsx
|
||||
│ ├── HowItWorks.tsx
|
||||
│ ├── Features.tsx
|
||||
│ ├── Pricing.tsx
|
||||
│ ├── Footer.tsx
|
||||
│ ├── GradeCard.tsx # Reusable A/B/C/D grade display
|
||||
│ ├── BlogCard.tsx # Post preview card for blog index
|
||||
│ └── ui/ # Shared UI primitives
|
||||
├── content/
|
||||
│ └── blog/ # MDX blog posts
|
||||
├── lib/
|
||||
│ ├── supabase.ts # Supabase client (browser)
|
||||
│ ├── api.ts # Backend API client
|
||||
│ └── blog.ts # MDX parsing + post listing helpers
|
||||
├── public/
|
||||
│ └── ... # Static assets
|
||||
├── tailwind.config.ts
|
||||
├── next.config.ts
|
||||
└── package.json
|
||||
```
|
||||
|
||||
## Design System
|
||||
- **Colors:** Dark background (#0a0a0a), card backgrounds (#141414), accent green (#22c55e for A grade), yellow (#eab308 for B), orange (#f97316 for C), red (#ef4444 for D)
|
||||
- **Typography:** Inter for body, JetBrains Mono for grades/numbers
|
||||
- **Cards:** Rounded, subtle border, slight glow on hover
|
||||
- **Grade colors:** A=green, B=yellow, C=orange, D=red — consistent across all UI
|
||||
|
||||
## Acceptance Criteria
|
||||
1. Landing page loads with hero, how it works, features, pricing, footer
|
||||
2. Pricing cards show all 3 tiers with correct prices and feature lists
|
||||
3. Founder badges visible on Analyst and Desk cards
|
||||
4. CTA buttons link to correct destinations (scan, checkout)
|
||||
5. Login/signup pages functional with Supabase Auth
|
||||
6. Email capture form in footer collects email (store in Supabase or simple list)
|
||||
7. Blog index at /blog lists posts sorted by date
|
||||
8. Individual blog posts render MDX with correct formatting
|
||||
9. Blog posts include Open Graph tags and JSON-LD structured data
|
||||
10. All copy follows BetonBLK voice (direct, confident, concise)
|
||||
11. Responsive: works on mobile, tablet, desktop
|
||||
12. Deploys to Vercel
|
||||
13. Lighthouse performance score > 90
|
||||
14. Dark theme throughout, grade colors consistent
|
||||
|
||||
## Test Plan
|
||||
- Visual regression: screenshots at 3 breakpoints (mobile/tablet/desktop)
|
||||
- Link verification: all CTAs navigate correctly
|
||||
- Auth flow: signup → login → redirect to /scan
|
||||
- Email capture: submit email → stored
|
||||
- Blog: index page renders, individual post renders MDX
|
||||
- Blog: SEO tags present in page source
|
||||
- Lighthouse audit: performance > 90, accessibility > 85
|
||||
@@ -0,0 +1,149 @@
|
||||
# Feature 3.2 — Scan UI
|
||||
|
||||
## Overview
|
||||
The parlay scanner interface. Users input legs via a manual form builder, submit for analysis, and see color-coded grades with full reasoning. Displays scan count for free tier users, upgrade prompts at the limit.
|
||||
|
||||
## Dependencies
|
||||
- Feature 2.1 — Parlay Scan API (`POST /api/scan/parlay`)
|
||||
- Feature 3.1 — Landing Page (shared layout, auth, design system)
|
||||
|
||||
## Page: /scan
|
||||
|
||||
### Layout
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ Header: BetonBLK logo + nav + scan counter │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─── Leg Builder ────────────────────────┐ │
|
||||
│ │ Player: [___________] (autocomplete) │ │
|
||||
│ │ Stat: [points ▼] Line: [26.5] │ │
|
||||
│ │ Direction: [Over ▼] Book: [DK ▼] │ │
|
||||
│ │ [+ Add Leg] │ │
|
||||
│ └────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ Legs: ┌──┐ ┌──┐ ┌──┐ │
|
||||
│ │L1│ │L2│ │L3│ (removable chips) │
|
||||
│ └──┘ └──┘ └──┘ │
|
||||
│ │
|
||||
│ [🔍 Scan Parlay] │
|
||||
│ │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ Results (after scan): │
|
||||
│ │
|
||||
│ ┌─ Overall ──────────────────────────────┐ │
|
||||
│ │ PARLAY GRADE: [B] Confidence: 68% │ │
|
||||
│ │ Correlations: 1 flag │ │
|
||||
│ └────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─ Leg 1 ────────────────────────────────┐ │
|
||||
│ │ Jokic Over 26.5 pts [A] 85% │ │
|
||||
│ │ Season: 26.3 | Last 10: 28.1 │ │
|
||||
│ │ Home: 27.8 | vs LAL: 30.5 │ │
|
||||
│ │ Kill conditions: none │ │
|
||||
│ └────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─ Leg 2 ────────────────────────────────┐ │
|
||||
│ │ LeBron Over 8.5 reb [C] 52% │ │
|
||||
│ │ ... │ │
|
||||
│ └────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─ Correlations ─────────────────────────┐ │
|
||||
│ │ ⚠ Same game, same team: LeBron + AD │ │
|
||||
│ └────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [📋 Save to Bet Tracker] [🔄 New Scan] │
|
||||
│ │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Components
|
||||
|
||||
**ScanCounter** (header)
|
||||
- Shows "3 of 5 scans used" for free tier
|
||||
- Shows "Unlimited" for paid tiers
|
||||
- Progress bar visual
|
||||
|
||||
**LegBuilder**
|
||||
- Player input with autocomplete (calls GET /players/search on NBA stats service)
|
||||
- Stat type dropdown: points, rebounds, assists, threes, blocks, steals, pra, turnovers
|
||||
- Line: numeric input
|
||||
- Direction: over/under toggle
|
||||
- Book: DraftKings, FanDuel, BetMGM dropdown
|
||||
- "Add Leg" button → adds to legs list (2-12 legs)
|
||||
|
||||
**LegChip**
|
||||
- Compact display: "Jokic O26.5 pts DK"
|
||||
- Remove button (X)
|
||||
- Reorderable (nice-to-have)
|
||||
|
||||
**ScanButton**
|
||||
- Disabled until 2+ legs added
|
||||
- Loading state during API call
|
||||
- Calls `POST /api/scan/parlay` with auth token
|
||||
|
||||
**ResultsPanel**
|
||||
- Overall parlay grade card (large, color-coded)
|
||||
- Individual leg cards with grade, confidence, edge, reasoning summary
|
||||
- Correlation flags section with icons
|
||||
- Kill conditions listed per leg
|
||||
|
||||
**UpgradePitch** (modal or inline)
|
||||
- Appears at scan 5 (from API response `upgrade_pitch`)
|
||||
- Shows personalized hook, insight, CTA
|
||||
- "Subscribe" button → Stripe checkout
|
||||
- "Maybe Later" dismisses
|
||||
|
||||
**GradeCard** (shared component from 3.1)
|
||||
- Letter grade (A/B/C/D) with background color
|
||||
- Confidence percentage
|
||||
- Edge percentage
|
||||
|
||||
### State Management
|
||||
```
|
||||
scanState = {
|
||||
legs: [], // array of leg objects
|
||||
isScanning: false, // loading state
|
||||
results: null, // API response
|
||||
error: null, // error message
|
||||
scanCount: 0, // from user profile
|
||||
scansRemaining: 5, // computed
|
||||
upgradePitch: null, // from API when applicable
|
||||
}
|
||||
```
|
||||
|
||||
### API Integration
|
||||
```
|
||||
1. Player search: GET http://localhost:8000/players/search?name=...
|
||||
→ Autocomplete results (debounced, 300ms)
|
||||
|
||||
2. Scan: POST /api/scan/parlay
|
||||
Headers: { Authorization: Bearer <jwt> }
|
||||
Body: { legs: [...] }
|
||||
→ Full parlay analysis response
|
||||
|
||||
3. Save to tracker: POST /api/bets/quickslip
|
||||
→ Creates bet from scan results
|
||||
```
|
||||
|
||||
## Acceptance Criteria
|
||||
1. Leg builder allows adding 2-12 legs with all required fields
|
||||
2. Player autocomplete searches as user types (debounced)
|
||||
3. Scan button calls API and displays results with grades
|
||||
4. Grade cards show correct colors (A=green, B=yellow, C=orange, D=red)
|
||||
5. Correlation flags displayed with appropriate severity icons
|
||||
6. Kill conditions listed per leg
|
||||
7. Scan counter shows remaining scans for free tier
|
||||
8. Upgrade pitch modal appears at scan 5 with personalized content
|
||||
9. "Save to Bet Tracker" creates a bet via quickslip API
|
||||
10. Error states handled: API down, invalid input, auth expired
|
||||
11. Responsive: works on mobile and desktop
|
||||
12. Loading states during API calls
|
||||
|
||||
## Test Plan
|
||||
- Component tests: LegBuilder adds/removes legs correctly
|
||||
- Component tests: GradeCard renders correct color per grade
|
||||
- Integration: full scan flow → results displayed
|
||||
- Upgrade pitch: appears at correct scan count
|
||||
- Error handling: API error → user-friendly message
|
||||
- Responsive: mobile leg builder usable
|
||||
@@ -0,0 +1,141 @@
|
||||
# Feature 3.3 — Bet Tracker UI
|
||||
|
||||
## Overview
|
||||
Dashboard for tracking bets, viewing performance, and discovering behavioral patterns. Shows win rate, ROI, and at 30+ bets, surfaces a behavioral pattern card with insights.
|
||||
|
||||
## Dependencies
|
||||
- Feature 1.5 — Bet Submission API (`GET /api/bets`, `GET /api/bets/performance`, `PATCH /api/bets/:id/settle`)
|
||||
- Feature 3.1 — Landing Page (shared layout, auth, design system)
|
||||
|
||||
## Page: /tracker
|
||||
|
||||
### Layout
|
||||
```
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ Header: BetonBLK logo + nav │
|
||||
├──────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─ Performance Cards ─────────────────────┐ │
|
||||
│ │ [ROI: +8.3%] [Win Rate: 55%] [Bets: 40]│ │
|
||||
│ │ Period: [Weekly ▼] [Monthly] [All Time] │ │
|
||||
│ └─────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─ Behavioral Pattern Card ───────────────┐ │
|
||||
│ │ (appears at 30+ bets) │ │
|
||||
│ │ "You hit 68% on player points overs │ │
|
||||
│ │ but only 35% on rebounds. Consider │ │
|
||||
│ │ focusing on your edge." │ │
|
||||
│ └─────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─ Quick Submit ──────────────────────────┐ │
|
||||
│ │ [📸 Screenshot] [✏️ Quick Slip] │ │
|
||||
│ └─────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─ Bet History ───────────────────────────┐ │
|
||||
│ │ Filter: [All ▼] [DraftKings ▼] │ │
|
||||
│ │ │ │
|
||||
│ │ ┌─ Bet Row ──────────────────────────┐ │ │
|
||||
│ │ │ Jokic O26.5 pts | DK | $20 │ │ │
|
||||
│ │ │ Status: PENDING [Settle ▶] │ │ │
|
||||
│ │ └───────────────────────────────────┘ │ │
|
||||
│ │ │ │
|
||||
│ │ ┌─ Bet Row ──────────────────────────┐ │ │
|
||||
│ │ │ 3-leg parlay | FD | $10 │ │ │
|
||||
│ │ │ Status: WON ✅ +$81.18 │ │ │
|
||||
│ │ └───────────────────────────────────┘ │ │
|
||||
│ │ │ │
|
||||
│ │ [Load More] │ │
|
||||
│ └─────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Components
|
||||
|
||||
**PerformanceCards**
|
||||
- 3 stat cards: ROI %, Win Rate %, Total Bets
|
||||
- Period toggle: weekly / monthly / all_time
|
||||
- Color: green if positive ROI, red if negative
|
||||
- Calls `GET /api/bets/performance`
|
||||
|
||||
**BehavioralPatternCard**
|
||||
- Only appears when sample_size >= 30
|
||||
- Analyzes bet history to find:
|
||||
- Best stat type (highest win rate)
|
||||
- Worst stat type (lowest win rate)
|
||||
- Best book (highest ROI)
|
||||
- Streak data (current win/loss streak)
|
||||
- Computed client-side from bet history data
|
||||
- Card with insight text + recommendation
|
||||
|
||||
**QuickSubmitBar**
|
||||
- Two buttons: Screenshot upload, Quick Slip form
|
||||
- Screenshot: opens file picker → calls `POST /api/bets/screenshot` → confirm modal
|
||||
- Quick Slip: opens inline form → calls `POST /api/bets/quickslip`
|
||||
|
||||
**BetHistory**
|
||||
- Paginated list of bets from `GET /api/bets`
|
||||
- Filters: status (all/pending/won/lost), book
|
||||
- Each row shows: legs summary, book, amount, status, profit/loss
|
||||
- Pending bets have a "Settle" button → opens settle modal
|
||||
|
||||
**SettleModal**
|
||||
- Select outcome: Won / Lost / Push / Void
|
||||
- For each leg: actual value input (optional, for detailed tracking)
|
||||
- Submit → calls `PATCH /api/bets/:id/settle`
|
||||
- On success: refreshes performance cards + bet row
|
||||
|
||||
**BetDetailExpander**
|
||||
- Click a bet row to expand full details:
|
||||
- All legs with odds
|
||||
- Linked scan session grade (if any)
|
||||
- Placed at / Settled at timestamps
|
||||
- Profit calculation breakdown
|
||||
|
||||
### State
|
||||
```
|
||||
trackerState = {
|
||||
performance: { weekly: {}, monthly: {}, all_time: {} },
|
||||
selectedPeriod: 'monthly',
|
||||
bets: [],
|
||||
total: 0,
|
||||
filters: { status: null, book: null },
|
||||
page: 0,
|
||||
loading: false,
|
||||
patternCard: null, // computed when bets.length >= 30
|
||||
}
|
||||
```
|
||||
|
||||
### Behavioral Pattern Analysis (Client-Side)
|
||||
```
|
||||
When bets.length >= 30 AND settled bets >= 30:
|
||||
1. Group bets by stat_type from slip_data.legs
|
||||
2. For each stat_type: compute win_rate
|
||||
3. Best = highest win_rate (min 5 bets in category)
|
||||
4. Worst = lowest win_rate (min 5 bets in category)
|
||||
5. Generate insight text:
|
||||
"You hit {best_rate}% on {best_stat} {best_dir}s
|
||||
but only {worst_rate}% on {worst_stat}. Consider
|
||||
focusing on your edge."
|
||||
```
|
||||
|
||||
## Acceptance Criteria
|
||||
1. Performance cards show ROI, win rate, and bet count for selected period
|
||||
2. Period toggle switches between weekly/monthly/all_time
|
||||
3. Behavioral pattern card appears at 30+ settled bets with personalized insight
|
||||
4. Quick submit bar opens screenshot or quickslip flow
|
||||
5. Bet history loads with pagination (20 per page)
|
||||
6. Status and book filters work correctly
|
||||
7. Settle modal updates bet status and refreshes performance
|
||||
8. Bet detail expander shows full leg breakdown
|
||||
9. Linked scan session grade shown when available
|
||||
10. Positive ROI shown in green, negative in red
|
||||
11. Responsive: works on mobile and desktop
|
||||
|
||||
## Test Plan
|
||||
- Component tests: PerformanceCards renders correct values
|
||||
- Component tests: BetHistory filters and paginates
|
||||
- Component tests: SettleModal sends correct API call
|
||||
- Integration: submit bet → appears in history → settle → performance updates
|
||||
- Pattern card: appears at 30+ bets with correct insight
|
||||
- Responsive: mobile layout usable
|
||||
@@ -0,0 +1,165 @@
|
||||
# Feature 3.4 — Stripe Integration
|
||||
|
||||
## Overview
|
||||
Subscription billing for Analyst ($19.99/mo) and Desk ($49.99/mo) tiers. Founder codes lock a permanent discounted rate. Handles checkout, subscription management, webhooks for tier changes, and cancellation.
|
||||
|
||||
## Dependencies
|
||||
- Feature 1.4 — Database Schema (`users.tier`, `users.stripe_customer_id`, `users.founder_status`)
|
||||
- Feature 3.1 — Landing Page (pricing cards link to checkout)
|
||||
|
||||
## Stripe Products
|
||||
|
||||
| Product | Price ID (create in Stripe) | Monthly | Founder Monthly |
|
||||
|---|---|---|---|
|
||||
| Analyst | price_analyst_monthly | $19.99 | $14.99 |
|
||||
| Analyst Founder | price_analyst_founder | $14.99 | — |
|
||||
| Desk | price_desk_monthly | $49.99 | $34.99 |
|
||||
| Desk Founder | price_desk_founder | $34.99 | — |
|
||||
|
||||
Founder pricing: separate Stripe Price objects with `metadata.is_founder: true`. Once subscribed to a founder price, it's locked — no price increase even if founder pricing ends.
|
||||
|
||||
## Endpoints
|
||||
|
||||
### POST /api/stripe/checkout
|
||||
Creates a Stripe Checkout session. Redirects user to Stripe's hosted checkout.
|
||||
|
||||
**Request body:**
|
||||
```json
|
||||
{
|
||||
"tier": "analyst",
|
||||
"founder_code": "FOUNDER2026"
|
||||
}
|
||||
```
|
||||
|
||||
**Response (200):**
|
||||
```json
|
||||
{
|
||||
"checkout_url": "https://checkout.stripe.com/c/pay/cs_...",
|
||||
"session_id": "cs_..."
|
||||
}
|
||||
```
|
||||
|
||||
**Logic:**
|
||||
1. Look up user from auth token
|
||||
2. If user already has a Stripe customer ID, use it. Otherwise create one.
|
||||
3. Determine price ID:
|
||||
- If `founder_code` is valid → use founder price
|
||||
- Otherwise → use standard price
|
||||
4. Create Stripe Checkout Session with:
|
||||
- `customer`: Stripe customer ID
|
||||
- `line_items`: [{ price: price_id, quantity: 1 }]
|
||||
- `mode`: 'subscription'
|
||||
- `success_url`: `{BASE_URL}/scan?upgraded=true`
|
||||
- `cancel_url`: `{BASE_URL}/pricing`
|
||||
- `metadata`: { user_id, tier, is_founder }
|
||||
5. Return checkout URL
|
||||
|
||||
### POST /api/stripe/webhook
|
||||
Stripe webhook endpoint. Handles subscription lifecycle events.
|
||||
|
||||
**Events handled:**
|
||||
| Event | Action |
|
||||
|---|---|
|
||||
| `checkout.session.completed` | Set user tier + stripe_customer_id + founder_status |
|
||||
| `customer.subscription.updated` | Update tier if plan changed |
|
||||
| `customer.subscription.deleted` | Revert user to free tier |
|
||||
| `invoice.payment_failed` | Log warning, Stripe handles retry |
|
||||
|
||||
**Webhook verification:** Validate `stripe-signature` header using webhook signing secret.
|
||||
|
||||
### POST /api/stripe/portal
|
||||
Creates a Stripe Customer Portal session for subscription management (cancel, update payment, view invoices).
|
||||
|
||||
**Response (200):**
|
||||
```json
|
||||
{
|
||||
"portal_url": "https://billing.stripe.com/p/session/..."
|
||||
}
|
||||
```
|
||||
|
||||
### GET /api/stripe/status
|
||||
Returns current subscription status for the authenticated user.
|
||||
|
||||
**Response (200):**
|
||||
```json
|
||||
{
|
||||
"tier": "analyst",
|
||||
"is_founder": true,
|
||||
"subscription_status": "active",
|
||||
"current_period_end": "2026-04-21T00:00:00Z",
|
||||
"cancel_at_period_end": false
|
||||
}
|
||||
```
|
||||
|
||||
## Founder Code System
|
||||
|
||||
Simple validation — not a full promo code engine:
|
||||
```
|
||||
VALID_FOUNDER_CODES = ['FOUNDER2026', 'BETONBLK', 'EARLYBIRD']
|
||||
```
|
||||
|
||||
Stored in environment variable: `FOUNDER_CODES=FOUNDER2026,BETONBLK,EARLYBIRD`
|
||||
|
||||
When a valid founder code is used:
|
||||
1. Checkout uses the founder price ID
|
||||
2. `users.founder_status` set to `true`
|
||||
3. Founder badge displayed in UI
|
||||
|
||||
Founder codes have an expiry date (env var `FOUNDER_CODE_EXPIRY`). After expiry, new subscriptions use standard pricing, but existing founder subscribers keep their rate.
|
||||
|
||||
## Webhook Security
|
||||
- Stripe sends signature in `stripe-signature` header
|
||||
- Verify using `stripe.webhooks.constructEvent(body, sig, webhook_secret)`
|
||||
- Raw body required (not parsed JSON) — needs `express.raw()` middleware on webhook route
|
||||
- Webhook secret stored in env: `STRIPE_WEBHOOK_SECRET`
|
||||
|
||||
## Service Architecture
|
||||
```
|
||||
src/
|
||||
├── services/
|
||||
│ └── stripeService.js # Checkout, portal, webhook handling, tier updates
|
||||
├── routes/
|
||||
│ └── stripe.js # POST /checkout, POST /webhook, POST /portal, GET /status
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
```
|
||||
STRIPE_SECRET_KEY=sk_...
|
||||
STRIPE_WEBHOOK_SECRET=whsec_...
|
||||
FOUNDER_CODES=FOUNDER2026,BETONBLK,EARLYBIRD
|
||||
FOUNDER_CODE_EXPIRY=2026-06-30
|
||||
BASE_URL=https://betonblk.com
|
||||
```
|
||||
|
||||
## Acceptance Criteria
|
||||
1. `POST /api/stripe/checkout` creates a Stripe Checkout session and returns URL
|
||||
2. Founder code applies founder pricing when valid
|
||||
3. Invalid/expired founder code falls back to standard pricing
|
||||
4. Webhook `checkout.session.completed` updates user tier and stripe_customer_id
|
||||
5. Webhook `customer.subscription.deleted` reverts user to free tier
|
||||
6. Webhook signature verification rejects invalid signatures
|
||||
7. `POST /api/stripe/portal` returns Stripe Customer Portal URL
|
||||
8. `GET /api/stripe/status` returns current subscription info
|
||||
9. Founder subscribers keep their rate permanently (locked price in Stripe)
|
||||
10. All Stripe operations use the service role Supabase client (bypasses RLS)
|
||||
|
||||
## Test Plan
|
||||
|
||||
### Unit Tests (stripeService.js)
|
||||
- Correct price ID selected for analyst + founder code
|
||||
- Correct price ID selected for desk without founder code
|
||||
- Expired founder code falls back to standard
|
||||
- Tier mapping: checkout metadata → correct tier string
|
||||
- Webhook event parsing: checkout.session.completed → tier update
|
||||
|
||||
### Integration Tests
|
||||
- Checkout flow: request → returns valid URL
|
||||
- Webhook: mock checkout.session.completed → user tier updated in DB
|
||||
- Webhook: mock subscription.deleted → user reverted to free
|
||||
- Webhook: invalid signature → 400
|
||||
- Portal: returns URL for existing customer
|
||||
- Status: returns correct tier and subscription info
|
||||
- Auth required on checkout/portal/status (not on webhook)
|
||||
|
||||
## Open Questions
|
||||
- **Stripe test mode:** Build and test entirely in Stripe test mode. Switch to live keys at launch. No code changes needed — just env var swap.
|
||||
Reference in New Issue
Block a user