Watch any BookMyShow page. Get a Discord DM the second tickets go on sale.
Self-host in one command. Bun + TypeScript + SQLite. No API keys, no credit card, no browser.
curl -fsSL https://raw.githubusercontent.com/Ishannaik/seatsniper/main/install.sh | bash
You tell it a BookMyShow link and a date. It polls the site, and the moment that
date is bookable you get one DM. Say date:any instead and it pings you every
time a new date unlocks or a new cinema starts showing the movie. Trim the
noise with filters: format:IMAX,4DX only pings for those screens,
theatre:PVR,INOX only for those cinemas, and days:fri,sat only on those
weekdays. (format: filters date and showtime alerts; "new cinema appeared"
alerts are not format-filtered, but they are theatre-filtered โ see
Commands.)
Works for movies, concerts, plays, any event BookMyShow lists in India.
It observes and notifies. It never buys tickets, holds seats, or fills carts.
- ๐ฏ Features
- ๐ Quick start
- ๐ณ Run with Docker
- ๐ฆ Manual setup
- ๐ฎ Commands
- ๐ง How it works
- โ๏ธ Configuration
- ๐บ๏ธ Project layout
- ๐ง BookMyShow quirks
- ๐ค Contributing
| ๐ Watch one date | One DM the moment that date opens. The watch then deletes itself. No spam. |
| ๐ฌ Subscribe to a movie | date:any = a DM every time a new date unlocks or a new cinema appears. |
| ๐ฅ Format, theatre + day filters | Only ping for what you care about: format:IMAX,4DX matches by name (ScreenX spelling is flexible, e.g. SCREENX or SCREEN X), theatre:PVR,INOX by cinema name or venue code, days:fri,sat by weekday. All three filter date and showtime alerts; new-cinema alerts are theatre-filtered but not format-filtered. |
| โ Validates at creation | The link is checked against the live site when you save it, so a broken watch fails immediately, not silently. |
| โก One request per movie | 50 watches on the same movie cost the same as 1. Coalesced polling keeps BookMyShow happy. |
| ๐ฑ User-install commands | Works in DMs and servers, installs straight to your account. |
| ๐พ SQLite, zero config | No database server, no Docker required. One file. |
One command on Linux or macOS. It installs Bun, clones the repo, prompts for your Discord credentials, registers the slash commands, and starts the bot under pm2:
curl -fsSL https://raw.githubusercontent.com/Ishannaik/seatsniper/main/install.sh | bash
install.shsupports Linux and macOS. On Windows, use the Manual setup or Docker instructions below. A datacenter VPS in India can work with Safari TLS to BookMyShow, while a residential home IP is optional, not required. See the measured findings.
Non-interactive, for CI or a box with no terminal:
DISCORD_TOKEN=... DISCORD_CLIENT_ID=... bash <(curl -fsSL https://raw.githubusercontent.com/Ishannaik/seatsniper/main/install.sh)Then:
- Invite the bot with the URL the installer prints.
- In Discord, run
/watchand paste a BookMyShow link. - Wait. The DM arrives when tickets go live.
git clone https://github.com/Ishannaik/seatsniper.git && cd seatsniper
cp .env.example .env # fill in DISCORD_TOKEN and DISCORD_CLIENT_ID
docker compose run --rm seatsniper bun run commands # register slash commands
docker compose up -dThe database lives in a named volume, so the bot survives container restarts.
There is no HTTP healthcheck. docker compose ps showing "up" only means the
process is running, not that the bot is logged in and polling. Configure
UPTIME_KUMA_PUSH_URL; the bot pings it after each poll, so a missing push
means polling stopped even if the container is still up.
bun install # installs via bun.sh
cp .env.example .env # DISCORD_TOKEN, DISCORD_CLIENT_ID
bun run commands # register slash commands
bun run start # or: bun run dev for watch modeBehind pm2
pm2 start "$HOME/.bun/bin/bun" --name seatsniper --interpreter none --time -- run src/index.ts
pm2 saveBun auto-loads .env from the working directory.
| Command | What it does |
|---|---|
/watch link:<url> date:YYYY-MM-DD |
DM once when that date opens, then delete the watch |
/watch link:<url> date:any |
Subscribe. DM when a new date or cinema appears |
/watch link:<url> format:IMAX,4DX theatre:PVR,INOX days:fri,sat after:18:00 before:23:00 |
Optional filters on any watch. Ping only for these formats, cinemas, weekdays, or start times. after:/before: are IST wall clock, 24-hour (18:00) or 12-hour (6:00 PM); before: is exclusive and the window cannot wrap past midnight |
/list |
Show your active watches |
/stop id:<n> |
Stop watch n from /list |
/help |
How the bot works |
What
format:does and does not cover. A subscription (date:any) can alert on two different things: a new date opening, and a new cinema starting to list the film.format:is enforced on the new-date half โ the bot re-fetches showtimes for each fresh date and drops the ones with no matching show. It is not enforced on the new-cinema half: fresh venues are detected by venue code alone, so a new cinema listing the film in any format still produces a DM. The filter line on that DM reflects the filters you set, not a format that was checked for the cinema.days:behaves the same way โ it applies to dates, and a cinema is not a date.
theatre:is the exception โ it is enforced on the new-cinema half. A fresh venue already carries its name and code from the same response the diff is computed from, so filtering it costs no extra request and needs none of the coalescing the format check does.theatre:PVRtherefore stays silent when an INOX starts listing the film. It matches case-insensitively as a substring of the cinema name or its venue code, sotheatre:PVRcatches "PVR: Phoenix Palladium" andtheatre:IMOBworks if you'd rather paste a code.
Each user can hold up to 5 watches.
- Save.
/watchparses the link, then hits BookMyShow once to confirm the movie exists. Bad links fail here, not days later. - Poll. Every
POLL_INTERVAL_SEC(default 600 s) the bot checks each watch. Watches on the same movie share one request. - Detect. Availability comes from the
showDateCodeon each show. BookMyShow silently serves the nearest bookable date when the one you asked for is closed, so the URL, the HTTP status, and the page header all lie. The per-show field does not. - Notify. A dated watch DMs you once, then removes itself. A subscription compares today's dates and cinemas against what it has seen before and DMs the difference.
Failure is never silence. A blocked or unparseable response throws and logs; after 3 consecutive failures the bot tells you, so a dead poller cannot sit quietly forever.
Within one poll cycle, the bot keeps a map keyed by movie (city + event).
bookableDatesCached returns the cached BookMyShow response when the same movie
is watched again, so 50 watches on one film cost roughly one request per cycle.
beginCycle() clears that map at the start of every poll so an answer is never
served across cycles.
Format filters are intentionally the exception for subscription watches: each fresh date requires an additional showtimes request. Dated watches already have their showtimes and apply the format filter in memory. This is a deliberate tradeoff, not a bug.
Coalescing reduces request volume on whatever egress you run. It is not a substitute for suitable egress: BookMyShow checks the TLS fingerprint, and its geographic restrictions can still block datacenter IPs outside India.
| Variable | Required | Default | Purpose |
|---|---|---|---|
DISCORD_TOKEN |
yes | Bot token from the Discord developer portal | |
DISCORD_CLIENT_ID |
yes | Application ID from the same page | |
DISCORD_GUILD_ID |
no | Register commands in one guild instantly instead of waiting ~1 h for global | |
POLL_INTERVAL_SEC |
no | 600 |
Seconds between poll cycles (recommend at least 120-300 for personal use) |
STAGGER_MS_MIN |
no | 2000 |
Shortest pause between two watches inside one cycle |
STAGGER_MS_MAX |
no | 5000 |
Longest pause between two watches inside one cycle |
DB_PATH |
no | seatsniper.db |
SQLite file location |
UPTIME_KUMA_PUSH_URL |
no | Uptime Kuma push URL; the bot pings it after each poll | |
BMS_TLS_PROFILE |
no | safari_ios_18_0 |
node-tls-client profile for the BookMyShow session. Rotate only between Safari/Firefox profiles if Cloudflare starts blocking (see BookMyShow quirks). Chrome profiles are rejected, not merely discouraged. An unknown or unsupported name fails at startup. |
Keep POLL_INTERVAL_SEC at or above 120-300 seconds for personal use. Faster
polls do not require a residential IP; they increase request volume and
ban/rate-limit risk on any IP class. The default of 600 remains the safe
baseline.
POLL_INTERVAL_SEC spaces the cycles apart; the stagger spaces the watches
inside one cycle. Each watch is followed by a random pause drawn uniformly from
[STAGGER_MS_MIN, STAGGER_MS_MAX). That is anti-burst insurance against looking
automated, so setting both to 0 is allowed but discouraged โ it removes the only
thing spacing your requests out, at a rate-limit and bot-signature risk. Same point as
above: it is not a residential-IP question, since fingerprint and request shape matter
more than where the traffic comes from.
An unparseable or negative value is not fatal โ the bot logs a [config] warning and
uses the default. A STAGGER_MS_MAX below STAGGER_MS_MIN is raised to the minimum,
giving a fixed delay, rather than being silently swapped.
src/
index.ts bot, poll loop, slash handlers
bms.ts BookMyShow client, URL parsing, availability
db.ts SQLite: watches, seen dates, seen venues
messages.ts Discord copy and embeds
stagger.ts inter-watch stagger config (STAGGER_MS_MIN / STAGGER_MS_MAX)
register.ts slash command registration
assets/ logo
docs/ design specs and measured findings
- Geo-fenced. Datacenter IPs outside India can be blocked. A home server in
India is the safest host. The bot speaks Safari's TLS fingerprint via
node-tls-client, which passes BookMyShow's Cloudflare checks. - No heuristics. Availability is a field comparison, not a scrape-and-guess.
The measured findings behind this live in
docs/superpowers/specs/2026-07-27-bms-access-findings.md.
| Symptom | Cause | Fix |
|---|---|---|
bun install / TLS lib missing |
Bun blocks trusted deps | bun pm trust --all |
| HTTP 403 from BookMyShow | Chrome profile, bare curl, or wrong headers | Use the Safari profile only; read the measured findings |
| "Need residential proxy?" | Prior-art myth | No for Safari TLS on a capable host; a measured Oracle datacenter works |
| Slash commands missing | Global vs guild registration | Set DISCORD_GUILD_ID for instant guild commands |
| DMs never arrive | User closed DMs | The bot falls back to a channel; open DMs with the bot |
| Integration tests fail in CI | Live BookMyShow from US runners | Keep integration.ts manual and not part of CI |
- One shared hosted instance concentrates ban risk. A single public bot is one IP ban away from going dark, and multi-tenant "forever free" bots are fragile.
- SeatSniper is observe-only. It never buys or holds tickets, by design.
When it will break: Cloudflare may harden the Safari handshake, an abused IP
can lose reputation even with a valid profile, and a BookMyShow HTML reshape
can surface as a BmsError or as silence until the parser is updated.
Bug reports, feature ideas, and PRs are welcome. Start with CONTRIBUTING.md, which covers the setup and the hard constraints the bot was built around. Report security issues privately, see SECURITY.md. New contributors can start at the good first issues label.
MIT, see LICENSE.
โญ If SeatSniper saved you a ticket, star the repo so the next person finds it.
Observes and notifies only. Never buys tickets. Made for the Friday 9 AM rush.
