The Problem
Price trackers like Keepa and CamelCamelCamel fail on non-standard retailers and often scrape the wrong value — a "Save $200" discount or a monthly payment instead of the actual price. PriceGhost solves this with a multi-strategy extraction system where the user arbitrates conflicting results.
What This Does
PriceGhost is a self-hosted full-stack app: a React/TypeScript frontend (frontend/), an Express/TypeScript backend (backend/), and a PostgreSQL schema (database/init.sql). It monitors product prices from any website, notifies via Telegram/Discord/Pushover, and tracks price history.
The core differentiator is the Price Voting Modal — four independent extraction methods (JSON-LD, site-specific scrapers, generic CSS, AI analysis) run in parallel and "vote" on the price. When they disagree, the user sees all candidates with confidence scores and picks the correct one.
How It Is Wired
Execution starts at backend/src/index.ts (startServer), which calls runMigrations (a database write via client.query) and then serves the API. The scraper pipeline lives in backend/src/services/scraper.ts — scrape calls parsePrice 14 times and addPrice 5 times, and scrapeProduct runs the extraction strategies. The AI fallback is in backend/src/services/ai-extractor.ts, which calls a model for inference and makes outbound network calls.
The frontend routes through frontend/src/api/client.ts — 12 modules import it, making it the highest-blast-radius hub. clearMessages is called from 22 places and prepareHtmlForAI from 16, so changes to those ripple widely. The call graph shows no circular dependencies, which keeps the module graph clean.
The notification path is checkPrices -> sendNotifications (3 edges), handled by backend/src/services/notifications.ts, which owns all outbound notification calls. The auth chain is backend/src/middleware/auth.ts (JWT generation, cryptographic ops), used by 7 modules.
How To Use It
The repo includes docker-compose.yml and Dockerfiles for both services, so the container path is the fastest:
git clone https://github.com/moses-y/PriceGhost
cd PriceGhost
cp backend/.env.example backend/.env # configure DB, JWT secret, notification keys
docker-compose up --build
The frontend runs on Vite (dev) or nginx (prod, via frontend/nginx.conf). For local development, run npm install in both frontend/ and backend/, then start each with npm run dev. The backend requires a PostgreSQL instance and the env vars in backend/.env.example.
Real-World Use
A user tracks a GPU across Newegg and Best Buy. PriceGhost's site-specific scrapers handle both, but when Newegg returns a bundle price instead of the card alone, the AI arbitration flags the discrepancy, the modal shows both candidates, and the user picks the single-card price. Notifications fire when it drops below the target.
Code Health & Issues
Static analysis found 18 findings (5 high, 13 medium, 0 low). The high-severity items:
- High — Deep nesting (depth 8) in
backend/src/services/scraper.ts,frontend/src/components/Layout.tsx,frontend/src/pages/Settings.tsx; hard to follow control flow. - High — Oversized files:
backend/src/models/index.tsat 895 lines,backend/src/services/scraper.ts,frontend/src/pages/Settings.tsx. - High — 63 duplicated 6-line blocks across 19 files; extract shared helpers.
- Medium — Hub module
frontend/src/api/client.ts(12 dependents); high churn blast radius.
SDLC gaps: no test suite (46 source files, zero tests), no LICENSE file (defaults to all-rights-reserved), wildcard CORS in backend/src/index.ts, and SQL string interpolation in backend/src/models/index.ts. CI uses mutable GitHub Action tags (@v3, @v5) instead of pinned SHAs, and the Docker base image node:20-slim is unpinned by digest.
The Bottom Line
The multi-strategy extraction with user arbitration is genuinely differentiated and the codebase is cleanly structured with no circular dependencies. The lack of tests and the SQL injection risk in the models layer are the two things to fix before production. For a technical user who wants control over what they track, this is a solid self-hosted option.