Monitor a product price for changes using the Step API

The Step API lets you drive a cloud browser session from your own code, one action at a time. This guide scrapes a single product price, stores it, and sends an alert whenever the price changes on the next run. The example tracks a pair of trainers on secretsales.com, but the pattern works on any product page.

Before you begin


  • Node.js 18 or later, so the built-in fetch is available.
  • The axiom-api library installed: npm install axiom-api.
  • An axiom.ai API key from the Code Dashboard, set as the AXIOM_API_KEY environment variable.
  • A webhook URL to receive alerts (Slack, Discord, or any endpoint that accepts JSON), set as ALERT_WEBHOOK_URL.

Scrape the price


axiom.scrape() extracts records from the current page using a CSS selector you choose at runtime. On the product page, the price sits in an element whose class contains productFullDetail-productPriceFinal, so match it with a partial-class selector.

The class name carries a generated suffix (something like productFullDetail-productPriceFinal-a1b2c) that changes when the site rebuilds. Matching the stable substring with [class*='...'] keeps the selector working across those rebuilds.

import { AxiomApi } from 'axiom-api';

const axiom = new AxiomApi(process.env.AXIOM_API_KEY);

const PRODUCT_URL =
  "https://www.secretsales.com/66165d1f6b502-nike-nike-air-trainer-1-essential-mens-white-trainers-white/";

async function getPrice() {
  await axiom.browserOpen();
  try {
    await axiom.goto(PRODUCT_URL);

    const rows = await axiom.scrape(
      null,                                                            // scrape the current page
      { hierarchy: "[class*='productFullDetail-productPriceFinal']" }, // partial class match
      null,                                                            // no pagination
      1,                                                               // one record
      {}                                                               // default settings
    );

    // scrape() returns a 2D array (rows x fields), so the value is at [0][0]
    return rows?.[0]?.[0]?.trim() ?? null;                             // e.g. "£54.99"
  } finally {
    await axiom.browserClose();                                       // always free the session
  }
}

Note: If the price is exposed in the page's structured data rather than a class, axiom.scrapeMetadata() is a lighter way to read a single field.

Warning: An open session consumes runtime quota. Always close it in a finally block so a mid-script error can't leave it running.

Convert the scraped string to a number so you can compare it later.

function toNumber(priceText) {
  return Number(priceText.replace(/[^0-9.]/g, "")); // "£54.99" -> 54.99
}

Store a baseline to compare against


To know when a price changes, keep the last value between runs. A small JSON file is enough to start. Swap it for a database or key-value store in production.

import { readFileSync, writeFileSync, existsSync } from 'node:fs';

const STORE = "./price.json";

function loadLastPrice() {
  if (!existsSync(STORE)) return null;
  return JSON.parse(readFileSync(STORE, "utf8")).price;
}

function savePrice(price) {
  const record = { price, checkedAt: new Date().toISOString() };
  writeFileSync(STORE, JSON.stringify(record, null, 2));
}

Detect a change


Compare the freshly scraped price against the stored baseline. On the first run there's nothing to compare against, so just set the baseline.

async function checkPrice() {
  const current = toNumber(await getPrice());
  const previous = loadLastPrice();

  if (previous === null) {
    console.log(`First run. Baseline set to ${current}.`);
  } else if (current !== previous) {
    console.log(`Price changed: ${previous} -> ${current}`);
    await sendAlert(previous, current);
  } else {
    console.log(`No change. Still ${current}.`);
  }

  savePrice(current);
}

Send an alert


When the price moves, post a message to your webhook. This example uses a Slack-style incoming webhook, but any endpoint that accepts a JSON body works.

async function sendAlert(previous, current) {
  const direction = current < previous ? "dropped" : "rose";

  await fetch(process.env.ALERT_WEBHOOK_URL, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      text: `Nike Air Trainer 1 price ${direction}: £${previous} -> £${current}`,
    }),
  });
}

Note: To alert by email instead, send the same payload to a transactional email API such as Postmark or SendGrid in place of the webhook call.

Run it on a schedule


The script checks the price once per run. To watch for changes over time, run it on a schedule:

  • A cron job on a server: 0 * * * * node check-price.js runs it hourly.
  • A scheduled CI job, such as GitHub Actions with a schedule trigger, which needs no server of your own.

Persist price.json, or move the baseline into a database, so the value survives between scheduled runs.

Full script


import { AxiomApi } from 'axiom-api';
import { readFileSync, writeFileSync, existsSync } from 'node:fs';

const axiom = new AxiomApi(process.env.AXIOM_API_KEY);
const STORE = "./price.json";

const PRODUCT_URL =
  "https://www.secretsales.com/66165d1f6b502-nike-nike-air-trainer-1-essential-mens-white-trainers-white/";

async function getPrice() {
  await axiom.browserOpen();
  try {
    await axiom.goto(PRODUCT_URL);
    const rows = await axiom.scrape(
      null,
      { hierarchy: "[class*='productFullDetail-productPriceFinal']" },
      null,
      1,
      {}
    );
    return rows?.[0]?.[0]?.trim() ?? null;
  } finally {
    await axiom.browserClose();
  }
}

function toNumber(priceText) {
  return Number(priceText.replace(/[^0-9.]/g, ""));
}

function loadLastPrice() {
  if (!existsSync(STORE)) return null;
  return JSON.parse(readFileSync(STORE, "utf8")).price;
}

function savePrice(price) {
  writeFileSync(STORE, JSON.stringify({ price, checkedAt: new Date().toISOString() }, null, 2));
}

async function sendAlert(previous, current) {
  const direction = current < previous ? "dropped" : "rose";
  await fetch(process.env.ALERT_WEBHOOK_URL, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      text: `Nike Air Trainer 1 price ${direction}: £${previous} -> £${current}`,
    }),
  });
}

async function checkPrice() {
  const current = toNumber(await getPrice());
  const previous = loadLastPrice();

  if (previous === null) {
    console.log(`First run. Baseline set to ${current}.`);
  } else if (current !== previous) {
    console.log(`Price changed: ${previous} -> ${current}`);
    await sendAlert(previous, current);
  } else {
    console.log(`No change. Still ${current}.`);
  }

  savePrice(current);
}

checkPrice();

Run it with node check-price.js.

Next steps


  • Learn the full axiom.scrape() signature to paginate and cap results.
  • See Start a session and the other Step API actions.
  • Track many products at once by looping the check over an array of URLs.