Export ad records from Microsoft's public Bing/Microsoft Advertising Ad Library — ad id, advertiser, title, description, display and destination URLs, and creative assets. Search by advertiser name, resolved server-side to an AdvertiserId. Deduplicated rows, no login or API key required.
Implemented the real two-step resolve-then-paginate flow against
adlibrary.api.bingads.microsoft.com (T02-T09):
src/models.py — ActorInput (advertiser-name search, blank-entry
stripping, non-empty validation before any charge) + ResultRow.
src/parser.py — pure JSON parsing for /Advertisers and /Ads
payloads, guarded AssetJson decode (empty/malformed -> [],
never raises), missing-AdId row skip with a logged warning.
src/scraper.py — curl-cffi transport with exponential backoff on
408/429/503 (Retry-After honoured), resolve_advertisers()
(slices the single /Advertisers response to maxAdvertisersPerName,
never a second call), fetch_ads_for_advertiser() (skip steps by
24, hard-capped at 999, incremental within-advertiser dedup by
AdId), and BingAdLibraryScraper.search() orchestration — per-term
and per-advertiser fault isolation, cross-term advertiser reuse,
global AdId dedup, and the REQ-7/REQ-8 any_success boundary (a
genuine zero-match run succeeds; only total transport failure across
every search term fails the run).
src/main.py — SDK wiring, PPE charges, BingFetchError raised only
when nothing in the whole run ever got a parseable response.
Replaced the T01 scaffold's placeholder-row src/main.py and
tests/test_main.py with the real implementation and its integration
tests (T09/T11).
Added promo.json (T12) — GTM manifest per docs/PROMO-MANIFEST.md.
51 tests passing across test_models.py, test_parser.py,
test_scraper.py, test_main.py (2 additional opt-in test_smoke.py
cases excluded from the default run).
PPE pricing unchanged from the scaffold: actor-start $0.20 +
ad-result $0.003 per unique AdId (effective $3.20 / 1 000 ads),
matching the fleet's other ad-library Actors (Pinterest, Meta,
LinkedIn, TikTok).