Quick answer
A cloud QA run of the Ricardo.ch Listings Scraper reported SUCCEEDED with zero rows. That statement alone should already worry you: it means every single page request Ricardo.ch got hit with came back a non-retryable 403, and the run finished green anyway. Two separate bugs produced that one green-but-empty run. First, the Actor shipped {"useApifyProxy": false} — no proxy at all — because local recon against Ricardo.ch's search page found no anti-bot surface. That was true from a home connection and false from Apify's own egress, which the target refuses outright. Second, and worse: a run that fetched zero pages and delivered zero rows still reported success, which is exactly the shape of a run that bills the customer the start fee and hands back nothing while every dashboard reads 100%.
Why did local recon say "no anti-bot" when the cloud got 403'd on every page?
Because "reachable from here" and "reachable from Apify's proxy infrastructure" are two different claims, and only one of them was tested. Ricardo.ch's search page is keyless and shows no visible anti-bot challenge to a normal browser or a local curl — no CAPTCHA, no JS challenge page, nothing that reads as deliberate blocking. That's real, and it's also incomplete: the same URL, requested through Apify's datacenter egress, gets an outright 403 on the first page and the second. The target isn't running a subtle fingerprint check that a local connection happens to pass — it's differentiating by IP reputation at a level that never shows up until you actually route through the infrastructure you're going to ship on.
The fix is the one this codebase keeps re-learning: default and prefill now name RESIDENTIAL explicitly and pin CH as the exit country. The country pin isn't just about clearing the block, either — Ricardo is a Swiss-only marketplace, so an exit from the wrong country risks a marketplace that quietly serves different prices and availability, which would be wrong data wearing the shape of correct data.
Why is "SUCCEEDED with 0 rows" the more dangerous bug of the two?
Because it hides the first bug from every automated check that exists to catch exactly this. A transport failure on every page is loud in the run log — but a run that catches that failure, gives up cleanly, and still exits 0 turns a loud problem into a silent one. Nothing about a SUCCEEDED status with an empty dataset trips a health dashboard, a customer-facing error, or an alert; it just looks like a search that happened to match nothing. Under Pay-Per-Event billing, that run still charges the flat start fee for a customer who got literally zero value back — and it was the third Actor found doing exactly this in the same day's release pass, which is why it's now treated as its own category of bug rather than folded into ordinary retry logic.
The fix is narrow and deliberate: main() now checks summary.pages_fetched == 0 after the crawl finishes, and fails loud — a real non-zero exit with a clear status message — rather than falling through to a quiet success. This is distinct from, and doesn't touch, the fleet-wide rule that a search which genuinely matches nothing should still succeed; the difference is whether the target ever answered a single request at all.
A run that fetched zero pages cannot have zero rows for an honest reason — it never got the chance to find anything. Reporting that as success is not a smaller version of the empty-search-is-fine rule; it's the one case that rule was never meant to cover.
What the Actor gives you
One row per Ricardo.ch listing matching your search slug — title, price in CHF, listing URL, image, and a stable numeric listing ID, deduplicated across pages. Under the hood, page 1 parses Ricardo's own application/ld+json structured data block, while page 2 and beyond carry no ld+json at all and are parsed from the rendered listing anchors instead — two different wire shapes for the same data, both handled, both recorded per row via a source_format field so you can see which path produced it.
Honest limitations 🚧
Search-results scraping only — no per-listing detail-page enrichment beyond what the results page itself carries. Prices that Ricardo lists as "Preis auf Anfrage" (price on request) come back null rather than a fabricated number.
FAQ
Do I need a Ricardo.ch account?
No — this uses the public search results page, no login required.
Why does the default proxy configuration require residential Swiss exits when the site has no visible anti-bot wall?
Because it 403s Apify's datacenter egress even though it shows nothing to a local connection — confirmed in cloud QA on both pages of a real run. It's also a Swiss-only marketplace, so the exit country decides whether prices and availability are even correct.
What happens if my search slug matches nothing?
The run succeeds with zero rows and a status message naming the slug — that's different from a transport failure, which now fails loud instead of reporting a false success.
Does pagination ever silently return the same data twice?
No — rows are deduplicated by listing ID across pages within a run.
Pricing
$0.20 per run plus $0.004 per listing row — $4.20 per 1,000 listings. A run that matches nothing costs only the start fee.
→ Ricardo.ch Listings Scraper on Apify
Built by Devil Scrapes. We rotate residential proxies through the right exit country, retry with backoff, and — as this one shows — we fail loud when a run never actually reached the target, instead of reporting a clean success on nothing.
Top comments (0)