Watches eticket.railway.uz (O'zbekiston Temir Yo'llari) for train seats on your route, holds them the second they appear, and sends a link to your phone so you can pay from bed.
Built on the site's own JSON API — no browser automation, no Selenium, no Playwright in the hot path. A poll is one HTTP request; grabbing a seat is one call. Includes a full reference for the undocumented eticket.railway.uz API, which may be the most useful part of this repo if you're building something else against it.
Tickets for popular Uzbek routes — Toshkent → Nukus, Toshkent → Buxoro, anything to Samarqand in season — sell out the hour they open. Seats do come back: cancellations, blocks released by the railway, people whose holds expire. They appear at random, often at 3am, and they're gone in minutes.
So the bot polls every ~8 seconds. When enough seats appear together, it holds them — the same call the website makes when you press Davom etish — which locks them for ~11 minutes and blocks other buyers. Then it messages you on Telegram with a link. You open it on your phone, type your Uzcard/Humo number and the SMS code, and go back to sleep.
The bot never pays. It only holds, which is reversible and expires on its own. The card step is always a human decision.
- Seat monitor — polls one or more dates, filters by train number and carriage class
- Group-aware seat picking — prefers a whole empty compartment, otherwise the tightest cluster that still meets your bottom-berth requirement (nobody wants to give grandma a top bunk)
- Auto-hold — one API call, so it wins races against people clicking through the UI
- Pay from your phone — a mobile payment page served from your own machine
- Live hold countdown — the authoritative deadline from the server, not a guess
- One-tap release — free the seats instantly if you change your mind
- Telegram alerts — start, matches, holds, failures, and a periodic "still alive"
- Survives restarts — if the process dies overnight it resumes watching by itself
- Tickets afterwards — passengers, seats, PDF, and the verify-QR link once payment clears
git clone https://github.com/AydosJs/railway-bot.git
cd railway-bot
npm installCreate .env.local:
RAILWAY_USERNAME=998XXXXXXXXX # your eticket.railway.uz phone login
RAILWAY_PASSWORD=your-password
TELEGRAM_BOT_TOKEN=... # from @BotFather
TELEGRAM_CHAT_ID=... # from @userinfobot
# PUBLIC_URL=https://your-tunnel # only if you expose the dashboard beyond your wifiThen:
npm run build
npm run overnight # caffeinate + next start, so the Mac doesn't sleep through the dropOpen http://localhost:3000, set your route, dates and passengers, tick Auto-grab, press
Start watching.
Passenger names are matched against the saved-passengers list on your railway account, so add everyone there first. If a name doesn't match, the bot alerts you instead of holding — it will never book a stranger because of a typo.
lib/railway.ts typed API client — auth, search, seat maps, hold, cancel, payment
lib/seatpick.ts which seats to take
lib/monitor.ts the poll loop and the grab
lib/paywatch.ts the payment-status WebSocket
app/dashboard.tsx control panel
app/pay/ the phone payment flow
scripts/ the capture tooling used to reverse-engineer all of it
Three findings made this simpler than expected, all documented in API.md:
- CSRF is self-serve. The server only checks that
X-XSRF-TOKENmatches theXSRF-TOKENcookie — a UUID you generate yourself is accepted. No browser session required. - The login endpoint has no captcha. The login page does, but
POST /api/v1/auth/logintakes username and password directly and returns a JWT. - Card payment is plain JSON. No hosted checkout, no iframe, no redirect:
payme/do-payment→payme/create-card(number + MM/YY) →payme/verify-card(SMS code), with results pushed overwss://eticket.railway.uz/ws. That's why payment doesn't have to happen on the machine that made the hold — this app hosts the card form itself.
The server binds to 0.0.0.0, so anything on your wifi can reach it. The Telegram message
contains http://<your-lan-ip>:3000/pay/<orderId>. Tap it and you get the seats, the price, a live
countdown, then card → SMS → done. Away from home? Point a tunnel (ngrok, Tailscale, Cloudflare)
at port 3000 and set PUBLIC_URL.
Card details are forwarded straight to the railway payment API and never touch disk — only the last four digits appear in the activity log.
API.md documents the endpoints captured from the live site: search, seat maps, saved passengers, the order/hold lifecycle, hold deadlines and cancellation, the full Payme card chain, and the status WebSocket protocol. Each entry notes how it was verified. If you're building anything against eticket.railway.uz, start there — it will save you the week this took.
Automated booking probably violates the site's terms of service. This was written for one family buying four tickets, not for resale or scale.
- Keep polling at 8 seconds or slower (the UI enforces a 5-second floor).
- A hold is real: it ties up seats on your account and appears in your order list. Unpaid holds expire in ~11 minutes; Release frees them immediately.
- Auto-grab is off by default. Arming it is always a deliberate click.
- Don't use this to hoard inventory. Someone else needs that berth.
Provided as-is, for education and personal use. You are responsible for how you use it.
MIT — see LICENSE.
Keywords: Uzbekistan Railways API, eticket.railway.uz, O'zbekiston Temir Yo'llari, UTY, UZ railway train tickets, Toshkent Nukus Samarqand Buxoro poyezd chiptasi, train ticket bot, seat availability monitor, ticket alert, Afrosiyob booking, Payme API integration, Uzcard Humo payment, reverse engineered API, Next.js TypeScript automation.