# Company Tech Stack Monitor — Change Events + Firmographics (`johnatan029/company-stack-monitor`) Actor

Buying signals for B2B sales: monitor company domains and get CHANGE EVENTS when a target adopts or removes a tool (Shopify, HubSpot, Marketo...) or switches email provider. Loaded-resource detection, no text-mention false positives. Pay per event written. Not affiliated with detected vendors.

- **URL**: https://apify.com/johnatan029/company-stack-monitor.md
- **Developed by:** [Johnn Mottin](https://apify.com/johnatan029) (community)
- **Categories:** Lead generation, Business, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 change events

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/platform/actors/running/actors-in-store#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

## Company Tech Stack Monitor — Change Events + Firmographics

**A target that just adopted — or dropped — a tool is a company in motion, and that is when they buy.** Today that signal costs $250/month at Wappalyzer or $295/month at BuiltWith, with no pay-per-use option. This Actor watches a batch of company domains and returns **only the change events**: `STACK_ADDED` (they started loading Klaviyo), `STACK_REMOVED` (Marketo is gone), `MX_PROVIDER_CHANGE` (they migrated Google Workspace → Microsoft 365). No login, no browser, no LLM.

**Not affiliated with, sponsored by, or endorsed by any vendor detected** (Shopify, HubSpot, Marketo, Intercom, Salesforce, etc.). All data comes from each company's public homepage, its HTTP response headers, and public DNS.

Who it's for:

- **B2B sales / SDR teams:** "3 of my 80 target accounts changed their stack this week" straight to Slack or your CRM via Apify integrations — a timed reason to reach out.
- **Competitive & partner intel:** track who is winning or losing accounts in your category, week over week.
- **Agencies:** watch a prospect list for the platform migrations that create projects.

### How it works

You provide **domains**. For each one, in order: `robots.txt` (a site that disallows everything is skipped as a controlled status), the homepage (one request), and an MX lookup over public DNS-over-HTTPS. Default mode is **monitor**: the first visit stores a free baseline; later runs diff against it and write one record per change. **Every monitor run also writes one free `RUN_SUMMARY`** with per-domain status — so a week with no changes still yields a non-empty dataset that says "monitored: 80 domains, 0 changes". `snapshot` mode is a one-shot export of the full detected stack plus firmographics.

**You are only charged for events (or snapshot records) actually written.**

### Detection: loaded resources, never text mentions

This is the design decision that makes the product trustworthy. A signature only fires when the tool is **actually loaded**:

- the URL of a `script`/`link`/`iframe`/`img` resource matches a vendor domain (`js.hs-scripts.com`, `static.klaviyo.com`, `cdn.shopify.com`…);
- a **response header** identifies the CDN/host (`server: cloudflare`, `x-vercel-id`…);
- a **structural framework marker** is present (`__NEXT_DATA__`, `ng-version`).

A blog post that *writes about* HubSpot and Marketo loads neither — and produces **zero** detections. There is a dedicated regression test for exactly that page: a false `STACK_ADDED` would be a charged event **and** a false buying signal to a salesperson, which is the worst failure this Actor could have.

The signature library is **versioned** (`signaturesVersion` travels in every record) and extensible — vendors change their script domains over time, so coverage is best-effort and the list evolves. When the library version changes between runs, **removal events are suppressed** for that run: a rewritten signature must never look like a customer who dropped a tool.

### Input

```json
{
  "domains": ["stripe.com", "hubspot.com"],
  "mode": "monitor"
}
```

| Field | Type | Default | Description |
|---|---|---|---|
| `domains` | array | required | 1–100 domains or URLs; protocol/www/path stripped |
| `mode` | string | `monitor` | `monitor` (change events + free summary) or `snapshot` (full export) |
| `eventTypes` | array | all 3 | `STACK_ADDED`, `STACK_REMOVED`, `MX_PROVIDER_CHANGE` |
| `maxResults` | integer | 5000 | Cap on charged records. If it truncates a domain, that domain's state is **not** advanced |
| `requestDelayMs` | integer | 500 | Polite pacing per request (floor 250 ms, deliberate) |

### Output

```json
{
  "recordType": "CHANGE_EVENT",
  "eventType": "STACK_ADDED",
  "domain": "gymshark.com",
  "signatureKey": "klaviyo",
  "signatureLabel": "Klaviyo",
  "category": "marketing",
  "signaturesVersion": "2026-07-30.1",
  "previousCheckedAt": "2026-07-23T06:00:00.000Z",
  "detectedAt": "2026-07-30T06:00:00.000Z"
}
```

`snapshot` writes `DOMAIN_RESULT` records (full `stack[]` with labels and categories, `mxProvider`, `mxRecords`, title, meta description, `resourcesScanned`).

### Schedule it (recommended — cloud, not your desktop)

Stack changes are a weekly signal. **Use Apify's own Schedules, not a local scheduler** — set it once and it runs in the cloud whether or not your machine is on.

1. Save your prospect list as a **Task** (Console → the Actor → *Create task*).
2. Console → **Schedules → Create schedule**, add the Task, set the cron (e.g. Mondays 6am → `0 6 * * 1`).
3. Route the dataset to Slack/Sheets/your CRM via Apify integrations — new events only.

### Honest limits (read before relying on it)

- **Homepage only.** Detection reads the site's front page, not a deep crawl. A tool used only inside a checkout, a subdomain or a logged-in area will not be seen. This is not a BuiltWith-scale profiler and does not claim to be.
- **Tools injected by inline JavaScript are not detected.** A signature fires only on statically loaded resources (script/link/iframe/img URLs, response headers, structural markers). A tool bootstrapped exclusively by inline JS — e.g. injected at runtime by a tag manager — leaves no static resource URL in the HTML and produces a documented **false negative**. Detection is strict by design: a false `STACK_ADDED` would be a charged event and a false buying signal to a salesperson, which is worse. Coverage is best-effort; the signature list evolves.
- **Best-effort coverage.** ~40 curated signatures across e-commerce, CMS, marketing/CRM, support, analytics, payments, frameworks and CDN/hosting. Vendors change their delivery domains; the versioned library evolves, and `signaturesVersion` on every record tells you which vintage produced it.
- **Suspicious responses never produce removals.** If the homepage comes back implausibly small (challenge/error page) or detection drops to zero when history had signatures, the domain is marked `DETECTION_DEGRADED`: no events, and **the stored state is left untouched** so the next healthy run compares against real data.
- **Per-domain robots respected.** `User-agent: * / Disallow: /` means the domain is skipped (`DOMAIN_ROBOTS_DISALLOWED`), never fetched. A 403 becomes `DOMAIN_BLOCKED` — no proxy rotation, no evasion.
- **MX is enrichment, not gospel.** A DNS failure leaves `mxProvider: null` instead of inventing a change; `other` means a provider outside the recognized list.
- **Not covered in v0.1** (measured and deliberately excluded): JSON-LD `Organization` data is unreliable (absent on 5/5 sampled sites) and the unauthenticated GitHub org API allows only 60 requests/hour, which cannot sustain batches. Both may return as opt-in enrichments later.

### Ops notes

- `STATS`: per-domain summary (status, stack size, MX, events), HTTP counters, degraded/suppressed counters, field-completeness check, warnings. `ERRORS` on failure.
- Diff state lives in the named key-value store `COMPANY-STACK-STATE`, one key per domain. Delete a key to force a fresh baseline.
- Cost drivers: up to 3 requests per domain per run (robots + homepage + DNS) plus records written.

### FAQ

**Do I need an account or API key for any of the detected vendors?** No. The Actor reads each company's public homepage, its HTTP response headers and public DNS — no login anywhere.

**Why did it miss a tool I know the company uses?** Detection only fires on statically loaded resources. A tool injected exclusively by inline JavaScript (for example through a tag manager), or one used only in a checkout, a subdomain or a logged-in area, is a documented false negative — see "Honest limits" above. The Actor never guesses.

**What exactly am I charged for?** Per record written to the dataset (Pay Per Event) — everything discarded by filters or caps costs you nothing, and the `RUN_SUMMARY` record is free. The Pricing tab on this page is always the authoritative source for current rates and for any per-run fee.

**Can I schedule it?** Yes — that is the intended use. See "Schedule it" above.

**Is this affiliated with the vendors it detects?** No. This is an unofficial community Actor, not affiliated with, sponsored by or endorsed by any vendor detected.

# Actor input Schema

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

1–100 domains or URLs (e.g. "stripe.com"). Protocol, www and paths are stripped. Each domain's robots.txt is checked first — a site that disallows everything is skipped as a controlled status.

## `mode` (type: `string`):

monitor (default): diff vs the previous run — only change events are charged; first visit of a domain creates a free baseline. snapshot: full stack + firmographics export, one record per domain.

## `eventTypes` (type: `array`):

Which change events to emit. Default: all of STACK\_ADDED, STACK\_REMOVED, MX\_PROVIDER\_CHANGE.

## `maxResults` (type: `integer`):

Global cap on charged records. The free run summary does not count. If the cap truncates a domain's events, that domain's diff state is NOT advanced — nothing is silently lost.

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

Polite pacing before each request (robots → homepage → DNS are serial per domain; up to 4 domains in parallel). The 250 ms floor is deliberate.

## `debug` (type: `boolean`):

Verbose logs.

## Actor input object example

```json
{
  "domains": [
    "stripe.com",
    "hubspot.com"
  ],
  "mode": "monitor",
  "eventTypes": [
    "STACK_ADDED",
    "STACK_REMOVED",
    "MX_PROVIDER_CHANGE"
  ],
  "maxResults": 5000,
  "requestDelayMs": 500,
  "debug": false
}
```

# 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": [
        "stripe.com",
        "hubspot.com"
    ],
    "mode": "monitor"
};

// Run the Actor and wait for it to finish
const run = await client.actor("johnatan029/company-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 = {
    "domains": [
        "stripe.com",
        "hubspot.com",
    ],
    "mode": "monitor",
}

# Run the Actor and wait for it to finish
run = client.actor("johnatan029/company-stack-monitor").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).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": [
    "stripe.com",
    "hubspot.com"
  ],
  "mode": "monitor"
}' |
apify call johnatan029/company-stack-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=johnatan029/company-stack-monitor",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

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