Build a full-stack web application called "Ad ↔ Landing Page Correlation Engine" that detects messaging drift between Meta ads and their landing pages.
Tech Stack
- Frontend: Next.js 14 (App Router) with TypeScript, Tailwind CSS, shadcn/ui
- Backend: Next.js API routes
- AI: Anthropic Claude API (claude-sonnet-4-20250514) via raw fetch (NOT the SDK) for MCP calls
- Meta Ads Data: GoMarble MCP server at https://apps.gomarble.ai/mcp-api/sse
- Landing Page Scraping: Firecrawl API (https://api.firecrawl.dev/v1/scrape) with fallback to basic fetch + cheerio
- State: React state (no database needed)
Environment Variables (.env.local)
ANTHROPIC_API_KEY=<your anthropic key>
FIRECRAWL_API_KEY=<your firecrawl key>
GOMARBLE_API_KEY=<your gomarble key>
Architecture
One page dashboard. User enters Meta Ad Account ID (e.g. act_123456789), clicks "Run Analysis". Backend runs a 5-step pipeline and streams progress via Server-Sent Events.
SSE event format:
data: { "step": 1, "status": "running|done|error", "detail": "...", "data": {...} }
CRITICAL: How to call GoMarble MCP
The Anthropic SDK does NOT support mcp_servers yet. Use raw fetch to the Anthropic API with the beta header:
async function callGoMarbleMCP(prompt: string) {
const response = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": process.env.ANTHROPIC_API_KEY!,
"anthropic-version": "2025-01-01",
"anthropic-beta": "mcp-client-2025-04-04"
},
body: JSON.stringify({
model: "claude-sonnet-4-20250514",
max_tokens: 4096,
mcp_servers: [
{
type: "url",
url: "https://apps.gomarble.ai/mcp-api/sse",
name: "gomarble",
authorization_token: process.env.GOMARBLE_API_KEY
}
],
messages: [{ role: "user", content: prompt }]
})
});
return response.json();
}
Use this callGoMarbleMCP function for Step 1 and Step 2. For Step 4-5 (AI analysis with no MCP), you can use the Anthropic SDK normally.
Backend: API Route /api/analyze (POST → SSE stream)
Request body: { accountId: string }
Step 1: Fetch Ad-Level Insights
Call GoMarble MCP with this prompt:
Use the facebook_get_adaccount_insights tool with these EXACT parameters:
- act_id: "${accountId}"
- fields: ["ad_id", "ad_name", "spend", "impressions", "clicks", "ctr", "cpc", "cpm"]
- level: "ad"
- date_preset: "last_30d"
- sort: "spend_descending"
- limit: 20
- filtering: [{"field": "impressions", "operator": "GREATER_THAN", "value": "0"}]
Return the raw data only.
Parse MCP tool results from response.content — look for blocks with type: "mcp_tool_result" and extract ad data.
Step 2: Fetch Creative Details
Take ad_ids from Step 1 (top 15) and call GoMarble MCP:
Use the facebook_get_ad_creative_details tool with:
- ad_ids: ${JSON.stringify(adIds)}
Return ALL creative details including object_story_spec, asset_feed_spec, link URLs, headlines, body text.
Extract:
- Landing page URLs from object_story_spec.link_data.link or asset_feed_spec.link_urls
- Ad copy from object_story_spec.link_data.message, .name (headline), .description
- CTA type
Step 3: Scrape Landing Pages (Firecrawl)
For each unique landing page URL (max 8):
const res = await fetch("https://api.firecrawl.dev/v1/scrape", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${process.env.FIRECRAWL_API_KEY}`
},
body: JSON.stringify({
url,
formats: ["markdown"],
onlyMainContent: true,
waitFor: 3000,
})
});
Add 1-second delays between requests. Deduplicate URLs (multiple ads may point to same page).
Step 4 & 5: AI Analysis + Scoring
Send all collected data to Claude (SDK is fine here, no MCP needed):
You are an expert performance marketing analyst specializing in ad-to-landing-page alignment audits.
## Ad Data with Creative Copy:
${JSON.stringify(adLPPairs)}
## Scraped Landing Page Content:
${JSON.stringify(scrapedPages)}
For EACH ad-landing page pair, analyze alignment and return a JSON array:
[
{
"ad_name": "string",
"ad_id": "string",
"spend": "string",
"impressions": "string",
"clicks": "string",
"ctr": "string",
"landing_page_url": "string",
"alignment_score": 0-100,
"severity": "critical|warning|good",
"mismatches": [
{
"type": "offer_mismatch|messaging_drift|cta_mismatch|tone_mismatch|value_prop_gap|urgency_mismatch",
"ad_says": "exact claim from ad",
"page_says": "what landing page actually shows",
"impact": "high|medium|low",
"explanation": "why this matters"
}
],
"recommendations": ["specific actionable fix"],
"wasted_spend_estimate": "estimated $ wasted",
"overall_assessment": "2-3 sentence summary"
}
]
CRITICAL: Use ONLY the data provided above. Do NOT fabricate or hallucinate any data. If data is missing, say so explicitly. Every ad_id, URL, and metric must come from the data I provided.
Scoring: 90-100 = perfect alignment, 70-89 = minor drift, 50-69 = significant mismatch, 0-49 = critical.
Focus on: offer consistency, CTA alignment, messaging tone, value prop match, urgency mismatch.
Return ONLY valid JSON. No markdown fences.
Frontend: Dashboard Page (dark theme)
- Header: "Ad ↔ Landing Page Correlation Engine"
- Subtitle: "Detect messaging drift between your Meta ads and landing pages"
- Powered by: GoMarble + Claude AI + Firecrawl
- Input: Text field for Meta Ad Account ID + "Run Analysis" button
- Pipeline Progress: 5 steps with status badges (Waiting → Running → Complete/Failed)
- Summary Cards (3 in a row): Total Ads Analyzed | Average Alignment Score (circular ring) | Critical Issues count
- Results List (sorted worst-first): Each card shows:
- Score ring (green >80, amber 50-80, red <50)
- Ad name + severity badge (CRITICAL/WARNING/GOOD)
- Metrics row (spend, impressions, clicks, CTR)
- Landing page URL (clickable)
- Overall assessment
- Mismatches: type, "Ad says" vs "Page says", impact badge
- Recommendations with arrow bullets
SSE Implementation
Backend streams progress. Frontend consumes via fetch + ReadableStream.
File Structure
ad-lp-engine/
├── app/
│ ├── page.tsx
│ ├── layout.tsx
│ ├── globals.css
│ └── api/analyze/route.ts
├── components/
│ ├── ScoreRing.tsx
│ ├── PipelineProgress.tsx
│ ├── ResultCard.tsx
│ └── SummaryCards.tsx
├── lib/
│ ├── types.ts
│ ├── gomarble.ts
│ ├── scraper.ts
│ └── analyzer.ts
├── .env.local
└── package.json
Build this app. Start with npx create-next-app@latest ad-lp-engine --typescript --tailwind --app --src=no. Install: @anthropic-ai/sdk, cheerio. Initialize shadcn with npx shadcn@latest init.
IMPORTANT: Make sure .env.local values are loaded correctly. Use process.env.ANTHROPIC_API_KEY directly — do NOT let ~/.zshrc or shell exports override them. Explicitly read from .env.local if needed.