agentsclimarketplace

Optimizer

Skill chanktb/claude-google-ads/skills/optimizer

Acts on a running account to improve performance: campaign tiering, search-term mining into negatives, budget reallocation, target ROAS/CPA adjustments, bid-strategy changes, asset refresh, and consolidation. Diagnoses root cause (not just symptoms) and produces a dated action plan. Proposes changes — applies via pusher with approval. Generalized fork of a production optimizer; reads account-context.yaml, respects guardrails and margin tiers. Use when the user says "optimize", "improve ROAS", "lower CPA", "add negatives", "raise tROAS", "reallocate budget", "fix underperformers", "weekly ads review".From its SKILL.md

Install
npx -y skills add chanktb/claude-google-ads --skill optimizer

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

2 things to look at

  • 10 stars10 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
  • runs commandsInstructs the agent to run 3 commands, including `python ${CLAUDE_PLUGIN_ROOT}/skills/optimizer/scripts/tiering.py campaigns.json [--target-roas T]` and 2 more.

SKILL.md

12.5 KB, ~3.0k tokens by cl100k_base, as published. Nobody here has run it

Google Ads — Optimizer (act)

Find what's dragging the account and fix it — with data backing and root-cause reasoning, not symptom swatting. The optimizer proposes changes; it applies them only through pusher (approval gate). For a scored health check use audit; this skill is about performance and money.

Operating rules

  • Every recommendation has data backing (specific numbers, not vague advice).
  • Read everything from account-context.yaml (margin_tiers, brand_terms, guardrails, AOV). If the context is missing, run setup first — never optimize a live account without it.
  • Read the context connections block first. If store/GA4 is missing, do the in-platform analysis and label every true-ROAS / store-revenue conclusion UNVERIFIED — connect store/GA4; never fabricate a store-revenue figure to compute "true ROAS". Prefer guiding the user to connect over guessing.
  • 3-source attribution (see ${CLAUDE_PLUGIN_ROOT}/references/optimization-playbook.md): store revenue = ground truth; Google Ads in-platform = for Smart Bidding; GA4 = channel mix. A 20-35% Ads-vs-GA4 gap is normal.
  • Honor guardrails: change-event cooldown, ignore paused, margin-tier ROAS. Don't flag a house-brand line for low ROAS above its tier min_roas.

Model dispatch (run cheap, decide expensive) — see ${CLAUDE_PLUGIN_ROOT}/references/model-tier-dispatch.md

  • Scout (haiku) — running tiering.py and search_term_miner.py (scripts return their own output).
  • Routine (sonnet) — STEP 1 performance pull + store/GA4 fetch; STEP 3 dual-source search-term pull (per-PMax campaign_search_term_insight loop). Dispatch as general-purpose sub-agents; return raw, don't conclude.
  • Judge (main session) — tier verdicts, what to block vs keep (esp. never-block-brand), STEP 5 profitability call, STEP 6 root-cause, STEP 7 dated action plan. The numbers come cheap; the decisions stay here.

STEP 1 — Collect performance

Scope to the active set first (ENABLED + impressions in the window) — never tier or "optimize" a campaign that hasn't served in the period; entity status=ENABLED can include long-dead campaigns' assets (see ${CLAUDE_PLUGIN_ROOT}/skills/audit/references/gaql-notes.md). Then pull campaign performance, search terms (≤30d or explicit dates), and asset-group performance via the MCP; pull store revenue + GA4 channel mix via the data-source/GA4 fallback chain. Note what's unavailable.

STEP 2 — Tier campaigns

Classify Gold / Silver / Bronze / Dead by ROAS relative to the margin-tier target, plus conversion volume per period. Output a ranked verdict table (Scale / Keep / Reduce / Pause / Kill) with the numbers. Runnable: python ${CLAUDE_PLUGIN_ROOT}/skills/optimizer/scripts/tiering.py campaigns.json [--target-roas T] (margin-tier aware; flags learning risk + pause-candidate savings).

STEP 3 — Mine search terms → negatives (BOTH sources)

Pull from both: search_term_view (Search/Branded) AND campaign_search_term_insight looped per active PMax/Demand-Gen campaign (requires a single campaign_id filter — see gaql-notes.md; it returns categories + conv/value, not per-term cost). search_term_view does NOT contain PMax terms, so PMax-heavy accounts need the insight or you'll miss most of the spend. Find top spend, top revenue, wasted (Search:

$10 spend & 0 conv; PMax: irrelevant categories with clicks but no conversions). Propose Exact/Phrase negatives in themed lists; respect the "what NOT to block" rules (store / coupon / cheap-brand) in the playbook; never block brand terms (brand intent, even at 0 conv). Catch cross-brand leakage via brand_terms.

🛑 RESELLER BRAND RULE — never negate a brand the store SELLS (the #1 false-positive)

For a distributor/reseller, a search term containing a brand you carry is buying intent, not a competitor — auto-negating it kills your own sales. Classify every brand-bearing term into THREE buckets, not two:

  1. OWN house brand → never block (brand defense). [brand_terms]
  2. CARRIED brand (a brand in your catalog/feed) — split by whether it already has its own campaign:
    • has a dedicated campaign → blocking it in a catch-all (e.g. "Lite") is OK only to ROUTE traffic to the specialist campaign (anti-cannibalization). Label it "route to <brand> campaign", NOT "competitor".
    • no dedicated campaignNEVER block. The catch-all is its ONLY home — it lives there because it hasn't been split out yet, not because it's a competitor. A 0-conv carried-brand term is a stock/PDP/feed problem OR a split-into-its-own-campaign candidate once volume justifies it. Recommend the split, never a negative.
  3. NOT carried (store genuinely doesn't sell it) → a candidate competitor negative — flag for the user to CONFIRM it isn't carried before blocking. Never assert "competitor" on your own. Populate the miner's brand inputs every run (don't run it blind): carried_brands = the distinct brand values from the store catalog / the Shopping feed (shopping_performance_view product brand, or the D14 inventory join) ∪ any brands_carried in account-context.yaml; brands_with_own_campaign = brands that map to an existing dedicated campaign (derive from the active-campaign names, e.g. "ND pMax OPI" → opi). Without these, the miner will mislabel carried brands (Kupa/Chaun Legend/Kiara Sky on ND) as competitors — exactly the bug to avoid. Runnable: python ${CLAUDE_PLUGIN_ROOT}/skills/optimizer/scripts/search_term_miner.py terms.json (input keys: brand_terms, carried_brands, brands_with_own_campaign, min_spend, terms — each term may carry conversions/conv_value) → wasted spend + categorized, ready-to-copy wrapped negatives, with carried-brand / routing / competitor-candidate buckets kept separate and carried-brand-without-a-campaign held back from blocking.

N-gram modifiers + conflict detection (built in to the miner)

  • N-gram surfacing: the miner aggregates 1- and 2-word tokens across the whole waste pool, ranked by $ and by how many terms each appears in (≥2 = recurring). One phrase negative on a recurring junk modifier (e.g. "how to", "jobs", "tutorial") kills many wasted queries at once — far more efficient than per-term negatives. Own-brand and carried-brand tokens are excluded from the surfacing (never propose cutting a brand you sell). Treat as candidates — a broad modifier can over-reach; eyeball before pushing.
  • Conflict detection — NEVER negate a converter: the miner cross-checks every proposed negative against the CONVERTING queries (conversions/conv_value > 0) and drops any that would block one (exact = same query, phrase = substring), listing them under "Conflict-blocked". Always pass terms WITH their conversion fields so this fires. At push time, also run validate_changeset.py --converting-terms <converters.json> (a JSON list of converting queries) so the pusher gate re-blocks any negative that would cut proven revenue — defense-in-depth alongside the never-block-brand and carried-brand gates.

STEP 4 — Asset groups / creative

Flag asset groups with ROAS below the tier target and meaningful spend, POOR ad strength (ENABLED + has impressions — GUARD-4), and URL overlap. Recommend creative refresh via assets.

STEP 5 — Cross-reference store + GA4 (true profitability)

True ROAS = store revenue (by product/vendor) ÷ Google Ads spend. If a line's TOTAL store revenue (all channels) is below its Ads spend, it's losing money regardless of in-platform ROAS. Surface channel mix (how much is organic/direct/email) so pausing decisions account for non-paid revenue.

STEP 5.5 — Run the seven money-leak diagnostics (the core engine)

Work through D1-D14 in ${CLAUDE_PLUGIN_ROOT}/references/diagnostic-playbook.md against the active set: D1 bid/ target health (decode bidding_strategy_system_status first — budget-capped vs tROAS-too-high vs starved vs learning; breakeven ROAS = 1/margin), D2 budget pacing & allocation (misallocation: shift $ low-ROAS→high-ROAS), D3 geo waste, D4 dayparting, D5 search-term/spam/wrong-brand, D6 structure, D7 Quality Score, D8 PMax channel/placement distribution (don't assume "Display burns it" — pull segments.ad_network_type, name the real surface; most levers DON'T work on PMax), D9 ad copy/assets/extensions (missing sitelinks/callouts, duplicate headlines, LOW assets). Each becomes a prescription with a real $ impact, a step size, and a cooldown gate. Caveat baked in: budget-lost-IS is blind on Smart Bidding/PMax — diagnose budget constraint via spend-vs-budget + system_status, not budget-lost-IS. For any tROAS/budget scale move, follow the Scaling Ladder (budget first then target, never both same week, ≤+20% budget / 10–20% target steps, 14-day cooldowns, never reverse inside the window).

STEP 6 — Diagnose root cause (System Thinking)

Don't stop at symptoms. For each problem ask "why" until you reach the cause (e.g. low ROAS → irrelevant search terms → missing negatives → no weekly review process). Check interactions: campaigns competing for the same queries, cross-brand budget leakage, over-reliance on paid vs organic. Project the cost of inaction vs the savings from fixing.

STEP 7 — Action plan

Produce a dated plan in three buckets, each item with: what to do · exact UI/Editor steps · why (data) · status checkbox.

  • Today: pause Dead campaigns, cut budget on Bronze, add negative lists, fund under-invested branded.
  • Tomorrow: review yesterday; raise tROAS ≤0.2-0.3x (only if ≥15 conv/week, else consolidate first); scale Gold (+budget gradually); plan consolidations.
  • 2-week roadmap: dated timeline, each action typed (urgent/optimize/plan). Respect the tROAS step-up discipline and change-event cooldown throughout.

STEP 8 — Emit the change-set (the pusher hand-off)

Serialize the action plan into a change-set.json (contract: ${CLAUDE_PLUGIN_ROOT}/references/change-set.md; template: ${CLAUDE_PLUGIN_ROOT}/templates/change-set.json) — a typed list of edits to the LIVE account (add_negatives, exclude_geo, exclude_products, adjust_budget, adjust_target_roas, pause, add_extensions, add_audience_signal, exclude_placements). This is distinct from a builder's campaign-spec.json (which creates a NEW campaign).

  • Every action carries a reason (the $ leak / numbers), its diagnostic D-code, est_impact_per_mo, and a push_path. Express a budget reallocation as a pair (a down/pause on the source + an adjust_budget up on the receiver) so each leg is gated on its own.
  • Encode the discipline in the data, don't rely on prose: mark target.within_cooldown: true for any campaign with a recent change_event (D13) or under the change-event-cooldown guardrail, set target.last_change_direction, and pass conv_per_week on a tROAS raise. The pusher's validator BLOCKS a scale move on a within-cooldown target — so a campaign that "shouldn't scale yet" must NOT carry a do-now scale action (defer it to the prose roadmap, or omit it). PMax scales DOWN by pausing a low-ROAS asset group, never a budget cut.
  • The optimizer proposes the change-set; it never writes. Hand it to pusher (or /google-ads-push), which validates (validate_changeset.py) and renders operator actions (changeset_to_actions.py) behind the approval gate.

Applying changes

Account mutations go through pusher (approval gate + spend cap). The optimizer prepares the change-set (STEP 8); it does not write to the account directly.

To build / refine later

  • Runnable analyzers: scripts/tiering.py + scripts/search_term_miner.py. Done.
  • Emit changes as a structured change-set the pusher can consume (STEP 8 → change-set.json; validated/rendered by pusher's validate_changeset.py + changeset_to_actions.py). Done.

What ships with it: 2 files

15.2 KB alongside SKILL.md, 2 of them executable

scripts/

Keep looking

Skills are one crate of 325,949. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.