Meta Ad Library Winning Ads Finder & Creative Brief
Pricing
from $8.00 / 1,000 ad analyzeds
Meta Ad Library Winning Ads Finder & Creative Brief
Find any brand's winning Facebook & Instagram ads from the Meta Ad Library, tiered Proven Winner (30+ days), Rising or Testing, with a creative brief of hooks, angles, formats and CTAs. Flags new ads. Keyword search mode lists the brands advertising in a niche.
Pricing
from $8.00 / 1,000 ad analyzeds
Rating
0.0
(0)
Developer
Carina Hatton
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
2 days ago
Last modified
Categories
Security
Limited permissionsShare
Find any brand's winning Facebook and Instagram ads, see which ones are Proven Winners, and get a ready-to-use creative brief.
Give this Actor a brand's Facebook page, Ad Library link or name. It pulls the brand's ads from the public Meta Ad Library and sorts every creative into Proven Winner, Rising or Testing. It then writes a brand-level creative brief covering the hooks, angles, formats, CTAs and landing pages behind the winners. Schedule it and it flags new ads since the last run, so you see a competitor's new tests as they appear.
Built for DTC brands, media buyers, creative strategists and e-commerce agencies who want to know what a competitor keeps paying to run, not just a raw dump of ads.
Selling to ecommerce brands instead of studying one? Switch to keyword search mode. Type a niche keyword like "collagen" and you get a list of the advertisers running ads on it, one row per brand, with the website their ads point to and a link to their Ad Library page. Jump to keyword search.
Why this Actor
- Winner tiers built on a media buyer's rules. The tiers follow rules from Carina Hatton, who has run Meta ads since 2013 and runs the She Crushes ECom creative agency. The core rule: an ad has to run at least 30 days to count as a winner, and a long run with many versions is the strongest signal.
- A brief, not a spreadsheet. You get one Markdown and JSON brief per brand, ready to paste into a doc or hand to a creative team.
- Monitoring built in. Run it daily or weekly. Each run marks new concepts, new versions of known concepts, and ads that stopped.
- Transparent and tunable. Every threshold and weight sits in one
scoringConfiginput, and each row shows the score components. - Prospect lists from the same Actor. Keyword search mode finds the brands paying to advertise on a keyword, so you know who has an ad budget before you pitch them.
What you get
| Output | What's in it |
|---|---|
| Ranked concepts (dataset) | One row per concept, with tier, score, days running, versions, active status, format, hook, primary text, CTA, landing page, angles, media URLs, Ad Library link and isNew / newAdIds |
Creative brief (BRIEF.md, BRIEF) | Tier counts, recommendations, top hooks with links, angle share among winners vs all, formats, CTAs, landing-page patterns and copy length |
Run summary (RUN_SUMMARY) | Counts, tier counts, the monitoring diff (new ads, new concepts, no-longer-active ads), the scoring config used and the source scraper run ID |
| Advertisers (dataset, keyword search mode) | One row per advertiser across all your search terms: page name and ID, website and domain, active ads seen, first seen date, matched terms, sample ad copy and Ad Library link |
Winner tiers
| Tier | Rule (defaults) | What it means |
|---|---|---|
| Proven Winner | Running 30+ days (hard minimum) | The brand has kept paying for it for a month or more |
| Rising | 14–29 days and 3+ versions | Young, but the brand is already iterating on it |
| Testing | Everything else | New or lightly tested |
Inside each tier, concepts are ranked by a 0–100 score. Days running carries the most weight, and versions count most when the ad has also run a long time:
days = min(daysRunning / 180, 1) ^ 0.5variation = min(ln(versions) / ln(20), 1)longRunVar = days * variationactive = 1 if still running else 0spend = only if Meta reports spend (see Limitations); otherwise dropped, never estimatedscore = 100 * (0.45*days + 0.30*longRunVar + 0.05*variation + 0.10*active [+ 0.10*spend]) / (sum of weights used)
- A concept is one Meta collation group. The Ad Library groups an ad's versions under one
collation_id, and each group is one concept. Ads are never merged by similar copy, launch date or guesswork. An ad without acollation_idis its own concept. - Versions is Meta's
collation_count, summed over the group's ad cards. - Days running runs from the start date to today if the ad is active, or to its end date, inclusive.
Sample output (AG1 by Athletic Greens, US, active ads)
This sample is from a live run on 2026-10-30 that analyzed 100 active US ads from Ad Library page 183869772601, re-scored with the collation-only grouping above. The run produced 69 concepts: 32 Proven Winners, 13 Rising and 24 Testing.
| Rank | Tier | Score | Days | Versions | Format | CTA | Hook |
|---|---|---|---|---|---|---|---|
| 1 | Proven Winner | 56.4 | 148 | 1 | DCO | Shop now | "💪 90 Days of AG1." |
| 2 | Proven Winner | 48.6 | 101 | 1 | DCO | Shop now | "🚨 Military and First Responders get 25% off AG1 🚨" |
| 4 | Proven Winner | 43.7 | 38 | 4 | DCO | Shop now | "Feeling the slump?" |
| 5 | Proven Winner | 41.7 | 38 | 3 | DCO | Shop now | "🚨AGZ is here — AG1's first-ever sleep product 🚨" |
| 34 | Rising | 45.7 | 26 | 13 | DCO | Shop now | "Hugh Jackman has been drinking AG1 since 2021…" |
Brief excerpt (examples/sample-BRIEF-ag1.md):
Based on 8 Proven Winners out of 69 concepts (100 ad cards). Tiers: 32 Proven Winner (30+ days), 13 Rising (14-29 days, several variations), 24 Testing.
- Lean into "New / launch": 50% of Proven Winners vs 34.8% of all concepts.
- Dominant format among Proven Winners: DCO (100% of Proven Winners).
- Default CTA on Proven Winners: "Shop now" (100%).
- Most Proven Winners send traffic to drinkag1.com/best-green-powder-offers.
Sample dataset row: examples/sample-concept-row-ag1.json. The sample shows one brand on one day. It describes what that brand was running, not how those ads performed.
Pricing (pay per event)
You pay only for what a run produces.
| Event | Price | When |
|---|---|---|
| Actor start | $0.00005 | Once per run (Apify's standard start fee, up to 1 GB memory) |
| Ad analyzed | $0.008 per ad | Each ad card analyzed |
| Creative brief | $0.50 per brand per run | Once per run. Includes the optional AI summary (your own key) |
| Monitoring check | $0.05 per brand per run | Once per run, only when monitoring is on |
Keyword search mode charges only the start fee and ad-analyzed. See what keyword search costs.
The ad data also costs money. Ads are fetched by calling the public Store Actor curious_coder/facebook-ads-library-scraper. That run is billed to your account at its own Store price, $0.00075 per ad plus its own $0.00005 start fee at the time of writing. You'll see it as a separate run in your Apify Console.
Worked example: one brand, 100 ads, monitoring on
| Item | Cost |
|---|---|
| This Actor: start | $0.00005 |
| This Actor: 100 ads × $0.008 | $0.80 |
| This Actor: creative brief | $0.50 |
| This Actor: monitoring check | $0.05 |
| This Actor subtotal | $1.35 |
| Scraper: 100 ads × $0.00075 + $0.00005 start | $0.075 |
| Total for the run | ≈ $1.43 |
Ways to control cost:
maxAdscaps how many ads are fetched and analyzed.scraperMaxChargeUsdcaps the scraper run.- Apify's maximum cost per run caps this Actor. The Actor first sets aside enough for the brief (and the monitoring check), then analyzes only as many ads as the rest allows. The minimum useful limit is about $0.56 with monitoring on (start + brief + monitoring check + one ad).
- Turn
monitoringoff for one-off research. dataSource: "dataset"re-analyzes ads you've already scraped, so there's no scraper cost (this Actor's events still apply).- A plain brand name may trigger a short name-search pass first (
discoveryMaxAds, 20 ads by default) to find the page ID, which adds scraper cost. A page URL or Ad Library link avoids it.
How to use it
- Paste a brand into Brand. The best input is the Ad Library link with
view_all_page_id=…. A Facebook page URL or a plain name also works. - Choose a country (default
US) and an active status (activeis best for finding current winners). - Set Max ads (100 is a good start).
- Run it, then open the Overview dataset view and
BRIEF.mdin the key-value store. - To monitor a brand, schedule the Actor with the same input. Later runs fill in
isNew/newAdIdsand the New ads view.
Example input:
{ "brand": "https://www.facebook.com/ads/library/?active_status=active&ad_type=all&country=US&media_type=all&search_type=page&view_all_page_id=183869772601","country": "US", "activeStatus": "active", "maxAds": 100, "monitoring": true, "briefTopN": 8 }
Keyword search: find advertisers in a niche
Brand mode tells you what one competitor is running. Keyword search tells you who's running ads in a niche at all, which is the list you want before a single cold email goes out.
Set mode to keywordSearch and give it one or more search terms. The Actor runs a Meta Ad Library keyword search for each term (active ads in the US by default), pages through the results up to your maxAdsPerTerm, and rolls the ads up by Facebook page. A brand that shows up under three of your terms is still one row.
A brand with active ads on "dog treats" this week has a Meta budget and a creative team (or a founder) making ads right now. That's a warmer lead than a scraped list of Shopify stores. You still have to pitch them well.
Keyword search doesn't score ads or write a brief. It builds the list. When a brand on it looks interesting, run brand mode on its adLibraryUrl to see what's working for them.
Example input:
{ "mode": "keywordSearch", "searchTerms": ["collagen", "dog treats"], "country": "US","keywordActiveStatus": "active", "maxAdsPerTerm": 100 }
Sample rows from a 50-ad test search for "collagen" (US, active ads, 2026-11-04). That run found 37 advertisers, and 35 of them had a website domain.
| Advertiser | Domain | Active ads seen | First seen | Ad Library |
|---|---|---|---|---|
| SpoiledChild | spoiledchild.com | 8 | 2026-05-09 | page 103345698622969 |
| True Sea Moss Health | trueseamoss.com | 3 | 2026-05-19 | page 101371852670671 |
| Beauty From Bees | beautyfrombees.ca | 2 | 2026-01-19 | page 2003695169857895 |
Output fields (one dataset row per advertiser. Anything Meta doesn't show is null, never guessed):
| Field | What it is |
|---|---|
pageName, pageId | The Facebook page running the ads |
website, domain | Most common site the page's ads link to, and its root domain |
websiteSource | ad_link_url or ad_caption, where the domain came from |
domainIsRetailer | true when the only domain found is a marketplace or shoppable-link service (Amazon, Costco, Sam's Club, mikmak and similar) |
otherDomains | Other domains seen in the same page's ads |
activeAdCount | Active ads from this page among the ads fetched in this run |
activeAdCountIncludingVersions | Same, counting Meta's "N ads use this creative" versions |
adsSeen | All ads from this page in this run |
activeAdCountMayBeCapped | true when a matched term had more results than maxAdsPerTerm let through |
firstSeenDate | Earliest start date among this page's ads in the run |
adLibraryUrl | The page's active ads in the Ad Library |
pageProfileUrl, pageCategories, pageLikeCount | As Meta shows them on the ad |
matchedTerms | Which of your search terms found this page |
sampleAdCopy, sampleAdUrl | Primary text of one of the page's ads (first 300 characters) and its Ad Library link |
What keyword search costs. You pay the ad-analyzed event ($0.008) once per ad processed, plus the scraper's own $0.00075 per ad. No creative brief or monitoring check is charged in this mode. An ad that matches two of your terms is charged once. maxAdsPerTerm is your cost control: 2 terms at 100 ads each is at most 200 ads, so at most $1.60 for this Actor plus $0.15 to $0.20 for the scraper. The scraper fetches results in pages of about 30 ads and can go one page past your limit. This Actor drops those extra ads and doesn't charge for them, but the scraper bills them, so its spending cap is set to cover one extra page per term (override it with scraperMaxChargeUsd).
Read these before you trust the numbers:
activeAdCountis not the brand's total. It counts only the ads this run fetched that matched your keywords. A brand with 400 active ads that appears 3 times in your results shows 3. OpenadLibraryUrlfor the full picture. With a smallmaxAdsPerTermon a big keyword, most rows will haveactiveAdCountMayBeCapped: true. In the "collagen" test, Meta reported more than 50,000 results and the run fetched 50.- Results come in Meta's impressions order, so bigger advertisers surface first. Smaller brands sit deeper in the results and need a higher
maxAdsPerTerm, which costs more. - Some pages have no domain. Their ads link to a Facebook page, a form or nothing (2 of 37 pages in the test). Others link only to a retailer, like Amazon or Costco, flagged with
domainIsRetailer. When a page's ads show both its own site and a retailer, its own site is used. - Creators running ads for a brand appear as their own advertiser with the brand's domain. In the "collagen" test, three different pages pointed to trueseamoss.com. Group by
domainif you want one row per brand. - Meta matches your words anywhere in the ad text, so expect a few off-niche advertisers.
exactPhrase: truetightens multi-word terms.
Input reference
| Field | Default | Notes |
|---|---|---|
mode | brand | brand (analyze one brand, the default) or keywordSearch (find advertisers) |
brand | required in brand mode | Ad Library URL, Facebook page URL, or brand name |
searchTerms | none | Keyword search mode. One Ad Library keyword search per term |
maxAdsPerTerm | 100 | Keyword search mode. Ads fetched per term. This drives cost. |
keywordActiveStatus | active | Keyword search mode. active, all or inactive |
exactPhrase | false | Keyword search mode. Match the exact phrase instead of all words in any order |
country | US | Ad Library country code, or ALL |
activeStatus | all | Brand mode. all, active or inactive |
maxAds | 200 | Brand mode. Cap on ads fetched and analyzed. This drives cost. |
rescopeToPage | true | After a name search, re-fetch the matched page's ads only |
discoveryMaxAds | 20 | Ads fetched in the name-search pass that only finds the page ID |
scoringConfig | see below | Tiers, weights and thresholds |
briefTopN | 10 | Concepts the brief is built from |
outputMode | all | all concepts, or winners (Proven Winners only) |
monitoring / monitorStoreName | true / winning-ads-monitor | Per-brand history in a named key-value store in your account |
dataSource / sourceDatasetId | curious_coder | apify_official (billed at that Actor's own price, brand mode only), or dataset |
scraperMaxChargeUsd | auto | Spending cap passed to the scraper run |
llmProvider / llmApiKey / llmModel | none | Optional AI summary using your own key (secret input, never written to output) |
Default scoringConfig:
{ "weights": { "daysRunning": 0.45, "longRunVariations": 0.3, "variationCount": 0.05, "isActive": 0.1, "reportedSpend": 0.1 },"daysCap": 180, "daysExponent": 0.5, "variationCap": 20, "spendCapUsd": 100000,"minDaysForWinner": 30, "winnerScoreThreshold": 0, "minDaysForRising": 14, "minVariationsForRising": 3 }
Weights are normalized, so they don't need to sum to 1. Set a weight to 0 to ignore that signal.
Limitations (please read)
- No spend or performance data for US commercial ads. Meta's Ad Library shows spend and impression ranges only for ads it classifies as political or social-issue. For ordinary commercial ads those fields are empty, and in the AG1 sample none of the 100 ads had spend data. "Winning" is therefore inferred from how long an ad runs and how many versions it has. It is not measured from sales, ROAS or spend, and the tiers are not a prediction of how an ad will perform for you. When Meta does report spend, the Actor uses Meta's range and labels it. It never estimates spend.
- Long-running ads may have been paused. Days running is measured from the start date. The Ad Library doesn't show whether an ad was paused in between.
- Strict collation grouping can split one idea into several concepts. Grouping follows Meta's
collation_idonly. If a brand launches the same copy as several separate ads, each one is a separate concept. In the AG1 sample, the hook "Sold out after launch." appeared in 20 separate collation groups. - Identical hooks can appear as separate rows. That happens when Meta lists them as separate ad groups; each row's
metaCollationIdshows Meta's own group ID. - Partial view. Ads are requested in Meta's impressions order for the page. A small
maxAdsonly covers part of a large advertiser's library. - Rejected page URLs. The scraper rejects some Facebook page URLs ("Page is private"). The Actor then falls back to a name search plus page re-scoping, which costs extra scraper calls.
- English-centric angles. Angle detection is keyword based and works best on English copy.
- Not affiliated with Meta. The Actor reads only the public Ad Library through a third-party Store scraper, and results depend on that scraper and on Meta's Ad Library.
FAQ
Does it show how much a brand spends? Not for commercial ads, because Meta doesn't publish it (see Limitations).
Can I change the 30-day rule? Yes. Set scoringConfig.minDaysForWinner and the other thresholds yourself. The defaults follow Carina Hatton's rules.
Can I get a list of brands advertising in my niche? Yes. That's keyword search mode. It returns advertisers, not ranked ads, and the ad counts cover only what the run fetched (see Keyword search).
Is the AI summary required? No. The brief is rule-based and needs no LLM. The AI summary is optional and uses your own OpenAI or Anthropic key, and that provider bills you directly for tokens.
Local development
npm install && npm test # unit tests (node:test)apify run # uses storage/key_value_stores/default/INPUT.json