Skip to content

Delisted Detection

mdeguzis edited this page Jun 27, 2026 · 2 revisions

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.

The three signals

  1. appdetails success flag. https://store.steampowered.com/api/appdetails?appids=X&filters=basic returns {"X": {"success": false}} when the storefront has no app page for that ID. This is the primary trigger but not authoritative on its own.
  2. Store page redirect. HEAD https://store.steampowered.com/app/X/ returns 200 OK for live apps and 302 Location: https://store.steampowered.com/ for delisted / banned / invalid IDs. The redirect is the second signal.
  3. 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 marked unknown so 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.

Why this works

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.

Cache schema

game-images-cache.json entries pick up the new status value:

{
  "220":    { "status": "ok",       "probed_at": "2026-06-26" },
  "1889410":{ "status": "delisted", "probed_at": "2026-06-26" },
  "12345":  { "status": "unknown",  "probed_at": "2026-06-26" }
}

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 in url
  • 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

Surfacing to the frontend

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.

ProtonDB-only stub fallback (#122)

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.

What this still does not catch

  • Region-locked apps behave like delisted from the perspective of a US-based pipeline. Steam will return success: false for 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.

Related issues

Clone this wiki locally