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
fetchis available. - The
axiom-apilibrary installed:npm install axiom-api. - An axiom.ai API key from the Code Dashboard, set as the
AXIOM_API_KEYenvironment 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
finallyblock 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.jsruns it hourly. - A scheduled CI job, such as GitHub Actions with a
scheduletrigger, 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.