A modular, fully configurable Python bot that automates the Ticketmaster ticket-buying flow using Playwright browser automation. Login, on-sale polling, the virtual waiting room, ticket selection, add-to-cart, and (optionally) end-to-end checkout are each their own swappable subsystem.
DISCLAIMER. This tool is for educational and personal use only. Automating purchases on Ticketmaster may violate their Terms of Service and the US BOTS Act. Use at your own risk. Do not use to scalp tickets.
| Subsystem | What ships | Source | Docs |
|---|---|---|---|
| Vendor adapters | TicketmasterAdapter (US/CA flow) and TicketmasterSGAdapter (Singapore flow); host-based auto-detection + plugin loader for additional vendors |
src/vendors/, src/vendors/ticketmaster/, src/vendors/ticketmaster_sg/ |
docs/vendors.md, docs/vendors_ticketmaster_sg.md, docs/architecture.md |
| Selection strategies | cheapest, best_available, section_target, price_range, multi_section, accessible, seat_quality, random_pick, composite, interactive_seatmap, resale_filter (wrapper), vfan_aware (wrapper) |
src/strategies/, src/strategies/factory.py |
docs/strategies.md |
| Notifiers | desktop toast, generic webhook, Discord embeds, Slack, Telegram (MarkdownV2), fan-out via MultiplexNotifier |
src/notifiers/, src/notifiers/multiplex.py |
docs/notifiers.md |
| Anti-detection | Cubic-Bezier mouse moves, normally-distributed human_type, pre-flight warmer, mobile/desktop profile, stealth patches |
src/humanize/, src/utils/stealth.py |
docs/humanize.md |
| Per-account proxy | Sticky or round-robin URL assignment, threaded into launch_persistent_context |
src/proxy/manager.py |
docs/humanize.md |
| Parallel multi-account | ParallelCoordinator races N runners with staggered launch, stop-on-first-success |
src/orchestrator/parallel.py |
docs/parallel.md |
| Hooks | Hook ABC, lifecycle dispatcher, ScreenshotOnFailureHook, SlowDownAfterFailureHook |
src/hooks/, src/orchestrator/lifecycle.py |
docs/hooks.md |
| Observability | Per-run artefact dir (PNG + DOM + console + HAR), JSON logs, aiosqlite history, Prometheus /metrics, FastAPI /status+/logs/tail+/stop |
src/utils/run_artifacts.py, src/utils/history.py, src/observability/metrics.py, src/observability/control.py |
docs/observability.md |
| Layered config | Defaults → --profile overlay → ${VAR} env expansion → --set key.path=value CLI, plus per-event arrays |
src/utils/config_loader.py, config/profiles/ |
docs/architecture.md |
| Selector registry | Logical-name → fallback-list YAML, accessed via locator() helper; zero inline selectors in source |
src/registry/selectors.py, config/selectors/ticketmaster.yaml |
docs/architecture.md |
| CLI | src/cli.py (argparse) → src/main.py (bootstrap) |
src/cli.py, src/main.py |
docs/architecture.md |
# 1. Set up the virtualenv and Playwright.
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
playwright install chromium
# 2. Configure the event and credentials.
cp config/accounts.yaml.example config/accounts.yaml && $EDITOR config/accounts.yaml
$EDITOR config/config.yaml # set the event URL and your preferences
# 3. Validate the config without launching a browser.
python run.py --dry-run
# 4. Print the fully-resolved config (defaults + profile + --set overlays).
python run.py --explain
# 5. First real run — keep auto-purchase off and the browser visible.
python run.py --no-headless --no-auto-purchase${VAR} placeholders in config/accounts.yaml are expanded from the
environment, including .env (auto-loaded via python-dotenv). The fallback
env vars TM_EMAIL / TM_PASSWORD activate when no accounts.yaml is
present.
The bot ships with two vendor adapters and auto-detects which one to use from the event URL:
| Event URL host | Vendor adapter | Where to look |
|---|---|---|
ticketmaster.com, www.ticketmaster.com, livenation.com |
ticketmaster (US/CA, default) |
src/vendors/ticketmaster/ |
ticketmaster.sg, *.ticketmaster.sg |
ticketmaster_sg (Singapore) |
src/vendors/ticketmaster_sg/, docs/vendors_ticketmaster_sg.md |
# Auto-detect (recommended). Vendor picked from the first event URL.
python run.py --events https://www.ticketmaster.com/event/REPLACE_WITH_ID
python run.py --events https://ticketmaster.sg/activity/detail/<gameCode>
# Explicit override. Required when you want to --dry-run or --explain
# without a live URL in the events list.
python run.py --vendor ticketmaster_sg --dry-runThe SG adapter is a worked second-vendor example covering OAuth login
on auth.ticketmaster.com (with a Singapore-specific client_id and
identity.ticketmaster.sg redirect), the Yii image CAPTCHA +
invisible reCAPTCHA Enterprise + Queue-It captcha workflow, SGD price
parsing ($, S$, SGD , SGD$), and a separate selector YAML
(config/selectors/ticketmaster_sg.yaml). See
docs/vendors_ticketmaster_sg.md for
the SG operator guide and docs/vendors.md for the
VendorAdapter contract.
Every flag below is declared in src/cli.py and documented in that
module's docstring. The --dry-run and --explain paths never launch a
browser.
# I/O paths
python run.py --config config/config.yaml
python run.py -c config/config.yaml --accounts config/accounts.yaml
python run.py -a config/accounts.yaml --account-name primary
# Vendor selection. Two adapters ship: `ticketmaster` (US/CA) and
# `ticketmaster_sg` (Singapore). When --vendor is omitted, the bot
# auto-detects by the first event URL's host: ticketmaster.sg picks
# the SG adapter, everything else picks ticketmaster. Invalid names
# exit 2 with the offending value in stderr (e.g. --vendor nope).
python run.py --vendor ticketmaster
python run.py --vendor ticketmaster_sg
# Layered config overlays
python run.py --profile fast # apply config/profiles/fast.yaml
python run.py --profile safe --set checkout.auto_purchase=false
python run.py --set tickets.quantity=4 --set tickets.max_price=300
python run.py --events https://www.ticketmaster.com/event/A --events https://...B
# Browser / checkout overrides
python run.py --headless # force headless
python run.py --no-headless # force visible
python run.py --auto-purchase # force final-click (DANGEROUS)
python run.py --no-auto-purchase # force stop-at-cart
# Parallel multi-account orchestration
python run.py --parallel --max-parallel 3 --stagger 5
# Validation-only paths
python run.py --dry-run # exit 0/2
python run.py --explain # print resolved YAML, exit 0--set accepts repeated key.path=value overrides and coerces booleans
(true|false), integers, floats, and null from the string form. --events
accepts repeated values; each value may itself be a comma-separated list, so
both --events A --events B and --events A,B work.
The configuration schema lives in src/utils/config_loader.py and is
authoritative: every shipped knob is a field on BotConfig or one of its
nested dataclasses. Top-level groups:
events— list of{url, on_sale_time, refresh_interval_seconds, strict_host}entries. The legacyevent: {...}form still loads and is normalised into a one-elementeventslist at load time.tickets—quantity(1-8),strategy(one of the names in the matrix above), plus per-strategy sub-blockssection_target,price_range,multi_section,resale_filter,vfan_aware, and a nestedinner_strategyblock for wrapper strategies.checkout—auto_purchase,price_tolerance,payment.card_last_four,delivery.preferred,delivery.allow_any.timing—page_timeout_seconds,queue_check_interval_seconds,action_delay_seconds: [min, max],max_total_runtime_seconds,hold_open_seconds, and the nestedhumanizeblock (enabled,mouse,typing).browser—headless,slow_mo_ms,user_data_dir,locale,timezone,profile(desktopormobile), and the nestedstealthblock.logging—level,file,format(richorjson), andlogging.artifacts.record_har.notifications—desktop,sound.proxy—enabled,policy(stickyorround_robin),urls.accounts— list of{email, password, name}; values may contain${VAR}placeholders.
Profile YAMLs in config/profiles/ (fast.yaml, safe.yaml) overlay a
named subset of these keys when --profile is passed. See
docs/architecture.md for the
overlay order.
INIT → LOGIN → NAVIGATE → [WAIT_SALE | QUEUE] → SELECT → CART → CHECKOUT → DONE
Each step is implemented in its own module under
src/vendors/ticketmaster/. The runner in src/vendors/ticketmaster/core.py
fires lifecycle events (declared in src/orchestrator/lifecycle.py) at every
transition so registered hooks observe — and can react to — every step.
- INIT —
src/utils/config_loader.py::load_configresolves the overlays, thensrc/main.pyinstantiates the vendor adapter from the--vendorflag. - LOGIN —
src/vendors/ticketmaster/auth.pyreuses the persistent Chromium profile undersessions/when cookies are valid; otherwise it types credentials (humanised whentiming.humanize.typing.enabled) and pauses for any captcha challenge. - NAVIGATE —
src/vendors/ticketmaster/navigator.pyopens the event URL and polls for tickets going on sale. Naiveevent.on_sale_timevalues are localised usingbrowser.timezone. - WAIT_SALE / QUEUE —
src/vendors/ticketmaster/queue.pydetects the virtual waiting room and waits for release without re-gotoing the event URL (which would invalidate the queue token). - SELECT — the configured
SelectionStrategy(see docs/strategies.md) inspects the DOM via the selector registry and clicks one row. - CART —
src/vendors/ticketmaster/cart.pysets the quantity, ticks the terms checkbox (never marketing opt-ins), and clicks Add to Cart. - CHECKOUT —
src/vendors/ticketmaster/checkout.pypicks a preferred delivery option, selects a saved card, and (whencheckout.auto_purchaseis true) re-reads the order summary to verify section/quantity/price before clicking Place Order.
When the browser is visible and checkout.auto_purchase is false the
window is held open for timing.hold_open_seconds (default 600 s) so you
can finish checkout yourself.
ticketmaster-bot/
├── run.py # Top-level CLI entry point
├── src/
│ ├── cli.py # argparse surface (all flags)
│ ├── main.py # bootstrap: config → vendor → runner
│ ├── registry/ # Generic Registry[T] + 5 instances
│ ├── vendors/
│ │ ├── base.py # VendorAdapter ABC
│ │ ├── ticketmaster/ # US/CA: auth/navigator/queue/cart/checkout/core
│ │ └── ticketmaster_sg/ # SG: SG-scoped selectors + SGD price + Queue-It
│ ├── strategies/ # 12 SelectionStrategy implementations
│ ├── notifiers/ # desktop/webhook/discord/slack/telegram + multiplex
│ ├── humanize/ # mouse/typing/warmer/profile
│ ├── proxy/ # per-account proxy manager
│ ├── hooks/ # Hook ABC + built-in hooks
│ ├── orchestrator/ # lifecycle dispatcher + ParallelCoordinator
│ ├── observability/ # Prometheus metrics + FastAPI control panel
│ └── utils/ # config_loader, logger, stealth, run_artifacts, history
├── config/
│ ├── config.yaml # Main runtime config
│ ├── profiles/ # fast.yaml, safe.yaml
│ ├── selectors/
│ │ ├── ticketmaster.yaml # US/CA logical-name → fallback list
│ │ └── ticketmaster_sg.yaml # SG logical-name → fallback list
│ └── accounts.yaml.example # Copy to accounts.yaml (gitignored)
├── docs/ # One subsystem doc per directory above
├── tests/ # pytest + pytest-asyncio + real Chromium
├── sessions/ # Playwright persistent profiles (gitignored)
├── logs/ # Runtime logs + per-run artefact dirs (gitignored)
├── requirements.txt
├── pyproject.toml
└── README.md
12 strategies ship out of the box, registered with the singleton in
src/registry/strategies.py and built from tickets: by
src/strategies/factory.py::build_strategy. Wrapper strategies
(resale_filter, vfan_aware) delegate to a leaf strategy defined under
tickets.inner_strategy. See docs/strategies.md for
the per-strategy YAML examples.
MultiplexNotifier (src/notifiers/multiplex.py) fans events out to any
combination of desktop, webhook, discord, slack, and telegram.
Channels are constructed via the registry in src/registry/notifiers.py
from the notifications: block; per-channel failures are isolated.
Per-channel payload shapes (Discord embeds, Telegram MarkdownV2, etc.) are
catalogued in docs/notifiers.md.
Two adapters ship today:
ticketmaster— the US/CA flow atsrc/vendors/ticketmaster/. The reference implementation of theVendorAdaptercontract; coversticketmaster.com(and the historic Live Nation host fallbacks).ticketmaster_sg— the Singapore flow atsrc/vendors/ticketmaster_sg/. A second worked vendor example coveringticketmaster.sg, with SG-specific OAuth login (PingFederate with a region-specificclient_id+identity.ticketmaster.sgredirect), Yii image CAPTCHA + invisible reCAPTCHA Enterprise + Queue-It handling, and an SGD price parser ($,S$,SGD,SGD$).
Picking between them is automatic: omit --vendor and the bot reads
the host of events[0].url. ticketmaster.sg (and subdomains like
www.ticketmaster.sg) selects ticketmaster_sg; everything else —
including ticketmaster.com — falls through to ticketmaster. Pass
--vendor <name> to override the auto-detection. See
docs/vendors.md for the full contract and
docs/vendors_ticketmaster_sg.md for
the SG operator guide (captcha workflow, regional payment notes, SG
configuration knobs).
Adding a third vendor is a one-package drop-in: subclass
VendorAdapter (src/vendors/base.py), return a runner with the
per-step modules wired up, register the class via the
ticketmaster_bot.vendors entry-point group or by calling
src.registry.vendors.register(name, cls) at import time, and add a
config/selectors/<vendor>.yaml for the new DOM. The Singapore
adapter is the canonical second-vendor example for both the code
layout and the host-based auto-detection table in src/main.py.
Every runner owns a HookRegistry (src/hooks/__init__.py). The
lifecycle event names live in src/orchestrator/lifecycle.py; a hook
that raises is logged and skipped so it never aborts the run.
ScreenshotOnFailureHook and SlowDownAfterFailureHook ship out of the
box; docs/hooks.md walks through writing your own.
src/humanize/mouse.py::bezier_move walks the cursor along a cubic
Bezier with normally-distributed delays; src/humanize/typing.py types
character-by-character; src/humanize/warmer.py performs a pre-flight
visit pattern; src/humanize/profile.py swaps between desktop and
mobile fingerprints. src/utils/stealth.py applies the
navigator.webdriver / WebGL / plugins / languages patches. See
docs/humanize.md for tuning knobs.
src/orchestrator/parallel.py::ParallelCoordinator races up to
--max-parallel runners, staggered by --stagger seconds; the first
runner whose run() returns True sets a shared asyncio.Event and
the coordinator cancels the rest. See
docs/parallel.md for the race semantics and the
interaction with src/proxy/manager.py.
logs/run-<UTC>-<account>/ collects screenshot.png, dom.html,
console.log, and network.har on failure. The Prometheus exporter
(src/observability/metrics.py) and the FastAPI control panel
(src/observability/control.py, exposing /status, /logs/tail, and
/stop) run side-by-side. Run history is persisted to an aiosqlite DB
via src/utils/history.py. See
docs/observability.md for endpoint shapes and
schema.
.venv/bin/python -m pytest # full suite
.venv/bin/python -m pytest -n 8 # parallel unit
.venv/bin/python -m pytest -n 4 tests/integration # parallel integration (real Chromium)
.venv/bin/python -m ruff check src tests # lint
.venv/bin/python -m ruff format src tests # format
.venv/bin/python -m mypy src --strict-optional # type checkTests drive real headless Chromium against the HTML fixtures under
tests/fixtures/. Outbound notifier tests hit real endpoints when their
credentials are present in .env (Discord/Telegram/webhook) and are
pytest.skip-ed cleanly otherwise. See AGENTS.md for the full
contributor checklist (commit conventions, recipes for adding strategies
/ notifiers / hooks / vendors, and the mock/stub audit gate).
- Credentials live in
.envorconfig/accounts.yaml(both gitignored). Reference env vars in YAML with${VAR_NAME}. - Keep
checkout.auto_purchase: falseuntil a flow is verified end-to-end. - Persistent Chromium profiles in
sessions/contain auth cookies — treat them as secrets. Stealth (src/utils/stealth.py) removes the most obvious automation tells, but modern bot detection (Imperva, Datadome, Akamai) has many other signals. Run from a residential IP, use a real account with purchase history, and don't fan out parallel sessions from the same IP.
- Login fails / treated as logged out repeatedly — run with
browser.headless: false, log in once manually, and the persistent session insessions/will reuse cookies. - Bot can't find tickets — selectors may have changed. Set
logging.level: DEBUG, inspectlogs/bot.log, and update the fallback list inconfig/selectors/ticketmaster.yaml. - Captcha during cart/checkout — the bot pauses for up to 5 minutes while you solve it in the visible browser.
event.on_sale_timerejected as naive — either include a timezone offset (2026-06-01T10:00:00-05:00) or setbrowser.timezoneso the loader can localise it.
For personal/educational use. No warranty of any kind.