Extract popularity, critic scores, prices (always per 75cl bottle) and winery info from Wine-Searcher.com. Input wine names, URLs or LWIN codes; get structured JSON. Success-only billing: $0.025 per wine actually extracted, errors and not-found are free.
All notable changes to Wine-Searcher Scraper from List are documented here.
Format based on Keep a Changelog . Versions follow the Apify build numbering (0.1.XX).
v0.1.167 (2026-06-25)
Changed
Documentation polish. Clarified that concurrency tuning is an advanced API-level override (the default of 6 wines in parallel suits all batch sizes), harmonized the pricing wording to "pay-per-event", and tidied the wording of earlier changelog entries. No functional change to the scraper.
v0.1.166 (2026-06-25)
Fixed
Grape variety, vintage and label image are now returned consistently. A subset of wines could come back with these three fields empty even though the information is available on Wine-Searcher. They are now populated for those wines too, so every resolved wine carries the same complete field set.
v0.1.165 (2026-06-25)
Fixed
Unrelated-wine matches are now caught and reported instead of returned. When Wine-Searcher could not find the exact wine requested, a lookup could occasionally resolve to a completely different wine (another producer, another region) and return it as if correct — with a misleading price. When the resolved wine shares no distinctive word with your query, the row now returns a clear "resolved wine does not match the query" error instead of incorrect data.
The cheapest price now always reflects a standard 750ml bottle. Magnum, jeroboam, half-bottle and other non-standard formats are skipped when picking the lowest offer, so the reported price is no longer inflated by a large-format listing (or under-stated by a half-bottle).
More vintages are populated. When the structured vintage field is missing, the vintage is now recovered from the leading year of the wine name, filling in vintages that were previously left empty on clearly vintage-dated wines.
Changed
The wine label is now shown as an image thumbnail in the dataset Overview, instead of a plain link.
v0.1.164 (2026-06-24)
Changed
The default dataset view now shows more columns — the label image link, the winery page link, and the critic-review count are added to the Overview table, alongside the existing fields. All of these were already present in the data; this only enriches the default table display.
Documentation accuracy — refreshed the README's throughput/concurrency notes and clarified which fields can be empty for niche or very new wines.
v0.1.163 (2026-06-24)
Fixed (winery details & popularity coverage)
More wines now return their winery link and popularity ranking. On wine pages that list the producer in a compact "Winery" info card (rather than a full producer carousel), the link to the producer was being missed, leaving wineryUrl and winePopularity empty even though the producer page exists on Wine-Searcher. The parser now reads that card too, recovering winery details and popularity for many smaller producers (e.g. Château Malmaison, Julien Meyer, Gabriel & Paul Jouard).
v0.1.162 (2026-06-22)
Improved (popularity coverage under load)
Popularity enrichment now recovers automatically within a run after a temporary block. Previously, a short burst of blocked winery lookups would pause popularity enrichment for the entire remainder of the run; now enrichment briefly backs off, then automatically resumes once conditions clear. This noticeably improves winePopularity coverage on large runs and when several runs execute at the same time. Core wine data (prices, scores, grape, vintage, winery) was never affected.
v0.1.161 (2026-06-19)
Fixed (price precision)
Per-bottle prices derived from case listings are now rounded to 2 decimals. Prices from cases (e.g. a case of 3 or 6 divided down to a single bottle) could be emitted with long floating-point tails (e.g. 1116.6666667); they are now consistently rounded (e.g. 1116.67), matching the rest of the price output.
v0.1.160 (2026-06-19)
Fixed (pricing quality)
Prices are now sourced from the UK fine-wine market by default, which has the deepest merchant coverage (including in-bond offers) and gives the most representative "cheapest worldwide" figure. This restores reliable pricing and coverage; raw prices come back in GBP and every result still carries cheapestPriceEur normalized to EUR for cross-wine comparison. (Power users can still target a specific market by passing proxyCountry in the API input.)
v0.1.159 (2026-06-17)
Changed
Simplified input — removed the "Advanced Settings" section. The proxy is no longer locked to a single country, which broadens proxy coverage and improves reliability against blocks. Prices now reflect global merchant availability and are always provided normalized to EUR via cheapestPriceEur for cross-wine comparison, alongside the original local-currency cheapestPriceAmount / cheapestPriceCurrency. The guidance-only run-timeout field was also removed — the run timeout is set through the standard Apify run options.
v0.1.158 (2026-06-17)
Fixed (data quality)
Popularity ranking now resolves for vintage-prefixed wine names. Wines named in the common "Vintage Producer" style (e.g. "2017 Château Margaux") were failing to retrieve their winePopularity because the vintage year was treated as part of the producer name during the winery-page lookup, even though popularity is ranked per wine across all vintages. The year is now ignored when matching, recovering the ranking for short producer names. The most impactful data-quality fix of this release.
Changed (performance)
Faster by default. The default level of parallelism has been increased, so multi-wine runs now complete in roughly half the wall-clock time, at no cost to reliability. Existing per-request pacing and safeguards are unchanged.
Documentation
Usage guidance: always include the vintage when you care about price — it determines which vintage's price the actor returns.
v0.1.157 (2026-06-15)
Fixed (search resolution)
More wine-name searches now resolve to a direct wine page. When a search lands on a Wine-Searcher results page instead of a single wine, the actor now (a) ignores filter/refinement links when looking for the wine and (b) retries once with a noise-reduced query (dropping parenthetical asides, a leading importer/company prefix, and adjacent duplicate words). Recovers wines that previously returned "no direct page found". Only the results-page path is affected — direct hits are unchanged.
v0.1.156 (2026-06-13)
Added
Four new fields from Wine-Searcher's structured data, at no extra cost:
vintage — vintage year as a number (null for non-vintage wines).
labelImageUrl — absolute URL to the wine label image.
criticReviews — per-critic reviews as an array of { author, score } (e.g. Robert Parker 95, Jasper Morris 90), so you get individual critic ratings, not just the aggregate score. Nested array — use JSON/JSONL for clean access (CSV/Excel flatten it into columns).
v0.1.155 (2026-06-13)
Added
cheapestPriceEur — prices normalized to EUR. Every result now also carries the cheapest price converted to EUR at current exchange rates, so prices are directly comparable across wines regardless of the merchant's local currency. The original cheapestPriceAmount / cheapestPriceCurrency are unchanged; cheapestPriceEur is null when the amount is missing or a conversion rate is unavailable.
v0.1.154 (2026-06-13)
Fixed (data quality)
Currency accuracy. When an offer (or the average-price header) carries no detectable currency symbol, cheapestPriceCurrency is now null instead of being silently labelled USD. Prevents mislabelled currencies in mixed-region results — applies to both the structured offer-card path and the average-price fallback.
Per-bottle price precision. Case-listing prices divided down to a single 75cl bottle are now rounded to 2 decimals — no more long floating-point tails (e.g. 95.83 instead of 95.8341666…).
Fixed (resilience & cost)
Smarter handling of unrecoverable scraping errors. A fatal scraping-service error now stops the run immediately instead of retrying it against every remaining wine; a malformed-request error retries at most once. Eliminates wasted retry storms that previously slowed runs without recovering data.
Timeout-aware shutdown. As a run approaches its time limit it now stops launching new wines early enough for in-flight work to finish, and skips the optional winery-popularity enrichment when there isn't time to complete it (those wines are returned with winePopularity: null). Large runs now end as a clean partial result instead of being killed mid-task. The recommended-minimum-timeout guidance in the input schema was updated to match the measured throughput.
Changed
Internal developer notes refreshed and the billing-table wording tightened to outcome-level rows.
When Wine-Searcher anti-bot-blocks /merchant/ (winery) pages, each optional popularity fetch burned scraper.ts's full block-retry budget (~8 min) and returned null — so a blocked session could hang a run for minutes per wine. Added a run-level circuit breaker (src/winery.ts): after 3 consecutive slow failures (a null taking >60s, indicating blocking rather than a legit "not found"), winery enrichment is skipped for the rest of the run. Main wine data is unaffected (popularity is optional), so a blocked session now degrades fast instead of hanging. Pure recordWineryOutcome/isWineryCircuitTripped policy (+4 unit tests; reset on the winery-buffer reset helper).
v0.1.149 (2026-06-03)
Added (name→LWIN matching via shared wine-core match table)
When a wineNames input resolves against the shared wine-match-table (populated from AVA's manual corrections + LWIN catalog, read at run start via wine-core loadMatchTable), the task is upgraded to a native /find/lwin{code} lookup — Wine-Searcher's authoritative LWIN resolution — instead of the fuzzy /find/{name} search. New extractVintage splits the vintage so the LWIN is built at the right granularity (lwin7+vintage, or lwin7+'1000' for NV). Misses and the no-table case fall back to the existing fuzzy search unchanged (graceful — the actor never depends on the table being present). WineTask.viaMatchTable + a run-log hit-count surface the upgrade rate. Abbreviation expansion is shared from wine-core (expandAbbreviations).
v0.1.148 (2026-06-01)
Added (selector-rot insurance)
JSON-LD schema.org/Product extraction layer (src/jsonld.ts). The parser now reads structured data first and falls back to CSS selectors per field. Wine-Searcher serves a complete Product schema on every detail page — when WS rotates a CSS class (the common cause of silent regressions), users keep getting full data via JSON-LD instead of nulls.
3 raw HTML fixtures captured from live Wine-Searcher pages (tests/fixtures/ws-detail-*.html, tests/fixtures/ws-search-results.html) — first time the test suite covers raw page HTML rather than hand-crafted snippets, so selector rot is caught at PR time, not by users.
No public API change: input schema, output schema, dataset row shape, and pricing model are unchanged. Selector-rot insurance only.
v0.1.140 (2026-06-01)
Fixed (SEV-3)
Currency mapping now covers 25+ symbols (JPY, CNY, HKD, CHF, AUD, CAD, NZD, SGD, ZAR, BRL, ARS, COP, INR, PHP, IDR, KRW, MYR, RUB, VND, NGN, THB, RSD, KES, AED, PLN, CZK, NOK, HUF, RON, etc.). Unknown symbols now return null instead of mislabeling as USD — callers can decide how to handle.
inputType field is now always present on dataset rows (success AND graceful-error rows). Previously omitted on errors, forcing downstream consumers to defend against undefined.
New npm run prepush:version-check script catches version drift between CHANGELOG.md and package.json before deploy.
v0.1.139 (2026-06-01)
Fixed (SEV-4 cost containment)
Cascade-block circuit breaker added. The actor now monitors the ratio of blocked errors over a sliding window of 30 requests. When the rate exceeds 50% past the warmup phase (first 10 requests), the run exits SUCCEEDED with shutdownReason: 'cascading_blocks' rather than continuing to burn compute against a hostile target. Eliminates runaway-cost runs observed in production (3/30 runs TIMED-OUT over 6h).
OUTPUT KV record now includes shutdownReason for run-level observability.
v0.1.138 (2026-06-01)
Fixed
Source code visibility hardened. Local .gitignore now excludes src/*.ts, tests/, dist/main.js.map from the Apify upload set (these were previously bundled by apify push). .actor/actor.json adds isSourceCodeHidden: true to also hide remaining files in the Apify Console UI. Compliance with the global "source-files privacy" rule. No runtime change.
[0.1.136] (2026-05-21)
Changed
Rate limiter switched from a uniform 250 ms delay between launches to a gaussian-distributed delay (mean 1500 ms, stddev 500 ms, floored at 250 ms). The target's anti-bot sensors use uniform-timing patterns as a bot signature — even with concurrency 3, three requests landing every 250 ms is robotic. With the new jitter, 10 initial requests now span ~15-20 s instead of 2.5 s, much closer to human browse cadence.
Rationale
Anti-bot best practice is that every action must continue to look human — passing the first request is not enough. Combined with the 0.1.135 concurrency drop, this gives the actor two independent levers against burst-detection: lower parallelism + non-uniform per-request timing. Trade-off: throughput per minute decreases proportionally to the new mean delay (250 ms → 1500 ms ≈ 6× slower at the launch step), but successful scrapes per minute is what matters, not theoretical request rate. If 0.1.135's concurrency drop wasn't enough to keep blocks down, this should close the gap.
[0.1.135] (2026-05-21)
Changed
DEFAULT_MAX_CONCURRENCY lowered from 10 to 3. Two back-to-back production runs reproduced a full-cascade anti-bot block failure on the first batch of 10 parallel requests: the burst was flagged as bot traffic, all 10 wines exhausted their 3 retries with HTTP 403, run effectively delivered 0 scraped wines. The previous default of 10 was calibrated against a single isolated run; under back-to-back load or stricter anti-bot modes it craters. Default 3 gives a launch pattern ~3× less aggressive (~9 req/3s burst vs ~30 req/3s) and brings actual scrape success back. MAX_CONCURRENCY_HARD_CAP stays at 10 so power users on cooperative sessions can opt into faster wall-clock manually.
README updated: default concurrency mentions changed from 10 to 3, expected wall-clock for 1000 wines from ~2 h to ~6 h (default) with the ~2 h figure noted as the conc=10 opt-in.
Trade-off
1000-wine batches now take ~6 h at default vs ~2 h previously. Reliability is prioritized over raw speed; the per-wine price is unchanged.
[0.1.134] (2026-05-21)
Added
TSV pipeline now expands common French wine-name abbreviations (Gds → Grands, NSG → Nuits-Saint-Georges, VV → Vieilles Vignes, Vge → Village, Chass → Chassagne, Mch → Montrachet). Whole-word, case-insensitive matching; no false positives on words like VVin. Aligns POS-shorthand inputs with Wine-Searcher's canonical naming.
TSV pipeline now flips known reversed-producer surnames to canonical order ("Lachaux Charles" → "Charles Lachaux"). Conservative whitelist of one entry to start; we only add names when a customer run surfaces them as a systematic miss. Most producers (e.g. "Lamy Hubert") are left alone because Wine-Searcher's fuzzy search handles them already.
8 new unit tests for abbreviation expansion + producer flip.
Context
Run FNRPfCGBMDFRUgSI4 (115 wines, build 0.1.133): 51/59 succeeded (86%). The 8 remaining failures split into 3 categories: (A) reversed producer order — 2 cases for "Lachaux Charles", (B) abbreviations in wine names — 2-3 cases like "Gds Suchots", and (C) inputs Wine-Searcher genuinely cannot resolve (generic queries like "Leflaive Chardonnay", 2023 vintages not yet indexed, or obscure producers). This release covers categories A and B. Category C is documented as a Wine-Searcher coverage limitation — no fix possible from the scraper side.
[0.1.133] (2026-05-21)
Added
TSV / Excel-paste input support: when a wine name contains tab characters (typical of POS export pasted from a cellar-management spreadsheet — format "Producer\tWine\tColor\tFormat\tVintage"), the actor now extracts producer + wine name + vintage and drops color/bottle-format metadata. Previously the entire string was URL-encoded as a single search query, which Wine-Searcher could not parse (e.g. "La Vougeraie Vougeot 1er cru Les Cras Rouge 75 cl 2011" → no match). Now the same input becomes "La Vougeraie Vougeot 1er cru Les Cras Rge 2011" and matches reliably. Implemented in parseTsvWineInput() which feeds into the existing cleanWineName() pipeline; 10 new unit tests cover the format variants (Magnum, Jéroboam, accented names, empty fields, vintage in non-final column).
[0.1.132] (2026-05-21)
Changed
Internationalization sweep: all remaining French log strings, status messages, error messages, JSDoc and inline comments across src/ and tests/ translated to English. Aligns with the project's English-only doctrine for internal docs and applicative logs. Wine names (Château, Pétrus, Rosé, Cuvée, etc.) preserved as proper nouns. 279/279 tests passing. No runtime behavior change.
[0.1.131] (2026-05-21)
Changed
Refactor: handleFatalError + FatalErrorOutcome extracted from src/main.ts into a dedicated src/error-handling.ts module. Pure module with no Apify SDK dependency, unit-testable without the 90-line vi.mock block that the previous test file required to import main.ts. No behavior change; the deployed binary is functionally identical.
[0.1.129] (2026-05-20)
Changed
MEMORY_TIERS recalibrated for the post-0.1.121 concurrency cap of 10 (was 40). New recommended-memory floors: ≤200 wines → 512 MB, ≤500 wines → 1024 MB, ≤800 wines → 2048 MB, >800 wines → 4096 MB (was 8192). Aligns with the new defaultMemoryMbytes: 4096 so default-configured 1000-wine runs no longer fire a spurious "memory tight" warning. Refuse thresholds untouched — genuinely under-provisioned configs still get a hard stop.
French strings in src/memory-guard.ts translated to English (header comment, refuse messages, warn message). Aligns with the internal-language doctrine.
[0.1.128] (2026-05-20)
Changed
defaultMemoryMbytes lowered from 8192 to 4096. The 8192 default was provisioned for the old concurrency-40 mode; since 0.1.121 the concurrency hard-cap is 10, which cuts peak memory pressure roughly fourfold. Observed peak in production: ~50 MB. maxMemoryMbytes stays at 8192 so users with 1000-wine batches can still scale up if the memory-guard warning fires. This also lowers the default compute footprint.
[0.1.126] (2026-05-20)
Fixed
Empty or malformed input no longer crashes the run with exit 1. Calls with inputType: "wineNames" (or urls, lwins) and an empty/missing corresponding array, an unknown inputType, a batch over the 1000-wine cap, or zero items surviving per-item validation, now exit cleanly with a single dataset row carrying the error reason. Prevents buggy user scripts from crashing the actor on repeated bad input.
Per-item errors (LWIN format, URL domain, empty name) keep their existing behavior of being partitioned into the invalid[] array — only top-level structural errors changed semantics.
Changed
Internal: validateInput now throws a typed InputValidationError (with code and field metadata) instead of a generic Error. Error messages migrated to English in line with the project doctrine. New handleFatalError helper in src/main.ts classifies caught errors and converts validation errors into structured dataset rows + status messages. 278 unit tests cover the new paths.
[0.1.122] (2026-05-18)
Changed
Canonical name unified across 3 artefacts to "Wine-Searcher Scraper from List" (hyphenated): README H1 + H2, actor.json title, input_schema.json title (was 3 different forms)
actor.json description, seoTitle and seoDescription reordered to popularity-first ("popularity, critic scores, prices")
## Legal & Compliance H2 renamed to ## Legal & compliance (sentence case per de-AI-ification doctrine)
Cross-promo to sister wine-searcher-grape-scraper: slug fixed (was broken wine-searcher-region-scraper), wording updated from "Region Scraper" to "Grape Scraper" and from "appellation" to "grape variety"
Pricing: Starter plan tier renamed $49 → $29/month with recomputed capacity (~1,160 wines/month)
Cross-promo table cell and Option 1 link text: "Wine Searcher Scraper from List" → "Wine-Searcher Scraper from List" (hyphenated)
Features bullets reformatted: **Title Case** - AI-tell pattern replaced with **Sentence case**:
Removed all em-dashes from input_schema.json (6 → 0)
Removed all em-dashes from README and remaining .actor/*.json (zero em-dash policy 2026-05-18)
[0.1.121] (2026-05-13)
Changed
Concurrency capped at 10 (default + hard cap). Empirically: conc=30 delivered 7.9 wines/min on the 102-wine reference run vs 8.77 wines/min at conc=10 — higher concurrency triggered more anti-bot blocks on the winery pages, dragging wall-clock with 30-60s retry backoffs that cancelled the parallelism win. Conc=10 is the measured sweet spot for reliability.
MAX_CONCURRENCY_HARD_CAP = 10 introduced in src/main.ts. The previous schema range 1-50 is now clamped to 1-10. Existing callers passing maxConcurrency: 30+ will be silently capped — no breaking change for the success path.
Adaptive concurrency caps removed (LARGE_BATCH_THRESHOLD, VERY_LARGE_BATCH_THRESHOLD, MAX_CONCURRENCY_LARGE_BATCH, MAX_CONCURRENCY_VERY_LARGE_BATCH, RECOMMENDED_MIN_CONCURRENCY) — dead code once the hard cap is 10.
Default timeoutSecs 7200 → 14400 (4h) to accommodate 1000-wine runs at conc=10 (~2 h expected, comfortable margin for retries).
Why this reversal
The 0.1.120 default of conc=40 was based on linear extrapolation from a single 100-wine / conc=10 benchmark (8.77 wines/min → 35 wines/min @ conc=40). The 102-wine / conc=30 run that actually existed in production showed worse throughput than conc=10 — the bottleneck is the block-retry tail, not raw concurrency. Aggressive concurrency burns retries, not throughput.
Trade-off
1000-wine batches now take ~2 h instead of the previously claimed ~30 min. The per-wine price is unchanged ($0.025/wine).
[0.1.120] — 2026-05-13
Changed
Run capacity raised: 500 → 1000 wines.MAX_ITEMS_PER_RUN (input.ts) and maxItems (input_schema, 3 places) raised to 1000. DEDUP_READBACK_LIMIT (main.ts) 1000 → 2000 to stay strictly > MAX_ITEMS_PER_RUN after container migration.
Default memory: 1024 → 8192 MB (maxMemoryMbytes aligned to 8192). Provisioned for 1000 wines @ conc=40 with comfortable headroom.
Default concurrency: 30 → 40. Target 1000 wines / 30 min under the measured throughput baseline (~8.77 wines/min @ conc=10, assuming linear scaling up to conc=40).
Adaptive concurrency caps recalibrated for the 1000-wine range:
LARGE_BATCH_THRESHOLD 200 → 400 (conc cap 25 → 40)
VERY_LARGE_BATCH_THRESHOLD 400 → 800 (conc cap 20 → 35)
RECOMMENDED_MIN_CONCURRENCY 20 → 30
LARGE_INPUT_WARNING_THRESHOLD 300 → 1000 (the "split into batches of 500-800 wines" warning now fires only beyond the new ceiling).
Memory guard
Tier-2 refusal added: <2048 MB on >700 wines → refuse (before scraping). Prevents likely OOM in the 1000-wine range.
Recommended-memory tiers introduced via recommendedMemoryForTasks(taskCount): ≤200 → 1024 MB, ≤500 → 2048 MB, ≤800 → 4096 MB, >800 → 8192 MB. The warn is now proportional to batch size instead of a fixed threshold.
Minimum warn threshold: no memory nudge below 100 wines (any config passing the refusal tiers stays "ok").
Tests
270 tests passing (+ 11 new: 1 for the tier-2 refusal, 3 for the ok at 8192/1000, 8 parametrized on recommendedMemoryForTasks).
Expected performance (to validate in live)
1000 wines @ conc=40, 8192 MB → ~30 min wall-clock (vs ~57 min @ conc=20 with old default).
User cost unchanged: $25 / 1000 wines (PPE unchanged at $0.025).
Why now
Analysis of the last 61 runs (30 days): all 5 non-trivial incidents (TIMED-OUT, ABORTED, FAILED) in May involved batches of 350-500 wines with 512-1024 MB RAM. The 1024 MB default was under-provisioned beyond ~300 wines.
[0.1.112] — 2026-05-10
Fixed
"Showing results for…" page detection previously missed: the isSearchResultsPage scan was limited to the first 30,000 characters of HTML, but Wine-Searcher pages are 250+ KB and the marker can appear around ~50 KB. Now a full HTML scan (~1 ms overhead). 33 wines per batch of 500 (~7%) fell through this gap — their H1 "Showing results for 'lwin1065208'" became the wineName, all other fields were null, and PPE was charged for junk data.
Downstream guard isSuccessfulResult: if despite the upstream detection a wineName starts with "Showing results for", billing is now refused (defense-in-depth backstop). Estimated saving: ~$0.83 per batch of 500 wines, and popularity-on-success rises from 92.4% → ~98%.
Changed
MAX_ADDITIONAL_WINERY_PAGES 5 → 10: for large producers (Méo-Camuzet with >60 wines listed across >6 pages), the specific wine was on page 7+ and the algorithm gave up after 5 additional pages. PER_WINERY_BUDGET_MS = 600s remains the hard cap, so this extension does not create uncontrolled fan-out.
Tests
252 → 254 (+2 tests sur le backstop "Showing results for" dans isSuccessfulResult).
[0.1.116] — 2026-05-10
Fixed
cheapestPriceAmount is now ALWAYS per-bottle — when the offer card is a case ("Case of 12", "6x75cl", "12 bottles"), the price is divided by the number of bottles before being returned. Before this fix, some EU merchants (e.g. SELECTION SOMMELIER on Smith Haut Lafitte 2020 → €408 for 12 bottles) returned the full case price, causing a silent massive overpricing (~12×). Mainly affects Bordeaux/Burgundy wines sourced from Europe. If you have an internal baseline built from the raw price, expect prices to drop for these wines — this is the correct behavior per the cheapestPriceAmount = "per 75cl bottle" documentation.
Changed
New bottlesPerUnit field in the dataset: 1 for single-bottle (default case), 2-24 for detected cases. Allows reconstructing the case price (cheapestPriceAmount × bottlesPerUnit) or auditing the normalization. Detected patterns: case of N, Nx75cl, Nx750ml, N bottles, N-pack. Sanity guard: if the per-bottle price after division falls outside [$1, $100000], the offer is skipped (likely false positive).
Bug reported by a user via the Apify Store Issues tab, reproduced in production on Smith Haut Lafitte 2020.
[0.1.110] — 2026-05-10
Changed
Concurrency caps raised after stress tests: MAX_CONCURRENCY_LARGE_BATCH 15→25 (>200 wines) and MAX_CONCURRENCY_VERY_LARGE_BATCH 10→20 (>400 wines). Calibrated on cold-start stress tests at 350 and 500 wines where memory stayed ≤320 MB peak thanks to the LRU-bounded winery HTML buffer from 0.1.103. Expected speedup ~1.8×–2× on large batches without OOM risk (pre-LRU memory = 80+ MB for 200 wineries → now capped at ~12 MB).
Fixed
memory guard warn message fixed: now recommends the next tier up (1024→2048 MB for batches >300 wines already at 1024 MB) instead of the contradictory "1024 MB recommended" when the user is already at 1024 MB. Discovered during stress tests.
[0.1.106] — 2026-05-10
Changed
PPE policy hardened: billing only on full success.Actor.charge('wine-extracted') is now guarded by isSuccessfulResult(data) which requires wineName !== null && no error. Consequence: notFound, infrastructure errors (timeout, blocked, transient_api, transient_net, rate_limit), internal bugs, and parsed-empty results are pushed to the dataset as structured errors but are no longer billed. Previously every push (except invalid input + auth) was billed.
Fixed
Parsed-empty detection + analytics alarm. When a scrape succeeds (HTML 200 OK) but parsing returns wineName/price/winery all null, this is now treated as a structured error (error: "Parsed empty (possible site change)") without billing PPE and with a parsedEmptyCount counter. If > 0, an explicit warning prompts checking the Wine-Searcher DOM. Free DOM-change canary.
Isolated success counter: the final "Done: N wine(s) extracted" message now uses validCompletedCount (real successes only) instead of pushedInputValues.size which included error rows.
Defensive try/catch around pushAndCharge in the global catch of processWine — a transient Apify storage hiccup during error-handling no longer flips the run to FAILED.
maxConcurrency clamped between 1 and 50 (NaN → default). Prevents 2h CPU spins on malformed REST API input.
Sanitizer logs now also scrub the literal API key value (in addition to the URL and provider brand name).
Internal data reads return defensive copies to prevent callee mutations from corrupting shared state.
LRU peek() non-touching + identity guard before winery-buffer eviction to prevent a stale handler from deleting an entry freshly pushed by another consumer (rare race post-eviction).
DEDUP_READBACK_LIMIT = 1000 named constant + assertion > MAX_ITEMS_PER_RUN at startup. Guarantees that any future move to >1000 wines per run will not cause silent duplicate entries after container migration.
Tests
245 → 252 tests (+7). Full suite passes locally.
[0.1.103] — 2026-05-09
Fixed
Startup memory guard: a run with a structurally OOM-bound RAM × volume combination (e.g. 512 MB × 350 wines) is now refused immediately with a clear action message, instead of running for 18 minutes before a SIGKILL.
Non-fatal validation: an invalid LWIN, URL, or wine name no longer kills the entire batch. Rejected entries are pushed to the dataset as structured errors (without PPE billing), letting the user see exactly what was rejected. Before this fix, a single 18-digit LWIN format would fail an input of 500 wines.
Double winery-retry layer removed: the external retry layer (MAX_WINERY_SCRAPE_RETRIES) that duplicated scraper.ts's internal policy is removed. Worst-case per winery drops from 10.5 min to ~8 min on blocks, freeing the concurrency pool and avoiding run timeouts.
Winery HTML buffer bounded (LRU 30): the winery HTML buffer is now limited to 30 entries (≈ 12-30 MB max) instead of growing linearly. Prevents OOM SIGKILLs on batches >300 wines at 512 MB memory.
Per-winery budget 10 min: a new global ceiling protects against the worst-case where multiple pagination pages of the same winery are all blocked (which could otherwise reach 6 × 8 min = 48 min per winery).
[0.1.102] — 2026-05-08
Changed
Winery timeout increased from 90s to 120s for better reliability on slow winery pages.
Tests
Exported MAX_WINERY_SCRAPE_RETRIES constant; tests now derive all magic numbers from it. Fixed invalid HTML fixtures (<a> without <td> wrapper). Renamed shadowed WINERY_URL variable.
[0.1.100] — 2026-05-07
Fixed
Winery URL fallback for producers without carousel/profile section. When the wine page lacks the "Also from..." carousel and profile section (small producers like Edmond Vatan), the parser now searches all /merchant/{id}-{slug} links on the page and matches the slug against the extracted winery name. Fixes winePopularity: null for wines where the winery name was found but the URL wasn't.
Changed
Internal data-format versioning fix so wines affected by the missing winery URL fallback now return complete winery data.
Winery popularity: multi-page search for wines not on page 1. Winery pages show 10 wines per page sorted by popularity. When a wine isn't on page 1, all Chassagne-Montrachet entries (for example) tie at the same score, and the first entry's rank is returned incorrectly. The scraper now detects ambiguous matches (multiple entries tied at the best score) and lazily scrapes additional winery pages (/11, /21…) until finding a confident match. Fixes wines like Colin-Morey Les Charmes, Baudines, La Garenne which were all returning "487th" (Corton-Charlemagne's rank from page 1).
Added
detectWineryPagination() — extracts additional page URLs from winery HTML pagination links.
Multi-page fetch loop in fetchWineryPopularity() with lazy evaluation (stops on first confident match) and cap at 5 additional pages.
Tests
9 new tests: pagination detection (4), tie detection (2), multi-page resolution (3). 203 tests total.
[0.1.98] — 2026-05-07
Fixed
Stale shared popularity corrected. Wines extracted before 0.1.97 could carry the wrong shared popularity (all wines from the same winery getting the first wine's rank). Each wine now gets its own correctly parsed rank.
Changed
Internal data-format versioning added so future fixes propagate cleanly.
[0.1.97] — 2026-05-07
Fixed
Winery popularity: per-wine parsing instead of shared result. Refactored the winery HTML buffer to store raw HTML instead of the parsed popularity string. Each wine now gets its own popularity ranking from the winery page — previously, all wines from the same winery shared the first wine's ranking (e.g. all 5 Colin-Morey wines returned "487th" instead of their individual ranks).
Changed
In-memory winery dedup now stores Promise<string | null> (HTML) instead of parsed popularity.
New internal fetchWineryHtml() handles scrape + buffer + retries. fetchWineryPopularity() now calls it then parses individually per wine name.
Tests
New test: "concurrent wines share same scrape but get individual popularity" — 2 wines from the same winery get different rankings (487th vs 2,622nd) with a single scrape call. 194 tests total.
[0.1.96] — 2026-05-07
Fixed
Winery popularity: normalize hyphens and accents in name matching.parseWineryPopularity() now normalizes both the wine name and the winery page HTML text before comparison — hyphens are converted to spaces and diacritics are stripped (Unicode NFD). Fixes winePopularity: null for producers like Pierre-Yves Colin-Morey where the winery page uses different hyphenation/accentuation than the wine page (e.g. "Pierre Yves" vs "Pierre-Yves", "Chatenière" vs "Chateniere").
Winery timeout raised from 60s to 90s. Some winery pages (e.g. Pierre-Yves Colin-Morey) intermittently require >60s due to scraping provider retry loops. The increased timeout improves first-attempt success rate without excessive pool blocking.
Tests
tests/winery.test.ts — 4 new tests for hyphen/accent normalization (hyphens in name only, hyphens in HTML only, both, neither). 194 tests total (was 190).
[0.1.92] — 2026-05-07
Fixed
Winery popularity: internal retries benefit all concurrent consumers. Moved retry logic inside the shared Promise so that when multiple wines share the same winery, a transient scrape failure triggers up to 2 internal retries (5s delay each) and ALL concurrent consumers receive the successful result. Previously, the external wineryFailCount mechanism only worked for sequential requests — under concurrency 30, all wines from the same winery joined the same failing Promise with no retry opportunity.
Changed
Removed wineryFailCount Map and handleWineryFailure() — replaced by internal retry loop within fetchWineryPopularity().
The winery-buffer reset helper simplified (no longer needs to reset fail counters).
Tests
tests/winery.test.ts — Fully rewritten: 7 tests (was 6). New test: "concurrent wines benefit from internal retry on failure" — 5 concurrent calls, first scrape fails, retry succeeds, all 5 get the result. 190 tests total.
[0.1.89] — 2026-05-07
Added
Warning on large batches (>300 wines). A log warning now recommends splitting into batches of 200-300 wines when more than 300 are submitted, to prevent performance degradation.
Success rate monitoring. At the end of each run, the analytics module calculates and logs the success rate (%). If the rate drops below 85% on batches >10 wines, a warning is emitted to flag potential recurring errors. The success rate tracks scraping infrastructure failures (timeouts, blocked requests, API errors) — not whether a wine exists on Wine-Searcher. Invalid or unknown wines are still scraped successfully and return a result row with partial data (name from Wine-Searcher's "Showing results for…" fallback, no score/price).
successRate field in RunAnalytics (persisted in internal analytics storage).
tests/analytics.test.ts — 3 new tests (successRate calculation, null on empty, threshold constant). 20 tests total (was 17).
[0.1.88] — 2026-05-07
Changed
Default memory raised to 1024 MB (was 512 MB). Prevents OOM kills (exit 137) on large batches that previously hit the 512 MB ceiling at ~500 wines.
Adaptive concurrency cap. Batches >400 wines are automatically capped at concurrency 10; batches >200 wines at concurrency 15 — regardless of the user's maxConcurrency setting. Reduces peak memory footprint by limiting in-flight HTML pages.
Added
Winery timeout (60s). Each winery scrape is now wrapped in a Promise.race with a 60-second deadline. If a winery page is blocked and the scraping provider retries for minutes, the timeout fires, the winery is evicted from the in-memory winery dedup buffer (allowing a future retry), and the wine is pushed with winePopularity: null instead of blocking the entire pool.
tests/winery.test.ts — 1 new test (winery timeout returns null). 6 tests total (was 5).
[0.1.86] — 2026-05-05
Fixed
winePopularity null on batch runs. When multiple wines share the same winery and the winery scrape fails (blocked, timeout), the null result was stored permanently in the Promise-based in-memory winery dedup buffer. All subsequent wines from the same producer inherited null popularity without retrying. Now failed entries are evicted from the buffer with up to 2 retries per winery URL before giving up.
Added
tests/winery.test.ts — 5 tests covering retry logic for fetchWineryPopularity.
[0.1.85] — 2026-05-05
Fixed
403 errors on user URLs. URLs passed in urls mode are now normalized: country/currency suffixes (/usa/usd, /fr/eur) are stripped and non-ASCII characters (e.g. Rosé) are percent-encoded. This prevents systematic 403 errors caused by malformed URLs.
Increased blocked request retries from 2 to 3 (4 total attempts, backoff 30s + 60s + 120s) to handle transient blocks more reliably.
Added
normalizeWineSearcherUrl() exported from src/input.ts for URL sanitization.
"Blocked by Wine-Searcher (403 errors)" troubleshooting section in README.
[0.1.82] — 2026-05-05
Added
Graceful shutdown. Actor now handles aborting (user cancel), migrating (container migration), and a soft deadline (timeout − 45s) to stop cleanly. In-flight wines finish, partial results are delivered as SUCCEEDED with a clear status message (Partial: X/Y wines (stopped by {reason})). Reduces TIMED-OUT and ABORTED failure rates.
Typed shutdown reason.ShutdownReason union type ('user abort' | 'migration' | 'timeout') prevents stringly-typed bugs.
Changed
Timeout estimation formula corrected. Now accounts for concurrency: max(120, ⌈batchSize ÷ concurrency⌉ × 25) instead of batchSize × 8. Documentation updated in input schema and README.
[0.1.80] — 2026-05-05
Fixed
Memory pressure reduced — fixes OOM on large batches (500 wines). In-memory winery dedup now stores parsed popularity strings instead of raw HTML, freeing ~60 MB of permanently retained data. Wine page HTML is released immediately after parsing, saving ~9 MB at concurrency 30. Combined savings prevent OOM kills (exit code 137) that occurred at 503/512 MB.
Dead code removed — redundant guarded block eliminated.
Changed
Internal data accessors simplified to return T | null directly (was a wrapped object) — callers no longer need defensive destructuring.
[0.1.77] — 2026-04-25
Changed
README optimized for Apify Store. Complete restructure aligned with Vivino actor template: added "What is" intro, "Which wine scraper should I use?" cross-selling table, "Quick Start — Test in 60 seconds", "Why scrape Wine-Searcher?" use cases, data extraction table with "Always included" column, configuration table with JSON examples, "Tips for best results", "Troubleshooting" (5 scenarios), "Privacy & Security", "Resources", "License". Pricing reformatted with tiers. FAQ enriched (10 questions). Changelog limited to 10 most recent versions. "Related Wine Scrapers" moved up and expanded.
[0.1.76] — 2026-04-22
Added
POS/inventory wine name cleaning. Wine names from POS systems (e.g. Champagne, Dom Perignon Brut, 2013, Champagne, France) are now automatically cleaned before searching Wine-Searcher. The pipeline strips category prefixes (Champagne, Port, Dessert Wine, Red Blend, Sauvignon Blanc…), bottle sizes in parentheses ((375ml), (Split 187ml), (1.5L)…), and replaces commas with spaces. The original input is preserved in inputValue — only the search URL is cleaned. This dramatically improves match rates for clients sending POS/inventory-formatted wine lists.
New exported function cleanWineName() in src/input.ts with 27 unit tests.
Changed
170 total tests (was 139).
[0.1.74] — 2026-04-21
Fixed
Numeric LWIN codes no longer crash the actor. REST API and integration clients (n8n, Make, Python, etc.) sending LWIN codes as numbers ([1067130]) instead of strings (["1067130"]) caused an immediate fatal error. normalizeLwinEntry now accepts number entries and converts them to strings before validation. Affects LWIN7, LWIN11 and longer codes.
Missing inputType no longer crashes the actor. API clients omitting the inputType field (which is only auto-filled by the Apify Console UI) caused an immediate fatal error. validateInput now auto-detects the input type from whichever array field is populated (lwins, urls, or wineNames).
Changed
LwinEntry type extended to accept number in addition to string and object formats.
10 new tests covering both fixes (139 total, was 129).
[0.1.73] — 2026-04-20
Changed
Unified pipeline. Merged 2-phase architecture (Phase 1: wine pages → Phase 2: winery pages) into a single pipeline where each task chains wine scrape → parse → winery scrape → push without waiting for other tasks. Eliminates idle time between phases (~15-20% throughput improvement).
Analytics: phase1DurationMs + phase2DurationMs replaced by single scrapingDurationMs.
Scraping response metrics (recordResponseMetrics) downgraded from log.info to log.debug — reduces log noise on large batches while keeping the end-of-run summary in log.info.
Removed
WinePhaseResult intermediate interface (no longer needed with unified pipeline).
Winery-buffer reset call between phases (buffer is empty at startup).
[0.1.71] — 2026-04-20
Changed
Console UI and output streamlined. Removed two experimental input fields, an internal timestamp field from dataset rows, an unused storage schema declaration in actor.json, and stale FAQ/pricing rows; reduced log verbosity at the default level.
[0.1.69] — 2026-04-19
Added
API Integration guide. README now documents synchronous (run-sync-get-dataset-items) and asynchronous (/runs) API calls with cURL, Node.js and Python examples. Dataset export formats table (JSON, CSV, Excel, XML, JSONL) with field filtering.
Workflow & database integration guide. New "Integrate into Your Workflow" section: scheduled runs (cron examples), webhooks (Flask → PostgreSQL example), full database integration examples (Node.js + PostgreSQL, Python + SQLite), no-code integrations table (Google Sheets, Airtable, Zapier, Make, n8n), and large catalog batching pattern (>500 wines).
Changed
FAQ "Can I integrate this with my existing tools?" now links to the new integration section instead of a generic answer.
[0.1.67] — 2026-04-19
Changed
All wines billed uniformly at $0.025. Updated all marketing copy (README, input schema) to reflect that every extracted wine carries the standard $0.025 PPE charge regardless of internal data origin. The code already billed uniformly — this aligns the documentation with actual behavior. Removed "free re-fetch" mentions from key features, pricing table, FAQ, and input field descriptions.
[0.1.63] — 2026-04-19
Added
Run analytics. Structured metrics are now persisted to internal storage at the end of every run. Includes batch size, input type, success/error/not-found counts, phase durations, and full scraping retry distribution. Enables data-driven monitoring of actor health and usage patterns.
Timeout guidance. New informational timeoutSecs field in the Apify Console input UI with recommended values. The actor now warns at startup if the allocated run timeout looks too low for the batch size (formula: max(120, batchSize × 8) seconds). README FAQ enriched with a batch-size-to-timeout recommendation table.
Review solicitation. End-of-run logs now include a visible call-to-action with the Apify Store review link (with affiliate tag). The actor status message shows the wine count on completion.
Fixed
Missing analytics tracking on 4 error paths: HTML-level 404 detection, search-results redirect failures, and Phase 2 inner catch fallback were not counted in notFoundCount / errorCount.
Changed
Finalization logic (logScrapingSummary, analytics persist) moved to a finally block — ensures metrics are always saved, even on fatal errors.
Extracted cloneScrapingMetrics() helper in scraper.ts to eliminate duplicated deep-copy logic between getScrapingMetrics() and getAnalyticsSnapshot().
[0.1.62] — 2026-04-19
Added
LWIN16/LWIN18 support. Longer LWIN codes (12+ digits) are now automatically truncated to the first 11 digits (LWIN11) before URL construction. Previously, these codes caused a validation error — now they work seamlessly for users whose wine management software exports extended LWIN formats.
[0.1.61] — 2026-04-18
Changed
maxConcurrency removed from the Apify Console input UI — concurrency is now fixed at 30 for all users. The parameter remains functional via the REST API for power users.
README FAQ updated: concurrency no longer advertised as a tunable setting.
[0.1.60] — 2026-04-17
Fixed
Duplicate dataset entries on long runs. When Apify migrates the actor container mid-run, the script restarts from scratch on the same dataset — previously producing duplicate entries (up to 1.5× the expected item count). Now: existing dataset items are read at startup to detect restarts, already-pushed wines are filtered from the task list, and a Set-based guard in pushAndCharge prevents any double push within the same execution.
[0.1.59] — 2026-04-17
Changed
Default maxConcurrency bumped from 10 to 30 — the actor now scrapes up to 30 wines in parallel (was 10), tripling throughput for large batches.
Input schema description updated with realistic timing (500 wines ≈ 30-60 min) and consistent guidance.
README FAQ updated to reflect new defaults and validated performance data.
Removed
Internal operational metrics no longer appear in the actor logs. Logs now show request count + observed concurrency only.
Provider brand references scrubbed from JSDoc comments and log lines — all naming is now generic (scraping-api).
[0.1.57] — 2026-04-16
Fixed
HTTP 403 no longer kills the entire run. Previously, a single 403 from the scraping provider was classified as permanent_auth (dead API key) and triggered Actor.exit(1) — dropping all successfully parsed wines from the dataset. Now: 401 = permanent_auth (fatal), 403 = blocked (retryable with 30s/60s backoff + jitter). A 403 isolated to one URL is retried; if all attempts fail, that wine is marked as error and the run continues.
[0.1.56] — 2026-04-16
Added
Differentiated retry strategy with 7 failure categories: permanent_auth, not_found, rate_limit, transient_api, transient_net, blocked, timeout. Each category has its own retry policy (0-5 retries, custom backoff, jitter ±30%).
Retry distribution stats in end-of-run log: attempt-1=N (X%), attempt-2=M (Y%), failed=K (Z%) + failure categories breakdown.
Retry-After header respected on HTTP 429 responses.
ScrapeResult discriminated union type replaces raw string | null returns from scraper — callers get structured success/failure data.
Changed
scrapeWithRetry refactored: classifyError pure function determines category, RETRY_POLICY table drives retry/backoff per category, exponential backoff with ±30% jitter prevents thundering herd.
not_found (HTTP 404) is no longer retried (0 retries) — saves provider budget on wines that don't exist.
Removed
Legacy MAX_RETRIES constant and uniform retry logic.
[0.1.55] — 2026-04-16
Changed
maxConcurrency upper limit raised from 15 to 50.
RECOMMENDED_MIN_CONCURRENCY raised from 10 to 20 — runtime warning triggers for large batches with concurrency below 20.
Added
README FAQ: "Which run timeout should I set?" (prompted by an external user's TIMED-OUT run with timeoutSecs: 15).
[0.1.54] — 2026-04-16
Added
Scraping instrumentation: per-request log with concurrency remaining/limit and request ID. End-of-run synthesis with total request count and min concurrency observed.
Hidden adaptive-request-mode flag (REST API only, not in input schema) for internal A/B testing of the scraping request configuration.
Tested
Internal A/B test on 20 diverse wines: the alternative request mode offered no measurable benefit but doubled runtime due to 6× more HTTP 403 retries. Decision: keep the default mode.
[0.1.53] — 2026-04-16
Changed
Rate limiter reduced from 2s to 250ms between scraping requests — major throughput improvement.
Default maxConcurrency raised from 3 to 10, upper limit from 10 to 15.
Maximum wines per run capped at 500 (was 1000) with input validation — prevents runs that are mathematically impossible to complete within the timeout.
Default run timeout set explicitly to 2 hours in actor.json.
Fixed
TIMED-OUT runs reduced from ~9.3% to near zero (root cause: 2s rate limiter + concurrency 3 + no batch cap).
[0.1.38–0.1.52] — 2026-03-23 → 2026-04-16
Changed
Scraping backend upgraded for better reliability and coverage. Transparent to users — same input, same output.
Internal scraping client refactored to provider-neutral naming (ScraperClient, SCRAPING_API_BASE_URL).
parseWinePage(html) consolidated to a single cheerio.load() call (was 3× per wine).
Various parser optimizations: regex on raw HTML instead of $('body').text(), toAbsoluteUrl() helper, isProTeaserCard() predicate.
buildWineResult() and pushAndCharge() helpers eliminate Phase 2 code duplication.
fetchWineHtml() helper encapsulates mock vs live dispatch.
Internal storage handle memoized (avoids re-opening it on every call).
WineTaskInputType union type replaces raw string for WineTask.inputType.
[0.1.37] — 2026-03-23
Added
Optional scrapingApiKey input field marked as isSecret for users who want to provide their own key (overrides environment variable).
[0.1.36] — 2026-03-23
Changed
Scraping API key environment variable renamed to a provider-neutral SCRAPING_API_KEY. Apify secret updated accordingly.
[0.1.35] — 2026-03-23
Fixed
Apify quality score improvements: all input field descriptions rewritten (what + why + how + example format), minItems/maxItems added to arrays.
Missing API key no longer crashes with Failed status — now exits cleanly via Actor.exit() with a user-friendly message.
Scraping-provider mention removed from the README footer.
[0.1.34] — 2026-03-19
Fixed
LWIN object format support: {"lwin7": "1131644", "vintage": 2021} and {"lwin11": "11316442021"} now work correctly (was stringified as [object Object]).
Phase 2 errors no longer crash the actor — failed winery scrapes push results with winePopularity: null instead.
Added
normalizeLwinEntry() function with validation (7-digit LWIN7, 10-11 digit LWIN11).
README Field Reference table completed (14 → 18 fields).
[0.1.32] — 2026-03-19
Added
Internal storage schema declared in actor.json.
[0.1.31] — 2026-03-19
Changed
Pricing model changed: from bring-your-own-key to scraping included at $0.025/wine. Zero setup required for users.
The scraping API key was removed from user input — now managed via Apify secret.
README completely rewritten for new pricing model.
[0.1.30] — 2026-03-19
Added
Output schema (.actor/output_schema.json) with full JSON Schema for all 18 dataset fields.
Fixed
Dataset push crash on null fields: winePopularity can be null — schema updated to ["string", "null"] types.
[0.1.29] — 2026-03-19
Added
SEO metadata and Apify Store categories in actor.json.
[0.1.27] — 2026-03-18
Fixed
Winery name fallback: bottle size suffix ( - 75cl) no longer pollutes extracted name; vintage years (19xx/20xx) are skipped as candidates.
[0.1.23–0.1.26] — 2026-03-17
Added
Smart winery retry: wines with missing winery data (winePopularity: null but wineryUrl present) are automatically retried in Phase 2 instead of serving stale nulls forever.
Winery name fallback via offer-card consensus: when a producer has no dedicated Wine-Searcher page (no /merchant/ link), the winery name is extracted from offer descriptions using frequency-based voting across cards.
Changed
Complete marketing rewrite: README with SEO structure (6 key features, 4 use cases, pricing table, 7 FAQ), input schema descriptions enriched, actor.json SEO description.
Global rate limiter (2s between requests) prevents burst patterns.
Winery-specific backoff increased to 5s/15s/30s (was 2s/4s).
In-memory Promise-based winery dedup — duplicate winery requests share a single scrape.
Phase 1 (wine pages) and Phase 2 (winery pages) now run sequentially instead of interleaved.
[0.1.16] — 2026-03-17
Changed
PPE pricing set to $0.008/wine (later raised to $0.025 in 0.1.31).
Scraping API key configured via Apify secret (no longer required in input).
[0.1.13] — 2026-03-16
Added
Search results page detection: when Wine-Searcher returns a search results page instead of a wine profile (common with ambiguous wine names), the actor detects it and follows the first wine link automatically.
HTML scan window extended to 100k characters (Wine-Searcher header/nav occupies 70k+ chars before content).
[0.1.7–0.1.12] — 2026-03-16
Changed
Migrated from Node.js 20 to Node.js 22.
proxyCountry parameter implemented (controls which merchant offers and prices are displayed).
Concurrency pool hardened.
[0.1.1–0.1.6] — 2026-03-07
Added
Initial release: Apify Actor extracting wine scores, prices, winery info and popularity from Wine-Searcher.com.