agentsclimarketplace

Ad spy

Skill NovateStudioGit/novate-studio-skills/paid-growth/ad-spy

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".From its SKILL.md

Install
npx -y skills add NovateStudioGit/novate-studio-skills --skill ad-spy

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

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

SKILL.md

5.9 KB, ~1.6k tokens by cl100k_base, 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 Library country filter.
  • 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

  1. Resolve the page IDscripts/resolve_page_id.py does a keyword scrape, groups results by page_name, picks the best name match, and returns (page_id, matched_name, confidence). If brand is already a numeric ID, skip resolution. Confidence is high (substring match), med (prefix overlap), or low (no name overlap → flag in output).
  2. Pull by pagescripts/pull.py builds the view_all_page_id + search_type=page Ad Library URL and posts to the Apify actor. Saves raw JSON to out/<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").
  3. Parsescripts/parse.py walks each ad and extracts:
    • days_live from start_dateend_date (or now if active)
    • is_videoTrue if snapshot.videos or any cards[].video_*_url is set, else static
    • body_text — DCO-unwrap: prefer snapshot.cards[].body and .title over snapshot.body (which is the {{product.brand}} template). Also pull from extra_texts.
    • angle — slug from link_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
  4. Rank + classify:
    • Sort ads by days_live desc — 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 % vs static %.
  5. 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.

Hard-won gotchas (DO NOT regress)

  1. Pull by page ID, not keyword. Keyword search returns ~60% relevant for unique brand names, far less for generic ones. Use resolve_page_id.py first.
  2. 150-ad cap per page pull. For multi-page advertisers (e.g. Grüns runs two: 107585658730958 + 738568902671176), iterate. If total_pulled >= 150, flag capped: true in output and recommend paginating.
  3. DCO copy lives in snapshot.cards[].body / .title, NOT snapshot.body. snapshot.body for DCO ads is the literal template string {{product.brand}}. ~98% of Grüns ads were DCO/SHOP_NOW.
  4. Video classification: check snapshot.videos OR any of video_preview_image_url / video_hd_url / video_sd_url / watermarked_video_sd_url on cards. If none → static (cards or images).
  5. days_live proxy: (end_date or now) - start_date) / 86400. Long-runners (60+ days) are the winning creatives; copy their angle/format.
  6. Landing-page slug = persona/angle. Group by link_url slug; the slug names the avatar Meta optimised toward.
  7. resolve_page_id.py mis-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 is med/low OR the matched name looks wrong, read all_pages_seen — the dominant count is usually the real brand — and re-resolve with that exact page name (or pass --page-id directly). 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.
  8. --quiet is a parse.py flag ONLY, NOT pull.py. Passing --quiet to pull.py makes argparse error out and the pull silently fails. pull.py flags are --country --count --page-id --no-resolve --out-dir + the brand positional. To pull without resolution, use pull.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.

What ships with it: 3 files

18.1 KB alongside SKILL.md, 3 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.