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
- 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.
- Statistical pre-filtering before AI — send only anomalies to the AI, not all metrics. This cuts API costs 10x and improves analysis quality.
- Cheaper models for narrative, smarter models for analysis — GPT-4o for root-cause reasoning, GPT-4o-mini for the executive brief.