DEV Community

Cover image for How to Build an Amazon Price Monitoring Pipeline
Nexscope Team for Nexscope

Posted on Originally published at nexscope.ai

How to Build an Amazon Price Monitoring Pipeline

A price monitor needs more than a scheduled HTTP request. Amazon product pages can expose a Buy Box price, list price, deal price, Prime price, coupon-adjusted price, FBA and FBM prices, and periods where a curve is absent. Collapsing those observations into one ambiguous number creates noisy alerts.

This tutorial builds a small Node.js pipeline that requests selected price curves, preserves missing values, saves a timestamped snapshot, and detects changes against the previous run.

Amazon price monitoring workflow from scheduled API collection to stored snapshots and alerts

Define the Monitoring Contract

The current Amazon Product Price Series API accepts one ASIN, a marketplace domain, and a history window. The documented domain IDs are:

Domain Marketplace Domain Marketplace
1 US 2 UK
3 Germany 4 France
5 Japan 6 Canada
8 Italy 9 Spain
10 India 11 Mexico
12 Brazil

The optional days field defaults to 90 and supports up to 365 days. Curve flags let the caller request only the signals needed by the application.

Store the API key in the server environment:

export NEXSCOPE_API_KEY="nk_your_key_here"
export AMAZON_ASIN="B072MQ5BRX"
Enter fullscreen mode Exit fullscreen mode

Request Price History

The client retries network failures, HTTP 429 responses, and server errors. Invalid input and authentication errors fail immediately.

const API_KEY = process.env.NEXSCOPE_API_KEY;
const ENDPOINT =
  "https://api.nexscope.ai/api/skill-api/v1/skills/amazon-product-price-series/run";

if (!API_KEY) throw new Error("Set NEXSCOPE_API_KEY first");

const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function fetchPriceSeries(body, maxAttempts = 3) {
  let lastError;

  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
    let response;

    try {
      response = await fetch(ENDPOINT, {
        method: "POST",
        headers: {
          Authorization: `Bearer ${API_KEY}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify(body),
      });
    } catch (error) {
      lastError = error;
      if (attempt < maxAttempts) {
        await wait(500 * 2 ** (attempt - 1));
        continue;
      }
      break;
    }

    if (response.ok) return response.json();

    const message = await response.text();
    const retryable = response.status === 429 || response.status >= 500;
    if (!retryable) throw new Error(`Request failed (${response.status}): ${message}`);

    lastError = new Error(`Transient API failure (${response.status})`);
    if (attempt < maxAttempts) await wait(500 * 2 ** (attempt - 1));
  }

  throw lastError;
}

const payload = await fetchPriceSeries({
  asin: process.env.AMAZON_ASIN,
  domain: 1,
  days: 90,
  showPrice: true,
  showPriceList: true,
  showPriceDeal: true,
  showPricePrime: true,
  showPriceFba: true,
  showPriceFbm: true,
  showPriceCoupon: true,
  showSellerCount: true,
});
Enter fullscreen mode Exit fullscreen mode

Do not put this request in client-side browser code. A browser bundle would expose the bearer token.

Normalize Curves Without Inventing Data

The documented response can contain curves such as buyboxPrice, price, priceList, priceDeal, pricePrime, priceFba, priceFbm, priceCoupon, and sellerCount. Preserve every timestamp and keep absent curves as null.

const CURVES = [
  "buyboxPrice",
  "price",
  "priceList",
  "priceDeal",
  "pricePrime",
  "priceFba",
  "priceFbm",
  "priceCoupon",
  "sellerCount",
];

function normalizePoint(point) {
  if (Array.isArray(point)) {
    return { at: point[0] ?? null, value: point[1] ?? null };
  }
  return {
    at: point?.timestamp ?? point?.time ?? point?.date ?? null,
    value: point?.value ?? point?.price ?? null,
  };
}

function normalizeCurve(value) {
  if (!Array.isArray(value)) return null;
  return value.map(normalizePoint).filter((point) => point.at !== null);
}

function normalizePricePayload(payload, context) {
  const source = payload?.data ?? payload ?? {};
  const curves = Object.fromEntries(
    CURVES.map((name) => [name, normalizeCurve(source[name])]),
  );

  return {
    schemaVersion: 1,
    asin: context.asin,
    domain: context.domain,
    collectedAt: new Date().toISOString(),
    currency: source.currency ?? null,
    curves,
  };
}
Enter fullscreen mode Exit fullscreen mode

This normalizer is deliberately defensive. Verify the actual response shape in a test account before promoting it to production, and keep the raw response for debugging.

Save One Snapshot Per Run

For a local demonstration, Node's file APIs are enough. A production monitor should use durable object storage or a database, make writes idempotent, and retain a run ID.

import { mkdir, readFile, rename, writeFile } from "node:fs/promises";

const STATE_DIR = new URL("./price-state/", import.meta.url);
const STATE_FILE = new URL(`${process.env.AMAZON_ASIN}-us.json`, STATE_DIR);

async function readPrevious() {
  try {
    return JSON.parse(await readFile(STATE_FILE, "utf8"));
  } catch (error) {
    if (error.code === "ENOENT") return null;
    throw error;
  }
}

async function saveAtomically(snapshot) {
  await mkdir(STATE_DIR, { recursive: true });
  const temporary = new URL(`${STATE_FILE.pathname}.tmp`, "file://");
  await writeFile(temporary, JSON.stringify(snapshot, null, 2));
  await rename(temporary, STATE_FILE);
}
Enter fullscreen mode Exit fullscreen mode

Atomic replacement prevents readers from opening a half-written JSON file. A database implementation should enforce a unique key such as (asin, domain, collectedAt).

Detect Meaningful Changes

The latest point is not necessarily “now.” Record its source timestamp, and compare like with like: same ASIN, marketplace, curve, and currency.

function latest(curve) {
  if (!Array.isArray(curve) || curve.length === 0) return null;
  return curve
    .filter((point) => Number.isFinite(Number(point.value)))
    .sort((a, b) => String(a.at).localeCompare(String(b.at)))
    .at(-1) ?? null;
}

function detectChange(previous, current, curveName, thresholdPercent = 3) {
  const before = latest(previous?.curves?.[curveName]);
  const after = latest(current?.curves?.[curveName]);

  if (!before || !after) return null;
  const oldValue = Number(before.value);
  const newValue = Number(after.value);
  if (!Number.isFinite(oldValue) || !Number.isFinite(newValue) || oldValue === 0) {
    return null;
  }

  const percent = ((newValue - oldValue) / oldValue) * 100;
  if (Math.abs(percent) < thresholdPercent) return null;

  return {
    curve: curveName,
    oldValue,
    newValue,
    percent: Number(percent.toFixed(2)),
    previousAt: before.at,
    currentAt: after.at,
  };
}

const context = { asin: process.env.AMAZON_ASIN, domain: 1 };
const current = normalizePricePayload(payload, context);
const previous = await readPrevious();

const alerts = ["buyboxPrice", "priceDeal", "priceCoupon"]
  .map((curve) => detectChange(previous, current, curve, 3))
  .filter(Boolean);

console.log(JSON.stringify({ context, alerts }, null, 2));
await saveAtomically(current);
Enter fullscreen mode Exit fullscreen mode

Emit an alert only after the snapshot is valid, but decide whether to save before or after notification based on the retry guarantees of the messaging system. A production queue should attach an idempotency key so a retried job does not send the same alert twice.

Schedule the Collector

Run the script with an external scheduler such as cron, a cloud scheduler, or a job queue. This is safer than leaving a long-running setInterval process responsible for business-critical timing.

# Every six hours; use an explicit server timezone and monitor failures.
15 */6 * * * /usr/bin/node /srv/price-monitor/index.mjs
Enter fullscreen mode Exit fullscreen mode

The API documentation does not publish a fixed rate limit or freshness guarantee. Start sequentially, measure real responses, add backoff, and size the schedule to the account's actual limits and monitoring goal.

Production Checks

  • Preserve marketplace, currency, ASIN, curve, and source timestamp with every observation.
  • Treat missing curves as unknown, not as zero price or out of stock.
  • Test coupons, Prime-only prices, variations, multiple sellers, and unavailable products.
  • Require confirmation across repeated observations before triggering automated repricing.
  • Monitor job failures separately from “no price change.”
  • Do not treat a price movement as proof of demand, inventory, or competitor intent.

Next Step

The Amazon Product Price Series API exposes selectable price and marketplace curves for a single ASIN request.

Build with the Amazon Product Price Series API →

Disclosure: This article was prepared with AI-assisted editing using the current published API documentation as its technical source of truth.

Top comments (0)