# Hotel Price Comparison & Deal Finder (`alpercsv/hotel-deal-arbitrage-finder`) Actor

Compare hotel prices from Booking.com, Expedia, Agoda, Trip.com & more in one run. Normalizes rooms, currencies and taxes, then ranks offers with a 0–100 Deal Score to reveal where a hotel is truly cheapest. Free — just paste the hotel's TripAdvisor link.

- **URL**: https://apify.com/alpercsv/hotel-deal-arbitrage-finder.md
- **Developed by:** [Alper](https://apify.com/alpercsv) (community)
- **Categories:** Travel, E-commerce, Automation
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $20.00 / 1,000 deal analysis reports

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Hotel Deal Arbitrage Finder — OTA Price Comparison & Deal Intelligence API

**Find where a hotel is actually cheapest — and whether the price is truly a deal.**

A hotel price comparison and **hotel rate arbitrage** engine, not another hotel scraper. Give it a hotel name and city; it finds the property, pulls real rates across booking sites (Booking.com, Expedia, Agoda, Trip.com and more), normalizes currency, taxes and room conditions, and ranks every offer with a transparent **0–100 Deal Score** — flagging suspiciously low prices on the way. Scan one hotel or a bulk list of 20, and turn it into a **hotel price monitoring** service with webhook/Telegram alerts.

```text
INPUT                              OUTPUT (real run, Aug 2026)
─────                              ──────
Charisma De Luxe Hotel             🔥 4 booking sites checked
Kusadasi · Aug 25 → 31             Agoda      €1,494   ← cheapest
3 adults                           Trip.com   €1,848
                                   Booking    €2,556
      RUN (~30 seconds)            ─────────────────────────────
                                   You save €1,062 (41.5%) vs Booking
                                   Deal Score · savings % · booking link
```

That 41.5% gap is not a typo — it's what the same room, same dates actually cost across sites when we ran this. Nobody spots these gaps manually. This Actor finds them automatically.

### What it does

- **Hotel price comparison across OTAs** — Booking.com, Expedia, Agoda, Trip.com rates via TripAdvisor's public rate-comparison data
- **Automatic hotel identification** — just name + city; the TripAdvisor page is resolved automatically (URL/key optional, but 100% reliable when given)
- **Bulk Hotel Arbitrage Scanner** — scan up to 20 hotels in one run, get a ranked table of the biggest OTA price gaps
- **Deal Score (0–100)** — documented, configurable scoring: price advantage vs median, offer quality (refundable/board/taxes), data confidence
- **Price anomaly detection** — offers far below the market median get flagged "verify before booking", never silently trusted
- **Hotel price monitoring** — schedule runs and get webhook or Telegram alerts when price/savings/score conditions hit
- **Honest normalization** — currency (live ECB rates), taxes, board basis, refundability; unknown values are labeled unknown, never guessed
- Fails gracefully: one source going down never kills the run

### Why it beats a plain hotel scraper

A hotel price scraper gives you a pile of numbers. This gives you decisions:

| Question | Answer in the output |
|---|---|
| Where is this hotel cheapest? | `cheapestOffer` |
| How much cheaper? | `deal.savingsAmount`, `deal.savingsPercent` |
| Is it *really* a good deal? | `deal.dealScore` + label |
| Which of my 20 hotels has the biggest OTA price gap? | bulk `ranking` table |
| Are compared offers actually equivalent? | `comparisonGroupId` — a non-refundable room-only rate never unfairly "beats" a refundable breakfast rate |
| Is the price suspiciously low? | `anomaly` / `possiblePricingError` flags |
| Cheapest vs best value? | both returned, with a human explanation of the trade-off |

### Real case study (live data, August 2026)

| Hotel (4 nights, 2 adults) | Cheapest | Most expensive | Gap |
|---|---:|---:|---:|
| Charisma De Luxe, Kusadasi (6 n) | **€1,494** Agoda | €2,556 Booking | **41.5%** |
| Hotel 54, Sakarya | **€228** Agoda | €320 Booking | **28.8%** |
| Hotel ON7, Sakarya | **€280** Trip.com | €416 Booking | **32.7%** |
| Ramada Thermal Sapanca | €648 (all sites) | €648 | 0% — perfect parity |

Same rooms, same dates. The engine found every gap — and correctly reported the Ramada as "no arbitrage here".

### Quick start (no API keys, no URLs)

```json
{
  "hotelName": "Charisma De Luxe Hotel",
  "destination": "Kusadasi, Turkey",
  "checkIn": "2026-08-25",
  "checkOut": "2026-08-31",
  "adults": 3,
  "currency": "EUR"
}
```

That's it — the Actor finds the hotel's TripAdvisor page itself and compares OTA rates. For guaranteed identification (recommended for production), add the hotel's TripAdvisor URL:

```json
{ "tripAdvisorUrl": "https://www.tripadvisor.com/Hotel_Review-g297972-d633631-Reviews-Charisma_De_Luxe_Hotel-Kusadasi.html" }
```

### Bulk Hotel Arbitrage Scanner

Scan a whole list and rank the deals — for travel agencies, deal sites and affiliates hunting underpriced hotel rates:

```json
{
  "destination": "Kusadasi, Turkey",
  "checkIn": "2026-08-25",
  "checkOut": "2026-08-31",
  "adults": 2,
  "minimumSavingsPercent": 15,
  "hotels": [
    { "hotelName": "Charisma De Luxe Hotel" },
    { "hotelName": "Hotel 54", "destination": "Sakarya, Turkey" },
    { "hotelName": "Ramada Resort Thermal Sapanca", "destination": "Sapanca, Turkey" }
  ]
}
```

Output: a ranked table (`recordType: "hotel-summary"` per hotel in the Dataset, full ranking in the Key-Value Store):

```text
##  Hotel                    Cheapest    Median   Saving  Score  Via
1  Charisma De Luxe Hotel   1494 EUR    1848     19.2%   87     Agoda.com
2  Hotel 54                 228 EUR     276      17.4%   56     Agoda.com
3  Ramada Thermal Sapanca   648 EUR     648      0%      16     Booking.com
```

Hotels below `minimumSavingsPercent` are flagged `belowSavingsThreshold`. Up to 20 hotels per run (cost control). For bulk scans, providing `tripAdvisorUrl`/`tripAdvisorHotelKey` per hotel (or a `rapidApiKey`) makes identification faster and fully reliable.

### Hotel price monitoring & alerts

Combine with [Apify Schedules](https://docs.apify.com/platform/schedules) (e.g. every 6–12 hours) and configure alerts:

```json
{
  "alerts": {
    "maxTotalPrice": 750,
    "minSavingsPercent": 20,
    "webhookUrl": "https://your-app.com/hooks/hotel-deals",
    "telegramBotToken": "123456:ABC...",
    "telegramChatId": "-100123456"
  }
}
```

When a run finds a matching deal, a JSON payload hits your webhook and/or a formatted message lands in Telegram — price drop tracking for hotels, without building anything. Conditions: `maxTotalPrice`, `minSavingsPercent`, `minDealScore` (any match triggers; with no conditions set, every successful run notifies).

### Use with AI agents (MCP)

This Actor is pay-per-event priced and fully usable from AI agents through the [Apify MCP server](https://docs.apify.com/platform/integrations/mcp) — connect Apify to Claude, Cursor or any MCP-capable agent and ask:

> "Find the cheapest booking site for Charisma De Luxe Hotel, Aug 25–31, 2 adults — and tell me if it's actually a good deal."

The agent calls the Actor with structured input and gets back the full deal report (cheapest source, savings, Deal Score, anomaly flags) as JSON.

### How hotels are identified

1. `tripAdvisorHotelKey` (e.g. `g297972-d633631`) — explicit, always wins
2. `tripAdvisorUrl` — the hotel's tripadvisor.com page URL; key extracted automatically
3. **Automatic by name + city** — via Xotelo's Search API when `rapidApiKey` is set (free tier at rapidapi.com, most reliable automatic option), otherwise via a single polite search-engine lookup of the hotel's public TripAdvisor page (keyless, best-effort — can be rate-limited under bulk load)

Automatic candidates are ranked with the same fuzzy matcher used everywhere else (Jaro-Winkler + token sets, handles "Charisma De Luxe" ≡ "Charisma Deluxe Kuşadası") and rejected below a 0.6 confidence threshold — the engine refuses to guess the wrong hotel.

### Price sources (honest status)

| Provider id | Source | Status | Requirements |
|---|---|---|---|
| `xotelo` | Xotelo — free public API exposing TripAdvisor's OTA rate comparison (Booking.com, Expedia, Agoda, Trip.com, …) | ✅ Working, verified live | none (hotel identification as above) |
| `google-hotels` | Google Hotels via SerpApi (documented, authorized commercial API) | ✅ Implemented, parser fixture-tested; requires your key | `serpApiKey` input (secret) or `SERPAPI_API_KEY` env var |
| `mock` | Deterministic demo offers | 🚫 Not registered in the production Actor; unit tests only | — |

No provider bypasses CAPTCHAs, logins, or anti-bot systems. Adding a new source is a one-file job (`src/providers/provider-template.ts` documents the rules).

**Note on Xotelo:** rates are average nightly base rates per OTA; room name, board and refundability are not exposed, so those offers carry honest `unknown` values and a conservative confidence score. Totals derived from nightly averages can differ from checkout totals — always verify on the booking page.

### Input

Required: `checkIn`, `checkOut` (ISO dates, checkout after checkin, max 30 nights, not in the past), plus either `hotelName` + `destination` (single mode) or a non-empty `hotels` list (bulk mode).

Optional (defaults in parentheses): `adults` (2), `children` (0), `childrenAges`, `rooms` (1), `currency` (EUR), `market`, `language` (en), `tripAdvisorUrl`, `tripAdvisorHotelKey`, `hotelUrls`, `hotels`, `alerts`, `rapidApiKey`, `serpApiKey`, `maximumPrice`, `minimumSavingsPercent`, `filters` (`refundableOnly`, `breakfastIncludedOnly`, `allInclusiveOnly`, `taxesIncludedOnly`, `providers`), `sortBy` (`dealScore` | `price` | `bestValue` | `savingsPercent`), `maxOffersPerProvider` (25), `enabledProviders`, `includeRawData` (false), `timeoutSecs` (120), `cacheTtlMinutes` (20), `dealScoreWeights`.

### Output

Dataset: one record per normalized offer (`recordType: "offer"`), plus `"summary"` (single mode) or `"hotel-summary"` ranking rows (bulk mode). Key-Value Store: `DEAL_REPORT` (full JSON) and `DEAL_SUMMARY_TEXT` (human-readable).

```json
{
  "status": "success",
  "cheapestOffer": { "provider": "xotelo", "subSource": "Agoda.com", "normalizedPrice": 1494, "pricePerNight": 249, "dealScore": 87, "bookingUrl": "https://..." },
  "bestValueOffer": { "...": "may differ from cheapestOffer, with bestValueReason explaining why" },
  "deal": { "dealScore": 87, "label": "Excellent deal", "savingsAmount": 354, "savingsPercent": 19.2 },
  "marketStats": { "lowestPrice": 1494, "medianPrice": 1848, "highestPrice": 2556, "providersChecked": 1, "validOffers": 3, "currency": "EUR" },
  "comparisonGroups": [], "offers": [], "warnings": [], "fetchedAt": "..."
}
```

### Deal Score explained

| Component | Max | How |
|---|---|---|
| **Price Advantage** | 60 | vs the **median of equivalent offers only** (same comparison group). Savings saturate at 30%; being group-cheapest adds an undercut bonus vs the second-cheapest. A lone offer gets a neutral 35% — no market evidence. |
| **Offer Quality** | 20 | refundable +0.40 · all-inclusive +0.30 / full board +0.25 / half board +0.20 / breakfast +0.15 · taxes confirmed +0.20 · pay at property +0.10 |
| **Confidence / Freshness** | 20 | 0.5 × offer data quality + 0.35 × hotel-match confidence + 0.15 × freshness (cached results decay) |

**90–100** Exceptional · **80–89** Excellent · **70–79** Good · **60–69** Fair · **0–59** Normal/weak. Weights configurable via `dealScoreWeights`.

### API usage

#### JavaScript

```javascript
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: 'YOUR_APIFY_TOKEN' });

const run = await client.actor('alpercsv/hotel-deal-arbitrage-finder').call({
    hotelName: 'Charisma De Luxe Hotel',
    destination: 'Kusadasi, Turkey',
    checkIn: '2026-08-25',
    checkOut: '2026-08-31',
    adults: 3,
    currency: 'EUR',
});
const report = await client.keyValueStore(run.defaultKeyValueStoreId).getRecord('DEAL_REPORT');
console.log(report.value.deal, report.value.cheapestOffer);
```

#### Python

```python
from apify_client import ApifyClient
client = ApifyClient("YOUR_APIFY_TOKEN")

run = client.actor("alpercsv/hotel-deal-arbitrage-finder").call(run_input={
    "destination": "Kusadasi, Turkey",
    "checkIn": "2026-08-25", "checkOut": "2026-08-31", "adults": 2,
    "hotels": [{"hotelName": "Charisma De Luxe Hotel"}, {"hotelName": "Hotel 54", "destination": "Sakarya, Turkey"}],
})
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    if item.get("recordType") == "hotel-summary":
        print(item["rank"], item["hotelName"], item.get("cheapestPrice"), item.get("savingsPercent"))
```

### Use cases

- **Travel agencies** — quote the genuinely best bookable rate, with conditions
- **Hotel deal websites & newsletters** — auto-surface only offers above a Deal Score threshold
- **Price-alert / price-drop services** — scheduled runs + webhook/Telegram alerts out of the box
- **Affiliate sites & travel bloggers** — real OTA price-gap data for content and link ranking
- **Travel SaaS & OTA intelligence** — rate-parity and competitive monitoring via API
- **AI agents** — hotel deal answers through Apify's MCP integration
- **Consumers & travel data analysts** — one run before booking; normalized, deduplicated data

### Limitations

- Rate coverage comes from TripAdvisor's comparison surface (via Xotelo); niche OTAs may be missing. Google Hotels adds coverage with a SerpApi key.
- Automatic name-based identification is best-effort without a `rapidApiKey`; for bulk scans provide URLs/keys or a RapidAPI key.
- Prices are snapshots (`fetchedAt`), cached at most `cacheTtlMinutes`; FX uses daily ECB reference rates.
- Xotelo totals derive from nightly averages with unknown tax treatment (`taxConfidence: "unknown"`) — verify final totals on the booking page.

### Responsible usage

Only publicly accessible or explicitly authorized APIs; conservative concurrency, bounded retries with backoff, request pacing, and caching. No CAPTCHA circumvention, no credential use, no access-control bypasses — providers that would need them are not included. You are responsible for complying with the terms of any API keys you supply.

# Actor input Schema

## `hotelName` (type: `string`):

Name of the hotel to compare, e.g. "Charisma De Luxe Hotel". With just name + destination the Actor finds the hotel's TripAdvisor page automatically; adding "tripAdvisorUrl" makes identification 100% reliable. Leave empty only when using the bulk "hotels" list.

## `destination` (type: `string`):

City and country of the hotel, e.g. "Kusadasi, Turkey". Used for automatic hotel identification and to disambiguate similar names. In bulk mode this is the fallback destination for entries without their own.

## `checkIn` (type: `string`):

Check-in date in ISO format (YYYY-MM-DD). Must not be in the past.

## `checkOut` (type: `string`):

Check-out date in ISO format (YYYY-MM-DD). Must be after check-in; stays up to 30 nights are supported.

## `adults` (type: `integer`):

Number of adult guests.

## `children` (type: `integer`):

Number of children.

## `childrenAges` (type: `array`):

Age of each child (0-17). When provided, the count must match the "children" field.

## `rooms` (type: `integer`):

Number of rooms.

## `currency` (type: `string`):

3-letter ISO currency code all prices are normalized into (live ECB rates; offers are dropped rather than converted with invented rates).

## `market` (type: `string`):

Optional 2-letter market code (e.g. "TR", "DE") passed to providers that support market-specific pricing.

## `language` (type: `string`):

Language code for provider results where supported.

## `tripAdvisorUrl` (type: `string`):

Optional but recommended: the hotel's page URL on tripadvisor.com. It identifies the property with 100% certainty. Without it the Actor auto-resolves the hotel from name + destination (search-engine lookup, best-effort; or the Xotelo Search API when a "rapidApiKey" is provided).

## `hotels` (type: `array`):

Bulk mode: scan up to 20 hotels in one run and get a ranked arbitrage table. Each entry: {"hotelName": "...", "destination": "... (optional, falls back to the top-level destination)", "tripAdvisorUrl": "... (optional)", "tripAdvisorHotelKey": "g...-d... (optional)"}. When non-empty, the top-level hotelName is ignored.

## `alerts` (type: `object`):

Turn the Actor into a price monitor (combine with Apify Schedules): {"maxTotalPrice": 750, "minSavingsPercent": 20, "minDealScore": 80, "webhookUrl": "https://...", "telegramBotToken": "...", "telegramChatId": "..."}. A notification is sent when any scanned hotel matches the conditions.

## `rapidApiKey` (type: `string`):

Optional RapidAPI key for Xotelo's Search endpoint — makes hotel-name auto-resolution reliable (the keyless search-engine fallback can be rate-limited). Free tier at rapidapi.com (search "Xotelo"). Alternatively set the RAPIDAPI\_KEY environment variable.

## `hotelUrls` (type: `array`):

Optional additional URLs identifying the same hotel on other sites (used by future providers).

## `tripAdvisorHotelKey` (type: `string`):

Alternative to the TripAdvisor URL: the raw location key like "g297972-d302274". When set, it takes precedence over the URL.

## `serpApiKey` (type: `string`):

Optional SerpApi key enabling the Google Hotels provider. Stored as a secret. Alternatively set the SERPAPI\_API\_KEY environment variable.

## `maximumPrice` (type: `integer`):

Hide offers above this total price (in the selected currency). Market statistics still include them.

## `minimumSavingsPercent` (type: `integer`):

Hide offers saving less than this percentage vs the median equivalent offer.

## `filters` (type: `object`):

Optional condition filters: {"refundableOnly": false, "breakfastIncludedOnly": false, "allInclusiveOnly": false, "taxesIncludedOnly": false, "providers": \[]}

## `sortBy` (type: `string`):

How to order the offers in the output.

## `maxOffersPerProvider` (type: `integer`):

Cost-control cap on offers collected from each provider.

## `enabledProviders` (type: `array`):

Restrict the run to these provider ids (empty = all available). Known ids: "xotelo", "google-hotels", "mock".

## `includeRawData` (type: `boolean`):

Attach each provider's raw payload to offers (larger output, useful for debugging).

## `timeoutSecs` (type: `integer`):

Overall budget for provider searches. Providers still running when it elapses are aborted and reported as warnings.

## `cacheTtlMinutes` (type: `integer`):

Reuse provider results for identical searches within this window (0 disables caching). Hotel inventory changes fast, so 15-30 minutes is recommended.

## `dealScoreWeights` (type: `object`):

Optional overrides: {"priceAdvantageMax": 60, "offerQualityMax": 20, "confidenceMax": 20}.

## Actor input object example

```json
{
  "hotelName": "Charisma De Luxe Hotel",
  "destination": "Kusadasi, Turkey",
  "adults": 2,
  "children": 0,
  "childrenAges": [],
  "rooms": 1,
  "currency": "EUR",
  "language": "en",
  "hotels": [],
  "alerts": {},
  "hotelUrls": [],
  "filters": {},
  "sortBy": "dealScore",
  "maxOffersPerProvider": 25,
  "enabledProviders": [],
  "includeRawData": false,
  "timeoutSecs": 120,
  "cacheTtlMinutes": 20
}
```

# Actor output Schema

## `offers` (type: `string`):

No description

## `dealReport` (type: `string`):

No description

## `dealSummary` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "hotelName": "Charisma De Luxe Hotel",
    "destination": "Kusadasi, Turkey"
};

// Run the Actor and wait for it to finish
const run = await client.actor("alpercsv/hotel-deal-arbitrage-finder").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "hotelName": "Charisma De Luxe Hotel",
    "destination": "Kusadasi, Turkey",
}

# Run the Actor and wait for it to finish
run = client.actor("alpercsv/hotel-deal-arbitrage-finder").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "hotelName": "Charisma De Luxe Hotel",
  "destination": "Kusadasi, Turkey"
}' |
apify call alpercsv/hotel-deal-arbitrage-finder --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,alpercsv/hotel-deal-arbitrage-finder"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/yON2ZDLpQO95GDfWW/builds/echTsw3uUugrrb2zx/openapi.json
