Agoda hotels scraper and Agoda price tracker: search results for any city and dates — nightly and total price with and without taxes, guest rating, reviews, stars, photos. Schedule it for hotel price tracking: price drops and rises flagged between runs. One flat row per property, no personal data.
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 priceViewinclusive (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.
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).
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 taskagoda-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.