RivalTrace: what changed on competitor websites this week, and what it means
Snapshots of competitor data are compared week to week. Only the difference goes to Claude, which returns a fixed-format brief: what changed, what it means, what to do, and a threat level.
At a glance
Competitor pages change quietly, and nobody has time to check them every week.
Pricing, plan limits, headlines and feature lists change without announcement. Checking them by hand is the kind of job that gets dropped in a busy week.
Sending whole pages to a model every week is expensive and noisy. RivalTrace compares this week’s snapshot with last week’s in code first, stops when nothing changed, and only asks the model to explain the difference.
Snapshot
An Apify actor captures the competitor’s data; it is stored as a dated snapshot.
How it works
Each stage as it exists in the repository. Select a stage to see what it uses and what happens when it fails.
Apify snapshot
Runs the configured Apify actor and stores the result as a snapshot
apify-client, PostgreSQL
ApifyScrapeError; no snapshot stored
Execution sequence
One item, traced through the system boundaries.
- n8n schedule to RivalTrace API: POST webhook (shared secret)
- RivalTrace API to RivalTrace API: for each competitor: DeepDiff two newest snapshots
- RivalTrace API to RivalTrace API: no change → skip (no model call)
- RivalTrace API to Claude: sanitised delta only
- Claude replies to RivalTrace API: CompetitorInsight JSON
- RivalTrace API to Email / Slack: HTML brief + Slack message
One item, start to finish
The first snapshot of a competitor is stored. With nothing to compare, the run treats it all as new.
Next week the Team plan price differs. DeepDiff reports one modified value.
Only that change is sent to Claude, which returns what changed, what it means and what to do.
The brief is saved to history, emailed, and posted to Slack if a webhook is set.
The week-to-week comparison
Edit this week’s values. The output uses the same three buckets the delta engine produces.
Each run stores the scraped data as a snapshot and compares it with the previous one. Only the difference is sent to Claude. Edit this week's values; the first column is last week's snapshot.
Logic copied from RivalTrace app/services/delta_engine.py and app/services/data_sanitizer.py. Inputs are yours to change; nothing here calls a model or a server.
Architecture
The layers and the decision path, taken from the repository code.
| Layer | Component | Tech Stack | Responsibility |
|---|---|---|---|
| Layer 1: Trigger | API and webhook | FastAPI, shared-secret webhook for n8n | Start capture and briefing runs |
| Layer 2: Memory | SnapshotsKEY | PostgreSQL, SQLAlchemy, Alembic | Dated competitor state and briefing history |
| Layer 3: Analysis | Delta, then modelKEY | DeepDiff, sanitiser, Claude, Pydantic | Find the change in code; explain it with the model |
| Layer 4: Delivery | Brief and portal | Resend, Slack webhook, Next.js portal, Stripe billing | Send and show briefs |
- Weekly trigger goes to DeepDiff: two newest snapshots
- DeepDiff goes to Delta empty?: added / removed / modified
- Delta empty? goes to Claude insight: changed
- Delta empty? falls back to Skip: no change
- Claude insight goes to Email / Slack: validated brief
Decisions and trade-offs
| Decision | Instead of | Why | Where in the code |
|---|---|---|---|
| Compare in code before calling a model | Sending full pages to the model every week | Most weeks most pages do not change; those runs cost nothing and produce no noise. | app/services/delta_engine.py, app/api/v1/orchestration.py |
| Structured snapshots, not screenshots | Visual page diffs | Comparing data means a moved button or new cookie banner is not a change unless the data changes. | app/models/snapshot.py |
| Fixed brief schema | Free-text model output | Every brief has the same sections and a threat level, validated by Pydantic before it is stored. | app/schemas/insights.py |
| Bounded model input | Unbounded deltas | The sanitised delta is capped at 15,000 characters, so one noisy page cannot blow up a run. | app/services/data_sanitizer.py |
Known limits
Only as good as the scraper
RivalTrace compares what the Apify actor returns. If a site blocks the actor or changes layout, the snapshot changes too and needs checking.
Schedule lives outside
Weekly runs are triggered by an external scheduler such as n8n calling the webhook. There is no scheduler inside the service.
Small test suite
The repository tests the deterministic core with six focused pytest cases. The model and delivery paths are not covered by tests.
Results
Only numbers produced by a committed, repeatable script appear here as measured. Until then, each result says how it will be measured.
On a fixed, committed set of saved snapshot pairs with known real and cosmetic changes: share of reported changes that are real.
Share of competitor checks with an empty delta over the same snapshot set.
Only measurable with a client.
Run it yourself
Local quickstart from the README. Full instructions ↗
git clone https://github.com/arslanvuzmal/RivalTrace.git
cd RivalTrace
cp .env.example .env # set WEBHOOK_SECRET, JWT_SECRET_KEY and provider keys
docker compose up --build -d
docker compose exec app alembic upgrade head
# portal: cd frontend && npm install && NEXT_PUBLIC_API_URL=http://localhost:8000 npm run devQuestions people ask first
Which pages can it watch?
Whatever an Apify actor can capture from public pages. RivalTrace stores and compares the actor’s output; it does not log in to anything.
How does it avoid noise?
It compares structured data, not screenshots, and skips the model entirely when nothing changed.
Where do briefs go?
An HTML email through Resend, a Slack message if a webhook is set, and the briefing history in the client portal.
How often does it run?
Whenever it is triggered. The intended setup is a weekly schedule in n8n calling the secured webhook.
Related reading & systems
Want RivalTrace working in your business?
Book a 20-minute scoping call. I will review your current systems, map where manual friction is costing you time, and give you an honest quote.