# Materiality Watch — Criteria-Filtered Change Monitor (`shawn_madadha/materiality-watch`) Actor

Monitor multiple URLs on a schedule. Define what matters in plain English and Claude AI filters every diff against YOUR criteria. Portfolio mode, consolidated digests, per-URL timeline, webhook support.

- **URL**: https://apify.com/shawn\_madadha/materiality-watch.md
- **Developed by:** [Shawn Madadha](https://apify.com/shawn_madadha) (community)
- **Categories:** AI, Automation, Developer tools
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, NaN bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.01 / 1,000 results

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.

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

## What's an Apify Actor?

Actors are a software tools running on the Apify platform, for all kinds of web data extraction and automation use cases.
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.

In JavaScript/TypeScript projects, use official [JavaScript/TypeScript client](https://docs.apify.com/api/client/js.md):

```bash
npm install apify-client
```

In Python projects, use official [Python client library](https://docs.apify.com/api/client/python.md):

```bash
pip install apify-client
```

In shell scripts, use [Apify CLI](https://docs.apify.com/cli/docs.md):

````bash
# MacOS / Linux
curl -fsSL https://apify.com/install-cli.sh | bash
# Windows
irm https://apify.com/install-cli.ps1 | iex
```bash

In AI frameworks, you might use the [Apify MCP server](https://docs.apify.com/platform/integrations/mcp.md).

If your project is in a different language, use 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

## Materiality Watch

#### AI-powered website change monitoring that only alerts you on what actually matters.

Every change monitor floods you with noise. Footer edits, ad rotation, timestamp updates, hero image swaps. You set up monitoring to stay informed — instead you learn to ignore it.

**Materiality Watch flips the model.** You describe, in plain English, what kinds of changes you care about. Claude AI evaluates every diff against *your* criteria and suppresses everything else.

> *"Pricing changes, API deprecations, leadership changes. Ignore blog posts, copy polish, and navigation tweaks."*

Same page. Same diff. Different verdict for different users.

---

### Why this exists

There are 10+ website change monitors on the [Apify Store](https://apify.com/store). They all do the same thing: scrape a URL, diff it, summarize with generic AI. The category exists but is unwon — every competitor sits at 2-29 total users.

The gap: **nobody lets the user define what "material" means.**

| | Generic monitors | Materiality Watch |
|---|---|---|
| **Filter logic** | "Is this significant?" (generic) | Your criteria in plain English |
| **URLs per run** | Usually 1 | 1-50 (portfolio mode) |
| **Output** | One alert per URL | One consolidated digest |
| **History** | Last snapshot only | Per-URL timeline (50 events) |
| **Webhook** | Per-URL fire | Single consolidated payload |
| **False positives** | High | Low (criteria-filtered) |

---

### How it works

````

URLs + Criteria ──> Fetch ──> Diff ──> Claude AI Filter ──> Material changes only
│
"Does this match the
user's criteria?"
│
┌────────┴────────┐
│                  │
YES                 NO
┌─────┴─────┐       (suppressed
│           │        silently)
Per-change   Consolidated
records      digest
│           │
└─────┬─────┘
│
Webhook POST
(single fire/run)

````

On each scheduled run:

1. **Fetch** every URL concurrently, extract clean text via `trafilatura`
2. **Diff** against the last snapshot in Apify's key-value store
3. **Classify** with Claude Haiku — evaluate each diff against your criteria, score significance 1-5, tag categories
4. **Suppress** immaterial changes silently
5. **Record** material changes to the dataset with structured metadata
6. **Digest** — Claude writes a 3-6 sentence executive summary across all changes
7. **Webhook** — one consolidated POST per run (not one per URL)
8. **Timeline** — append to per-URL change history for audit trail

---

### Use cases

| Who | Watches | Criteria example |
|---|---|---|
| **Founders** | Competitor pricing pages | *"Pricing changes, plan name or tier changes, new product launches"* |
| **DevRel** | Upstream API docs | *"API deprecations, breaking changes, new endpoints, schema changes"* |
| **Compliance** | Regulatory bodies, T&C pages | *"New requirements, enforcement actions, regulatory language changes"* |
| **Growth** | Competitor landing pages | *"Value prop changes, CTA changes, social proof updates"* |
| **Security** | Vendor security pages | *"New vulnerabilities, policy changes, incident disclosures"* |

---

### Quick start

#### Input

```json
{
  "urls": [
    "https://www.anthropic.com/pricing",
    "https://openai.com/api/pricing/"
  ],
  "criteria": "Pricing changes, model availability, rate limit changes. Ignore blog posts and footer changes.",
  "anthropic_api_key": "sk-ant-...",
  "emit_digest": true
}
````

#### Run locally

```bash
apify run --purge
```

The first run establishes baselines (no alerts). Subsequent runs detect and classify changes.

#### Deploy

```bash
apify push
```

Then schedule in the [Apify Console](https://console.apify.com) — e.g., every 6 hours.

***

### Output

#### Per material change

```json
{
  "url": "https://example.com/pricing",
  "detected_at": "2026-05-07T14:00:00+00:00",
  "is_material": true,
  "significance": 4,
  "categories": ["pricing", "plan-name"],
  "summary": "The 'Pro' tier was renamed to 'Team' and price increased from $20 to $25/month.",
  "diff_excerpt": "-Pro Plan — $20/month\n+Team Plan — $25/month"
}
```

#### Consolidated digest

```json
{
  "type": "digest",
  "n_changes": 3,
  "digest": "Anthropic raised Pro to $25 and renamed it to Team. OpenAI added a new tier. No changes to Mistral pricing."
}
```

***

### Architecture

```
src/
├── main.py          ## Actor logic — fetch, diff, classify, digest
├── __main__.py      ## Entrypoint
└── __init__.py

.actor/
├── actor.json       ## Apify actor configuration
├── input_schema.json
└── Dockerfile
```

**Stack:** Python 3.13 + Apify SDK + httpx + trafilatura + Claude API (Haiku)

**Cost:** ~$0.02/run + ~$0.001-0.003 per URL in Anthropic API costs. A portfolio of 20 URLs checked 4x/day runs about $5/month.

***

### Input reference

| Field | Required | Default | Description |
|---|---|---|---|
| `urls` | Yes | — | URLs to monitor (1-50) |
| `criteria` | No | General materiality rules | Plain-English filter for what matters |
| `anthropic_api_key` | No | `ANTHROPIC_API_KEY` env var | Required for AI filtering |
| `webhook_url` | No | — | Consolidated POST on material changes |
| `emit_digest` | No | `true` | Generate executive digest per run |
| `classify_model` | No | `claude-haiku-4-5-20251001` | Model for diff classification |
| `digest_model` | No | `claude-haiku-4-5-20251001` | Model for digest generation |

***

### MCP Server (Agent Bridge)

This repo also includes an MCP server (`src/index.ts`) that wraps Apify actors as agent-friendly tools — including `monitor_url` which routes to this actor. See the [build plan](https://github.com/ShawnMadadha/materiality-watch#mcp-server-agent-bridge) for details on the 7 high-level tools:

`scrape_url` · `web_search` · `web_research` · `monitor_url` · `extract_structured` · `find_actor` · `run_actor`

***

### Roadmap

- \[ ] CSS selector / XPath restriction (diff only a region of the page)
- \[ ] Image diffing for visual changes
- \[ ] RSS feed ingestion alongside URLs
- \[ ] Slack / Discord webhook formatting
- \[ ] Named watchlists with persistent config

***

### License

MIT

# Actor input Schema

## `urls` (type: `array`):

The list of webpages to monitor in this run. Each URL is tracked independently with its own snapshot history.

## `criteria` (type: `string`):

Plain-English description of what kinds of changes you actually want to be alerted about. Claude filters every detected diff against this. Be specific — vague criteria let trivia through. Examples: 'pricing or plan name changes only'; 'API deprecations, schema changes, or new endpoints'; 'leadership names on the about page'; 'regulatory language changes in the Terms of Service'.

## `anthropic_api_key` (type: `string`):

Required for criteria-based filtering and digest generation. Without it, every diff is reported with default significance (no filtering, no digest).

## `webhook_url` (type: `string`):

If provided, a single consolidated payload (with digest + all material changes for this run) is POSTed here whenever any material change is detected.

## `emit_digest` (type: `boolean`):

If true, generate one synthesized digest across all material changes per run and push it as a separate dataset record.

## `classify_model` (type: `string`):

Anthropic model used to filter diffs against criteria. Haiku is fast/cheap and works well for this. Override only if you have a reason.

## `digest_model` (type: `string`):

Anthropic model used to write the consolidated digest. Haiku is the default. Use a Sonnet model if you want richer prose.

## Actor input object example

```json
{
  "urls": [
    "https://www.anthropic.com/pricing",
    "https://www.anthropic.com/news"
  ],
  "criteria": "Pricing changes, plan or tier changes, new product launches, leadership changes, API deprecations or breaking changes, and changes to Terms or Privacy Policy. Ignore copy polish, blog posts, social embeds, navigation tweaks, and timestamps.",
  "emit_digest": true,
  "classify_model": "claude-haiku-4-5-20251001",
  "digest_model": "claude-haiku-4-5-20251001"
}
```

# Actor output Schema

## `materialChanges` (type: `string`):

Structured records of website changes that matched the user's materiality criteria. Each record includes the URL, detection timestamp, significance score (1-5), category tags, a plain-English summary, and a diff excerpt.

## `digests` (type: `string`):

Consolidated 3-6 sentence summaries synthesizing all material changes detected in a single run. One digest per run, only generated when material changes exist and emit\_digest is enabled.

## `changeTimelines` (type: `string`):

Historical change records per monitored URL, stored in the key-value store. Each URL maintains up to 50 timeline entries with timestamps, significance scores, categories, and summaries.

# 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 = {
    "urls": [
        "https://www.anthropic.com/pricing",
        "https://www.anthropic.com/news"
    ],
    "criteria": "Pricing changes, plan or tier changes, new product launches, leadership changes, API deprecations or breaking changes, and changes to Terms or Privacy Policy. Ignore copy polish, blog posts, social embeds, navigation tweaks, and timestamps."
};

// Run the Actor and wait for it to finish
const run = await client.actor("shawn_madadha/materiality-watch").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 = {
    "urls": [
        "https://www.anthropic.com/pricing",
        "https://www.anthropic.com/news",
    ],
    "criteria": "Pricing changes, plan or tier changes, new product launches, leadership changes, API deprecations or breaking changes, and changes to Terms or Privacy Policy. Ignore copy polish, blog posts, social embeds, navigation tweaks, and timestamps.",
}

# Run the Actor and wait for it to finish
run = client.actor("shawn_madadha/materiality-watch").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 '{
  "urls": [
    "https://www.anthropic.com/pricing",
    "https://www.anthropic.com/news"
  ],
  "criteria": "Pricing changes, plan or tier changes, new product launches, leadership changes, API deprecations or breaking changes, and changes to Terms or Privacy Policy. Ignore copy polish, blog posts, social embeds, navigation tweaks, and timestamps."
}' |
apify call shawn_madadha/materiality-watch --silent --output-dataset

```

## MCP server setup

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

```

## OpenAPI specification

```json
{
    "openapi": "3.0.1",
    "info": {
        "title": "Materiality Watch — Criteria-Filtered Change Monitor",
        "description": "Monitor multiple URLs on a schedule. Define what matters in plain English and Claude AI filters every diff against YOUR criteria. Portfolio mode, consolidated digests, per-URL timeline, webhook support.",
        "version": "0.1",
        "x-build-id": "BKmTEF5gXdyjjW3wK"
    },
    "servers": [
        {
            "url": "https://api.apify.com/v2"
        }
    ],
    "paths": {
        "/acts/shawn_madadha~materiality-watch/run-sync-get-dataset-items": {
            "post": {
                "operationId": "run-sync-get-dataset-items-shawn_madadha-materiality-watch",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for its completion, and returns Actor's dataset items in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        },
        "/acts/shawn_madadha~materiality-watch/runs": {
            "post": {
                "operationId": "runs-sync-shawn_madadha-materiality-watch",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor and returns information about the initiated run in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/runsResponseSchema"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/acts/shawn_madadha~materiality-watch/run-sync": {
            "post": {
                "operationId": "run-sync-shawn_madadha-materiality-watch",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for completion, and returns the OUTPUT from Key-value store in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        }
    },
    "components": {
        "schemas": {
            "inputSchema": {
                "type": "object",
                "required": [
                    "urls"
                ],
                "properties": {
                    "urls": {
                        "title": "URLs to watch",
                        "type": "array",
                        "description": "The list of webpages to monitor in this run. Each URL is tracked independently with its own snapshot history.",
                        "items": {
                            "type": "string"
                        }
                    },
                    "criteria": {
                        "title": "Materiality criteria",
                        "type": "string",
                        "description": "Plain-English description of what kinds of changes you actually want to be alerted about. Claude filters every detected diff against this. Be specific — vague criteria let trivia through. Examples: 'pricing or plan name changes only'; 'API deprecations, schema changes, or new endpoints'; 'leadership names on the about page'; 'regulatory language changes in the Terms of Service'."
                    },
                    "anthropic_api_key": {
                        "title": "Anthropic API key",
                        "type": "string",
                        "description": "Required for criteria-based filtering and digest generation. Without it, every diff is reported with default significance (no filtering, no digest)."
                    },
                    "webhook_url": {
                        "title": "Webhook URL (optional)",
                        "type": "string",
                        "description": "If provided, a single consolidated payload (with digest + all material changes for this run) is POSTed here whenever any material change is detected."
                    },
                    "emit_digest": {
                        "title": "Emit consolidated digest",
                        "type": "boolean",
                        "description": "If true, generate one synthesized digest across all material changes per run and push it as a separate dataset record.",
                        "default": true
                    },
                    "classify_model": {
                        "title": "Classifier model (advanced)",
                        "type": "string",
                        "description": "Anthropic model used to filter diffs against criteria. Haiku is fast/cheap and works well for this. Override only if you have a reason.",
                        "default": "claude-haiku-4-5-20251001"
                    },
                    "digest_model": {
                        "title": "Digest model (advanced)",
                        "type": "string",
                        "description": "Anthropic model used to write the consolidated digest. Haiku is the default. Use a Sonnet model if you want richer prose.",
                        "default": "claude-haiku-4-5-20251001"
                    }
                }
            },
            "runsResponseSchema": {
                "type": "object",
                "properties": {
                    "data": {
                        "type": "object",
                        "properties": {
                            "id": {
                                "type": "string"
                            },
                            "actId": {
                                "type": "string"
                            },
                            "userId": {
                                "type": "string"
                            },
                            "startedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "finishedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "status": {
                                "type": "string",
                                "example": "READY"
                            },
                            "meta": {
                                "type": "object",
                                "properties": {
                                    "origin": {
                                        "type": "string",
                                        "example": "API"
                                    },
                                    "userAgent": {
                                        "type": "string"
                                    }
                                }
                            },
                            "stats": {
                                "type": "object",
                                "properties": {
                                    "inputBodyLen": {
                                        "type": "integer",
                                        "example": 2000
                                    },
                                    "rebootCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "restartCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "resurrectCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "computeUnits": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "options": {
                                "type": "object",
                                "properties": {
                                    "build": {
                                        "type": "string",
                                        "example": "latest"
                                    },
                                    "timeoutSecs": {
                                        "type": "integer",
                                        "example": 300
                                    },
                                    "memoryMbytes": {
                                        "type": "integer",
                                        "example": 1024
                                    },
                                    "diskMbytes": {
                                        "type": "integer",
                                        "example": 2048
                                    }
                                }
                            },
                            "buildId": {
                                "type": "string"
                            },
                            "defaultKeyValueStoreId": {
                                "type": "string"
                            },
                            "defaultDatasetId": {
                                "type": "string"
                            },
                            "defaultRequestQueueId": {
                                "type": "string"
                            },
                            "buildNumber": {
                                "type": "string",
                                "example": "1.0.0"
                            },
                            "containerUrl": {
                                "type": "string"
                            },
                            "usage": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "integer",
                                        "example": 1
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "usageTotalUsd": {
                                "type": "number",
                                "example": 0.00005
                            },
                            "usageUsd": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "number",
                                        "example": 0.00005
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
```
