●FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtask●LIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its own●MCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS●PLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callback●SONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15●IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October●FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtask●LIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its own●MCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS●PLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callback●SONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15●IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October
Building a Real-Time Market Research Agent with Claude — Automate Competitor Analysis, Price Monitoring & Trend Detection
Learn how to combine Claude's web search, structured output, and tool-calling capabilities to build a production-grade market research agent that automates competitor analysis, price monitoring, and trend detection.
Setup and context — Why You Need a Market Research Agent in 2026
The difference between thriving and falling behind often comes down to information: how fresh it is, and how fast you can act on it. Monitoring competitor pricing changes, tracking industry trends, and keeping pulse on user sentiment on social media — doing all this manually is simply no longer viable.
Claude's web search, tool-calling, and 200K context window capabilities can be combined to build a surprisingly capable market research agent with very little code. In this article, we'll implement all of the following from scratch:
Competitor analysis agent: Periodically scans competitor websites, social media, and press releases to detect key changes
Price monitoring agent: Scrapes SaaS pricing pages and e-commerce sites, alerting you to price increases or cuts in real time
Trend detection agent: Extracts insights from news, Reddit, and X (formerly Twitter) and generates concise summaries
Integrated report pipeline: Merges output from all three agents and delivers a unified report to Slack and Notion automatically
This guide targets intermediate-to-advanced engineers and solopreneurs who already understand Claude API basics (chat completions and tool use).
Prerequisites and Setup
Required Environment
Node.js 20+ (ES modules support)
Python 3.11+ (as an alternative runtime)
Anthropic API key (claude-sonnet-4-6 recommended)
Slack Bot Token (for Slack notifications)
Notion Integration Token (for Notion delivery)
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦A SimHash + LLM hybrid diffing pattern that strips noise out of Claude Web Search results (measured 78% reduction in monthly LLM calls)
✦Production architecture that gets around the Cloudflare Workers 30-second CPU time limit using Queues — three agents in parallel for ~¥412/month
✦Twelve years of indie iOS/Android development across 50M downloads, applied to *what* you should actually monitor: feature segmentation over pricing
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Our market research agent is designed as a tool-use agent. Claude receives instructions from the pipeline, autonomously calls tools like web search, scraping, and data aggregation, and produces a final report.
[Scheduler]
↓ trigger (recurring)
[Market Research Agent]
↓
┌─ web_search() … fetch news & press releases
├─ fetch_page() … scrape competitor & pricing pages
├─ analyze_trends() … extract social media insights
└─ compare_data() … detect delta from last run
↓
[Claude Analysis & Summarization]
↓
[Delivery Pipeline (Slack / Notion)]
Stateful vs. Stateless Agents
Market research requires comparing current state against prior state — you need to know whether a price has changed since last week. This article uses a simple JSON file-based store, but in production, PostgreSQL or Cloudflare KV provides better durability and availability.
Step 1 — Implementing the Competitor Analysis Agent
Tool Definitions
We define the tools Claude will use. Web search leverages Claude's built-in capability; data persistence uses local JSON files.
// src/tools/competitorTools.jsexport const competitorTools = [ { name: "web_search", description: "Search the web for up-to-date information. Use this to find competitor press releases, news, and product updates.", input_schema: { type: "object", properties: { query: { type: "string", description: "Search query. Including company names, product names, and date ranges improves accuracy." }, max_results: { type: "number", description: "Maximum number of results to return (default: 5)" } }, required: ["query"] } }, { name: "save_competitor_data", description: "Saves competitor analysis data locally for delta comparison on the next run.", input_schema: { type: "object", properties: { competitor_name: { type: "string" }, data: { type: "object", description: "Analysis data to store (pricing, features, news, etc.)" }, timestamp: { type: "string" } }, required: ["competitor_name", "data", "timestamp"] } }, { name: "load_previous_data", description: "Loads previously saved competitor analysis data for comparison.", input_schema: { type: "object", properties: { competitor_name: { type: "string" } }, required: ["competitor_name"] } }];
Core Agent Logic
// src/agents/competitorAgent.jsimport Anthropic from "@anthropic-ai/sdk";import fs from "fs/promises";import path from "path";const client = new Anthropic();const DATA_DIR = "./data/competitors";async function executeTool(toolName, toolInput) { switch (toolName) { case "save_competitor_data": { await fs.mkdir(DATA_DIR, { recursive: true }); const filePath = path.join(DATA_DIR, `${toolInput.competitor_name}.json`); await fs.writeFile(filePath, JSON.stringify(toolInput, null, 2)); return { success: true, message: `Saved data for ${toolInput.competitor_name}` }; } case "load_previous_data": { const filePath = path.join(DATA_DIR, `${toolInput.competitor_name}.json`); try { const raw = await fs.readFile(filePath, "utf8"); return JSON.parse(raw); } catch { return { exists: false, message: "No previous data found (first run)" }; } } default: return { error: `Unknown tool: ${toolName}` }; }}export async function runCompetitorAnalysis(competitors) { const systemPrompt = `You are a market research specialist. For each competitor provided,conduct a thorough investigation covering:1. Latest press releases and news (past 7 days)2. Product and feature changes3. Pricing updates4. Social media and community sentiment5. Delta from previous data (what changed)Guidelines:- Clearly distinguish facts from inferences- Rank findings by importance (high / medium / low)- Include 1–2 sentences of business implication per finding- Output in English`; const userMessage = `Analyze these competitors: ${competitors.join(", ")}For each company: use web_search to gather the latest information, call load_previous_data to comparewith prior state, and save updated data with save_competitor_data.Finish with a structured summary for all companies.`; let messages = [{ role: "user", content: userMessage }]; while (true) { const response = await client.messages.create({ model: "claude-sonnet-4-6", max_tokens: 8192, system: systemPrompt, tools: competitorTools, messages }); messages.push({ role: "assistant", content: response.content }); if (response.stop_reason === "end_turn") { const textBlock = response.content.find(b => b.type === "text"); return textBlock?.text ?? "Analysis complete (no text output)"; } if (response.stop_reason === "tool_use") { const toolResults = []; for (const block of response.content) { if (block.type !== "tool_use") continue; console.log(`[Tool call] ${block.name}:`, block.input); const result = await executeTool(block.name, block.input); console.log(`[Tool result]:`, result); toolResults.push({ type: "tool_result", tool_use_id: block.id, content: JSON.stringify(result) }); } messages.push({ role: "user", content: toolResults }); } }}
Step 2 — Implementing the Price Monitoring Agent
Price monitoring is all about detecting change. We compare current prices against the last saved state and alert only when something has moved.
// src/agents/priceMonitorAgent.jsimport Anthropic from "@anthropic-ai/sdk";import fs from "fs/promises";import path from "path";const client = new Anthropic();const PRICE_DATA_DIR = "./data/prices";const priceTools = [ { name: "extract_pricing_from_page", description: "Extracts pricing information from a URL and returns structured data.", input_schema: { type: "object", properties: { url: { type: "string", description: "URL of the pricing page" }, product_name: { type: "string", description: "Product or company name" } }, required: ["url", "product_name"] } }, { name: "compare_prices", description: "Compares current pricing data against the previously saved snapshot and detects changes.", input_schema: { type: "object", properties: { product_name: { type: "string" }, current_prices: { type: "object" } }, required: ["product_name", "current_prices"] } }];async function executePriceTool(toolName, toolInput) { if (toolName === "compare_prices") { const filePath = path.join(PRICE_DATA_DIR, `${toolInput.product_name}.json`); let previous = null; try { previous = JSON.parse(await fs.readFile(filePath, "utf8")); } catch { /* First run */ } await fs.mkdir(PRICE_DATA_DIR, { recursive: true }); const saveData = { product_name: toolInput.product_name, prices: toolInput.current_prices, updated_at: new Date().toISOString() }; await fs.writeFile(filePath, JSON.stringify(saveData, null, 2)); if (!previous) { return { change_detected: false, message: "Initial snapshot saved (nothing to compare)" }; } const changes = []; for (const [plan, price] of Object.entries(toolInput.current_prices)) { const prevPrice = previous.prices?.[plan]; if (prevPrice !== undefined && prevPrice !== price) { const diff = typeof price === "number" ? price - prevPrice : "changed"; const direction = typeof diff === "number" ? (diff > 0 ? "price increase" : "price decrease") : ""; changes.push({ plan, prev: prevPrice, current: price, diff, direction }); } } return { change_detected: changes.length > 0, changes, previous_updated: previous.updated_at }; } return { error: `Unknown tool: ${toolName}` };}export async function runPriceMonitoring(targets) { const systemPrompt = `You are a price monitoring agent. Analyze the pricing pages of SaaS ande-commerce sites and accurately report any differences from the previous snapshot.Output format:- If changes found: product name, plan name, previous price, current price, percent change, business implication- If no changes: state "No changes detected" briefly- Use original currency (USD, JPY, EUR, etc.)`; const userMessage = `Investigate pricing for the following products:\n${ targets.map(t => `- ${t.name}: ${t.url}`).join("\n") }\n\nUse web_search to fetch current pricing, then compare_prices to diff against the previous snapshot.`; let messages = [{ role: "user", content: userMessage }]; while (true) { const response = await client.messages.create({ model: "claude-sonnet-4-6", max_tokens: 4096, system: systemPrompt, tools: priceTools, messages }); messages.push({ role: "assistant", content: response.content }); if (response.stop_reason === "end_turn") { const textBlock = response.content.find(b => b.type === "text"); return textBlock?.text ?? "Price monitoring complete"; } if (response.stop_reason === "tool_use") { const toolResults = []; for (const block of response.content) { if (block.type !== "tool_use") continue; const result = await executePriceTool(block.name, block.input); toolResults.push({ type: "tool_result", tool_use_id: block.id, content: JSON.stringify(result) }); } messages.push({ role: "user", content: toolResults }); } }}
Step 3 — Implementing the Trend Detection Agent
This agent extracts insights from news and social media. Claude's 200K context window is ideal for ingesting large volumes of text in a single pass.
// src/agents/trendAgent.jsimport Anthropic from "@anthropic-ai/sdk";const client = new Anthropic();export async function runTrendDetection(keywords, timeRange = "past_7_days") { const systemPrompt = `You are a trend analysis specialist. For the keywords provided,conduct thorough research across multiple sources and synthesize insights.Analysis areas:1. Rapidly rising topics (explain *why* they are gaining traction now)2. Top 3 news stories with brief summaries (prioritize authoritative sources)3. Community reaction on X (Twitter), Reddit, and Hacker News4. Relationship to competitor activities uncovered elsewhere5. Short-term predictions for the next 1–2 weeks (be explicit about confidence level)Output structure:## 🔥 Rising Trends## 📰 Key News (Top 3)## 💬 Community Sentiment## 🔮 Short-Term Forecast## 📊 Action Items (Business Implications)`; const response = await client.messages.create({ model: "claude-sonnet-4-6", max_tokens: 6144, system: systemPrompt, messages: [ { role: "user", content: `Run a comprehensive trend analysis for the following keywords over the ${timeRange} period.Keywords: ${keywords.join(", ")}Run multiple web_search calls from different angles before synthesizing your findings.` } ], tools: [ { name: "web_search", description: "Search the web for trend information.", input_schema: { type: "object", properties: { query: { type: "string" }, time_filter: { type: "string", enum: ["past_day", "past_week", "past_month"] } }, required: ["query"] } } ] }); const textBlock = response.content.find(b => b.type === "text"); return textBlock?.text ?? "Trend analysis complete";}
Step 4 — Building the Integrated Report Pipeline
This pipeline runs all three agents in parallel, merges their output with Claude, and delivers the final report to Slack and Notion.
// src/pipeline/reportPipeline.jsimport Anthropic from "@anthropic-ai/sdk";import { runCompetitorAnalysis } from "../agents/competitorAgent.js";import { runPriceMonitoring } from "../agents/priceMonitorAgent.js";import { runTrendDetection } from "../agents/trendAgent.js";const client = new Anthropic();const CONFIG = { competitors: ["OpenAI", "Google Gemini", "Mistral AI"], priceTargets: [ { name: "OpenAI ChatGPT Plus", url: "https://openai.com/pricing" }, { name: "Google Gemini Advanced", url: "https://one.google.com/about/ai-premium" } ], trendKeywords: ["Claude AI", "LLM 2026", "AI agents", "generative AI pricing"]};async function sendToSlack(report) { if (!process.env.SLACK_BOT_TOKEN) return; await fetch("https://slack.com/api/chat.postMessage", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${process.env.SLACK_BOT_TOKEN}` }, body: JSON.stringify({ channel: process.env.SLACK_CHANNEL_ID, text: `📊 Market Research Report — ${new Date().toLocaleDateString("en-US")}`, blocks: [ { type: "section", text: { type: "mrkdwn", text: `*📊 Market Research Report — ${new Date().toLocaleDateString("en-US")}*` } }, { type: "divider" }, { type: "section", text: { type: "mrkdwn", text: report.slice(0, 2900) } } ] }) });}async function saveToNotion(title, content) { if (!process.env.NOTION_TOKEN) return; await fetch("https://api.notion.com/v1/pages", { method: "POST", headers: { "Authorization": `Bearer ${process.env.NOTION_TOKEN}`, "Notion-Version": "2022-06-28", "Content-Type": "application/json" }, body: JSON.stringify({ parent: { database_id: process.env.NOTION_DATABASE_ID }, properties: { title: { title: [{ text: { content: title } }] }, Date: { date: { start: new Date().toISOString().split("T")[0] } } }, children: [ { object: "block", type: "paragraph", paragraph: { rich_text: [{ type: "text", text: { content: content.slice(0, 2000) } }] } } ] }) });}async function generateIntegratedReport(competitorReport, priceReport, trendReport) { const response = await client.messages.create({ model: "claude-sonnet-4-6", max_tokens: 4096, messages: [ { role: "user", content: `Synthesize the following three reports into a concise executive summary.## Competitor Analysis Report${competitorReport}## Price Monitoring Report${priceReport}## Trend Report${trendReport}Your integrated report must include:1. **Top 3 Insights This Week**2. **Urgent Alerts** (anything requiring immediate action)3. **Action Items for Next Week**4. **Medium- to Long-Term Implications**Target audience: business owners. Prioritize information that drives decisions.` } ] }); return response.content.find(b => b.type === "text")?.text ?? "";}export async function runMarketResearchPipeline() { console.log("🚀 Market research pipeline started:", new Date().toLocaleString("en-US")); try { // Run all three agents in parallel for maximum throughput const [competitorReport, priceReport, trendReport] = await Promise.all([ runCompetitorAnalysis(CONFIG.competitors), runPriceMonitoring(CONFIG.priceTargets), runTrendDetection(CONFIG.trendKeywords) ]); console.log("✅ All three agents done — generating integrated report..."); const integratedReport = await generateIntegratedReport( competitorReport, priceReport, trendReport ); const title = `Market Research Report — ${new Date().toLocaleDateString("en-US")}`; await Promise.allSettled([ sendToSlack(integratedReport), saveToNotion(title, integratedReport) ]); console.log("✅ Report delivered"); return integratedReport; } catch (error) { console.error("❌ Pipeline error:", error); throw error; }}
Scheduling with node-cron
// src/scheduler.jsimport cron from "node-cron";import { runMarketResearchPipeline } from "./pipeline/reportPipeline.js";// Every Monday at 8:00 AM JSTcron.schedule("0 8 * * 1", runMarketResearchPipeline, { timezone: "Asia/Tokyo"});console.log("⏰ Market research scheduler running");
# Start the schedulernode src/scheduler.js# One-off test runnode -e "import('./src/pipeline/reportPipeline.js').then(m => m.runMarketResearchPipeline())"
Common Errors and How to Fix Them
Error 1: overloaded_error — API Rate Limiting
Running three agents in parallel can trigger rate limits. Wrap API calls with a retry helper.
async function callWithRetry(fn, maxRetries = 3, delayMs = 2000) { for (let i = 0; i < maxRetries; i++) { try { return await fn(); } catch (err) { if (err.status === 529 && i < maxRetries - 1) { console.warn(`Rate limit hit — retrying in ${delayMs * (i + 1)}ms`); await new Promise(r => setTimeout(r, delayMs * (i + 1))); } else { throw err; } } }}const report = await callWithRetry(() => runCompetitorAnalysis(competitors));
Error 2: Agent Loop Does Not Terminate
Guard against infinite tool-calling loops with a step cap.
Always serialize tool results to a JSON string before returning them to Claude.
// ❌ Wrong — passing an object directly will cause an errorcontent: result// ✅ Correct — always stringifycontent: JSON.stringify(result)
Error 4: Slack Message Too Long
Slack block elements have a 3,000-character limit. Split long reports into chunks.
function chunkText(text, maxLength = 2800) { const chunks = []; for (let i = 0; i < text.length; i += maxLength) { chunks.push(text.slice(i, i + maxLength)); } return chunks;}
Advanced Patterns
Multimodal: Screenshot-Based Price Extraction
Claude's vision capability lets you extract pricing data from page screenshots — useful for sites that block traditional scrapers.
Fine-tuning the system prompt for your industry dramatically improves output quality.
const INDUSTRY_PROMPTS = { saas: "Pay special attention to pricing model shifts (per-seat, usage-based, flat rate) and trial/freemium strategy changes.", ecommerce: "Focus on shipping costs, loyalty point changes, and seasonal promotion strategies.", ai: "Prioritize benchmark performance, API rate limits, context window upgrades, and model deprecations."};const systemPrompt = BASE_PROMPT + "\n\n" + (INDUSTRY_PROMPTS[industry] ?? "");
Six Operational Gotchas the Docs Don't Mention
Here are six things I've learned running this market research agent on Cloudflare Workers + Claude API since January 2026 — none of which are in Anthropic's official documentation. If you're building the same stack, these should save you some trial and error.
1. The Web Search Tool returns slightly different results for the same query, day to day
If you call Claude's Web Search Tool with the same query on consecutive days, you'll typically see 3 — 4 of the top 10 URLs swap out. Naively treating "the URL set diff = an industry change" leaves you with ~80% noise, and the actual signal you want to catch (a competitor shipping a new feature) gets buried.
The fix is a two-stage check: compute SimHash on the top 5 URL sets for both runs, then only run the LLM diff when Jaccard similarity drops below 0.7.
import { createHash } from "node:crypto";function urlSetSimHash(urls) { const tokens = urls.flatMap((u) => new URL(u).hostname.split(".")); const bits = new Array(64).fill(0); for (const token of tokens) { const hash = createHash("md5").update(token).digest(); for (let i = 0; i < 64; i++) { const bit = (hash[Math.floor(i / 8)] >> (i % 8)) & 1; bits[i] += bit ? 1 : -1; } } return BigInt("0b" + bits.map((b) => (b > 0 ? "1" : "0")).join(""));}function jaccardSimilarity(setA, setB) { const a = new Set(setA); const b = new Set(setB); const intersection = [...a].filter((x) => b.has(x)).length; const union = new Set([...a, ...b]).size; return intersection / union;}export function shouldRunLlmDiff(prevUrls, currUrls) { const sim = jaccardSimilarity(prevUrls, currUrls); return sim < 0.7; // run the LLM only when sim drops below 0.7}
This two-stage check cut my measured monthly LLM call count by 78%.
2. Three agents in parallel won't fit inside the Workers 30-second CPU limit
Running the three agents (competitor analysis / price monitoring / trend detection) with Promise.all blows past the Workers CPU time limit while waiting on Claude API responses. The Step 4 implementation in this article is written as a single request for clarity, but in production you'll want Cloudflare Queues splitting it across discrete steps.
Via Queues, each request stays at 5 — 8 seconds of CPU time, and the three agents combined run me about ¥412/month including the Workers Paid plan (measured April 2026, ~8,400 requests/month).
3. Slack Block Kit has a 3,000-character limit per section block
If you post a trend report to Slack and a single section block goes over 3,000 characters, the whole message fails. Chunk the Markdown report into 2,500-character blocks and post multiple section blocks instead.
function chunkForSlack(report, chunkSize = 2500) { const blocks = []; for (let i = 0; i < report.length; i += chunkSize) { blocks.push({ type: "section", text: { type: "mrkdwn", text: report.slice(i, i + chunkSize) } }); } return blocks;}
4. Spin up a separate agent for Privacy Policy / Terms changes
If you leave diffing to the LLM, small Privacy Policy revisions (cookie policy, data retention windows) get dismissed as "no meaningful change" and slip past. These have direct legal exposure, so a safer pattern is a separate agent that hashes the raw HTML of a fixed URL with MD5 and notifies on any bit-level change.
In my setup, I scan 5 competitors × the three fixed URLs /privacy, /terms, /pricing every Monday at 09:00 JST. I get 1 — 2 hits per month; over the last six months, only 3 actually mattered.
5. Competitor pricing changes cluster around weekends, month-ends, and quarter-ends
Same logic as the eCPM swings AdMob shows around quarterly advertiser budget cycles: competitor price moves bunch up Thursday night through Sunday morning, in the final week of every month, and at quarter-ends (final weeks of March, June, September, December). This is just pattern recognition from running indie iOS/Android apps since 2014.
Weighting the polling frequency by day of week cuts Claude API calls while improving hit rate.
function getPollingInterval(date) { const day = date.getDay(); // 0 = Sunday const dom = date.getDate(); const month = date.getMonth() + 1; const isQuarterEnd = [3, 6, 9, 12].includes(month) && dom >= 25; const isMonthEnd = dom >= 28; const isWeekend = day === 0 || day === 6 || day === 5; if (isQuarterEnd) return 30 * 60 * 1000; // 30-minute interval if (isMonthEnd || isWeekend) return 2 * 60 * 60 * 1000; // 2-hour interval return 6 * 60 * 60 * 1000; // 6-hour interval on weekdays}
6. The Tier 1 50 req/min rate limit gets blown easily by 3 sites × 5 competitors in parallel
50 req/min on Anthropic API Tier 1 (≥$5 credit) sounds like plenty, but 3 agents × 5 competitors running through Promise.all peaks at 15 requests fired simultaneously and returns 429s with retry-after. I cap actual throughput at 12 req/min with a sliding window and queue the overflow.
class RateLimitedQueue { constructor(maxPerMin = 12) { this.maxPerMin = maxPerMin; this.timestamps = []; } async waitForSlot() { const now = Date.now(); this.timestamps = this.timestamps.filter((t) => now - t < 60_000); if (this.timestamps.length >= this.maxPerMin) { const wait = 60_000 - (now - this.timestamps[0]); await new Promise((r) => setTimeout(r, wait + 100)); return this.waitForSlot(); } this.timestamps.push(Date.now()); }}
Twelve Years of Indie Development: A Framework for Prioritizing Market Research
So much for the implementation. One last meta thought before we close. I'm Masaki Hirokawa — I've been building iOS/Android apps as an indie developer since 2014, accumulating around 50 million downloads. Here's how I think about what a market research agent should actually be tuned to watch for.
Track feature segmentation, not pricing
In 2018 I ran a wallpaper app where a competitor shipped an "auto-rotate photo" feature, and the very next month my 7-day retention dropped 18%. At the time I assumed "drop the price and they'll come back" and cut my monthly tier from ¥240 to ¥120 — ARPU halved and the churn didn't stop.
The lesson: copying a competitor's price just degrades your CAC. What's actually worth watching is the boundary of their feature segmentation. What lives in Free / Pro / Pro+ tells you almost everything about their revenue structure. My Claude prompts always include a feature_segmentation_diff perspective.
const PRIORITY_PROMPT = `Classify changes by the following priority:- P0: Feature segmentation boundary change (Free ↔ Paid, Tier 1 ↔ Tier 2)- P1: New feature launches — especially AI/automation features- P2: Major UI/UX overhauls- P3: Pricing changes- P4: Marketing copy changes (usually no notification needed)`;
Don't just stack AdMob, Stripe, and Web Search data side by side
Running four parallel sites under Dolice Labs (Claude Lab / Gemini Lab / Antigravity Lab / Rork Lab) means three data streams flow in daily: AdMob (app ad revenue), Stripe (membership billing), and Web Search (competitor moves). Stacking all of them into the same daily Notion report creates so much noise that decisions stall.
My priority rule is simple:
High (same-day action): Stripe MRR drops more than 10% week-over-week
Medium (next-week review): AdMob eCPM moves more than ±20% week-over-week
Low (next-month review): A competitor ships a new feature
Notification only (quarterly review): A competitor changes pricing or refreshes their UI
When the Claude agent's output flows into a Notion Linked Database, I make sure that priority tag is attached before the row lands. That single discipline cut my Slack notification fatigue dramatically.
One weekly run is enough to monitor AI-blog competitors
The competitors for my four Dolice Labs sites (AI technical blogs) ship articles via Claude API almost every day. My instinct was "I have to check this daily," but after running the SimHash diff once per day for three months, only 6 events actually changed any of my decisions.
Five of those 6 events were posted on the weekend (Friday — Sunday), so I now run the agent just once, every Monday at 07:00 JST. API costs are 1/7, Slack notifications are 1/7, and the insight density hasn't suffered.
"Real-timeness" and "signal density" tend to be inversely correlated. The single most important design decision for a market research agent is "how much delay do you allow."
Reflecting on the ¥1M/month AdMob days — watch your own users, not your competitors
Between 2016 and 2019, when I was clearing ¥1M/month from AdMob, I spent a lot of time on competitive analysis. Looking back, the thing that actually moved the numbers wasn't competitor monitoring — it was optimizing for my own users. Specifically, fixing crashes that showed up in Crashlytics, and 30-day retention cohort analysis.
A market research agent is at most a process for filtering what your own judgment has to consider. Final decisions should be driven by your own user data. When you run the agent in this article, I'd recommend appending a "so what does this mean for my D7 retention?" self-review section to every output.
Summary
In this article, we built a production-capable market research pipeline by combining Claude's web search, tool-calling, and long context capabilities.
Key takeaways:
Competitor analysis agent: Stateful delta detection to surface only what has changed
Price monitoring agent: Automatic snapshot comparison to instantly flag price shifts
Trend detection agent: Batch processing large amounts of web content using Claude's 200K context
Integrated pipeline: Parallel agent execution with Slack and Notion auto-delivery
You can use this system as-is, or extend it with industry-specific prompts, additional agents (e.g., a patent monitoring agent or SEC filing tracker), and richer delivery formats. The goal is always the same: get signal faster than your competition and act on it decisively.
For building out the revenue automation side of this, see our Claude Automated Revenue Systems Complete Guide. For the web monitoring fundamentals, Cowork Web Monitoring Automation Guide is a great complement. For deeper coverage of web search filtering and citation-aware outputs, visit Claude API Web Search Complete Guide.
Share
Thank You for Reading
Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.