Skip to content
Arslan Vuzmal Lone
PROJECT · INTELLIGENCE · COMPETITOR MONITORING

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.

By Arslan Vuzmal Lone·Built July 2026·Updated August 2026·Open source
RivalTrace → weekly briefOne run
HIGHCompetitor A · what changed
Team plan £49/mo → £79/moPricing
→What it means
Moving up-market; small teams may look elsewhere
→What to do
Mention fixed pricing on sales calls this month
—Competitor B
No change since last snapshot · skipped
Figure 1:The sections every brief has. Sample data.

At a glance

FOR
Founders and product teams who track a few competitors
DOES
Snapshot · compare · interpret the change · brief by email and Slack
INPUT
Competitor data captured by Apify actors
OUTPUT
Weekly HTML email, optional Slack message, briefing history in a portal
BUILT WITH
FastAPI, PostgreSQL, Alembic, DeepDiff, Claude, Resend, Next.js
HUMAN IN LOOP
People read the brief and decide; nothing acts on its own

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.

One competitor through a weekly runStep 1 of 4
Step 01

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.

How it runsSelect a step · arrow keys move
Capture
Compare
Interpret
Deliver
Step 01 · Capture

Apify snapshot

What happens

Runs the configured Apify actor and stores the result as a snapshot

Uses

apify-client, PostgreSQL

If it fails

ApifyScrapeError; no snapshot stored


Execution sequence

One item, traced through the system boundaries.

A weekly run, triggered from n8n
  1. n8n schedule to RivalTrace API: POST webhook (shared secret)
  2. RivalTrace API to RivalTrace API: for each competitor: DeepDiff two newest snapshots
  3. RivalTrace API to RivalTrace API: no change → skip (no model call)
  4. RivalTrace API to Claude: sanitised delta only
  5. Claude replies to RivalTrace API: CompetitorInsight JSON
  6. RivalTrace API to Email / Slack: HTML brief + Slack message

One item, start to finish

1. Baseline

The first snapshot of a competitor is stored. With nothing to compare, the run treats it all as new.

2. A price moves

Next week the Team plan price differs. DeepDiff reports one modified value.

3. Interpreted

Only that change is sent to Claude, which returns what changed, what it means and what to do.

4. Delivered

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.

plan_starter: £29/mo
plan_team: £49/mo
headline: Built for enterprise
integrations: Slack, Teams
modified_items: plan_team: "£49/mo" → "£79/mo"; integrations: "Slack, Teams" → "Slack, Teams, WhatsApp"
removed_items: none
added_items: none
This delta, stripped of HTML and cut to 15,000 characters, is what Claude is asked to interpret.

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.

System Architecture StackTop-down request path
System Architecture Stack
LayerComponentTech StackResponsibility
Layer 1: TriggerAPI and webhookFastAPI, shared-secret webhook for n8nStart capture and briefing runs
Layer 2: MemorySnapshotsKEYPostgreSQL, SQLAlchemy, AlembicDated competitor state and briefing history
Layer 3: AnalysisDelta, then modelKEYDeepDiff, sanitiser, Claude, PydanticFind the change in code; explain it with the model
Layer 4: DeliveryBrief and portalResend, Slack webhook, Next.js portal, Stripe billingSend and show briefs
RivalTrace decision pathRed dashed line = fallback route
  • 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

DecisionInstead ofWhyWhere in the code
Compare in code before calling a modelSending full pages to the model every weekMost 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 screenshotsVisual page diffsComparing data means a moved button or new cookie banner is not a change unless the data changes.app/models/snapshot.py
Fixed brief schemaFree-text model outputEvery brief has the same sections and a threat level, validated by Pydantic before it is stored.app/schemas/insights.py
Bounded model inputUnbounded deltasThe sanitised delta is capped at 15,000 characters, so one noisy page cannot blow up a run.app/services/data_sanitizer.py
Source: Read from github.com/arslanvuzmal/RivalTrace

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.

To be measured
Change-detection precision

On a fixed, committed set of saved snapshot pairs with known real and cosmetic changes: share of reported changes that are real.

To be measured
Runs that skip the model

Share of competitor checks with an empty delta over the same snapshot set.

To be measured
Value to a team

Only measurable with a client.


Run it yourself

Local quickstart from the README. Full instructions ↗

bash
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 dev
Versions: Python · PostgreSQL · Docker Compose · Node.js

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

AI Workflow Audit · €390 · Credited toward build · or email arslanvuzmallone@gmail.com