Skip to main content
TutorialsSeptember 9, 202615 min readUpdated September 9, 2026

Building a Social Listening AI Agent with Claude and xpoz MCP

Build a social listening AI agent with Claude and the xpoz SDK: search Twitter/X, Reddit, Instagram and TikTok, read comments, write a report. Code included.

TL;DR

Across Twitter/X, Reddit, Instagram, and TikTok for August 26 to September 9, 2026, "espresso machine" conversation was low-to-moderate volume and mostly niche ... The one thing to act on is durability and repairability messaging: multiple posts specifically cite short lifespans (De'Longhi pump/grinder dying, Breville needing $200 in parts) and ask about long-term serviceability ...

Building a Social Listening AI Agent with Claude and xpoz MCP

Building a Social Listening AI Agent with Claude and xpoz MCP

By the end of this tutorial you will have a script that takes a brand and its competitors, searches Twitter/X, Reddit, Instagram and TikTok for the last two weeks, reads the comments on the posts that drew the most reaction, and writes a markdown report with a sentiment split, quoted posts with links, a competitor table and suggested actions. The whole agent is about 150 lines of Python, and the finished code is on GitHub at XPOZpublic/social-listening-agent.

The agent has two halves. Claude runs the loop: it decides which searches to run, reads the results, pulls comments where it wants more context, and writes the report. Two tools do the fetching, and they wrap the Python SDK for xpoz, the social data platform we build, which searches a pre-indexed archive of billions of posts so there are no platform developer accounts or scrapers to maintain. A trial token needs no sign-up, so you can run the example before deciding whether you need an account.

What Will the Agent Produce?

Here is a trimmed excerpt of a real report the example produced on September 9, 2026, for the topic "espresso machines" with Breville and De'Longhi as competitors, using a trial token and ten tool calls. The full report is in the repository under examples/.

# Social listening report: espresso machines

## Summary
Across Twitter/X, Reddit, Instagram, and TikTok for August 26 to September 9, 2026,
"espresso machine" conversation was low-to-moderate volume and mostly niche ...
The one thing to act on is durability and repairability messaging: multiple posts
specifically cite short lifespans (De'Longhi pump/grinder dying, Breville needing
$200 in parts) and ask about long-term serviceability ...

## Sentiment split
| Platform | Positive | Neutral | Negative | Sample size |
|---|---|---|---|---|
| Twitter/X | 2 | 6 | 1 | 9 |
| Reddit | 3 | 5 | 4 | 12 |

## Top themes
**1. Reliability and repairability anxiety**
- "What gives me pause is the repair side of it... I don't want a dead screen to turn
  a four figure machine into e-waste." (Reddit, r/BuyItForLife) : https://reddit.com/r/BuyItForLife/comments/1wbfr1a/...

## Competitor mentions
| Name | Posts found | Dominant sentiment | Representative example |
| Breville | 5 | Mixed, leaning positive with maintenance gripes | ... |
| De'Longhi | 5 | Negative to neutral, driven by reliability complaints | ... |

The report ends with a Method section listing every search that ran, the sample sizes, and the caveat that counts are samples rather than totals. That section is what makes the output auditable: anyone can rerun the same queries.

How Is the Agent Structured?

Three files carry the whole thing. tools.py defines two tools and the SDK calls behind them. agent.py holds the system prompt, the loop, and report writing. requirements.txt lists three packages: anthropic, xpoz, and python-dotenv.

ToolWhat it calls in the SDKWhat it returns to Claude
search_posts(platform, query, start_date, end_date, limit)client.twitter.search_posts, client.reddit.search_posts, client.instagram.search_posts, client.tiktok.search_postsJSON rows: platform, id, author, text, likes or score, replies, date, url
get_comments(platform, post_id, limit)client.twitter.get_comments, client.reddit.get_post_with_comments, client.instagram.get_comments, client.tiktok.get_commentsJSON rows: platform, id, author, text, likes, date

The split matters for cost and for clarity. Claude never sees raw platform payloads; it sees one flat shape for every platform, which keeps its context small and makes the report consistent whether a post came from a subreddit or a TikTok caption.

How Are the Tools Defined?

Claude tools are JSON schemas. The search tool exposes the platform as an enum, the query as free text, and optional dates and a limit. The description tells Claude that the query language supports exact phrases and boolean operators, which is what lets it write searches like "espresso machine" AND (broken OR refund OR love OR switched) on its own.

TOOL_DEFINITIONS = [
    {
        "name": "search_posts",
        "description": (
            "Search public posts on one platform by keyword. Supports exact phrases in double quotes "
            "and AND / OR / NOT operators. Returns normalized posts with author, text, engagement, date, and url."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "platform": {"type": "string", "enum": ["twitter", "reddit", "instagram", "tiktok"]},
                "query": {"type": "string"},
                "start_date": {"type": "string", "description": "YYYY-MM-DD, inclusive"},
                "end_date": {"type": "string", "description": "YYYY-MM-DD, inclusive"},
                "limit": {"type": "integer", "minimum": 1, "maximum": 100},
            },
            "required": ["platform", "query"],
        },
    },
    {
        "name": "get_comments",
        "description": "Fetch the comments or replies on one post, to read how people reacted to it.",
        "input_schema": {
            "type": "object",
            "properties": {
                "platform": {"type": "string", "enum": ["twitter", "reddit", "instagram", "tiktok"]},
                "post_id": {"type": "string"},
                "limit": {"type": "integer", "minimum": 1, "maximum": 100},
            },
            "required": ["platform", "post_id"],
        },
    },
]

The SDK call behind the search tool is one line per platform, because every platform namespace in the xpoz SDK exposes the same search_posts method with the same date filters. The tool requests only the fields it needs, which makes each call faster and keeps the JSON that goes back to Claude short.

POST_FIELDS = {
    "twitter": ["id", "text", "author_username", "like_count", "reply_count", "retweet_count", "created_at_date"],
    "reddit": ["id", "title", "selftext", "author_username", "subreddit_name", "score", "comments_count", "post_url", "permalink", "created_at_date"],
    "instagram": ["id", "caption", "username", "like_count", "comment_count", "code_url", "created_at_date"],
    "tiktok": ["id", "description", "username", "like_count", "comment_count", "play_count", "created_at_date"],
}


def search_posts(client, platform, query, start_date, end_date, limit):
    namespace = getattr(client, platform)
    result = namespace.search_posts(query, start_date=start_date, end_date=end_date, limit=limit, fields=POST_FIELDS[platform])
    return [normalize_post(platform, post) for post in result.data]

Try this with Xpoz

No API keys needed. Query Twitter, Reddit, Instagram & TikTok with natural language.

Get Started Free

How Does the Normalizer Work?

Each platform names things differently: a tweet has text and like_count, a Reddit post has title, selftext and score, a TikTok post has description and play_count. The normalizer maps them to one row shape and builds a public URL for each post, so the report can link every quote. The Twitter and TikTok branches look like this; Reddit and Instagram follow the same pattern.

def normalize_post(platform, post):
    if platform == "twitter":
        return {
            "platform": platform,
            "id": post.id,
            "author": post.author_username,
            "text": post.text,
            "likes": post.like_count,
            "replies": post.reply_count,
            "reposts": post.retweet_count,
            "date": post.created_at_date,
            "url": f"https://x.com/{post.author_username}/status/{post.id}",
        }
    ...
    return {
        "platform": platform,
        "id": post.id,
        "author": post.username,
        "text": post.description,
        "likes": post.like_count,
        "replies": post.comment_count,
        "plays": post.play_count,
        "date": post.created_at_date,
        "url": f"https://www.tiktok.com/@{post.username}/video/{post.id}",
    }

Comments follow the same idea. Reddit returns a post with its comment tree from get_post_with_comments, the other three platforms return a paginated list from get_comments, and both are flattened into rows with author, text, likes and date.

What Does the System Prompt Ask For?

The system prompt does two jobs: it gives Claude a method, and it fixes the report format. The method is four steps with a budget: search the topic on every platform, run a second pass with opinion words, search each competitor, read comments on the two or three posts with the most reaction, then stop after at most a set number of tool calls. The budget keeps a run predictable in cost and time.

The report contract names six sections and what each holds. Fixing the sections is what makes reports comparable week to week and what stops the model from writing an essay. The prompt also tells Claude to quote posts verbatim, keep every URL exactly as returned, and to say in the Method section when a search returned nothing rather than inventing coverage.

SYSTEM_PROMPT = """You are a social listening analyst. You have two tools over a social data index covering Twitter/X, Reddit, Instagram, and TikTok.

Work in this order:
1. Search the brand or topic on every platform for the requested window. Use exact phrases for multi-word names. Run one or two more searches with complaint, praise, or comparison words (for example "broken", "love", "vs", "switched") to surface opinions, not just announcements.
2. Search each competitor at least once so the report can compare share of conversation.
3. Read the comments on the two or three highest-engagement posts to capture how people reacted.
4. Stop searching after {search_budget} tool calls at most, then write the report.

Then write the final answer as a markdown report with exactly these sections:
# Social listening report: {topic}
## Summary (3-5 sentences: volume, overall sentiment, the one thing to act on)
## Sentiment split (a table: platform, positive, neutral, negative, sample size)
## Top themes (3-6 themes; each with a one-line description, one or two quoted example posts with their url, and which platform they came from)
## Competitor mentions (a table: name, posts found, dominant sentiment, one representative example)
## Suggested actions (3-5 concrete actions, each tied to a theme or post above)
## Method (which searches ran, the date window, sample sizes, and the caveat that counts are samples, not totals)

Classify sentiment from the post text itself. Quote posts verbatim and keep every url exactly as returned. Do not invent posts, numbers, or urls. If a search returns nothing, say so in Method."""

How Does the Agent Loop Run?

The loop is the standard Messages API pattern for tool use. Call Claude with the tools and the conversation so far. If the response stops for tool_use, run every tool block it contains, append the assistant turn and then one user turn holding all the tool results, and call again. When the stop reason is anything else, the text in the response is the report. Returning all results in a single user message is what keeps Claude issuing parallel calls, which is why a ten-search run finishes in a couple of minutes.

def run(topic, competitors, days, search_budget, per_search_limit):
    claude = anthropic.Anthropic()
    xpoz = XpozClient(os.environ["XPOZ_API_KEY"])
    system = SYSTEM_PROMPT.format(search_budget=search_budget, topic=topic)
    messages = [{"role": "user", "content": build_prompt(topic, competitors, days)}]
    report_text = ""
    for _ in range(MAX_TURNS):
        response = claude.messages.create(
            model=MODEL,
            max_tokens=MAX_TOKENS,
            system=system,
            tools=TOOL_DEFINITIONS,
            messages=messages,
        )
        messages.append({"role": "assistant", "content": response.content})
        if response.stop_reason != "tool_use":
            report_text = "".join(block.text for block in response.content if block.type == "text")
            break
        results = []
        for block in response.content:
            if block.type != "tool_use":
                continue
            arguments = dict(block.input)
            arguments["limit"] = min(int(arguments.get("limit") or per_search_limit), per_search_limit)
            try:
                content = run_tool(xpoz, block.name, arguments)
                results.append({"type": "tool_result", "tool_use_id": block.id, "content": content})
            except XpozError as error:
                results.append({"type": "tool_result", "tool_use_id": block.id, "content": str(error), "is_error": True})
        messages.append({"role": "user", "content": results})
    xpoz.close()
    path = REPORTS_DIR / f"{datetime.date.today().isoformat()}-{slugify(topic)}.md"
    path.write_text(report_text, encoding="utf-8")
    return path

Two details are worth copying into your own agents. The per-search limit is clamped in the loop, not left to the model, so a trial token's five-result cap or your own cost ceiling holds regardless of what Claude asks for. And a failed SDK call goes back as a tool result flagged is_error rather than crashing the run, so Claude can route around a platform that returned nothing and say so in the Method section.

Here is what the loop printed on the espresso run, one line per tool call: three topic searches, three competitor searches, two comment pulls and two more platform searches, ten calls in total.

[1] search_posts {"platform": "twitter", "query": "\"espresso machine\"", "start_date": "2026-08-26", "end_date": "2026-09-09", "limit": 5}
[2] search_posts {"platform": "reddit", "query": "\"espresso machine\"", ...}
[3] search_posts {"platform": "twitter", "query": "\"espresso machine\" AND (broken OR refund OR love OR switched)", ...}
[4] search_posts {"platform": "reddit", "query": "Breville", ...}
[5] search_posts {"platform": "reddit", "query": "De'Longhi", ...}
[6] search_posts {"platform": "twitter", "query": "Breville vs De'Longhi", ...}
[7] get_comments {"platform": "reddit", "post_id": "1w3ve9l", "limit": 5}
[8] get_comments {"platform": "reddit", "post_id": "1w6ynsa", "limit": 5}
[9] search_posts {"platform": "instagram", "query": "espresso machine", ...}
[10] search_posts {"platform": "tiktok", "query": "espresso machine", ...}
report written to reports/2026-09-09-espresso-machines.md after 10 tool calls

How Do You Run It?

Clone the repository, create a virtual environment, install the three packages, and put two keys in .env: an Anthropic API key and an xpoz key. For the xpoz key, the fastest path is a trial token, which needs no account and lasts five days.

git clone https://github.com/XPOZpublic/social-listening-agent.git && cd social-listening-agent
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
cp .env.example .env

curl -X POST https://api.xpoz.ai/api/trial/token \
  -H "Content-Type: application/json" \
  -d '{"source": "read the social listening agent tutorial on xpoz.ai", "useCase": "trying the example agent on my brand"}'

.venv/bin/python agent.py "your brand" --competitors "Competitor A" "Competitor B" --days 14 --limit 5

The trial token returns at most five results per call and reads cached data only, which is enough to see the loop work end to end. With a full key from xpoz.ai/get-token you can raise --limit, page through complete result sets, export CSV, and get live data. The model is Claude Sonnet 5 by default; changing one constant swaps it.

What Does a Run Cost?

Two meters run. On the Claude side, a ten-call run with five results per call keeps every tool result short, and the conversation plus a 1,000-word report costs a few cents at Claude Sonnet 5 rates of $2 per million input tokens and $10 per million output tokens. On the xpoz side, queries are charged per search rather than per result. The trial token is free for five days. The Free tier is a one-time allowance of up to 75,000 results with no credit card, and Pro is $20 a month for up to 1,000,000 results a month, so a weekly run for one brand and a handful of competitors sits comfortably inside Free while you evaluate.

Can You Do This Without Code?

Yes. The same search and comment tools sit behind the xpoz remote MCP server at https://mcp.xpoz.ai/mcp, which Claude Desktop and Claude Code connect to with a Google sign-in and no installation. Ask Claude for the same report in plain language and it will run the searches itself. The setup steps are in How to Connect Claude or ChatGPT to Live Social Media Data. The code version earns its keep when you want scheduled runs, a fixed report format your team can compare week to week, or a pipeline that feeds the report somewhere else.

For standing monitoring rather than a one-off sweep, pair the agent with tracked keywords and users so the index keeps collecting in the background; How to Set Up Continuous Social Media Monitoring with an AI Agent covers that pattern. If sentiment is the main question, How to Run Social Media Sentiment Analysis with an AI Agent goes deeper on classification, and Best MCP Servers for Social Media Data surveys the wider tool landscape.

What Would You Change for Production?

Three things, in order. First, replace the ad hoc date window with tracked items so each run reads a complete, continuously collected dataset rather than a sample. Second, raise the per-search limit and switch the search tool to the SDK's paging mode so Claude can ask for the next page of a busy query instead of the first twenty posts. Third, store each report next to the previous one and pass the last report into the prompt, so the agent reports what changed rather than restating what it found last week.

Frequently Asked Questions

What does a social listening AI agent do?

It takes a brand or topic and a list of competitors, searches public posts on Twitter/X, Reddit, Instagram and TikTok for a date window, reads the comments on the posts with the most reaction, classifies sentiment and intent from the text, groups the posts into themes, and writes a report with quoted examples, links, a competitor comparison, and suggested actions. The example in this tutorial does all of that in about 150 lines of Python.

Do I need the official platform APIs to build one?

No. The example calls the xpoz Python SDK, which searches a pre-indexed archive of billions of posts across the four platforms, so there are no per-platform developer accounts, app reviews, or scrapers to maintain. A trial token needs no sign-up and lasts five days; the Free tier is a one-time allowance of up to 75,000 results and Pro is $20 a month for up to 1,000,000 results a month.

How does Claude decide which searches to run?

The system prompt gives Claude a method (topic search per platform, an opinion-word pass, one search per competitor, comments on the top posts) and a budget of tool calls. Claude picks the queries, the platforms and the order itself, returns tool_use blocks, and the loop executes them with the SDK and feeds the JSON back until Claude stops calling tools and writes the report.

Can I get the same report without writing code?

Yes. The same search and comment tools are exposed by the xpoz remote MCP server at https://mcp.xpoz.ai/mcp, which Claude Desktop and Claude Code connect to with a Google sign-in. Asking Claude for the report in plain language runs the same searches; the code version is for scheduled runs, custom report formats, and pipelines.

Share this article

Ready to Get Started?

Start building AI-powered social intelligence workflows today. No API keys required.