# Building an AI Agent for Social Media Monitoring (https://www.socialcrawl.dev/blog/building-ai-agent-social-media-monitoring) > Learn to build an AI agent for social media monitoring in under 150 lines of Python. Step-by-step tutorial: fetch posts, classify sentiment, send alerts. A working AI social media monitoring agent fits in under 150 lines of Python. This tutorial walks from zero to a running agent that uses Claude's tool-calling API to reason over posts and the [SocialCrawl unified API](https://www.socialcrawl.dev/v1) to fetch brand mentions from Instagram and TikTok across 43 platforms with a single auth header, then classifies sentiment and fires a Slack alert when it finds something worth escalating. There are now 5.66 billion active social media identities worldwide, and the average person uses 6.83 platforms every month [(DataReportal Digital 2026)](https://datareportal.com/reports/digital-2026-two-in-three-people-use-social-media). Monitoring 43 platforms manually means 42 different JSON schemas, 42 auth flows, and 42 rate-limit policies. One API key, one normalized schema, one agent loop fixes that. A working AI social media monitoring agent fits in under 150 lines of Python: Claude's tool-calling API decides which tool to run next, the SocialCrawl unified API fetches brand mentions through `GET /v1/instagram/hashtag` and `GET /v1/tiktok/search`, and a Slack webhook delivers the alert. Both endpoints share one `x-api-key` header and return the same normalized schema — with `engagement_rate`, `estimated_reach`, and `language` pre-computed — so the agent reads fields instead of doing arithmetic. > "Agents are systems where LLMs dynamically direct their own processes and tool usage, maintaining control over how they accomplish tasks." — Anthropic Research, [Building Effective Agents](https://www.anthropic.com/research/building-effective-agents) **Stack:** Python 3.11+ · Anthropic SDK (Claude) · SocialCrawl REST API · `httpx` · `python-dotenv` --- ## Prerequisites Before running any code, you need: - Python 3.11+ - An **Anthropic API key** from [console.anthropic.com](https://console.anthropic.com) - A **SocialCrawl API key** from the [SocialCrawl docs](/docs) (free tier includes 100 credits) - A **Slack webhook URL** for alerts (optional but recommended) Install dependencies: ```bash pip install anthropic httpx python-dotenv ``` Create a `.env` file: ```bash ANTHROPIC_API_KEY=sk-ant-... SOCIALCRAWL_API_KEY=sc-... SLACK_WEBHOOK_URL=https://hooks.slack.com/services/... ``` --- AI agent architecture for social media monitoring: the reasoning loop choosing tools, fetching data, and firing alerts ## How Does the Agent Architecture Work? The agent is a `while` loop where Claude decides which tool to call next based on the result of the previous call. [Anthropic's engineering team distinguishes](https://www.anthropic.com/research/building-effective-agents) **workflows** (predefined code paths where LLMs are orchestrated through fixed steps) from **agents** (the LLM directs its own tool usage, choosing what to do and when to stop). This tutorial builds the latter — the orchestration logic lives inside Claude, not in Python. If you're new to Claude's agent surface, our [guide to Claude agents](/blog/how-to-use-claude-agents) covers Managed Agents and sub-agents; for the wider ecosystem, see the [AI agent frameworks field guide](/blog/ai-agent-frameworks-2026-developer-field-guide). | Dimension | Workflow | Agent | | --------------------- | ----------------------------- | ---------------------------------------- | | Control flow | Predefined in code | LLM decides at runtime | | Predictability | High | Lower (but recoverable) | | Best for | Narrow, repeatable tasks | Open-ended tasks with branching | | Iteration guard | Implicit (fixed steps) | Explicit (`max_iterations`) | | Social monitoring fit | Poor — each platform branches | Excellent — LLM routes posts dynamically | The loop: ```text System prompt + tool definitions (cache-anchored) | Claude reasons | stop_reason: "tool_use" → fetch_posts(platform="instagram", query="yourbrand") | SocialCrawl API → [{id, content, engagement_rate, estimated_reach, platform, author}] | tool_result appended to messages | Claude reasons again → stop_reason: "tool_use" → classify_post(post={...}) | → stop_reason: "tool_use" → send_alert(post={...}, classification={...}) | stop_reason: "end_turn" ``` SocialCrawl normalizes every platform to the same schema (`content`, `engagement_rate`, `platform`, `author`, `timestamp`), so the system prompt needs zero platform-specific logic. Instagram and TikTok look identical to the reasoning loop. Three tool definitions, each with `strict: True` which [guarantees calls always match the schema](https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview): ```python from dotenv import load_dotenv load_dotenv() SOCIALCRAWL_BASE_URL = "https://www.socialcrawl.dev/v1" SOCIALCRAWL_API_KEY = os.environ["SOCIALCRAWL_API_KEY"] ANTHROPIC_API_KEY = os.environ["ANTHROPIC_API_KEY"] SLACK_WEBHOOK_URL = os.environ.get("SLACK_WEBHOOK_URL", "") # Tool definitions — cache-anchored (stable, passed as system-level content) TOOLS = [ { "name": "fetch_posts", "description": ( "Fetch recent posts from a social media platform matching a search query. " "Returns a list of posts with normalized fields: id, content, engagement_rate, " "estimated_reach, platform, author, timestamp. " "Use platform='instagram' or platform='tiktok'. " "Example: fetch_posts(platform='instagram', query='yourbrand')" ), "input_schema": { "type": "object", "properties": { "platform": { "type": "string", "enum": ["instagram", "tiktok"], "description": "The social platform to search.", }, "query": { "type": "string", "description": "Hashtag or keyword to search for.", }, }, "required": ["platform", "query"], }, "strict": True, }, { "name": "classify_post", "description": ( "Classify a single social media post for sentiment, urgency, and escalation. " "Returns {sentiment, urgency, escalate, reason}. " "Pass the full post dict from fetch_posts." ), "input_schema": { "type": "object", "properties": { "post": { "type": "object", "description": "A post dict with fields: id, content, engagement_rate, estimated_reach, platform, author.", "properties": { "id": {"type": "string"}, "content": {"type": "string"}, "engagement_rate": {"type": "number"}, "estimated_reach": {"type": "number"}, "platform": {"type": "string"}, "author": {"type": "string"}, }, "required": ["id", "content", "platform"], } }, "required": ["post"], }, "strict": True, }, { "name": "send_alert", "description": ( "Send a Slack alert for a post that requires attention. " "Only call this when classify_post returns escalate=true." ), "input_schema": { "type": "object", "properties": { "post": { "type": "object", "description": "The post dict from fetch_posts.", "properties": { "id": {"type": "string"}, "content": {"type": "string"}, "engagement_rate": {"type": "number"}, "platform": {"type": "string"}, "author": {"type": "string"}, }, "required": ["id", "content", "platform", "author"], }, "classification": { "type": "object", "description": "The classification dict from classify_post.", "properties": { "sentiment": {"type": "string"}, "urgency": {"type": "string"}, "escalate": {"type": "boolean"}, "reason": {"type": "string"}, }, "required": ["sentiment", "urgency", "escalate", "reason"], }, }, "required": ["post", "classification"], }, "strict": True, }, ] ``` --- ## What API Endpoints Power the Agent? Two SocialCrawl endpoints do all the data fetching, and both return a unified schema so the agent reads identical fields regardless of platform. The `fetch_posts` function wraps these [SocialCrawl](/docs/api-reference) endpoints: - `GET /v1/instagram/hashtag?tag=` ([Instagram docs](/platforms/instagram)) - `GET /v1/tiktok/search?q=` ([TikTok docs](/platforms/tiktok)) Both use the same `x-api-key` header. The response already includes pre-computed `engagement_rate`, `estimated_reach`, and `language`, so the agent reads a field rather than doing arithmetic. ```python def fetch_posts(platform: str, query: str) -> list[dict]: """ Fetch posts from SocialCrawl's unified API. Returns a list of normalized post dicts. """ endpoint_map = { "instagram": f"{SOCIALCRAWL_BASE_URL}/instagram/hashtag", "tiktok": f"{SOCIALCRAWL_BASE_URL}/tiktok/search", } param_map = { "instagram": {"tag": query}, "tiktok": {"q": query}, } url = endpoint_map[platform] params = param_map[platform] headers = {"x-api-key": SOCIALCRAWL_API_KEY} for attempt in range(4): try: response = httpx.get(url, params=params, headers=headers, timeout=15.0) response.raise_for_status() data = response.json() posts = data.get("data", data.get("posts", [])) return [ { "id": p.get("id", ""), "content": p.get("content", p.get("caption", p.get("text", ""))), "engagement_rate": p.get("engagement_rate", 0.0), "estimated_reach": p.get("estimated_reach", 0), "platform": platform, "author": p.get("author", p.get("username", "unknown")), "timestamp": p.get("timestamp", p.get("created_at", "")), } for p in posts ] except httpx.HTTPStatusError as e: if e.response.status_code == 429: # Exponential backoff with jitter for rate limiting wait = 2 ** attempt + random.random() time.sleep(wait) else: raise except httpx.RequestError: if attempt == 3: raise time.sleep(1.5) return [] ``` --- ## How Do AI Agents Classify Sentiment? AI agents classify sentiment by passing each post through a focused LLM call that returns structured JSON with sentiment, urgency, and escalation fields. `classify_post` makes a direct Claude API call with a tight system prompt. Because `engagement_rate` and `estimated_reach` are already in the post payload, the prompt reasons over three fields only: sentiment, urgency, escalate. One inference call, no extra enrichment step. Transformer-based classifiers reach up to 94% accuracy on social media sentiment benchmarks, substantially higher than traditional lexicon-based tools like VADER on informal or emoji-heavy text [(ACL Anthology, 2024)](https://aclanthology.org/2024.wassa-1.27/). ```python CLASSIFICATION_SYSTEM_PROMPT = ( "You are a social media brand monitoring analyst. " "Given a social media post with pre-computed engagement metrics, classify it. " "Return ONLY a JSON object with these exact fields:\n" " sentiment: 'positive' | 'negative' | 'neutral'\n" " urgency: 'immediate' | 'routine' | 'ignore'\n" " escalate: true | false\n" " reason: one sentence explaining your decision\n\n" "Escalate if: sentiment is negative AND (urgency is immediate OR engagement_rate > 0.05). " "Urgency is 'immediate' if the post requires brand response within 2 hours. " "Do not include any text outside the JSON object." ) client = anthropic.Anthropic(api_key=ANTHROPIC_API_KEY) def classify_post(post: dict) -> dict: """ Classify a post's sentiment, urgency, and escalation priority. Uses a focused Claude call, not the full agent loop. """ post_summary = ( f"Platform: {post['platform']}\n" f"Author: {post['author']}\n" f"Content: {post['content'][:500]}\n" f"Engagement rate: {post.get('engagement_rate', 0):.4f}\n" f"Estimated reach: {post.get('estimated_reach', 0):,}" ) response = client.messages.create( model="claude-haiku-4-5", # Fast + cost-efficient for high-volume classification max_tokens=256, system=CLASSIFICATION_SYSTEM_PROMPT, messages=[{"role": "user", "content": post_summary}], ) raw = response.content[0].text.strip() try: return json.loads(raw) except json.JSONDecodeError: # Fallback if the model adds preamble start = raw.find("{") end = raw.rfind("}") + 1 return json.loads(raw[start:end]) ``` --- The monitoring loop: scheduled sweeps over social mentions catching negative posts for escalation ## How Does the Monitoring Loop Work? The monitoring loop is a `while` loop that keeps calling Claude until it emits `stop_reason == "end_turn"` or hits a `max_iterations = 10` ceiling. Two things worth calling out: a `seen_ids` set deduplicates posts within each polling cycle, and the system prompt is cache-anchored so repeated runs don't pay full input token costs. Prompt caching reduces input token cost by up to 90% and latency by up to 85% for cached prefixes [(Anthropic Prompt Caching docs)](https://docs.claude.com/en/docs/build-with-claude/prompt-caching). [Anthropic recommends the iteration guard](https://www.anthropic.com/research/building-effective-agents) for all production agents. ```python AGENT_SYSTEM_PROMPT = ( "You are a social media monitoring agent for brand reputation management. " "Your job:\n" "1. Use fetch_posts to retrieve recent mentions for each keyword on each platform.\n" "2. Use classify_post on each post to determine sentiment and urgency.\n" "3. Use send_alert for any post where classify_post returns escalate=true.\n" "4. After processing all posts, stop (do not loop indefinitely).\n\n" "Be systematic: process one platform+keyword combination at a time. " "Do not classify the same post twice." ) def run_monitoring_agent(keywords: list[str], platforms: list[str]) -> dict: """ Run one monitoring cycle. Returns a summary dict with counts. """ seen_ids: set[str] = set() alerts_fired = 0 posts_processed = 0 max_iterations = 10 iteration = 0 # Build the initial user message listing what to monitor task_description = ( f"Monitor these keywords: {', '.join(keywords)}\n" f"On these platforms: {', '.join(platforms)}\n" "Fetch posts, classify each one, and alert on anything that needs escalation." ) messages = [{"role": "user", "content": task_description}] # Tool dispatch table tool_dispatch = { "fetch_posts": lambda inp: fetch_posts(**inp), "classify_post": lambda inp: classify_post(inp["post"]), "send_alert": lambda inp: send_alert(inp["post"], inp["classification"]), } while iteration < max_iterations: iteration += 1 response = client.messages.create( model="claude-haiku-4-5", max_tokens=4096, system=[ { "type": "text", "text": AGENT_SYSTEM_PROMPT, # Cache the system prompt — it never changes between calls "cache_control": {"type": "ephemeral"}, } ], tools=TOOLS, # Cache the tool definitions — stable across the entire loop # Prompt caching reduces cost on repeated polling cycles messages=messages, ) if response.stop_reason == "end_turn": break if response.stop_reason != "tool_use": # Unexpected stop reason — bail safely break # Append the full assistant turn (required for tool_use blocks to be valid) messages.append({"role": "assistant", "content": response.content}) # Execute all tool calls in this turn tool_results = [] for block in response.content: if block.type != "tool_use": continue tool_name = block.name tool_input = block.input tool_use_id = block.id # Deduplication: skip posts we've already seen this cycle if tool_name == "fetch_posts": raw_posts = tool_dispatch[tool_name](tool_input) new_posts = [p for p in raw_posts if p["id"] not in seen_ids] for p in new_posts: seen_ids.add(p["id"]) posts_processed += len(new_posts) result_content = json.dumps(new_posts) elif tool_name == "send_alert": tool_dispatch[tool_name](tool_input) alerts_fired += 1 result_content = json.dumps({"status": "alert_sent"}) else: result = tool_dispatch[tool_name](tool_input) result_content = json.dumps(result) tool_results.append( { "type": "tool_result", "tool_use_id": tool_use_id, "content": result_content, } ) messages.append({"role": "user", "content": tool_results}) return { "posts_processed": posts_processed, "alerts_fired": alerts_fired, "iterations": iteration, } ``` --- ## How Do You Send Alerts and Run the Agent on a Schedule? Alerts go out through a Slack webhook, and scheduling is a one-liner with the `schedule` library or APScheduler. `send_alert` closes the loop: the Slack payload includes platform, author, content, sentiment, urgency, and engagement rate, so the recipient has enough context to act without opening a separate dashboard. ```python def send_alert(post: dict, classification: dict) -> dict: """ Post a Slack alert for an escalated mention. Returns {"status": "sent"} on success. """ if not SLACK_WEBHOOK_URL: print(f"[ALERT] {post['platform']} | {post['author']} | " f"{classification['sentiment'].upper()} | {post['content'][:120]}") return {"status": "printed_to_stdout"} message = { "text": ( f":rotating_light: *Brand mention requires attention*\n" f"*Platform:* {post['platform'].capitalize()}\n" f"*Author:* @{post['author']}\n" f"*Sentiment:* {classification['sentiment']} | " f"*Urgency:* {classification['urgency']}\n" f"*Reason:* {classification['reason']}\n" f"*Engagement rate:* {post.get('engagement_rate', 0):.2%}\n" f"*Content:* {post['content'][:200]}" ) } response = httpx.post(SLACK_WEBHOOK_URL, json=message, timeout=10.0) response.raise_for_status() return {"status": "sent"} # Entry point if __name__ == "__main__": result = run_monitoring_agent( keywords=["yourbrand", "yourproduct"], platforms=["instagram", "tiktok"], ) print(f"Done. Posts processed: {result['posts_processed']}, " f"alerts fired: {result['alerts_fired']}, " f"loop iterations: {result['iterations']}") ``` To run on a schedule, wrap the call in a simple polling loop or use `APScheduler`: ```python schedule.every(5).minutes.do( run_monitoring_agent, keywords=["yourbrand"], platforms=["instagram", "tiktok"], ) while True: schedule.run_pending() time.sleep(30) ``` Five-minute intervals work for most cases. For crisis-sensitive categories, drop to 1-2 minutes and batch classification calls to keep inference costs flat. --- ## What Could Go Wrong? Six failure modes are worth planning for before running this in production, with concrete runtime numbers so you can size your budget and retries. 1. **Inference cost drift.** A single polling cycle against two platforms and two keywords uses roughly 1,500-2,500 input tokens (system prompt + tool defs + conversation) and 400-800 output tokens per Claude turn. Claude Haiku 4.5 is priced at $1 per million input tokens and $5 per million output tokens [(Anthropic Pricing)](https://www.anthropic.com/pricing). With prompt caching enabled, expect under $0.01 per polling cycle — roughly $2-$4 per day at 5-minute intervals. 2. **`429` from SocialCrawl.** Handled in `fetch_posts` with exponential backoff (2s, 4s, 8s + jitter, 4 attempts). For a decorator approach: `pip install tenacity` and `@retry(wait=wait_exponential(multiplier=1, min=2, max=30), stop=stop_after_attempt(4))`. SocialCrawl doesn't impose per-second rate limits — throttling happens at the credit layer, so `429` almost always means an upstream platform limit has been hit. 3. **Agent loops forever.** The `max_iterations = 10` guard prevents runaway inference. If you hit it consistently, verify `send_alert` is returning a valid `tool_result` with content. Anthropic's guidance is explicit: always set an iteration ceiling and log the final `stop_reason` [(Building Effective Agents)](https://www.anthropic.com/research/building-effective-agents). 4. **Duplicate alerts on restart.** `seen_ids` resets in memory. Persist to Redis (`SADD seen_posts {id}`) or SQLite (`INSERT OR IGNORE INTO seen_posts VALUES (?)`) and restore at startup. For a 7-day retention window, SQLite with a `created_at` column and a nightly `DELETE WHERE created_at < datetime('now', '-7 days')` is usually sufficient. 5. **Empty results.** Query too narrow, or the platform uses hashtag matching. For Instagram, omit the `#`. See the [SocialCrawl API reference](/docs/api-reference) for per-platform syntax. 6. **Slow classification at volume.** Batch up to 10 posts per call by changing `post: object` to `posts: array` in the tool schema and returning a JSON array. Typical single-post classification runs ~400ms round-trip; a 10-post batch completes in ~600ms — a 6-7x throughput win on classification-heavy workloads. --- ## What's Next? This agent covers Instagram and TikTok. Adding [YouTube](/platforms/youtube) or Reddit is a one-line change to `endpoint_map` inside `fetch_posts`. The reasoning loop, system prompt, and tool definitions stay identical. That's the direct payoff of a unified schema. Before wiring up the agent, paste any social URL into the [SocialCrawl Data Explorer](/explorer) and see your data before writing a single line of code. It confirms that `engagement_rate` and `estimated_reach` are populated for your target accounts. If you need a deeper look at Instagram's current API landscape — rate limits, what broke in 2024, and which scraping options are legally defensible — read [Instagram API & Scrapers in 2026: What Still Works](/blog/instagram-scraping-2026). For the full endpoint list, rate limits, and credit costs, see the [SocialCrawl API docs](/docs). --- ## Frequently Asked Questions ### What's the difference between social media monitoring and social listening? Social media monitoring is reactive: you track specific mentions, keywords, and tags as they occur. Social listening is the broader layer, analyzing patterns to extract intent and sentiment trends. This agent does both. `fetch_posts` handles monitoring (raw mentions), and `classify_post` handles listening (structured meaning from each post). ### How do AI agents handle multiple social media platforms? Without a unified API, each platform needs its own tool function, auth flow, and schema mapping. With SocialCrawl's unified schema, one `fetch_posts` tool covers all 43 platforms using the same fields, the same `x-api-key` header, and the same response shape. The reasoning loop never needs to know which platform it's reading, so adding platforms requires zero changes to agent logic. ### What are the most common mistakes when building social monitoring agents? Five issues that kill production agents: (1) platform-specific parsing that breaks on schema changes; (2) no deduplication, causing repeated alerts; (3) missing rate-limit handling, so `429` errors silently kill the loop; (4) no iteration guard, risking runaway inference costs; (5) feeding raw unstructured data to the LLM instead of pre-normalized fields, which inflates token usage and hurts classification accuracy. ### How much does it cost to run this AI monitoring agent? The two variable costs are inference and data. For classification, `claude-haiku-4-5` processes roughly 500 posts per dollar at typical post lengths. SocialCrawl charges 1 credit per Standard endpoint call on the free tier (100 credits included); each `fetch_posts` call to Instagram hashtag or TikTok search consumes 1 credit and returns up to 20 posts. A 5-minute polling loop monitoring two platforms and two keywords runs fewer than 30 credits per hour at normal cadence.