# MGID Creative Fatigue Detector

Detects teaser fatigue via period-over-period stats. Rules live **here**.

Tools: [../_shared/ads-mcp-tools.md](../_shared/ads-mcp-tools.md)  
Assumptions: [../_shared/assumptions.md](../_shared/assumptions.md)

---

## Required inputs

| Input | Required | Default |
|---|---|---|
| Campaign ID(s) | Yes | simulator scope list, user ids, or named campaigns when run standalone |
| Current period | Yes | last 7 days |
| Baseline period | Yes | prior 7 days (days 8–14) |
| Target CPA | No | from campaign stats or user |

### Campaign type gate

Only `content`, `product` and `push` campaigns have teasers. Skip `search_feed`
and `newsletter` and say so in the output — see the campaign-type table in
[../_shared/ads-mcp-tools.md](../_shared/ads-mcp-tools.md). On `push`, widget-level
signals are absent, so rely on teaser-level CTR/CPA only.

**Refresh recommendations name what to replace, never the replacement copy.**
New headlines and images come from MGID's own creative generation, not from the
agent.

---

## MCP tools

| Tool | Purpose |
|---|---|
| `run_custom_report` | **Primary** — all teasers + metrics in one call per campaign (requires **customReports** grant) |
| `list_teasers` | Active teaser count + status guardrail (no metrics) |
| `get_teaser` | Config, moderation status |
| `get_teaser_statistics` | **Fallback** — per teaser when custom report unavailable |
| `get_campaign_statistics` | Campaign avg CTR/CPA for signal #2 and #5 |

### Bulk stats via custom report

One `run_custom_report` per campaign beats N× `get_teaser_statistics`. Use
dimension **`teaserId`**, filter by **`filters.campaigns`**, and the same date
windows as other signal skills.

**ads-mcp implementation:** `run_custom_report` calls the v2 statistics reports
endpoint, which accepts **`filters.dateRange` only** — no `dateRangePrevious`.
There is no comparison mode through ads-mcp today.

**Period-over-period:** run **two** reports per campaign — current window and
baseline window — and join rows on `teaserId`. Still **2 calls/campaign**, not
one per teaser.

Example params (current 7d, top spenders first):

```json
{
  "dimensions": ["teaserId"],
  "metrics": ["clicks", "spent", "ctr", "conversionsBuy", "conversionsCostBuy"],
  "filters": {
    "campaigns": [12061865],
    "dateRange": {
      "dateFrom": "2026-06-30T00:00:00.000+03:00",
      "dateTo": "2026-07-06T23:59:59.999+03:00"
    }
  },
  "orders": { "spent": "desc" },
  "limit": 100
}
```

Repeat with `dateRange` set to the prior 7d for baseline metrics.

Paginate with `offset` when `meta.total` > `limit`. Skip teasers with zero
current spend unless `list_teasers` shows them as active and recently launched.

**Grant missing:** fall back to `get_teaser_statistics` per teaser; cap at **20**
teasers (prior-spend order from a single-period custom report is unavailable —
use `list_teasers` + campaign stats to pick candidates).

---

## Minimum data (**ASSUMPTION**)

Per teaser, in current period:

- clicks ≥ **100**, OR
- impressions ≥ **1,000** (if returned by statistics tool)

Skip non-active teasers. Count active teasers — never pause if only one remains.

---

## Fatigue signals (**ASSUMPTION**)

Compare **current 7d vs prior 7d** only (no day-over-day — too noisy on native):

1. CTR drop ≥ 30% with min clicks
2. CPA increase ≥ 50% vs campaign avg with ≥ 5 conversions
3. CVR drop ≥ 35% vs baseline with ≥ 200 clicks
4. Spend ≥ $30, conversionsBuy = 0, clicks ≥ 150
5. Teaser CPA > 2× best active teaser (both ≥ 5 conversions)

---

## Recommendations

- `pause_teaser` when fatigue + ≥ 2 other active teasers
- `launch_teaser_variant` — guidance only; no create/moderation MCP tools yet
- Rejected teasers: explain via `get_teaser`; user fixes in UI — no `send_to_moderation` tool

---

## Impact (**ASSUMPTION**)

```
spend_removed = teaser_spend_7d / 7
recovery_rate = 0.3 to 0.7  (sibling absorption)
net_spend_delta = -spend_removed × (1 - recovery_rate)
```

---

## Apply

`set_teaser_status` (write scope).

---

## Output

**Chat:** table of teasers to pause/monitor.

**Return:** `Recommendation[]`.
