2026 · Chrome MV3 · WXT · React 19 · TypeScript

УЗ Ticket Hunter

Chrome MV3 extension that monitors Ukrzaliznytsia tickets and auto-reserves seats the moment they appear — payment stays human.

Role
Solo engineer — architecture, implementation, release
Year
2026
Stack
Chrome MV3 · WXT · React 19 · TypeScript
УЗ Ticket Hunter — cover

The problem

Tickets on popular Ukrzaliznytsia routes sell out in minutes — and the 45-day pre-sale drop happens at times nobody wants to babysit a refresh button for. The official site has no alerts, no waitlist, no mercy. You either get lucky or you don't travel.

The approach

A Chrome MV3 extension built with WXT + React 19 + TypeScript strict that turns the browser into a patient hunter. You give it a route, a date, and preferred seats; it watches, and the instant a match appears it reserves the seat and calls you in — you complete the payment manually within the ~15-minute hold window. Deliberately human-in-the-loop: no card storage, no auto-payment, no reCAPTCHA bypass — the human solves the captcha in the tab when asked.

УЗ Ticket Hunter landing — hybrid service worker + content script architecture

Hybrid architecture: the service worker monitors while the content script books on the live tab.

How a hunt actually runs

From the user's side it's one form: stations (a combobox backed by the live UZ search endpoint), date, favourite trains, carriage types in UZ's own notation (Л/К/П/С1/С2), passengers picked from your saved UZ profile, even the "bed linen" service. Then one of two modes:

  • Monitoring — the service worker polls every 5–30 s in the background, minding its own business.
  • Scheduled — sleeps at zero cost until the exact pre-sale second (the 45-day drop opens at a known time), then fires a tight 200–500 ms sprint of requests. Built for the 09:00 ticket-drop rush.

On a match, the flow is: reserve seats → hold → create order → watch the cart queue (respecting the API's own retry_in hints) → then a loud notification, a sound from an offscreen page, and a payment tab opened for the human. The card shows "Reserved at HH:MM" — the exact minute the hold expires, because that's the number that matters when you're sprinting for a payment terminal.

If UZ throws a reCAPTCHA mid-booking, the extension brings the tab to the front and waits — you solve it, the hunt continues by itself. A 429? Back off, cool down, retry. The scheduler and the orchestrator are pure functions over timing decisions, unit-tested with WXT's fakeBrowser.

The hybrid architecture

The technically interesting part is the split:

  • Monitoring runs in the service worker, polling the app.uz.gov.ua API read-only — fast, cheap, no page needed.
  • Booking must happen in a content script on the live booking.uz.gov.ua tab, to reuse the SPA's reCAPTCHA token and session. The SW talks to the tab over an RPC bridge with a keep-alive port.
  • Authorization is read by the content script from the page (Bearer + session id) and cached in chrome.storage.session.
  • Origin headers the SW can't set via fetch are rewritten by a scoped declarativeNetRequest rule — limited to the API host and SW requests only, so it never touches the booking tab and never bypasses reCAPTCHA.
  • State lives in chrome.storage.local behind a tiny reactive store the popup, options page and SW all subscribe to — one source of truth for every screen.

The whole surface is two faces of the same UI: the popup for quick control, a full-screen options page for the same flows when you'd rather see the log wide. A second tab in the popup — "My tickets" — reads your actual UZ orders (active, future, paginated archive) with seat, price and PDF links per ticket.

Fair play by design

Polling intervals are deliberately bounded, with backoff on 429s and a pre-sale mode that sleeps efficiently until the 45-day drop, then fires a tight burst of requests. No analytics, no trackers, no backend — everything runs locally. The README states it plainly: personal use, respect the ToS, don't resell.

УЗ Ticket Hunter extension — job list, train picker, reserved-ticket view
Job list, train picker, and the reserved-ticket view.

The outcome

An open-source (MIT), privacy-first extension live on the Chrome Web Store, with a landing site at uzhunter.app. The architecture notes — endpoint maps, fixtures, timing decisions — are documented in the repo for anyone building against the UZ API responsibly.