Facebook Ads Competitor Intelligence avatar

Facebook Ads Competitor Intelligence

Pricing

from $50.00 / 1,000 results

Go to Apify Store
Facebook Ads Competitor Intelligence

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

code craker

Maintained by Community

Actor 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

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

AreaWhat you get
Competitor ad discoveryKeyword / brand search (any-word or exact phrase) and direct Page-ID lookups, per country, active / inactive / all, optional start-date range
Advertiser informationPage name, Page ID, Page URL, Instagram handle, page categories, like count, disclaimer / funding byline where publicly displayed
Creative trackingPrimary text, headline, description, caption, carousel cards, creative type (IMAGE / VIDEO / CAROUSEL / COLLECTION / TEXT / UNKNOWN), public image / video / thumbnail URLs
CTA analysisRaw CTA label plus a normalized CTA category (SHOP_NOW, LEARN_MORE, SIGN_UP, BOOK_NOW, …)
Offer detectionRule-based detection of discounts, prices, free offers, BOGO, financing terms, sales and promotions, plus urgency wording
Ad ageStart / end dates (ISO-8601), days running, ad age and a transparent age category
Platform placementFacebook, Instagram, Messenger, Audience Network, Threads, WhatsApp – only when the source provides evidence
Landing-page analysisFinal 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 detectionDeterministic text, media and creative fingerprints; de-duplication by ad ID, ad URL and fingerprint + advertiser, across scroll batches and search terms
Advertiser statisticsPer-advertiser totals, creative mix, offer share, CTA usage, platform, landing-page and age distributions
Competitor comparisonSide-by-side observable metrics per advertiser (no "winner" claims)
Historical change detectionNEW / UNCHANGED / MODIFIED labels per ad and a REMOVED list, compared against the previous run with the same configuration
RobustnessRetries, 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

  1. For each search term / Page ID a public Ad Library URL is built with your country, status, match type and date filters.
  2. A headless Chromium page (Playwright via Crawlee) loads it. Images, video and fonts are blocked to save bandwidth.
  3. 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.
  4. Scrolling stops when maxAdsPerSearch is reached, when Meta reports no further page, after maxScrollRoundsWithoutNewAds idle rounds, or at a hard cap derived from maxAdsPerSearch.
  5. Ads are de-duplicated (within a search and across all searches) and filtered by status.
  6. Deterministic intelligence is derived; landing pages are fetched once per unique URL (only when enabled).
  7. Optional historical comparison labels each ad against the previous snapshot.
  8. 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

FieldTypeDefaultDescription
searchTermsstring[]Keywords, brand or competitor names. Duplicates (case-insensitive) are removed. Required unless pageIds is given.
pageIdsstring[][]Numeric Facebook Page IDs to collect ads for directly.
countrystringZAISO 3166-1 alpha-2 code or ALL. Invalid codes fail validation.
adStatusACTIVE | INACTIVE | ALLACTIVEAd Library status filter.
searchTypeKEYWORD_UNORDERED | KEYWORD_EXACT_PHRASEKEYWORD_UNORDEREDKeyword match mode.
maxAdsPerSearchinteger 1–5000100Stop collecting after this many unique ads per search term / page.
startDateMin / startDateMaxYYYY-MM-DDOptional start-date range applied through the Ad Library filter.
includeInactiveAdsbooleanfalseKeep ads whose status resolves to INACTIVE (relevant with adStatus: ALL). Forced to true when adStatus is INACTIVE.
includeLandingPagebooleantrueVisit each ad's public landing page once and extract metadata.
includeMediabooleantrueInclude public image / video / thumbnail URLs. Media is never downloaded.
includeAdvertiserStatisticsbooleantrueCompute per-advertiser statistics and the competitor comparison.
enableHistoricalComparisonbooleanfalseCompare with the previous run that used the same search configuration.
historyStoreNamestringfacebook-ads-competitor-intelligence-historyNamed key-value store holding history snapshots.
maxConcurrencyinteger 1–103Parallel browser pages and parallel landing-page requests.
maxScrollRoundsWithoutNewAdsinteger 1–204Idle-scroll safety stop.
navigationTimeoutSecsinteger 15–30060Ad Library page load timeout.
landingPageTimeoutSecsinteger 5–12020Landing-page request timeout.
proxyConfigurationobject{ "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:

CodeCountry
ZASouth Africa
ZWZimbabwe
GBUnited Kingdom
USUnited States
CACanada
AUAustralia
ALLAll 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 adStatusAd Library filterOutput status values you may see
ACTIVEactiveACTIVE
INACTIVEinactiveINACTIVE
ALLallACTIVE, 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:

offerTypeExamples detectedofferValue
BOGObuy one get one, BOGO, 2 for 1matched phrase
DISCOUNT20% off, 30% discount, save up to 50%, R500 off, half price20%, R500
FREEfree delivery, free consultation, free quote, free test drivefree delivery
FINANCINGno deposit, interest free, 0% APR, from R999 per month, monthly payments from …R999
PRICEfrom $99, only R1,999, R249 999$99, R1,999
SALEsale, clearance, markdowns
PROMOTIONspecial 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 between startDate and the run time.
  • daysRunning = the same for active ads; for inactive ads it is endDate − startDate (null if endDate is unavailable).
  • adAgeCategory thresholds (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:

TypeTypical 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
HOMEroot path
UNKNOWNnothing 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:

changeStatusMeaning
NEWAd ID not present in the previous snapshot
UNCHANGEDSame ad ID and identical creative (or text) fingerprint
MODIFIEDSame 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 maxConcurrency in 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, maxScrollRoundsWithoutNewAds and 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[].error with errorType (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_BLOCKED when 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.ts for 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-intelligence
npm install
npx playwright install chromium # local browser for development runs
npm run build

Create the input locally (Apify SDK reads it from the default key-value store):

mkdir -p storage/key_value_stores/default
cp smoke/input.json storage/key_value_stores/default/INPUT.json
npm 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 Actor
node 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

  1. Install the Apify CLI and log in: npm i -g apify-cli && apify login.
  2. 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.
  3. Open the Actor in Apify Console, fill in the input form (the schema provides defaults, validation and a proxy editor) and press Start.
  4. Results: Dataset tab (one row per ad, exportable as JSON / CSV / Excel), Key-value store tab (OUTPUT, ADVERTISER_STATISTICS, COMPETITOR_COMPARISON, HISTORICAL_COMPARISON).
  5. Schedule the Actor (e.g. daily or weekly) with enableHistoricalComparison: true to 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.