Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.mavera.io/llms.txt

Use this file to discover all available pages before exploring further.

Scenario

Your ads start strong then decay — frequency climbs, CTR drops, CPA spikes. By the time you notice, you’ve wasted budget. This job pulls frequency and performance time series per ad, detects when frequency exceeds a threshold while CTR drops below its initial level, and triggers Mave to research fresh creative angles before performance craters further.

Architecture

Code

import os, requests
from datetime import datetime, timedelta

META = os.environ["META_ACCESS_TOKEN"]
ACCT = os.environ["META_AD_ACCOUNT_ID"]
MV = os.environ["MAVERA_API_KEY"]
GRAPH = "https://graph.facebook.com/v24.0"
MB = "https://app.mavera.io/api/v1"
MH = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}

FREQUENCY_THRESHOLD = 3.0
CTR_DROP_PCT = 0.25

# 1. Pull daily performance per ad (last 14 days)
insights = requests.get(
    f"{GRAPH}/{ACCT}/insights",
    params={
        "access_token": META,
        "fields": "ad_id,ad_name,frequency,ctr,cpc,impressions,clicks,spend",
        "level": "ad",
        "time_increment": 1,
        "date_preset": "last_14d",
        "limit": 500,
    },
).json().get("data", [])

# 2. Group by ad and analyze trend
from collections import defaultdict
ad_series = defaultdict(list)
for row in insights:
    ad_series[row["ad_id"]].append({
        "date": row.get("date_start"),
        "frequency": float(row.get("frequency", 0)),
        "ctr": float(row.get("ctr", 0)),
        "cpc": float(row.get("cpc", 0)),
        "spend": float(row.get("spend", 0)),
        "name": row.get("ad_name", ""),
    })

fatigued = []
for ad_id, series in ad_series.items():
    series.sort(key=lambda x: x["date"])
    if len(series) < 3:
        continue

    first_3_ctr = sum(d["ctr"] for d in series[:3]) / 3
    last_3_ctr = sum(d["ctr"] for d in series[-3:]) / 3
    latest_freq = series[-1]["frequency"]
    total_spend = sum(d["spend"] for d in series)

    ctr_decline = (first_3_ctr - last_3_ctr) / max(first_3_ctr, 0.01)

    if latest_freq > FREQUENCY_THRESHOLD and ctr_decline > CTR_DROP_PCT:
        fatigued.append({
            "ad_id": ad_id,
            "name": series[0]["name"],
            "frequency": latest_freq,
            "ctr_initial": first_3_ctr,
            "ctr_current": last_3_ctr,
            "ctr_decline": ctr_decline,
            "total_spend": total_spend,
        })

print(f"Fatigued ads: {len(fatigued)} / {len(ad_series)} active")

if not fatigued:
    print("No fatigued ads detected.")
else:
    # 3. Build context for Mave
    fatigue_report = "\n".join(
        f"- \"{f['name']}\": freq={f['frequency']:.1f}, CTR dropped {f['ctr_decline']:.0%} "
        f"({f['ctr_initial']:.2f}% → {f['ctr_current']:.2f}%), spend=${f['total_spend']:.0f}"
        for f in fatigued
    )

    # 4. Get fresh angles from Mave
    research = requests.post(f"{MB}/mave/chat", headers=MH, json={
        "message": f"""These Meta ads are showing fatigue — frequency is high and CTR is declining.
Research fresh creative angles to replace or refresh them.

FATIGUED ADS:
{fatigue_report}

For each fatigued ad:
1. Why the current angle is likely fatiguing (audience saturation, message wear-out, etc.)
2. 3 fresh creative angles that maintain the core value prop but use new hooks
3. Recommended format changes (video length, static vs carousel, UGC vs polished)
4. Targeting adjustments to reduce frequency without losing quality"""
    }).json()

    print("\n=== Ad Fatigue Analysis & Recommendations ===")
    print(research.get("content", ""))

Example Output

Fatigued ads: 3 / 18 active

=== Ad Fatigue Analysis & Recommendations ===

### 1. "Summer Sale Hero — 30s" (freq 5.2, CTR -42%)
**Why:** Audience has seen this 5+ times. The promotional urgency ("limited time")
loses credibility after repeated exposure.

**Fresh angles:**
- Customer success story: "How [Company] saved 40% — and what they did with the savings"
- Problem-agitation: "Still spending 4 hours on reports? Here's what changed for 200 teams"
- Social proof carousel: 3 customer quotes with results

**Format:** Switch from 30s video to 15s Reel with text overlay — lower production, higher novelty.
**Targeting:** Exclude past converters and high-frequency viewers. Expand to 2% lookalike.

### 2. "Product Demo — Features" (freq 4.1, CTR -35%)
**Why:** Feature demos fatigue fastest — once they've seen it, there's no new information.

**Fresh angles:**
- Before/after workflow comparison (problem → solution framing)
- "Day in the life" UGC-style showing real usage
- Competitor comparison (without naming) — "The old way vs the new way"

Error Handling

Meta’s frequency is a lifetime metric for the ad, not daily. For daily frequency, divide daily impressions by daily reach (requires reach field).
Filter out ads with fewer than 1,000 impressions — small sample sizes produce noisy CTR values.
Daily breakdown (time_increment=1) is limited to 90 days. For longer ranges, use time_increment=7 or time_increment=monthly.

All Meta Ads Jobs

Browse all Meta Ads integration jobs

Mave Agent

AI research agent for creative strategy