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

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

### Changelog

History of `agoda-hotels-scraper` (folder `actors/agoda-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 following `priceView`).
- **`taxesIncluded`** = `true` with `priceView` `inclusive` (the default), `false` with `exclusive`, `null` when the property has no price.
- **`priceTotalAllIn`** = `priceTotal` (inclusive) or `priceTotal` + `taxesAndFees` (exclusive), `null` without a price (`allInPriceTotal` of `@scraper-studio/core/schema`): the inclusive total in both views (within a cent of rounding, tested on every committed result).
- `__traces__/default-output.json` re-recorded: only `schemaVersion` and the two new columns of the rows differ from the 2026-10-22 recording (checked by a diff of the two files, 154 rows).
- Dataset schema: both columns documented; *Overview* shows `priceTotalAllIn` next to `priceTotal`, *Prices* shows both after `priceBeforeDiscount`. README: *Which price to compare* paragraph, 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 *Cities*, prefilled with `["Lisbon"]` (default still `[]`), its description starting with "One or more …".
- **`location` kept** (example tasks, daily checks and API callers use it), titled *Single city*, moved below `cityId` and `searchUrls`, 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 a `cityId` or `searchUrls` (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-22_agoda-hotels-extend-monitor.md`, 2026-10-22, model: the Expedia and Booking Actors, same option and column names): `monitorPrices` (off by default), `monitorStoreName` (default `agoda-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 (`agoda-monitor/1`, key `search-<place>-<currency>-<hash>`, or `search-city-<id>-…` for a city id): the identity is the destination (the place text of the form, up to case and spacing, since the record is read before Agoda's suggestion service resolves it; or the city id of a `cityId` search or a search URL), 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 search URL with its own dates is a fixed stay), the adults and the children's ages, the sort, the currency, the locale and `priceView`; 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` (in the price view of the search), or `pricePerNight` when the property has 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). A property seen without a price (`isAvailable` false: sold out, or price not ready) is reported `unchanged` and keeps its last recorded price. `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 (place resolved, 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. The monitoring state is kept per task, not per label: a `cityId` search and a search URL of the same city share the label `city <id>`. 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; the "properties without a price" warning is computed over the properties compared.
- **Default output unchanged**: `src/default-output.trace.test.ts` replays the committed inputs and eight representative inputs (default `{}`, Lisbon cheapest, EUR in French, a family with prices before taxes, three cities with `locations`, a `cityId`, fixed dates in Agoda's ranking, 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 "agoda price tracker" niche** (11 results, 18 users at most in the top 10, Store API on 2026-10-22): title and README line 1 **Agoda Hotels Scraper & Price Tracker**; description, SEO title and description with "Agoda price tracker", "hotel price tracking" and price drops; README opening with "Agoda hotel price monitor" and "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, properties without a price, 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; the *More scrapers* link to Expedia carries its current title.
- **Example tasks** (10, the platform limit): *Track Agoda hotel prices in Tokyo daily* and *Spot hotel price drops in Bangkok on Agoda* (`monitorPrices`, relative stays, *Changes* view, `onlyChanges` shown in the form); *Hotels in Tokyo for a 3-night stay* is retired (same sort, currency and stay as *Cheapest hotels in Lisbon*; Tokyo stays covered by the tracker).

#### 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**: `cityId` (positive integer, ignored when `searchUrls` is given), `searchUrls` (one per line, http(s) with a `city` parameter, a URL that is not an Agoda city search stops the run), `childAges` (sent as an Agoda family search with the children in the occupancy, instead of an unverified claim about which properties are kept), `maxResults` (the prefill of the form).
- **Bounds and patterns**: `cityId` gets `maximum` 9007199254740991, the bound of the code (`z.int()`, safe integers); `searchUrls` items get the pattern `^\s*[Hh][Tt][Tt][Pp][Ss]?:` (an http or https URL, surrounding spaces and upper-case scheme accepted as the code does).
- **`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**: `seoDescription` carries the search phrase "Agoda hotels"; `store.json › searchSynonyms` lists "Agoda scraper", "Agoda hotel prices", "hotel price tracking", "hotel price comparison" and "rate shopping", all in the README head the Store search indexes. README input table: `maxResults` prefill and `searchUrls` format.
- **Example task** `agoda-lisbon-hotels-agoda-ranking`: SEO title rewritten as the long-tail search it targets ("Agoda hotels in Lisbon by Agoda ranking – prices & ratings"); published with `scripts/publish-tasks.mjs` at the publication of the Actor.
- 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 with `location` or `cityId`, refused with a clear message beyond). The precedence of the previous versions is kept for the single destination (search URLs, then `cityId`, then `location`); the cities of `locations` are always searched, by name, each with its own identity, after the `cityId` or `location` search and before the search URLs. 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 is written and charged once, under the first one; a property cut by a destination's cap stays available to the next ones; `searchLocation` is the city that found the property (as resolved by Agoda); 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 labelled a run made of a `cityId` with "Lisbon" (`searchLocation`) and would have searched Lisbon in runs made of `locations`. The code applies "Lisbon" only when the input names no destination at all (`{}`, a cleared field: unchanged); a `cityId` alone is now labelled `city <id>`, like a search URL.
- **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; `locale` becomes a select with suggested locales en-us, en-gb, fr-fr, de-de, es-es… (any locale still accepted, default en-us; unverified ones titled so); `sortBy` and `currency` already list the most requested values first. 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 export 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 properties per city; 9 tasks in all).

#### Unreleased (README visibility)

- **README first screen and links** (brief `docs/cloud-tasks/2026-10-20_studio-readme-visibility-new-actors.md`, 2026-10-21), same layout as the public Actors (PR #50). The Store search indexes only the first ~3,000 README characters, so they now hold: an opening sentence with the phrase agents search ("Agoda hotels scraper", "Agoda scraper", rate shopping, hotel price tracking with scheduled runs, hotel price comparison), 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/agoda-hotels-scraper` 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 travel Actors already public) 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. Checked by `runStoreChecks` (`readmeVisibility`). No code change: rows, logs and charges are identical.

#### Unreleased

- **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/agoda.md`, brief `docs/cloud-tasks/2026-10-20_agoda-build.md`): Agoda internal GraphQL search API (`POST /graphql/search`, operation `citySearch`, query text of the audit's final capture sent as is, public `ag-*` / `cr-*` headers, no cookie, no token), 45 or 100 properties per request (100 by default, the server maximum), `pageNumber` 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.
- Place resolution: a city name goes through Agoda's suggestion API (first city entry, else the city of the first matching area), a `cityId` or Agoda search URLs (`searchUrls`, with their own dates and occupancy) are used directly; an unknown place skips the search with an explicit warning.
- Transport and proxies of the audit: got-scraping from Apify **datacenter** proxies by default (29/29 calls at the audit, no challenge), one sticky session per search; an unusable response (status ≠ 200 other than 400, non-JSON body, response without data) is retried up to 3 times on a new session with a pause of 1, 2, 4 s, then the page is abandoned; a 400 with `errors[]` (the server lists every invalid field) fails the run at once with the reasons in the log.
- Query = the audit's final selection with the GraphQL aliases that keep the response clear of keys the privacy filter refuses (`reviewSummary: reviews`, `listerType: hostType`, `listerLevel: hostLevel`) and without `displayName`; never any guest, reviewer or engagement field.
- Output schema `travel-listing/1.1` with `ratingMax: 10`; prices of the cheapest offer per night (`pricePerNight`) and for the stay (`priceTotal`), taxes and fees included by default or excluded with `priceView: "exclusive"`; `taxesAndFees` = difference on the stay; `priceBeforeDiscount` the strike-through price per night; `isAvailable` false (all prices null) when the property is sold out or its price was not ready; `starRating` from Agoda's stars (null when 0); `propertyType` from the accommodation type codes seen in the captures (hotel, hostel, guest house, apartment, serviced apartment); `city` / `region` / `country` / `countryCode` and coordinates from the property itself; rooms, beds and bathrooms for non-hotel rentals; badges (sponsored, character tag, insider deal, promotion, award, lister level); no personal data (privacy filter on every row).
- Sort `PRICE_ASC` by default (deterministic: Agoda sorts on the price before taxes), `REVIEW_SCORE_DESC` validated, `RANKING` (the site's default, variable order, deduplicated) offered, `STAR_RATING_DESC` and `DISTANCE_ASC` marked unverified; `USD` / `EUR` and `en-us` / `fr-fr` validated, other currencies and locales passed unverified; dates fixed or relative (`checkInDaysFromNow`, `nights`); children with ages (`childAges`) switch the search to a family search.
- 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.
