The problem
Popular Uzbek trains — Afrosiyob, Sharq, Nasaf — sell out within seconds when the railway releases new dates on eticket.railway.uz. Refreshing the booking page manually is hopeless. I built a bot that does the refreshing for me, and pings me on Telegram the instant a seat matching my filter (route, date, brand, class) becomes available.
Tech Stack Used
What it does
- Telegram mini app UI — a Svelte 5 web app served inside Telegram. Set up filters with a horizontal day-scroller, bottom-sheet station picker, and brand/class chips that respect the user's native Telegram theme (dark/light auto).
- Real-time monitoring — a background worker polls the railway's internal JSON API every ~8 seconds, deduplicating filters by
(route, date)so 100 watchers on the same route still cost one upstream call. - Sub-10-second notification latency — from a seat appearing to my phone buzzing, end-to-end.
- Production deploy on Oracle Cloud Free Tier — Caddy with auto-TLS via Let's Encrypt, systemd unit, cron-driven git auto-deploy that turns
git push→ live in under 60 seconds.
How it works (under the hood)
The site has no public API. The Angular frontend talks to internal JSON endpoints — I reverse-engineered the relevant ones by reading the bundle:
GET /api/v1/csrf-token→Set-Cookie: XSRF-TOKEN=<uuid>POST /api/v3/handbook/trains/listwithX-XSRF-TOKEN: <uuid>and a body like{ "directions": { "forward": { "date": "YYYY-MM-DD", "depStationCode": "2900000", "arvStationCode": "2900700" }}}- The response carries trains, cars, tariffs and
freeSeats— exactly enough to evaluate each user filter and trigger notifications.
The token rotates per request, so the client refreshes on 403 and retries once. Empirically the API is fine with ~5–10 s polling per unique (route, date) group; the worker also handles 429 with a 5 s back-off.
Mini app
The mini app is a standalone Svelte 5 SPA built with Vite — 17 KB gzipped JS + 2.8 KB CSS. It uses Telegram's WebApp SDK for native theme variables, MainButton, BackButton, and haptic feedback, so it feels like a first-party Telegram screen rather than an embedded webview.
Authentication uses Telegram's initData HMAC: every API request carries the signed payload as a header, the server verifies it against the bot token, and only allowlisted user IDs can read or mutate filters.
Deploy & ops
Production runs on a single Oracle Cloud Always-Free VM. The whole pipeline is a one-shot bash script — Node 22, pnpm, Caddy with auto-TLS, ufw, systemd unit. A 30-line cron script polls origin/main every minute and rolls forward when a new commit lands. Logs to /var/log/uzrail-deploy.log.
Try it
The bot is private to specific Telegram IDs (the railway API has informal rate limits per IP, so a wide-open bot would degrade). The repo is open-source — clone it, point it at your own Telegram bot token, and you have your own watcher in five minutes.
