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 ones in my browser.
agent: searches 20 destinations in parallel
# Dest City Price Route Stops Temp 1 AYT Antalya €236 Pegasus direct 0 15°C 2 NCE Nice €260 Air France via CDG 1 12°C 3 ATH Athens €331 KLM via AMS 1 13°C 4 RAK Marrakech €347 Finnair/TAP via LIS 1 20°C 5 LPA Gran Canaria €443 Norwegian direct 0 21°C 6 SIN Singapore €710 KLM via AMS 1 31°C 7 BKK Bangkok €859 Finnair/Qatar via DOH 1 33°C Opens Antalya, Gran Canaria, and Singapore on Google Flights for booking.
20 parallel flyr calls. Each search takes 1-2 seconds. 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. Run searches in parallel. Pipe JSON into jq, feed it to scripts, or let an AI agent search dozens of routes and compile results for you -- 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
Concurrent searches
Search 5 destinations at once:
for; do
&
done |
AI agent usage
flyr's structured JSON output makes it a natural tool for LLM agents. An agent can:
- Search dozens of routes in parallel
- Filter by price, stops, departure time
- Compare destinations and compile results
- Open the best options directly in a browser
Returns structured data the agent can parse and reason about -- prices, airlines, segments, times, carbon emissions, all as clean JSON.
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
-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:
--json JSON to stdout
--pretty Pretty-printed JSON to stdout
--currency <CODE> [default: USD]
--lang <CODE> [default: en]
CONNECTION:
--proxy <URL> HTTP or SOCKS5 proxy
--timeout <SECS> [default: 30]
Output
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 29 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 20 tests -- validation rules, date handling, leap years
License
GPL-3.0