Adalytics — Stop The Waste.

Creative intelligence for mobile advertisers. Stop wasting budget on ads that stopped working.


Inspiration

Every day, mobile advertisers are making expensive decisions in the dark.

You're running hundreds of ads across dozens of campaigns. Some are working. Some are bleeding budget. Most sit somewhere in between — quietly underperforming while you're too buried in spreadsheets to notice. By the time you realise an ad has stopped working, you've already wasted weeks of spend on something audiences have long since tuned out.

We'd seen this problem framed as a data challenge. But sitting with the Smadex dataset for the first time, we realised it was actually a communication challenge. The data wasn't missing — it was there, all 192,000 rows of it. What was missing was a way to read it that didn't require a data scientist and three hours of Python. Creative Directors can't act on a CSV. Media buyers can't explain a CTR decay curve to a brand team.

We built Adalytics to close that gap. Not just another dashboard — a system that reads the visual DNA of every ad, connects it to performance, and speaks in plain language to the people who need to make the call.


What It Does

Pick a company. Pick a campaign. Instantly understand what's happening to your creative.

Every ad in Adalytics gets:

  • A performance score — a single composite number derived from CTR, CVR, ROAS, and spend efficiency, normalised per KPI goal so campaigns are always compared fairly
  • A statusTOP PERFORMER, STABLE, FATIGUING, or UNDERPERFORMER
  • An AI-generated visual analysis — GPT-4o reads the actual creative image and explains why it's working: Is it the motion? The faces? The discount badge? The emotional tone?
  • A fatigue timeline — day-by-day CTR and CVR trends from launch to today, with the exact moment an ad starts falling off marked and quantified
  • A concrete recommendation — Scale, Monitor, Pivot, or Pause — with reasoning and estimated spend saved

Beyond individual creatives, the Cluster Map runs a PCA on six visual feature scores across all 1,080 creatives and plots them in 2D. Click a cluster and GPT-4o labels it: "High-motion, low-clutter gaming creatives — 72% are top performers." That's the intelligence that informs your next brief.

And if you'd rather just ask? Jordi, our ElevenLabs voice AI advisor, has real-time access to the full dataset. Ask out loud: "Which creatives are fatiguing the fastest?" and get an answer in seconds.


How We Built It

The Stack

Layer Technology
Frontend React 19 + Vite + TailwindCSS v4 + TanStack Router
Database Supabase (PostgreSQL)
Asset Storage Supabase Storage (1,080 PNG creatives)
AI — Vision + Text OpenAI GPT-4o via Supabase Edge Functions
AI — Voice ElevenLabs Conversational AI
Data Pipeline Node.js + csv-parse → Supabase service role
Deployment Vercel

The Data Pipeline

The dataset arrived as six CSVs totalling ~192,000 rows and 1,080 PNG assets. We wrote a Node.js import script to ingest everything into Supabase PostgreSQL over the service role API, then uploaded all assets to a public Supabase Storage bucket. The join structure is clean:

$$ \text{advertisers} \xrightarrow{\text{advertiser_id}} \text{campaigns} \xrightarrow{\text{campaign_id}} \text{creatives} \xrightarrow{\text{creative_id}} \text{creative_daily_stats} $$

The Performance Score

Each campaign optimises for a different KPI — CPA, ROAS, IPM, or CTR — so raw cross-campaign comparison is meaningless. We normalise within KPI cohorts using min-max scaling before computing the composite perf_score:

$$ \hat{m}_i = \frac{m_i - \min(m)}{\max(m) - \min(m)} $$

The dataset's pre-engineered perf_score already encodes this logic, so we use it directly as our primary ranking signal rather than recomputing.

Fatigue Detection

The fatigue signal lives in ctr_decay_pct — the relative CTR drop between the first and last 7 days of a creative's life. Fatigued creatives in the dataset consistently show:

$$ \text{ctr_decay_pct} \in [-0.88,\ -0.78] $$

meaning CTR drops 78–88% from early life to end of life. We plot this as a timeline using days_since_launch as the x-axis, with a vertical marker at fatigue_day and a shaded region showing post-fatigue wasted spend.

The AI Layer

GPT-4o Vision runs server-side inside a Supabase Edge Function — the API key never touches the browser. Results are cached in a creative_ai_cache table (keyed by creative_id) so the same creative is never analysed twice. The prompt instructs the model to return structured JSON:

{
  "visual_summary": "...",
  "why_it_works_or_fails": "...",
  "key_signals": ["...", "..."],
  "fatigue_risk": "low | medium | high",
  "creative_grade": "A"
}

This lets us render each field independently in the UI rather than dumping a wall of text.

View-Through Attribution

We built a live attribution layer on top of the dataset using first-party localStorage — no third-party cookies, fully privacy-compliant. On impression, we write to Supabase's impression_log table. On conversion, we look up prior exposure and tag the conversion as view_through or click_through. Supabase Realtime surfaces the live counter in the Dashboard.

Jordi — The Voice Advisor

Jordi is an ElevenLabs Conversational AI agent with six client-side tools that query Supabase directly from the browser. A Supabase Edge Function issues a signed URL per session so the ElevenLabs API key stays server-side. The tools are:

Tool What it does
get_fatigued_creatives Top fatiguing creatives by spend
get_top_performers Best creatives by perf score
get_vertical_summary Aggregate metrics by vertical
get_wasted_spend Post-fatigue spend breakdown
get_creative_recommendation AI action card for a specific creative
get_creatives Filtered creative list by status + vertical

Challenges We Faced

Cross-campaign KPI normalisation. Each campaign targets a different KPI. Comparing a gaming campaign (optimised for IPM) against a fintech campaign (optimised for ROAS) using raw numbers produces garbage rankings. It took longer than expected to build the mental model for when to filter, when to group, and when to normalise.

The AI cache invalidation question. GPT-4o vision calls are expensive and slow (~3–5s per creative). We needed every creative to feel instant on second load without serving stale data forever. We settled on a simple generated_at timestamp and refresh logic — not perfect, but good enough for a hackathon dataset that doesn't change.

Wiring ElevenLabs inside React. The @elevenlabs/react SDK requires useConversation to run inside a ConversationProvider. Our first attempt called the hook at the top level of the component, which crashed silently. The fix — wrapping the outer component in the provider and extracting the inner logic into a child — took an embarrassingly long time to diagnose.

Visual scores as ML features vs UI labels. The six numeric scores (text_density, readability_score, etc.) are powerful ML features but confusing to display raw. A clutter_score of 0.74 means nothing to a creative director. We spent time writing contextual labels and tooltips that translate the numbers into plain language without losing precision.

Supabase Storage + CORS. Getting the 1,080 PNG assets to render in the browser from Supabase Storage required the bucket to be public and the correct CORS policy on the Supabase project. A missing header cost us a full hour of debugging a problem that looked like a React rendering issue.


What We Learned

  • Data quality beats model complexity. The dataset's pre-computed visual scores (novelty_score, clutter_score, etc.) turned out to be more immediately useful than running our own CV model. Clean, well-documented features beat a black box every time.
  • Voice UI has a much higher activation bar than we expected. Getting Jordi to feel useful rather than gimmicky required careful system prompt design, strict tool descriptions, and making sure the agent always calls a tool before speaking — never invents numbers.
  • Server-side AI is non-negotiable for production. Keeping OpenAI and ElevenLabs keys inside Supabase Edge Functions from day one saved us from a security incident later. The architecture cost 30 minutes to set up and protected us for the rest of the hackathon.
  • PCA for creative clustering is surprisingly legible. We expected the cluster map to need a lot of hand-holding to interpret. In practice, the six visual scores cluster naturally — gaming creatives with high motion and low clutter form a tight group that's visually obvious on the scatter plot without any labelling.

What's Next

  • Real-time fatigue alerts — Supabase Realtime watching creative_daily_stats for creatives whose 7-day rolling CTR drops below a configurable threshold, with push notifications to a Slack channel
  • Variant generator — give GPT-4o the current creative's visual scores and ask it to suggest a new creative brief targeting the opposite fatigue-risk profile
  • Actual CV pipeline — replace the synthetic visual scores with a real vision model (CLIP embeddings + custom classifiers) so the system works on any uploaded creative, not just the synthetic dataset
  • Advertiser-level benchmarking — vertical-normalised percentile rankings so an advertiser can see not just their own performance but where they sit in the market

Built With

React · Vite · TailwindCSS · TanStack Router · Supabase · PostgreSQL · OpenAI GPT-4o · ElevenLabs · Recharts · Radix UI · TypeScript · Vercel

Built With

Share this project:

Updates