Blog/Tutorials

Build a price watcher in an afternoon

Watching a number for changes is the same little loop every time: read it, compare it, tell someone. The hard part was always getting the number. Here is the whole watcher in about forty lines of Node.

Jhon Snack
Founding Engineer · 20 Jun 2026 · 6 min read

Watching a number for changes is the same little loop every single time: read it, compare it to the last one you saw, and tell someone if it moved. The hard part was always getting the number in the first place. With a hosted API that part is one line, so let’s build the whole watcher this afternoon. It comes out to about forty lines of Node.

maviapiread a valueyour jobcompare to lastevery 30 minchanged?notify you
The whole app in one picture. Everything hard about the first box already happened on our side.

Read the number

Start with the one call that used to be a whole project. Point at any catalog endpoint that returns a value you care about, a price, a score, a count, an availability flag, and read the field off clean JSON. Here it is a product price:

watcher.mjs
const KEY = process.env.MAVIAPI_KEY;

async function currentPrice() {
  const res = await fetch(
    "https://api.maviapi.com/v1/sites/example-shop/product?id=aeron-b",
    { headers: { Authorization: `Bearer ${KEY}` } }
  );
  if (!res.ok) throw new Error(`maviapi returned ${res.status}`);

  const { priceUsd } = await res.json();
  return priceUsd;
}

No scraping, no browser, no parsing. Swap the site and endpoint for whatever you actually want to watch; the rest of the watcher does not change.

Remember the last one

A change means “different from last time,” so the watcher needs the smallest possible memory: the previous value. A single JSON file on disk is plenty. Read what we saw last, write what we see now, and hand back both so the next step can compare them:

watcher.mjs
import { readFile, writeFile } from "node:fs/promises";
const STATE = "./last-price.json";

async function lastPrice() {
  try {
    return JSON.parse(await readFile(STATE, "utf8")).priceUsd;
  } catch {
    return null; // first run, nothing saved yet
  }
}

async function remember(priceUsd) {
  await writeFile(STATE, JSON.stringify({ priceUsd }));
}

Tell someone when it moves

Now wire the loop together. Read the current value, compare it to the last, save the new one, and only shout when it actually dropped. Sending to a Slack incoming webhook keeps the “notify” step to a single request, but a console.log works just as well while you build:

watcher.mjs
async function notify(text) {
  await fetch(process.env.SLACK_WEBHOOK, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ text }),
  });
}

async function check() {
  const now = await currentPrice();
  const before = await lastPrice();
  await remember(now);

  if (before !== null && now < before) {
    await notify(`Price dropped: $${before} to $${now}`);
  }
}

check();

That is the entire program. Run node watcher.mjs once to prime it, and the next run that sees a lower price pings your channel.

Put it on a timer

A watcher that runs once is just a check. To make it a watcher, run it on a schedule. If you already have a box or a scheduled function, a cron line is the whole deployment:

crontab
# check every 30 minutes
*/30 * * * * cd /srv/watcher && node watcher.mjs

Don’t want to host the cron at all? Save the same endpoint as a scheduled dataset and let it keep itself current, then point your watcher at the saved result. Same loop, one less thing to run.

Two habits keep a scheduled caller polite. Each successful check spends one credit, so a 30-minute cadence is a couple of dozen a day, not thousands. And if you ever tighten the interval, read the rate-limit headers and back off when they tell you to, rather than hammering on a fixed timer.

Forty lines, one dependency-free script, and a number you used to check by hand now checks itself. The same three steps, read, compare, tell, watch anything the catalog can return. Point it at the value that matters to you and get on with your day.

Written by
Jhon Snack

Works across the stack that makes an endpoint hold: the extraction layer, the edge request path, and the docs. Writes about how the sausage gets made, and why it stays fresh when the web underneath it does not.

Keep reading

All posts →

New posts, no inbox required

We publish when we have something worth saying: new APIs, product updates, and engineering notes. Follow along however you like.