Facebook Ads Competitor Intelligence
Pricing
from $50.00 / 1,000 results
Facebook Ads Competitor Intelligence
Turn public Meta Ad Library data into structured competitor advertising intelligence: ad discovery, creative type, CTA, offer detection, ad age, landing pages, advertiser statistics, deduplication and historical change detection. Deterministic, no AI (V1).
Pricing
from $50.00 / 1,000 results
Rating
0.0
(0)
Developer
code craker
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
a day ago
Last modified
Categories
Share
Turn public Meta Ad Library data into structured competitor advertising intelligence.
This Actor collects publicly available advertising data from the Meta Ad Library for the competitors, brands or keywords you specify and turns each ad into a structured intelligence record: creative type, call-to-action, detected offers and urgency, ad age, landing-page classification, creative fingerprints, duplicate detection, per-advertiser statistics, a competitor comparison and change detection between runs.
It is not "another Facebook scraper". Raw ads are only the starting point – the value is the deterministic intelligence layer built on top of them.
V1 is deliberately not an AI product. Every derived field is produced by transparent, rule-based logic that you can audit and reproduce. No LLM, embedding, or AI API key is used or required. An optional AI enrichment layer is planned as V2.
Table of contents
- Overview
- Features
- Who should use it
- Use cases
- How it works
- Input parameters
- Country codes
- Ad status
- Output fields
- Example input
- Example output
- Offer detection
- CTA analysis
- Creative analysis
- Ad age
- Landing-page analysis
- Creative fingerprints and duplicate detection
- Advertiser statistics and competitor comparison
- Historical comparison
- Proxy configuration
- Performance and cost
- Error handling
- Limitations
- Responsible usage
- Local development
- Testing
- Docker
- Apify deployment
- Roadmap (V2)
Overview
You provide competitor names, keywords and/or Facebook Page IDs plus a country. The Actor opens the public Meta Ad Library, applies the country, status and date filters, loads results with controlled scrolling, reads Meta's own public JSON payloads (instead of fragile CSS selectors), de-duplicates the ads and enriches every ad with deterministic intelligence. Each ad becomes one Dataset item; aggregated statistics and comparisons are stored in the run's key-value store.
Features
| Area | What you get |
|---|---|
| Competitor ad discovery | Keyword / brand search (any-word or exact phrase) and direct Page-ID lookups, per country, active / inactive / all, optional start-date range |
| Advertiser information | Page name, Page ID, Page URL, Instagram handle, page categories, like count, disclaimer / funding byline where publicly displayed |
| Creative tracking | Primary text, headline, description, caption, carousel cards, creative type (IMAGE / VIDEO / CAROUSEL / COLLECTION / TEXT / UNKNOWN), public image / video / thumbnail URLs |
| CTA analysis | Raw CTA label plus a normalized CTA category (SHOP_NOW, LEARN_MORE, SIGN_UP, BOOK_NOW, …) |
| Offer detection | Rule-based detection of discounts, prices, free offers, BOGO, financing terms, sales and promotions, plus urgency wording |
| Ad age | Start / end dates (ISO-8601), days running, ad age and a transparent age category |
| Platform placement | Facebook, Instagram, Messenger, Audience Network, Threads, WhatsApp – only when the source provides evidence |
| Landing-page analysis | Final URL, domain, title, meta description, HTTP status, redirect count and a heuristic page type (PRODUCT, SERVICE, CONTACT, BOOKING, PRICING, BLOG, HOME, FORM, UNKNOWN) |
| Fingerprints & duplicate detection | Deterministic text, media and creative fingerprints; de-duplication by ad ID, ad URL and fingerprint + advertiser, across scroll batches and search terms |
| Advertiser statistics | Per-advertiser totals, creative mix, offer share, CTA usage, platform, landing-page and age distributions |
| Competitor comparison | Side-by-side observable metrics per advertiser (no "winner" claims) |
| Historical change detection | NEW / UNCHANGED / MODIFIED labels per ad and a REMOVED list, compared against the previous run with the same configuration |
| Robustness | Retries, timeouts, per-ad and per-search error capture, graceful handling of login walls / CAPTCHAs (never bypassed) |
Who should use it
- Marketing and advertising agencies monitoring client competitors
- Brand, growth and performance-marketing teams
- E-commerce operators researching category advertising
- Market researchers and analysts
- Creative strategists studying formats, hooks and offers
- Anyone building competitor dashboards on top of Apify
Use cases
- Competitor research and competitor ad discovery
- Creative research: which formats, hooks and CTAs competitors run
- Offer and promotion monitoring (discounts, financing, free-delivery claims)
- Campaign monitoring: new, changed and removed ads over time
- Landing-page strategy research (product pages vs. lead forms vs. booking pages)
- Brand monitoring for unauthorized or look-alike advertising
- Market research and social-media advertising analysis
- Advertising-strategy benchmarking across a set of brands
How it works
- For each search term / Page ID a public Ad Library URL is built with your country, status, match type and date filters.
- A headless Chromium page (Playwright via Crawlee) loads it. Images, video and fonts are blocked to save bandwidth.
- The first page of results is read from the JSON Meta embeds in the document; further pages are read from the JSON responses Meta's page itself requests while scrolling. No private endpoints, credentials or cookies are used.
- Scrolling stops when
maxAdsPerSearchis reached, when Meta reports no further page, aftermaxScrollRoundsWithoutNewAdsidle rounds, or at a hard cap derived frommaxAdsPerSearch. - Ads are de-duplicated (within a search and across all searches) and filtered by status.
- Deterministic intelligence is derived; landing pages are fetched once per unique URL (only when enabled).
- Optional historical comparison labels each ad against the previous snapshot.
- Ads are pushed to the Dataset; statistics, comparison, history and a run summary are written to the key-value store.
If Meta serves a login wall, CAPTCHA or rate-limit page, the affected search is reported as ACCESS_BLOCKED and the run continues with the other searches. The Actor never attempts to bypass such controls.
Input parameters
| Field | Type | Default | Description |
|---|---|---|---|
searchTerms | string[] | – | Keywords, brand or competitor names. Duplicates (case-insensitive) are removed. Required unless pageIds is given. |
pageIds | string[] | [] | Numeric Facebook Page IDs to collect ads for directly. |
country | string | ZA | ISO 3166-1 alpha-2 code or ALL. Invalid codes fail validation. |
adStatus | ACTIVE | INACTIVE | ALL | ACTIVE | Ad Library status filter. |
searchType | KEYWORD_UNORDERED | KEYWORD_EXACT_PHRASE | KEYWORD_UNORDERED | Keyword match mode. |
maxAdsPerSearch | integer 1–5000 | 100 | Stop collecting after this many unique ads per search term / page. |
startDateMin / startDateMax | YYYY-MM-DD | – | Optional start-date range applied through the Ad Library filter. |
includeInactiveAds | boolean | false | Keep ads whose status resolves to INACTIVE (relevant with adStatus: ALL). Forced to true when adStatus is INACTIVE. |
includeLandingPage | boolean | true | Visit each ad's public landing page once and extract metadata. |
includeMedia | boolean | true | Include public image / video / thumbnail URLs. Media is never downloaded. |
includeAdvertiserStatistics | boolean | true | Compute per-advertiser statistics and the competitor comparison. |
enableHistoricalComparison | boolean | false | Compare with the previous run that used the same search configuration. |
historyStoreName | string | facebook-ads-competitor-intelligence-history | Named key-value store holding history snapshots. |
maxConcurrency | integer 1–10 | 3 | Parallel browser pages and parallel landing-page requests. |
maxScrollRoundsWithoutNewAds | integer 1–20 | 4 | Idle-scroll safety stop. |
navigationTimeoutSecs | integer 15–300 | 60 | Ad Library page load timeout. |
landingPageTimeoutSecs | integer 5–120 | 20 | Landing-page request timeout. |
proxyConfiguration | object | { "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"] } | Apify Proxy / custom proxy settings. Residential recommended (see below). |
Country codes
country must be an ISO 3166-1 alpha-2 code as used by the Ad Library's country selector, for example:
| Code | Country |
|---|---|
ZA | South Africa |
ZW | Zimbabwe |
GB | United Kingdom |
US | United States |
CA | Canada |
AU | Australia |
ALL | All countries |
Codes are case-insensitive. Anything that is not a valid alpha-2 code (e.g. South Africa, UK) is rejected with a clear validation error – it is never silently reinterpreted.
Ad status
The public Ad Library supports three statuses, which map directly to the input:
Input adStatus | Ad Library filter | Output status values you may see |
|---|---|---|
ACTIVE | active | ACTIVE |
INACTIVE | inactive | INACTIVE |
ALL | all | ACTIVE, INACTIVE |
status is normalized from Meta's is_active flag; sourceStatus keeps the original value (active / inactive). If the flag is missing, status is UNKNOWN and sourceStatus is null.
Output fields
Each ad is one Dataset item. All values are null when the source does not provide them – nothing is fabricated.
Advertiser: advertiserName, advertiserPageName, advertiserPageId, advertiserPageUrl, instagramHandle, pageCategories, pageLikeCount, disclaimer, fundingEntity
Ad identity: adId (Ad Library ID), adUrl, collationId, collationCount, platforms[], status, sourceStatus, searchTerm, searchPageId, country, scrapedAt
Dates and age: startDate, endDate, daysRunning, adAgeDays, adAgeCategory
Creative: primaryText, headline, description, caption, callToAction, normalizedCallToAction, creativeType, sourceDisplayFormat, imageUrls[], videoUrls[], thumbnailUrls[], cardCount, cards[]
Offer: offerDetected, offerType, offerValue, offerText, urgencyDetected
Landing page: landingPageUrl, finalUrl, landingPageDomain, pageTitle, metaDescription, httpStatus, redirectCount, landingPageType
Fingerprints / history: creativeFingerprint, textFingerprint, mediaFingerprint, changeStatus
Errors (non-fatal): error, errorType, errorStage
Key-value store records: OUTPUT (run summary incl. per-search results and errors), ADVERTISER_STATISTICS, COMPETITOR_COMPARISON, HISTORICAL_COMPARISON (when enabled).
Example input
{"searchTerms": ["Toyota", "Ford", "Mazda"],"country": "ZA","adStatus": "ACTIVE","maxAdsPerSearch": 100,"includeInactiveAds": false,"includeLandingPage": true,"includeMedia": true,"includeAdvertiserStatistics": true,"enableHistoricalComparison": false,"maxConcurrency": 3,"proxyConfiguration": { "useApifyProxy": true }}
Example output
{"advertiserName": "Example Motors","advertiserPageName": "Example Motors","advertiserPageId": "200000000000001","advertiserPageUrl": "https://www.facebook.com/examplemotors/","instagramHandle": "examplemotors","adId": "1001000000000001","adUrl": "https://www.facebook.com/ads/library/?id=1001000000000001","collationId": "col-1","collationCount": 3,"platforms": ["Facebook", "Instagram"],"status": "ACTIVE","sourceStatus": "active","startDate": "2026-08-01T00:00:00.000Z","endDate": null,"daysRunning": 44,"adAgeDays": 44,"adAgeCategory": "ESTABLISHED","primaryText": "Drive away in a new hatchback with 20% off selected models. Limited time offer - ends Sunday!","headline": "Spring Sale on Hatchbacks","description": "Offer valid while stocks last","caption": "example-motors.test","callToAction": "Learn More","normalizedCallToAction": "LEARN_MORE","creativeType": "VIDEO","sourceDisplayFormat": "VIDEO","imageUrls": [],"videoUrls": ["https://video.example-cdn.test/v/hd/abc123.mp4?..."],"thumbnailUrls": ["https://img.example-cdn.test/t/abc123.jpg?..."],"cardCount": null,"cards": [],"offerDetected": true,"offerType": "DISCOUNT","offerValue": "20%","offerText": "20% off","urgencyDetected": true,"landingPageUrl": "https://www.example-motors.test/vehicles/hatchback?utm_source=fb&utm_campaign=spring","finalUrl": "https://www.example-motors.test/vehicles/hatchback?utm_source=fb&utm_campaign=spring","landingPageDomain": "example-motors.test","pageTitle": "Hatchback Range | Example Motors","metaDescription": "Explore the Example Motors hatchback range. Book a test drive today.","httpStatus": 200,"redirectCount": 0,"landingPageType": "PRODUCT","creativeFingerprint": "eb7ea4c3…","textFingerprint": "5a1c0f…","mediaFingerprint": "9d02b1…","changeStatus": "NEW","disclaimer": null,"fundingEntity": null,"pageCategories": ["Car dealership"],"pageLikeCount": 12345,"searchTerm": "Toyota","searchPageId": null,"country": "ZA","scrapedAt": "2026-09-14T00:00:00.000Z","error": null,"errorType": null,"errorStage": null}
Offer detection
Offer detection is rule-based (regular expressions over the primary text, headline, description, CTA and carousel card texts). Rules are evaluated in priority order and the first match wins:
offerType | Examples detected | offerValue |
|---|---|---|
BOGO | buy one get one, BOGO, 2 for 1 | matched phrase |
DISCOUNT | 20% off, 30% discount, save up to 50%, R500 off, half price | 20%, R500 |
FREE | free delivery, free consultation, free quote, free test drive | free delivery |
FINANCING | no deposit, interest free, 0% APR, from R999 per month, monthly payments from … | R999 |
PRICE | from $99, only R1,999, R249 999 | $99, R1,999 |
SALE | sale, clearance, markdowns | – |
PROMOTION | special offer, promotion, promo code, voucher, coupon, giveaway, limited time offer | – |
urgencyDetected is evaluated independently (limited time, ends Sunday, hurry, while stocks last, only 5 left, …).
Values are only ever copied from the text – nothing is inferred. When nothing matches, offerDetected is false and all offer fields are null. Currency-style patterns can occasionally match non-offer numbers; treat the result as a signal, not a verdict.
CTA analysis
callToAction is the label shown in the ad. normalizedCallToAction is derived first from Meta's structured CTA type and, when that is absent, from the label text. Categories:
SHOP_NOW, LEARN_MORE, SIGN_UP, BOOK_NOW, CONTACT_US, GET_QUOTE, SEND_MESSAGE, WHATSAPP_MESSAGE, DOWNLOAD, APPLY_NOW, WATCH_MORE, GET_OFFER, SUBSCRIBE, ORDER_NOW, CALL_NOW, GET_DIRECTIONS, INSTALL_APP, PLAY_GAME, DONATE, REQUEST_TIME, SEE_MENU, LISTEN_NOW, BUY_TICKETS, GET_SHOWTIMES, VISIT_PROFILE, OPEN_LINK, OTHER (a CTA exists but is not categorised), UNKNOWN (no CTA available).
Creative analysis
creativeType is taken from Meta's display format (IMAGE, VIDEO, CAROUSEL, MULTI_IMAGES, DCO → CAROUSEL, DPA → COLLECTION) and falls back to counting images / videos / cards. A "carousel" with a single media element is reported as that element's type. When no evidence exists the value is UNKNOWN – never a guess. sourceDisplayFormat keeps Meta's original value.
Carousel ads additionally expose cardCount and cards[] (title, body, description, CTA, link, image / video URL per card).
Ad age
adAgeDays= days betweenstartDateand the run time.daysRunning= the same for active ads; for inactive ads it isendDate − startDate(null ifendDateis unavailable).adAgeCategorythresholds (days since start): NEW 0–7 · RECENT 8–30 · ESTABLISHED 31–90 · LONG_RUNNING 91+ · UNKNOWN when the start date is missing.
Meta reports the current date as the end date of ads that are still running; the Actor therefore leaves endDate null for active ads.
A long-running ad has simply been observed running for that period. This is not evidence that the ad is successful or profitable.
Landing-page analysis
With includeLandingPage: true the Actor fetches each unique landing page once (HTTP GET through the configured proxy, with one direct retry if the proxy itself fails; follows up to 10 redirects, no deeper crawling) and records finalUrl, landingPageDomain, pageTitle, metaDescription, httpStatus and redirectCount. Facebook link wrappers (l.facebook.com/l.php?u=…) are unwrapped first; Meta-hosted destinations (Facebook, Instagram, WhatsApp, Messenger) are recorded but not fetched.
landingPageType is a heuristic based on URL path segments and, secondarily, the page title:
| Type | Typical signals |
|---|---|
PRODUCT | /product, /shop, /vehicles, /models, /deals, /specials, /collections |
SERVICE | /services, /solutions, /treatments, /courses, /finance |
CONTACT | /contact, /enquire, /find-a-dealer, /locations |
BOOKING | /book, /reservation, /appointment, /test-drive |
PRICING | /pricing, /plans, /packages, /quote |
BLOG | /blog, /news, /articles, /guides |
FORM | /apply, /register, /signup, /subscribe, /download, /demo |
HOME | root path |
UNKNOWN | nothing matched |
The classification is deliberately simple and transparent; it will be wrong for some sites.
Creative fingerprints and duplicate detection
textFingerprint– SHA-256 of the normalized primary text, headline and description (lower-cased, diacritics and punctuation removed, URLs stripped).mediaFingerprint– SHA-256 of the media asset file names (Meta's per-region CDN hosts and rotating signature parameters are ignored). It identifies the same file reference; it does not prove two creatives look identical, and Meta occasionally serves a different rendition of the same creative, which then registers as a change.creativeFingerprint– SHA-256 combining advertiser, text fingerprint, media fingerprint and the normalized landing URL (tracking parameters removed).
Fingerprints are stable across runs and independent of includeMedia.
De-duplication keys, in order: Ad Library ID → normalized ad URL → creative fingerprint + advertiser. An ad loaded twice while scrolling, or found under two search terms, appears once. collationId / collationCount expose Meta's own grouping of near-identical ad versions.
Advertiser statistics and competitor comparison
With includeAdvertiserStatistics: true, ADVERTISER_STATISTICS contains one entry per advertiser:
{"advertiser": "Example Motors","advertiserPageId": "200000000000001","statistics": {"totalAds": 42, "activeAds": 35, "inactiveAds": 7, "unknownStatusAds": 0,"imageAds": 13, "videoAds": 24, "carouselAds": 5, "otherAds": 0,"averageAdAgeDays": 34, "medianAdAgeDays": 28, "longestRunningAdDays": 121,"offerAds": 17, "discountAds": 9, "urgencyAds": 6,"commonCTAs": [{ "cta": "LEARN_MORE", "count": 20 }, { "cta": "SHOP_NOW", "count": 12 }],"platformDistribution": { "Facebook": 42, "Instagram": 39 },"landingPageDistribution": { "PRODUCT": 30, "BOOKING": 8, "UNKNOWN": 4 },"landingDomainDistribution": { "example-motors.test": 42 },"offerTypeDistribution": { "DISCOUNT": 9, "FINANCING": 5, "FREE": 3 },"adAgeCategoryDistribution": { "ESTABLISHED": 20, "LONG_RUNNING": 12, "RECENT": 10 },"uniqueCreatives": 31},"note": "Statistics are computed only from the ads collected in this Actor run. …"}
COMPETITOR_COMPARISON lists observable metrics side by side (total / active ads, video / image / carousel percentages, offer and urgency percentages, average ad age, longest-running ad, top CTA, top landing-page type, platforms). No ranking, "winner" or "best competitor" claim is made – these are only the numbers observed in the collected sample, not the advertiser's complete history, spend or performance.
Historical comparison
With enableHistoricalComparison: true the Actor stores a snapshot of every ad's identity and fingerprints in the named key-value store (historyStoreName) under a key derived from the search configuration (terms, page IDs, country, status, match type, date range). The next run with the same configuration loads it and sets changeStatus per ad:
changeStatus | Meaning |
|---|---|
NEW | Ad ID not present in the previous snapshot |
UNCHANGED | Same ad ID and identical creative (or text) fingerprint |
MODIFIED | Same ad ID but a different creative / text fingerprint |
Ads present previously but not observed now are listed in HISTORICAL_COMPARISON.removed (they are not dataset items because they were not observed in this run). Without a previous snapshot every ad is NEW; with the feature disabled changeStatus is null.
Limitations: comparison is only meaningful when the configuration is identical; a smaller maxAdsPerSearch or Meta's ranking changes can make ads look REMOVED while they still run. The previous snapshot is kept under <key>-previous for inspection. Named key-value stores persist across runs on the Apify platform (and under ./storage locally).
Proxy configuration
proxyConfiguration uses the standard Apify proxy editor and defaults to Apify Proxy's RESIDENTIAL group. This matters: in testing, Meta served the first page of results (about 30 ads) to datacenter IPs but rejected every pagination request, so datacenter proxies cap each search at roughly 30 ads. With RESIDENTIAL selected the Actor automatically sets the proxy country to the search country. Nothing is hard-coded: credentials come from the platform (or APIFY_PROXY_PASSWORD locally). Without a proxy the Actor still runs (useful locally) and reports blocks honestly.
Performance and cost
- One browser page per search term / Page ID, at most
maxConcurrencyin parallel. - Images, video and fonts are blocked in the browser; media is never downloaded, only URLs are stored.
- Landing pages are fetched only with
includeLandingPage: true, once per unique URL, using lightweight HTTP requests (not a browser). - History storage is touched only with
enableHistoricalComparison: true. - Scrolling is bounded by
maxAdsPerSearch, Meta's own "no more results" signal,maxScrollRoundsWithoutNewAdsand a hard cap. - Rough guide: 100 ads for one term take about 30–60 seconds of browser time plus landing-page requests.
Error handling
One failed ad or search never stops the run:
- Navigation errors are retried (2 retries with Crawlee's back-off); a search that ultimately fails is reported in
OUTPUT.searches[].errorwitherrorType(ACCESS_BLOCKED,TIMEOUT,NETWORK_ERROR,PARSE_ERROR,UNKNOWN). - Landing-page failures are recorded on the ad (
error,errorType,errorStage: "landing-page-enrichment") while the ad itself is still emitted. - If the structured payload is unavailable but the page renders, a text-based fallback recovers Ad IDs and dates; such records carry
errorStage: "ad-discovery"so you know they are partial. - The run finishes with
SUCCEEDED,PARTIAL(some searches failed) or fails explicitly when every search failed (ACCESS_BLOCKEDwhen Meta blocked all of them). No data is ever fabricated to make a run look successful.
Limitations
- Only publicly available Ad Library data is used. Spend, impressions, reach, conversions, ROAS and revenue are not available for regular commercial ads and are not reported.
- Meta changes its interface without notice. All URL parameters, payload keys and block indicators are centralized in
src/meta/selectors.tsfor quick updates. - Meta may block or throttle automated access (login wall, CAPTCHA, HTTP 429). The Actor detects and reports this; it does not bypass it.
- Offer detection, CTA normalization, creative classification and landing-page typing are deterministic heuristics – expect some misses and false positives.
- Historical comparison depends on identical configurations between runs (see above).
- Ad age reflects observed running time only, never success.
- Search results are governed by Meta's relevance ranking; a keyword search can include ads from unrelated advertisers that mention the keyword.
Responsible usage
This Actor processes only publicly available advertising information from the Meta Ad Library, which Meta publishes for transparency purposes. It does not log in, does not use cookies or credentials, does not access private accounts or private ads, and does not bypass CAPTCHAs, rate limits or other security mechanisms.
You are responsible for using the collected data in compliance with Meta's terms, applicable laws, privacy and data-protection regulations (e.g. GDPR, POPIA, CCPA) and advertising / consumer-protection rules in your jurisdiction. Do not use the output to harass advertisers, to republish copyrighted creatives, or to build profiles of private individuals.
Local development
Requirements: Node.js 20+ (22 recommended), npm, and Chromium for Playwright.
git clone <this repository>cd facebook-ads-competitor-intelligencenpm installnpx playwright install chromium # local browser for development runsnpm run build
Create the input locally (Apify SDK reads it from the default key-value store):
mkdir -p storage/key_value_stores/defaultcp smoke/input.json storage/key_value_stores/default/INPUT.jsonnpm start
Or simply run the bundled smoke configuration (Toyota, ZA, 3 ads, no landing pages / media):
npm run smoke # prepares the input and runs the compiled Actornode scripts/verify-smoke-output.mjs # validates OUTPUT + dataset items against the dataset schema
Results land in storage/datasets/default/ and storage/key_value_stores/default/. Copy .env.example to .env if you want to set APIFY_TOKEN / APIFY_PROXY_PASSWORD for Apify Proxy locally. No AI keys are needed.
Useful scripts: npm run build, npm start, npm run start:dev (tsx, no build), npm test, npm run lint, npm run typecheck, npm run check (build + lint + test), npm run smoke.
Testing
$npm test
Vitest runs 90+ tests covering input validation, URL / text / date normalization, CTA normalization, offer detection, creative classification, ad age, fingerprints, de-duplication, historical comparison, statistics, the Meta payload parser (GraphQL bodies, embedded HTML, block detection, DOM fallback), landing-page enrichment (mocked fetcher) and the end-to-end pipeline including a check that every emitted record conforms to .actor/dataset_schema.json. All external Meta traffic is replaced by small synthetic fixtures in tests/fixtures/.
Docker
docker build --platform linux/amd64 -t facebook-ads-competitor-intelligence .# full Docker smoke test: browser launch, mocked test-suite inside the container, live Actor run./scripts/docker-smoke.sh
The image is based on apify/actor-node-playwright-chrome:22-1.63.0 (Node 22, Playwright 1.63.0 with Chromium and system dependencies). The Apify base image is published for linux/amd64; on Apple Silicon Docker Desktop runs it under emulation.
Apify deployment
- Install the Apify CLI and log in:
npm i -g apify-cli && apify login. - From the project directory run
apify push– the Actor definition in.actor/actor.json, the input schema and the Dockerfile are used to build the Actor in the cloud. - Open the Actor in Apify Console, fill in the input form (the schema provides defaults, validation and a proxy editor) and press Start.
- Results: Dataset tab (one row per ad, exportable as JSON / CSV / Excel), Key-value store tab (
OUTPUT,ADVERTISER_STATISTICS,COMPETITOR_COMPARISON,HISTORICAL_COMPARISON). - Schedule the Actor (e.g. daily or weekly) with
enableHistoricalComparison: trueto track new, modified and removed competitor ads over time.
Roadmap (V2)
V2 will add an optional, clearly separated AI enrichment layer (never required to run the Actor): AI ad summaries, creative and hook analysis, target-audience inference, competitor strategy summaries, offer analysis, landing-page analysis and narrative competitive reports. The V1 pipeline (src/pipeline.ts) already produces the clean, deterministic records such a layer would consume.