# Changelog of Expedia Hotels Scraper & Price Tracker (`myth84/expedia-hotels-scraper`) Actor

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

### Changelog

History of `expedia-hotels-scraper` (folder `actors/expedia-hotels`). Versions are the Apify build numbers.

#### 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 Expedia displays, taxes and fees included).
- **`taxesIncluded`** is read from the card's own tax line (`parseCardPrices` › `taxesIncludedFromText`, the first `LodgingEnrichedMessage` that mentions taxes): `true` for "Total with taxes and fees" or "taxes et frais compris" (every card of the committed captures, USD and EUR), `false` only for a line that says the taxes are left out ("before taxes", "+ … taxes & fees", "hors taxes", "non compris"), `null` without a tax line or without a price. Before, the README said `priceTotal` "always" includes them; it now says what the card says.
- **`priceTotalAllIn`** = `priceTotal` when `taxesIncluded` is `true`, `null` otherwise: Expedia does not itemise the taxes, so they are never estimated (`allInPriceTotal` of `@scraper-studio/core/schema`).
- `__traces__/default-output.json` and `__traces__/rows-by-fixture.json` re-recorded: only `schemaVersion` and the two new columns of the rows differ (checked by a diff of the files, 135 and 48 rows).
- Dataset schema: both columns documented, `priceTotal` no longer called "the column to compare" (that is `priceTotalAllIn`); *Overview* shows `priceTotalAllIn` next to `priceTotal`, *Prices* shows both after `priceBeforeDiscount`. README: `priceTotal` / `priceTotalAllIn` bullet, FAQ on taxes, sample output with `priceTotalAllIn` (instead of `propertyType`), 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 *Destinations*, 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 destination*, 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.
- `src/__traces__/default-output.json` re-recorded for `inputs/daily-check.json` only: a `locations` run adds its per-search line ("16 properties written") and `searches` statistics; rows and charges unchanged.
- **Form behaviour to know**: `locations` is always searched, also with `startUrls` (unchanged), so the prefilled Lisbon is searched too unless the list is cleared; its description says so. Before, the prefilled `location` was ignored in that case.
- **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 (price monitoring, Price Tracker title)

- **Price monitoring** (brief `docs/cloud-tasks/2026-10-21_expedia-hotels-extend-monitor.md`, 2026-10-21, model: the Booking Actor, same option and column names): `monitorPrices` (off by default), `monitorStoreName` (default `expedia-price-monitor`, lower-cased, letters, digits and dashes) and `onlyChanges` (needs `monitorPrices`). The previous prices live in the customer's named key-value store, one record per search (`expedia-monitor/1`, key `search-<destination>-<currency>-<hash>`): the identity is the destination text (form or pasted URL, with the URL's `regionId`), the stay (fixed dates, or `relative:<N>:<M>` for check-in in N days for M nights, so that a schedule compares a constant lead time; a URL with its own dates is a fixed stay), the adults, the sort, the currency and the point of sale (site, site id, partner id, locale); the page size is left out. Each row of a monitoring run carries `changeType` (`new` / `price_changed` / `unchanged`) and, on price changes, `previousPrice`, `priceDelta`, `priceDeltaPercent`. **Compared price**: `priceTotal` (taxes and fees included), or `pricePerNight` when the card shows no total; the record keeps which one (`priceBasis`) and a total is never compared with a nightly price, nor two prices in different currencies (reported `unchanged`, the new price becomes the reference). `onlyChanges` writes and charges only the new properties and the price changes; in monitoring mode `maxResults` caps the properties compared (per destination with `locations`). Records are written only after the crawl, for the searches that ended cleanly (no abandoned page, no invalid row, no charge cut: fail-closed); properties unseen for 90 days are dropped; a record of another format is ignored with a warning (new reference); a store that cannot be read fails the run before its first request (`MonitorStoreError`, nothing charged), a record that cannot be written fails the run with the reason after the rows are saved. A monitoring run that compared properties and found no change succeeds with an empty dataset. `RUN_STATS.monitor` (searches, baseline, compared, new, price changes, unchanged, written, records written / kept) and `monitorBaseline`, `extra.monitor*` counters (0 when off), end-of-run line with the monitoring summary.
- **Default output unchanged**: `src/default-output.trace.test.ts` replays the committed inputs and six representative inputs (default `{}`, EUR, price sort, three cities with `locations`, search URLs with and without dates) on the fixtures, wired as `main.ts` wires them, against `__traces__/default-output.json` recorded with the previous code before the change: same rows, charges, log lines and statistics (the new monitoring counters aside, checked off).
- **Output**: dataset view *Changes* (change, name, dates, total, previous total, difference, %, currency, rating, destination, link); column descriptions of `changeType`, `previousPrice`, `priceDelta`, `priceDeltaPercent` (no longer "always null").
- **Store texts on the "expedia price tracker" niche** (13 results, top 10 at 4 users at most, `docs/research/2026-10-21_recherches-sans-reponse.md`): title and README line 1 **Expedia Hotels Scraper & Price Tracker**; description, SEO title and description with "Expedia price tracker", "hotel price tracking" and price drops; README opening with "Expedia hotel price monitor" and "hotel price drop alerts" (added to `store.json › searchSynonyms`, all within the indexed head). README: *Price monitoring* section (scheduled input, a `price_changed` row produced by the code, the compared-price rule, fixed vs relative dates, fail-closed, one record per search), input table rows, cost and empty-run notes, FAQ *How often should I run it, and how do I get only price changes?* (replaces *How fresh is the data…*, FAQ kept at 10), switching table row, MCP bullet.
- **Example tasks** (10, the platform limit): *Track Expedia hotel prices in New York daily* and *Spot hotel price drops in Paris on Expedia* (`monitorPrices`, relative stays, *Changes* view, `onlyChanges` shown in the form); *Top-rated hotels in Rome* is retired (its input ran Expedia's recommended order, not a rating sort; Rome stays in the three-city task).

#### 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` (most fields already did since the agent-ready rewrite).
- **Descriptions rewritten**: `maxResults` (the prefill of the form), `startUrls` (the reason of its empty default: search by destination).
- **Bounds and patterns**: none needed (every number already had `minimum` and `maximum`, `startUrls` items already had their http(s) pattern).
- **`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` is unchanged (it already parses to the same input).
- **Store texts**: `store.json › searchSynonyms` lists "Expedia scraper", "Expedia hotel prices", "hotel price tracking", "track hotel prices" and "rate shopping", all in the README head the Store search indexes (title, description, README line 1, SEO title and description already carried "Expedia hotels"). README input table: `maxResults` prefill.
- No code change: rows, logs and charges are identical.

#### Unreleased (several destinations, input written for AI agents)

- **Several destinations per run** (brief `docs/cloud-tasks/2026-10-20_travel-hotels-extend-agent-ready.md`, 2026-10-21, model: `airbnb-listings` 0.3): new field `locations` (`stringList`, empty by default) next to `location` (`location` alone = the behaviour of the previous versions; both = union without duplicates, same text up to case and spacing; empty lines dropped; at most 50 destinations, refused with a clear message beyond). One search per destination with the same dates, adults, point of sale, sort and page size, each with its own anonymous device id, then the search URLs; `location` is still left out when `startUrls` is filled (the form prefills it), the destinations of `locations` are always searched. With `locations`, `maxResults` caps each destination and each search URL (the runner's charger allows `maxResults` × searches, a full search stops without stopping the run); a property found by two destinations for the same stay is written and charged once, under the first one; a property cut by a destination's cap stays available to the next ones; `searchLocation` (and so `city` / `country`) is the destination that found the property; one log line and one `RUN_STATS.searches` entry per search; a warning when `maxRequests` is likely to stop the run before its last destinations. Without `locations`, `maxResults` keeps capping the whole run: the charger's run-level stop is reproduced by `stopAtRunCap`, and the rows, logs and charges of every committed input and example task are those of the previous version (test replaying each of them at three `maxResults` on the fixtures, before / after).
- **`location` prefilled, no longer a schema default**: Apify adds the schema defaults to every input, which would have searched Lisbon in runs made of `locations`. The code applies "Lisbon, Portugal" only when the input names no destination at all (`{}`, a cleared field: unchanged).
- **Input form written as the documentation of a tool**: every description under 500 characters (one sentence on the field, the format with an example, the default and why: *Default 100: enough to compare a city, about $0.24 on the Free plan*, figure checked against the pricing file in force), no internal detail (`startUrls` shortened from 911 characters); `sortBy` in the order of the most requested (recommended, cheapest, guest rating, highest price, class, distance). Store tests: no required field, no default on a required field, every default named in its description, description length, `locations` documented with `maxResults` per destination.
- **README**: `locations` row of the input table, three-city input example, *For AI agents (MCP)* minimal input with `locations`, FAQ *Can I search several cities in one run?* (the support question moved to *Integrations and API*, FAQ kept at 10), pricing line per destination, `searchLocation` documented. Example task *Compare hotel prices in Paris, Rome and Barcelona* (`locations`, 20 hotels per city; 9 tasks in all).

#### Unreleased (star rating, property type, price meaning)

- **Repair** (brief `docs/cloud-tasks/2026-10-20_expedia-hotels-repair-star-rating.md`, 2026-10-21), after the agent test bench of 2026-10-20 (New York, USD: "starRating was null for all results"). Diagnosis on the committed fixtures: expedia.com sends `headingSection.productRating: null` on every card, including the 8 Lisbon hotels that expedia.fr rates 3 to 5 in the same search, so the class is absent from the expedia.com response, not missed by the code. No query change (a new field risks a 400 `GRAPHQL_VALIDATION_FAILED`).
- `starRating` is now read from every form: `productRating.rating` as a number or a numeric text (a text no longer drops the card at validation), its `accessibility` wording ("Catégorie d’hébergement : 4", "4.0 out of 5", "4 stars"), then a star badge ("5-star hotel", "Hôtel 4 étoiles"); half steps kept; guest scores ("9.4 out of 10"), place names ("Hotel Floride Etoile") and discounts never count. Rows with `starRating`: 8 of 8 on the expedia.fr capture, 0 on the five expedia.com captures, equal to the cards that carry the class (test per fixture).
- `propertyType` takes the type the property name states, with the labels of the Booking Actor (`Hostel`, `Guest house`, `Apartment`, `Aparthotel`, `Bed and breakfast`, `Motel`, `Ryokan`, `Riad`, `Boat`, `Campsite`), otherwise `Hotel`, or `null` on Expedia's alternate scale when the name says nothing; `roomType` is `hotel_room` for hotel-like types only, `unknown` for hostels, apartments and boats. On the 48 fixture rows, 8 change: 3 guest houses (`Guest house`, still `hotel_room`), 2 hostels (`Hostel` / `unknown`, were `Hotel` / `hotel_room`) and the houseboat in its 3 captures (`Boat` / `unknown`; it was `null` on expedia.fr and `Hotel` / `hotel_room` on expedia.com); `starRating` is unchanged on every fixture row and the 40 other rows are identical.
- README, FAQ and column descriptions: `priceTotal` includes taxes and fees on both sites; `pricePerNight` is the nightly rate **before** taxes and fees on expedia.com ("$174 nightly" for "$582 total" over 3 nights) and the average night **with** them on expedia.fr ("173 € par nuit" for 518 €); `starRating` is filled with `EUR` (expedia.fr) and `null` with `USD`. The FAQ on exports (covered by *Integrations and API*) gives way to one on `starRating`; the price FAQ answers the taxes question.
- Non-regression: `src/__traces__/rows-by-fixture.json`, recorded with the code of 0.1 before the repair, holds every other column of the 48 fixture rows and is unchanged.

#### 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* ($2.40 per 1,000 properties; Apify's $5 monthly credit covers about 2,083), computed from `pricing/2026-10-20_pay-per-event.autopilot.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. Hotel price tracking and comparison (scheduled runs, same stay, same adults, same currency) are now named in the opening sentence, the README description, the Actor description and the SEO description (Store texts to send with the next update). 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

- **Search URLs in input** (brief `docs/cloud-tasks/2026-10-20_expedia-hotels-extend-urls.md`, 2026-10-20): new `startUrls` field (default `[]`, output unchanged when empty): Expedia `Hotel-Search?…` URLs pasted from the browser, one search per URL, converted offline to the variables of the `SearchResultsAndFilters` call (`destination` text plus `regionId` when present, `startDate` / `endDate` or `d1` / `d2` or `chkin` / `chkout`, `adults` or `rm1=a2`, `sort`, point of sale from the host: expedia.com → USD, expedia.fr → EUR, the advanced `pointOfSale` for its own site). What a URL leaves out comes from the form (dates, adults, sort); children, several rooms, unknown sorts, other sites and non-search pages fail the run with the URL number and the reason; filter and map parameters are ignored and listed in the start log. Deduplication is now per stay (the same hotel for two stays gives two rows). `location` is ignored when `startUrls` is filled; `maxResults` still caps the whole run. Tests: the URLs of the captures rebuild the exact `_meta` variables of every fixture; the default search and its rows are unchanged. **To verify at the first run**: `destination.regionId` next to `regionName` is not in the audit's captures (a 400 `GRAPHQL_VALIDATION_FAILED` would mean dropping it in `buildDestination`).
- **Output schema `travel-listing/1.2`** (additive, brief `docs/cloud-tasks/2026-10-20_travel-extend-monitor.md`, 2026-10-20): the four price-monitoring columns of the travel studio (`changeType`, `previousPrice`, `priceDelta`, `priceDeltaPercent`) are appended to every row, always `null` for this Actor (no monitoring mode). Dataset schema regenerated, README example and column table updated; no other change in the rows.
- **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.

#### 0.1 — 2026-10-20

- First version, written offline on the committed fixtures of the audit (`docs/audits/expedia.md`, brief `docs/cloud-tasks/2026-10-20_expedia-build.md`): Expedia internal GraphQL search API (`POST /graphql`, operation `SearchResultsAndFilters`, query text sent as is, no cookie, no token, mandatory `client-info` header exposed as the `clientInfo` input), 50 or 100 results per request, `resultsStartingIndex` pagination until the announced total / `maxResults` / two pages without a new property, global deduplication by property id, pay-per-event `result` charging, `RUN_STATS`, explicit failure on empty output.
- Anti-bot recipe of the audit: `impit` transport with the iOS 18 Safari fingerprint, Apify **residential** proxies (US) by default and a **new proxy session for every attempt**; a refused attempt (429, blocking body, non-JSON body, response without data) is retried up to 5 times with a pause of 1, 2, 4, 8, 8 s, then the page is abandoned; a 400 `GRAPHQL_VALIDATION_FAILED` (schema change) or a refused `client-info` header fails the run at once with the fix in the log.
- Query = the final capture of the audit plus the `dynamicMap` block (coordinates by card id, validated together by probe run 6) minus `guestRatingSectionV2` (legacy field, always null, refused by the privacy filter); never any review or guest field.
- Output schema `travel-listing/1.1` with `ratingMax: 10`; `priceTotal` and `pricePerNight` are the displayed prices, taxes and fees included (`taxesAndFees` null); `priceBeforeDiscount` the strike-through total; `region` the neighbourhood of the card; `city` / `country` / `countryCode` derived from the destination text (the card carries no address); `starRating` from the card's product rating when shown (expedia.fr), `propertyType` "Hotel" unless Expedia uses its alternate (non-hotel) rating scale; no personal data (privacy filter on every row).
- Points of sale: `currency` USD → expedia.com (en_US), EUR → expedia.fr (fr_FR); advanced `pointOfSale` object (domain, siteId, tpid, locale, currency) for other sites, not verified.
- Dates fixed (`checkIn` / `checkOut`) or relative (`checkInDaysFromNow`, `nights`) so that schedules and example tasks never expire; sort `RECOMMENDED` and `PRICE_LOW_TO_HIGH` validated, the other orders marked unverified; filters out of scope.
- Store readiness: README, input schema, dataset schema with Overview / Prices / Location views, output schema, 8 example tasks with relative dates, consistency tests (README ⇔ schemas ⇔ code, Ajv replay of fixture rows), crawler tests on a fake HTTP client.
