# Build a hotel rate-parity watcher with Claude Desktop and SQLite

> Source: <https://dev.to/huynhtanan/build-a-hotel-rate-parity-watcher-with-claude-desktop-and-sqlite-287c>
> Published: 2026-10-21 02:10:30+00:00

Rate parity is the agreement every hotel signs with each OTA: *no booking site can list the room cheaper than our website*. In practice it breaks constantly — one site runs a flash discount, another forgets to resync a package rate. If you own a hotel, catching that drift in 24 hours instead of 10 days is money.

This post wires the Google Hotels MCP server to a 20-line SQLite logger. Claude Desktop asks the question, the script writes the diff. You end up with a daily table like this:

```
hotel                          date         booking   expedia   agoda    delta
Ritz Paris                     2026-11-15   €1,480    €1,520    €1,480   -2.6%
Hotel Lutetia                  2026-11-15   €820      €820      €795     -3.1%
Mandarin Oriental Paris        2026-11-15   €1,650    €1,650    €1,650    0.0%
```

Disclosure: the MCP server (`huynhtanan/google-hotels-mcp`, open source) calls three paid Apify actors I wrote. Apify's free tier ships $5 of credit per month — about 60 full-city searches — before you need to upgrade. No card required to sign up.

Same install as [the previous post](https://dev.to/huynhtanan/add-google-hotels-to-claude-desktop-as-an-mcp-server-2-minute-setup-1m00). Add this block to `claude_desktop_config.json`:

```
{
  "mcpServers": {
    "google-hotels": {
      "command": "uvx",
      "args": ["--from", "git+https://github.com/huynhtanan/google-hotels-mcp.git@v0.1.2", "google-hotels-mcp"],
      "env": { "APIFY_TOKEN": "apify_api_..." }
    }
  }
}
```

Restart Claude Desktop. Three tools appear: `search_hotels`, `hotel_price_calendar`, `search_vacation_rentals`.

``` python
import sqlite3

db = sqlite3.connect("rate_parity.db")
db.executescript("""
CREATE TABLE IF NOT EXISTS snapshot (
  captured_at TEXT NOT NULL,
  hotel       TEXT NOT NULL,
  stay_date   TEXT NOT NULL,
  ota         TEXT NOT NULL,
  price       REAL NOT NULL,
  currency    TEXT NOT NULL,
  PRIMARY KEY (captured_at, hotel, stay_date, ota)
);
CREATE INDEX IF NOT EXISTS snapshot_recent
  ON snapshot (hotel, stay_date, captured_at DESC);
""")
db.commit()
```

Keeping `currency` as a column (instead of baking EUR into the field name) means the same table can hold USD, GBP, or BRL snapshots when you expand beyond one region.

*"Snapshot the top 20 Paris hotels across the next 30 check-in dates: for each date, call `search_hotels` with `query="hotels in Paris"`, `include_provider_prices=true`, `max_results=20`, two adults, EUR. Insert each (hotel, stay_date, ota, price) tuple into the snapshot table in `rate_parity.db`."*

Claude issues one `search_hotels` call per date (30 calls total), each returning ~20 hotels and their `providers` array. That's 30 × 20 = 600 (hotel, date) rows per snapshot. The whole thing runs in your Claude Desktop session — no external cron.

Here is what the MCP tool actually returns for one hotel on one date (lightly trimmed):

```
{
  "name": "Hotel Lutetia",
  "checkIn": "2026-11-15",
  "checkOut": "2026-11-16",
  "providers": [
    {"name": "Booking.com", "price": 820, "currency": "EUR"},
    {"name": "Expedia",     "price": 820, "currency": "EUR"},
    {"name": "Agoda",       "price": 795, "currency": "EUR"},
    {"name": "Hotels.com",  "price": 820, "currency": "EUR"}
  ],
  "rating": 4.6,
  "reviewsCount": 2341
}
```

Once snapshot rows exist, a single SQL query surfaces drift:

```
WITH latest AS (
  SELECT hotel, stay_date, ota, price, currency,
         ROW_NUMBER() OVER (PARTITION BY hotel, stay_date, ota ORDER BY captured_at DESC) AS rn
  FROM snapshot
),
today AS (SELECT * FROM latest WHERE rn = 1),
agg AS (
  SELECT hotel, stay_date, currency,
         MIN(price) AS cheapest, MAX(price) AS reference,
         COUNT(DISTINCT ota) AS ota_count
  FROM today GROUP BY hotel, stay_date, currency
  HAVING ota_count >= 2
)
SELECT hotel, stay_date, currency, cheapest, reference,
       ROUND((cheapest - reference) * 100.0 / reference, 2) AS delta_pct
FROM agg
WHERE (cheapest - reference) * 100.0 / reference <= -3.0
ORDER BY delta_pct ASC
LIMIT 50;
```

The `ota_count >= 2` guard is important — a hotel with only one OTA listed is not a parity signal; it is just a single price point.

Any row that comes back is a rate-parity violation at least 3% deep. Pipe it into Slack, email, or just a daily markdown digest.

*"Of today's drift rows, which 3 would cost the hotel the most if a corporate traveler booked 10 nights on the cheapest OTA instead of the reference OTA?"*

Claude runs the arithmetic directly against the drift rows and writes a one-paragraph summary. The whole feedback loop — scrape, diff, prioritize, explain — runs inside one chat window.

`search_hotels` pricing: `$0.002/hotel + $0.002/provider-prices` row → 600 × $0.004 ≈ `examples/rate_parity/` run (20 hotels × 7 nights = $0.56/day) stays in the free tier for ~9 days a month
The actor is pay-per-result — you pay only for rows that actually came back, not for failed runs or empty days.

The MCP server, the SQLite schema, and the drift query are in [huynhtanan/google-hotels-mcp](https://github.com/huynhtanan/google-hotels-mcp) under `examples/rate_parity/`. One-file CLI, no framework — the same script can also run from a cron, which is handy if you want nightly snapshots without keeping Claude Desktop open.

If you have a single hotel and want to try this against your own property — ping me (comments are open). I'm interested in which fields the parity teams actually care about so I can bake them into the next cut of the server.
