An open API service indexing awesome lists of open source software.

https://github.com/affromero/fairtrail

The price trail airlines don't show you. Track flight prices over time with natural language search. Self-hosted, open source, bring your own LLM.
https://github.com/affromero/fairtrail

claude docker flight-prices flights llm nextjs open-source playwright price-tracker self-hosted typescript web-scraping

Last synced: about 2 months ago
JSON representation

The price trail airlines don't show you. Track flight prices over time with natural language search. Self-hosted, open source, bring your own LLM.

Awesome Lists containing this project

README

          

# Fairtrail

**The price trail airlines don't show you.**

Track flight prices over time. Self-hosted. Open source. Bring your own LLM.

[![GitHub Release](https://img.shields.io/github/v/release/affromero/fairtrail)](https://github.com/affromero/fairtrail/releases/latest)
[![CI](https://img.shields.io/github/actions/workflow/status/affromero/fairtrail/ci.yml?label=CI)](https://github.com/affromero/fairtrail/actions/workflows/ci.yml)
[![Docker](https://img.shields.io/badge/Docker-deployed-2496ED?logo=docker&logoColor=white)](https://github.com/affromero/fairtrail/pkgs/container/fairtrail)
[![License: MIT](https://img.shields.io/github/license/affromero/fairtrail)](https://github.com/affromero/fairtrail/blob/main/LICENSE)
[![TypeScript](https://img.shields.io/badge/TypeScript-strict-blue)](https://www.typescriptlang.org/)
[![Next.js](https://img.shields.io/badge/Next.js-15-black?logo=next.js)](https://nextjs.org)
[![Prisma](https://img.shields.io/badge/Prisma-6-2D3748?logo=prisma)](https://prisma.io)
[![Socket](https://img.shields.io/badge/Socket-protected-blueviolet?logo=socket.dev)](https://socket.dev)
[![min-release-age](https://img.shields.io/badge/min--release--age-7%20days-brightgreen)](https://docs.npmjs.com/cli/v10/using-npm/config#min-release-age)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/affromero/fairtrail/pulls)


Fairtrail -- price evolution charts cycling through JFK->CDG, LAX->NRT, ORD->FCO

CLI Demo -- headless mode with Claude Code & Codex


Fairtrail CLI -- search with Claude Code and Codex side by side, then live price charts

Screenshots


Landing page (dark)



JFK -> CDG price chart

---

## Quick Start

```bash
curl -fsSL https://fairtrail.org/install.sh | bash
```

If you have [Claude Code](https://docs.anthropic.com/en/docs/claude-code) or [Codex](https://github.com/openai/codex) installed, the setup script detects it automatically. Otherwise, it asks you to paste an API key.

Once it finishes:

1. Open [localhost:3003](http://localhost:3003)
2. Or run `fairtrail search "NYC to Tokyo in July under $800"`
3. Fairtrail starts tracking prices immediately

## Why Fairtrail?

Airlines change flight prices hundreds of times a day. They use dynamic pricing to maximize what you pay. **No one shows you the price trend because the companies with the data profit from hiding it.**

The longer version

1. **Aggregators want you inside their app.** Google Flights and Hopper track price history internally but lock it behind your account.
2. **"Buy or Wait" is more profitable than transparency.** A black-box prediction keeps you dependent on their platform.
3. **Airlines don't want price transparency.** If you can see that a route dips 3 weeks before departure, that undermines dynamic pricing.

Fairtrail exists because the data is useful to *you* -- just not to the companies that have it.

### What you get

- **Natural language search** -- `"NYC to Paris around June 15 +/- 3 days"`
- **Price evolution charts** -- see how fares move over days and weeks
- **Shareable links** -- send `/q/abc123` to anyone, no login required
- **Direct booking links** -- click any data point to go straight to the airline
- **Airline comparison** -- see which carriers are cheapening vs. getting expensive
- **VPN price comparison** -- test the myth: do prices change when you browse from different countries?
- **Self-hosted** -- your searches stay private, your data stays on your machine
- **Agent-friendly API** -- hook Claude Code, Codex, or any agent into your instance

## VPN Price Comparison

Test the myth that VPN location affects flight prices. Fairtrail can scrape the same query from multiple countries and show the results side by side.

### How it works

1. An [ExpressVPN](https://www.expressvpn.com) sidecar container runs alongside Fairtrail
2. For each scrape run, Fairtrail routes Playwright through the VPN's SOCKS5 proxy
3. All browser signals align to the target country (see full list below)
4. Your local (no VPN) price is always captured as a baseline
5. The chart shows a per-country comparison view

### Anti-detection: what Fairtrail does beyond switching your IP

Changing your IP is not enough. Websites detect mismatches between your IP and browser signals. Fairtrail aligns everything to match the target country:

| Signal | What Fairtrail does |
|--------|-------------------|
| **IP address** | Routed through VPN exit node via SOCKS5 proxy |
| **Timezone** | `timezoneId` set to match the country (e.g. `Europe/Berlin` for DE) |
| **Language** | `Accept-Language` header and `navigator.languages` aligned to locale |
| **Geolocation** | Geolocation API returns capital city coordinates |
| **Google hint** | `gl=` country parameter set on Google Flights URL |
| **WebRTC** | ICE candidates blocked -- real IP never exposed via `RTCPeerConnection` |
| **DNS** | Queries forced through the SOCKS5 proxy (`--host-resolver-rules`) |
| **Canvas fingerprint** | Subtle pixel noise injected per session to randomize `toDataURL` hash |
| **WebGL fingerprint** | Unmasked renderer/vendor strings spoofed via `WEBGL_debug_renderer_info` |
| **AudioContext** | Micro-noise added to `getFloatFrequencyData` output |
| **Screen dimensions** | `screen.width/height`, `outerWidth/Height`, `availWidth/Height` matched to viewport |
| **Exit verification** | After connecting, exit IP is geolocated to verify the country matches |

### Setup

1. During install, say **yes** to "Set up ExpressVPN?" and paste your [activation code](https://www.expressvpn.com/setup) -- or paste it later in **Settings**
2. The VPN sidecar starts automatically with Fairtrail (no extra commands needed)
3. When creating a new tracker, toggle **"Compare prices from different countries"** and pick which countries to compare
4. Each scrape run: local baseline first, then each VPN country sequentially
5. On the chart page, use the **view filter** to switch between:
- All countries (full detail)
- Country comparison (cheapest price per country over time)
- Local only / individual country isolation

docker-compose.vpn.yml details

The VPN sidecar uses [`misioslav/expressvpn`](https://hub.docker.com/r/misioslav/expressvpn) and exposes:
- SOCKS5 proxy on port 1080 (internal, used by Playwright)
- REST API on port 8000 (internal, used by Fairtrail to switch countries)

Requirements:
- `EXPRESSVPN_CODE` in `~/.fairtrail/.env`
- Docker host must have `/dev/net/tun` (kernel TUN module)
- The sidecar needs `NET_ADMIN` capability

Only Playwright traffic goes through the VPN. Database, Redis, and web UI traffic stay on normal Docker networking.

Supported countries

US, GB, DE, FR, ES, IT, NL, IE, JP, KR, IN, AU, CA, MX, BR, AR, CO, TH, SG, HK

Each country profile aligns: locale, timezone, Accept-Language header, and geolocation to match the VPN exit point. Currency stays user-controlled (independent from VPN country).

## Requirements

- [Docker Desktop](https://docs.docker.com/get-docker/)
- One of:
- [Claude Code](https://docs.anthropic.com/en/docs/claude-code) (free with Claude Pro/Max)
- [Codex](https://github.com/openai/codex) (free with ChatGPT Pro)
- An API key from Anthropic, OpenAI, or Google
- [Ollama](https://ollama.com), [llama.cpp](https://github.com/ggml-org/llama.cpp), or [vLLM](https://docs.vllm.ai)

LLM Providers

Fairtrail needs an LLM for two things: parsing natural language queries and extracting price data from Google Flights pages.

| Provider | Auth | Cost | Notes |
|----------|------|------|-------|
| **Claude Code** | Auto-detected (host `~/.claude`) | Free (Pro/Max plan) | Subscription CLI |
| **Codex CLI** | Auto-detected (host `~/.codex`) | Free (ChatGPT Pro) | Subscription CLI |
| **Anthropic** | `ANTHROPIC_API_KEY` | Pay-per-token | Claude Haiku 4.5 (default) |
| **OpenAI** | `OPENAI_API_KEY` | Pay-per-token | GPT-4.1 Mini |
| **Google** | `GOOGLE_AI_API_KEY` | Pay-per-token | Gemini 2.5 Flash |
| **Ollama** | None (local) | Free | Select in admin UI |
| **llama.cpp** | None (local) | Free | Select in admin UI |
| **vLLM** | None (local) | Free | GPU-accelerated (port 8000) |
| **OpenAI + custom URL** | `OPENAI_BASE_URL` | Varies | OpenRouter or any OpenAI-compatible endpoint |

**Three ways to use Fairtrail:**

- **Subscription users** (Claude Pro/Max, ChatGPT Pro) -- auto-detected, auth tokens mounted read-only.
- **API key users** -- paste a key, passed via env var, never written to disk.
- **Local model users** -- select Ollama/llama.cpp/vLLM in the admin UI, type your model ID.

## How It Works

```
You type: "SFO to Tokyo sometime in July +/- 5 days"
|
v
+------------------+
| LLM Parser | Extracts origin, destination,
| (Claude/GPT) | date range, flexibility
+--------+---------+
|
v
+------------------+
| Playwright | Navigates Google Flights
| (headless) | with your exact query
+--------+---------+
|
v
+------------------+
| LLM Extractor | Reads the page, extracts
| (configurable) | structured price data
+--------+---------+
|
v
+------------------+
| PostgreSQL | Stores price snapshots
| + Prisma | with timestamps
+--------+---------+
|
v
+------------------+
| Plotly.js | Interactive chart at
| /q/[id] | a shareable public URL
+------------------+
```

The built-in cron runs on a configurable interval (default: every 3h). Each run captures prices across all active queries and the chart pages update automatically.

## Managing Fairtrail

```
Usage: fairtrail [command]

Commands:
(none) Start Fairtrail (Ctrl+C to stop)
search ".." Search and track a flight from the terminal
start Start in background
stop Stop -- pauses all price tracking until you start again
logs View live logs
status Check if running
update Pull latest version and restart
version Show version and commit
uninstall Remove Fairtrail and all data
help Show this help
```

Headless CLI

Run Fairtrail entirely in the terminal:

```bash
fairtrail --headless # Interactive search wizard
fairtrail --headless --backend claude-code # Use Claude Code as AI backend
fairtrail --headless --backend codex # Use Codex as AI backend
fairtrail --headless --list # Show all tracked queries
fairtrail --headless --view # Live price chart (auto-refreshes every 30s)
fairtrail --headless --view --tmux # Split grouped routes into tmux panes
```

Without `--headless`, `--view` opens the chart in your browser and `--list` opens the admin dashboard.

**Features:**
- Natural language search, same as the web
- Braille chart with per-airline colored trend lines
- Live refresh with countdown bar
- Multi-destination ("Frankfurt to Bogota or Medellin")
- tmux integration for grouped routes
- Backend selection: `--backend claude-code|codex|anthropic|openai|google|ollama|llamacpp|vllm`

Fairtrail CLI

Configuration

All settings are in `~/.fairtrail/.env` (generated by the installer):

| Variable | Default | Description |
|----------|---------|-------------|
| `ANTHROPIC_API_KEY` | -- | Anthropic API key |
| `OPENAI_API_KEY` | -- | OpenAI API key |
| `OPENAI_BASE_URL` | -- | Custom endpoint (vLLM, OpenRouter) |
| `GOOGLE_AI_API_KEY` | -- | Google AI API key |
| `OLLAMA_HOST` | `http://localhost:11434` | Ollama server address |
| `POSTGRES_PASSWORD` | `postgres` | Database password |
| `ADMIN_PASSWORD` | Auto-generated | Admin panel password |
| `CRON_ENABLED` | `true` | Enable built-in scrape scheduler |
| `CRON_INTERVAL_HOURS` | `3` | Hours between scrape runs |
| `HOST_PORT` | `3003` | Host port for Fairtrail |
| `EXPRESSVPN_CODE` | -- | ExpressVPN activation code (for VPN comparison) |

Why self-host instead of using fairtrail.org?

- **It can't work any other way.** A centralized service scraping Google Flights gets IP-banned within days. Thousands of self-hosted instances, each making a few quiet requests from different IPs, is the only architecture that survives.
- **Your searches stay private.** No one sees what routes you're watching.
- **You control the scrape frequency.** Default is every 3 hours. Want every hour? Change one setting.
- **Free with Claude Code, Codex, or a local model.**
- **Your data, your database.** Price history lives in your own Postgres.

Community Data

Fairtrail is fully decentralized. You run everything on your own machine.

**fairtrail.org** aggregates anonymized price data that self-hosted instances **opt in** to share.

**What gets shared (opt-in only):** route, travel date, price, currency, airline, stops, cabin class, scrape timestamp.

**What is never shared:** your queries, search history, preferences, API keys, IP address, or identity.

Enable in Settings or during setup. Explore community data at [fairtrail.org/explore](https://fairtrail.org/explore).

Agent & CLI Integration

Your local instance exposes a REST API. See [`AGENTS.md`](AGENTS.md) for the full reference.

```bash
# Parse a natural language query
curl -s -X POST http://localhost:3003/api/parse \
-H "Content-Type: application/json" \
-d '{"query": "NYC to Paris around June 15 +/- 3 days"}' | jq .

# Create a tracked query
curl -s -X POST http://localhost:3003/api/queries \
-H "Content-Type: application/json" \
-d '{ ... }' | jq .

# Trigger an immediate scrape
curl -s http://localhost:3003/api/cron/scrape \
-H "Authorization: Bearer $CRON_SECRET" | jq .

# Get price data
curl -s http://localhost:3003/api/queries/{id}/prices | jq .
```

Settings

Access at `/admin` (no login required on self-hosted instances):

- **Manage queries** -- pause, resume, delete, adjust scrape frequency
- **Configure LLM** -- choose extraction provider and model
- **Monitor costs** -- see LLM API usage per scrape run
- **View fetch history** -- success/failure status, errors, snapshot counts
- **VPN setup** -- paste ExpressVPN activation code, configure default countries

## Development

```bash
npm install
docker compose up -d db redis
npm run db:push
npm run db:generate
npm run dev
```

Tech Stack

| Layer | Technology |
|-------|------------|
| Frontend | Next.js 15 (App Router), TypeScript, CSS Modules |
| Database | PostgreSQL 16 + Prisma ORM |
| Cache | Redis 7 (optional) |
| Browser | Playwright (headless Chromium) |
| LLM | Anthropic, OpenAI, Google, Claude Code, Codex, Ollama, llama.cpp, or vLLM |
| Charts | Plotly.js (interactive) |
| Cron | Built-in (node-cron) or external trigger |
| VPN | ExpressVPN sidecar (Docker, SOCKS5 proxy) |

## Contributing

Pull requests welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.

Why Playwright + LLM Instead of Google's Internal API?

Google Flights has an undocumented internal API that returns structured JSON without a browser. The [`fli`](https://github.com/punitarani/fli) project reverse-engineers it. We investigated and decided against it.

**What the direct API gives you:** sub-second searches, no browser, no LLM cost.

**What it costs you:**

| | Fairtrail | [fli](https://github.com/punitarani/fli) |
|---|---|---|
| Approach | Playwright + LLM extraction | Reverse-engineered internal API |
| Speed | 3-10s per search | Sub-second |
| Booking links | Yes | No |
| Currency control | Yes (`&curr=`, `&gl=` params) | No |
| Fare class / cabin | Yes | No |
| Seats remaining | Yes | No |
| VPN comparison | Yes (Docker sidecar) | No |
| Price tracking | Built-in (cron + Postgres) | Manual |
| Shareable charts | Yes (`/q/[id]`) | No |

Both approaches share the same risk: Google can break either one at any time. We'd rather depend on the stable, public-facing UI than on undocumented internal array positions.

**Use Fairtrail if** you want to track prices over time, see trends, get booking links, and share charts.

**Use [fli](https://github.com/punitarani/fli) if** you want instant programmatic lookups from scripts.

## Related Projects

| Project | Description |
|---------|-------------|
| [**fli**](https://github.com/punitarani/fli) | Google Flights API reverse-engineering (Python) |
| [**jetlog**](https://github.com/pbogre/jetlog) | Self-hosted personal flight journal with world map and stats |
| [**PriceToken**](https://github.com/affromero/pricetoken) | Real-time LLM pricing API, npm/PyPI packages, and live dashboard |
| [**gitpane**](https://github.com/affromero/gitpane) | Multi-repo Git workspace dashboard for the terminal |
| [**kin3o**](https://github.com/affromero/kin3o) | AI-powered Lottie animation generator CLI |

Disclaimer & Legal

**Fairtrail is an informational tool only.** Flight prices shown are scraped from third-party sources and may be inaccurate, outdated, or incomplete. Airlines change prices based on demand, search history, seat availability, and other factors. **Do not make purchasing decisions based solely on Fairtrail data.** Always verify prices directly with the airline before buying.

Fairtrail is a personal tool that scrapes publicly available flight pricing data. In the US, scraping publicly accessible websites does not violate the [Computer Fraud and Abuse Act](https://en.wikipedia.org/wiki/Computer_Fraud_and_Abuse_Act) ([*hiQ Labs v. LinkedIn*, 9th Cir. 2022](https://en.wikipedia.org/wiki/HiQ_Labs_v._LinkedIn)). Fairtrail does not circumvent any login, paywall, or technical access control.

**Users are solely responsible for complying with the terms of service of any website they interact with through Fairtrail.** This project is not affiliated with Google, any airline, or any travel booking platform.

This software is provided as-is for personal and educational use.

## License

MIT