System diagram
Components
Bot Core (Telegraf)
The entry point. Initializes the Telegraf instance, registers command handlers, starts the cron job, and callsclearStaleSnapshot() at startup to prevent serving deals from a previous day.
CronJob Scheduler
Runs on a configurable cron expression (default:0 9 * * * — 9 AM Colombia time). Calls fetchAndMarkDeals(), which runs the full pipeline and records notified game IDs to prevent re-broadcasting the same game within DEDUP_DAYS days.
Commands Handler
Handles/start, /deals, and /help. The /deals command calls fetchDeals(), which is read-only — it never writes to the deduplication log. A per-chat-ID rate limit (default 45 s) prevents spam.
Notifier Service
BroadcastsFilteredDeal[] to all registered Telegram chat IDs. Introduces a configurable delay between sends to respect Telegram’s rate limits.
DealsService (Orchestrator)
The central coordinator (dealsService.ts). Owns the in-memory pipeline lock that prevents concurrent executions from racing on the JSON files. Exposes two public functions:
| Function | Used by | Side effects |
|---|---|---|
fetchDeals() | /deals command | None — read-only |
fetchAndMarkDeals() | Cron broadcast | Writes to notified_games.json |
Layer 1 — Rules Filter
Pure, synchronous function. No I/O. Applies hard thresholds for discount, price, and quality scores, then removes already-notified games. See Filter pipeline.Layer 2 — AI Filter (GPT-4o-mini)
Receives candidates that passed Layer 1. GPT selects up to 10 games and returns one short reason per selection. All price and URL data comes from CheapShark, not from the model. See AI curation.Cache System
Two JSON files underdata/:
snapshot.json— the last successfulFilteredDeal[]plus a candidate hash and timestamp.notified_games.json— deduplication log withsteamAppID+notifiedAtper game.
Data flows
- Cron broadcast path
- /deals command path
Scheduler fires
The cron job triggers
fetchAndMarkDeals() at the configured schedule (default 9 AM Bogotá).Snapshot check
If a fresh snapshot already exists for today (same calendar day in
America/Bogota), it is returned immediately without re-running the pipeline.Fetch raw deals
fetchSteamDeals() calls CheapShark with maxPrice and pageSize filters. The Metacritic threshold is intentionally not passed upstream — see Filter pipeline.Layer 1: rules filter
applyHardFilters() removes deals that are under-discounted, over-priced, or already notified.Hash check
hashCandidates() produces a 16-character SHA-256 digest of the candidate set. If it matches the stored snapshot hash, GPT is skipped and the cached selection is reused.Layer 2: AI curation
filterDealsWithAI() sends candidates to GPT-4o-mini. Only IDs, titles, and rating scores are sent — not prices or URLs.Reconstruct and persist
buildFilteredDeals() assembles FilteredDeal[] from the original CheapShark data using GPT’s selected IDs. The snapshot is saved to disk.Format and broadcast
getExchangeRate() fetches the current USD → COP rate (12-hour in-memory cache, falls back to 4,000 COP/USD). formatDealsMessage() renders each deal in HTML with COP and USD prices. The Notifier Service broadcasts the message to all subscribers. markAsNotified() records the game IDs to prevent future re-broadcasts.When is GPT called?
| Scenario | GPT called? |
|---|---|
| First run of the day, no snapshot | Yes |
| Candidates changed since last snapshot | Yes |
/deals requested, fresh snapshot exists | No — snapshot served |
| Cron fires, same candidates as last snapshot | No — hash matched |
| GPT call fails, fresh snapshot exists | No — snapshot used as fallback |
| GPT call fails, no fresh snapshot | Error returned (ai_error) |
Filter pipeline
How Layer 1 deterministic rules work, including the OR quality logic and rejection criteria.
AI curation
How GPT-4o-mini selects from candidates and what it contributes to the final deal.
Caching and deduplication
Snapshot freshness, candidate hash caching, and the deduplication log.