Agent-first, read-only CLI for searching Google Flights (itineraries, price calendar, multi-city).
Project description
gfly ✈
Google Flights for agents. A read-only, JSON-first flight-search CLI an LLM can drive — no API key, no account, no OAuth.
gfly.sh · the read-only Google Flights CLI for agents
┌───────────────────────────────────────────────────────────────────────────────┐
│ ✈ G F L Y · D E P A R T U R E S READ-ONLY · NO KEY │
├───────────┬──────────────────────────────┬────────────────────┬─────────────────┤
│ GATE │ DESTINATION │ FLIGHT │ STATUS │
├───────────┼──────────────────────────────┼────────────────────┼─────────────────┤
│ search │ JFK → LHR itineraries │ gfly search │ ● ON TIME │
│ dates │ cheapest-day price calendar │ gfly dates │ ● ON TIME │
│ multi │ multi-city journeys │ gfly multi │ ● ON TIME │
│ airports │ resolve IATA codes offline │ gfly airports │ ● ON TIME │
│ doctor │ health + throttle state │ gfly doctor │ ● ON TIME │
│ schema │ machine-readable contract │ gfly schema │ ● ON TIME │
└───────────┴──────────────────────────────┴────────────────────┴─────────────────┘
Why gfly
Google has had no public flights API since QPX shut down in 2018. The community tools that fill
the gap are built for humans (Rich TUIs) or hand agents an MCP server with a self-declared-unstable
JSON shape. gfly is engineered for an LLM agent in a loop:
gfly |
the others | |
|---|---|---|
JSON by default (stable, versioned schemaVersion) |
✅ | ⚠️ "experimental" |
schema + embedded agent contract (zero external files) |
✅ | ❌ |
Semantic exit codes for the real failures (BLOCKED, SCHEMA_DRIFT, RATE_LIMITED) |
✅ | ❌ |
Token-bounded output (--limit, --offset, --select) |
✅ | ❌ |
| Read-only by design — can't book, can't mutate | ✅ | varies |
| Persistent politeness throttle (survives the fresh-process-per-call model) | ✅ | ❌ |
| Zero auth on the default backend | ✅ | varies |
[!NOTE] The default backend rides a reverse-engineered, undocumented endpoint via
fast-flights. It is fragile by nature and will break when Google changes its response — that's exactly why gfly surfacesSCHEMA_DRIFT/BLOCKEDas structured, actionable errors and ships a swappable SerpApi backend as the reliability escape hatch. See SECURITY.md and Risks.
Try it in 10 seconds (no install, no key)
uvx gfly search JFK LHR --depart 2026-08-15
That's the whole onboarding. No account, no API key — the agent (or you) just runs it.
Install
| Method | Command |
|---|---|
| uv (recommended) | uv tool install gfly |
| uvx (zero-install trial) | uvx gfly search JFK LHR --depart 2026-08-15 |
| pipx | pipx install gfly |
| pip | pip install gfly |
Requires Python ≥ 3.10. Ships fast-flights (google engine) + offline IATA data; the serpapi
backend needs no extra dependency.
Quickstart
# one-way, cheapest first — JSON straight into jq
gfly search JFK LHR --depart 2026-08-15 --sort price --json \
| jq '.itineraries[] | {price, airlines, stops, durationMinutes}'
# round-trip, business, nonstop only
gfly search SFO NRT --depart 2026-09-10 --return 2026-09-24 --cabin business --stops nonstop
# cheapest day to fly across a window (one search per day — keep it small)
gfly dates JFK LHR --depart-range 2026-08-01..2026-08-07
# multi-city
gfly multi --leg JFK:CDG:2026-08-01 --leg CDG:FCO:2026-08-05 --leg FCO:JFK:2026-08-12
# resolve airports offline (don't make the agent guess codes)
gfly airports search "london"
# paginate: page 2
gfly search JFK LHR --depart 2026-08-15 --limit 5 --offset 5 --json
Output discipline: data on stdout, every note/warning/error on stderr — so a pipe stays
clean. At a TTY you get readable tables; piped or --json, you get the stable envelope.
Backends
| Backend | Auth | Data | Notes |
|---|---|---|---|
google (default) |
none | reverse-engineered Google Flights | free, fragile, rate-limited; --proxy routes around IP blocks |
serpapi |
API key | live SerpApi JSON | multi is google-only; set the key once (below) |
echo "$SERPAPI_KEY" | gfly auth login --backend serpapi --token-stdin # → OS keyring
gfly --backend serpapi search JFK LHR --depart 2026-08-15
Authenticate (only if you choose serpapi)
gfly follows the gh model. The google backend needs nothing.
gfly auth login --backend serpapi --token-stdin # secret via STDIN, never argv
gfly auth status --backend serpapi # tests + redacts; non-zero on problems
gfly auth logout --backend serpapi # removes LOCAL credential only
- Storage order:
GFLY_SERPAPI_KEYenv → OS keyring →0600XDG file fallback (a warning prints if perms can't be secured). Secrets are never accepted via flags (they'd leak tops//proc). - Revocation is separate from logout — rotate the key at serpapi.com.
- Run
gfly doctoranytime to check auth, keyring, connectivity, and throttle state.
Rate limits & not getting banned
The google backend is scraped, so the #1 controllable ban vector is request rate. gfly ships a
persistent, cross-process politeness throttle (default --min-interval 12s) — because an agent
invokes the CLI as a fresh process each call, the throttle state lives on disk, not in memory.
It fails fast rather than hanging: when a request would be too soon, you get a structured error with
retryAfterSeconds — not a silent multi-minute sleep (which would deadlock an agent loop).
gfly search JFK LHR --depart 2026-08-15 --wait # opt INTO blocking until clear
gfly search JFK LHR --depart 2026-08-15 --min-interval 0 # disable (riskier)
gfly --backend serpapi ... # the reliability escape hatch
Politeness reduces ban risk; it doesn't eliminate it (datacenter IPs can be CAPTCHA'd regardless) —
that's what --proxy and serpapi are for.
GATE STATUS — exit codes
A first-class contract. gfly schema always prints the authoritative table.
┌──────┬──────────────────┬──────┬───────────────────────────────────────────────┐
│ CODE │ NAME │ CODE │ NAME │
├──────┼──────────────────┼──────┼───────────────────────────────────────────────┤
│ 0 │ ok │ 8 │ retryable (transient network) │
│ 2 │ usage / parse │ 10 │ config error │
│ 3 │ empty results │ 13 │ input required (--no-input hit a prompt) │
│ 4 │ auth required │ 20 │ BLOCKED (CAPTCHA/soft-block; retryAfter) │
│ 5 │ not found │ 21 │ SCHEMA_DRIFT (upstream parse broke) │
│ 7 │ rate limited │ 130 │ cancelled (SIGINT) │
└──────┴──────────────────┴──────┴───────────────────────────────────────────────┘
Errors are structured on stderr: { "error", "code", "remediation" } (+ retryAfterSeconds on
throttle/block), so an agent can back off, switch backend, or report instead of crashing.
For agents
gfly agent # prints the embedded SKILL.md — the full usage contract, in the binary
gfly schema # command tree + flags + exit codes + live safety/throttle state + env vars
Itinerary fields are an append-only contract: price, currency, airlines[],
flightNumbers[], durationMinutes, stops, layovers[]{airport,minutes}, departure, arrival,
origin, destination, co2Grams, co2DeltaPct, isBest, bookingToken. Third-party text is
fenced as untrusted by default (--no-wrap-untrusted to disable).
Risks & ToS
- The default backend uses an unofficial, undocumented endpoint. Expect periodic breakage
(surfaced as
SCHEMA_DRIFT) and rate-limiting/CAPTCHA (surfaced asBLOCKED). Pin the version. gflyis read-only — it searches, it cannot book.- The
serpapibackend is a third-party paid service with its own ToS; it's a fallback, never the sole path. See SECURITY.md for the credential threat model.
Development
uv sync --extra dev
uv run pytest -q # contract + behavior tests (incl. the schema-snapshot gate)
vhs demo/gfly.tape # regenerate the demo GIF
See CONTRIBUTING.md and AGENTS.md.
License
Dual-licensed under either of MIT or Apache-2.0 at your option.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file gfly-0.1.0.tar.gz.
File metadata
- Download URL: gfly-0.1.0.tar.gz
- Upload date:
- Size: 1.2 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.24 {"installer":{"name":"uv","version":"0.11.24","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
63bb242e047e367e552e21841275ffae2c537e707807f33fcf9851a9c87d53aa
|
|
| MD5 |
9efca3d446bd921de661dc008c71961c
|
|
| BLAKE2b-256 |
ac54df1de39c0fa04cb813344ac8423b820e0b4cbb9081ac9573aad1c82f04e6
|
File details
Details for the file gfly-0.1.0-py3-none-any.whl.
File metadata
- Download URL: gfly-0.1.0-py3-none-any.whl
- Upload date:
- Size: 35.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.24 {"installer":{"name":"uv","version":"0.11.24","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e7e337f79dc216809b3bdb7c119f1278f7a498ac2a795cd5669aff31380cc048
|
|
| MD5 |
07fed4c6026b2c8ea01658230e6d5b3e
|
|
| BLAKE2b-256 |
0ab98b7fcb3d2af7e7f248dedb7ecb50d8d1ac08e70747f8f79d450f5049bfef
|