# Website Tech Change Monitor (`lowlanddata/tech-change-monitor`) Actor

Watch a list of websites and hear only when their technology stack actually changes: a shop leaves WooCommerce, a prospect adds Shopify, an analytics tag disappears, a CDN switches. Each domain is remembered between runs, so an alert is always a real transition and a quiet check costs nothing.

- **URL**: https://apify.com/lowlanddata/tech-change-monitor.md
- **Developed by:** [Lowland Data](https://apify.com/lowlanddata) (community)
- **Categories:** Developer tools, MCP servers, Agents
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $18.50 / 1,000 stack change alerts

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## Website Tech Change Monitor

Watches a list of websites (clients, prospects, competitors) and alerts you **only when something in their technology stack changes**: a shop moves off WooCommerce, a prospect adds Shopify, a client's analytics tag disappears, a site switches CDN. It remembers every domain's stack between runs, so an alert always means a real transition, never "here is the same list again".

**Live health record:** [lowlanddata.com/status/tech-change-monitor](https://lowlanddata.com/status/tech-change-monitor/) - 30-day success rate and daily canary results, failures included. Full input/output reference: [tool page](https://lowlanddata.com/tools/tech-change-monitor/).

Organization-level data only: domains and technologies, never owners, never emails. The subscription tools that answer this question charge $250–995/month and sell contact data alongside; this one does neither.

### Quick start (30 seconds)

1. Paste the domains to watch, one per line.
2. Hit **Start** once: the first run baselines each domain's current stack (one row per domain, flagged `event: "baseline"`).
3. Add a **daily or weekly schedule**. From then on, runs are silent until a stack actually changes:

```json
{
  "domain": "client-shop.be",
  "event": "change",
  "added": ["Shopify", "Klarna"],
  "removed": ["WooCommerce", "WordPress"],
  "ecommerce": "Shopify",
  "cms": null,
  "cdn": "Cloudflare",
  "technologies": ["Cloudflare", "Google Tag Manager", "Klarna", "Shopify"],
  "observedAt": "2026-09-05T14:00:00.000Z"
}
```

That row reads: the client replatformed from WooCommerce to Shopify and turned on Klarna. You knew before their newsletter did.

### Who uses this

- **Agencies and freelancers** watch client portfolios: a dropped analytics tag or a silent replatform is an account-management heads-up, delivered instead of discovered.
- **Sales teams** watch prospect lists for buying signals: "moved to Shopify" is a better trigger than any cold-email cadence.
- **SaaS vendors** watch whether customers' sites still carry their snippet, and what churned customers switched to.
- **Competitive watchers** learn when a rival adds a payment method, an A/B tool, or a new frontend.

### How the monitor thinks

- **First run = baseline.** Every domain's stack is remembered (and delivered, unless `emitBaseline` is off).
- **After that, only transitions.** A run where nothing changed delivers nothing and costs almost nothing.
- **`added` / `removed`** list the exact technologies that appeared or vanished, and the flat columns show the current state.
- **Memory** lives in a named key-value store in your account (`stateStoreName`). Inside it the record is keyed by the **watch list itself** plus `minConfidence`, so adding or removing a domain (or nudging the threshold) starts a fresh baseline for the whole list and charges it as one. Settle the list before you schedule it; `resetState: true` re-baselines deliberately. A domain not seen for 90 days ages out of the memory and baselines again.
- **An unreachable domain is not a change.** Fetch failures are reported separately and leave the domain's memory untouched, so one flaky morning never floods you with fake "everything removed" alerts.
- **A change has to happen twice before you hear about it.** Analytics tags load on consent, in some countries and not others, and a single fetch can miss one. A stack that differs from the remembered one is held back and only alerts when the next run sees the same thing, so what reaches you is a transition rather than a bad moment on the network.
- **Only the stack raises an alert.** Detections that describe the connection rather than the site, HTTP/3 negotiated on one request for instance, stay in the `technologies` list and never trigger a billed alert.

### How much does it cost?

Pay per outcome: $1.99 per 1,000 baseline rows, charged once when a domain is first seen, then about 2 cents per change alert. Watching 200 domains weekly costs $0.40 to baseline and, at a handful of real changes a month, some 15 cents a month after that, against the $250+/month the incumbent subscription tools charge. A check that finds nothing changed costs nothing.

### Not technical? Let your AI assistant set it up

```text
I want alerts when websites change their technology stack, using the Apify
actor lowlanddata/tech-change-monitor. Build me the input JSON.

Fields:
- domains: array of domains or URLs to watch (required)
- includeDns: boolean, watch the mail stack too (default true)
- minConfidence: 0-100 detection floor (default 50)
- emitBaseline: deliver the first-run snapshot (default true)
- stateStoreName: one name per watch list (default "tech-change-monitor-state")

Here is my list: <paste domains>

Give me the JSON only, then tell me how to schedule it weekly and get an
email when a run produces rows.
```

### Input

| Field            | Type     | Default                     | Notes                                          |
| ---------------- | -------- | --------------------------- | ---------------------------------------------- |
| `domains`        | string\[] | -                           | The watch list; normalized to hostnames.       |
| `includeDns`     | boolean  | `true`                      | Mail-stack changes count too.                  |
| `minConfidence`  | integer  | `50`                        | Detection floor; higher = fewer, surer alerts. |
| `emitBaseline`   | boolean  | `true`                      | Off = silent first run.                        |
| `stateStoreName` | string   | `tech-change-monitor-state` | One per watch list.                            |
| `resetState`     | boolean  | `false`                     | Forget everything and re-baseline.             |

### Use it from your code

```bash
curl -X POST \
  "https://api.apify.com/v2/acts/lowlanddata~tech-change-monitor/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"domains":["client-a.be","client-b.nl"],"stateStoreName":"portfolio-watch"}'
```

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

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('lowlanddata/tech-change-monitor').call({
  domains: ['prospect-1.de', 'prospect-2.fr'],
  stateStoreName: 'prospects-q4',
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

```python
from apify_client import ApifyClient

client = ApifyClient(token=os.environ["APIFY_TOKEN"])
run = client.actor("lowlanddata/tech-change-monitor").call(run_input={
    "domains": open("portfolio.txt").read().split(),
    "emitBaseline": False,
})
for alert in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(alert["domain"], "added:", alert.get("added"), "removed:", alert.get("removed"))
```

### Use it with AI agents (MCP)

```bash
claude mcp add apify --transport http "https://mcp.apify.com?actors=lowlanddata/tech-change-monitor"
```

Cursor or Claude Desktop: add a custom connector with `https://mcp.apify.com?actors=lowlanddata/tech-change-monitor`. Runs are billed to your Apify account. Details: [Apify MCP docs](https://docs.apify.com/platform/integrations/mcp).

Prompts that work once connected:

- "Watch these 20 client sites and tell me if any of them changes stack."
- "Did anything on my prospect list move to Shopify since last week?"

### Is this legal?

Yes: one polite homepage request per domain per run, public DNS lookups, and only what the sites themselves announce. No logins, nothing bypassed, and no personal data of any kind in the source or the output.

### Honest limits

- Detection inherits the scanner's limits: headless frontends can mask the commerce engine, and an empty column means "not visible from the front door". Alerts fire on what the page *shows* changing. The corpus lists 7,722 technologies but this pair reads pages over HTTP without a browser, so the 2,332 fingerprints defined by JavaScript or DOM state are out of reach - about 5,400 are detectable, and a stack change confined to the rest is invisible here. Some rows are also *implied* rather than observed (a WordPress hit implies PHP), so an implied technology appearing or disappearing can move an alert without anything on the page having visibly changed.
- A site A/B-testing its own frontend can genuinely flap between stacks; raise `minConfidence` or watch weekly instead of daily if a domain gets noisy.

### Related actors

- [Website Tech Scanner](https://apify.com/lowlanddata/website-tech-scanner), the one-shot sibling: full stack detail with evidence per detection, no memory.

### Troubleshooting

- **Second run returned nothing**. That is the product working: nothing changed.
- **A wave of alerts after weeks of silence**: check them before doubting them; platform migrations really do land in one deploy.
- **A domain keeps failing to fetch**: it is listed in the run summary as a failure, uncharged; its memory stays intact until it is reachable again.

# Actor input Schema

## `domains` (type: `array`):

Websites to watch - bare domains or full URLs, one per line. The monitor remembers each domain's stack and alerts only when it changes.

## `includeDns` (type: `boolean`):

Adds mail-stack and verification-record detections via public DNS. Cheap; turn off for very large fetch-only scans.

## `minConfidence` (type: `integer`):

Drop detections below this confidence. Every kept detection carries the evidence that triggered it.

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

Upper bound on scanned domains (and dataset rows).

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

One polite homepage fetch per domain; the default datacenter proxy is plenty.

## `emitBaseline` (type: `boolean`):

On the very first run, deliver one row per domain with its current stack. Turn off to seed the memory silently.

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

Named key-value store holding each domain's remembered stack. Use a different name per watch list.

## `resetState` (type: `boolean`):

Forget every remembered stack and re-baseline on the next run.

## `reportFailures` (type: `boolean`):

When a run fails, send the developer the actor name and build, an error category, the error message (truncated) and a one-way hash of your input - so a bug can be fixed without anyone asking you to reproduce it. <b>Your input itself, your results and your identity are never sent, and successful runs send nothing at all.</b> Switch this off and the actor behaves exactly the same.

## Actor input object example

```json
{
  "domains": [
    "wordpress.org",
    "www.allbirds.com",
    "apify.com"
  ],
  "includeDns": true,
  "minConfidence": 50,
  "maxItems": 1000,
  "proxyConfiguration": {
    "useApifyProxy": true
  },
  "emitBaseline": true,
  "stateStoreName": "tech-change-monitor-state",
  "resetState": false,
  "reportFailures": true
}
```

# Actor output Schema

## `changes` (type: `string`):

One row per domain whose technology stack changed, naming what was added and what disappeared. First run: one baseline row per domain, its stack as it stands today; afterwards only the transitions.

# 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 = {
    "domains": [
        "wordpress.org",
        "www.allbirds.com",
        "apify.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("lowlanddata/tech-change-monitor").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 = { "domains": [
        "wordpress.org",
        "www.allbirds.com",
        "apify.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("lowlanddata/tech-change-monitor").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 '{
  "domains": [
    "wordpress.org",
    "www.allbirds.com",
    "apify.com"
  ]
}' |
apify call lowlanddata/tech-change-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,lowlanddata/tech-change-monitor"
        }
    }
}
```

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/W3gAuaPPjKvtwiAWv/builds/V2nqhSEnXXdIr0CdQ/openapi.json
