Configuration
All thresholds are configurable via environment variables. The defaults are:| Variable | Default | Description |
|---|---|---|
MIN_DISCOUNT_PERCENT | 50 | Minimum savings percentage |
MIN_METACRITIC_SCORE | 70 | Minimum Metacritic score (0–100) |
MIN_STEAM_RATING_PERCENT | 70 | Minimum Steam rating percentage |
MAX_PRICE_USD | 60 | Maximum sale price in USD |
DEALS_PAGE_SIZE | 60 | Number of deals to fetch from CheapShark |
DEDUP_DAYS | 7 | Days before a notified game can appear again |
Filter logic
Each deal is evaluated against two categories of criteria.Rejection conditions (all must pass)
A deal is rejected immediately if any of these is true:savings < MIN_DISCOUNT_PERCENT— discount is too shallow.salePrice > MAX_PRICE_USD— absolute price is too high.steamAppIDis in the recent deduplication set — game was already notified withinDEDUP_DAYSdays.
Quality condition (OR logic)
A deal that survives the rejection conditions must also meet at least one quality signal:metacriticScore >= MIN_METACRITIC_SCORE(only evaluated if a Metacritic score exists)steamRatingPercent >= MIN_STEAM_RATING_PERCENT
The OR logic means a game with no Metacritic score can still pass if its Steam community rating is strong enough — and vice versa.
Why Metacritic is not filtered upstream
CheapShark supports ametacritic query parameter to request only deals above a score threshold. This filter is intentionally not used.
Because the quality check is an OR between Metacritic and Steam Rating, filtering by Metacritic upstream would silently discard all games that have no Metacritic score but have excellent Steam reviews. The full candidate universe must be fetched and evaluated locally.
Source code
rulesFilter.ts
applyHardFilters has no I/O. The notifiedIds set is built by dealsService.ts before calling this function, which keeps rulesFilter.ts pure and trivially testable.Worked example
Assume default thresholds:MIN_DISCOUNT_PERCENT=50, MIN_METACRITIC_SCORE=70, MIN_STEAM_RATING_PERCENT=70, MAX_PRICE_USD=60.
| Game | Savings | Sale price | Metacritic | Steam rating | Verdict |
|---|---|---|---|---|---|
| Game A | 75% | $9.99 | 82 | 88% | Pass — discount ✓, price ✓, Metacritic ✓ |
| Game B | 60% | $14.99 | 0 (none) | 91% | Pass — discount ✓, price ✓, Steam rating ✓ |
| Game C | 80% | $4.99 | 65 | 68% | Reject — neither quality signal passes |
| Game D | 40% | $7.99 | 85 | 90% | Reject — discount below minimum |
| Game E | 70% | $74.99 | 90 | 95% | Reject — price exceeds maximum |
| Game F | 65% | $12.99 | 75 | 80% | Reject — already notified this week |
What comes next
Candidates from Layer 1 are hashed and compared against the stored snapshot. If the hash matches, the previous AI selection is reused. If not, they are sent to GPT-4o-mini for curation.AI curation
How GPT-4o-mini curates the candidate set and what the model actually contributes.
Caching and deduplication
How the candidate hash prevents unnecessary GPT calls.