# Changelog of Airbnb Listings Scraper & Price Tracker (`myth84/airbnb-listings-scraper`) Actor

- **URL**: https://apify.com/myth84/airbnb-listings-scraper/changelog.md
- **Full Actor documentation**: https://apify.com/myth84/airbnb-listings-scraper.md

### Changelog

History of `airbnb-listings-scraper` (Apify id `FEyfCaGZt8QhTDD76`, folder `actors/airbnb-listings`). Versions are the Apify build numbers; entries before this file existed were reconstructed from the project journal on 2026-10-19.

#### Unreleased (all-in price: `taxesIncluded` and `priceTotalAllIn`)

- Brief `docs/cloud-tasks/2026-10-23_travel-prices-all-in.md` (2026-10-23): rows are **`travel-listing/1.5`**, two columns after `taxesAndFees`, nothing else changes (`priceTotal` keeps its meaning: the total Airbnb displays, after discounts).
- **`taxesIncluded`** from Airbnb's own price style (`parsePrice` › `taxesIncludedOf`): `true` for `REGULATED_TOTAL` (European sites, "au total": in the committed airbnb.fr captures the "Total" line is the sum of the lines above it, the "Taxes" line included, tested card by card), `false` for `TOTAL_ONLY` (airbnb.com, "for 3 nights": every card whose total Airbnb speaks says "total before taxes"), `null` for any other style, for a breakdown that contradicts the style (a taxes line under `TOTAL_ONLY`, "before taxes" under `REGULATED_TOTAL`), or without a price.
- **`priceTotalAllIn`** = `priceTotal` when `taxesIncluded` is `true`, `null` otherwise (`allInPriceTotal` of `@scraper-studio/core/schema`): Airbnb's taxes and service-fee lines are parts of the total, never an amount left out, so nothing is added and the taxes of an airbnb.com total are never estimated. With the default `domain` (`www.airbnb.com`), `priceTotalAllIn` is therefore `null`; a European `domain` fills it.
- The three `__traces__/*.json` re-recorded: only the version tag and the two new columns of the rows differ (checked by a diff of the files).
- Dataset schema: both columns documented; *Overview* shows `priceTotalAllIn` next to `priceTotal`, *Prices* shows both after `taxesAndFees`. README: *Which price to compare* paragraph (with the `domain` advice), sample output with `taxesIncluded` (instead of `roomType`), the price notes, the strengths worth knowing when switching (hard `maxResults` cap never charged beyond, `onlyChanges`), *More scrapers* pointing at `priceTotalAllIn`.

#### Unreleased (list-first input for AI agents)

- Brief `docs/cloud-tasks/2026-10-23_travel-hotels-locations-first.md` (2026-10-23), after Apify's post on Actors dropped by agents for a string primary input ("Make your primary input an array"): **`locations` is the first field of the form**, titled *Locations*, prefilled with `["Lisbon, Portugal"]` (default still `[]`), its description starting with "One or more …".
- **`location` kept** (example tasks, daily checks and API callers use it), titled *Single location*, moved below `startUrls`, without prefill; description "Single destination (kept for compatibility; prefer `locations`)…". No field is both required and with a default; every description stays under 500 characters.
- **`inputs/daily-check.json` = the new prefilled form**: `locations` instead of `location`, as `runInputQualityChecks` requires (the daily check runs what Apify's daily test runs). Same search, same rows and charges; the daily-check task is updated at the next release.
- Input form texts: the form description starts with "Enter one or more locations"; `maxResults` refers to the new title *Locations*.
- **README**: the *Input* section (introduction, example, table) shows `locations` first; nothing else changes.
- Tests: the prefilled form is `locations` alone, searched once, non-empty within `maxResults` on the fixtures and with the rows of the former prefilled form (`location` alone). No code change: `location`, `locations` or both are accepted as before.

#### Unreleased (quality score: input documented for AI agents)

- Brief `docs/cloud-tasks/2026-10-20_studio-quality-score-insights.md` (2026-10-21): the input form passes the shared checks of `runInputQualityChecks` and the Store texts those of `runSearchTextChecks`.
- **Descriptions rewritten** (what the field does, an example of the format, the default and its reason): `locations` (shortened under 500 characters), `startUrls` (example URL; which URL filters are kept and which fields replace them, as `search.ts` does), `checkIn`, `checkOut`, `adults`, `children`, `infants`, `pets`, `maxResults`, `priceMin`, `priceMax`, `roomTypes`, `currency`, `locale`, `domain`, `monitorPrices`, `monitorStoreName`, `onlyChanges`, `proxyConfiguration`, `splitSearchArea`, `includeMapPins`, `maxRequests`, `delayMs`, `debugStoreHtml`.
- **Bounds**: `maximum` 1,000,000,000 on `priceMin` and `priceMax` (the code accepts any non-negative whole number; no realistic nightly price reaches the bound, even in VND or IDR).
- **`maxResults` prefilled with 20** (default unchanged, 100): the prefilled input that Apify's daily test runs is now exactly the input of our daily check.
- **`inputs/daily-check.json`** reduced to `{"location": "Lisbon, Portugal", "maxResults": 20}`: the keys equal to the defaults are dropped (`adults`, `locale`, `domain`, `delayMs`, `proxyConfiguration`), `currency` goes from EUR to the default USD and `splitSearchArea` from false to the default true (20 listings never saturate the 15 pages that trigger a split). The live daily-check task follows when the pilot re-sends it.
- **Store texts**: `store.json › searchSynonyms` lists "Airbnb scraper", "Airbnb price tracker", "vacation rental" and "short-term rental"; the opening paragraph of the README says "vacation rental and short-term rental data" (was hyphenated) so that both phrases are in the head the Store search indexes. README input table: `startUrls`, `maxResults` (prefill), `priceMin`, `priceMax`.
- No code change: rows, logs and charges are identical.

#### Unreleased (stay dates and comparable prices)

- **Relative stay** (brief `docs/cloud-tasks/2026-10-20_airbnb-listings-extend-stay-dates.md`, 2026-10-20, findings of the agent panel of 2026-10-20, model: the Booking Actor): new fields `checkInDaysFromNow` (0–365) and `nights` (1–90), both empty by default, used when `checkIn` / `checkOut` are empty: check-in that many days after the UTC day of the run, for that many nights (one of the two alone: check-in in 30 days, or 3 nights). Resolved once when the input is read; every destination and start URL of the run searches that stay (start-URL dates are replaced, as with fixed dates). Fixed dates win over the relative fields (the start log says they were ignored).
- **Without any date the search is unchanged** (flexible one-week stays, same tasks, URLs and rows apart from the price fix below), and the start log now warns that each listing is priced for its own proposed stay, so the prices are not comparable; start URLs that carry their own dates are not concerned. The start log shows the stay: fixed, relative (with its definition) or flexible.
- **`checkIn` / `checkOut` / `nights` of every row are the stay really priced**: the listing's own dates (flexible search), else the dates of the request Airbnb answered (`rawParams` of the embedded `StaysSearch` request, new `parse.ts` › `pageStayOf`), else the run's. Rows of a start URL with its own dates and no input dates now carry those dates (they were empty).
- **Price monitoring with a relative stay**: the record identity holds `stay: "relative:<days>:<nights>"` instead of the dates of the day, so a daily schedule keeps one record and compares the total of the stay at a constant lead time (same number of nights, the dates move). The identity, the record key and the comparison of fixed and flexible searches are those of version 0.3 (keys checked against the previous code).
- **`pricePerNight` is now the nightly price after discounts**, before taxes and itemised fees, on every card shape: when Airbnb shows the base nightly rate followed by discount lines (special offer, early-bird, long stay), the discount lines are spread over the nights (e.g. 84.00 € → 71.20 €); when Airbnb already shows the discounted nightly rate, it is unchanged. So `priceTotal` = `pricePerNight` × `nights` + `taxesAndFees` + `cleaningFee` to display rounding (checked on every committed fixture: gap < 1). It previously mixed the two meanings (13 of 118 fixture rows change; `priceTotal`, the price compared by monitoring, is unchanged).
- **`priceBeforeDiscount`** is also filled when Airbnb shows the discount lines without a strike-through price: `priceTotal` + the discount lines (1 fixture row: 704 €).
- **`bedrooms`**: a card that lists beds without a bedroom line now gives 1 for a private room and 0 for an entire studio / loft (6 fixture rows); shared and hotel rooms, and every card of a flexible search (Airbnb shows no bed line there), stay `null`.
- Replayed traces re-recorded: only `pricePerNight` of the 3 listings with a discount line and one `priceBeforeDiscount` changed; URLs, statistics and log lines are identical. Dataset field descriptions, input schema, README (input table, *What the prices mean*, flexible dates FAQ, price monitoring) updated; new tests `src/stay.test.ts`.

#### Unreleased (README visibility)

- **README first screen and links** (brief `docs/cloud-tasks/2026-10-20_studio-readme-visibility.md`, 2026-10-20). The Store search indexes only the first ~3,000 README characters, so they now hold: an opening sentence with the phrase agents search and its synonyms, a "No login, no cookies, no personal data" line, the Free-plan price in figures in *How much* ($3.00 per 1,000 listings; Apify's $5 monthly credit covers about 1,666), computed from `pricing/2026-10-19_pay-per-event.json` (Free tier of the primary event, one `apify-actor-start` event deducted), and a real output snippet (12 key columns of the README example row). Further down: *Use it from your AI assistant* (MCP URL `https://mcp.apify.com?tools=myth84/<name>` and a ready-to-copy prompt with a minimal input), *Alternatives and switching* (official API, home-made scraper, input-name mapping table, no competitor named), *More scrapers by Flatrow Data Studio* (Store links to the other Actors of the studio) with an invitation to rate the Actor (no counterpart), and the non-affiliation line at the very bottom. The cost section and the MCP cost bullet quote the same figures; the line 1 heading is unchanged. `store.json` now names its studio (`travel`), read by the link check. Checked by `runStoreChecks` (`readmeVisibility`): keyword in the opening prose, snippet equal to the example row and within 3,000 characters, figures equal to the pricing file in force (a new grid fails the test until the README follows), MCP block with a valid input, links to existing Actors of the same studio only, no `fpr=`. No code change: rows, logs and charges are identical.

#### Unreleased (several destinations, deployed first as a `beta` build)

- **Several destinations per run** (brief `docs/cloud-tasks/2026-10-20_airbnb-listings-extend-locations.md`, 2026-10-20): new field `locations` (list, empty by default) next to `location`; the two are merged without duplicates (same text up to case and spacing), empty lines are ignored, at most 50 destinations per run (clear error beyond). Each destination is a separate search with the same dates, guests, filters and currency; its rows carry it in `searchLocation`.
- With `locations`, **`maxResults` applies to each destination** (and to each start URL), like `maxResults` per search of the AliExpress Actor: a full destination stops without stopping the run, the charger allows `maxResults` × searches. Without `locations`, `maxResults` keeps capping the whole run (`location` and start URLs together) and the output, the charges and the log lines are unchanged (replayed traces, explicit tests).
- A listing found by two destinations is written and charged once, under the first destination of the list; the listings a full destination saw but did not take stay available to the next destinations. With price monitoring, each destination keeps its own record (distinct key) with the listings it compared, and `maxResults` is the number of listings compared per destination.
- Log: one line per destination at the end (listings, duplicates skipped, not searched when the run stopped before it, no listing found), a warning at start when `maxRequests` looks too low for the number of searches; `RUN_STATS.searches` (per destination) when `locations` is used.
- README: input table, three-city input example, FAQ entry *Can I search several cities in one run?* (the export question, already covered by *Integrations and API*, was removed to keep 10 questions); `searchLocation` description updated in the dataset schema; one more example task (`airbnb-paris-rome-barcelona-prices`), 10 in total (the platform maximum).

#### Unreleased (version 0.3, deployed first as a `beta` build)

- **Price monitoring** (brief `docs/cloud-tasks/2026-10-20_travel-extend-monitor.md`, 2026-10-20, model: the AliExpress Actor): `monitorPrices` (off by default), `monitorStoreName` (default `airbnb-price-monitor`) and `onlyChanges`; previous total prices kept in the customer's named key-value store, one record per search (location or URL, site, currency, language, dates, guests, filters; the quadrants of a split area share the record of their search); `changeType` `new` / `price_changed` / `unchanged` with `previousPrice`, `priceDelta`, `priceDeltaPercent` on every row; `onlyChanges` writes and charges only the new listings and the price changes; first run = reference (`RUN_STATS.monitorBaseline`), `RUN_STATS.monitor` summary and `extra.monitor*` counters; records written only after a run whose searches ended cleanly (fail-closed); a monitoring run without any change succeeds with an empty dataset; in monitoring mode `maxResults` is the number of listings compared. A price is compared only for the same currency and the same stay dates (flexible searches: listings whose proposed week moved are `unchanged`).
- **Output schema `travel-listing/1.2`** (additive): `changeType`, `previousPrice`, `priceDelta`, `priceDeltaPercent` appended to every row (null outside monitoring runs); new dataset view *Changes*. Without the new options the rows are identical to 0.2 apart from the new columns (replayed traces).
- README: section *Price monitoring*, input table, FAQ entry; one more example task (`airbnb-lisbon-price-monitor`), 9 in total.

#### Unreleased (version 0.2, deployed first as a `beta` build)

- **Store texts** (brief `docs/cloud-tasks/2026-10-20_studio-promote-store-texts.md`, 2026-10-20): README opens with what you get / for whom / how much, shows the real input of the validation run, a pricing table that points to the Pricing tab (no figure in the text), a *For AI agents (MCP)* section with a minimal input, the key output fields, the cost per 1,000 results and the typical time; FAQ kept to 8–10 questions; no proxy or anti-bot detail left in the texts visible on the Store. Tests on the Store lengths (`seoTitle` ≤ 60, `seoDescription` ≤ 155, `description` ≤ 300), the README structure and the forbidden mentions. No code change.
- Internal: the crawl runs on the studio's generic paged-search crawler and Actor runner (`@scraper-studio/core/crawl`, `/actor-runner`); platform code is reduced to Airbnb hooks (search URL, parsing, cursors, quadrant splitting). Requests, rows and log lines are unchanged (replayed traces).
- `RUN_STATS`: `listingsSeen` / `listingsPushed` / `splits` become `itemsSeen` / `itemsPushed` / `extra.splits`; new `bytesTransferred`, `bytesTransferredEstimates`; `bytesSource` can be `gzip-estimate` (wire bytes estimated by gzip when HTTP/2 gives no Content-Length), so the estimated cost per 1,000 is now close to the real transfer instead of the decompressed size.
- No pause after the last page of a search (saves `delayMs` per search).
- Tests: crawler covered end to end on committed fixtures (pagination, caps, deduplication, splitting, rotation).
- Dataset schema now generated from the unified `travel-listing` schema and `.actor/dataset-fields.json` / `.actor/dataset-views.json` (`scripts/gen-dataset-schema.mjs`); generated JSON unchanged.
- Store readiness tests moved to the shared `@scraper-studio/core/testing` checks (same coverage); this changelog referenced from `actor.json`.
- `defaultMemoryMbytes: 256` in `actor.json` (the Actor card already runs at 256 MB since 2026-10-19): applied at the next `apify push`.

#### 0.1.6 — 2026-10-19

- Actor output schema added (`.actor/output_schema.json`: `listings`, `runStats`), required by the Store to publish.
- Published on the Apify Store (`https://apify.com/myth84/airbnb-listings-scraper`), category Travel, pay-per-event pricing; KYC validated the same day, listed in the default Store search.
- Daily control run in place: task `airbnb-listings-daily-check` (Lisbon, flexible dates, 20 listings, datacenter proxies, 256 MB, 300 s), schedule every day at 06:15 Europe/Paris, e-mail alert through `apify/send-mail` on failed, timed-out or aborted runs.

#### 0.1.5 — 2026-10-19

- Pay-per-event counter fixed: `ResultCharger` read the SDK's total event count (which includes the free dataset event); it now reads the count of its own `result` event. Pricing guard `assertPricingConsistent('result')` at start-up.
- Pay-per-event pricing set through the API: `result` 0.003 / 0.0015 / 0.0012 / 0.001 $ (FREE / BRONZE / SILVER / GOLD; PLATINUM and DIAMOND aligned on GOLD by Apify), `apify-actor-start` 0.00005 $, minimum budget 0.05 $ per run. Billing trial: 20 listings, 20 `result` events charged.
- Store metadata set through the API: category TRAVEL, SEO title and description.

#### 0.1.3 — 2026-10-19

- Scale control run: 400 unique listings in 25 requests and 79 s, area splitting triggered after saturation (15 pages), 0 blocked, 0.0021 $ (≈ 0.005 $ per 1,000 listings).

#### 0.1.1 – 0.1.2 — 2026-10-19

- First deployment as `airbnb-listings-scraper`; validation run: 60 listings in 4 datacenter requests, 0.0008 $, no row rejected by the dataset schema. A first run had failed on forbidden characters in key-value store keys (debug mode): fixed.
- Hotel cards (property and room type) handled; meaning of `pricePerNight` documented (base nightly rate before discounts, fees and taxes).

#### 0.1 — 2026-10-19

- First version, written offline on committed fixtures: embedded `StaysSearch` state parsing, normalisation to `travel-listing/1.1` (`listedBy` added: EU trader status, never a person), one sticky session per search, cursor pagination, global deduplication, map-area splitting, map pins, pay-per-event `result` charging, `RUN_STATS`, explicit failure on empty output.
- Store readiness delivered by cloud task n°1 (PR #1): README, input schema, dataset schema with Overview / Prices / Location views, consistency tests (README ⇔ schemas ⇔ code, Ajv replay of fixture rows).
