Scrape used-car listings from standvirtual.com, Portugal's #1 car marketplace (OLX/Adevinta group) — price (EUR), make, model, year, mileage, fuel, gearbox, engine power, seller (dealer/private), location, and photos. Export to JSON or CSV; optionally enrich with the full Portuguese description.
Fix: 44% customer FAILED rate (24/54 30-day runs, live build 0.2.1) traced
to a second, independent bug beyond the 0.2.0 proxy-session fix —
_fetch_search_page collapsed "genuinely blocked" and "search parsed fine
but matched zero listings" into the same empty result, and _iter_listings
raised RuntimeError("possible block") on page 1 for both. A customer
applying a narrow searchUrl filter (specific make/model/price band with
no current matches) got a hard FAILED run instead of a SUCCEEDED run with
zero rows — the exact anti-pattern in ops/os/EMPTY-IS-NOT-A-FAILURE-2026-08-19.md,
which this Actor's bespoke error string wasn't covered by.
Fix: _fetch_search_page now returns None only for genuinely unreadable
pages (retries exhausted, or no parseable __NEXT_DATA__), and uses the
payload's own totalCount to confirm a real empty search vs. an anomalous
"claims results but zero edges" page (still treated as a block signal).
_iter_listings raises loud only on None; a confirmed-empty page 1 now
finishes the run normally with zero rows.
Added tests/test_empty_search_vs_block.py — regression test that fails
on the old code and passes on the fix; also locks in that unreadable pages
and inconsistent totalCount/edges pages still fail loud.
0.2.0 — 2026-08-26
Fix: 51% customer success rate (23/51 30-day FAILED + 2 TIMED-OUT) traced to
a sticky-proxy-session bug. Apify Proxy pins one exit IP per session_id;
the Actor resolved a single proxy URL (one session_id) once at startup and
reused it across every block retry, so a flagged exit failed all 5 retries
identically and the run raised on page 1 ("possible block"). Reproduced
live (run aj7e8X1AB00HBi2im): HTTP 403 on all 4 logged attempts despite
"session rotation" that only rotated the curl-cffi TLS fingerprint, not the
proxy exit. Fix: proxy_url_factory mints a genuinely new session_id —
and therefore exit IP — on every retry, in both main.py (proxy
resolution) and scraper.py (the retry loop).
Hardening: pinned apifyProxyCountry: "PT" in the default
proxyConfiguration (model + input schema). A Portugal-only classifieds
site is more likely to flag exits from outside PT; empirically 0/2 blocked
vs 1/3 blocked on random-country residential exits during reproduction.
Added tests/test_proxy_rotation.py — regression test that fails on the
old single-proxy-url code path (_new_session was synchronous and never
re-consulted a factory) and passes once every retry mints a distinct exit.
Fix: actor-start was charged before input validation ran. scraper.run()
is an async generator, so ActorInput.model_validate() only executed
lazily on the first __anext__() — after main() had already billed the
customer. Input is now validated synchronously in main() before any
charge fires.
Added a regression test asserting the proxy session_id template matches
Apify's ^[\w._~]+$ regex, is unique per call, and that the proxy-url
factory mints a fresh one on every invocation.
Published as build 0.2.1, QA PASS (5/5 rows, actor-start×1 +
result-row×5 charged correctly, no 403/block on the search page).
0.1.0 — 2026-06-05
First publish to Apify Store (EU car-scraper fleet rollout).