The same two seats for the Eagles at Sphere on December 11, 2026 (section 110, row 36, seats 4, 5) were listed on 4 marketplaces at once. Per ticket, all-in, as captured on October 18, 2026:
- $385 on TickPick
- $397 on SeatGeek
- $403 on Vivid Seats
- $406 on Gametime
Buy the pair on Gametime and you pay $42 more for the same seats.
Finding that by hand means opening 7 sites. The script below checks all 7 in one run, keeps only the listings that can actually sell you a pair, and ranks them by what you'd pay.
The cheapest pair, per marketplace
| Marketplace | Cheapest pair, per ticket | Section · row | Fee | Listings that sell 2 |
|---|---|---|---|---|
| TickPick | $385.00 | 110 · 36 | Included | 609 of 729 |
| Ticketmaster | $396.33 | FLR1 · 22 | Itemized | 43 of 52 |
| SeatGeek | $397.00 | 110 · 36 | Itemized | 795 of 933 |
| Vivid Seats | $403.00 | 110 · 36 | Itemized | 547 of 652 |
| Gametime | $406.00 | 110 · 36 | Itemized | 113 of 307 |
| StubHub | $410.58 | 110 · 36 | Included | 282 of 345 |
| GoTickets | $423.00 | 110 · 36 | Itemized | 170 of 223 |
Run it
Get an API key, save compare.py below, install requests, and run it. The command uses the first upcoming show the search finds, or the one you click:
Searching the catalog…
export TICKETS_DEV_KEY=your_api_key
python compare.py "https://www.stubhub.com/the-eagles-las-vegas-tickets-12-11-2026/event/161551904" --quantity 2 --top 8$ python compare.py "eagles sphere" --quantity 2 --top 8
The Eagles | Sphere at The Venetian Resort | 2026-12-11T20:30:00-08:00
Marketplace Captured (UTC) Matching listings
ticketmaster 2026-10-18T19:42:24 43
vividseats 2026-10-18T19:42:33 547
stubhub 2026-10-18T19:42:46 282
seatgeek 2026-10-18T19:42:57 795
tickpick 2026-10-18T19:43:12 609
gametime 2026-10-18T19:43:21 113
gotickets 2026-10-18T19:43:35 170
Cheapest 2 tickets, all-in, USD
Marketplace Section Row Per ticket For 2 Fee itemized
tickpick 110 36 385.00 770.00 no, included
tickpick 101 27 394.00 788.00 no, included
ticketmaster FLR1 22 396.33 792.66 yes
ticketmaster FLR1 17 396.33 792.66 yes
ticketmaster FLR1 18 396.33 792.66 yes
ticketmaster FLR1 19 396.33 792.66 yes
ticketmaster FLR1 20 396.33 792.66 yes
ticketmaster FLR5 22 396.33 792.66 yesPass any event name or a marketplace event URL. Add --section 110 or --row 12 to narrow it down, and --top for more rows.
"""Compare one event's live listings across ticket marketplaces.
python compare.py "<event name or URL>" --quantity 2
python compare.py "<event name or URL>" --quantity 2 --section 110
Set TICKETS_DEV_KEY to your API key. Accessible seating (wheelchair and
companion seats) is left out unless you pass --accessible.
"""
import argparse
import os
import re
import time
from concurrent.futures import ThreadPoolExecutor
import requests
API = "https://api.tickets.dev/v1"
HEADERS = {"x-api-key": os.environ["TICKETS_DEV_KEY"]}
# Marketplaces name some sections differently: the same floor section can be
# "Floor 5" on one and "5" on another. Map names you have checked to one key,
# per marketplace. Anything unmapped is compared by its own name, lowercased.
SECTION_ALIASES = {
# ("vividseats", "floor 5"): "5",
}
def find_event(query):
"""Step 1: one event, with its URL on every marketplace that lists it."""
response = requests.get(
f"{API}/events", params={"query": query}, headers=HEADERS, timeout=30
)
if not response.ok:
raise SystemExit(f"Event lookup failed: {error_code(response)}")
events = response.json()["events"]
if not events:
raise SystemExit(f"No upcoming event matches {query!r}")
if len(events) > 1:
print(f"{len(events)} events match, using the first. "
"Pass a marketplace event URL to pick one exactly.")
return events[0]
def error_code(response):
try:
return response.json()["error"]["code"]
except (ValueError, KeyError, TypeError):
return f"HTTP {response.status_code}"
def capture(source):
"""Step 2: one capture is one event on one marketplace. Retries what the
API marks retryable, such as 429 rate_limited when capacity is full, and
never raises, so one marketplace failing doesn't take the others down."""
marketplace = source["marketplace"]
wait = 0
for attempt in range(3):
time.sleep(wait)
try:
response = requests.get(
f"{API}/capture", params={"url": source["url"]}, headers=HEADERS,
timeout=120,
)
except requests.RequestException as error:
failure, wait = type(error).__name__, 10
continue
if response.ok:
return {"marketplace": marketplace, "snapshot": response.json()}
failure = error_code(response)
try:
retryable = response.json()["error"]["retryable"]
except (ValueError, KeyError, TypeError):
retryable = False
if not retryable:
break
retry_after = response.headers.get("Retry-After", "")
wait = int(retry_after) if retry_after.isdigit() else 10
return {"marketplace": marketplace, "error": failure}
def sellable(listing):
"""The quantities a buyer can actually buy. A listing of 3 that sells
as "1,3" can't sell you 2. With no split rules published, assume only
the whole listing."""
quantities = listing["sellableQuantities"].replace(" ", "").split(",")
return {int(q) for q in quantities if q.isdigit()} or {listing["quantity"]}
ACCESSIBLE = re.compile(r"accessib|wheelchair|companion", re.IGNORECASE)
def accessible(listing):
"""Wheelchair and companion seats: row WC, or said so in the listing."""
text = f"{listing.get('ticketType') or ''} {listing.get('listingNotes') or ''}"
return listing["row"].strip().upper() == "WC" or bool(ACCESSIBLE.search(text))
def normalize(marketplace, snapshot):
"""Step 3: one row per listing, with the fields the comparison needs."""
rows = []
for listing in snapshot["listings"]:
section = listing["section"].strip().casefold()
total = listing["totalPrice"]
rows.append({
"marketplace": marketplace,
"section": listing["section"],
"section_key": SECTION_ALIASES.get((marketplace, section), section),
"row": listing["row"],
"quantities": sellable(listing),
# All-in per ticket. A missing total stays missing: falling back
# to ticketPrice would rank a pre-fee price against all-in ones.
"total": total if isinstance(total, (int, float)) and total > 0 else None,
"fee_itemized": listing["fee"] > 0,
"accessible": accessible(listing),
"currency": snapshot["currency"],
})
return rows
def matches(row, quantity, section, row_label, include_accessible=False):
"""Step 4: listings that can sell this many tickets, where you want them."""
if row["accessible"] and not include_accessible:
return False
if quantity not in row["quantities"]:
return False
if section and row["section_key"] != section.strip().casefold():
return False
if row_label and row["row"].strip().casefold() != row_label.strip().casefold():
return False
return True
def main():
parser = argparse.ArgumentParser()
parser.add_argument("event", help="event name, or any marketplace event URL")
parser.add_argument("--quantity", type=int, default=2)
parser.add_argument("--section")
parser.add_argument("--row")
parser.add_argument("--top", type=int, default=10)
parser.add_argument("--accessible", action="store_true",
help="include wheelchair and companion seats")
args = parser.parse_args()
event = find_event(args.event)
print(f"{event['name']} | {event['venue']['name']} | {event['eventDateLocal']}")
# Plans cap how many captures run at once, so keep the pool small.
with ThreadPoolExecutor(max_workers=4) as pool:
results = list(pool.map(capture, event["sources"]))
comparable = []
print(f"\n{'Marketplace':<13} {'Captured (UTC)':<20} Matching listings")
for result in results:
marketplace = result["marketplace"]
if "error" in result:
print(f"{marketplace:<13} {'unavailable':<20} {result['error']}")
continue
snapshot = result["snapshot"]
rows = [r for r in normalize(marketplace, snapshot)
if matches(r, args.quantity, args.section, args.row, args.accessible)]
priced = [r for r in rows if r["total"] is not None]
comparable += priced
detail = f"{len(rows)}"
if len(priced) < len(rows):
detail += f" ({len(rows) - len(priced)} without an all-in price)"
if not snapshot["listings"] and snapshot["note"]:
detail += f" | {snapshot['note'][:60]}"
print(f"{marketplace:<13} {snapshot['capturedAt'][:19]:<20} {detail}")
if not comparable:
print("\nNo listing on any marketplace matches those filters.")
# Step 5: rank by all-in price, one currency at a time.
for currency in sorted({r["currency"] for r in comparable}):
best = sorted((r for r in comparable if r["currency"] == currency),
key=lambda r: r["total"])[:args.top]
print(f"\nCheapest {args.quantity} tickets, all-in, {currency}")
print(f"{'Marketplace':<13} {'Section':<9} {'Row':<5} "
f"{'Per ticket':>11} {f'For {args.quantity}':>11} Fee itemized")
for r in best:
print(f"{r['marketplace']:<13} {r['section']:<9} {r['row']:<5} "
f"{r['total']:>11,.2f} {r['total'] * args.quantity:>11,.2f} "
f"{'yes' if r['fee_itemized'] else 'no, included'}")
if __name__ == "__main__":
main()What it handles for you
- It counts only listings you can buy. Sellers set split rules, so a listing's quantity isn't always an amount you can order. On StubHub, the 3 tickets in section 404, row 18 sell as 1 or 3, so they can't fill an order for 2. Of 3,241 listings across the 7 marketplaces, 2,559 can sell a pair.
- It ranks on
totalPrice, the all-in price per ticket. Ticketmaster, Vivid Seats, SeatGeek, Gametime and GoTickets itemize a fee, while StubHub and TickPick show one all-in figure with the fee already inside. A listing without an all-in price is counted and left out of the ranking, because a price before fees always looks cheaper than it is. - Section names don't match across marketplaces. The same floor section is "FLR1" on Ticketmaster, "Floor 1" on Vivid Seats, "floor 1" on SeatGeek and "1" on Gametime. Add the ones you've checked to
SECTION_ALIASES. - It leaves out wheelchair and companion seats, which most buyers can't use. Pass
--accessibleto include them. - If a marketplace fails, it shows as unavailable and the rest still compare. Each marketplace shows when it was captured, and prices are ranked one currency at a time.
How the capture works
The script makes two kinds of call. /v1/events finds the event and its URL on every marketplace that lists it, and is never billed. Then one capture per marketplace returns every listing on that event page, live, in the same listing schema. For matching events, capturing and normalizing listings step by step, see how to track ticket prices across marketplaces.
What it costs
One comparison is one capture per marketplace: 7 here, so $0.35 on the Starter plan ($0.050 a capture), down to $0.035 on High Volume ($0.005). Event lookups and failed captures are never billed. If you show comparisons on a website, keep the latest capture per marketplace and refresh it on a schedule. Capturing on every page view multiplies the bill by your traffic. Plans are on the pricing page.
Going further
- For a web page, return the ranked rows as JSON from your server and render them, so the key never reaches the browser.
- To show seat maps, capture with
includeVenueMaps=true. Each snapshot then has an SVG section map whose shapes carry that marketplace's section names. Fetch the SVG on your server and send it with the rows; see the seat map API. - For price history, run it on a schedule and keep every capture, as in tracking ticket prices.
- To hear when a pair drops under your price, set up ticket price and inventory alerts.





