Work / PriceTrail
Side project · public · started October 2026
A price monitor that collects, stores and alerts on its own, every day.
PriceTrail tracks game and hardware prices across stores in Brazil and the US. It is the public version of the managed data extraction I do for clients: sources that change, a collector that survives them, and an API on top.
- Role
- Design, backend, frontend, ops
- Stack
- Python · PostgreSQL · Next.js · GitHub Actions
- Status
- In progress · open source soon
- Links
- Live site and source code coming with the first deploy
- 23:19:49steam-br · 10 offers · ok · 0.3s
The interface
A catalog, a page per product, and a status page that admits when something broke.
Warm paper, a serif and a mono font, on purpose nothing like this site. Every card shows the latest price per store, the list price when the store published one, and when it was last checked.
What it does
Small on purpose. Every piece earns its place.
- Daily price history
- One observation per offer per day, kept forever, with the 30-day low and high on every product.
- Two kinds of badge
- “On sale” when the store itself published a list price; “↓ −x%” when the price fell below the last different observation. Both can show at once.
- Price alerts by email
- Pick a store and a target price. You confirm by link, every email has an unsubscribe link, and there are no accounts.
- Status page
- Each run is recorded per source: status, offers read, duration, error. A store that changed its page shows up before anyone sees a stale price.
- Read-only JSON API
- /api/products, /api/products/{slug} and /api/status. No key; cached and refreshed after each run.
- Two currencies, two countries
- Steam in Brazil and the US side by side, so the same game shows in BRL and USD.
How the data is collected
Read each store the way it publishes, never guess.
There is no general-purpose scraper. Each source is a small adapter with one method, fetch(offers), and a parser kept as a pure function so it can be tested offline against saved responses. Four kinds so far:
The rules it follows
- Only public product data: name, price, list price, availability. No personal data from any store.
- robots.txt and terms are checked before a source is added; the two Shopify stores state their product pages are crawlable.
- Identified User-Agent, one request per second, at most one run a day.
- A product missing from a page or feed is skipped and the run becomes “partial”; a price is never guessed or recorded as “unavailable”.
- Failures back off exponentially. A source that keeps failing is skipped for the day, never retried in a loop.
Public storefront JSON
Steam (Brazil, US)
The store's own price endpoint, batched where it allows, one request per region.
Product pages · schema.org
Fangamer
Reads the Product offers from JSON-LD or microdata, so the same adapter works on any store that publishes structured data.
Atom product feed
8BitDo
The Shopify-style feed many stores expose at /collections/all.atom: one request for the whole catalog.
Official API
Mercado Livre
Client-credentials app registered with the marketplace; the source is skipped while the keys are absent, never scraped.
How it runs
No server to keep alive.
A GitHub Actions cron runs migrate → seed → collect → notify → revalidate. The catalog lives in a TOML file and is seeded into Postgres; the collector writes one price point per offer and one row per run; alerts go out after the run; the site drops its cache last, so it never shows a half-finished day.
The API
Everything the site shows, you can also fetch.
Products, offers, 30-day lows and the status of every source are read-only JSON, refreshed right after each daily run. It is the same shape I hand to clients: stable and documented.
$ Under the hood
Boring choices, so it keeps running.
- Schema
- Six tables in plain SQL (products, offers, price_points, runs, sources, alerts), migrations applied in order by the CLI, and a source_status view that feeds both the status page and pricetrail status.
- Tests
- pytest: parsers run against fixtures saved from each store; collector and alert tests run on the real database in throwaway schemas.
- CI
- Every push runs pytest and ruff for the collector, tsc and eslint for the site.
- Site
- Next.js 16 with cached components tagged “prices”; the collector calls /api/revalidate with a secret after each run. The price chart is a server-rendered SVG, one panel per currency.
- CLI
- pricetrail migrate · seed · collect · notify · status, the same commands the cron runs.
Figures are a snapshot of the project's own database on 2026-10-06.
Your data
Need something like this for your own prices, listings or catalog data?
If it is public on other sites, I can collect it every day, keep it clean and hand it to you as an API or a page your team can use.


