-
Notifications
You must be signed in to change notification settings - Fork 0
Delisted Detection
How the pipeline decides a Steam app has been pulled from the store. Lives in scripts/pipeline/game_images.py.
The signal Steam exposes is not clean. appdetails?success: false fires for genuine delistings, regional restrictions, hidden / unreleased apps, and rare API hiccups, with no field to distinguish them. We borrow SteamDB's heuristic: combine multiple independent signals so a flap on one endpoint cannot mark a live game as delisted.
-
appdetails success flag.
https://store.steampowered.com/api/appdetails?appids=X&filters=basicreturns{"X": {"success": false}}when the storefront has no app page for that ID. This is the primary trigger but not authoritative on its own. -
Store page redirect.
HEAD https://store.steampowered.com/app/X/returns200 OKfor live apps and302 Location: https://store.steampowered.com/for delisted / banned / invalid IDs. The redirect is the second signal. -
Run-wide canary. Before any per-app probing we HEAD the store page for Half-Life 2 (
220). If it responds with anything other than 200, Steam's storefront is misbehaving and the whole run skips delisted detection. Apps that would have been flagged stay markedunknownso the next run can re-evaluate.
An app gets status = "delisted" in game-images-cache.json only when all three conditions hold:
- appdetails returned
success: false - store page returned 302 to the homepage
- canary was up at run start
Any single failure path produces status = "unknown" and we retry next run. We never produce false-positive delistings from a single endpoint flap.
SteamDB's "API banned" classification observes the same response pattern. We tested:
| AppID | appdetails success | Store page | Verdict |
|---|---|---|---|
| 220 Half-Life 2 | true | 200 OK | live |
| 1190460 DEATH STRANDING | true | 200 OK | live |
| 1889410 PAPA Three Kingdoms | false | 302 to home | delisted |
| 603780 Arcane preRaise | false | 302 to home | delisted |
| 999999999 (invalid) | false | 302 to home | flagged as delisted, but we only probe known IDs |
A Steam-wide storefront outage would cause 220 to also redirect, which the canary catches, and the whole run skips delisted flagging.
game-images-cache.json entries pick up the new status value:
Status meanings:
-
ok— the standard akamai header URL works, no override needed -
hashed— the akamai URL 404s but Steam returned a hashed URL we store inurl -
delisted— multi-signal confirmed the app is gone from the store -
unknown— appdetails returned false but the second signal did not corroborate, OR canary was down -
missing— legacy value; pre-multi-signal cache entries that have not been re-probed yet
enrich_search_index_with_delisted runs after build_game_images. It reads the cache, finds entries with status == "delisted", and sets column 7 of each matching row in search-index.json to true, padding to 8 columns so releaseYear at column 6 is preserved.
js/app/components/game-page.js:273 reads indexHit[7] and renders an inline DELISTED chip next to the title with a hover tooltip. The chip explains people often still own the game via family share, backups, or regional accounts, so reports remain valid.
The chip is only on the game-detail page, not on search result cards or the topbar dropdown.
Some delisted apps eventually fall out of Steam's GetAppList entirely. They are still known to ProtonDB (signal export) and people often still own them via family share or backups. Without a Steam catalog title we cannot stub them through the normal steam_catalog pass.
generate_search_index takes a protondb_signal_titles argument. After the steam_catalog stub pass, any app id in protondb_signal_titles that has not already been emitted gets a row using the ProtonDB-stored title. The same enrich_search_index_with_delisted pass then writes column 7 = true if the game-images cache also confirms it. End result: the app is searchable AND flagged when the visitor opens it.
-
Region-locked apps behave like delisted from the perspective of a US-based pipeline. Steam will return
success: falsefor some regional apps. The store page 302 also fires. We currently flag these as delisted. Acceptable for now -- if a US Steam user cannot buy the game, treating it as delisted is close to correct UX. - Hidden / private dev apps (developers can mark their app private for testing) return the same response pattern. We have not observed this in the wild for ProtonDB-known apps.
- Multi-run confirmation would catch the remaining ambiguity but requires the cache to persist across pipeline runs. Tracked in #109.
- #108 detect and label delisted Steam games -- this work
- #109 cache + rate limit -- multi-run confirmation needs the cache to survive between runs
- #117 staging verification gap -- pipeline data changes cannot be verified end-to-end on staging today
- #122 discovery of delisted games not in steam_catalog -- some delisted apps drop out of GetAppList entirely; ProtonDB signal catalog can fill that gap
{ "220": { "status": "ok", "probed_at": "2026-06-26" }, "1889410":{ "status": "delisted", "probed_at": "2026-06-26" }, "12345": { "status": "unknown", "probed_at": "2026-06-26" } }