agentsclimarketplace

Audit

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

Find where your Google Ads budget leaks, cut waste, and scale your ecommerce store safely — 15 skills: audit, money-leak diagnostics, plan, campaign builders, safe push, monitor, optimize.

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

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

One thing to look at

  • 9 stars9 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.

What its author says it does

Copied from the file, not written here

Audits and scores an existing Google Ads account across conversion tracking, wasted spend, account structure, keywords/Quality Score, ads & assets (incl. PMax, AI Max, Demand Gen), and settings. Read-only. Generalized fork of the ads-google audit: reads everything from account-context.yaml and applies the operator's guardrails — no brand/vertical hardcoding. Use when the user says "audit", "score my account", "what's wrong with my ads", "wasted spend", "account review", "health check".

SKILL.md

32.5 KB, as published. Nobody here has run it

Google Ads — Audit

Read-only diagnostic. Score the account, surface issues by severity, and — critically — never fire a finding on an unverified assumption. Reuse the live MCP detection proven in setup.

STEP 0 — Load context (MANDATORY, before any scoring)

  1. Read account-context.yaml from the working directory. If missing, run setup first.
  2. Pull from it: google_ads.customer_id, brand_terms, competitor_terms, margin_tiers, data_source (for landing-page URL checks), measurement, and guardrails.
  3. Output is written to the working directory (GOOGLE-ADS-AUDIT.md), never into the plugin.

⚠️ The six guards (apply to EVERY check — they prevent the most common wrong findings)

These are generic; guardrails in the context may strengthen or add to them. Honor both.

  • GUARD-1 Conversions — check the WHOLE picture, not the action list. Inspect campaign-level conversion goals + which actions each active campaign actually fires (segment by segments.conversion_action_name), and each action's source. A primary action no campaign uses is inert; a second purchase action kept secondary (e.g. GA4 purchase alongside the Shopify channel "App Purchase") is the CORRECT anti-double-count setup, not a bug. Only flag a real micro-conversion goal or two value-carrying purchase sources both primary and both firing. Before flagging ANY conversion-action contamination (out-of-brand / micro / duplicate), check primary_for_goal. A non-primary action does not enter Smart Bidding and does not affect performance — it's harmless; surface it as a NOTE (tidy-up), never a scored finding. Only a PRIMARY out-of-brand/micro action that an active campaign optimizes toward is a real issue. See ${CLAUDE_PLUGIN_ROOT}/references/conversion-tracking-logic.md.
  • GUARD-2 Negatives — FOUR sources, merge ALL. (1) campaign_criterion (negative=true) — campaign-level; (2) campaign_shared_setshared_setshared_criterion — SHARED LISTS (the bulk: e.g. "Account Level Negative", "OPI Negative", brand blocks); (3) customer_negative_criterion — ACCOUNT-LEVEL negatives applied account-wide; (4) campaign_criterion.brand_list.shared_set — brand exclusions. Only flag a gap if terms are absent across ALL FOUR; if a shared/account list covers them, cite it, don't flag. A campaign_criterion-only pull is the #1 false-positive in this audit — it looks "thin" when coverage is actually 100+ terms/campaign. After merging all four, declare pulled:[...,"negatives_complete"] so the report renders a real verdict; without that assertion a campaign-only bundle renders VERIFY (re-pull), never a false "weak negatives".
  • GUARD-3 Budget scaling — check change_event first. Before recommending a budget/bid increase, pull recent change history (~14d). A recent budget/bid/asset/structure change → recommend cooldown, not scale. (Strengthen with any change-event-cooldown guardrail in context.)
  • GUARD-4 Ad strength — confirm ENABLED + impressions. Before flagging POOR ad strength, confirm ad_group.status AND ad_group_ad.status are ENABLED and the group has impressions. Paused = intentional; do not flag.
  • GUARD-5 ROAS — apply margin tiers from context. Read margin_tiers; a line with a tier accepts its min_roas (house brands run lower ROAS by design). Never apply a generic ROAS threshold to such a line or a strategic campaign. If unsure, ASK rather than flag.
  • GUARD-6 No false "absence". MCP results can be truncated (token cap or your limit). Before claiming "no video / no signal / no negative / no X", re-query filtered on the enum NAME (e.g. field_type = 'YOUTUBE_VIDEO') or pull with returned-rows < limit (proof of completeness). NEVER infer presence/absence by decoding enum integers from memory. Track failed fetches as a G-SYS1 diagnostic; never silently skip a check — explain why data is unavailable.

Audit scope — only what's LIVE in the window (build this FIRST, before any check)

Establish the active campaign set: campaigns that are ENABLED AND had impressions > 0 during the audit window. Exclude removed, paused, and ENABLED-but-dormant (zero-impression / long-off) campaigns. Then scope EVERY other pull to that set — asset groups, ad groups, ads/ad-strength, keywords, search terms. Entity-level status = ENABLED is NOT sufficient: a campaign turned off years ago can still contain ENABLED asset groups / ad groups, and auditing them produces stale, misleading findings. Filter downstream queries by campaign.id IN (active set), or pull them WITH metrics over the window and drop zero-impression rows app-side (metrics can't go in a GAQL WHERE). Never audit a campaign that hasn't served in the window.

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

  • Scout (haiku) — STEP 0 context read; the money_leak_report.py render (→ the single AUDIT.html) after the Judge writes audit-result.json; single URL/landing 200 checks.
  • Routine (sonnet) — build the active-campaign set; the account data-bundle pull (campaigns, assets, change history); the dual-source search-term pull (per-PMax campaign_search_term_insight loop). Dispatch as general-purpose sub-agents (they keep MCP); tell them to return raw rows, not score.
  • Judge (main session) — applying GUARD-1…6, every PASS/WARN/FAIL call, the score, the Gap-to-100 ledger, Quick Wins. Never delegate a guard or a verdict — a Scout pulls numbers, the Judge says what they mean.

Process

  1. Pre-flight: load context + guards; build the active campaign set (above); stage the GUARD data pulls (campaign conversion goals, merged negatives, change_event, ad_group.status) scoped to it.
  2. Collect account data via the Google Ads MCP, scoped to the active campaign set. Honor GAQL gotchas in ${CLAUDE_PLUGIN_ROOT}/skills/audit/references/gaql-notes.md (dedup keywords; ENABLED-only + impressions-in-window; explicit YYYY-MM-DD date ranges; search_term_view can't use a 90-day DURING). MANDATORY pulls — a missing one = a false finding, not a clean account (this is the checklist a dispatched Scout MUST be handed in full):
    • campaigns + metrics (active set), budgets, bidding strategy + tROAS
    • conversion_action (account) + per-campaign segments.conversion_action_name usage (GUARD-1)
    • asset_group / ad_group_ad ad-strength; keyword_view + Quality Score (deduped)
    • campaign extensions coverage (D9): SELECT campaign.id, campaign.status, campaign_asset.field_type, campaign_asset.status FROM campaign_asset WHERE campaign.status='ENABLED' AND campaign_asset.status='ENABLED'. field_type gives the coverage (SITELINK / CALLOUT / STRUCTURED_SNIPPET / PRICE / IMAGE). Two gotchas, both seen live: (1) campaign.status MUST be in the SELECT because it's filtered (else EXPECTED_REFERENCED_FIELD_IN_SELECT_CLAUSE); (2) do NOT also select asset.type here — it throws INVALID_ARGUMENT on many MCPs (pull asset text separately if needed). Count field_type per campaign (not just presence) so "thin extensions" findings use real counts. If it still errors, flag extensions UNVERIFIED — verify in UI, never assume present/absent.
    • search terms both sources: search_term_view + per-PMax campaign_search_term_insight
    • negatives from ALL FOUR sources (GUARD-2) — NOT just campaign_criterion: (a) campaign_shared_set (which shared lists attach to each campaign — count only status=2 ENABLED; many lists are status=3 REMOVED/old) → shared_criterion (the terms in each ENABLED list); (b) customer_negative_criterion (account-level negatives applied to every campaign — a separate resource, easy to miss; the first ND pull omitted it and produced a false "weak negatives" finding that had to be corrected mid-audit). A brand block / cross-brand block / location block usually lives in a SHARED or ACCOUNT-level list, so a campaign_criterion-only pull shows "no negatives" when coverage is actually extensive. Never flag G07/G14/G-PM5/G-PM6 (brand exclusion / negative coverage) without having pulled shared_criterion AND customer_negative_criterion. After merging, declare pulled:[...,"negatives_complete"].
    • PMax Brand exclusions are a SEPARATE layer from negative keywords — pull them explicitly: campaign_criterion selecting campaign_criterion.brand_list.shared_set + .negative (filter campaign_criterion.brand_list.shared_set IS NOT NULL), then resolve each shared_set.name. These rows have an EMPTY keyword.text, so a keyword-text-only pull DROPS them and you'll wrongly report "no brand exclusion". Check whether a brand list matching the own brand (a list named after the own brand) is excluded on each PMax campaign. An account can run BOTH a brand negative-keyword list AND a brand-list exclusion (belt-and-suspenders) — read both before judging.
    • change_event (~14d)
    • channel split (D8): segments.ad_network_type per campaign — this WORKS for PMax too (network 2 Search/Shopping dominates; small YouTube/Display/cross). NEVER report channel as "verify in UI". Pull metrics.conversions alongside cost — a cost-only pull makes every network look like 0-conv burn.
    • device split (D10): segments.device per campaign (2 Mobile · 3 Tablet · 4 Desktop · 5 CTV · 6 Other).
    • dayparting (D3/D4): segments.hour + segments.day_of_week per campaign + metrics — for the hourly heatmap and best/worst hour/day. And ad schedule: campaign_criterion.ad_schedule.* + bid_modifier (a flat bid_modifier=0 across the window = NO dayparting bid strategy — a real opportunity finding).
    • geo — ALL regions (D3): geographic_view scoped to active campaigns, metrics.cost_micros > 0, no top-N limit. Then resolve EVERY segments.geo_target_region id to a name via geo_target_constant (id, name, canonical_name). Raw geoTargetConstants/NNNNN ids in the report = an unfinished pull, never ship them.
    • asset TEXT + per-asset metrics (D9) — pull the TEXT and cost, NOT just counts (a counts-only pull is wrong; the verbatim text + per-asset spend is what surfaces "this headline spent $X at 0 conv"): SELECT campaign.id, asset_group_asset.field_type, asset.text_asset.text, metrics.cost_micros, metrics.conversions, metrics.conversions_value, metrics.clicks FROM asset_group_asset WHERE campaign.status='ENABLED' AND asset_group_asset.status='ENABLED' AND segments.date BETWEEN ... (HEADLINE ft2 / DESCRIPTION ft3 / LONG_HEADLINE ft18). asset_group_asset DOES return per-asset metrics — a text-only or count-only pull just looks empty (gaql-notes). For Search RSAs, ad_group_ad.ad. responsive_search_ad.headlines is a RepeatedComposite the serializer can't return — get RSA asset text + metrics from ad_group_ad_asset_view instead (don't write "verify in UI"). The ONLY genuine UI-only asset field is asset_group_asset.performance_label (UNRECOGNIZED on some API versions).
    • extension TEXT (D9) — not just field_type counts: pull the sitelink/callout/snippet/price text so the report shows the actual extensions. Either campaign_asset joined to the asset text fields (asset.sitelink_asset.link_text, asset.callout_asset.callout_text, asset.structured_snippet_asset.values, asset.price_asset.*) or the asset resource by id. bundle key ext_text:[{campaign_id,field_type,text}].
    • conversion lag (D12): segments.conversion_lag_bucket — the "can I trust short-window ROAS?" gate.
    • settings & measurement that DO read (pull these — they were once wrongly punted to UI): location type campaign.geo_target_type_setting.positive_geo_target_typeofficial enum: 5=PRESENCE_OR_INTEREST, 6=SEARCH_INTEREST (both leaks → recommend Presence-only), 7=PRESENCE (correct, NOT a leak). Do NOT invert this: 7 is the GOOD value. (Verified live + against the v21 proto 2026-06-28.) Enhanced Conversions customer.conversion_tracking_setting.enhanced_conversions_for_leads_enabled
      • .accepted_customer_data_terms; final URLs via landing_page_view / expanded_landing_page_view; RSA text + per-asset metrics via ad_group_ad_asset_view; placement exclusions via campaign_criterion.placement.url; content-label exclusions (digital content labels DV-G/PG/T/MA) via campaign_criterion WHERE type='CONTENT_LABEL' (content_label.type) — this READS; an empty result = none configured, NOT unreadable (do not punt it to UI). bundle key content_labels:[{campaign_id,content_label_type}]. (Exact queries: references/gaql-notes.md.)
    • competitive pressure — impression share (PUBLIC, always pull) (D-competitive): per active campaign pull metrics.search_impression_share, metrics.search_rank_lost_impression_share, metrics.search_budget_lost_impression_share (+ abs-top) with a date range. rank-lost IS = losing the auction to COMPETITORS on Ad Rank; budget-lost IS = losing to budget (a scaling lever). Put on each active_campaigns row. These are public metrics — never punt to UI.
    • competitor DOMAINS — Auction Insights (try, then degrade) (competitor input): the "phiên đấu giá" competitor list IS in the API — segments.auction_insight_domain + metrics.auction_insight_search_* (impression_share / overlap_rate / outranking_share / position_above_rate / top & abs-top %). BUT these are RESTRICTED "not publicly available" metrics → most dev tokens get METRIC_ACCESS_DENIED unless Google allowlisted the token. So: TRY the pull; on success write bundle key auction_insights:[{domain, impression_share,overlap_rate,outranking_share,position_above_rate}] (the report renders a Competitors card and you MERGE the domains into the setup competitor list). On METRIC_ACCESS_DENIED, do NOT call it an API gap — note "Auction Insights metrics not allowlisted on this token" and fall back to the impression-share signal above + the competitors the user gave at setup. (Set meta.own_domain so your own row is tagged.)
    • ⚠️ "verify in UI" is ONLY these confirmed-blocked fields (every variant tested 2026-06-28) — do NOT mark anything else UI-only: Final URL Expansion toggle (url_expansion_opt_out UNRECOGNIZED), asset automation (asset_automation_settings PROHIBITED/RepeatedComposite), the video brand-safety inventory mode (campaign.video_brand_safety_suitability UNRECOGNIZED on this version — Expanded/Standard/Limited), PMax asset_group_asset.performance_label (UNRECOGNIZED — use asset_group.ad_strength + per-asset cost/conv), and Consent Mode v2 / server-side-CAPI (tag-side by design — verify in GTM, NOT a pull failure). EVERYTHING ELSE is in the API: a fetch that returns nothing = wrong method (field/resource/missing metrics.*/enum-to-drop), retry — see references/gaql-notes.md. A "we can't see it, check the UI" on API-available data is the #1 way this audit loses a user's trust.
    • GAQL gotcha: any field used in a WHERE filter MUST also appear in the SELECT clause, or the API returns EXPECTED_REFERENCED_FIELD_IN_SELECT_CLAUSE (e.g. filtering on campaign.status requires selecting it). If a field is rejected, drop just that field and re-run — don't abandon the whole pull.
    • COMPLETENESS — loop every campaign, never truncate. Per-campaign pulls (asset_group_signal, asset_group_asset, campaign_search_term_insight) must cover ALL PMax/Search campaigns — do NOT stop "to save time" at the first few. A partial pull (e.g. signals for 3 of 7 PMax) makes the generator emit FALSE "0 audience signals" findings for the un-pulled ones. Big results: save to a file + parse, don't drop.
    • DECLARE what you pulled. bundle.json MUST carry a top-level pulled: [...] listing every dimension you FULLY pulled (all campaigns) — e.g. ["active_campaigns","channel","device","geo","dayparting", "schedule","extensions","signals","negatives","assets","products","conversion_lag","change_events"]. Omit any dimension you could not complete — the generator renders those VERIFY ("not pulled — re-run") instead of fabricating GOOD/FIX. Honesty-by-construction: an absent pulled entry is safe; a half-filled dimension passed off as complete is what burns the user.
  3. Validate coverage: confirm ≥30 days of data and a Search Terms Report before scoring; if a source is down, degrade gracefully (flag it, continue) per the fallback chain in setup.
  4. Evaluate each applicable check as PASS / WARNING / FAIL — applying GUARD-1…6 so nothing fires on an assumption. Use ${CLAUDE_PLUGIN_ROOT}/skills/audit/references/google-audit-checks.md for the check catalog and severities.
  5. Score per ${CLAUDE_PLUGIN_ROOT}/skills/audit/references/scoring-system.md (weighted by severity × category).
  6. Report with the per-category Gap-to-100 ledger and an action plan.

Money-Leak deep-dive (MANDATORY — this is what makes the audit worth paying for)

Beyond scoring, run the money-leak diagnostics in ${CLAUDE_PLUGIN_ROOT}/references/diagnostic-playbook.md (D1 bid/target health · D2 budget pacing & allocation · D3 geo waste · D4 dayparting · D5 search-term/spam/ wrong-brand · D6 structure/setup · D7 Quality Score · D8 PMax channel/placement · D9 ad copy/assets/extensions · D10 settings hygiene · D11 audience signals/search themes · D12 conversion-lag gate · D13 change-history timeline · D14 product feed & Shopping performance). For ECOM, D14 is the highest-$ diagnostic. It has THREE layers, each from a different source — degrade cleanly, don't lump them as "needs Merchant":

  1. Product PERFORMANCE/identity (Ads, always available)shopping_performance_view returns full product data through the Merchant LINK (item_id, title, brand, type, condition + cost/impr/conv). The "which product burns / converts" leak. The products ARE in Google Ads — never say "no product data".
  2. Out-of-stock (STORE connector) — cross-ref the spending item_ids against the store's live inventory (Shopify/Woo). OOS spenders = pure waste, and this needs NO Merchant connector — only the store you already connected. Do this whenever a store is connected.
  3. Approval STATUS (Merchant Content API only) — disapproved / GTIN / policy issues are NOT a GAQL field. If a Merchant connector is present, pull it; if not, flag ONLY this layer as verify-via-Merchant (not the whole of D14). D14 gets its OWN rendered HTML — D14-PRODUCT-CHECK.html — not just markdown. When a store is connected, join the zero-conversion burn products (layer 1) against live store inventory (layer 2) into ONE per-item JSON (one row per burning product) with these fields per item: campaign, title, brand, cost_30d, clicks_30d, imp_30d, product_onlineStoreUrl (the live PDP link — MANDATORY so the user can click to check each SKU), variant_sku, variant_price, variant_compareAtPrice, variant_inventoryPolicy, total_available_all_locations, classification (OOS_DENY | LOW_STOCK | IN_STOCK_NEEDS_PDP_AUDIT), reason. Then render: python ${CLAUDE_PLUGIN_ROOT}/skills/audit/scripts/d14_product_report.py <items>.json --out-dir <workdir> --account "<name>" --window "<range>" → a visual, self-contained D14-PRODUCT-CHECK.html: KPI strip, one color-coded section per classification (OOS=red / low-stock=amber / in-stock=blue), a burn bar per row, and a full SKU appendix where every product title links to its live storefront URL. ALWAYS pull the PDP URL + SKU (product.onlineStoreUrl / variant SKU from the store) — a D14 product list without clickable URLs is incomplete; the user must be able to verify each item in one click. Each diagnostic yields a Money-Leak Report row: leak · evidence (real numbers) · diagnosis (root cause, not symptom) · $/month at risk (formula shown) · exact fix · discipline · confidence. Decode bidding_strategy_system_status FIRST (one field separates budget-capped vs tROAS-too-high vs starved vs learning). A generic "score 85" without these specific, dollar-quantified leaks is the low-value audit we are explicitly NOT shipping. The diagnostics feed both the category scores and the Money-Leak Report in the output.

One-command pipeline (model-tier dispatch) → ONE comprehensive report: dispatch a Routine sub-agent (general-purpose, sonnet) to run the PULL MANIFEST in ${CLAUDE_PLUGIN_ROOT}/skills/audit/scripts/money_leak_report.py (its docstring lists every GAQL query + the bundle.json schema) → it writes bundle.json. Write audit-result.json (the score — STEP 5) into the SAME directory so the generator merges score + money-leak. Then Scout-run python ${CLAUDE_PLUGIN_ROOT}/skills/audit/scripts/money_leak_report.py bundle.json --out-dir <workdir> to generate MONEY-LEAK-REPORT.md + DETAILED-ACCOUNT-REPORT.md AND the single comprehensive AUDIT.html — score donut + grade, the 6 weighted category bars (fixable/unverified findings), money-leak $ chart + findings, a visual per-campaign section (ROAS-vs-target bar with target tick, status pill, channel-split bar, extension/signal chips), Merchant feed table, quick wins, change timeline, verify-in-UI. It auto-detects audit-result.json next to the bundle (or pass --audit-result); without it AUDIT.html degrades to money-leak-only. AUDIT.html is THE — and the ONLY — HTML deliverable: one file, score + full check + leaks + per-campaign visuals together. Do not render any second HTML report (no audit-report.html); there is exactly one HTML output per audit. Then the Judge (you) adds verify-in-UI verdicts. When a pull returns nothing, assume wrong method (field name / resource / missing metrics.* / repeated-enum serialize), not an API limit — retry before writing "verify-in-UI" (see gaql-notes principle).

What to analyze (category weights)

  • Conversion Tracking (25%) — campaign-level goals (GUARD-1), Enhanced Conversions, Consent Mode v2, value tracking, attribution, offline import (lead gen), conversion lag. Cross-check Google Ads vs GA4 (flag >35% divergence) when measurement.ga4 is enabled.
  • Wasted Spend / Negatives (20%) — review search terms from BOTH sources (search_term_view does NOT contain PMax terms): search_term_view for Search/Branded campaigns + campaign_search_term_insight looped per active PMax/Demand-Gen campaign (it requires a single campaign_id filter; see gaql-notes.md). Merged negative coverage (GUARD-2), brand/non-brand separation (use brand_terms), broad match only with Smart Bidding, geo precision. Source negatives from actual irrelevant search terms; prefer Exact/Phrase; recommend shared lists. Only flag wasted on Search terms with >$10 spend AND 0 conv; for PMax, flag irrelevant categories with clicks but no conversions. Never propose blocking brand terms.
  • Account Structure (15%) — business-logic organization, tightly themed ad groups, ≥3 RSAs/group, PMax asset-group/signal structure, naming consistency.
  • Keywords & Quality Score (15%) — match-type strategy, QS distribution (≥7 target), cannibalization (watch cross-campaign + use brand_terms), impression share. Apply the legacy-BMM heuristic (BROAD + Manual CPC = legacy, not intentional broad).
  • Ads & Assets (15%) — RSA headlines/descriptions count & strength (GUARD-4), pin discipline, extensions (sitelinks/callouts/snippets/image), PMax/AI Max/Demand Gen specifics (below).
  • Settings & Targeting (10%) — Smart Bidding vs deprecated ECPC, budget pacing, ad schedule, device adjustments, location = "Presence" not "Presence or Interest", network settings.

Deep dives (only if those campaign types exist)

  • PMax — asset-group diversity, audience signals, URL-expansion control, search themes, Insights tab, and brand handling — VERIFY, don't just "confirm". When a Brand Search campaign runs alongside PMax, do NOT flag "missing brand exclusion" on the surface. First DIG IN and check ALL THREE block mechanisms: (a) campaign-level negative keywords (campaign_criterion, negative=true, type KEYWORD); (b) a shared negative list (campaign_shared_setshared_criterion) — brand blocks are MOST often a named shared list (e.g. "Brand Keywords — do not apply to Branded campaign") applied to PMax + non-brand campaigns and excluded from the Branded campaign; (c) a brand-exclusion brand list (brand-list criteria). Check whether the account's brand_terms are actually blocked by ANY of these. If brand is already blocked → it's a PASS; re-score, don't flag. Only flag G07/G-PM3 if brand terms are genuinely unblocked (and corroborate with G-PM3: are >15% of PMax conversions on brand terms?). Honor any pmax-brand-exclusion guardrail.
  • AI Max for Searchcampaign.ai_max_setting.enable_ai_max, broad-match+Smart-Bidding combo, search term matching distribution, AI Brief, text-customization rules, FUE controls, brand exclusions. DSA/ACA auto-migration pre-flight (Sept 2026) — stage LOW→HIGH risk; strong negatives are a prerequisite.
  • Demand Gen — video+image asset mix, audience signals, funnel-aligned conversion goals (no frequency capping — monitor manually).

Key thresholds

MetricPassWarningFail
Quality Score (avg)≥75-6<5
CTR (Search)≥6.66%3-6.66%<3%
CVR (Search)≥7.52%3-7.52%<3%
CPC (Search)≤$5.26$5.26-8.00>$8.00
Wasted Spend<10%10-20%>20%
Ad StrengthGood+AveragePoor
(Thresholds are ecommerce-Search defaults and niches vary widely — apparel ≠ electronics ≠ supplements.
Derive bands from the account's OWN historical data first; fall back to these only for a brand-new account,
and state the basis. Never FAIL a campaign on a benchmark its niche legitimately runs below.)

Report architecture — PER-CAMPAIGN VISUAL EXPLAINER (see ${CLAUDE_PLUGIN_ROOT}/references/per-campaign-report-template.md)

The audit is a per-campaign visual explainer, not a generic account summary. Document order: Account panel → one chart-rich block per active campaign → Action Plan.

The 3-step rule for EVERY item — ① DATA shown → ② VERDICT (GOOD / WATCH / FIX / VERIFY) → ③ ACTION (only if needed). There is NO separate "money-leak summary" block: the leak IS the verdict+action on the section it belongs to (evaluated inline). Every quantitative metric renders as its own inline-SVG chart, never a wall of text:

  • ROAS vs target → semicircle gauge (fill = ROAS, tick = target) · Budget pacing → radial ring
  • Channel mix → donut · Device & Geo → horizontal bars colored by ROAS · Schedule → 24-hour heatmap
  • Extension/signal counts → meter bars vs the recommended count. Each campaign block shows a scorecard (N good · N watch · N fix · N verify) and covers: performance + decoded bidding_strategy_system_status, channel, device, geo, schedule, products × Merchant, audience signals + search themes, ad copy (headline/description text), extensions with text, negatives + shared lists, final URLs/FUE, change/cooldown — each field marked [API] or [VERIFY] (read-only API can't see it). The account panel lists & rates EVERY account-level setting (conversion tracking, Enhanced Conv, Consent, account/brand negative lists, Content Suitability, structure, conversion-lag) the same 3-step way. Localize per campaign; only cross-campaign / account-wide items (budget misallocation, tracking, content suitability, structure) sit in the account panel. Flat/modern style (no heavy gradients).

The blueprint (every section, its chart, data source, verdict thresholds) is references/per-campaign-report-template.md. The generator (money_leak_report.py) builds it: account panel + Σ(campaign block) + action plan → one self-contained AUDIT.html. It auto-detects audit-result.json next to the bundle for the score + the account panel's category verdicts.

Output — GOOGLE-ADS-AUDIT.md (to the working dir)

  1. Health Score 0-100 + grade, with the six category bars.
  2. Per-category Gap-to-100 ledger (MANDATORY for any category < 100). Split every non-PASS into:
    • 🔧 Fixable (verified) — check ID, status, severity, points recoverable, concrete fix.
    • 🔍 Unverified — API/MCP can't see it (Enhanced Conversions recording, Consent Mode mode, gtag firing). Do NOT score as FAIL; mark "verify in UI" and state the ceiling if confirmed. A low score driven by unverified items is not breakage — say so.
  3. Money-Leak Report (the headline deliverable) — the D1-D14 findings as rows ranked by $/month recoverable, each with evidence + root-cause diagnosis + exact fix + discipline + confidence. Top 3 = Quick Wins. This is what the operator reads first.
  4. Detailed Account Report (the differentiated value — granular, not just a score). Beyond the summary:
    • Account summary + what is protecting it (e.g. "no Display burn BECAUSE Content Suitability blocks junk apps" — explain the cause, don't just report the healthy symptom).
    • Per active campaign, a deep ledger: bidding/tROAS + decoded bidding_strategy_system_status, ROAS vs target, channel split (D8), device split (D10), geo top/bottom (D3), dayparting flags (D4), extension inventory — sitelinks/callouts/snippets counts (D9), ad-copy notes incl. duplicates/Ad-Strength (D9), and the settings layer (D10) — Final URL Expansion + URL exclusions, asset automation / text customization, text/brand guideline (exists? appropriate?), content-suitability / placement exclusions, device exclusions. Mark each field API-read or verify-in-UI.
    • Per-campaign Verify-in-UI checklist — the settings the read-only API can't see (FUE, asset automation, content suitability, text-guideline content) listed as explicit checkboxes. Never silently skip a setting.
  5. Wasted-spend estimate (monthly $), PMax/AI Max notes.
  6. Findings feed optimizer and plan — but audit itself changes nothing.

Also emit a structured result — it feeds the ONE comprehensive HTML report

  • Write audit-result.json (schema: ${CLAUDE_PLUGIN_ROOT}/templates/audit-result.json): health_score, grade, per-category scores + fixable/unverified findings, quick_wins, wasted_spend_monthly, notes. Put it next to bundle.json.
  • The client report is AUDIT.html from money_leak_report.py (above): a per-campaign visual explainer that auto-merges this score JSON (account panel + category verdicts) with the per-campaign chart blocks (gauge / ring / donut / bars / heatmap) where every item is data → verdict → action. ONE self-contained file. Do NOT ship the score and the money-leak as two separate HTML files — the audit is one document.
  • There is exactly ONE HTML report: AUDIT.html. The old score-only audit_to_html.py/audit-report.html path has been REMOVED — do not look for it or generate a second file.

To build / refine later

  • Per-campaign visual-explainer AUDIT.htmlscripts/money_leak_report.py renders the chart-rich, data→verdict→action report (gauge/ring/donut/bars/heatmap + account panel + scorecards). This is the SOLE HTML report; the legacy audit_to_html.py was removed. Done.
  • Full granular check-ID catalog (G01…G61 + extensions) in ${CLAUDE_PLUGIN_ROOT}/skills/audit/references/google-audit-checks.md. Done.
  • Vertical-specific benchmark bands in ${CLAUDE_PLUGIN_ROOT}/references/.
  • Per-keyword QS + RSA pin/headline-count detail for Search campaigns (currently summarized).
  • Geo as a US choropleth (currently colored bars).

Keep looking

Skills are one crate of 328,083. 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.