Workers

Competitor watcher

Periodic competitor surveillance. For each tracked competitor brand, checks several signal surfaces, diffs against the last snapshot, writes any detected change to competitor_changes. Scores changes for threat magnitude so the dashboard's competitor panel can highlight what matters.

Updated today

Overview

At a glance
Worker
watcher
Workflow
WatcherWorkflow
Sibling workers
scrape, render, search
D1
ultron-operations (reads brands, writes competitor_changes)
News feeds
techcrunch, venturebeat, the-verge, wired, techmeme
Output
competitor_changes rows scored by ThreatComponents

Worker shape

wrangler.toml (key bindings)toml
1name = "watcher"
2main = "src/index.ts"
3
4[[workflows]]
5name = "competitor-watcher"
6binding = "WATCHER"
7class_name = "WatcherWorkflow"
8
9[[d1_databases]]
10binding = "OPS_DB"
11database_name = "ultron-operations"
12
13[[kv_namespaces]]
14binding = "SHARED_CONFIG"
15
16[triggers]
17crons = ["0 */6 * * *"] # every 6 hours

Signal sources

Each brand has a configurable subset.

SignalHow it is fetched
Pricing page diffscrape worker fetches pricing page, hash compared to last snapshot
Hero copy diffscrape worker on the homepage hero block
Press mentionssearch worker against the five news feeds plus general web
Open job postingsultron-jobs / ultron-linkedin-jobs-guest results filtered to the brand
Funding announcementssearch + Pitchbook profile diff
Review velocityg2-reviews and capterra-reviews row counts vs last tick
Social cadenceInstagram + TikTok trend dataset for the brand's handles
Github activityscrape worker on the brand's primary github org
Sentiment shiftsearch worker plus a small LLM classification pass over recent mentions

Workflow steps

WatcherWorkflow.run sketchts
1async run(event, step) {
2 const brands = await step.do('load-brands',
3 () => loadActiveBrands(this.env)
4 )
5
6 for (const brand of brands) {
7 const signals = brand.signals // JSON array of enabled signal keys
8
9 for (const sig of signals) {
10 // Per-brand-per-source check is its own step so one broken
11 // source for one brand never aborts the whole tick.
12 await step.do(`check-${brand.id}-${sig}`, async () => {
13 const result = await checkSignal(this.env, brand, sig)
14 if (result.changed) {
15 await postCustomEvent(this.env, brand, result.change)
16 }
17 })
18 }
19 }
20}

Threat components

Each change is scored across five dimensions.

DimensionRangeDriven by
impact0 to 5Magnitude of the change (e.g. funding round size vs hiring a single engineer)
urgency0 to 5How fresh the change is and whether it is mid-cycle (pricing drop tomorrow vs last quarter)
overlap0 to 5How much the change targets the workspace's ICP
proximity0 to 5How close geographically and category-wise
confidence0 to 5Signal source reliability
Note
The composite threat score is the weighted sum used by the dashboard's competitor panel to sort changes. Operators can retune weights per workspace in SHARED_CONFIG.

Change rows

ultron-operations.competitor_changessql
1CREATE TABLE competitor_changes (
2 id TEXT PRIMARY KEY,
3 brand_id TEXT NOT NULL REFERENCES brands(id),
4 kind TEXT NOT NULL, -- 'pricing' | 'press' | 'hire' | ...
5 title TEXT NOT NULL,
6 body TEXT,
7 source_url TEXT,
8 source_label TEXT,
9 threat_impact INTEGER,
10 threat_urgency INTEGER,
11 threat_overlap INTEGER,
12 threat_proximity INTEGER,
13 threat_confidence INTEGER,
14 raw_json TEXT, -- the full signal payload
15 detected_at TEXT NOT NULL DEFAULT (datetime('now'))
16);

File map

cf-workers/watcher
wrangler.toml
src
index.ts
workflow.tsWatcherWorkflow + checkSignal dispatcher
signals
pricing.ts
hero.ts
press.ts
jobs.ts
funding.ts
reviews.ts
social.ts
github.ts
sentiment.ts
threat.tsThreatComponents scorer