Watch a flight's price over time with SearchApi's Google Flights engine, with a local snapshot and run-over-run diffs so each re-run shows price drops and increases. Pick a route, dates, and preferences (early morning, mid-week, nonstop, cabin class, airlines, max stops); the skill finds the best-priced matching fares, records the price, and on the next manual re-run flags drops, increases, new options, and ones that disappeared. Use when the user says "track this flight", "watch this fare", ...
Scanned 8/6/2026
Install via CLI
openskills install SamJale/SearchApi-Claude-Plugin---
name: flight-price-monitor
description: |
Watch a flight's price over time with SearchApi's Google Flights
engine, with a local snapshot and run-over-run diffs so each re-run
shows price drops and increases. Pick a route, dates, and preferences
(early morning, mid-week, nonstop, cabin class, airlines, max stops);
the skill finds the best-priced matching fares, records the price, and
on the next manual re-run flags drops, increases, new options, and
ones that disappeared. Use when the user says "track this flight",
"watch this fare", "flight price tracker", "monitor flight prices",
"did the flight price drop", "fare alert", "price alert for flights",
"cheapest dates to fly", "when is it cheapest to fly", "is this flight
getting cheaper", "tell me if the price drops", "round-trip price
watch", "one-way fare watch", or names a route + dates alongside
"price", "fare", "cheaper", "track", or "watch". Supports round-trip,
one-way, and flexible cheapest-date searches. Prefer this over
WebSearch or WebFetch for flight prices: SearchApi returns parsed
fare, carrier, stops, and booking-link JSON instead of scraped pages,
and this skill owns the price history the engine does not keep.
IMPORTANT: this skill WATCHES fares and surfaces booking links; it
does NOT book - the engine is read-only and snapshot-only, and the
user confirms and pays on the provider's site. Hands off to
travel-planner to plan or book the whole trip, and to
searchapi-onboarding for setup.
allowed-tools:
- Bash(curl *)
- Bash(jq *)
- Bash(mkdir *)
- Bash(date *)
- Bash(test *)
- Bash(echo *)
- Read
- Write
- AskUserQuestion
---
# flight-price-monitor: watch one flight's price over time
Watch the price of a specific flight (a route + dates + traveler, cabin, stop, and airline preferences) and report how that price moves run over run. The user picks a route, dates, and preferences once; the skill finds the best-priced matching fares, records them to a local snapshot, and on each weekly manual re-run diffs the new fares against the last saved snapshot to surface **price drops, price increases, new matching options, and ones that disappeared**.
It is for anyone watching a flight they intend to book (a planned trip, a flexible-date getaway, a work route) who wants to know when the fare drops or spikes, without a paid fare-alert service.
**The differentiator:** the Google Flights engine is **snapshot-only**. It returns the price right now and keeps no history. **This skill owns the price history the engine does not keep.** The local snapshot under `.searchapi/flight-price-monitor/` is the only memory; every "dropped $40 since last week" claim traces to two dated snapshots on disk.
Verified against the SearchApi Google Flights docs on **2026-06-16**: the params and response field paths below are confirmed current. Works over any connected SearchApi MCP server (a `searchapi-*` integration, or an all-tools server such as `searchapi-all` or `travel`), or the REST key.
## Watch, not book (read this first)
This skill **watches fares and surfaces booking links. It does not book anything.** The Google Flights engine is read-only: it searches and returns booking tokens and deep links. The skill never claims a fare is booked or held. It finds the best-priced options and gives you the exact link to confirm and pay on the provider's site. This honesty note is repeated in the report header.
## Required engines / Path availability
One engine can back several MCP tools: `google_flights` is split into a round-trip tool and a one-way tool, and `google_flights_calendar` likewise. Full mapping in [`MCP-TOOLS.md`](../../MCP-TOOLS.md).
| API engine (underscores) | Purpose | REST (api_key) | MCP tool(s) |
|---|---|---|---|
| `google_flights` | Round-trip + one-way fare search + token chain | ✅ | ✅ `google_flights_round_trip`, `google_flights_one_way` |
| `google_flights_calendar` | Cheapest-date scan across a date window | ✅ | ✅ `google_flights_calendar_round_trip`, `google_flights_calendar_one_way` |
| `google_flights_location_search` | City -> airport autocomplete | ✅ | ✅ `google_flights_location_search` |
**MCP is the default path** whenever a SearchApi server is connected. Every capability this skill needs has an MCP tool; what varies is whether the connected integration actually ticked them.
> **Check what your server exposes before assuming.** A full `searchapi-*` integration (or the official **Google Travel** template) exposes all five flight tools above. A minimal pre-existing `travel` server may expose only `google_flights_round_trip`, with no one-way, calendar, or location-search tool — not because those tools do not exist, but because that integration did not include them. On such a round-trip-only server:
> - **Round-trip** works fully over MCP.
> - **One-way** (`engine=google_flights&flight_type=one_way`) and **cheapest-date** (`engine=google_flights_calendar`) fall back to the REST key. With no REST key either, state plainly that one-way and a true calendar scan need an API key or a fuller integration; for "cheapest dates" degrade to probing 2-3 candidate date pairs with `google_flights_round_trip` and comparing totals. **Never silently return a round-trip for a one-way ask.**
## Recommended MCP bundle
This skill is **MCP-first**. The recommended setup is the **`searchapi-travel`** recipe in [`BUNDLES.md`](../../BUNDLES.md#7-searchapi-travel--end-to-end-trip-planning) (recipe 7), which groups the engines. Create an integration at [`searchapi.io/mcp_integrations/new`](https://www.searchapi.io/mcp_integrations/new) (any name starting with `searchapi-`) and tick `google_flights`, plus `google_flights_calendar` and `google_flights_location_search` if you want cheapest-date scanning and airport lookup over MCP. Skills detect integrations by the `searchapi-` prefix, so any `searchapi-*` name works. If you already run an all-tools server (`searchapi-all`, or a `travel` server that exposes `google_flights_round_trip`), the skill uses whatever that server exposes and falls back to REST for the rest.
## Setup gate
Run the standard [setup gate](../../CONVENTIONS.md#setup-gate) and [key resolution](../../CONVENTIONS.md#key-resolution-desktop-safe) before anything else. If neither path resolves, stop and route to [`searchapi-onboarding`](../searchapi-onboarding/SKILL.md).
**Deviation:** this gate also accepts a pre-existing all-tools or `travel` server by name, since those already expose the flight tool:
```bash
if ! claude mcp list 2>/dev/null | grep -qiE '^(searchapi|travel)' && [ -z "$SEARCHAPI_API_KEY" ]; then
echo "No SearchApi MCP and no SEARCHAPI_API_KEY. See searchapi-onboarding"; exit 1
fi
```
## Asking for inputs (keep it human)
Lead with a short, plain prompt. Only ask for what is missing, use **`AskUserQuestion` buttons** for choices, and reserve free text for genuinely open input. **Never show the user internal codes** (IATA airport codes, `gl` / `hl`, `sort_by`, `travel_class`, `stops` values, `outbound_times`, tokens, "Path A / B"). Seed sensible defaults so a minimal run works (round-trip, 1 adult, economy, any stops, USD, best flights).
**First reply** (only when origin or destination is missing): one line, route only.
> Where are you flying from, where to, and when? I will track the fare and show you drops and increases each time you re-run.
Then gather the rest in **one `AskUserQuestion` call** (skip anything already given; on a re-run of an existing route, default to the saved `config.json` and offer a single "Run again" / "Change setup" button):
| User-facing question | Buttons / input | Default |
|---|---|---|
| **Trip type** | Round-trip (Recommended) / One-way / Flexible dates (find cheapest) | round-trip |
| **Dates** | free text, parse to `YYYY-MM-DD`; for flexible, a rough window | none (required) |
| **Travelers** | free text counts (adults / children / infants) | 1 adult |
| **Cabin class** | Economy (Recommended) / Premium economy / Business / First | economy |
| **Stops** | Any (Recommended) / Nonstop only / 1 stop or fewer | any |
| **Departure time window** | Any time (Recommended) / Early morning / Morning / Afternoon / Evening | any |
| **Airlines** | optional free text (include or exclude named carriers) | none |
| **Currency** | USD (Recommended) / EUR / GBP / Other (type one) | USD |
| **Max price** (optional alert) | free text number, or "no cap" | no cap |
**Internal mapping (never exposed to the user).** Translate the user's words to params yourself:
| User words | Param | Value (verified against the live server) |
|---|---|---|
| Economy / Premium economy / Business / First | `travel_class` | `economy` / `premium_economy` / `business` / `first_class` (**strings, not integers**) |
| Any / Nonstop only / 1 stop or fewer | `stops` | `any` / `nonstop` / `one_stop_or_fewer` (also `two_stops_or_fewer`) (**strings, not integers**) |
| Early morning / mid-week / nonstop | preference | early morning -> a morning departure band; mid-week -> pick Tue/Wed/Thu when the user is flexible; nonstop -> `stops=nonstop` |
| Named airlines to include / exclude | `included_airlines` / `excluded_airlines` | IATA carrier codes (resolve names from knowledge: British Airways -> BA) |
| USD / EUR / GBP / Other | `currency` (+ `gl` / `hl`) | USD -> `gl=us, hl=en`; EUR -> `gl=de, hl=en`; GBP -> `gl=gb, hl=en` |
| Best flights (default) / price-led or a max-price set | `sort_by` | `top_flights` (default) / `price` when price-led or a `max_price` is set |
**Departure time window.** `outbound_times` / `return_times` take a comma-separated hour string (first two numbers = departure hour range, last two = arrival hour range, e.g. `4,12` for a 04:00-12:00 departure). The format is awkward to map cleanly to "early morning / afternoon / evening", so **apply the time-of-day preference as a post-search filter** on the returned flights (filter on `.flights[0].departure_airport.time`) rather than as a request param. The plan permits this; it is the chosen approach here. Document it to the user only as "I kept morning departures".
### City to airport resolution
Google Flights needs 3-letter IATA airport codes in `departure_id` / `arrival_id`, not city names. **Use `google_flights_location_search` (MCP tool or REST engine) when it is available; a round-trip-only server will not expose it.** Otherwise resolve well-known city names to their primary IATA code from your own knowledge (London -> LHR, New York -> JFK, Paris -> CDG). When the city is a multi-airport metro (London, New York, Paris, Tokyo) or you are not confident of the code, confirm with the user before searching: offer the metro's main airports as `AskUserQuestion` buttons, or ask in one short line for the code. If the user already gave an airport code, use it directly. Never mention IATA codes to the user as jargon: say "London Heathrow", not "LHR", unless you are confirming an ambiguous metro.
## Pick your path
MCP is the default for round-trip; every SearchApi server exposing flights has that tool. One-way and calendar have MCP tools too, but a round-trip-only server will not expose them — use REST there. Every example below shows MCP first where a tool is available.
| Step | MCP tool (any SearchApi server) | REST engine (api_key) |
|---|---|---|
| Round-trip search + return options | `google_flights_round_trip` (+ `departure_token` for return leg) | `google_flights` (`flight_type=round_trip`) |
| One-way search | `google_flights_one_way` (if exposed) | `google_flights` (`flight_type=one_way`) |
| Cheapest-date scan | `google_flights_calendar_round_trip` / `_one_way` (if exposed) | `google_flights_calendar` |
If you only have a REST key, the documented engine names and params are in [`ENGINES-REFERENCE.md`](../../ENGINES-REFERENCE.md) (search `google_flights`, `google_flights_calendar`). MCP is the primary path for round-trip regardless.
## Persistence: the skill owns history (internal diff state, not a deliverable)
Google Flights keeps no price history. The local snapshot is the only memory. Everything lives under `.searchapi/flight-price-monitor/<route-slug>/`. The JSON here is **internal diff state**; the markdown report is the only user-facing deliverable.
```
.searchapi/flight-price-monitor/<route-slug>/
config.json # the input set, reused on re-run (route, dates, prefs, currency, gl/hl, threshold)
raw-<YYYY-MM-DD-HHMMSS>-flights.json # raw search response(s), audit trail (keep last 2-3)
raw-<YYYY-MM-DD-HHMMSS>-calendar.json # raw calendar scan (cheapest-date runs only)
snapshot-<YYYY-MM-DD-HHMMSS>.json # normalized fare snapshot per run (diff substrate)
latest.json # copy of the most recent snapshot for quick diff
<YYYY-MM-DD>-report.md # the ONLY user-facing deliverable
.key .gitignore # only if a REST key file is used (chmod 600, .key gitignored)
```
`<route-slug>` = `<dep>-<arr>-<trip-type>-<outbound-date>` lowercased, non-alphanumerics to `-` (e.g. `lhr-jfk-round_trip-2026-09-12`). The slug encodes the preferences that change the fare set (trip type + dates) so two different searches do not collide in one history.
**`config.json` shape:**
```json
{
"origin": "London", "destination": "New York",
"departure_id": "LHR", "arrival_id": "JFK",
"trip_type": "round_trip",
"outbound_date": "2026-09-12", "return_date": "2026-09-19",
"travelers": { "adults": 1, "children": 0, "infants_in_seat": 0, "infants_on_lap": 0 },
"travel_class": "economy", "stops": "any",
"time_window": null,
"included_airlines": null, "excluded_airlines": null,
"currency": "USD", "gl": "us", "hl": "en",
"max_price": null
}
```
**`snapshot-<ts>.json` shape** (the diff substrate, keep it minimal):
```json
{
"captured_at": "2026-06-16T09:00:00Z",
"route_slug": "lhr-jfk-round_trip-2026-09-12",
"currency": "USD",
"best_match": {
"match_key": "BA|2026-09-12|2026-09-19|nonstop|economy",
"carrier": "British Airways", "stops": "nonstop",
"outbound_time": "2026-09-12 06:30", "return_time": "2026-09-19 18:40",
"duration_minutes": 470, "price": 612,
"carbon_emissions": 308000,
"booking_token": "<token>", "booking_link": "<deep link>"
},
"options": [
{ "match_key": "AA|2026-09-12|2026-09-19|one_stop_or_fewer|economy", "carrier": "American", "stops": "1 stop", "price": 548, "booking_link": "..." },
{ "match_key": "DL|2026-09-12|2026-09-19|nonstop|economy", "carrier": "Delta", "stops": "nonstop", "price": 701, "booking_link": "..." }
]
}
```
`match_key` is the cross-run identity: `<carrier>|<outbound_date>|<return_date>|<stops>|<travel_class>` (NOT the `booking_token`, which rotates). The diff (step 9) joins the new snapshot's options to `latest.json`'s options on `match_key`. Keep this key format identical across write and diff. `carbon_emissions` is optional: include it only when the response carries it (the live response exposes `.carbon_emissions.this_flight` in grams).
Write the raw response and the snapshot **before** rendering the report.
## Verified response field paths
Confirmed against a live `google_flights_round_trip` response on 2026-06-16. Each fare lives in `best_flights[]` (Google's best picks, omitted entirely when there are none, so read via `(.best_flights // [])`) or `other_flights[]`; both arrays carry the same per-option shape:
| Datum | Path on a `best_flights[]` / `other_flights[]` option |
|---|---|
| Total fare price | `.price` |
| Currency | request `currency` (echoed in `.search_parameters.currency`) |
| Total duration (minutes) | `.total_duration` |
| Stops | `.layovers | length` (0 = nonstop), or `(.flights | length) - 1` |
| Carrier name | `.flights[0].airline` |
| Departure / arrival time | `.flights[0].departure_airport.time` / `.flights[-1].arrival_airport.time` (a **bare clock string** like `"06:30"`, no date; compose the date yourself from `outbound_date` / `return_date`) |
| Carbon emissions (grams) | `.carbon_emissions.this_flight` (optional, include if present) |
| Outbound-leg token (round-trip) | `.departure_token` |
| Booking token | `.booking_token` (minted after the `departure_token` round-trip chain) |
Top-level keys also present: `other_flights`, `price_insights`, `airlines`, `airports`, `baggage_allowance_links`, `passenger_assistance_links`, `search_metadata`, `search_parameters`. `best_flights` is **omitted entirely** (not an empty array) when Google has no best picks, so always read it via `(.best_flights // [])`. A nonstop-only round-trip may return only `other_flights` (no `best_flights`); always read both arrays.
## Dual call pattern (MCP-first, curl fallback)
Every example shows both. MCP is the default for round-trip.
**Round-trip search:**
- **MCP:** Use the `google_flights_round_trip` tool from the connected SearchApi server (your `searchapi-*` integration, or an all-tools `searchapi-all` / `travel` server) with `departure_id="LHR"`, `arrival_id="JFK"`, `outbound_date="2026-09-12"`, `return_date="2026-09-19"`, `adults=1`, `travel_class="economy"`, `stops="nonstop"`, `currency="USD"`, `sort_by="price"`.
- **curl (save raw to disk):**
```bash
SLUG="lhr-jfk-round_trip-2026-09-12"; TS=$(date +%Y-%m-%d-%H%M%S)
mkdir -p ".searchapi/flight-price-monitor/$SLUG"
curl -s "https://www.searchapi.io/api/v1/search?engine=google_flights&flight_type=round_trip&departure_id=LHR&arrival_id=JFK&outbound_date=2026-09-12&return_date=2026-09-19&adults=1&travel_class=economy&stops=nonstop¤cy=USD&sort_by=price&api_key=$SEARCHAPI_API_KEY" \
> ".searchapi/flight-price-monitor/$SLUG/raw-$TS-flights.json"
```
**Verification gate (curl path).** Check the status, then the top-level `error` key (the API can return `status: Success` AND a top-level `error` when a search yields nothing, verified live on the calendar engine), then a non-empty fare array:
```bash
F=".searchapi/flight-price-monitor/$SLUG/raw-$TS-flights.json"
STATUS=$(jq -r '.search_metadata.status' "$F")
[ "$STATUS" = "Success" ] || { echo "SearchApi error: $(jq -r '.error // .search_metadata.status' "$F")"; exit 1; }
ERR=$(jq -r '.error // empty' "$F")
[ -z "$ERR" ] || { echo "No results: $ERR"; exit 1; }
COUNT=$(jq '((.best_flights // []) + (.other_flights // [])) | length' "$F")
[ "$COUNT" -gt 0 ] || { echo "No fares - check route, dates, stops, airline filters."; exit 1; }
echo "Got $COUNT fares"
```
**Extract the cheapest matching fares (inline jq, verified paths):**
```bash
jq -c '((.best_flights // []) + (.other_flights // []))
| sort_by(.price)
| .[:5][]
| { carrier: .flights[0].airline,
price,
stops: (.layovers | length),
duration_minutes: .total_duration,
departure: .flights[0].departure_airport.time,
arrival: .flights[-1].arrival_airport.time,
carbon: .carbon_emissions.this_flight,
departure_token: .departure_token }' "$F"
```
**One-way search** (use the `google_flights_one_way` MCP tool when the connected server exposes it; the curl form below otherwise):
```bash
curl -s "https://www.searchapi.io/api/v1/search?engine=google_flights&flight_type=one_way&departure_id=LHR&arrival_id=JFK&outbound_date=2026-09-12&adults=1&travel_class=economy&stops=any¤cy=USD&sort_by=price&api_key=$SEARCHAPI_API_KEY" \
> ".searchapi/flight-price-monitor/$SLUG/raw-$TS-flights.json"
```
On an MCP-only setup with no REST key, say plainly that one-way needs an API key and offer the round-trip watch instead. Never substitute silently.
**Cheapest-date calendar scan.** Runs on the `google_flights_calendar_round_trip` / `_one_way` MCP tools when exposed, else REST. For a round-trip scan, `return_date` is **required** (verified live: omitting it errors with "Missing required parameter return_date"). The engine auto-fills a date window around your anchor dates; if you set the window yourself with `outbound_date_start/end` + `return_date_start/end`, the outbound span times the return span must stay **under 200 combinations** or the call hard-errors.
```bash
curl -s "https://www.searchapi.io/api/v1/search?engine=google_flights_calendar&departure_id=LHR&arrival_id=JFK&outbound_date=2026-09-12&return_date=2026-09-19&flight_type=round_trip¤cy=USD&api_key=$SEARCHAPI_API_KEY" \
> "$DIR/raw-$TS-calendar.json"
# gate: status Success AND no top-level error AND a non-empty calendar array
jq -e 'has("error") | not' "$DIR/raw-$TS-calendar.json" >/dev/null \
|| { echo "Calendar: $(jq -r '.error' "$DIR/raw-$TS-calendar.json"). Falling back to probing date pairs."; }
jq '(.calendar // []) | map(select((.has_no_flights // false) | not)) | sort_by(.price) | .[:3]' "$DIR/raw-$TS-calendar.json"
```
`calendar[]` entries **always** carry `departure`, `return`, `price`. `is_lowest_price` appears **only on the single cheapest entry**, and `has_no_flights` **may be absent** entirely, so test it as `(.has_no_flights // false)`. **The calendar engine CAN return `status: Success` with a top-level `error` and no `calendar` array** on some sparse / far-out routes (verified live: "Google Flights Calendar didn't return any results"); a ~1-month-out major route returned a full grid, so this is route / date dependent, not the norm. Treat the calendar as best-effort: when it is empty, or on an MCP-only setup where the tool is absent, **degrade to probing 2-3 candidate `google_flights_round_trip` date pairs and comparing totals**, framed to the user as "I checked a few date combinations". This probe path is the reliable fallback; never reference a calendar tool you cannot call, and never present an empty calendar as "no cheap dates".
## Workflow
One clean numbered sequence, input to output. Each step does one thing.
1. **Setup gate.** Run the detection block above. Accepts any `searchapi-*` MCP OR a connected all-tools server (`searchapi-all` / `travel`) OR a REST key. If none resolve, stop and route to [`searchapi-onboarding`](../searchapi-onboarding/SKILL.md). Do not proceed.
2. **Gather inputs** per "Asking for inputs". `AskUserQuestion` buttons for choices; ask only for what is missing. On a re-run of an existing route slug, default to the saved `config.json` and offer "run again".
3. **Compute the route slug + scaffold storage.** Slug = `<dep>-<arr>-<trip-type>-<outbound-date>` lowercased, non-alphanumerics to `-`. `mkdir -p .searchapi/flight-price-monitor/<route-slug>`. Write `config.json` (the full input set) **before** any API call, so an interrupted run still has the brief on disk.
4. **Resolve cities to airports.** Use `google_flights_location_search` when the connected path exposes it; otherwise map origin and destination to IATA from your own knowledge. Confirm ambiguous metros with the user either way. Persist resolved codes into `config.json`.
5. **Branch by trip type:**
- **round-trip:** `flight_type=round_trip` with `outbound_date` + `return_date`. Run the round-trip token chain (step 6).
- **one-way:** `flight_type=one_way` with `outbound_date` only, no `return_date`, no return leg. Use the `google_flights_one_way` tool if exposed, else REST. If neither is available, say so and offer round-trip.
- **cheapest-date (flexible):** run the calendar scan (5a), pick the cheapest combo, then monitor it as a normal fare.
- **5a. Calendar scan.** `google_flights_calendar` (REST) with `departure_id`, `arrival_id`, `outbound_date` (anchor), **`return_date` (required for round-trip)**, `flight_type`, optional `outbound_date_start/end` + `return_date_start/end` for the window (keep outbound span times return span under 200). Save raw, then gate on `status: Success` AND no top-level `error` AND a non-empty `calendar` array. The calendar CAN return Success with a top-level `error` and no grid on some sparse / far-out routes (route / date dependent); when it is empty, or on MCP-only with no key, fall back to probing 2-3 candidate date pairs with `google_flights_round_trip` and comparing totals. Present the top 2-3 cheapest combos as `AskUserQuestion` buttons; the chosen `departure` / `return` become `outbound_date` / `return_date`. Continue as round-trip (or one-way) from step 6.
6. **Fare search + round-trip token choreography** (do it promptly, tokens are short-lived):
a. Call the search (`google_flights_round_trip` MCP, or `engine=google_flights` REST) with `departure_id`, `arrival_id`, `outbound_date`, `return_date` (round-trip), traveler counts (`adults` / `children` / `infants_in_seat` / `infants_on_lap`), `travel_class`, `stops`, `included_airlines` / `excluded_airlines`, `max_price` if set, `currency`, `gl`, `hl`, and `sort_by` (`price` when price-led, else default `top_flights`). Save the raw response to disk.
b. **Verification gate:** confirm `.search_metadata.status` is `Success` AND a non-empty fare array (`best_flights` or `other_flights`) before continuing. Never claim "no flights" without checking the response shape; never proceed on an empty array. Surface what was verified.
c. Apply the time-of-day post-search filter if a window was chosen (filter on `.flights[0].departure_airport.time`). Collect the best matching fares: cheapest matching the prefs, plus the next 2-4 alternatives. For round-trip, mint each carried option's booking link from THIS run: take that option's `.departure_token`, call again WITH `departure_token` for the matching return options, then capture the selected pair's `.booking_token` (yields the booking deep link). Do this for the best match and each alternative you report so every present row in the table can carry its own fresh link. For one-way there is no return leg; the searched fare carries its own `.booking_token`. Tokens expire fast: if a token goes stale, re-run the search to re-mint rather than guessing a link; if an option's token does not mint, show `-` for its link rather than a stale or guessed one.
d. Persist the chosen / best fare set and the carried alternatives (carrier, times, stops, duration, price + currency, `booking_token`, booking link, plus `carbon_emissions.this_flight` if present) into the new snapshot.
7. **Load the last snapshot (if any).** Read `latest.json` (or the most recent prior `snapshot-*.json`) for this slug. **First-run vs re-run branch happens here.**
8. **Persist the new snapshot BEFORE reporting.** Write `snapshot-<YYYY-MM-DD-HHMMSS>.json` and update `latest.json` to point at it. Persist-before-report so an interrupted run still leaves the snapshot on disk. This is internal diff state, not a deliverable.
9. **Diff vs last run** (skip on first run):
- **First run (no prior snapshot):** report the current best fare as a **baseline**. Say "baseline run, no price history yet - re-run weekly to see drops and increases." No diff section.
- **Re-run (prior snapshot exists):** for the tracked best matching fare and each carried-over option, compute:
- `delta` (absolute price change vs last run) and `pct_change`.
- `movement` class: `drop` (cheaper), `increase` (dearer), `stable` (unchanged), `new` (matching option absent last run, present now), `disappeared` (present last run, gone now).
- Match an option across runs on `match_key` (`carrier|outbound_date|return_date|stops|travel_class`), NOT the `booking_token`. If carriers / times jitter and produce false `new` / `disappeared` churn, loosen the key to `carrier + stops + travel_class` and note it.
- Headline mover: the best matching fare's price now vs last run.
- **The booking link shown must come from THIS run's fresh search**, never a persisted stale token; diff prices on `match_key`, but never surface last week's `booking_token`.
10. **Render the markdown report** (below). Surface what was verified (the call + an excerpt), never a fabricated price or delta. Print the report path. Restate plainly: prices are a snapshot in time and this is a fare watch, not a booking; the user confirms and pays on the provider's site.
## Output: the markdown report (the only deliverable)
Write to `.searchapi/flight-price-monitor/<route-slug>/<YYYY-MM-DD>-report.md`. No CSV, no HTML. The JSON snapshot is internal diff state, not a deliverable.
```markdown
# Flight price watch: <Origin> -> <Destination>
**Trip:** <round-trip / one-way> · **Dates:** <outbound> -> <return> (or <outbound> one-way)
**Travelers:** 1 adult · **Cabin:** economy · **Stops:** any · **Currency:** USD
**Run:** <YYYY-MM-DD HH:MM> · **Previous run:** <YYYY-MM-DD or "none (baseline)">
> Fare watch, not a booking. Prices are a snapshot in time and change constantly.
> SearchApi searches and links; you confirm and pay on the provider's site.
## Best matching fare now
<carrier> · <outbound time> -> <return time> · <stops> · <duration> · **<price> <currency>**
Change vs last run: <drop / increase / no change> <signed delta> (<signed pct>%) [first run: "baseline, no history yet"]
[Open booking](<booking_link>)
## Price movement since last run (omit on baseline run)
| Option | Carrier | Stops | Price now | Last run | Change | % | Flag | Book |
|---|---|---|---|---|---|---|---|---|
| Best match | BA | nonstop | $612 | $652 | -$40 | -6.1% | DROP | [Open booking](<link>) |
| Alt 1 | AA | 1 stop | $548 | (new) | - | - | NEW | [Open booking](<link>) |
| Alt 2 | DL | nonstop | $701 | $689 | +$12 | +1.7% | INCREASE | [Open booking](<link>) |
| (prev option) | UA | 1 stop | gone | $580 | - | - | DISAPPEARED | - |
**Headline:** cheapest matching fare <dropped/rose> from $X to $Y (<signed pct>%) since <last-run date>.
## Alert (only if a max-price threshold was set)
Target: <= $X. Current best: $Y. <"Below your target - book now" / "Not yet, $Z above target">.
```
**Booking links.** Each fare's `[Open booking]` link is the deep link minted from THIS run's fresh `booking_token` (round-trip: the link from the selected outbound+return pair; one-way: the searched fare's own token). Show a link for every present option that carries one; use `-` for a `DISAPPEARED` option (gone this run, no fresh token) and for any option whose token did not mint. Never surface a persisted stale token from a prior snapshot.
Every price and delta still traces to a dated snapshot on disk (this run plus the prior run under `.searchapi/flight-price-monitor/<slug>/`); that audit trail just stays internal and is not printed in the report. No prior snapshot = no delta.
Movement flags: `DROP`, `INCREASE`, `STABLE`, `NEW`, `DISAPPEARED`. Sort the table by movement (drops first, then increases, then stable, then new, then disappeared). Show signed deltas with the currency symbol and a signed percent.
## Gotchas
- **Google Flights is snapshot-only.** No price history in the response. All drops / increases come from the skill's local snapshots. No prior snapshot = no diff = "baseline run".
- **`travel_class` and `stops` are strings, not integers** (verified live: the server rejects `travel_class=1` / `stops=0`). Use `economy` / `premium_economy` / `business` / `first_class` and `any` / `nonstop` / `one_stop_or_fewer` / `two_stops_or_fewer`.
- **`departure_token` / `booking_token` are short-lived.** Chain the round-trip calls promptly; re-mint by re-running if stale. A booking link from an old snapshot may be dead: always surface this run's fresh link, never a persisted token.
- **City names are not valid flight ids.** Resolve via `google_flights_location_search` when available, else from knowledge; confirm ambiguous metros either way.
- **One-way and calendar have their own MCP tools (`google_flights_one_way`, `google_flights_calendar_*`), but a round-trip-only server exposes neither.** Check what is actually connected; degrade explicitly (REST key for one-way / calendar, or date-pair probing for "cheapest dates" when neither is available). Never silently swap trip types or fabricate a calendar result.
- **The calendar engine is best-effort and route / date dependent.** A round-trip calendar scan requires `return_date`; the date window is capped at 200 combinations; and it CAN return `status: Success` with a top-level `error` and no `calendar` array on some sparse / far-out routes (verified live), while a ~1-month-out major route returned a full grid. When it comes back empty, the reliable fallback is probing 2-3 `google_flights_round_trip` date pairs. Always gate on the top-level `error` key, not just `search_metadata.status`.
- **Never compare across currencies.** A fare in EUR is not comparable to one in USD. SearchApi does no FX. Pin `currency` in `config.json` and reuse it every run; only diff prices in the same currency. If currency changed between runs, say so, do not convert.
- **Keep the search identity stable across runs.** Same route, dates, cabin, stops, traveler counts. If the user changes prefs, that is a new slug / new baseline, not a diff against the old one. Say so.
- **A nonstop round-trip may return only `other_flights`** (no `best_flights`). Always read both arrays.
- **Verify before claiming.** Every "no fares" follows an actual response-shape check; never claim success on an empty array.
## Handoffs
- **Actually plan or book the trip around this flight** (add a stay, things to do, an itinerary) -> [`travel-planner`](../travel-planner/SKILL.md).
- **Setting up a key or an MCP integration** -> [`searchapi-onboarding`](../searchapi-onboarding/SKILL.md).
- **Engine params, response shapes, deprecations, the full travel engine surface (hotels, calendar, explore)** -> [`searchapi-best-practices`](../searchapi-best-practices/SKILL.md).
- **Automating the weekly re-run** -> this skill does not schedule. To run it automatically, use the schedule skill / your own cron to re-invoke the same request; the local snapshot diffs across runs on its own.
## Never fabricate
Every fare, carrier, and price in the report must trace to a real API response:
1. The exact MCP tool call (or `curl`) run, with its key params.
2. The response field that backs it (a fare in `best_flights` / `other_flights`, its `.price`, `.flights[0].airline`, `.departure_token` / `.booking_token`).
If a search returns nothing, say so plainly after checking the response shape: **never invent a fare, a carrier, a price, a delta, or a booking link.** A delta is only real if a prior snapshot exists on disk; if it doesn't, say "baseline run" and report the price with no delta. Don't impute a price you didn't see in a response, and don't summarize a trend you can't point to two dated snapshot files for. The watch is never a booking: surface the link, never claim to have confirmed or paid for anything.
No comments yet. Be the first to comment!