# 📈 Price History Builder - Volatility, Promo Cadence & Bands (`that_red_bird/price-history-builder`) Actor

📈 Accumulate per-product price history across scheduled runs into analytics nobody else offers. ✅ Volatility (stdev/CV), promo cadence (frequency, gap, discount depth), price bands with today's percentile, a next-drop estimate, and trend. Scrape URLs or feed a price list — testable offline.

- **URL**: https://apify.com/that\_red\_bird/price-history-builder.md
- **Developed by:** [mohamed alaya](https://apify.com/that_red_bird) (community)
- **Categories:** E-commerce
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Price History Builder

Every price monitor tells you the price changed. This one remembers **every price it has
ever seen** for a product and turns that history into analytics nobody else offers:
volatility, promo cadence, price bands, a next-drop estimate, and trend.

It is **stateful by design** — the value it delivers builds up run over run. Run it once
and you get a snapshot. Run it on a schedule for a few weeks and you get a real picture of
how a product actually gets priced.

### Honesty first: what this actor can and cannot know

- **History only exists from the first run onward.** This actor cannot see prices from
  before you started tracking a product. A product's first-ever run always reports
  `isFirstRun: true` with a one-point series — volatility, bands and cadence only become
  meaningful after several runs, and cadence needs multiple discount cycles before it means
  anything at all.
- **The next-drop estimate is extrapolation, not a promise.** It projects a window from the
  gaps between past discount events. A shop can change its pricing strategy at any time
  without warning. Every prediction row says so explicitly in its `note` field, and the
  actor refuses to predict at all until `minEventsForPrediction` discount events have been
  observed.

### Two ways to feed it prices

1. **Scrape URLs.** Put product pages in `products`. The price is auto-detected from
   JSON-LD, microdata/OpenGraph, common price containers, or a text scan — the same
   selector-free detection used by Price Drop Monitor. Supply `priceSelector` per item to
   override it.
2. **Supply a price list.** Put `{"key":"sku-123","price":19.99}` objects in `prices` — your
   own feed, a spreadsheet export, a previous scrape, anything. This needs **no network
   access at all**, which is what makes the analytics testable fully offline and reusable as
   a pure analytics step in any pipeline that already has prices.

You can mix both in the same run; they accumulate into the same per-`key` series in the
named state store (`stateStoreName`).

### What gets computed, per product, every run

| Field | What it means |
|---|---|
| `volatility` | Sample stdev and coefficient of variation (CV) of the whole series — CV is scale-free so a $50 item and a $5,000 item are comparable. |
| `promoCadence` | Detected discount events (price ≥ `promoDropThresholdPercent` below a rolling baseline), how often they happen, average gap in days, typical discount depth, longest gap. |
| `priceBands` | min / max / median of the series, and `currentPercentile` — is today's price actually good, or is it near the historical ceiling? |
| `nextDropEstimate` | A predicted window derived from the observed cadence, with a `confidence` (`LOW`/`MEDIUM`/`HIGH`) and an explicit note that it is extrapolation. |
| `trend` | `RISING` / `FALLING` / `STABLE` (a `trendFlatBandPercent` deadband absorbs noise) plus a linear-regression slope. |
| `alert` | `true` when `priceBands.currentPercentile <= alertPercentile` — today's price is at or below the historical Nth percentile. |

### Input

```json
{
  "products": [
    "https://shop.example/product/123"
  ],
  "prices": [
    { "key": "own-feed-sku-9", "price": 44.5, "currency": "USD" }
  ],
  "maxHistory": 90,
  "promoDropThresholdPercent": 8,
  "alertPercentile": 25
}
```

### Output

Per product: `key`, `price`, `checkedAt`, `isFirstRun`, `historyLength`, `priceHistory`
(the capped series), `volatility`, `priceBands`, `promoCadence`, `nextDropEstimate`,
`trend`, `alert`.

### Who uses it

Buyers deciding whether today's price is actually a deal · dropshippers timing
restocks around a competitor's known sale cadence · deal-alert sites that want a
statistically grounded "buy now" signal instead of a gut call · anyone who has ever
screenshotted a price history chart by hand.

# Actor input Schema

## `products` (type: `array`):

Product pages to scrape for today's price. Plain URLs, or objects like {"url":"https://…","label":"nike-air-42","priceSelector":".price"}. Price is auto-detected from JSON-LD, microdata or common price containers unless priceSelector is given. Leave empty if you only use "prices".

## `prices` (type: `array`):

Feed today's price directly instead of scraping — your own feed, spreadsheet export, or a previous scrape. Each item: {"key":"sku-123","price":19.99,"url":"https://…","currency":"USD","title":"…","at":"2026-01-01T00:00:00Z"}. Only "key" and "price" are required; "at" defaults to now. This is what makes the analytics testable fully offline and usable as a pure analytics step with no network access at all. Leave empty if you only use "products".

## `priceSelector` (type: `string`):

Default selector applied to every URL in "products" that doesn't specify its own priceSelector. Leave empty to rely on automatic price detection.

## `maxHistory` (type: `integer`):

Cap on how many past price points are kept per product key in the state store. Oldest points are dropped once the cap is reached.

## `promoDropThresholdPercent` (type: `integer`):

A price point counts as a discount event when it sits at least this many percent below the rolling baseline (the median of the prior points). Lower catches smaller sales; higher only catches deep discounts.

## `promoBaselineWindow` (type: `integer`):

How many of the immediately preceding price points are used to compute the rolling baseline that a new point is compared against for discount detection.

## `minEventsForPrediction` (type: `integer`):

The next-drop estimate is withheld until at least this many discount events have been observed for a product. More history in, more honest prediction out.

## `alertPercentile` (type: `integer`):

Fire an alert when today's price is at or below this percentile of everything ever observed for that product (0 = the all-time low, 50 = the median price, 100 = never alerts).

## `trendFlatBandPercent` (type: `integer`):

Total change across the series smaller than this percent (either direction) is classified as STABLE rather than RISING/FALLING, so normal noise doesn't get reported as a trend.

## `resetHistory` (type: `boolean`):

Forget stored price series and start fresh for every product in this run. Use after a bad scrape poisoned the history.

## `onlyAlerts` (type: `boolean`):

Push only rows where the good-deal alert fired. Recommended when tracking hundreds of products.

## `maxItems` (type: `integer`):

Hard cap on dataset rows per run. 0 = no cap.

## `renderJs` (type: `boolean`):

Only needed for shops in "products" that build the price in the browser. Slower and more expensive. Has no effect on the supplied "prices" list.

## `customHeaders` (type: `object`):

Extra request headers for scraping "products", e.g. a Cookie header for region-locked or logged-in pricing.

## `concurrency` (type: `integer`):

How many products (URL scrapes or supplied entries) to process in parallel.

## `stateStoreName` (type: `string`):

Named key-value store holding each product's accumulated price series. Use different names to keep separate history sets.

## `proxyConfiguration` (type: `object`):

Optional Apify proxy configuration for shops in "products" that block datacenter traffic. Has no effect on the supplied "prices" list.

## Actor input object example

```json
{
  "products": [
    "https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html"
  ],
  "maxHistory": 90,
  "promoDropThresholdPercent": 8,
  "promoBaselineWindow": 5,
  "minEventsForPrediction": 2,
  "alertPercentile": 25,
  "trendFlatBandPercent": 2,
  "resetHistory": false,
  "onlyAlerts": false,
  "maxItems": 0,
  "renderJs": false,
  "concurrency": 5,
  "stateStoreName": "price-history-builder-state"
}
```

# Actor output Schema

## `results` (type: `string`):

No description

## `downloadCsv` (type: `string`):

No description

## `summary` (type: `string`):

No description

## `count` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "products": [
        "https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("that_red_bird/price-history-builder").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "products": ["https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html"] }

# Run the Actor and wait for it to finish
run = client.actor("that_red_bird/price-history-builder").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "products": [
    "https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html"
  ]
}' |
apify call that_red_bird/price-history-builder --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,that_red_bird/price-history-builder"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/Gxb28ZgMpZNFo6jPx/builds/jQapP7CTQ2PWkY7mh/openapi.json
