First implementation. Captures taken 2026-10-08 (local, direct, Lithuanian exit).
- Explore feed scraper:
GET /api/explore/explore via curl-cffi (chrome131), always sending
Accept: application/json (without it the endpoint answers 415 text/html).
- Pagination: 24 wines per page, page ceiling recomputed from each response's
records_matched
(it drifts: 6,510 on 10-07 vs 6,101 on 10-08 for the same query). Stops at maxItems, an empty
matches, or the ceiling. Rows deduped on vintageId; the last page is truncated at maxItems.
- Retry: 408/429/503 and network errors, exponential backoff 2s to 30s, 5 attempts,
Retry-After
honoured. 403/429 rotate the proxy session_id when a proxy is configured. 415 and other 4xx are
not retried. A 200 that is not an explore payload is a failure, never an empty result.
- Proxy is OFF by default; when on,
apifyProxyCountry is pinned to countryCode
(explore_vintage.market.country follows the exit IP, emitted per row as marketCountry).
- PPE:
actor-start $0.02 once, result $0.003 per row. Zero rows charge only the start event.
- Fixtures: first 3
matches (and the duplicate records list) of two real captures
(US red, FR white page 1); every other key untouched. See tools/trim_fixtures.py.
Pre-registered rule: a sort is exposed only if the response is 200 JSON, the 24 rows are monotone in
the sorted field, and the order differs from the ratings_count baseline. Probed 2026-10-08,
US red, 1 request each, no proxy:
Input orderBy | API order_by | Result |
|---|
ratings_count | ratings_count | VERIFIED (default; baseline captures) |
rating | ratings_average | VERIFIED: 200 JSON, 24 rows, ratings_average non-increasing (desc), order differs from baseline |
price (not exposed) | price | UNVERIFIED: 200 JSON but price.amount NOT monotone for order=asc (20.13, 14.09, 33.44, 100.53, ...). Left out of the schema. |
Vintage URL: HEAD https://www.vivino.com/wines/1469676 returns 302 to
https://www.vivino.com/en/wines/1469676, so https://www.vivino.com/wines/<vintageId> resolves
and is used for url.