flyr
Google Flights from your terminal. Single binary, no API key, no browser.
prompt: find the cheapest round-trip from Helsinki to somewhere warm, departing tomorrow. open the best one in my browser.
agent runs:
flyr search -f HEL -t AYT,NCE,ATH,RAK,LPA,SIN,BKK -d 2026-03-01 --compact --top 1 --currency EUR --return-date 2026-03-08 --open
output:
=== AYT === €236 | HEL>SAW>AYT | 7h15m | 1 stop SAW | Pegasus | Mar01 06:30>15:45 === NCE === €260 | HEL>CDG>NCE | 5h50m | 1 stop CDG | Air France | Mar01 09:10>14:00 === ATH === €331 | HEL>AMS>ATH | 8h20m | 1 stop AMS | KLM | Mar01 07:45>18:05 === RAK === €347 | HEL>LIS>RAK | 10h05m | 1 stop LIS | Finnair, TAP | Mar01 11:00>21:05 === LPA === €443 | HEL>LPA | 6h30m | nonstop | Norwegian | Mar01 08:00>14:30 === SIN === €710 | HEL>AMS>SIN | 16h45m | 1 stop AMS | KLM | Mar01 07:45>06:30 === BKK === €859 | HEL>DOH>BKK | 14h20m | 1 stop DOH | Finnair, Qatar | Mar01 17:00>11:20 Opening: https://www.google.com/travel/flights/search?tfs=GhoSCjIwMjYtMDMtMDFqBRIDSEVMcgUSA0xQQRoaEgoyMDI2LTAzLTA4agUSA0xQQXIFEgNIRUxCAQFIAZgBAQ&tfu=EgYIABAAGAA&curr=EUR&hl=en
One command, 7 destinations, ~5k tokens. No browser, no clicking, no cookie banners. Pick the flights you want and the agent opens them directly on Google Flights to book.
Why
Google Flights has no API. The website is slow, requires a browser, and you can only search one route at a time. If you want to compare 10 destinations you're clicking through 10 separate searches, waiting for each page to load, fighting cookie banners.
flyr fixes this. It's a single static binary that scrapes Google Flights directly. Search multiple destinations in one call. Pipe output into scripts, or let an AI agent search dozens of routes and compile results -- like the table above.
Built for people who book flights programmatically -- whether that's a bash loop, a Python script, or an LLM agent that can run shell commands.
Install
Requires cmake, perl, and clang for the BoringSSL build (wreq dependency).
Arch: pacman -S cmake perl clang
Usage
Agent mode
flyr is designed for LLM agents. Three flags minimize token consumption:
--compact-- one line per flight, pipe-delimited, no box-drawing characters--top N-- only return the N cheapest results-t BCN,ATH,AYT-- multi-destination in a single invocation
Before (v1.0): 7 separate invocations, ASCII tables, all results → ~44k tokens. After (v1.1): 1 invocation, compact output, top 3 per destination → ~5k tokens.
An agent can:
- Search dozens of routes in one call
- Filter by price, stops, departure time
- Compare destinations and compile results
- Open the best options directly in a browser
Multi-destination search
Comma-separate destination codes in -t:
All destinations are searched concurrently. Output is grouped by destination.
Works with --return-date (each destination gets its own return leg), --top, all output modes.
Cannot be combined with --leg (use separate invocations for multi-city itineraries).
Concurrent searches (advanced)
For more complex scenarios beyond multi-destination, you can still run parallel shell processes:
for; do
&
done |
Localization
Results adapt to any language and currency Google Flights supports:
flyr search [OPTIONS]
REQUIRED (simple mode):
-f, --from <IATA> Departure airport (3-letter IATA code)
-t, --to <IATA> Arrival airport (comma-separate for multi-destination)
-d, --date <YYYY-MM-DD> Departure date
MULTI-CITY (replaces -f/-t/-d):
--leg <"DATE FROM TO"> Flight leg, repeatable
TRIP:
--return-date <YYYY-MM-DD> Return date (auto-sets round-trip)
--trip <TYPE> one-way | round-trip | multi-city [default: one-way]
--seat <CLASS> economy | premium-economy | business | first [default: economy]
FILTERS:
--max-stops <N> 0 = nonstop only
--airlines <AA,DL,...> Comma-separated IATA codes
PASSENGERS:
--adults <N> [default: 1]
--children <N> [default: 0]
--infants-in-seat <N> [default: 0]
--infants-on-lap <N> [default: 0]
OUTPUT:
--compact One-line-per-flight (recommended for scripts and AI agents)
--top <N> Show only the N cheapest results
--json JSON to stdout
--pretty Pretty-printed JSON to stdout
--open Open results in Google Flights
--currency <CODE> [default: USD]
--lang <CODE> [default: en]
CONNECTION:
--proxy <URL> HTTP or SOCKS5 proxy
--timeout <SECS> [default: 30]
Output
Compact (recommended for agents)
$ flyr search -f HEL -t BCN -d 2026-03-01 --compact --top 3 --currency EUR
€199 | HEL>MUC>BCN | 4h45m | 1 stop MUC | Lufthansa | Mar01 12:45>17:30
€215 | HEL>ARN>BCN | 5h20m | 1 stop ARN | SAS | Mar01 08:00>13:20
€312 | HEL>BCN | 3h20m | nonstop | Finnair | Mar01 08:00>11:20
Table (default)
$ flyr search -f HEL -t BKK -d 2026-03-01 --currency EUR
┌───────────────────┬───────────┬──────────────────┬──────────────────┬────────┬────────┬────────────┬───────┐
│ Airlines │ Route │ Depart │ Arrive │ Dur. │ Stops │ Aircraft │ Price │
├───────────────────┼───────────┼──────────────────┼──────────────────┼────────┼────────┼────────────┼───────┤
│ Finnair │ HEL → BKK │ 2026-03-01 17:00 │ 2026-03-02 07:15 │ 10h 15 │ Nonstop│ Airbus A350│ €589 │
│ Turkish, THAI │ HEL → IST │ 2026-03-01 19:00 │ 2026-03-02 05:20 │ 12h 40 │ 1 (IST)│ A321, A350 │ €498 │
│ │ IST → BKK │ │ │ │ │ │ │
└───────────────────┴───────────┴──────────────────┴──────────────────┴────────┴────────┴────────────┴───────┘
JSON
|
|
|
Exit codes
| Code | Meaning |
|---|---|
| 0 | Success |
| 2 | Validation error (bad airport code, invalid date, etc.) |
| 3 | Network error (timeout, DNS, TLS, proxy) |
| 4 | Rate limited or blocked by Google |
| 5 | Unexpected HTTP status |
| 6 | Parse error (Google changed their page structure) |
In --json mode, errors are structured JSON to stdout:
In human mode, errors go to stderr.
-
Query encoding -- Flight parameters are protobuf-encoded (hand-rolled encoder, ~130 LOC) and base64-encoded into the
tfsURL parameter, matching what Google Flights expects. -
HTTP request -- Uses wreq (reqwest fork) with Chrome 137 TLS fingerprint emulation to avoid bot detection. Pre-loads GDPR consent cookies to bypass the EU consent wall.
-
HTML parsing -- Extracts the
<script class="ds:1">tag, isolates thedata:JSON payload, parses with serde_json. -
Payload navigation -- The JSON payload is deeply nested arrays. Flight data lives at
payload[3][0][i], with segments, prices, carbon data, and metadata at fixed indices. All access is safe (no panics on missing data).
The crate exposes a public API for use as a library:
use *;
use FetchOptions;
let params = QueryParams ;
params.validate?;
let result = search.await?;
for flight in &result.flights
src/
├── main.rs CLI entry point (clap)
├── lib.rs Public API: search(query, options) -> Result<SearchResult>
├── proto.rs Hand-rolled protobuf encoder (~130 LOC)
├── query.rs Query building, validation, URL param generation
├── fetch.rs HTTP client with Chrome TLS impersonation + GDPR cookies
├── parse.rs HTML script extraction + JSON payload navigation
├── model.rs All data types (Serialize + Debug + Clone)
├── table.rs Human-readable table rendering with currency symbols
└── error.rs Error types with actionable messages
tests/
├── cli_test.rs 32 tests -- arg parsing, help output, error messages, exit codes
├── parse_test.rs 13 tests -- script extraction, JSON parsing, edge cases
├── proto_test.rs 6 tests -- byte-level protobuf correctness
└── query_test.rs 23 tests -- validation rules, date handling, leap years, browser URLs
License
GPL-3.0