Custom Mobile-frist Ad-tech MVP development by Daniel Afaqi Custom Mobile-frist Ad-tech MVP development by Daniel Afaqi

Custom Mobile-frist Ad-tech MVP development

Daniel Afaqi

Daniel Afaqi

Verified

Thrilla

A pay-per-verified-attention video advertising platform.
Advertisers stop paying for bots and passive views. Viewers get rewarded for genuine engagement. I built the whole stack — from the TikTok-style video feed to the fraud detection pipeline to the atomic per-view settlement engine — solo.

The Problem

Digital advertising is built on a broken premise: advertisers pay for impressions and clicks, but neither proves a human paid attention.
- Bots inflate the numbers. A meaningful share of every ad budget is spent on traffic that was never a real person.
- "View" is a fuzzy word. A half-second autoplay in a muted background tab counts the same as a fully-watched ad.
- Brands have no way to verify engagement. There's no checkpoint that says "this person actually saw this and processed it."
Thrilla is an attempt to fix that with a simple idea: don't charge for the view — charge only when the viewer proves they were paying attention.

What Thrilla Does

For viewers. You scroll through a TikTok-style vertical feed of short video ads. Each video is locked and non-skippable. When it ends, a quick interactive quiz about the content appears. Answer correctly and real money lands in your wallet. Skip or get it wrong, and nothing happens — neither you nor the advertiser is charged.
For advertisers. Upload a video, set a budget, write a quiz, hit publish. You're charged only when a verified view completes. Bots and passive viewers can't drain your budget. Each verified view splits a small platform fee between the viewer and Thrilla.
Cash out. Viewers connect their bank through Stripe Connect and withdraw their balance. Advertisers top up their account through Stripe Checkout.

Core Features

For viewers
- Mobile-first vertical swipe feed with rubber-band physics and network-adaptive video preloading
- Locked, non-skippable HLS playback via Cloudflare Stream
- Quiz challenge that unlocks the reward when answered correctly
- Daily cap (2 verified views per UTC day) to prevent reward farming
- Wallet balance, earnings history with daily charts, and one-click withdrawals via Stripe Connect
- Likes, view tracking, and milestone reward emails (first reward, £5, £10, £25…)
For advertisers
- Two verification paths at onboarding: registered business or influencer/creator — each with document uploads and admin review
- Campaign creation wizard: video upload via resumable TUS protocol straight to Cloudflare Stream, custom thumbnail, quiz builder, social links, budget, and audience targeting
- Auto-pause when the budget runs dry, auto-complete when the view cap is hit — no manual babysitting
- Stripe Checkout deposits, full transaction history, downloadable invoice receipts
- Analytics dashboard showing campaign performance and revenue trends
For admins
- User management with role-aware filtering and pagination
- Advertiser verification review queue (approve / reject / revoke)
- Support ticketing system with assignment and status tracking
- Editable platform-wide config (per-view price, viewer reward, platform fee)
- Audit emails for fraud alerts and config changes

Tech Stack

Frontend
- Next.js 16 (App Router) + React 19 — TypeScript end-to-end
- Tailwind CSS v4 + shadcn/ui built on Radix primitives
- TanStack React Query for all client-side async data
- React Hook Form + Zod for validated, type-safe forms
- @use-gesture/react for the swipe feed
- Video.js with HLS playback and 1080p quality capping
- Recharts for analytics dashboards
- FingerprintJS for device identification
Backend & data
- Supabase (Postgres + Auth + Storage + RLS) — 40 migrations, row-level security on every table, role-based policies
- Postgres RPCs for the operations that need atomicity — money settlement verify_and_settle_view) and feed serving get_active_feed_videos)
- 10 API route groups (advertiser, viewer, admin, videos, sessions, link-clicks, webhooks, auth, jobs, send-email)
- Server-side route protection via role-aware wrappers withAuth, withRole, withMinimumRole)
- Three-tier RBAC: admin > advertiser > viewer
Payments
- Stripe Checkout + Webhooks for advertiser deposits
- Stripe Connect for viewer payouts
### Video pipeline
- Cloudflare Stream for HLS delivery
- TUS resumable uploads tus-js-client) from the browser directly to Cloudflare Stream
Email
- Nodemailer over SMTP
- 24 templated, preference-aware notification types across auth, viewer, advertiser, and admin flows — sorted into priority tiers so transactional mail always sends and "nice-to-have" mail respects user toggles
### Infra & tooling
- Vercel (Fluid Compute, Vercel Analytics)
- ESLint, TypeScript strict mode

Architecture Highlights

Three pieces of this build that I'm especially proud of, explained plainly:
1. Atomic per-view settlement
Every time a viewer answers a quiz correctly, a long list of things needs to happen: mark the view verified, credit the viewer's wallet, record the platform fee, decrement the campaign's remaining budget, and auto-pause or auto-complete the campaign if it has just run out. Doing those as separate database writes is how money goes missing.
I collapsed all of it into one Postgres function — verify_and_settle_view — that runs inside a single transaction. Either everything happens, or nothing does. It uses a FOR UPDATE row lock so two concurrent verified views can't race on the same budget, and a partial unique index so the same user cannot be paid twice for the same video — even if the application layer has a bug.
2. Anonymous-session + fraud pipeline
Visitors get a fingerprint-based anonymous session the moment they land — before signup. That lets me rate-limit, detect duplicates, and track fraud signals from the very first video they watch.
The fraud pipeline scores each session 0–100 across canvas, WebGL, audio, and hardware fingerprints, plus IP duplication, VPN detection, and automation heuristics. Sessions scoring 80+ are blocked outright. When a guest eventually signs up, their session is linked to their new account so their behavior history (and any flags) transfers cleanly.
3. Smart feed RPC with verified-view handling
The video feed isn't a simple "give me active campaigns" query. It's a SECURITY DEFINER Postgres function that runs as the current user (or anonymous), filters to active campaigns, flags each video with already_verified for that viewer, and sorts verified videos to the bottom rather than hiding them. That way an engaged user never sees an empty feed, but the client knows to skip the quiz overlay on videos they've already earned from — they can keep watching, just not double-earn.
---

Challenges I Worked Through

Getting the money flow right. Early versions of the settlement function debited the advertiser's credit balance and inserted a deduction transaction on every verified view — double-counting the same spend, because the advertiser had already paid the full budget at publish time. I redesigned the model so the advertiser is debited once at publish (budget locked into the campaign), and verified views only decrement that campaign's remaining budget. Three migrations and one enum-cast bug later, every settlement is one atomic transaction with no silent failure modes — including a switch from UPDATE to INSERT ... ON CONFLICT on the platform-revenue row so revenue accounting can never quietly no-op.
Anonymous users inside a row-level-security world. Letting non-logged-in viewers watch and verify videos while keeping RLS (row-level security at the database) strict on every table took careful use of SECURITY DEFINER Postgres functions and a fingerprint-keyed anonymous-session table that bridges to a real user on signup. The result: guests can use the product fully, but the database never relaxes its access rules.
Mobile feed performance. Vertical swipe physics that don't feel janky required hand-tuning preload windows by network quality (3-slot preload on fast connections, 2-slot on slow), debouncing buffering states so brief network blips don't flash a spinner, and capping Video.js to 1080p so older phones don't thermally throttle mid-ad.
Email without becoming a spam machine. 24 notification types across 4 user roles, sorted into priority tiers (P0 transactional → P3 nice-to-have), with per-category preference toggles stored as JSON on each profile. Every send is wrapped in a fire-and-forget async block so a failed email can never block a successful payment or campaign action.
---
Roadmap
The product is live but still growing. Pieces in progress:
- Swipe and tap verification challenges — quiz is shipped; the other two challenge types are designed and waiting on UI work.
- Weekly digest emails for advertisers and viewers (the dispatch infrastructure is in place; the cron and templates are not).
- Expanded analytics dashboards — the data and APIs exist, but the visualization surface is intentionally minimal right now.
- Notification log table for deduplication and a full audit trail.
---

What I Learned Building This Solo

Owning Thrilla end-to-end meant designing the data model, writing the SQL, building the React app, wiring up Stripe both ways (Checkout and Connect), shipping a TikTok-style mobile feed, designing a fraud pipeline, and operating it all on Vercel and Supabase. Going from "no spec, just an idea" to a working money-handling product taught me that the boring parts — transactional boundaries, RLS, idempotency, error envelopes, preference-aware messaging — are what makes a product trustworthy. Happy to walk through any layer in detail.
Like this project

What the client had to say

Daniel is an amazing developer, he is an expert in his profession. He built my entire platform from start to finish proving that he is skilled in a variety of areas whilst also being hard working and responsive. Got an idea? Pick Daniel

Ryan Sloss, New Realm

May 16, 2026, Client

Posted Apr 9, 2026

Built a Saas ad-tech product where users can watch ads to earn and advertiser get verified viewership against their ads

Likes

0

Views

6

Timeline

Jan 28, 2026 - May 16, 2026

Clients

New Realm