Expedia hotels scraper and Expedia price tracker: search results for any destination and dates — nightly and total stay price, guest rating, reviews, class, location, photos. Schedule it for hotel price tracking: price drops and rises flagged between runs. One flat row per hotel, no personal data.
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.
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).
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).
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.