{"id":50723797,"url":"https://github.com/affromero/flight-finder","last_synced_at":"2026-06-10T02:04:20.676Z","repository":{"id":343083732,"uuid":"1175412846","full_name":"affromero/flight-finder","owner":"affromero","description":"Flight price tracker. Self-hosted, open source, bring your own LLM. The price trail airlines don't show you.","archived":false,"fork":false,"pushed_at":"2026-06-03T14:13:07.000Z","size":13887,"stargazers_count":93,"open_issues_count":1,"forks_count":12,"subscribers_count":3,"default_branch":"main","last_synced_at":"2026-06-03T16:07:28.500Z","etag":null,"topics":["claude","docker","flight-prices","flights","llm","nextjs","open-source","playwright","price-tracker","self-hosted","typescript","web-scraping"],"latest_commit_sha":null,"homepage":"https://flight-finder.org","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/affromero.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-03-07T17:18:13.000Z","updated_at":"2026-06-03T14:22:43.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/affromero/flight-finder","commit_stats":null,"previous_names":["affromero/fairtrail","affromero/flight-finder"],"tags_count":36,"template":false,"template_full_name":null,"purl":"pkg:github/affromero/flight-finder","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/affromero%2Fflight-finder","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/affromero%2Fflight-finder/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/affromero%2Fflight-finder/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/affromero%2Fflight-finder/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/affromero","download_url":"https://codeload.github.com/affromero/flight-finder/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/affromero%2Fflight-finder/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34133409,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-10T02:00:07.152Z","response_time":89,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["claude","docker","flight-prices","flights","llm","nextjs","open-source","playwright","price-tracker","self-hosted","typescript","web-scraping"],"created_at":"2026-06-10T02:04:19.958Z","updated_at":"2026-06-10T02:04:20.664Z","avatar_url":"https://github.com/affromero.png","language":"TypeScript","funding_links":["https://ko-fi.com/afromero"],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n\n# Flight Finder\n\n**The price trail airlines don't show you.**\n\nTrack flight prices over time. Self-hosted. Open source. Bring your own LLM.\n\n[![GitHub Release](https://img.shields.io/github/v/release/affromero/flight-finder)](https://github.com/affromero/flight-finder/releases/latest)\n[![CI](https://img.shields.io/github/actions/workflow/status/affromero/flight-finder/ci.yml?label=CI)](https://github.com/affromero/flight-finder/actions/workflows/ci.yml)\n[![Docker](https://img.shields.io/badge/Docker-deployed-2496ED?logo=docker\u0026logoColor=white)](https://github.com/affromero/flight-finder/pkgs/container/flight-finder)\n[![License: MIT](https://img.shields.io/github/license/affromero/flight-finder)](https://github.com/affromero/flight-finder/blob/main/LICENSE)\n[![TypeScript](https://img.shields.io/badge/TypeScript-strict-blue)](https://www.typescriptlang.org/)\n[![Next.js](https://img.shields.io/badge/Next.js-15-black?logo=next.js)](https://nextjs.org)\n[![Prisma](https://img.shields.io/badge/Prisma-6-2D3748?logo=prisma)](https://prisma.io)\n[![Socket](https://img.shields.io/badge/Socket-protected-blueviolet?logo=socket.dev)](https://socket.dev)\n[![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)\n[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/affromero/flight-finder/pulls)\n[![Ko-fi](https://img.shields.io/badge/Ko--fi-Support-FF5E5B?logo=ko-fi\u0026logoColor=white)](https://ko-fi.com/afromero)\n\n\u003cbr\u003e\n\n\u003cimg src=\"assets/demo.gif\" alt=\"Flight Finder -- price evolution charts cycling through JFK-\u003eCDG, LAX-\u003eNRT, ORD-\u003eFCO\" width=\"100%\"\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eCLI Demo -- headless mode with Claude Code \u0026 Codex\u003c/summary\u003e\n\u003cbr\u003e\n\u003cimg src=\"packages/cli/demo/flight-finder-demo.gif\" alt=\"Flight Finder CLI -- search with Claude Code and Codex side by side, then live price charts\" width=\"100%\"\u003e\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eScreenshots\u003c/summary\u003e\n\u003cbr\u003e\n\u003cimg src=\"assets/home.png\" alt=\"Landing page (dark)\" width=\"100%\"\u003e\n\u003cbr\u003e\u003cbr\u003e\n\u003cimg src=\"assets/chart-jfk-cdg.png\" alt=\"JFK -\u003e CDG price chart\" width=\"100%\"\u003e\n\u003c/details\u003e\n\n\u003c/div\u003e\n\n---\n\n## Migrating from Fairtrail?\n\nAlready running an install from before the rename? `fairtrail update` does everything for you: renames the database from `fairtrail` to `flight_finder`, moves `~/.fairtrail` to `~/.flight-finder`, pulls the new image, and keeps your tracked queries, prices, and settings intact. The `fairtrail` command itself keeps working as a deprecated alias through v1.0. Details and a manual fallback live in [MIGRATION.md](MIGRATION.md).\n\n## Quick Start\n\n```bash\ncurl -fsSL https://flight-finder.org/install.sh | bash\n```\n\nIf 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.\n\nOnce it finishes:\n\n1. Open [localhost:3003](http://localhost:3003)\n2. Or run `flight-finder search \"NYC to Tokyo in July under $800\"`\n3. Flight Finder starts tracking prices immediately\n\n## Why Flight Finder?\n\nAirlines 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.**\n\n\u003cdetails\u003e\n\u003csummary\u003eThe longer version\u003c/summary\u003e\n\n1. **Aggregators want you inside their app.** Google Flights and Hopper track price history internally but lock it behind your account.\n2. **\"Buy or Wait\" is more profitable than transparency.** A black-box prediction keeps you dependent on their platform.\n3. **Airlines don't want price transparency.** If you can see that a route dips 3 weeks before departure, that undermines dynamic pricing.\n\nFlight Finder exists because the data is useful to *you* -- just not to the companies that have it.\n\u003c/details\u003e\n\n### What you get\n\n- **Natural language search** -- `\"NYC to Paris around June 15 +/- 3 days\"`\n- **Price evolution charts** -- see how fares move over days and weeks\n- **Shareable links** -- send `/q/abc123` to anyone, no login required\n- **Direct booking links** -- click any data point to go straight to the airline\n- **Airline comparison** -- see which carriers are cheapening vs. getting expensive\n- **VPN price comparison** -- test the myth: do prices change when you browse from different countries?\n- **Self-hosted** -- your searches stay private, your data stays on your machine\n- **Agent-friendly API** -- hook Claude Code, Codex, or any agent into your instance\n\n## VPN Price Comparison\n\nTest the myth that VPN location affects flight prices. Flight Finder can scrape the same query from multiple countries and show the results side by side.\n\n### How it works\n\n1. An [ExpressVPN](https://www.expressvpn.com) sidecar container runs alongside Flight Finder\n2. For each scrape run, Flight Finder routes Playwright through the VPN's SOCKS5 proxy\n3. All browser signals align to the target country (see full list below)\n4. Your local (no VPN) price is always captured as a baseline\n5. The chart shows a per-country comparison view\n\n### Anti-detection: what Flight Finder does beyond switching your IP\n\nChanging your IP is not enough. Websites detect mismatches between your IP and browser signals. Flight Finder aligns everything to match the target country:\n\n| Signal | What Flight Finder does |\n|--------|-------------------|\n| **IP address** | Routed through VPN exit node via SOCKS5 proxy |\n| **Timezone** | `timezoneId` set to match the country (e.g. `Europe/Berlin` for DE) |\n| **Language** | `Accept-Language` header and `navigator.languages` aligned to locale |\n| **Geolocation** | Geolocation API returns capital city coordinates |\n| **Google hint** | `gl=` country parameter set on Google Flights URL |\n| **WebRTC** | ICE candidates blocked -- real IP never exposed via `RTCPeerConnection` |\n| **DNS** | Queries forced through the SOCKS5 proxy (`--host-resolver-rules`) |\n| **Canvas fingerprint** | Subtle pixel noise injected per session to randomize `toDataURL` hash |\n| **WebGL fingerprint** | Unmasked renderer/vendor strings spoofed via `WEBGL_debug_renderer_info` |\n| **AudioContext** | Micro-noise added to `getFloatFrequencyData` output |\n| **Screen dimensions** | `screen.width/height`, `outerWidth/Height`, `availWidth/Height` matched to viewport |\n| **Exit verification** | After connecting, exit IP is geolocated to verify the country matches |\n\n### Setup\n\n1. During install, say **yes** to \"Set up ExpressVPN?\" and paste your [activation code](https://www.expressvpn.com/setup) -- or paste it later in **Settings**\n2. The VPN sidecar starts automatically with Flight Finder (no extra commands needed)\n3. When creating a new tracker, toggle **\"Compare prices from different countries\"** and pick which countries to compare\n4. Each scrape run: local baseline first, then each VPN country sequentially\n5. On the chart page, use the **view filter** to switch between:\n   - All countries (full detail)\n   - Country comparison (cheapest price per country over time)\n   - Local only / individual country isolation\n\n\u003cdetails\u003e\n\u003csummary\u003edocker-compose.vpn.yml details\u003c/summary\u003e\n\nThe VPN sidecar uses [`misioslav/expressvpn`](https://hub.docker.com/r/misioslav/expressvpn) and exposes:\n- SOCKS5 proxy on port 1080 (internal, used by Playwright)\n- REST API on port 8000 (internal, used by Flight Finder to switch countries)\n\nRequirements:\n- `EXPRESSVPN_CODE` in `~/.flight-finder/.env`\n- Docker host must have `/dev/net/tun` (kernel TUN module)\n- The sidecar needs `NET_ADMIN` capability\n\nOnly Playwright traffic goes through the VPN. Database, Redis, and web UI traffic stay on normal Docker networking.\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eSupported countries\u003c/summary\u003e\n\nUS, GB, DE, FR, ES, IT, NL, IE, JP, KR, IN, AU, CA, MX, BR, AR, CO, TH, SG, HK\n\nEach country profile aligns: locale, timezone, Accept-Language header, and geolocation to match the VPN exit point. Currency stays user-controlled (independent from VPN country).\n\u003c/details\u003e\n\n## Requirements\n\n- [Node.js](https://nodejs.org/) \u003e= 22 (for local development; not needed for the Docker install path)\n- [Docker Desktop](https://docs.docker.com/get-docker/)\n- One of:\n  - [Claude Code](https://docs.anthropic.com/en/docs/claude-code) (free with Claude Pro/Max)\n  - [Codex](https://github.com/openai/codex) (free with ChatGPT Pro)\n  - An API key from Anthropic, OpenAI, or Google\n  - [Ollama](https://ollama.com), [llama.cpp](https://github.com/ggml-org/llama.cpp), or [vLLM](https://docs.vllm.ai)\n\n\u003cdetails\u003e\n\u003csummary\u003eLLM Providers\u003c/summary\u003e\n\nFlight Finder needs an LLM for two things: parsing natural language queries and extracting price data from Google Flights pages.\n\n| Provider | Auth | Cost | Notes |\n|----------|------|------|-------|\n| **Claude Code** | Auto-detected (host `~/.claude`) | Free (Pro/Max plan) | Subscription CLI |\n| **Codex CLI** | Auto-detected (host `~/.codex`) | Free (ChatGPT Pro) | Subscription CLI |\n| **Anthropic** | `ANTHROPIC_API_KEY` | Pay-per-token | Claude Haiku 4.5 (default) |\n| **OpenAI** | `OPENAI_API_KEY` | Pay-per-token | GPT-4.1 Mini |\n| **Google** | `GOOGLE_AI_API_KEY` | Pay-per-token | Gemini 2.5 Flash |\n| **Ollama** | None (local) | Free | Select in admin UI |\n| **llama.cpp** | None (local) | Free | Select in admin UI |\n| **vLLM** | None (local) | Free | GPU-accelerated (port 8000) |\n| **OpenAI + custom URL** | `OPENAI_BASE_URL` | Varies | OpenRouter or any OpenAI-compatible endpoint |\n\n**Three ways to use Flight Finder:**\n\n- **Subscription users** (Claude Pro/Max, ChatGPT Pro) -- auto-detected, auth tokens mounted read-only.\n- **API key users** -- paste a key, passed via env var, never written to disk.\n- **Local model users** -- select Ollama/llama.cpp/vLLM in the admin UI, type your model ID.\n\n**Picking a local model (Ollama, llama.cpp, vLLM):**\n\nThe parse step needs a model that follows strict JSON instructions. Tiny models tend to ramble or refuse. Stick with current generation families that have reliable structured output. Qwen3 / Qwen3.5 currently have the most stable tool calling and JSON behaviour in the small model class; Gemma 3n / Gemma 4 are strong alternatives with native function calling on the laptop tier.\n\n* **CPU only, tight RAM**: `qwen3:0.6b` (523MB) or `qwen3.5:0.8b` (1.0GB). JSON mode is forced server side, so even these can produce parseable output.\n* **CPU only, typical desktop**: `qwen3:1.7b` (1.4GB) or `qwen3:4b` (2.5GB, sweet spot if you have the RAM).\n* **CPU or GPU edge (5 to 8GB)**: `gemma3n:e2b` (5.6GB, 32K context) or `gemma4:e2b` (7.2GB, 128K context, newer).\n* **GPU (8GB+ VRAM)**: `qwen3.5:9b` (6.6GB, best JSON quality and speed balance), `qwen3:8b` (5.2GB), or `gemma4:e4b` (9.6GB, native function calling).\n\nAvoid models under 1B (TinyLlama, etc.) and older generations (Llama 3.x, Qwen 2.5). They tend to ramble even with JSON mode forcing valid syntax, because the field values still need to be semantically correct. For slow CPUs, bump `EXTRACT_TIMEOUT_MS` in `.env` if larger models keep timing out (default 90000).\n\u003c/details\u003e\n\n## How It Works\n\n```\nYou type: \"SFO to Tokyo sometime in July +/- 5 days\"\n                        |\n                        v\n              +------------------+\n              |   LLM Parser     |  Extracts origin, destination,\n              |  (Claude/GPT)    |  date range, flexibility\n              +--------+---------+\n                       |\n                       v\n              +------------------+\n              |   Playwright     |  Navigates Google Flights\n              |   (headless)     |  with your exact query\n              +--------+---------+\n                       |\n                       v\n              +------------------+\n              |  LLM Extractor   |  Reads the page, extracts\n              |  (configurable)  |  structured price data\n              +--------+---------+\n                       |\n                       v\n              +------------------+\n              |   PostgreSQL     |  Stores price snapshots\n              |   + Prisma      |  with timestamps\n              +--------+---------+\n                       |\n                       v\n              +------------------+\n              |  Plotly.js       |  Interactive chart at\n              |  /q/[id]        |  a shareable public URL\n              +------------------+\n```\n\nThe 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.\n\n## Scraping Constraints\n\nFlight Finder walks an ordered chain of price sources per query. The chain is admin allowlisted and per user orderable. Each source has different reliability:\n\n| Source           | Default | Reliability     | Notes |\n|------------------|---------|-----------------|-------|\n| Google Flights   | on      | High            | Three URL rotation + stealth context. Rate limit kicks in around 30 sustained requests per IP. |\n| Airline direct   | on      | High when supported | URL templates in `airline-urls.ts`. Falls through to the next source when an airline returns a stub page. |\n| Skyscanner       | off     | **Experimental** (40 to 70 percent in burst, drops under sustained load) | Cloudflare interstitials + bot detection. v1 is best effort. |\n| Kayak            | off     | **Experimental** (similar to Skyscanner) | PerimeterX bot detection. v1 is best effort. |\n\nSkyscanner and Kayak are off by default. Admin enables them in `/admin/config`; users then order them in `/account/settings`. When a source returns no flights the next source in the chain runs; an `all_filtered_out` result (real flights existed but query filters excluded them) short circuits the chain because changing sources cannot help.\n\nFor Skyscanner and Kayak to be production grade you would need residential proxies or paid CAPTCHA solving, neither of which Flight Finder ships. If those sources fail consistently for your route, leave them off.\n\n## Managing Flight Finder\n\n```\nUsage: flight-finder [command]\n\nCommands:\n  (none)       Start Flight Finder (Ctrl+C to stop)\n  search \"..\"  Search and track a flight from the terminal\n  start        Start in background\n  stop         Stop -- pauses all price tracking until you start again\n  logs         View live logs\n  status       Check if running\n  update       Pull latest version and restart\n  version      Show version and commit\n  uninstall    Remove Flight Finder and all data\n  help         Show this help\n\nAccount recovery (self hosted multi user mode):\n  reset-password \u003cusername\u003e \u003cpassword\u003e   Set a new password for a user\n  disable-accounts                       Turn multi user mode off (no login required)\n```\n\n\u003cdetails\u003e\n\u003csummary\u003eHeadless CLI\u003c/summary\u003e\n\nRun Flight Finder entirely in the terminal:\n\n```bash\nflight-finder --headless                              # Interactive search wizard\nflight-finder --headless --backend claude-code        # Use Claude Code as AI backend\nflight-finder --headless --backend codex              # Use Codex as AI backend\nflight-finder --headless --list                       # Show all tracked queries\nflight-finder --headless --view \u003cid\u003e                  # Live price chart (auto-refreshes every 30s)\nflight-finder --headless --view \u003cid\u003e --tmux           # Split grouped routes into tmux panes\n```\n\nWithout `--headless`, `--view` opens the chart in your browser and `--list` opens the admin dashboard.\n\n**Features:**\n- Natural language search, same as the web\n- Braille chart with per-airline colored trend lines\n- Live refresh with countdown bar\n- Multi-destination (\"Frankfurt to Bogota or Medellin\")\n- tmux integration for grouped routes\n- Backend selection: `--backend claude-code|codex|anthropic|openai|google|ollama|llamacpp|vllm`\n\n\u003cimg src=\"packages/cli/demo/flight-finder-demo.gif\" alt=\"Flight Finder CLI\" width=\"100%\"\u003e\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eConfiguration\u003c/summary\u003e\n\nAll settings are in `~/.flight-finder/.env` (generated by the installer):\n\n| Variable | Default | Description |\n|----------|---------|-------------|\n| `ANTHROPIC_API_KEY` | -- | Anthropic API key |\n| `OPENAI_API_KEY` | -- | OpenAI API key |\n| `OPENAI_BASE_URL` | -- | Custom endpoint (vLLM, OpenRouter) |\n| `GOOGLE_AI_API_KEY` | -- | Google AI API key |\n| `OLLAMA_HOST` | `http://localhost:11434` | Ollama server address |\n| `POSTGRES_PASSWORD` | `postgres` | Database password |\n| `ADMIN_PASSWORD` | Auto-generated | Admin panel password |\n| `CRON_ENABLED` | `true` | Enable built-in scrape scheduler |\n| `CRON_INTERVAL_HOURS` | `3` | Hours between scrape runs |\n| `HOST_PORT` | `3003` | Host port for Flight Finder |\n| `EXPRESSVPN_CODE` | -- | ExpressVPN activation code (for VPN comparison) |\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eMulti user mode (households)\u003c/summary\u003e\n\nSelf-hosting Flight Finder with your spouse, your roommates, or your whole\nfamily? Multi user mode gives each person their own login, their own\ntrackers, and their own preferences. Everyone watches their own flights\nwithout seeing each other's dashboards. You stay admin.\n\n#### When you want this\n\n- Two or more people sharing one self-hosted instance\n- Each person tracks different flights (work travel vs. personal trips)\n- Different default currencies or preferred airlines per person\n- You want the admin panel back to yourself\n\nIf you're the only user, leave it off — solo mode is simpler.\n\n#### Turning it on\n\nYou can enable multi user mode two ways:\n\n1. **During setup**: the last (optional) step of the setup wizard asks\n   \"Run Flight Finder for a household?\". Flip it on, pick a username and\n   password, and you're done.\n2. **Later from Settings**: open `/settings` -\u003e Multi user mode and\n   toggle it on. Same form, no restart needed.\n\nWhen you enable, three things happen atomically:\n\n1. Your first admin User is created (the username and password you typed)\n2. `ExtractionConfig.multiUserMode` flips to true\n3. Every existing tracker you already had is reassigned to your new\n   admin account, so nothing disappears\n\n#### Day to day\n\nOnce enabled, Flight Finder behaves like a normal multi-account app:\n\n- `/login` replaces the password-only admin form — same page for admins\n  and non-admins (post-login redirect picks `/admin` vs `/account` based\n  on the user's role)\n- Each user has `/account` showing only their own trackers\n- Each user has `/account/settings` for currency, country, preferred\n  airlines, and cabin class defaults\n- You (admin) get a new `/admin/users` page to add household members,\n  reset their passwords, promote them to admin, or delete them\n- The landing search bar is gated on a session — anonymous `POST\n  /api/queries` returns 401 so no orphan trackers leak in\n- Share links `/q/[id]` stay public — that's the whole point of a share\n  link; you can still send a chart to anyone\n\nA one-time banner on `/admin/users` reminds you to reassign any trackers\nthat got backfilled to you but actually belong to a household member.\nClick into `/admin/queries`, edit the tracker, set `userId` to the\nright person.\n\n#### What it does NOT do\n\n- It is **not** offered on flight-finder.org — the public site is single\n  tenant by design and will never have signup\n- It does **not** introduce email, password reset flows, or OAuth —\n  admin creates accounts manually and resets passwords from the panel\n- It does **not** restrict cron, the headless CLI's read views, or\n  share links — those work the same in both modes\n\n#### Locked out?\n\nForgot the admin password, or want the accounts gone entirely? Two recovery\ncommands run from the host. They exec inside the `web` container, so it has\nto be running:\n\n```bash\nflight-finder reset-password \u003cusername\u003e \u003cnew-password\u003e   # set a known password, keep accounts\nflight-finder disable-accounts                           # turn multi user mode off entirely\n```\n\n`reset-password` sets a new password for any user; log in with it, then\nmanage everyone else from `/admin/users`. `disable-accounts` flips multi\nuser mode off and clears the stored admin credential, dropping the instance\nback to solo self hosted mode where no login is required at all. Your\ntrackers survive either way. (The password you pass to `reset-password`\nis visible in your shell history and in the host process list while the\ncommand runs, so treat it as throwaway and change it once you are back in.)\n\nNever enabled multi user mode and forgot the solo admin password instead?\nSet `ADMIN_PASSWORD` in `~/.flight-finder/.env` and restart.\n\n#### Screenshots\n\n\u003cdetails\u003e\n\u003csummary\u003eSetup wizard - new \"Accounts\" step\u003c/summary\u003e\n\nThe wizard gets one new optional step at the end (self-hosted only).\nSkip it if you're solo.\n\n\u003cimg src=\"assets/accounts/03-setup-step3-accounts-skip.png\" alt=\"Setup wizard accounts step (skip)\" width=\"100%\"\u003e\n\nFlip the toggle on and the form expands for the admin username and\npassword:\n\n\u003cimg src=\"assets/accounts/04-setup-step3-accounts-fill.png\" alt=\"Setup wizard accounts step (filled)\" width=\"100%\"\u003e\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eLogin - unified form\u003c/summary\u003e\n\nOne login page for everyone. Post-login redirect picks `/admin` or\n`/account` based on whether the user is admin.\n\n\u003cimg src=\"assets/accounts/05-login-empty.png\" alt=\"Unified login form\" width=\"100%\"\u003e\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eAdmin dashboard - new \"Users\" link\u003c/summary\u003e\n\nThe admin nav gets a \"Users\" link and a Logout button in multi user\nmode (the existing self-hosted nav had no logout because there was no\nsession).\n\n\u003cimg src=\"assets/accounts/06-admin-dashboard-with-users-link.png\" alt=\"Admin dashboard with Users link\" width=\"100%\"\u003e\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eAdmin users page - backfill banner + table\u003c/summary\u003e\n\nFirst visit after enabling shows a dismissible banner with the backfill\ncount. Add new household members with the form below.\n\n\u003cimg src=\"assets/accounts/07-admin-users-empty.png\" alt=\"Admin users page with backfill banner\" width=\"100%\"\u003e\n\nAfter adding a second user:\n\n\u003cimg src=\"assets/accounts/09-admin-users-with-partner.png\" alt=\"Admin users page with two users\" width=\"100%\"\u003e\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eSettings - multi user mode section\u003c/summary\u003e\n\nOnce enabled, Settings shows a link to the user management page.\n\n\u003cimg src=\"assets/accounts/10-settings-multi-user-enabled.png\" alt=\"Settings multi user mode section\" width=\"100%\"\u003e\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eAccount - per user trackers\u003c/summary\u003e\n\nEach non-admin user sees only their own trackers. Empty state for a\nnew account looks like this:\n\n\u003cimg src=\"assets/accounts/11-account-empty-partner.png\" alt=\"Account page empty state for a non-admin user\" width=\"100%\"\u003e\n\nThe matching settings page lets each person set their own defaults:\n\n\u003cimg src=\"assets/accounts/12-account-settings-partner.png\" alt=\"Account settings form for currency, country, airlines, cabin class\" width=\"100%\"\u003e\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eLanding - welcome line for logged-in users\u003c/summary\u003e\n\nSmall \"Signed in as ...\" line replaces the silent state from solo\nmode, with quick links to `/account` and logout.\n\n\u003cimg src=\"assets/accounts/13-landing-signed-in.png\" alt=\"Landing page welcome line\" width=\"100%\"\u003e\n\n\u003c/details\u003e\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eWhy self-host instead of using flight-finder.org?\u003c/summary\u003e\n\n- **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.\n- **Your searches stay private.** No one sees what routes you're watching.\n- **You control the scrape frequency.** Default is every 3 hours. Want every hour? Change one setting.\n- **Free with Claude Code, Codex, or a local model.**\n- **Your data, your database.** Price history lives in your own Postgres.\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eCommunity Data\u003c/summary\u003e\n\nFlight Finder is fully decentralized. You run everything on your own machine.\n\n**Why share?** The price trail gets richer the more instances pool their history. When you opt in, your anonymized data points join a shared fare dataset everyone can explore, and you get community prices back on routes you have not scraped yourself. It is genuinely opt-in, reversible any time, and never touches anything personal.\n\n**flight-finder.org** aggregates anonymized price data that self-hosted instances **opt in** to share.\n\n**What gets shared (opt-in only):** route, travel date, price, currency, airline, stops, cabin class, scrape timestamp.\n\n**What is never shared:** your queries, search history, preferences, API keys, IP address, or identity.\n\nTurn on **Community Data Sharing** in Settings (or during setup) to contribute. If you run a shared hub that other instances contribute to, enable **Accept community registrations** in the same panel (off by default; rate limited and globally capped). Explore community data at [flight-finder.org/explore](https://flight-finder.org/explore).\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eAgent \u0026 CLI Integration\u003c/summary\u003e\n\nYour local instance exposes a REST API. See [`AGENTS.md`](AGENTS.md) for the full reference.\n\n```bash\n# Parse a natural language query\ncurl -s -X POST http://localhost:3003/api/parse \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"query\": \"NYC to Paris around June 15 +/- 3 days\"}' | jq .\n\n# Create a tracked query\ncurl -s -X POST http://localhost:3003/api/queries \\\n  -H \"Content-Type: application/json\" \\\n  -d '{ ... }' | jq .\n\n# Trigger an immediate scrape\ncurl -s http://localhost:3003/api/cron/scrape \\\n  -H \"Authorization: Bearer $CRON_SECRET\" | jq .\n\n# Get price data\ncurl -s http://localhost:3003/api/queries/{id}/prices | jq .\n```\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eSettings\u003c/summary\u003e\n\nAccess at `/admin` (no login required on self-hosted instances):\n\n- **Manage queries** -- pause, resume, delete, adjust scrape frequency\n- **Configure LLM** -- choose extraction provider and model\n- **Monitor costs** -- see LLM API usage per scrape run\n- **View fetch history** -- success/failure status, errors, snapshot counts\n- **VPN setup** -- paste ExpressVPN activation code, configure default countries\n\u003c/details\u003e\n\n## Development\n\nRequires Node.js \u003e= 22.\n\n```bash\nnpm install\ndocker compose up -d db redis\nnpm run db:push\nnpm run db:generate\nnpm run dev\n```\n\n\u003cdetails\u003e\n\u003csummary\u003eTech Stack\u003c/summary\u003e\n\n| Layer | Technology |\n|-------|------------|\n| Frontend | Next.js 15 (App Router), TypeScript, CSS Modules |\n| Database | PostgreSQL 16 + Prisma ORM |\n| Cache | Redis 7 (optional) |\n| Browser | Playwright (headless Chromium) |\n| LLM | Anthropic, OpenAI, Google, Claude Code, Codex, Ollama, llama.cpp, or vLLM |\n| Charts | Plotly.js (interactive) |\n| Cron | Built-in (node-cron) or external trigger |\n| VPN | ExpressVPN sidecar (Docker, SOCKS5 proxy) |\n\u003c/details\u003e\n\n## Contributing\n\nPull requests welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.\n\n\u003cdetails\u003e\n\u003csummary\u003eWhy Playwright + LLM Instead of Google's Internal API?\u003c/summary\u003e\n\nGoogle 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.\n\n**What the direct API gives you:** sub-second searches, no browser, no LLM cost.\n\n**What it costs you:**\n\n|  | Flight Finder | [fli](https://github.com/punitarani/fli) |\n|---|---|---|\n| Approach | Playwright + LLM extraction | Reverse-engineered internal API |\n| Speed | 3-10s per search | Sub-second |\n| Booking links | Yes | No |\n| Currency control | Yes (`\u0026curr=`, `\u0026gl=` params) | No |\n| Fare class / cabin | Yes | No |\n| Seats remaining | Yes | No |\n| VPN comparison | Yes (Docker sidecar) | No |\n| Price tracking | Built-in (cron + Postgres) | Manual |\n| Shareable charts | Yes (`/q/[id]`) | No |\n\nBoth 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.\n\n**Use Flight Finder if** you want to track prices over time, see trends, get booking links, and share charts.\n\n**Use [fli](https://github.com/punitarani/fli) if** you want instant programmatic lookups from scripts.\n\u003c/details\u003e\n\n## Related Projects\n\n| Project | Description |\n|---------|-------------|\n| [**fli**](https://github.com/punitarani/fli) | Google Flights API reverse-engineering (Python) |\n| [**jetlog**](https://github.com/pbogre/jetlog) | Self-hosted personal flight journal with world map and stats |\n| [**PriceToken**](https://github.com/affromero/pricetoken) | Real-time LLM pricing API, npm/PyPI packages, and live dashboard |\n| [**gitpane**](https://github.com/affromero/gitpane) | Multi-repo Git workspace dashboard for the terminal |\n| [**kin3o**](https://github.com/affromero/kin3o) | AI-powered Lottie animation generator CLI |\n\n\u003cdetails\u003e\n\u003csummary\u003eDisclaimer \u0026 Legal\u003c/summary\u003e\n\n**Flight Finder 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 Flight Finder data.** Always verify prices directly with the airline before buying.\n\nFlight Finder 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)). Flight Finder does not circumvent any login, paywall, or technical access control.\n\n**Users are solely responsible for complying with the terms of service of any website they interact with through Flight Finder.** This project is not affiliated with Google, any airline, or any travel booking platform.\n\nThis software is provided as-is for personal and educational use.\n\u003c/details\u003e\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faffromero%2Fflight-finder","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Faffromero%2Fflight-finder","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faffromero%2Fflight-finder/lists"}