# MGID Auto-Optimization Simulator

Orchestrator only: **gate → collect signals → merge → rank → preview**. Does not
reimplement signal rules — load and execute each signal skill's playbook.

Tools: [../_shared/ads-mcp-tools.md](../_shared/ads-mcp-tools.md)  
Schemas: [../_shared/schemas.md](../_shared/schemas.md)  
Merge/rank: [reference.md](reference.md)

---

## Signal skills (load, do not duplicate their rules)

| Skill | Output |
|---|---|
| `mgid-tracking-health-monitor` | Gate pass/fail |
| `mgid-budget-reallocator` | `reallocate_budget` |
| `mgid-source-auditor` | block / adjust_qf |
| `mgid-creative-fatigue-detector` | pause_teaser |

`mgid-campaign-launch-builder` — separate unless user asks setup + optimize.

---

## Required inputs

| Input | Required | Default |
|---|---|---|
| Scope | Yes | see **Campaign scope** below |
| Date range | Yes | last 7 days; baseline = prior 7d |
| Goal / target | Yes | user or `get_campaign` → `bidding.price` |
| Mode | Yes | `simulation` |

No `get_account_goals`. Target CPA must come from user or campaign config.

### Campaign scope

Default: analyze **all active campaigns** up to **25** (`DEFAULT_CAMPAIGN_SCOPE`).

| Active count | Behavior |
|---|---|
| ≤ 25 | Analyze **every** active campaign — no silent truncation |
| > 25 | **Stop and ask the user** before Phase 2 (see prompt below) |

User may override scope at any time:

- Named campaign IDs → analyze only those (still run gate on all active)
- “All active” / “full account” → analyze every active campaign regardless of count
- “Top N by spend” → analyze top N only; **state N and the cutoff spend in chat**

Never analyze only 1–2 campaigns unless the user explicitly scoped that way.

**When active count > 25, ask:**

```
This account has {N} active campaigns. Default scope covers up to 25.

How should I scope the simulation?
1. Top 25 by 7d spend (default — fastest)
2. All {N} active campaigns (more MCP calls, full portfolio)
3. Specific campaign IDs (you name them)
```

Wait for the user's choice before collecting signals. If they pick (1), rank by
`get_portfolio_statistics` → `spent` and take the top 25. Record scope metadata
in the plan file (see [reference.md](reference.md)).

---

## Workflow

### Phase 0 — Gate

Run `mgid-tracking-health-monitor` playbook.

On fail → fix-first plan only; `gates.tracking_health = fail`.

### Phase 1 — Scope campaigns

1. `get_portfolio_statistics` → spend, conversions and CPA for **every** campaign
   in one call
2. `list_campaigns` → filter `status.name == active`, pick up `type`, `limits`,
   `bidding`, `sourcesOptimization` → count **N**
3. Drop `search_feed` and `newsletter` campaigns from scope and record them as
   unsupported (see [../_shared/ads-mcp-tools.md](../_shared/ads-mcp-tools.md))
4. Apply **Campaign scope** rules (ask user if N > 25 unless user already specified scope);
   "top 25 by spend" sorts on the portfolio row's `spent`
5. `get_campaign_statistics` only for campaigns whose **daily rows** a signal
   skill actually needs (budget cap detection, period-over-period)

Phase 1 costs 2 calls regardless of portfolio size. Never open with one
`get_campaign_statistics` per campaign.

### Phase 2 — Collect signals

Execute each signal skill on **every campaign in scope**; collect `Recommendation[]`.
Do not re-apply their thresholds here.

Pass the scoped campaign list to budget reallocator and source auditor — they must
not silently shrink scope on their own.

**Creative fatigue scoping (priority order):**

1. **Primary:** campaigns with **no** source/budget/tcpa recommendations in this plan
   **and** avg daily spend &lt; 85% of `limits.dailyLimit` (underspending)
2. **Fallback:** campaigns with **7d spend ≥ $200** or top 5 by spend in scope (whichever set is smaller)

Use **`run_custom_report`** with dimension `teaserId` (see creative skill) when the
account has the customReports grant; fall back to `get_teaser_statistics` (v1) per teaser.

Record every campaign scanned in the plan file under `summary.creative_fatigue.campaigns_scanned` —
not only campaigns with pause recommendations.

### Phase 3 — Merge

Apply conflict rules in [reference.md](reference.md). Dedupe; populate `suppressed`.

### Phase 4 — Rank

Sort by `(confidence × estimated_savings) / risk`. Cap at 10 (**ASSUMPTION**).

### Phase 5 — Preview

**Chat:** summary block + table only.

**File:** write `optimization-plan-{plan_id}.json` with full `OptimizationPlan` +
`stats_snapshot` (plain numbers — no hashing).

Ask user to approve by rank number.

### Phase 6 — Apply (after approval, write scope)

Read plan file back. Re-fetch statistics; compare to `stats_snapshot` (**ASSUMPTION:**
warn if &gt;10% drift). Call write tools per [ads-mcp-tools.md](../_shared/ads-mcp-tools.md).

Plan storage, audit trail, idempotent apply → **ads-mcp server**, not this skill.

---

## Anomalies (simulator-only)

**ASSUMPTION:** flag when 7d totals vs prior 7d change ≥ 40% on CPC, CVR, or CPA.

Do **not** use day-over-day — native traffic is too volatile.

Emit `alert_anomaly` only (no auto-action).

---

## Guardrails

1. Never apply without explicit approval.
2. Simulation default.
3. Mark recommendations `mcp_tool_unavailable` when write tools absent.

---

## Example chat output

```
Optimization plan — SIMULATION
Plan ID: opt-2026-07-06-001
Scope: 16 / 16 active campaigns · 2026-06-29 → 2026-07-05
Goal: CPA ≤ $15.50 (from campaign 12061865)
Gates: tracking ✓
Plan file: optimization-plan-opt-2026-07-06-001.json

| # | Action | Entity | Reason | Impact | Risk |
| 1 | Block source | 242669 @ 12061865 | CPA 2.1× target | −$18–23/day | Low |
...
```

Do not paste full JSON into chat.


---

# Simulator — merge, rank, preview only

Signal skills own thresholds and impact formulas. This file covers orchestration
only.

Schemas: [../_shared/schemas.md](../_shared/schemas.md)  
Scope default: [../_shared/assumptions.md](../_shared/assumptions.md) (`DEFAULT_CAMPAIGN_SCOPE`)

---

## Campaign scope (orchestrator)

The simulator owns scope — signal skills analyze the campaign list they receive.

| Constant | Value |
|---|---|
| `DEFAULT_CAMPAIGN_SCOPE` | 25 |

### Selection algorithm

```
active = list_campaigns filtered to status=active
N = len(active)

if user gave explicit scope → use it
else if N <= 25 → scope = all active ids
else → ask user (see SKILL.md); default choice = top 25 by 7d spend
```

After scope is fixed, fetch `get_campaign_statistics` for each id in scope before
Phase 2.

### Plan file `scope` block

Always populate:

```json
"scope": {
  "auth_id": 200316,
  "client_id": 330838,
  "account_id": 330838,
  "total_active_campaigns": 42,
  "analyzed_campaign_ids": [12061397, 12061865],
  "analyzed_count": 25,
  "scope_mode": "top_by_spend",
  "scope_limit": 25,
  "user_confirmed": true
}
```

| `scope_mode` | Meaning |
|---|---|
| `all_active` | Every active campaign analyzed |
| `top_by_spend` | Subset ranked by 7d spend (record limit) |
| `user_specified` | User named ids or a custom filter |

When `analyzed_count < total_active_campaigns`, chat summary **must** show
`{analyzed} / {total} active campaigns` and name the scope mode.

---

## Conflict resolution

| Conflict | Resolution |
|---|---|
| Block vs budget to same campaign | Defer block unless CPA > 2× target (**ASSUMPTION**) and post-learning |
| Pause teaser + budget same campaign | Never pause last active teaser |
| Budget out + in same pair | Single net reallocation line |
| Multiple QF on same entity | Smallest decrease wins |
| Top converter | Defer to source-auditor downgrade rules |
| Tracking fail | Suppress all spend actions |
| CPA Tune learning | Suppress blocks — source-auditor owns detail |

---

## Ranking

```
score = (confidence_score × estimated_daily_savings) / risk_score
confidence: high=3, medium=2, low=1
risk: low=1, medium=2, high=3
```

Take top 10 unless user asks for full list.

---

## Apply order

When write tools available:

1. `set_teaser_status` (pause)
2. `update_quality_factors`
3. `update_source_filters` (blocks)
4. `update_campaign` (budget)

Stop on first hard failure; report partial apply in chat.

---

## Drift check at apply

Compare fresh `get_campaign_statistics` totals to `stats_snapshot` in plan file.

**ASSUMPTION:** if `spent` or `conversionsCostBuy` moved &gt;10%, warn and require
re-confirm before writes.

No sha256 — numeric compare only.

---

## Plan file location

Write under workspace, e.g. `./optimization-plan-{plan_id}.json`.

When ads-mcp adds plan storage API, prefer server-side plan id over local files.
