# Steam Sales & Discount Tracker - Deals, Prices, Events (`fanndev/steam-sales-discount-tracker`) Actor

Track every Steam discount: all ~9,500 deals with end dates and deal type, Daily/Weekend Deals, free-to-keep games, regional prices in USD, sale page items and the seasonal sale and Fest calendar. Built-in price-drop and ended-deal detection.

- **URL**: https://apify.com/fanndev/steam-sales-discount-tracker.md
- **Developed by:** [Faisal Ahdan naufal](https://apify.com/fanndev) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.40 / 1,000 results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Steam Sales & Discount Tracker

Track every discount on Steam in one actor:

- All ~9,500 items on sale right now, with the exact time each discount ends and what kind of deal it is.
- The storefront's Daily Deal, Weekend Deals, Free Weekends and publisher/franchise sales.
- Free-to-keep giveaways.
- Regional prices converted to USD.
- Every item on a sale page.
- Steam's official calendar of seasonal sales and Fests.

Schedule it (for example every 6 hours) and each record also tells you **how the price changed since the previous run**. It also flags **new deals** and the **lowest price seen so far**, and lists **deals that ended**.

### What you can collect

| Mode | What you get | Requests |
|---|---|---|
| **All discounted items** (`deals`) | Every game, DLC and package on sale, filterable by tag, type, keyword, max price and minimum discount: price, original price, %, savings, review score, platforms, **discount end time**, **deal type** (daily / weekend / midweek / launch / special / named publisher campaign), developers, publishers | 1 per 100 items (+1 for details) |
| **Daily Deal & spotlights** (`featured`) | Today's Daily Deal, every storefront spotlight (Weekend Deal, Free Weekend, Franchise Sale, developer sales) with its end time and item count, and the featured specials | ~5 |
| **Free games** (`freebies`) | Free-to-keep giveaways (100% off, with the claim deadline) and Free Weekends | ~4 |
| **Regional prices** (`appPrices`) | Price and discount of your games in each country you list, converted to USD, ranked cheapest first, with the difference to the US price | 2 per country |
| **Sale page items** (`salePage`) | Every game, DLC, soundtrack, package and bundle on a publisher, franchise or developer sale page, priced, with the sale's start and end | 2 per sale |
| **Sale & Fest calendar** (`saleEvents`) | Steam's published schedule: Winter/Spring/Summer/Autumn sales, every themed Fest and Next Fest, with dates, status (upcoming / live) and days until start | 1 |

### Example input

```json
{
    "mode": "deals",
    "country": "US",
    "tags": ["Roguelike"],
    "category": "games",
    "minDiscount": 50,
    "maxPrice": 15,
    "maxItems": 10000,
    "compareWithPrevious": true,
    "alertPriceDropPct": 20
}
```

Regional price comparison:

```json
{
    "mode": "appPrices",
    "apps": ["1808500", "https://store.steampowered.com/app/632360/Risk_of_Rain_2/", "Elden Ring"],
    "countries": ["US", "GB", "DE", "BR", "TR", "IN", "ID", "AR"]
}
```

### Example output

A `DEAL` record (shortened):

```json
{
    "recordType": "DEAL",
    "country": "US",
    "position": 1,
    "totalDiscounted": 9463,
    "itemKey": "App_632360",
    "name": "Risk of Rain 2",
    "storeUrl": "https://store.steampowered.com/app/632360/Risk_of_Rain_2/",
    "discountPct": 67,
    "currency": "USD",
    "finalPrice": 8.24,
    "originalPrice": 24.99,
    "savings": 16.75,
    "reviewSummary": "Overwhelmingly Positive",
    "reviewPositivePct": 95,
    "reviewCount": 164199,
    "platforms": ["win"],
    "discountEndsAt": "2026-10-22T17:00:00Z",
    "hoursLeft": 274.6,
    "discountType": "special",
    "developers": ["Hopoo Games"],
    "publishers": ["Gearbox Publishing", "2K"],
    "discountedPurchaseOptions": [{"name": "Risk of Rain 2 + Survivors of the Void Expansion", "bundleId": 73409, "discountPct": 61, "bundleDiscountPct": 11}]
}
```

The same kind of record on a scheduled run, after a price drop:

```json
{
    "recordType": "DEAL",
    "name": "Japanese Ramen Simulator",
    "finalPriceCents": 1274,
    "previousFinalPriceCents": 1774,
    "priceChangeCents": -500,
    "priceDropPct": 28.2,
    "isNewDeal": false,
    "lowestPriceSeenCents": 1274,
    "isLowestPriceSeen": true,
    "alert": true
}
```

An `APP_PRICE` record:

```json
{
    "recordType": "APP_PRICE",
    "name": "Risk of Rain 2",
    "country": "ID",
    "currency": "IDR",
    "finalPrice": 39599.0,
    "discountPct": 67,
    "finalPriceUsd": 2.21,
    "usdRank": 1,
    "vsUsPricePct": -73.2,
    "cheapestCountry": "ID",
    "discountEndsAt": "2026-10-22T17:00:00Z"
}
```

A `SALE_EVENT` record:

```json
{
    "recordType": "SALE_EVENT",
    "eventType": "seasonal_sale",
    "name": "Winter Sale 2026",
    "startDate": "2026-12-17",
    "endDate": "2027-01-04",
    "status": "upcoming",
    "daysUntilStart": 67,
    "durationDays": 18
}
```

Record types: `DEAL`, `DAILY_DEAL`, `SPOTLIGHT`, `FEATURED_SPECIAL`, `FREEBIE`, `APP_PRICE`, `SALE_PAGE`, `SALE_ITEM`, `SALE_EVENT`, `DEAL_ENDED`, `ERROR`. Each record has `_input`, `_source` (which endpoint produced it) and `_scrapedAt`. Prices come as both amounts (`finalPrice`) and Steam's integer cents (`finalPriceCents`).

### Monitoring (change detection)

With **Compare with previous run** on (default), the actor remembers every item's price per task and adds:

- `previousFinalPriceCents`, `priceChangeCents`, `priceDropPct`. These show the change since the last run.
- `isNewDeal`: the item is discounted now but was not discounted (or not listed) last time.
- `lowestPriceSeenCents`, `lowestPriceSeenAt`, `isLowestPriceSeen`: the lowest price this task has ever recorded for the item.
- `alert`: a new deal, or a drop of at least **Price-drop alert threshold**.
- **`DEAL_ENDED` records** (in `deals` and `freebies` mode): items that were on sale last run and are gone now.

Snapshots are kept per mode **and per filter set** in the named key-value store `steam-sales-tracker-snapshots`, so tasks for different countries or tags never interfere. Ended deals are only reported after a run that read the **whole** result list. If `maxItems` cut the list short, that check is skipped, because a missing item could just be beyond the cut.

### Good to know

- **Prices are regional.** Steam runs different discounts per country (the US had 9,463 deals and Indonesia 9,593 on the same day), so `country` decides both the list and the currency. Prices follow the `country` setting, not the proxy location.
- **Seasonal sales and Fests** build their pages dynamically, so `salePage` cannot list their items. Use `deals` with the Fest's tag instead (for example `"tags": ["Cooking"]` during the Cooking Fest). Use `saleEvents` to know when they start.
- `minDiscount` and `maxPrice` are applied after download, because Steam has no discount filter. They don't reduce how many pages are read.
- About 2% of Steam's specials list is shown at 0%. These are edition upgrades, and DLC whose *bundle* is what's discounted. They are dropped by default (`minDiscount: 1`). Set `minDiscount: 0` to keep them; `discountedPurchaseOptions` then shows where the discount actually is.
- **Free Weekend** records show the game's discount end, if it is also on sale. Steam does not publish the free-play window's end time in machine-readable form.
- `sortBy: reviewsDesc` only lists reviewed items (about half of all deals). That's Steam's behaviour.
- USD conversion uses the European Central Bank's daily reference rates (about 30 currencies). Prices in other currencies keep `finalPriceUsd: null`.
- Unknown sale pages, appids and tag names come back as `ERROR` records with a clear reason. They never produce empty or made-up data.

### Technical notes

- **No WAF, no login, no browser.** Every endpoint is public: Steam store search, `api/featuredcategories`, `api/appdetails`, the sale pages, Steamworks' public "Upcoming Steam Events" page, and Valve's official keyless store API (`IStoreBrowseService/GetItems`) for discount end dates and deal types. HTTP only (`curl_cffi`, Chrome TLS profile; it rotates through a profile ladder if a gate ever appears).
- **The one limit is per IP on store search**: about 40 quick requests, then HTTP 429 for ~20 seconds. The actor paces search to one request every 2.5 seconds. With a proxy, a 429 switches to a new exit IP; without one, it waits the lockout out. A full 9,500-deal crawl takes about 5 minutes.
- **Pagination**: 100 items per page (Steam's maximum), read to the true end of the list.
- **Completeness**:
  - Steam's default "relevance" order keeps re-sorting during a long walk. In testing, a full walk in that order saw only 9,153 of 9,463 items, and price order loses ~0.6% to ties.
  - So a run that asks for the **whole** list (`maxItems` ≥ the number of deals) walks it in **name order**, which saw 9,463 of 9,463. `sortOrder` on each record tells you which order `position` refers to.
  - The actor measures coverage on every run. Ended deals are only reported when at least 99.8% of the list was seen.
- **Silent traps handled**:
  - An unknown country makes `appdetails` fall back to USD and search return 0, so country codes are validated up front.
  - Ended sale pages redirect to the homepage with HTTP 200 and are detected.
  - Unknown `sort_by` and `category1` values are ignored by Steam, so input values are restricted to the honoured ones.
  - Bundle-owner discounts carry two different prices in the page, and the displayed one is used.
- Proxy: Apify Residential is pre-selected. Datacenter or no proxy also works, just slower on very large crawls.

# Actor input Schema

## `mode` (type: `string`):

deals = every discounted game/DLC/package on Steam (~9,500), with filters, discount end date and deal type. featured = storefront deal events: Daily Deal, Weekend Deals, Free Weekends, franchise/publisher sale spotlights and the featured specials. freebies = free-to-keep giveaways (100% off) and Free Weekends. appPrices = price and discount of your games in several countries, converted to USD. salePage = every item on a Steam sale page (publisher, franchise or developer sales) with prices. saleEvents = Steam's official calendar of seasonal sales, themed Fests and Next Fest.

## `country` (type: `string`):

Two-letter country code (US, GB, DE, ID, BR, ...). Steam discounts and prices are regional, so the deal list and currency follow this country. Used by every mode except appPrices and saleEvents.

## `maxItems` (type: `integer`):

Maximum deal / sale-item records to return. Steam lists about 9,500 discounted items in the US; set 10000 to read them all (about 5 minutes). Ended-deal detection only runs when the whole list was read.

## `minDiscount` (type: `integer`):

deals: only return items discounted by at least this much. Default 1 drops the ~2% of Steam's specials list shown at 0% (edition upgrades, DLC whose bundle is the discounted item); 0 keeps them. Applied after download (Steam has no discount filter), so it does not reduce the number of pages read.

## `maxPrice` (type: `number`):

deals: only return items whose discounted price is at most this amount, in the store country's currency (e.g. 10 = $10.00 in the US). Leave empty for no limit.

## `category` (type: `string`):

deals: restrict to games, DLC, software or soundtracks.

## `tags` (type: `array`):

deals: only items with ALL of these Steam tags. Tag names (Horror, RPG, Cooking, Roguelike, ...) or numeric tag ids. Useful for themed Fests: their sale pages are dynamic, so tag-filtered deals is how you list them.

## `searchTerm` (type: `string`):

deals: only discounted items matching this text (title, franchise, publisher).

## `sortBy` (type: `string`):

deals: order of results. Note that 'Best reviewed' only lists items that have reviews (about half of all deals).

## `hideFreeToPlay` (type: `boolean`):

deals: drop free-to-play games (their DLC deals still appear).

## `apps` (type: `array`):

appPrices: one per line - a Steam appid (1808500), a Steam store URL, or a game name (resolved through Steam store search; an exact title match wins).

## `countries` (type: `array`):

appPrices: two-letter country codes to compare. Each record gets its USD equivalent (ECB reference rates), the cheapest country and the difference versus the US price.

## `saleUrls` (type: `array`):

salePage: store.steampowered.com/sale/<name> URLs or just the <name>. Get current ones from the featured mode (SPOTLIGHT records with targetType 'sale'). Ended sales are reported as not_found.

## `includeDiscountDetails` (type: `boolean`):

Adds discountEndsAt, hoursLeft, discountType (weekend / weeklong / launch / special / named campaign), developers, publishers and discounted bundles, from Valve's official store API (1 extra request per 100 items).

## `includeUsdPrices` (type: `boolean`):

Convert every price to USD with the European Central Bank's daily reference rates (about 30 currencies; others stay null).

## `includePastEvents` (type: `boolean`):

saleEvents: also return calendar entries that already ended.

## `compareWithPrevious` (type: `boolean`):

Remember each item's price between runs and add previousFinalPriceCents, priceChangeCents, priceDropPct, isNewDeal, lowestPriceSeenCents and alert. In deals/freebies mode, items that disappeared since the last complete run come back as DEAL_ENDED records. Schedule the actor (e.g. every 6 hours) to turn it into a deal monitor. Snapshots are per mode and per filter set, in the named key-value store 'steam-sales-tracker-snapshots'.

## `alertPriceDropPct` (type: `integer`):

When comparing with the previous run, set alert=true on items whose price fell by at least this percentage (new deals are always alerted). 0 = alert on any drop.

## `maxConcurrency` (type: `integer`):

Parallel requests for price lookups. Store search pages are always paced to about 1 every 2.5 seconds per IP (Steam's rate limit).

## `maxRetries` (type: `integer`):

Attempts per request with exponential backoff before an item is reported as an error.

## `proxyConfiguration` (type: `object`):

Apify Residential proxy is recommended: Steam rate-limits store search per IP, and with a proxy a rate limit switches to a new IP instead of pausing ~25 seconds. Prices always follow the 'country' setting, not the proxy location.

## Actor input object example

```json
{
  "mode": "deals",
  "country": "US",
  "maxItems": 100,
  "minDiscount": 1,
  "category": "all",
  "sortBy": "relevance",
  "hideFreeToPlay": false,
  "apps": [
    "1808500",
    "https://store.steampowered.com/app/632360/Risk_of_Rain_2/",
    "Elden Ring"
  ],
  "countries": [
    "US",
    "GB",
    "DE",
    "BR",
    "TR",
    "IN",
    "ID",
    "AR"
  ],
  "saleUrls": [],
  "includeDiscountDetails": true,
  "includeUsdPrices": true,
  "includePastEvents": false,
  "compareWithPrevious": true,
  "alertPriceDropPct": 0,
  "maxConcurrency": 4,
  "maxRetries": 6,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

## `results` (type: `string`):

Every deal, price, sale item, sale event and diagnostic record from this run.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "mode": "deals",
    "country": "US",
    "maxItems": 100,
    "apps": [
        "1808500",
        "https://store.steampowered.com/app/632360/Risk_of_Rain_2/",
        "Elden Ring"
    ],
    "countries": [
        "US",
        "GB",
        "DE",
        "BR",
        "TR",
        "IN",
        "ID",
        "AR"
    ],
    "saleUrls": [],
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("fanndev/steam-sales-discount-tracker").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "mode": "deals",
    "country": "US",
    "maxItems": 100,
    "apps": [
        "1808500",
        "https://store.steampowered.com/app/632360/Risk_of_Rain_2/",
        "Elden Ring",
    ],
    "countries": [
        "US",
        "GB",
        "DE",
        "BR",
        "TR",
        "IN",
        "ID",
        "AR",
    ],
    "saleUrls": [],
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("fanndev/steam-sales-discount-tracker").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "mode": "deals",
  "country": "US",
  "maxItems": 100,
  "apps": [
    "1808500",
    "https://store.steampowered.com/app/632360/Risk_of_Rain_2/",
    "Elden Ring"
  ],
  "countries": [
    "US",
    "GB",
    "DE",
    "BR",
    "TR",
    "IN",
    "ID",
    "AR"
  ],
  "saleUrls": [],
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}' |
apify call fanndev/steam-sales-discount-tracker --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,fanndev/steam-sales-discount-tracker"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/r1M3fKFdWEtXE5lCY/builds/ce5fyz7qOtby7WNSL/openapi.json
