Claude Code Skill for Meta Ads Audits

Claude Code skill auditing Meta Ads export for waste patterns

I run Meta ads across a book of accounts. Most weeks that's somewhere between 30 and 50 active ad sets to scan. Monday morning, I used to open Ads Manager, click into each account, sort by spend, and try to eyeball which ad sets were burning money on tired creative or audience overlap.

That worked when I was managing one account. It stopped working as soon as the count grew. The problem isn't that the patterns are hard to spot — it's that there are too many rows, the data lives behind too many clicks, and the patterns I care about are the ones I'm most likely to miss when I'm bored.

So I built a Claude Code skill that does the audit for me. The starter version is below. You can paste it into your machine and run it today. The version I actually use is more involved, and I'll get to where that lives at the end.

What a Claude Code skill is

A skill is a directory in ~/.claude/skills/ with a SKILL.md file and any helper scripts. When you trigger it, Claude reads the SKILL.md, follows the instructions inside, and runs the scripts. That's the whole thing. No framework, no SDK. Just a folder Claude knows how to read.

The 3 waste patterns this starter flags

The starter version checks for the three patterns I see most often across client accounts. These aren't every waste pattern. They're the three that account for most of the obvious money leaks.

  • High-frequency audiences — Ad sets running at frequency above 3.5 with no creative refresh in 14 days. The audience has seen the ad too many times and conversions are dropping.
  • ROAS gap by ad set — Ad sets spending more than twice the average per ad set but returning less than half the account-average ROAS. Big spenders pulling down the account.
  • Naming-convention drift — Ad sets that don't match the account's taxonomy. Usually a sign somebody built a one-off and never came back to clean it up.

That's the starter. Three patterns. One script. Real output.

The starter skill

Two files. Copy them as-is.

First file is the skill instructions at ~/.claude/skills/meta-ads-audit/SKILL.md:

---
name: meta-ads-audit
description: Audit a Meta Ads CSV export for the 3 most common waste patterns — high-frequency audiences, ROAS-gap ad sets, and naming-convention drift. Trigger on "audit my meta ads", "audit meta ads export at [path]", "find waste in [csv]".
---

# meta-ads-audit

You audit a Meta Ads CSV export and produce a short markdown report of waste patterns.

## Inputs

The user gives you a path to a CSV exported from Meta Ads Manager. The export should be at the ad-set level, date range of the last 14 days, with these columns at minimum:
- Ad set name
- Amount spent (USD)
- Purchase ROAS (return on ad spend)
- Frequency
- Reach
- Ad delivery (active/paused)

If the user didn't give a path, ask once. Don't guess.

## Run the audit

From this skill's directory, run:

python3 scripts/audit.py


The script prints a markdown report to stdout. Return it to the user as-is. Don't reformat. Don't add commentary unless they ask.

## What the script flags

1. **High-frequency audiences** — Frequency > 3.5
2. **ROAS gap** — Spend > 2x ad-set average AND ROAS < 0.5x account average
3. **Naming drift** — Ad set name doesn't contain at least two of: a campaign tag (interest/lookalike/broad/retarget), a date code (Jan26/Feb26/etc), and a creative type (static/video/carousel)

## After the report

If the user asks "what should I pause?" — recommend pausing every flagged ad set in the ROAS-gap section. The frequency section needs creative refresh, not pausing. The naming section is hygiene, not waste.

Second file is the script at ~/.claude/skills/meta-ads-audit/scripts/audit.py:

#!/usr/bin/env python3
"""Audit a Meta Ads CSV export for 3 waste patterns. Stdlib only."""
import csv
import re
import sys
from statistics import mean

FREQ_THRESHOLD = 3.5
SPEND_MULTIPLE = 2.0
ROAS_MULTIPLE = 0.5
TAGS = ["interest", "lookalike", "broad", "retarget"]
DATE_RE = re.compile(r"\b(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)\d{2}\b", re.I)
CREATIVE = ["static", "video", "carousel", "ugc"]

def to_float(v):
    try:
        return float(str(v).replace("$", "").replace(",", ""))
    except (ValueError, TypeError):
        return 0.0

def naming_ok(name):
    n = name.lower()
    hits = 0
    if any(t in n for t in TAGS): hits += 1
    if DATE_RE.search(n): hits += 1
    if any(c in n for c in CREATIVE): hits += 1
    return hits >= 2

def main(path):
    rows = []
    with open(path, newline="", encoding="utf-8-sig") as f:
        for r in csv.DictReader(f):
            rows.append({
                "name": r.get("Ad set name", "").strip(),
                "spend": to_float(r.get("Amount spent (USD)", 0)),
                "roas": to_float(r.get("Purchase ROAS (return on ad spend)", 0)),
                "freq": to_float(r.get("Frequency", 0)),
            })
    rows = [r for r in rows if r["name"] and r["spend"] > 0]
    if not rows:
        print("No ad-set rows found. Export at the ad-set level."); return

    avg_spend = mean(r["spend"] for r in rows)
    roas_vals = [r["roas"] for r in rows if r["roas"] > 0]
    avg_roas = mean(roas_vals) if roas_vals else 0

    high_freq = [r for r in rows if r["freq"] > FREQ_THRESHOLD]
    roas_gap = [r for r in rows if r["spend"] > avg_spend * SPEND_MULTIPLE
                and r["roas"] > 0 and r["roas"] < avg_roas * ROAS_MULTIPLE]
    naming = [r for r in rows if not naming_ok(r["name"])]

    print(f"# Meta Ads Audit\n\n{len(rows)} ad sets | avg spend ${avg_spend:.0f} | avg ROAS {avg_roas:.2f}\n")
    print(f"## High-frequency audiences (freq > {FREQ_THRESHOLD})\n")
    for r in sorted(high_freq, key=lambda x: -x["freq"])[:10]:
        print(f"- **{r['name']}** — freq {r['freq']:.1f}, spend ${r['spend']:.0f}")
    print(f"\n## ROAS gap (spend > {SPEND_MULTIPLE}x avg, ROAS < {ROAS_MULTIPLE}x avg)\n")
    for r in sorted(roas_gap, key=lambda x: -x["spend"])[:10]:
        print(f"- **{r['name']}** — spend ${r['spend']:.0f}, ROAS {r['roas']:.2f}")
    print(f"\n## Naming-convention drift\n")
    for r in naming[:10]:
        print(f"- {r['name']}")

if __name__ == "__main__":
    main(sys.argv[1])

Both files are runnable as written. The script uses stdlib only — no pandas, no pip install.

How to use it

Three steps. First, make the directory and cd in:

mkdir -p ~/.claude/skills/meta-ads-audit/scripts
cd ~/.claude/skills/meta-ads-audit

Second, save the two files above as SKILL.md and scripts/audit.py. Make the script executable if you want:

chmod +x scripts/audit.py

Third, export a CSV from Ads Manager at the ad-set level for the last 14 days. Then from Claude Code, say:

audit my meta ads export at ~/Downloads/account-export.csv

Claude reads the skill, runs the script, and prints the report back to you. Total time from "open terminal" to "audit report on screen" is about 20 seconds, most of which is the CSV export from Meta.

Example output

Here's what a report looks like on a real account. Names changed.

# Meta Ads Audit

42 ad sets | avg spend $187 | avg ROAS 2.14

## High-frequency audiences (freq > 3.5)

- **Interest - Foodies & NYC Nostalgia - Feb26** — freq 5.2, spend $412
- **Lookalike 1% Purchasers - Static - Jan26** — freq 4.8, spend $389

## ROAS gap (spend > 2x avg, ROAS < 0.5x avg)

- **Broad - All Ages - Video - Feb26** — spend $620, ROAS 0.84

## Naming-convention drift

- Test ad set new audience
- Copy of Interest - Brunch Box - v2

Three real findings I can act on before my coffee finishes brewing. The broad ad set goes paused. The two high-frequency ad sets get new creative this week. The two oddly-named ones get renamed or archived.

Where the starter falls short

The starter is useful. It's also limited on purpose. Here's what it doesn't do.

  • One-shot CSV — You have to export the file every time. No live API pull, no scheduled audits, no Monday-morning email.
  • Three patterns out of fifteen — The patterns the production version checks include frequency-by-placement (Stories vs Feed often drift apart), audience overlap (two ad sets bidding against each other), attribution-window drift (7-day vs 1-day ROAS gap), and ad-fatigue curves (CTR decay over time). The starter doesn't touch any of those.
  • Single account at a time — One CSV, one account. If you run multiple accounts you do this each time.
  • No alerting — Nothing tells you when a pattern crosses a threshold. You have to remember to run it.
  • No naming learning — The starter hard-codes the taxonomy. The production version reads your existing ad set names, infers your convention, and flags drift against that — so it works on any account without configuration.

The production version

The full skill is part of the Ad Operator pack on operatorstack.app/packs/ad-operator ($149). It pulls live from the Meta Marketing API, runs 15+ waste patterns instead of three, handles multiple accounts in one command, and auto-runs every morning via launchd so the report lands in your inbox before you open the laptop.

I'm not going to oversell it. If the starter solves your problem, use the starter. The production version exists because the CSV workflow stops scaling fast once you're auditing more than one account on a regular cadence.

Bonus: API access

If you want to skip the CSV step in the starter, wire a Meta Marketing API access token. Once you have one, swap the csv.DictReader call in audit.py for an API call against the /insights endpoint at the ad-set level. Same script, same output, no manual export. You can then schedule it via launchd to run every morning at 7am. Meta's API docs cover the auth flow at developers.facebook.com/docs/marketing-apis.

Closing

The starter solves one Monday-morning chore on one account. It took me about 40 minutes to build and it's saved me an hour a week since. That's the right shape for a Claude Code skill — small, sharp, and pointed at a specific recurring task.

Build the starter. If you want the version that runs across all your accounts on a schedule, grab the Ad Operator pack. Or hire Clare Digital to run audits across your accounts.

Want these workflows without building them yourself?

This is one of the workflows I packaged into The Operator: pre-built Claude Code skills for marketers you can install and run today, plus The Lab, where new skills land every month. One-time payment, not a subscription.

Get The Operator for $397

Launch price, going up as the Lab grows. Prefer it done for you? Book a call with Clare Digital.