# Changelog of Redfin Property Listings Scraper (`devilscrapes/redfin-property-listings-scraper`) Actor

- **URL**: https://apify.com/devilscrapes/redfin-property-listings-scraper/changelog.md
- **Full Actor documentation**: https://apify.com/devilscrapes/redfin-property-listings-scraper.md

## Redfin Property Listings Scraper — Changelog

### 0.2 — 2026-08-24

- **Fix: customers charged for runs that produced zero rows.** Root cause —
  the account's default proxy group (`BUYPROXIES94952`, datacenter) gets a
  hard `405` from Redfin's edge on every single search-page request, live-
  probed from this machine across all four `BROWSER_PROFILES`. Confirmed the
  `405` was our own datacenter IP, not a wrong HTTP verb — `client.py`
  issues a genuine `GET` throughout, verified against `curl_cffi`'s source.
  `src/models.py`'s `proxy_configuration` field validator (spec REQ-11) now
  defaults `apifyProxyGroups` to `["RESIDENTIAL"]` instead of forbidding it
  — the original prohibition was written on the FREE plan against an
  `availableCount:0` cardinality misread (see memory
  `reference-residential-is-entitled`); this account has been on STARTER
  since 2026-08-20 and RESIDENTIAL clears with a clean `200`.
  `.actor/input_schema.json` default/prefill updated to match.
- **Fix (found while verifying the proxy fix): every row was silently
  dropped as "unparseable" — a second, independent defect masked by the
  405.** `src/parser.py::home_to_result_row` required `propertyId` to be a
  JSON string; the live `gis` payload sends it (and `listingId`) as a JSON
  **number**. New `_stringify_or_none()` helper stringifies numeric
  IDs/enum codes faithfully instead of dropping them. Also applies to
  `searchStatus`/`listingType`/`propertyType`/`uiPropertyType`, which are
  small integer enum codes in the live payload, not the semantic strings
  (`"Active"`, `"for-sale"`, `"Single Family Residential"`) spec.md and
  `tests/fixtures/gis_response_sf.json` originally — and never actually
  live-verified — assumed. Those four fields now surface as raw numeric-
  code strings rather than a guessed semantic label (guessing would
  fabricate a value, forbidden by REQ-8).
- Confirmed (not changed): the REQ-13 "zero rows across all entries fails
  loud" guard in `src/main.py::main()` was already correct in this build
  (`SystemExit(1)`, never a silent SUCCEEDED) — the 2026-08-15 build that
  shipped a false SUCCEEDED-with-zero-rows predates it. Regression test
  `tests/test_main.py::test_zero_rows_across_all_entries_raises_systemexit`
  already pins this and continues to pass.
- **Verified end-to-end**: `apify run` against the real RESIDENTIAL proxy
  (`tests/fixtures/input.qa.json`) now scrapes 8/8 real Redfin listings,
  exit code 0 — was 0 rows / exit 91 before this fix.
- See `docs/specs/redfin-property-listings-scraper/notes.md` for the full
  probe table and root-cause writeup.

### 0.1 — 2026-08-12

- Real crawler implemented end to end: `src/resolver.py` (pure RSC `gis`
  query-string extraction tolerating single-/double-escaped `/`,
  `ExpectedGeo`/`ResolvedRegion` DTOs, two-signal `passes_geo_guard()`
  for REQ-3's geo-splash defense), `src/client.py` (the only module
  importing `curl_cffi` — `RedfinSession` with browser-profile rotation,
  408/429/503/network-error retry with 2s→30s capped exponential
  backoff, `session_id` rotation, `{}&&`-prefix-stripping + `resultCode`
  validation for the `gis` envelope), `src/paginator.py` (price-bucket
  bisection past Redfin's 350-home server cap with `mpt=99`, `propertyId`
  dedup via a shared `seen_ids` set, per-bucket fault isolation),
  `src/parser.py` (pure `homes[]` dict → `ResultRow` mapping, never
  fabricating a value), and `src/main.py` (SDK wiring: `actor-start`/
  `result-row` PPE charging, per-entry geo-guard rotate-retry-once-then-
  skip, per-entry fault isolation, REQ-13 fail-loud on zero rows).
- Live-confirmed 2026-08-12 (real network fetches against redfin.com):
  the RSC regex, the `{}&&` envelope prefix, the 350-home cap, and the
  `mpt=99` bisection requirement all match spec.md's wire-format claims
  verbatim. Additionally confirmed empirically during implementation
  that `status="sold"` maps to `sold_within_days=90` (with `sf` absent
  from the query) — spec.md's Open Questions item 1 for `sold` is now
  resolved. The `pending` status mapping remains an unconfirmed
  best-effort placeholder (`STATUS_PARAMS["pending"]`, see the inline
  comment in `paginator.py`) after several live probes returned either
  the for-sale-shaped default or a bot-challenge page — paginator/parser
  stay permissive so a wrong guess degrades gracefully, never crashes.
  Also discovered live that `soldDate`/`openHouseStart`/`openHouseEnd`
  arrive as epoch-ms integers, not the ISO `YYYY-MM-DD` strings spec.md
  assumed — `parser._epoch_ms_or_str_to_iso()` handles both shapes.
- 62 unit/integration tests (`tests/test_resolver.py`,
  `tests/test_client.py`, `tests/test_paginator.py`,
  `tests/test_parser.py`, `tests/test_main.py`) — all pure/mocked, no
  live network in CI. `ruff check` clean.

### 0.0.1 — 2026-08-12

- Scaffolded: `.actor/` (actor.json, input/output/dataset schemas, PPE,
  Dockerfile), `src/models.py` fully implemented (`ActorInput` + `ResultRow`
  per spec, search\_urls-or-region-triple validator, RESIDENTIAL-proxy
  rejection), placeholder `src/main.py` (boots, validates input, pushes one
  `{"placeholder": true, ...}` row, exits — no real crawler), tests, README
  skeleton.
- Real implementation (`src/resolver.py`, `src/client.py`,
  `src/paginator.py`, `src/parser.py`) is pending — RSC `gis` extraction,
  price-bucket bisection past the 350-home cap, and the two-signal
  geo-splash guard are all live-confirmed wire format per
  `docs/specs/redfin-property-listings-scraper/spec.md` ("Assumptions") but
  not yet wired up; see `docs/specs/redfin-property-listings-scraper/tasks.md`
  T02–T12.
