Ad spy
Skill NovateStudioGit/novate-studio-skills/paid-growth/ad-spy
57 agent skills for Claude Code — creative production, paid growth, copywriting, ecommerce, email marketing & knowledge ops. By Novate Studio.
npx -y skills add NovateStudioGit/novate-studio-skills --skill ad-spyAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 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
Pull live Meta ads from the Ad Library for a competitor brand — page-ID-resolved, DCO-unwrapped, ranked by longevity, classified by creative format. Triggered by /ad-spy <brand> or "pull ads for X", "ad library teardown".
SKILL.md
5.9 KB, as published. Nobody here has run it
/ad-spy
Pull a competitor's live Meta / Facebook ads from the public Ad Library (via the Apify actor curious_coder/facebook-ads-library-scraper) and return a clean, ranked teardown. The official Meta Ad Library API only returns EU commercial ads, so scraping is required.
When to fire
/ad-spy <brand>- "pull ads for X"
- "ad library teardown for Y"
- "what's brand Z running on Meta right now"
- "show me brand A's active Facebook ads"
NOT for: owned-account performance (use METAAGENT performance.py), single-reel IG teardowns (/analyze-reel), site/offer/revenue recon (/gambit), or LinkedIn / lead-gen prospect lists (/linkedin-scraper, /lead-scraper).
Inputs
- brand (required) — display name OR a Meta Page ID (15+ digits → treated as page ID directly).
- country (default
US) — ISO code for the Ad Librarycountryfilter. - count (default
150) — max ads per page pull. Each page is hard-capped at 150 by the Ad Library; for advertisers with multiple pages, iterate page IDs.
Flow
- Resolve the page ID —
scripts/resolve_page_id.pydoes a keyword scrape, groups results bypage_name, picks the best name match, and returns(page_id, matched_name, confidence). If brand is already a numeric ID, skip resolution. Confidence ishigh(substring match),med(prefix overlap), orlow(no name overlap → flag in output). - Pull by page —
scripts/pull.pybuilds theview_all_page_id+search_type=pageAd Library URL and posts to the Apify actor. Saves raw JSON toout/<brand-slug>-<country>-page.json. Never pull by keyword for primary results — keyword returns mostly noise + mis-attributes generic names (Cadence → "Cadence OTC", Maple Diamonds → "Maple Cottage Designs", JAXXON → "JAXXON For Her"). - Parse —
scripts/parse.pywalks each ad and extracts:days_livefromstart_date→end_date(ornowif active)is_video—Trueifsnapshot.videosor anycards[].video_*_urlis set, else staticbody_text— DCO-unwrap: prefersnapshot.cards[].bodyand.titleoversnapshot.body(which is the{{product.brand}}template). Also pull fromextra_texts.angle— slug fromlink_url(e.g.brand.co/pages/<slug>→pcos,perimenopause,gut-health). Single richest persona signal.cta,display_format,collation_count,snapshot_url,page_name,ad_archive_id
- Rank + classify:
- Sort ads by
days_livedesc — long-runners = proven winners, 3-day clusters = active testing churn. - Group ads by
angle(landing-page slug) — count, median days, max days per angle. - Compute creative mix —
video %vsstatic %.
- Sort ads by
- Output:
- Raw JSON →
out/<brand-slug>-<country>-page.json - Parsed JSON →
out/<brand-slug>-<country>-parsed.json - Print a compact readout: total active ads, creative mix, angles table, top-20 longest-running, sample real (non-template) copy.
- Raw JSON →
Hard-won gotchas (DO NOT regress)
- Pull by page ID, not keyword. Keyword search returns ~60% relevant for unique brand names, far less for generic ones. Use
resolve_page_id.pyfirst. - 150-ad cap per page pull. For multi-page advertisers (e.g. Grüns runs two: 107585658730958 + 738568902671176), iterate. If
total_pulled >= 150, flagcapped: truein output and recommend paginating. - DCO copy lives in
snapshot.cards[].body/.title, NOTsnapshot.body.snapshot.bodyfor DCO ads is the literal template string{{product.brand}}. ~98% of Grüns ads were DCO/SHOP_NOW. - Video classification: check
snapshot.videosOR any ofvideo_preview_image_url/video_hd_url/video_sd_url/watermarked_video_sd_urlon cards. If none → static (cards or images). days_liveproxy:(end_date or now) - start_date) / 86400. Long-runners (60+ days) are the winning creatives; copy their angle/format.- Landing-page slug = persona/angle. Group by
link_urlslug; the slug names the avatar Meta optimised toward. resolve_page_id.pymis-PICKS on short/generic brand names even after grouping (e.g. "Ridge" → "Ridge Belts" med, "Ridge Wallet" → "Ridge Belts"; only "The Ridge" → correct page, high). When confidence ismed/lowOR the matched name looks wrong, readall_pages_seen— the dominant count is usually the real brand — and re-resolve with that exact page name (or pass--page-iddirectly). Some brands won't resolve at all via keyword scrape (e.g. "Our Place" / "From Our Place" → only ad-noise, never the brand page); substitute or get the page ID by hand rather than burning pulls.--quietis aparse.pyflag ONLY, NOTpull.py. Passing--quiettopull.pymakes argparse error out and the pull silently fails.pull.pyflags are--country --count --page-id --no-resolve --out-dir+ the brand positional. To pull without resolution, usepull.py "<name>" --page-id <id> --no-resolve.
Auth
Reads APIFY_API_TOKEN (or APIFY_TOKEN) and META_AD_LIBRARY_ACTOR from ~/Desktop/AGENTS/METAAGENT/.env. Never echo the token value.
Output structure
~/Desktop/AGENTS/METAAGENT/out/
<brand-slug>-<country>-page.json # raw Apify items
<brand-slug>-<country>-parsed.json # cleaned + ranked
Readout printed to stdout includes:
- Resolved page name + confidence flag
- Total active ads + cap status
- Creative mix (video vs static)
- Angles table (slug | count | median days | max days)
- Top-20 longest-running ads
- Sample non-template copy from the top runners
Downstream
Output is consumed by the (future) /ad-brief skill, which distils hook + angle + persona into a structured creative brief and writes it into the ExampleBrand Notion META tracker at Status=Brief.