B2C Landing Page & Freemium Monetization¶
Kwilo's public B2C surface: a marketing landing page plus a freemium chat experience for individual users (learners prepping for exams or seminars, freelance trainers) who arrive without an institution. They try the core product (AI chat with curriculum-aligned PPT generation, exam prep, deep research) for free, then convert to paid when they want to download artifacts or exceed usage limits. The free tier is the product demo: generate the artifact, show the preview, paywall the download.
Pricing is two-tier (Free + Pro ₹399/mo). The canonical source is b2c-two-tier-pricing.md. Free-tier mechanics (pooled monthly cap, watermarked downloads, locked Study/Lesson Plan) are owned there.
How it works¶
The two apps split by subdomain, sharing @kwilo/ui:
apps/site→kwilo.ai: public marketing, prerendered static HTML for SEO (scripts/prerender.mjs), dep-lean (~250KB gzipped), no TanStack Query or Zustand.apps/web→app.kwilo.ai: authenticated product SPA.apps/backend→api.kwilo.ai.
The same FastAPI backend serves B2B and B2C. B2C users are not a separate backend: they live in one shared "Individual Kwilo" org (individual institution type) with the b2c_user role. Quota and ownership are per-user, so row-level permission checks still work. Subscription tier (free / pro) is a separate field on User, not a role: roles define permissions, tier defines limits.
User journey:
Search / word-of-mouth / social
→ Landing page (kwilo.ai)
→ Signup (Phone | Google | Email)
→ Free tier: unlimited chat + metered artifact generation (preview only)
→ Hits cap or wants a download → PaywallModal
→ Pro ₹399/mo: unlimited generation, PPTX/DOCX/PDF downloads, priority, Study/Lesson Plan unlocked
Generation works on free; the download is gated. The user sees value (the preview) before being asked to pay for the file.
Auth handoff¶
Signup or login on kwilo.ai (email, Google, or phone) sets kwilo_access and kwilo_refresh as HttpOnly; Secure; SameSite=Lax; Domain=.kwilo.ai cookies, then redirects to app.kwilo.ai/<intent-path>. The browser sends the cookies on the next load, so apps/web sees an authenticated user without the frontend ever touching a token. SameSite=Lax is sufficient because both hosts are same-site at the parent-domain boundary, so no separate CSRF token is needed. Dev uses COOKIE_DOMAIN=.kwilo.local.
Signup methods¶
Tabs on /signup: Phone | Google | Email. Phone is the default because most visitors are in India.
| Method | Verification | Backend endpoints |
|---|---|---|
| Phone | MSG91 SMS OTP (6-digit) | /auth/phone-otp/send + /auth/phone-otp/verify |
trust the email_verified claim |
/auth/google/authorize + /auth/google/callback |
|
| register, then email-OTP verify before tokens are valid | /auth/register-individual + /auth/email-otp/send + /auth/email-otp/verify |
Email uses OTP (not a verification link) to keep all three paths consistent at a 6-digit code.
Fraud detection (soft-flag only)¶
At signup, apps/web computes a device fingerprint hash client-side (@fingerprintjs/fingerprintjs visitor ID, SHA-256) and posts device_fingerprint_hash; the backend stores it on the User record. If the same hash appears on 3 or more accounts, it logs auth.duplicate_fingerprint. GET /api/v1/admin/fraud/duplicate-fingerprints?hash= lists accounts sharing a fingerprint. There is no hard block: false positives (shared machines, college networks, cybercafes) outnumber real fraud, so admins review flagged clusters manually. IP-based clustering is avoided because Indian users sit heavily behind shared NAT.
Where it lives¶
- Marketing:
apps/site/src/pages/Individuals/(Hero, PersonaToggle, TrialInput, etc.),apps/site/src/lib/attribution.ts(dual-touch UTM capture),apps/site/scripts/prerender.mjs. - Product:
apps/web/src/pages/Signup.tsx(Phone/Google/Email tabs),apps/web/src/features/signup/intent.ts. - Backend:
apps/backend/src/api/v1/auth.py,apps/backend/src/services/quota.py,apps/backend/src/services/paywall.py,apps/backend/src/models/user.py.