Skip to content

Latest commit

ย 

History

85 Commits

Folders and files

NameName
Last commit message
Last commit date
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

SeatSniper

SeatSniper

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.

License: MIT CI Good first issues Runtime: Bun 1.2 Language: TypeScript discord.js v14 Storage: SQLite GitHub Stars

The DM SeatSniper sends

curl -fsSL https://raw.githubusercontent.com/Ishannaik/seatsniper/main/install.sh | bash


TL;DR

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.

Table of contents

๐ŸŽฏ Features

๐Ÿ“… 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.

๐Ÿš€ Quick start

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.sh supports 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:

  1. Invite the bot with the URL the installer prints.
  2. In Discord, run /watch and paste a BookMyShow link.
  3. Wait. The DM arrives when tickets go live.

๐Ÿณ Run with Docker

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 -d

The 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.

๐Ÿ“ฆ Manual setup

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 mode
Behind pm2
pm2 start "$HOME/.bun/bin/bun" --name seatsniper --interpreter none --time -- run src/index.ts
pm2 save

Bun auto-loads .env from the working directory.

๐ŸŽฎ Commands

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:PVR therefore stays silent when an INOX starts listing the film. It matches case-insensitively as a substring of the cinema name or its venue code, so theatre:PVR catches "PVR: Phoenix Palladium" and theatre:IMOB works if you'd rather paste a code.

Each user can hold up to 5 watches.

๐Ÿง  How it works

  1. Save. /watch parses the link, then hits BookMyShow once to confirm the movie exists. Bad links fail here, not days later.
  2. Poll. Every POLL_INTERVAL_SEC (default 600 s) the bot checks each watch. Watches on the same movie share one request.
  3. Detect. Availability comes from the showDateCode on 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.
  4. 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.

Request coalescing

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.

โš™๏ธ Configuration

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.

๐Ÿ—บ๏ธ Project layout

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

๐Ÿง— BookMyShow quirks

  • 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.

๐Ÿ”ง Troubleshooting

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

โš ๏ธ Limitations & hosting

  • 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.

๐Ÿค Contributing

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.

๐Ÿ“„ License

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.

About

Discord bot that watches BookMyShow and DMs you the moment tickets open. Self-hosted with one command. Bun + TypeScript + SQLite, no API keys.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

5 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages