Skip to main content
Glama
teatimedev

splitfare-mcp

by teatimedev
README.md
# splitfare 🎟️

**A flight-hacker's scanner for short trips: split tickets, trip-shape search,
and it knows what's on when you land.**

Booking sites answer *"how much are flights on these dates?"* splitfare
answers the question you actually have:

> *"Find me any weekend next month β€” out Friday or Saturday, landing before
> 2pm, home Sunday night β€” and tell me who's playing while I'm there."*

It prices the direct fares **and** every viable split-ticket combination
(separate cheap tickets glued together through hub airports β€” routings no
booking site will show you), sweeps whole months by *trip shape*, and pairs
every candidate window with the destination's event calendar.

<p align="center">
  <img src="docs/days.png" width="30%" alt="Every window in the month, priced, with headliners per night">
  <img src="docs/day-detail.png" width="30%" alt="A day's detail: boarding-pass tickets, alternatives, the night's lineup">
  <img src="docs/nights.png" width="30%" alt="The event calendar, browsable by date">
</p>

*Demo above: London β†’ Berlin weekends in September for 2 people β€” every
Fri→Sun window priced, with Berlin's club calendar (Resident Advisor) per
night.*

## What it does

- **Split-ticket routing** β€” direct fares plus self-transfer combos through
  your chosen hub airports (same airport, enforced minimum connection, home
  the same day), ranked by total price for your whole party.
- **Trip-shape search** β€” sweep a month for the cheapest N-night window with
  human constraints: *out on these weekdays, land by this time, home by that
  time*.
- **Overnight positioning** β€” when landing early is impossible same-day, it
  prices flying to the hub the evening before and catching the morning flight.
- **Multi-source fares** β€” Google Flights plus the airlines' own public fare
  APIs (Ryanair, easyJet, Wizz), fetched in parallel and merged, deduped by
  flight, cheapest fare wins. Each source has its own health check.
- **Price intelligence** β€” every route-date keeps a price history; results
  show a buy/wait verdict vs its own typical range (good/fair/typical/pricey)
  and a rising/falling/stable trend.
- **Watch mode** β€” set a trip shape + target price ("any 24hr window in
  September under Β£180 for 2") and get a Telegram ping the moment one
  appears. Cron-friendly.
- **Events fusion** β€” every candidate night shows who's playing, via pluggable
  providers (Resident Advisor worldwide, Ticketmaster, Skiddle, Ibiza
  Spotlight β€” or write your own in ~30 lines). `--events-filter big` turns
  the calendar into a search constraint: only windows with a tier-1 night.
- **A phone dashboard** β€” `publish` renders a static site: browse every
  window by day, tap into flights, alternatives and lineups. Host it anywhere.
- **AI-agent native** β€” an MCP server exposes the whole thing to any
  MCP-capable assistant (Claude, etc.), plus an `ask` command for plain
  English from the terminal.

## Quickstart

```bash
git clone https://github.com/teatimedev/splitfare && cd splitfare
python3 -m venv .venv && .venv/bin/pip install -e ".[mcp]"
.venv/bin/splitfare init     # your airports, hubs, party size, events provider
```

Then:

```bash
# price one out/back pair β€” direct + split combos + what's on
.venv/bin/splitfare window 2026-09-04 2026-09-06 --events

# sweep a month: cheapest 2-night windows, Friday departures, land by 2pm
.venv/bin/splitfare scan --month 2026-09 --nights 2 --out-dow fri --arrive-by 14:00

# render the phone dashboard (static site in ./site β€” host anywhere)
.venv/bin/splitfare publish --month 2026-09 --nights 2

# plain English (drives the tool via the `claude` CLI if you have Claude Code)
.venv/bin/splitfare ask "cheapest weekend next month for 2, landing before 2pm?"
```

The first scan of a month is slow (~1.5 s per route-date, politely
rate-limited). Results cache for 6 h; after that everything is instant.

### Picking your hubs

Hubs are what make split ticketing work: airports with **cheap budget-airline
service from both your home and your destination**. Think Ryanair / easyJet /
Wizz bases β€” Stansted, Luton, Barcelona, Bergamo, Vienna, Warsaw… 5–10 is the
sweet spot; every hub adds ~4 lookups per date, so more hubs = more combos
found but slower scans. The `init` wizard walks you through it.

## Using it with your AI agent (MCP)

splitfare ships an MCP server, so any MCP-capable assistant can hunt flights
for you:

```bash
claude mcp add splitfare -- /path/to/splitfare/.venv/bin/splitfare-mcp
```

Three read-only tools: `splitfare_find_flights` (one date pair, full detail),
`splitfare_scan_month` (cheapest windows across a month), `splitfare_whats_on`
(event calendar). Your agent gets structured JSON β€” routings, per-leg booking
links, connection gaps, lineups β€” and can reason about trade-offs
("Β£15 more but you'd land 6 hours earlier and Four Tet is playing").

Three things to know:

1. **Run `splitfare init` first** β€” the server reads the `config.json` next to
   the code (or at `$SPLITFARE_HOME`). Without it, tools return a setup hint.
2. Uncached calls take seconds-to-minutes; `scan_month` with `deep=0` on a
   cold cache can exceed some clients' tool timeouts β€” start with specific
   dates or `deep=4`.
3. Prices are totals for the configured party, in your configured currency.

## Configuration

`splitfare init` writes `config.json`:

| key | meaning |
|---|---|
| `origins` / `destination` | IATA code lists (several codes = searched together, e.g. all London airports) |
| `hubs` | self-transfer candidates between them |
| `adults` | party size β€” **all prices are totals for the party** |
| `currency` | ISO code; used for fetching, display, and booking links |
| `min_connect_minutes` | self-transfer buffer (default 120; separate tickets β€” bigger is safer) |
| `flight_sources` | `["google"]` or `["google", "ryanair"]` (Ryanair's public fare API as fallback) |
| `events.provider` | see table below |

`SPLITFARE_HOME` relocates config/cache/site for pip installs.

### Events providers

| provider | coverage | key needed | notes |
|---|---|---|---|
| `resident-advisor` | clubbing, worldwide | no | `area_id`: ibiza 25 Β· london 13 Β· berlin 34 Β· barcelona 20 (others: ra.co β†’ your city β†’ id in the network tab) |
| `ticketmaster` | concerts/festivals, worldwide | free, instant ([signup](https://developer.ticketmaster.com)) | `$TICKETMASTER_API_KEY`; set `city` in config |
| `skiddle` | UK clubbing & gigs | free ([apply](https://www.skiddle.com/api/join.php)) | `$SKIDDLE_API_KEY`; optional lat/long/radius |
| `ibiza-spotlight` | Ibiza only | no | includes door prices |
| `none` | β€” | β€” | flights only |

Adding one is a single function in [providers.py](providers.py).

### Flight sources

Primary data comes from **Google Flights** via
[fast-flights](https://github.com/AWeirdDev/flights) β€” the only free source
that aggregates all the budget carriers. On top of that, splitfare merges in
the airlines' own public (no-key) fare APIs, configured in
`"flight_sources"`:

| source | what it adds |
|---|---|
| `google` | every carrier on the route (always primary) |
| `ryanair` | Ryanair's public fare-finder; fills gaps + cheaper fare tiers |
| `easyjet` | easyJet's homepage fare-calendar endpoint (Akamai-gated; needs `EASYJET_COOKIES` from a real browser) |
| `easyjet-browser` | drives a real browser (camofox bridge) through easyJet's booking flow and scrapes results β€” works from residential IPs |
| `wizz` | Wizz Air's search API |

All enabled sources are fetched in **parallel** (per-source concurrency
budget β€” polite but ~4-5x faster than the old sequential sweep) and merged,
deduped by flight, keeping the cheapest fare. Fetches are cached; the cache
refetches automatically when you add a source that wasn't there before.

> **IP-dependent sources:** Ryanair works from anywhere, but easyJet/Wizz
> sometimes block datacenter IPs (VPSes). If a source shows as empty in the
> per-source health warning, run from a residential IP or drop it from
> `flight_sources` β€” google alone still covers those carriers via the
> aggregator. easyJet's current API (no public endpoint; Akamai-protected)
> is reverse-engineered in [docs/easyjet-api.md](docs/easyjet-api.md), with
> `EASYJET_COOKIES` session-cookie injection for residential use.

For the curious: Amadeus retired its self-service API in 2026, Kiwi's Tequila
closed to new signups, and Duffel doesn't carry Ryanair/Wizz β€” scraping the
aggregator remains the pragmatic play, which is also why this is a
*run-it-yourself* tool and not a website.

### Price history, buy/wait scoring, and watches

Every fetch appends the cheapest price to `.cache/price_history.jsonl`
(backfilled from older caches on first run). With β‰₯3 observations for a
route-date you get a **buy/wait signal** on every result:

- verdict vs the route-date's own history: `good` (≀ p25) / `fair` (≀ p50) /
  `typical` / `pricey` (> p75)
- a rising/falling/stable **trend** from recent observations

The dashboard shows "vs typical" badges and a **sparkline of the last 24
price observations** on each flight ticket.

**Watch mode** turns "run a scan" into "ping me when it happens":

```bash
splitfare watch add --month 2026-09 --nights 1 --max-total 180 --name "Sep 24hr"
splitfare watch list
splitfare watch check          # cron-friendly; alerts on new/cheaper hits only
splitfare watch remove ID
```

`watch check` scans each watch's trip shape and alerts when a window totals
at or below the target β€” only on first appearance or a β‰₯5% price drop, so
cron doesn't spam you. Alerts go to **Telegram** when configured:

```json
"alerts": { "telegram": { "bot_token_env": "TELEGRAM_BOT_TOKEN",
                           "chat_id": "123456789" } }
```

(create a bot with @BotFather; your chat id comes from @userinfobot). Without
a token, `watch check` prints to stdout and exits 0 β€” safe for cron either
way. Example cron: `0 8 * * * cd ~/projects/splitfare && .venv/bin/splitfare watch check`.

### Events as a constraint

`scan`/`publish`/`watch` take `--events-filter any|big`: only keep windows
whose night has any event, or at least one tier-1 venue (UshuaΓ―a, HΓ―, Pacha,
Amnesia, DC10, Eden…). "Cheapest 24hrs in Ibiza with a big night on" is one
command now.

## How it works, honestly

- **Any source can break.** Treat prices as estimates and book each leg
  directly with the airline β€” every result links to a matching search. The
  tool tracks per-source health (live vs empty fetches) and warns you per
  source instead of silently showing "no flights".
- **Split tickets carry real risk.** Separate bookings mean no
  missed-connection protection. The tool enforces a minimum gap and shows the
  gap on every result, but the risk is yours. Prefer long gaps.
- Be polite: requests are rate-limited (per source) and cached. Don't hammer it.

## Development

```bash
.venv/bin/pip install -e ".[dev]"
.venv/bin/pytest tests/ -q
```

PRs welcome β€” events providers for your scene, flight sources, and routing
improvements especially.

## License

MIT.