google-ads-audit
Skill11 starsChanged 5 months agoMITView on GitHub
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".
15 other projects use a version of this.
Adopted by
Most recently taken up by
- oivanfelipe/claude-skillstheir own version, first seen Nov 8, 2026
- optmyzr-skills/google-ads-audittheir own version, first seen Nov 1, 2026
- irinabuht12-oss/marketing-skillstheir own version, first seen Oct 27, 2026
- tidycatc132/claude-skillstheir own version, first seen Oct 25, 2026
- thescottking/b2b-pmm-skillstheir own version, first seen Oct 25, 2026
- nowork-studio/notfair-plugintheir own version, first seen Oct 14, 2026
Used with
Projects with it also use linkedin-ads-audit, meta-ads-audit and 2 more.
Other files people use alongside this one.
Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
gmmh1Skilllinkedin-ads-audit
Perform a full LinkedIn Ads account health check. Triggers on 'audit my LinkedIn Ads account', 'is this LinkedIn account set up well', 'inherited this LinkedIn account, check it', or 'is this account ready to scale'. Broad structural review — for a specific known symptom, use linkedin-ads-optimization instead.
Also in 4 projects that use this
gmmh1Skillmeta-ads-audit
Perform a full Meta Ads account health check. Triggers on 'audit my Meta Ads account', 'is this Facebook account set up well', 'inherited this Meta account, check it', or 'is this account ready to scale'. Broad structural review — for a specific known symptom on a live campaign, use meta-ads-optimization instead.
Also in 5 projects that use this
chrispanggSkillcontent-writer
Writing content across different platforms and styles. This skill should be used when creating blog posts, LinkedIn posts, X/Twitter threads, technical documentation, or other written content. It intelligently selects the appropriate writing style based on the platform, audience, and content type, then applies that style consistently throughout the piece.
Also in 3 projects that use this
indranilbanerjeeSkillkeyword-research
Research keywords via the connected data source: intent, gaps. Grouping → keyword-cluster. \"what keywords should we target\"
Also in 4 projects that use this
Other versions15 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 15
- irinabuht12-oss/marketing-skills★ 6.5ksame name
- nowork-studio/toprank★ 4.1ksame name
- nowork-studio/notfair-plugin★ 4.1ksame name
- cognyai/claude-code-marketing-skills★ 106same name
- itallstartedwithaidea/google-ads-skills★ 38same name
- optmyzr-skills/google-ads-audit★ 14same name
- advisorppc-org/advisorppc-plugin★ 1same name
- advisorppc/advisorppc-plugin★ 1same name
- kastriasani/google-ads-skills★ 1same name
- 1clickreport/skills★ 0same name
5 more not listed
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository14 other files it gives its agents.
- google-ads-assetsSkill
- google-ads-builder-branded-searchSkill
- google-ads-builder-demand-genSkill
- google-ads-builder-pmaxSkill
- google-ads-builder-searchSkill
- google-ads-experimentsSkill
- google-adsSkill
- google-ads-measurementSkill
- google-ads-optimizerSkill
- google-ads-planSkill
- google-ads-pusherSkill
- google-ads-routineSkill
- google-ads-setupSkill
- google-ads-trackerSkill
The file
We split this skill into the rules it gives an agent
11 rules. 0 are common, 1 is distinctive, and 10 are very specific to this project.
0
Common
Most of the 12 edited versions we read keep these.
1
Distinctive
This author's own choices. Worth a look.
10
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 13
Read-only diagnostic. Score the account, surface issues by severity, and — critically — never fire a finding on an unverified assumption.
- Very specific: line 17
Read account-context.yaml from the working directory.
- Very specific: line 41
After merging all four, declare pulled:[...,"negatives_complete"] so the report renders a real verdict.
- Very specific: line 43
GUARD-3 Budget scaling — check
change_eventfirst: Before recommending a budget/bid increase, pull recent change history (~14d). - Very specific: line 46
GUARD-4 Ad strength — confirm ENABLED + impressions: Before flagging POOR ad strength, confirm ad_group.status AND ad_group_ad.status are …
- Very specific: line 49
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 …
- Very specific: line 91
search terms both sources: search_term_view + per-PMax campaign_search_term_insight
- Very specific: line 110
Search/Shopping dominates.
- Distinctive: line 201
If a Merchant connector is present, pull it.
- Very specific: line 246
NOT contain PMax terms): search_term_view for Search/Branded campaigns + campaign_search_term_insight
- Very specific: line 343
Write audit-result.json (schema: ${CLAUDE_PLUGIN_ROOT}/templates/audit-result.json): health_score, grade, per-category scores + …
“Common” is measured: kept by at least 6 in 10 of the 12 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
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)
- Read
account-context.yamlfrom the working directory. If missing, runsetupfirst. - Pull from it:
google_ads.customer_id,brand_terms,competitor_terms,margin_tiers,data_source(for landing-page URL checks),measurement, andguardrails. - 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), checkprimary_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_set→shared_set→shared_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. Acampaign_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, declarepulled:[...,"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_eventfirst. Before recommending a budget/bid increase, pull recent change history (~14d). A recent budget/bid/asset/structure change → recommend cooldown, not scale. (Strengthen with anychange-event-cooldownguardrail in context.) - GUARD-4 Ad strength — confirm ENABLED + impressions. Before flagging POOR ad strength, confirm
ad_group.statusANDad_group_ad.statusare 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 itsmin_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; themoney_leak_report.pyrender (→ the singleAUDIT.html) after the Judge writesaudit-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-PMaxcampaign_search_term_insightloop). Dispatch asgeneral-purposesub-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
- 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. - 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_viewcan'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_nameusage (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_typegives the coverage (SITELINK / CALLOUT / STRUCTURED_SNIPPET / PRICE / IMAGE). Two gotchas, both seen live: (1)campaign.statusMUST be in the SELECT because it's filtered (elseEXPECTED_REFERENCED_FIELD_IN_SELECT_CLAUSE); (2) do NOT also selectasset.typehere — it throws INVALID_ARGUMENT on many MCPs (pull asset text separately if needed). Countfield_typeper 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-PMaxcampaign_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 onlystatus=2ENABLED; many lists arestatus=3REMOVED/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 acampaign_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 pulledshared_criterionANDcustomer_negative_criterion. After merging, declarepulled:[...,"negatives_complete"]. - PMax Brand exclusions are a SEPARATE layer from negative keywords — pull them explicitly:
campaign_criterionselectingcampaign_criterion.brand_list.shared_set+.negative(filtercampaign_criterion.brand_list.shared_set IS NOT NULL), then resolve eachshared_set.name. These rows have an EMPTYkeyword.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_typeper campaign — this WORKS for PMax too (network 2 Search/Shopping dominates; small YouTube/Display/cross). NEVER report channel as "verify in UI". Pullmetrics.conversionsalongside cost — a cost-only pull makes every network look like 0-conv burn. - device split (D10):
segments.deviceper campaign (2 Mobile · 3 Tablet · 4 Desktop · 5 CTV · 6 Other). - dayparting (D3/D4):
segments.hour+segments.day_of_weekper campaign + metrics — for the hourly heatmap and best/worst hour/day. And ad schedule:campaign_criterion.ad_schedule.*+bid_modifier(a flatbid_modifier=0across the window = NO dayparting bid strategy — a real opportunity finding). - geo — ALL regions (D3):
geographic_viewscoped to active campaigns,metrics.cost_micros > 0, no top-N limit. Then resolve EVERYsegments.geo_target_regionid to a name viageo_target_constant(id, name, canonical_name). RawgeoTargetConstants/NNNNNids 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_assetDOES 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.headlinesis a RepeatedComposite the serializer can't return — get RSA asset text + metrics fromad_group_ad_asset_viewinstead (don't write "verify in UI"). The ONLY genuine UI-only asset field isasset_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_assetjoined to theassettext fields (asset.sitelink_asset.link_text,asset.callout_asset.callout_text,asset.structured_snippet_asset.values,asset.price_asset.*) or theassetresource by id. bundle keyext_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_type— official 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 Conversionscustomer.conversion_tracking_setting.enhanced_conversions_for_leads_enabled.accepted_customer_data_terms; final URLs vialanding_page_view/expanded_landing_page_view; RSA text + per-asset metrics viaad_group_ad_asset_view; placement exclusions viacampaign_criterion.placement.url; content-label exclusions (digital content labels DV-G/PG/T/MA) viacampaign_criterionWHEREtype='CONTENT_LABEL'(content_label.type) — this READS; an empty result = none configured, NOT unreadable (do not punt it to UI). bundle keycontent_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 eachactive_campaignsrow. 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 getMETRIC_ACCESS_DENIEDunless Google allowlisted the token. So: TRY the pull; on success write bundle keyauction_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). OnMETRIC_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. (Setmeta.own_domainso 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_outUNRECOGNIZED), asset automation (asset_automation_settingsPROHIBITED/RepeatedComposite), the video brand-safety inventory mode (campaign.video_brand_safety_suitabilityUNRECOGNIZED on this version — Expanded/Standard/Limited), PMaxasset_group_asset.performance_label(UNRECOGNIZED — useasset_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/missingmetrics.*/enum-to-drop), retry — seereferences/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
WHEREfilter MUST also appear in theSELECTclause, or the API returnsEXPECTED_REFERENCED_FIELD_IN_SELECT_CLAUSE(e.g. filtering oncampaign.statusrequires 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.jsonMUST carry a top-levelpulled: [...]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 absentpulledentry is safe; a half-filled dimension passed off as complete is what burns the user.
- 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. - 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.mdfor the check catalog and severities. - Score per
${CLAUDE_PLUGIN_ROOT}/skills/audit/references/scoring-system.md(weighted by severity × category). - 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":
- Product PERFORMANCE/identity (Ads, always available) —
shopping_performance_viewreturns 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". - 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.
- 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-containedD14-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. Decodebidding_strategy_system_statusFIRST (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.ga4is enabled. - Wasted Spend / Negatives (20%) — review search terms from BOTH sources (
search_term_viewdoes NOT contain PMax terms):search_term_viewfor Search/Branded campaigns +campaign_search_term_insightlooped per active PMax/Demand-Gen campaign (it requires a singlecampaign_idfilter; see gaql-notes.md). Merged negative coverage (GUARD-2), brand/non-brand separation (usebrand_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_set→shared_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'sbrand_termsare 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 anypmax-brand-exclusionguardrail. - AI Max for Search —
campaign.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
| Metric | Pass | Warning | Fail |
| Quality Score (avg) | ≥7 | 5-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 Strength | Good+ | Average | Poor |
| (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)
- Health Score 0-100 + grade, with the six category bars.
- 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.
- 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.
- 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.
- Wasted-spend estimate (monthly $), PMax/AI Max notes.
- Findings feed
optimizerandplan— 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 tobundle.json. - The client report is
AUDIT.htmlfrommoney_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-onlyaudit_to_html.py/audit-report.htmlpath has been REMOVED — do not look for it or generate a second file.
To build / refine later
- Per-campaign visual-explainer
AUDIT.html—scripts/money_leak_report.pyrenders the chart-rich, data→verdict→action report (gauge/ring/donut/bars/heatmap + account panel + scorecards). This is the SOLE HTML report; the legacyaudit_to_html.pywas 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).
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

