Claude Watched 23 Competitor SKUs. Caught 11 Price Moves in

Everyone's aiming Claude agents at the stock market. Wrong target. On Wall Street your competition is HFT bots colocated inside the exchange. On your competitor's pricing page, your competition is Karen in ops pushing a CSV to the CMS on Wednesday afternoon. That's a signal slow enough to actually act on — and one I've been running for a fabrication client for the last six weeks.
Here are the receipts, the schema, and the exact 3-level agent wiring.
The pain nobody line-items on the P&L
A small B2B shop — fabrication, agency, wholesale distributor — burns 2 to 4 hours a week manually checking competitor sites. Owner opens four tabs Monday morning, eyeballs prices, tries to remember what they were last week, gives up, sends quotes at last month's numbers. Two things happen:
- Competitors moved up and you left money on the table.
- Competitors moved down and you lost the deal without knowing why.
On a $40k month, missing a 5% shift on three quotes is roughly $6,000 of margin gone silently. You can't line-item what you never invoiced, so nobody sees it. That's the problem worth automating — not day-trading fantasies.
The pattern I used is the same 3-level agent architecture the trading tutorials teach: setup, detection, autonomous action. Aimed at a target where the underlying signal moves in days, not microseconds.
Level 1: Setup — three pieces, one table
You need three things. That's it.
- Claude via API as the reasoning layer (handles messy HTML across sites that all describe price differently).
- FireCrawl as an MCP server for scraping. Pricing pages have JavaScript, cookie walls, geo redirects, sometimes Cloudflare. You don't want to babysit Playwright scripts on four different sites.
- One Postgres table. Seven columns. That's the whole schema.
CREATE TABLE price_snapshots (
id BIGSERIAL PRIMARY KEY,
sku TEXT NOT NULL,
competitor TEXT NOT NULL,
url TEXT NOT NULL,
price NUMERIC(10,2),
currency CHAR(3) DEFAULT 'USD',
scraped_at TIMESTAMPTZ DEFAULT NOW(),
raw_html_hash TEXT NOT NULL
);
CREATE INDEX idx_sku_time ON price_snapshots (sku, scraped_at DESC);
CREATE INDEX idx_hash ON price_snapshots (raw_html_hash);
The raw_html_hash column is the one that pays for itself. Before sending anything to Claude, you SHA-256 the scraped body and check whether the previous row for that SKU has the same hash. If yes, you skip the LLM call entirely and write nothing new. After the first week, roughly 4 out of 5 pages don't change day-to-day, so token spend drops about 80%.
Build time on this layer: under an hour if Postgres is already running. Two hours cold on a $5 VPS or a home server.
Level 2: Detection — one cron, one Telegram channel
A single scheduled job fires at 06:00 local time. Systemd timer, cron, GitHub Actions — pick your poison. The agent walks the SKU list, calls FireCrawl through MCP to fetch each page, and asks Claude to extract the price from the raw HTML.
The reason you use an LLM for extraction instead of a CSS selector: four competitor sites means four HTML structures, plus one of them re-themes twice a year. A selector-based scraper breaks silently. Claude reading the visible text does not.
The core detection loop:
async def check_sku(sku_row):
page = await firecrawl.scrape(sku_row["url"])
html_hash = hashlib.sha256(page.html.encode()).hexdigest()
last = await db.fetch_one(
"SELECT price, raw_html_hash FROM price_snapshots "
"WHERE sku=$1 ORDER BY scraped_at DESC LIMIT 1",
sku_row["sku"],
)
if last and last["raw_html_hash"] == html_hash:
return # short-circuit, no LLM call, no insert
price = await claude_extract_price(page.markdown, sku_row["sku"])
await db.execute(
"INSERT INTO price_snapshots (sku, competitor, url, price, "
"currency, raw_html_hash) VALUES ($1,$2,$3,$4,$5,$6)",
sku_row["sku"], sku_row["competitor"], sku_row["url"],
price, "USD", html_hash,
)
if last and abs(price - last["price"]) / last["price"] > 0.03:
await telegram_notify(sku_row, last["price"], price)
Two rules that matter:
- 3% threshold. Anything under 3% is noise — rounding, tax recalcs, promo timers. Log it, don't notify. In B2B pricing, sub-3% moves almost never justify a quote revision.
- Telegram is the whole interface. No dashboard, no email digest, no Notion database that nobody opens. One channel. One message per real change. Old price, new price, direct link to the competitor's page. The owner sees it on their phone during morning coffee.
Here's a real notification format that works:
🔺 CompetitorB raised SKU-A1420
$384.00 → $412.50 (+7.4%)
https://competitor-b.example.com/products/a1420
Last change: 41 days ago
That "last change" line is one extra SQL query and it changes behavior. A first move in six weeks reads very differently than the fourth move in a month.
Level 3: The autonomous quote adjustment (with a hard human gate)
This is where most builders stop and where the actual leverage is. Detection tells you a competitor moved. Level 3 acts on it.
The agent reads pending quotes from wherever they live — QuickBooks, a custom CRM, a Google Sheet, in my client's case a Postgres table their invoicing tool writes to. It matches line items against the tracked SKUs. When a competitor moved up on a matching product, it drafts a revised quote at the new target margin and drops it into a review queue.
It drafts. It does not send. Ever.
async def maybe_revise_quotes(sku, old_price, new_price):
if new_price <= old_price:
return # we ignore downward moves — see below
pending = await db.fetch_all(
"SELECT * FROM quotes WHERE status='draft' "
"AND $1 = ANY(line_item_skus)", sku
)
for q in pending:
revised = recompute_quote(q, sku, new_price, target_margin=0.32)
await db.execute(
"INSERT INTO quote_review_queue (original_id, revised_json, reason) "
"VALUES ($1, $2, $3)",
q["id"], revised, f"{sku}: competitor moved {old_price}→{new_price}"
)
await telegram_notify_review(q["id"], sku, old_price, new_price)
The approval gate is non-negotiable. You do not let an agent send priced quotes to customers unattended, no matter how good the model gets. One hallucinated line item on a $30k proposal and you're eating the difference or losing the client. The owner opens the review queue once a day, approves or rejects, approved ones go out.
The 6-week receipts
Runtime: 6 weeks. 23 SKUs tracked across 4 competitor sites. Here's the actual outcome, not projections:
| Metric | Value |
|---|---|
| Detected price changes >3% | 11 |
| Upward moves (actionable) | 7 |
| Downward moves (ignored) | 4 |
| Margin protected on in-flight quotes | ~$2,100 |
| Largest single quote affected | $12,400 |
| Claude API spend | ~$3/month |
| FireCrawl | Free tier (690 scrapes/mo) |
| Postgres | Existing home server, ~$0 marginal |
| All-in monthly cost | ~$4 |
The 7 upward moves let the client raise prices on matching in-flight quotes without losing competitive position — everyone else in the market already went up. The 4 downward moves were deliberately ignored. Chasing every price cut is how you race yourself to zero. The owner kept positioning on lead time and quality instead of matching the discount, and closed 3 of those 4 anyway.
Payback happened in week one. The first upward move we caught was on a $12,400 quote already sitting in draft.
Why the cost stays microscopic
- html_hash short-circuit skips ~80% of pages after week one
- Claude only sees the extracted markdown, not full raw HTML (2-4k tokens per call, not 20k)
- Sonnet-class model is plenty for structured price extraction — you don't need Opus for this
- 690 scrapes/month sits inside FireCrawl's free allocation
The part traders miss (the whole thesis)
Stock prices move because thousands of algorithmic actors race each other in microseconds. You cannot beat that with a Claude agent on a home server. You will lose money and it will take a while to admit it.
Your competitor's pricing page moves because a human updated a spreadsheet on Wednesday and pushed it to the CMS on Thursday. That's a signal with a 48-to-72 hour propagation window before the rest of the market notices. Claude reading that page at 6 AM Friday gives you a full week of advantage.
The slower the underlying process, the more valuable the automation.
Same pattern works on:
- Supplier catalogs — catch cost increases before your next PO
- Job postings — a competitor hiring 3 sales reps in your city is a signal
- Regulatory filings — quarterly, but material
- License renewals, permit databases, tender portals
- Product page changes — new SKU launched, old SKU discontinued
Any data source where a human is the bottleneck is a good target. Anywhere the underlying signal moves in seconds is not.
Where this shows up in real client work
Most of what I ship for small teams looks like this — one narrow monitoring loop, one Postgres table, one Telegram channel, one hard human gate before anything customer-facing goes out. Competitor price monitoring is one of the concrete workflows we run for bizflowai.io clients, usually alongside quote generation and invoice automation on the same Postgres. The unglamorous version — cron, hash short-circuit, three-column schema — beats the dashboard-first version every time because the owner actually reads a Telegram message and does not actually open a dashboard.
Want more like this?
I publish practical AI automation, GenAI engineering, and faceless content workflows on YouTube every week.
Subscribe to bizflowai.io on YouTube — never miss a new tutorial.
Planning an AI automation project or need a second opinion on your architecture?
Connect with me on LinkedIn — Lazar Milicevic, GenAI Engineer & bizflowai.io Founder.
Visit bizflowai.io for our services, case studies, and AI consulting.
Frequently asked questions
What is a competitor price monitoring agent for B2B?
A competitor price monitoring agent is an automated system that scrapes competitor pricing pages daily, detects meaningful price changes, and alerts the owner. For small B2B shops, it replaces the 2-4 hours per week spent manually checking competitor tabs. A typical build uses Claude as the reasoning layer, FireCrawl for scraping, and a Postgres table to snapshot prices with timestamps.
How do I build a price monitoring agent with Claude and FireCrawl?
Use a three-level architecture. Setup: connect Claude via API, FireCrawl as an MCP server for scraping, and a Postgres table with seven columns (SKU, competitor, URL, price, currency, scraped_at, raw_html_hash). Detection: a daily cron job scrapes each URL, extracts prices with Claude, and diffs against the last snapshot. Action: draft revised quotes into a review queue for owner approval.
Why does a raw_html_hash column matter for price scraping?
The raw_html_hash column stores a hash of each scraped page so the agent can skip re-parsing pages that haven't changed since the last run. This short-circuit cuts Claude API token spend by roughly 80 percent after the first week, bringing monthly LLM costs down to around 3 dollars for a 23-SKU, 4-competitor setup running daily.
When should a pricing agent send quotes automatically vs require approval?
Never send priced quotes to customers unattended. The agent should draft revised quotes into a review queue when competitor prices shift, but the owner must approve or reject each one before it goes out. This approval gate is non-negotiable regardless of model quality, because pricing errors sent directly to customers can damage margin, positioning, and trust irreversibly.
Why ignore competitor price drops under 3 percent?
Price changes under 3 percent in B2B are typically noise from rounding, tax recalculations, or currency drift, not strategic moves. Alerting on them creates fatigue. Additionally, chasing every downward price move races you to zero margin. A better strategy is to act on upward competitor moves to protect margin, while holding position on quality when competitors cut prices.