karthik.dev
Back to blogAI Engineering

AdIntel AI: Building an AI Campaign Intelligence Platform for Media Buyers

2026-07-08 · 5 min read

The Media Buyer's Problem

A performance media buyer managing campaigns across Meta, Google, TikTok, and Taboola spends most of their analysis time on a tedious task: logging into four dashboards, exporting CSVs, copying numbers into a spreadsheet, and then trying to figure out why a campaign is underperforming.

The "why" is the hard part. Platform dashboards show what happened (ROAS dropped 23%), but they don't explain why (targeting overlap with another campaign? creative fatigue? bid strategy change? time-of-day shift in audience behavior?).

AdIntel AI automates data consolidation and then uses AI to answer "why."

The Data Ingestion Layer

Each ad platform has a different API, a different data model, and different metric names. The ingestion layer normalizes them into one schema:

interface NormalizedCampaignMetrics {
  campaignId: string;
  platform: "meta" | "google" | "tiktok" | "taboola";
  date: Date;
  spend: number;           // USD
  impressions: number;
  clicks: number;
  conversions: number;
  revenue: number;         // attributed
  ctr: number;             // clicks / impressions
  cpc: number;             // spend / clicks
  cpa: number;             // spend / conversions
  roas: number;            // revenue / spend
  rawMetrics: Record<string, unknown>;  // platform-specific extras
}

Each platform connector maps its native metrics to this schema:

// src/connectors/meta.ts
export function normalizeMetaInsights(insights: MetaInsightsResponse): NormalizedCampaignMetrics[] {
  return insights.data.map(insight => ({
    campaignId: insight.campaign_id,
    platform: "meta",
    date: new Date(insight.date_start),
    spend: parseFloat(insight.spend),
    impressions: parseInt(insight.impressions),
    clicks: parseInt(insight.clicks),
    conversions: insight.actions?.find(a => a.action_type === "purchase")?.value ?? 0,
    revenue: parseFloat(insight.action_values?.find(a => a.action_type === "purchase")?.value ?? "0"),
    // ... computed metrics
    rawMetrics: insight,
  }));
}

The challenge: Meta calls them "actions," Google calls them "conversions," TikTok calls them "total_purchase." The normalization layer is the most brittle part of the system.

Anomaly Detection

Before calling AI, I run a statistical anomaly detector to surface significant changes:

function detectAnomalies(
  current: NormalizedCampaignMetrics,
  baseline: NormalizedCampaignMetrics[],
): Anomaly[] {
  const baselineRoas = mean(baseline.map(m => m.roas));
  const baselineStdDev = standardDeviation(baseline.map(m => m.roas));
  
  const anomalies: Anomaly[] = [];
  
  // ROAS z-score
  const roasZScore = (current.roas - baselineRoas) / baselineStdDev;
  if (Math.abs(roasZScore) > 2) {
    anomalies.push({
      metric: "roas",
      current: current.roas,
      baseline: baselineRoas,
      direction: roasZScore > 0 ? "up" : "down",
      magnitude: Math.abs(roasZScore),
    });
  }
  
  // Repeat for spend, CTR, CPA, impressions
  return anomalies;
}

Only anomalies with z-score > 2 (>2 standard deviations from baseline) are escalated to the AI layer. This keeps AI API costs low and ensures the AI only analyzes genuinely significant changes.

AI Root-Cause Analysis

async function generateRootCauseAnalysis(
  campaign: CampaignSummary,
  anomalies: Anomaly[],
  platformContext: PlatformContext,
): Promise<RootCauseAnalysis> {
  const prompt = `
    You are a performance marketing analyst. Analyze why this campaign's metrics changed.
    
    Campaign: ${campaign.name} on ${campaign.platform}
    Date Range: ${campaign.dateRange}
    
    Anomalies Detected:
    ${anomalies.map(a => `- ${a.metric}: ${a.direction} ${((a.magnitude - 1) * 100).toFixed(0)}% (z-score: ${a.magnitude.toFixed(2)})`).join("\n")}
    
    Platform Context:
    - Account-level ROAS change: ${platformContext.accountRoasChange}%
    - Audience overlap estimate: ${platformContext.audienceOverlap}%
    - Recent creative changes: ${platformContext.recentCreativeChanges}
    - Bid strategy: ${platformContext.bidStrategy}
    
    Identify the most likely root causes in priority order. For each:
    1. The probable cause
    2. The evidence supporting this hypothesis
    3. A specific test to confirm or rule it out
    4. A recommended action if confirmed
    
    Return as JSON: { causes: [{ hypothesis, evidence, test, action, confidence: 0-1 }] }
  `;
  
  const response = await openai.chat.completions.create({
    model: "gpt-4o",
    messages: [{ role: "user", content: prompt }],
    response_format: { type: "json_object" },
  });
  
  return JSON.parse(response.choices[0].message.content!);
}

Executive Briefing Generator

The CEO doesn't want z-scores. They want one paragraph:

async function generateExecutiveBrief(
  campaigns: CampaignSummary[],
  rootCauses: Record<string, RootCauseAnalysis>,
  timeframe: string,
): Promise<string> {
  const prompt = `
    Write a 150-word executive performance brief for ${timeframe}.
    
    Total spend: $${totalSpend(campaigns).toLocaleString()}
    Overall ROAS: ${weightedRoas(campaigns).toFixed(2)}x
    
    Top performing campaigns: ${topPerformers(campaigns).map(c => c.name).join(", ")}
    Underperforming campaigns: ${underPerformers(campaigns).map(c => c.name).join(", ")}
    
    Root causes identified:
    ${Object.entries(rootCauses).map(([campaign, analysis]) => 
      `${campaign}: ${analysis.causes[0].hypothesis}`
    ).join("\n")}
    
    Write a brief suitable for a C-suite weekly review. Focus on business impact, 
    not technical metrics. Use plain language.
  `;
  
  const response = await openai.chat.completions.create({
    model: "gpt-4o-mini",  // cheaper model for narrative generation
    messages: [{ role: "user", content: prompt }],
  });
  
  return response.choices[0].message.content!;
}

Key Lessons

  1. Metric normalization is the hardest engineering problem — not the AI layer. Every platform has subtly different metric definitions. Build your normalization layer with explicit test cases.
  2. Statistical pre-filtering before AI — send only anomalies to the AI, not all metrics. This cuts API costs 10x and improves analysis quality.
  3. Cheaper models for narrative, smarter models for analysis — GPT-4o for root-cause reasoning, GPT-4o-mini for the executive brief.

GitHub: github.com/karthikrshet/adintel.ai