A self-hosted engine that continuously tracks flight prices across user-defined routes, stores full price history, and pushes a Telegram alert when a route beats a price threshold or sets a new all-time low.
It's a personal monitoring and trend tool built on cached fare data — not a booking tool and not a live-fare search. The goal is to answer "is this route cheap right now, relative to its own history, and should I act?" without manually checking dozens of searches.
See
docs/PRD.mdfor the full product requirements, data-source rationale, and roadmap.
A long-lived worker (node-cron) wakes on a schedule and, for every active
watch:
- Expands
origins[] × destinations[]and the depart/return date windows into individual provider queries. - Fetches offers through a provider interface (Travelpayouts Data API today;
swappable by adding one file) and normalizes them to a single
FlightOffershape. - Writes every matching offer as an append-only
PriceSnapshot— current prices are never overwritten, which is what makes trend / new-low detection possible. - Compares the run's best price against the watch's stored history and threshold.
- Sends a Telegram alert for a new low or a below-threshold price — deduplicated via a DB unique constraint so the same deal never pings twice.
Snapshots are recorded even while a watch is snoozed; only alerts are suppressed.
A global daily message cap (configured in the Settings row) can throttle how
many Telegram alerts go out per day — capped deals stay eligible and re-alert once
the cap resets. The worker is stateless between runs (all state lives in Postgres)
and tolerates per-route API failures without aborting the cycle.
node-cron worker ──> FlightDataProvider ──> Travelpayouts client
│
├──> PriceSnapshot (append-only)
├──> AlertSent (dedupe-first) ──> Telegram
└── reads Watch config
- TypeScript +
tsx(ESM, run-from-source) - PostgreSQL + Prisma (single source of truth)
- node-cron scheduler
- Zod-validated environment (fails fast at boot if config is missing)
- Docker Compose for local dev and Coolify/VPS deployment
- Node.js 20+ and npm
- Docker (for the local Postgres) or any reachable PostgreSQL instance
- A Travelpayouts affiliate account → API token
- A Telegram bot token (via @BotFather) and your chat id (via @userinfobot)
npm install
cp .env.example .env # fill in TRAVELPAYOUTS_TOKEN, TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID
docker compose up -d db # or point DATABASE_URL at any Postgres
npx prisma migrate dev --name init
npm run seed # inserts one sample watch (Germany → Jordan, direct)
npm run worker:once # single fetch cycle — should snapshot + alertOnce verified, run the worker on its schedule:
npm run worker:start # runs once on boot, then on CRON_SCHEDULEAll config is environment variables, validated by src/lib/env.ts.
See .env.example for the annotated template.
| Variable | Required | Default | Description |
|---|---|---|---|
DATABASE_URL |
✅ | — | PostgreSQL connection string. |
TRAVELPAYOUTS_TOKEN |
✅ | — | Travelpayouts Data API token (Profile → API token). |
TRAVELPAYOUTS_MARKET |
uk |
Cache market (uk/us/ru…). EU routes often read cleaner under uk/us. |
|
TELEGRAM_BOT_TOKEN |
✅ | — | Bot token from @BotFather (primary alert channel). |
TELEGRAM_CHAT_ID |
✅ | — | Destination chat id from @userinfobot. |
CRON_SCHEDULE |
0 */4 * * * |
node-cron expression. Default: every 4 hours. crontab.guru | |
DEFAULT_CURRENCY |
EUR |
Default currency for new watches / fallback. | |
PROVIDER_REQUEST_DELAY_MS |
400 |
Delay between provider calls to respect rate limits. |
| Script | What it does |
|---|---|
npm run worker |
Worker in watch mode (auto-reload on source change). |
npm run worker:start |
Run the worker: once on boot, then on CRON_SCHEDULE. |
npm run worker:once |
A single fetch cycle, then exit. Useful for testing/cron-from-outside. |
npm run seed |
Insert the sample watch (idempotent — replaces by label). |
npm run prisma:generate |
Regenerate the Prisma client. |
npm run prisma:migrate |
Create/apply a dev migration. |
npm run prisma:deploy |
Apply migrations in production (migrate deploy). |
npm run prisma:studio |
Open Prisma Studio to browse/edit data. |
In v1, watches are created in the database (a dashboard UI is on the roadmap).
The fastest path is to copy src/worker/seed-example.ts
and adjust the fields, or edit rows directly via npm run prisma:studio.
A Watch supports:
- Route:
origins[]×destinations[](IATA codes) — all combinations searched. - Trip type:
ONE_WAYorRETURN. - Date windows:
departFrom/departToand (for returns)returnFrom/returnTo. A fixed date is a one-day window. - Constraints:
minStayDays,maxStops,directOnly,passengers. - Alerting:
threshold(alert at/below this, in whole currency units) +currency. - Control:
activeflag andsnoozeUntil(snapshots still recorded while snoozed).
See prisma/schema.prisma for the authoritative model.
The repo ships a docker-compose.yml with the worker +
Postgres (a dashboard service is stubbed in, commented out). On a VPS via
Coolify, point a resource at this file and supply the secrets as environment
variables. The worker container restarts cleanly because all state lives in the DB.
docker compose up -d # db + workerFor production migrations, run npm run prisma:deploy (uses migrate deploy, not
the interactive dev flow).
Prices come from Travelpayouts' cache of recent real searches (roughly the last ~48h, retained ~7 days) — not live, bookable availability. Treat alerts as a trend signal and always verify on the airline/search link before booking. This is intended and acceptable for a monitoring tool; see PRD §6 for the full rationale.
src/
worker/
index.ts # scheduler + run loop (--once for a single cycle)
run-watch.ts # core: collect → filter → snapshot → alert
seed-example.ts # sample watch
lib/
env.ts # Zod-validated environment
db.ts # Prisma client
providers/ # FlightDataProvider interface + Travelpayouts client
notify/telegram.ts
prisma/
schema.prisma # Watch / PriceSnapshot / AlertSent / Settings
migrations/
docs/PRD.md