Your competitor just dropped their price by 30% and added a free tier. You found out when a prospect said "I saw they now offer that for free" — three weeks after it happened.
Pricing changes are some of the highest-signal competitive events in B2B markets. A price cut means they're under pressure. A new free tier means they're repositioning. A plan removal means something isn't converting. Every one of these is an opportunity to update your positioning, arm your sales team, or reach out to churned accounts.
But most companies have no system for catching this signal. You refresh the competitor's pricing page manually when you remember, or hear about it from a prospect during a call. By then, the window has already passed.
This post shows you how to build an automated monitor that watches competitor pricing pages daily and fires a Slack alert the moment something changes — for under $8/month.
The Gap in Your Competitive Stack
Here's what existing tools charge for this capability:
- Crayon: $25,000+/year — enterprise CI platform with page change tracking
- Klue: $15,000+/year — battlecard automation plus change detection
- Kompyte: Enterprise pricing, sales-heavy procurement process
These platforms bundle monitoring with competitive battlecards, AI-generated summaries, and CRM integrations. Useful if you have a 20-person sales team and a CI budget. Overkill if you just want to know when a competitor changes their pricing page.
The underlying mechanism — scrape a page, diff against a stored snapshot, alert on change — costs pennies per run.
The Architecture
Four components:
-
Apify
apify/cheerio-scraper— lightweight page scrape extracting text from pricing elements - Node.js script — stores snapshots, diffs current vs. stored, filters noise from nav/footer/cookie banner changes
- Slack webhook — fires an alert with competitor name, old text, new text, and a link to the page
- HubSpot (optional) — tags open deals where the competitor appears; auto-creates an AE task to update the competitive talk track
Each component is stateless. The Node.js script holds state (stored snapshots in a JSON file) and orchestrates the flow.
Implementation
Step 1: Define Your Competitor List
Create a competitors.json file:
[
{
"name": "CompetitorA",
"pricingUrl": "https://competitora.com/pricing",
"selector": ".pricing-table"
},
{
"name": "CompetitorB",
"pricingUrl": "https://competitorb.com/pricing",
"selector": "#pricing-section"
}
]
selector is the CSS selector for the pricing container on each page. Use browser DevTools to find a stable selector that captures price information but excludes dynamic elements like nav menus, chat widgets, or cookie banners.
Step 2: Scrape and Extract Pricing Content
const { ApifyClient } = require('apify-client');
const competitors = require('./competitors.json');
const apify = new ApifyClient({ token: process.env.APIFY_TOKEN });
async function scrapePricingPage(competitor) {
const run = await apify.actor('apify/cheerio-scraper').call({
startUrls: [{ url: competitor.pricingUrl }],
pageFunction: `async function pageFunction(context) {
const { $, request } = context;
const content = $('${competitor.selector}').text().replace(/\\s+/g, ' ').trim();
return { url: request.url, content };
}`
});
const { items } = await apify.dataset(run.defaultDatasetId).listItems();
return items[0]?.content || '';
}
Step 3: Diff Against Stored Snapshot
const fs = require('fs');
const SNAPSHOTS_FILE = './snapshots.json';
function loadSnapshots() {
if (!fs.existsSync(SNAPSHOTS_FILE)) return {};
return JSON.parse(fs.readFileSync(SNAPSHOTS_FILE, 'utf8'));
}
function saveSnapshot(name, content) {
const snapshots = loadSnapshots();
snapshots[name] = { content, updatedAt: new Date().toISOString() };
fs.writeFileSync(SNAPSHOTS_FILE, JSON.stringify(snapshots, null, 2));
}
function detectChange(name, currentContent) {
const snapshots = loadSnapshots();
const stored = snapshots[name];
if (!stored) {
// First run — save baseline, no alert
saveSnapshot(name, currentContent);
return null;
}
if (stored.content !== currentContent) {
saveSnapshot(name, currentContent);
return {
name,
oldContent: stored.content,
newContent: currentContent,
detectedAt: new Date().toISOString()
};
}
return null;
}
Step 4: Send a Slack Alert
const https = require('https');
async function sendSlackAlert(change, pricingUrl) {
const message = {
blocks: [
{
type: 'header',
text: { type: 'plain_text', text: `⚠️ Pricing Page Changed: ${change.name}` }
},
{
type: 'section',
fields: [
{ type: 'mrkdwn', text: `*Competitor:*\n${change.name}` },
{ type: 'mrkdwn', text: `*Detected at:*\n${change.detectedAt}` }
]
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*Before:*\n\`\`\`${change.oldContent.slice(0, 300)}\`\`\``
}
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*After:*\n\`\`\`${change.newContent.slice(0, 300)}\`\`\``
}
},
{
type: 'actions',
elements: [{
type: 'button',
text: { type: 'plain_text', text: 'View Pricing Page' },
url: pricingUrl
}]
}
]
};
const payload = JSON.stringify(message);
const url = new URL(process.env.SLACK_WEBHOOK_URL);
return new Promise((resolve) => {
const req = https.request({
hostname: url.hostname,
path: url.pathname,
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Content-Length': payload.length }
}, resolve);
req.write(payload);
req.end();
});
}
Step 5: Orchestrate the Full Run
async function runPricingMonitor() {
console.log(`Starting pricing monitor for ${competitors.length} competitors`);
for (const competitor of competitors) {
try {
console.log(`Checking ${competitor.name}...`);
const currentContent = await scrapePricingPage(competitor);
const change = detectChange(competitor.name, currentContent);
if (change) {
console.log(`Change detected for ${competitor.name} — sending Slack alert`);
await sendSlackAlert(change, competitor.pricingUrl);
} else {
console.log(`${competitor.name}: no change`);
}
} catch (err) {
console.error(`Error checking ${competitor.name}:`, err.message);
}
}
}
runPricingMonitor().catch(console.error);
Step 6: Schedule with GitHub Actions
name: Competitor Pricing Monitor
on:
schedule:
- cron: '0 9 * * *' # Every morning at 9 AM UTC
workflow_dispatch:
jobs:
monitor:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm install
- run: node monitor.js
env:
APIFY_TOKEN: ${{ secrets.APIFY_TOKEN }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
Optional: CRM Write-Back
When a pricing change is detected, tag open deals in HubSpot where that competitor is tracked:
async function tagAffectedDeals(competitorName) {
const searchRes = await fetch('https://api.hubapi.com/crm/v3/objects/deals/search', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.HUBSPOT_TOKEN}`
},
body: JSON.stringify({
filterGroups: [{
filters: [{
propertyName: 'competitor_notes',
operator: 'CONTAINS_TOKEN',
value: competitorName
}]
}],
properties: ['dealname', 'hubspot_owner_id']
})
});
const { results } = await searchRes.json();
for (const deal of results) {
await fetch('https://api.hubapi.com/crm/v3/objects/tasks', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.HUBSPOT_TOKEN}`
},
body: JSON.stringify({
properties: {
hs_task_subject: `[Competitor Alert] ${competitorName} updated pricing — update talk track`,
hs_task_body: `${competitorName} changed their pricing page. Review competitive positioning before next call.`,
hs_task_priority: 'HIGH',
hs_task_status: 'NOT_STARTED',
hubspot_owner_id: deal.properties.hubspot_owner_id
},
associations: [{
to: { id: deal.id },
types: [{ associationCategory: 'HUBSPOT_DEFINED', associationTypeId: 216 }]
}]
})
});
}
}
What Does This Actually Cost?
| Component | Cost |
|---|---|
Apify apify/cheerio-scraper
|
~$0.05–0.10 per run |
| 10 competitors, daily checks | ~$0.50–$1.00/day max |
| Monthly total | $3–8/month |
Compare that to Crayon at $25,000+/year or Klue at $15,000+/year. You get the same core signal — page changed, here's what's different — for a rounding error.
What to Do When the Alert Fires
- Open their pricing page and screenshot the current state
- Compare to Wayback Machine snapshot to understand the full diff
- Update your competitive battlecard with the new pricing context
- Brief the sales team before their next call with any prospect who mentioned that competitor
- Consider re-engaging churned accounts if the competitor raised prices
The alert is the input. The competitive response is the output.
Summary
- Competitor pricing changes are high-signal events that most teams discover weeks late
- Enterprise CI tools charge $15,000–$25,000+/year for page change monitoring
- A lightweight Apify + Node.js stack delivers the same signal for $3–8/month
- CSS selector-based extraction gives you structured diffs, not just "something changed visually"
- GitHub Actions handles the scheduling at zero additional cost
The hardest part isn't the code — it's finding stable CSS selectors for each competitor's pricing page and filtering out nav/footer noise. Once those are dialed in, the alert fires reliably and your team stops hearing about pricing changes from prospects mid-call.
Top comments (0)