npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

trackanything

v0.1.0

Published

Watch the price of anything on any page. Self-hosted, no account, no subscription — and on most shops it needs no selector at all.

Downloads

26

Readme


Keepa wants $20 a month to remember what things used to cost. This does it on your own machine, for anything, not just Amazon.

npx trackanything add https://shop.example/headphones --target 199
npx trackanything check --notify ntfy:my-topic

Everything lives in ~/.trackanything/watches.json. There is no account, no hosted service, and the only thing it ever connects to is the page you told it to watch.

It usually needs no selector

Every other price tracker starts by asking you to right-click, inspect element, and paste a CSS selector — which then breaks the next time the shop ships a stylesheet.

It does not have to work that way. Google asks shops to publish their price in schema.org structured data, so most of them already do:

$ trackanything add https://shop.example/headphones

  Watching Aurora Over-Ear Headphones
  id a1b2c3d4 · £249.99 · read from json-ld
  No selector needed: the page publishes its price in structured data.

That number comes from a machine-readable block the shop maintains deliberately. It does not move when their CSS changes.

The order it tries, and why:

| | Source | Breaks when | |---|---|---| | 1 | schema.org JSON-LD | the shop stops caring about Google | | 2 | OpenGraph / product meta tags | the shop stops caring about link previews | | 3 | --selector you wrote | the shop ships CSS | | 4 | --json-path, for JSON APIs | the API changes | | 5 | --regex, last resort | anything at all |

You only descend that table when you have to:

trackanything add https://shop.example/desk --selector "#product .now"
trackanything add https://api.shop.example/p/42 --json-path data.product.price
trackanything add https://weird.example/thing --regex "Now only ([\d.]+)"

Find the selector before you commit to it. test fetches and extracts without saving anything:

trackanything test https://shop.example/desk --selector "#product .now"

And add always does one read immediately — a watch that silently cannot see the page is worse than an error, because you find out six weeks later when the sale you were waiting for has been and gone.

Being told

trackanything check --notify ntfy:my-topic --notify discord:https://…

| Channel | Spec | |---|---| | ntfy | ntfy:my-topic or ntfy:https://my-server/topic | | Discord | discord:https://discord.com/api/webhooks/… | | Slack | slack:https://hooks.slack.com/… | | Any webhook | webhook:https://… — gets JSON | | A local command | command:notify-send — gets $TRACK_* env vars and stdin | | Terminal | stdout |

All of them are things you already run or that need no account. There is no relay in the middle, because a price tracker that phones a company to tell you a price dropped has given away the thing you were avoiding.

Choose when with --on:

  • change (default) — any move
  • drop — only down, plus back-in-stock
  • target — once, when it crosses the price you would actually buy at
  • stock — only back-in-stock
  • never — record it, say nothing
Aurora Over-Ear Headphones dropped 28% to £179.99, the lowest it has been (was £249.99)

Keeping it running

Not a daemon. One pass, then it exits — which is one less thing to keep alive.

# crontab -e
0 */6 * * * npx trackanything check --notify ntfy:my-topic --quiet

Or in Docker:

docker compose up                          # one pass
docker compose --profile cron up -d        # a loop, if you would rather

--every 6h on a watch is a floor, not a schedule: run check as often as you like and each watch is only actually fetched when its own interval has elapsed. Concurrency is capped at three, because twenty parallel requests to one shop is how you get blocked.

The report

trackanything report prices.html

One self-contained file — no JavaScript, no remote requests, nothing to host. The charts are step charts, because a price is a step function: it was £449 until Tuesday and then it was £389, and joining those with a diagonal would draw a week of prices nobody was ever charged.

See it without watching anything

npx trackanything demo

Six months of invented history for an invented shop, and it fetches nothing. The test suite asserts that — the demo is given a fetch that throws.

History that does not grow forever

A watch checked every six hours for a year is 1,460 readings, and 1,455 of them say the same thing. So identical consecutive readings extend the existing point rather than appending a new one:

{ "value": 249.99, "from": "2026-03-01T…", "to": "2026-05-14T…", "checks": 296 }

Which is both the truth — it was £249.99 for eleven weeks — and the reason the file stays small enough to keep in a git repo if you want to.

When it fails

It will. Shops put pages behind bot checks, change their markup, and geo-redirect. So failures are recorded as data rather than thrown:

  ! Larkin Standing Desk    selector "#product .now" matched nothing with a number in it
  ! Aurora Headphones       HTTP 403

The history is untouched, the other watches still run, and list shows the error next to the watch until the next successful read.

On being polite. It sends a normal browser User-Agent, because a lot of CDNs serve a challenge page to anything that looks scripted and a challenge page has no price on it. That is the extent of it: no proxy rotation, no headless browser, no CAPTCHA solving. If a shop genuinely does not want to be read, this will not read it, and that is the correct outcome.

Everything else

trackanything list                     what you are watching
trackanything history <id|name>        every price it has been
trackanything pause|resume <id|name>
trackanything remove <id|name>

Watches are found by id, by name, or by any fragment of either — trackanything history headphones works.

Contributing

npm test    # 62 tests, no dependencies, no network

The most useful contributions:

  1. A page it cannot read. Add it to fixtures/pages.mjs with the shape that defeated it, and the extractor grows a case.
  2. A notification channel — one function in src/notify/index.mjs.
  3. A number format that parses wrong. parseAmount handles both hemispheres of decimal punctuation; if yours is a third, that is a bug with a test attached.

MIT. Fork it, rename it, ship it.