Self-hosted price monitoring for Kazakhstan β with "BUY NOW" alerts in Telegram.
Watches Kaspi.kz, OLX.kz, Krisha.kz and flight prices, scores every deal with an explainable 0β100 Hot Score, and pings you the moment a price becomes anomalously low.
π¬π§ English Β· π·πΊ Π ΡΡΡΠΊΠΈΠΉ Β· Architecture Β· Decisions (ADR)
Price trackers either need a SaaS subscription or break the first time a shop adds a captcha. CheapKZ is a small, readable, single-process service you can run on a $5 VPS:
- paste a link into the Telegram bot β it starts watching,
- it learns the price history of that item,
- it alerts you only when the price is genuinely good and says why.
It is also a deliberate exercise in building a scraper-backed service responsibly: polite by default, hard to abuse, and heavily tested.
| π₯ Explainable Hot Score | 0β100 from discount vs. old price, position in the item's own history, depth below its maximum and freshness. Alerts read like "β22% from old price Β· cheaper than 91% of its history". (ADR 0005) |
| π° Anomaly detector | select Krisha districts on a live map or Kaspi categories β a periodic scan flags listings β₯ N% cheaper per mΒ² than similar flats nearby and items that suddenly drop below their own median price |
| π― Alert rules | target price Β· drop of N% Β· historical-low percentile Β· new listings matching keyword / price filters |
| π§ Noise control | per-rule cooldowns, dedup, quiet hours that hot alerts can break through, mute, daily digest, price-trend regression ("wait or buy") |
| π‘ Anti-ban HTTP layer | per-host token bucket + jitter + global concurrency, UA rotation, weighted proxy pool with exponential cooldown, per-domain circuit breaker, captcha/DDoS-Guard detection |
| π Security-minded | SSRF guard re-validated on every redirect hop, secret redaction in logs/errors, constant-time token compare, HTML-escaped bot output |
| π Operable | Prometheus /metrics, /health, retention job, Docker image running as non-root, SQLite in WAL mode |
| π§© Extensible | a source is one file returning Offer[]; a generic CSS-selector adapter covers any other site without code |
| π§ͺ Tested | 159 tests: pure-logic unit tests, parser tests against fixtures, HTTP layer against a real local server, API integration tests |
Telegram bot ββ βββΆ Kaspi / OLX / Krisha / Flights / generic
REST + React ββΌββΆ SQLite ββ Scheduler ββΆ runWatcher ββΆ HttpGuard (SSRF Β· limiter Β· proxies Β· breaker)
β β² β
β βββββββ alerts βββ hotScore() + evaluateRules() (pure functions)
ββ Notifier βββΆ Telegram DM (quiet hours Β· mute Β· digest)
One watcher run: fetch β normalise β filter β score β evaluate rules β cooldown β persist β deliver. Full walk-through in docs/ARCHITECTURE.md.
Requires Node.js β₯ 23 β CheapKZ uses the built-in
node:sqlite, so there is nothing native to compile.
git clone https://github.com/Venius24/cheapkz.git
cd cheapkz
npm install
cp .env.example .env # set ADMIN_TOKEN (and TELEGRAM_BOT_TOKEN for the bot)
npm run dev # http://localhost:8080cp .env.example .env
mkdir -p data && sudo chown -R 1000:1000 data # container runs as uid 1000
docker compose up -d --buildnpm run build && npm startIn NODE_ENV=production start-up fails if ADMIN_TOKEN is empty, shorter than 16 chars, or still the example value.
| Variable | Purpose |
|---|---|
ADMIN_TOKEN |
protects the dashboard and API (openssl rand -hex 32) |
TELEGRAM_BOT_TOKEN |
enables the bot (from @BotFather); without it the web UI still works |
TRAVELPAYOUTS_TOKEN |
enables flight monitoring |
PROXIES |
comma-separated http://user:pass@host:port list |
MAX_CONCURRENCY, MIN_DELAY_MS |
global politeness knobs (default 3, 900..2400) |
RETENTION_DAYS |
price-history retention (default 365, 0 = keep forever) |
ALLOW_PRIVATE_TARGETS |
disables the SSRF guard β only for monitoring your own internal services |
Everything is documented in .env.example.
<paste a kaspi / olx / krisha link> start watching instantly
/list watchers with βΈ βΆ β» π buttons
/set 12 199990 target price for watcher 12
/drop 12 15 alert on a drop of β₯ 15%
/flights ALA NQZ 2026-09-12 watch flights
/hot best deals right now
/quiet 23:00 07:00 quiet hours (π₯ alerts break through)
/mute 2h Β· /digest 9 Β· /stats
All routes except /health need Authorization: Bearer <ADMIN_TOKEN>.
# create a watcher β the kind is auto-detected from the URL
curl -H "Authorization: Bearer $T" -H "content-type: application/json" \
-d '{"url":"https://kaspi.kz/p/apple-iphone-15/105621344/","target_price":189990}' \
http://localhost:8080/api/watchers
curl -H "Authorization: Bearer $T" "http://localhost:8080/api/deals?level=hot"
curl -H "Authorization: Bearer $T" http://localhost:8080/metrics # PrometheusAny other website, no code needed:
{ "url": "https://example.com/product", "kind": "generic",
"params": { "selector": ".price", "matchAll": false } }The interesting part of this repo is why it is built the way it is. Each decision has a short record in docs/adr/:
| Decision | Short version |
|---|---|
node:sqlite, not a native driver |
zero native deps, trivial backups; cost: Node β₯ 23 |
| Single egress point | adapters cannot skip rate limits, proxies or SSRF checks |
| Pure scoring & rules | the product's core logic has no I/O, so it is trivially testable |
| Manual redirects | a public URL redirecting to 169.254.169.254 must not slip through |
| Weighted score, not ML | small per-item data; users must be able to trust and read the result |
src/
api/ Fastify REST, SPA hosting, /metrics
bot/ grammY Telegram bot
core/ hotScore, rules, formatting, notifier (pure logic lives here)
db/ node:sqlite wrapper, migrations, queries
http/ HttpGuard, RateLimiter, Breaker, ProxyPool, SSRF guard
sources/ kaspi Β· olx Β· krisha Β· flights Β· generic
worker/ runWatcher pipeline + Scheduler
web/ React + Tailwind dashboard (Vite)
tests/ vitest suites
fixtures/ recorded HTML/JSON for parser tests
docs/ architecture + ADRs
npm run typecheck # strict TS, server + web
npm test # 159 tests
npm run buildSee CONTRIBUTING.md for how to add a new source.
CheapKZ is built to be polite: low default rates, jitter, caching, circuit breakers. Keep polling intervals at 15β30 minutes or more, respect each site's Terms of Service, and don't use it for bulk scraping. The project is not affiliated with Kaspi, OLX, Krisha or Aviasales; all trademarks belong to their owners.
- More delivery channels (email, Pushover)
- Seasonality-aware price forecast
- Currency-rate and subscription monitoring
- Pin the resolved IP per request to remove the DNS-rebinding window (ADR 0004)
MIT Β© 2026 Venius24