# Website Tech Stack Monitor: Added & Removed (`mmdincer/website-tech-stack-monitor`) Actor

Technology stack monitor for a public web page. Reads HTML, response headers and script URLs, then lists the technologies it can identify. Later runs report technologies added or removed since the last run. Obeys robots.txt. Does not scrape BuiltWith. Not affiliated with Wappalyzer or BuiltWith.

- **URL**: https://apify.com/mmdincer/website-tech-stack-monitor.md
- **Developed by:** [Muhammet Mustafa Dincer](https://apify.com/mmdincer) (community)
- **Features:** Limited permissions
- **Stats:** 1 total users, 1 monthly users, 75.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.50 / 1,000 website checkeds

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?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

### What is Website Tech Stack Monitor?

**Website Tech Stack Monitor** reads one public page and names the technologies it can see there: the CMS, the store platform, the analytics tag, the CDN, the framework. The first run saves that list. Every later run with the same watchlist name tells you what was **added** or **removed**. A technology lookup that returns one snapshot is a photo. This Actor is the diff.

Paste a domain or a page address, click **Start**, and schedule it. The Actor requests that page and its `robots.txt`. It reads the HTML, the response headers and the script, stylesheet, image and frame URLs. It does not log in, it does not run a browser, and it does not open BuiltWith, Wappalyzer or any other lookup site.

Because it runs on Apify you also get an API, schedules, webhooks and the dataset as JSON, CSV or Excel. Point a webhook at the run and the added or removed rows are the alert.

### Quick answer: when should you pick this Actor?

**Pick Website Tech Stack Monitor** when you want to know which tools a public site is using, and you want a later run to say what changed. A company that adds a marketing tag or drops a store platform is making a tooling decision. The change rows are that signal, taken from the site itself.

**Use a one-shot technology lookup** when you need a single dump and you do not care about the next week. This Actor still gives you that dump on the first run. The reason to keep it scheduled is the diff.

**Defaults that work:** leave the prefilled site (`https://wordpress.org`) and click **Start**. The first run returns a site summary with status `baseline_saved` and one technology row per match, with change type `baseline`. Run the same input again later. If nothing changed, the summary says so and, with the default **Only added or removed technologies** left on, no unchanged technology rows are written. A real first run of that prefilled site is saved in `assets/sample-run-data.json` (WordPress, Google Tag Manager, Google Fonts and nginx, on 5 November 2026).

### At a glance

| | |
|---|---|
| **Does** | Reads a public page and lists matching technologies. Later runs report added and removed technologies |
| **Input** | Domains or page addresses, up to 50. Optional category filter |
| **Output** | One `site` summary, plus one `technology` row per reported technology |
| **Change types** | `baseline`, `added`, `removed`, and `unchanged` when you ask for the full list |
| **Needs** | No login, no API key, no browser, no proxy |
| **Pricing** | Pay per event. The prices in `pricing-ppe.json` are set on the platform. About $0.003 to $0.006 per website checked, plus a start event |
| **Does not** | Scrape BuiltWith, run JavaScript, crawl the whole site, log in, or bypass a bot wall |

### Fingerprint licence

The detection **method** is the same idea as Wappalyzer: decide from the page itself, using its HTML, its response headers and its script URLs. The **patterns are not Wappalyzer's**.

This Actor ships an original catalogue, written for this repository and released under **Apache-2.0**, the same licence as the Actor (`package.json`). Each pattern looks for a publicly observable signal, such as a script host (`cdn.shopify.com`), a path (`/wp-content/`), a meta generator, or a response header (`cf-ray`). A paragraph that merely mentions a product does not match.

The following sources were checked and **not** copied into this Actor:

- **Current Wappalyzer data is not open.** The public Wappalyzer repository was removed in August 2023. Using today's Wappalyzer fingerprint files would mean using proprietary data. This Actor does not do that.
- **The maintained community continuation, enthec/webappanalyzer, is GPL-3.0.** The last public Wappalyzer snapshot (dochne/wappalyzer) is also GPL-3.0. GPL-3.0 is an open licence, and it is copyleft: shipping those JSON files inside this Actor would require this Actor's source to be offered under GPL-3.0. The rest of this repository is Apache-2.0. Those catalogues were not vendored.
- **An older npm `wappalyzer` range (5.9.33 up to, but not including, 6.10.55) was published as MIT**, which can sit inside an Apache-2.0 program if the MIT notice is kept. That range was not vendored either. The upstream repository is gone, later releases switched back to GPL-3.0, and a wrong pin would import copyleft patterns. The patterns in `src/fingerprints.js` were written here. They are not a transcription of `technologies/*.json`.

BuiltWith.com is not requested and not parsed. The research note for this Actor forbids that, and it is unnecessary: the site you name already publishes the HTML, the headers and the script URLs.

This is an engineering decision about which data this repository is allowed to ship. It is not legal advice.

### What the Actor can and cannot see

The catalogue is a curated list (CMS, ecommerce, website builders, JavaScript frameworks and libraries, analytics, tag managers, advertising pixels, marketing and support tools, payments, CDNs, web servers, a few frameworks, security and consent widgets, fonts, monitoring and search). It is not every product on the internet. **Absence is not proof.** A tool that is only injected after JavaScript runs, behind a tag manager, will not appear unless the HTML already contains its script URL. The Actor does not execute JavaScript and does not open a browser.

A match can be wrong. A tutorial page that loads `gtm.js`, or that quotes `gtag('config'...)` inside a script block, can look like it uses that product. Headers such as `Server: nginx` are marked **medium** confidence because a CDN often hides or replaces them. A technology marked **implied** was not seen directly: Next.js implies React, WooCommerce implies WordPress. The `impliedBy` field says so.

Evidence is short on purpose. Script URLs are stored without the query string, so a token in `?id=` or `?key=` is not written to the dataset. Cookie **names** can be a signal (`_shopify_…`, `laravel_session`). Cookie **values** are never stored. Request ids (`cf-ray`, `x-vercel-id`, and similar) are noted as "present" and the value is not stored.

### How a watch works

1. Open the Actor and click **Start**.
2. Keep the prefilled website, or replace it with your own domain or page.
3. Leave **Only added or removed technologies** on.
4. Click **Start**. The first successful run finishes with status `baseline_saved`.
5. Run it again later, or create a schedule. Download the **Technologies** view, or call the dataset API.

The baseline is stored in a named key-value store, `website-tech-stack-monitor-state`, under the watchlist name (`monitorName`, default `default`). The same name compares against the same baseline. Use two names if you want two independent watches. `example.com` and `www.example.com` share one home-page baseline. `example.com/pricing` is a different page and a different baseline. Changing the category filter, or shipping a new fingerprint catalogue version, saves a new baseline once, so the first run after that change does not report the whole stack as added.

If the run is not allowed to create that named store, the Actor does not crash. Apify's automated store test uses a token that answers the create call with HTTP 403 (`insufficient-permissions`). The Actor then writes the snapshot to this run's default key-value store and continues. That copy is not kept for the next run, so the check is a first look (`baseline_saved`) and the dataset still lists the stack. A customer run that can create the named store keeps using it.

A page that answers with a bot wall, a `robots.txt` disallow, or a crawl-delay over 10 seconds is skipped. The previous baseline is left as it was. A failed read is **not** reported as "every technology was removed". The Actor does not retry a 403 or a 429, and it does not rotate proxies. Redirects are followed, up to five, only when each hop is a public `http` or `https` address and `robots.txt` allows it. Private, local and internal hosts are refused before any request.

If the HTML is longer than the read limit, the summary says the page was cut. Removals are not reported for that run, because a tag past the limit would look deleted.

### What can you use it for?

#### See the stack once

Run any public page. Technology rows on that first run use change type `baseline`. The site row lists the names. Schedule nothing yet.

```json
{
  "websites": ["https://wordpress.org"],
  "monitorName": "wordpress"
}
```

#### Watch for a tool being added or removed

Keep the same `monitorName` and run again next week. An `added` row is a technology that was not in the previous baseline. A `removed` row is one that was, and is no longer visible on this page. Turn `onlyChanges` off if you also want the technologies that stayed, marked `unchanged`.

```json
{
  "websites": ["example.com", "https://example.com/pricing"],
  "onlyChanges": true,
  "categories": ["Analytics", "Marketing", "Ecommerce", "Tag manager"],
  "monitorName": "competitors"
}
```

#### One section of the catalogue

`categories` limits both detection and the baseline. A filter of `Analytics` will not report a new CMS. Leave the list empty to use the whole catalogue.

### Compliance

You choose the pages. The Actor only requests those URLs, plus `robots.txt` on the same hosts. It obeys `robots.txt` for the path it is about to fetch (RFC 9309, including a named group for the token `techstackmonitor`). It waits at least one second between requests to the same host, and longer when `Crawl-delay` asks for it, up to 10 seconds. It does not log in, does not send cookies you provide, and does not collect personal data. There is no field for an e-mail address, a phone number or an account name.

A site's terms may still restrict automated access. You are responsible for the sites you submit. If a rights holder complains about this Actor, the publisher should take it down. This project is **not affiliated** with Wappalyzer, BuiltWith, Apify's competitors, or any website you scan. Product names are used only to describe a technical signal.

### Pricing

Pay per event. The price is about **$0.003 to $0.006 per website** whose page was read, plus a start event. The prices in `pricing-ppe.json` are set on the platform:

| Event | Proposed free-plan price | When it is charged |
|---|---|---|
| Actor start (`apify-actor-start`) | $0.0005 | Once per run, by the platform (one event per GB of memory, at least one) |
| Website checked (`site-checked`) | $0.005 | Once per website whose page was read, including a baseline and a run that finds no change |

Paid plans in the price file step down from that $0.005: Bronze $0.0045, Silver $0.004, Gold and above $0.0035. All of those sit inside the $0.003 to $0.006 band.

A website is charged only when the read succeeds (`baseline_saved` or `compared`). Technology rows are included. These are not charged: `robots.txt` disallows, bot walls, refusals, unreachable hosts, invalid entries, and websites skipped because the run reached its maximum charge (`not_fetched`). The previous baseline of a skipped website is left as it was.

`Actor.charge` returning zero events is not a spending limit. The Apify SDK does that when the Actor has no pay-per-event price yet, and it leaves the limit flag off. The run still writes the rows. A real limit sets the limit flag, and only then are later websites left unread.

To cap spending, pass fewer websites or set a maximum cost per run. One website on the free-plan proposal is $0.0055 with the start event. Fifty websites, all read, are $0.2505.

Public list prices for the products people compare this work with, checked in the research for this Actor (5 November 2026): BuiltWith Basic is listed at $295 per month, and Wappalyzer Business at $450 per month. Those products are not called by this Actor, and those figures are not this Actor's price.

### Tips and limits

The home page is the default. A tag that exists only on the checkout or the blog will not show up unless you pass that URL. One address is one page, not a crawl.

A new fingerprint in a later version changes the catalogue version and starts a fresh baseline. That avoids a false "added" flood when the only thing that changed is the detector.

Web server and programming-language headers are easy to hide. Treat `nginx`, `Apache` and `PHP` as hints. Prefer a script host or a plugin path when you are making a decision.

### Support

Open an issue on the [GitHub repository](https://github.com/mmdincer/apify) with the input (the domain is enough), the run status and the site-row message. Do not paste cookie values or full page HTML if it contains anything private.

# Changelog

This Actor's version history is a separate document: https://apify.com/mmdincer/website-tech-stack-monitor/changelog.md

# Actor input Schema

## `websites` (type: `array`):

Domains or page addresses to check, one per line. <code>example.com</code> reads the home page. <code>https://example.com/pricing</code> reads that path. The query string is ignored. Up to 50 per run.

## `onlyChanges` (type: `boolean`):

After the first run, write a technology row only when a technology was added or removed. The first run still lists the whole stack, with change type <code>baseline</code>. Turn this off to list every detected technology on every run.

## `monitorName` (type: `string`):

Name of the saved baseline. Runs that use the same name compare against each other. Use different names to keep separate watchlists.

## `categories` (type: `array`):

Optional. Limit detection to these categories. Leave empty to use the whole catalogue. Changing this list saves a new baseline on the next run.

## `requestDelayMs` (type: `integer`):

Minimum pause between requests to the same host. The floor is 1000 ms and cannot be lowered. A <code>Crawl-delay</code> in robots.txt is honored up to 10 seconds; a longer delay skips that website.

## Actor input object example

```json
{
  "websites": [
    "https://wordpress.org"
  ],
  "onlyChanges": true,
  "monitorName": "default",
  "requestDelayMs": 1000
}
```

# Actor output Schema

## `technologies` (type: `string`):

Technologies detected on the page. The first run marks them baseline. Later runs show added and removed, and unchanged only when that option is off.

## `sites` (type: `string`):

One summary row per website: baseline saved, added and removed counts, or why a website was skipped.

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

Websites requested, baselines, comparisons, technologies added and removed, and the first problems.

# 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 = {
    "websites": [
        "https://wordpress.org"
    ],
    "onlyChanges": true,
    "monitorName": "default",
    "requestDelayMs": 1000
};

// Run the Actor and wait for it to finish
const run = await client.actor("mmdincer/website-tech-stack-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 = {
    "websites": ["https://wordpress.org"],
    "onlyChanges": True,
    "monitorName": "default",
    "requestDelayMs": 1000,
}

# Run the Actor and wait for it to finish
run = client.actor("mmdincer/website-tech-stack-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 '{
  "websites": [
    "https://wordpress.org"
  ],
  "onlyChanges": true,
  "monitorName": "default",
  "requestDelayMs": 1000
}' |
apify call mmdincer/website-tech-stack-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,mmdincer/website-tech-stack-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/OcYfQmeejoAQNkw9R/builds/YpAgAkk6dxKELvJZf/openapi.json
