GMBSPYLAB: from the Facebook Ad Library to a Telegram feed
A working system that collects ads from the public Facebook Ad Library. It's built on Playwright (browser automation) and async Python. Most of the work happens after collection: cleaning up the data, removing near-duplicate images, queues and delivery. 30–75k cards a day from 30+ markets go to Telegram, with a separate topic for each country.
Ad research subscriptions cost $300–800 a month
Commercial ad research services charge $300–800 a month for a subscription. I needed a live stream of ads from 30+ countries, including smaller markets in Europe, Latin America and Asia.
Getting the same public data through Meta's API requires a developer app that passes App Review. And facebook.com/ads/library builds the page in the browser, so simply downloading the page returns it empty.
The goal: collect a live stream of ads from 30+ countries into my own database, filter it by format (image, video, PWA landing page), by how long an ad has been running and by a consistent set of tags, and then post it to Telegram.
Four parts: collection, resilience, storage, delivery
Playwright without parsing the page
The ad listing is built in the browser, so a headless Chromium opens the page once. After that the system works with the same data the browser receives and spends no time going through the page structure card by card.
Requests go out at a measured pace: a pool of sessions, each tied to a market, at least 2.5 s between requests and a hard limit per session. The aim is a steady stream that covers the daily volume.
Changes on the source's side don't stop the collection
The source keeps changing: its markup and response parameters change often, and a simple parser quietly returns zeros after each change. Here the structure of the response is checked at every step. If it doesn't match, the session starts over and the stream keeps going. If the source returns an error or slows down, the session waits longer between attempts and recovers on its own. This happens regularly and needs no manual work.
Postgres 16, arq, and MinIO for media
SQLAlchemy 2 async, asyncpg and alembic migrations, with indexes on advertiser ID, country and first-seen date. arq on Redis runs background tasks (downloading media, removing duplicates). Images and videos go to MinIO / R2 through aioboto3. Near-duplicate images are removed by perceptual hash, which compares how images look: 30–75k raw cards become 5–12k unique ones a day.
Telegram via aiogram 3, in two formats
v1: a forum-style group. A topic is created with createForumTopic
for each of the 30+ markets (the 27 EU countries, the UK, Brazil and others such as TZ, MX, IN, CA, ZA and RS). Each country's ads go to its own topic, so filtering by market is easy.
v2: a hashtag channel. One stream, tagged with
#GEO #PWA #VIDEO #1DAY #FBPAGE.
A daily scheduled job updates the #NDAY
tag, which shows how many days the ad has been running.
What it's built with
- Playwright (headless Chromium)
- A pool of sessions, each tied to a market
- Request limits and growing pauses after errors
- Response structure checked at every step
- Python 3.12 + uv (package manager)
- SQLAlchemy 2 async + asyncpg
- PostgreSQL 16 + alembic migrations
- Pydantic v2 for all schemas
- arq + Redis 7 (background tasks)
- APScheduler (scheduled jobs)
- MinIO / Cloudflare R2 (aioboto3)
- SQLite (cache, local state)
- aiogram 3 (Bot API)
- Forum-style group with a topic per market (createForumTopic)
- Hashtag channel with a daily "running for N days" update
- Health checks with alerts to Discord and Telegram
- Playwright on Facebook (main source)
- ScrapeCreators (paid backup source outside the EU)
- Meta Graph API (for confirmed IDs)
- EU DSA Repository (regulatory data)
- structlog (structured logs)
- 16/16 tests passing (pytest)
- Health check and emergency stop
- Status messages to Discord via webhooks
What it delivers
after removing duplicates: 5–12k unique
the 27 EU countries, the UK, Brazil and markets outside the EU
a month for the whole setup: server and storage
How this helps clients: the same chain (collect, clean up, remove duplicates, deliver) works with any source you have lawful access to: your own Avito, Wildberries or Ozon seller account, official data exports, open ad libraries. On a call we pick the exact source and check what the platform's own terms allow you to do with the data.
Related cases
End-to-end attribution for two Shopify stores: from click to order
My own service on the Python standard library receives Shopify order notifications and checks their signature…
Shopify orders land in the warehouse sheets by themselves
Order numbers, tracking numbers, statuses and cancellations are filled in every 30 minutes. An Apps Script…
File exchange with a shipping partner: costs and tracking numbers come back into the sheet
The daily file builds itself and looks just like the working sheet, row colours included. The partner's reply…
Start with the 5,000 ₽ audit: a written report and a payback estimate
I'll tell you what to automate first, when it will pay for itself and whether your task needs AI at all. Sometimes it doesn't.
Or just ask a question. During business hours I reply within 2 hours