Content
Turn social chatter into a weekly listening brief
Search the last week of X posts about your brand and competitors, add the sourced web context, read the pages behind it, and archive each brief so next week starts from what you already know.
Copy the prompt
Replace the bracketed fields, then paste it into Claude Code, Codex, or any AI with terminal access. It installs the Felo skill, asks before spending credits, and runs the workflow.
Run a social listening brief for [brand, product, or category]. Watch terms: [brand], [product name], [competitor names], [category keywords]. Time window: last [7 / 14] days. Timezone: [timezone]. Audience: [team]. Sources allowed: public X posts and public web pages only. Produce: 1. A five-line summary of what changed in the conversation. 2. Themes, each with what people are saying, the posts that support it, and the date range those posts cover. 3. Competitor mentions: who is being recommended, and in which context. 4. Risks: complaints, outages, repeated questions, or backlash. 5. Next steps, split into "reply now", "watch", and "feed to product". 6. A source list with every post and page URL and its date. Rules: - Report only posts and pages the API returns. Never invent sentiment scores, engagement numbers, quotes, or sources. - Treat X as a signal, not a representative survey. Say when the sample is too small to conclude anything. - Keep a customer's own words separate from your summary of them, and quote briefly with a link. - Resolve the time window to explicit start and end dates, and state the timezone. - Before spending credits, show the searches and extractions you plan to make and wait for my approval. - Do not reply, post, follow, or message anyone. This brief is read-only. - Write the result to ./social-listening.md and summarize it in chat.
The copy also includes the install steps and safety rules.
What you get
Example output from a run. Not a live generation.
Social listening brief — 2026-10-30
What changed
- Complaints about the export flow are now the top theme.
- A cheaper rival is being recommended in pricing threads.
- The outage from 2026-10-27 is no longer being mentioned.
Themes
- Exports are slow since the update — @example_dev: "The export button now takes minutes for a 40-page doc." (2026-10-28); @example_ops: "Anyone else seeing exports queue at the end of the month?" (2026-10-29)
- A cheaper alternative is being recommended — Example Rival appears in threads where people ask for a lower price
Competitors
Example Rival is recommended when people ask for a cheaper alternative; the recommendation is about price, not features.
Risks
The export complaint appears in support-adjacent threads, so it may reach churn conversations.
Next
- Reply now: the export thread from 2026-10-28.
- Watch: whether the complaint spreads to the API audience.
- Feed to product: export latency at month end.
Sources
- https://x.com/example_dev/status/... — 2026-10-28
- https://example.com/export-thread — 2026-10-29
The exact calls
For developers: the same workflow as copy-paste code, in the order it runs.
Show the exact API calls
Search the last week of posts
X Search takes a query, a limit, and a Unix start time, so the window lives in the request instead of the prompt. Keep the handle, the timestamp, and the post URL — they are the citation — and keep the metrics the response returns. The helper below sends the key once and throws on a non-2xx response.
listening.ts const BASE = "https://openapi.felo.ai"; const headers = { Authorization: `Bearer ${process.env.FELO_API_KEY}`, "Content-Type": "application/json", }; async function felo<T>(path: string, body?: unknown): Promise<T> { const res = await fetch(`${BASE}${path}`, { method: body ? "POST" : "GET", headers, body: body ? JSON.stringify(body) : undefined, }); if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`); return res.json() as Promise<T>; } type XData = { data: { tweets: { content: string; author: { username: string }; created_at: string; metrics: { retweet_count: number; favorite_count: number; reply_count: number }; url: string; }[]; }; }; const since = Math.floor((Date.now() - 7 * 24 * 60 * 60 * 1000) / 1000); const search = await felo<XData>("/v2/x/tweet/search", { query: '"Acme" OR "Acme app"', limit: 50, since_time: since, }); const posts = search.data.tweets;Example output
{ "data": { "tweets": [ { "content": "The export flow is slow again.", "author": { "username": "example_dev" }, "created_at": "2026-10-28T09:14:00Z", "metrics": { "retweet_count": 4, "favorite_count": 21, "reply_count": 6 }, "url": "https://x.com/example_dev/status/..." } ] } }Add the sourced web context
X tells you what people are saying; Chat tells you what the public web says about the same thing — recent coverage, competitor announcements, published changes. Keep the answer and the resources separately: the answer is a draft summary, the resources are the citable evidence.
listening.ts type ChatData = { data: { answer: string; resources: { link: string; title: string; snippet: string }[]; }; }; const context = await felo<ChatData>("/v2/chat", { query: "Recent customer complaints and competitor mentions around Acme", }); const sources = context.data.resources.slice(0, 5);Example output
{ "answer": "The main threads are …", "sources": [ { "link": "https://example.com/thread", "title": "…", "snippet": "…" } ] }Read the pages behind the answer
A snippet is not evidence. Fetch the strongest pages in Markdown with readability enabled so quotes and headings survive without navigation clutter, and keep the extraction cache on — re-running the same brief should not re-fetch the same page. Three pages is usually enough for a weekly brief.
listening.ts type ExtractData = { data: { content: string } }; const pages = await Promise.all( sources.slice(0, 3).map((source) => felo<ExtractData>("/v2/web/extract", { url: source.link, output_format: "markdown", with_readability: true, with_cache: true, }).then((page) => ({ ...source, content: page.data.content })), ), );Example output
// pages[0].content "# Example thread summary\n\nExtracted page content…"
Compose the brief in Markdown
This step is plain code, not an API call: assemble the posts, the context answer, the extracted pages, and the source list into one Markdown string. Rank posts by the metrics the response returned rather than by a guess about reach, and keep every item's link and date beside it.
listening.ts const date = new Date().toISOString().slice(0, 10); const ranked = [...posts].sort( (a, b) => b.metrics.favorite_count - a.metrics.favorite_count, ); const brief = [ `# Social listening brief — ${date}`, "", "## What the web says", context.data.answer, "", "## What people are posting", ...ranked.slice(0, 10).map( (post) => `- @${post.author.username} (${post.created_at.slice(0, 10)}): ${post.content}\n ${post.url}`, ), "", "## Pages behind the answer", ...pages.map((page) => `- [${page.title}](${page.link})`), "", "## Sources", ...sources.map((source) => `- [${source.title}](${source.link})`), ].join("\n"); console.log(brief);Archive the brief and compare with last week
Run this once per workspace: create the archive LiveDoc and keep its
short_idin your config. Every later run adds one document resource to the same archive, which is what makes week two more useful than week one — retrieval over past issues shows what changed instead of asking you to reread them.listening.ts type LiveDocData = { data: { short_id: string } }; const archive = await felo<LiveDocData>("/v2/livedocs", { name: "Social listening archive", description: "One document resource per weekly brief", }); await felo(`/v2/livedocs/${archive.data.short_id}/resources/doc`, { title: `Social listening — ${date}`, content: brief, }); type Retrieved = { data: { title: string; content: string; score: number }[] }; const previous = await felo<Retrieved>( `/v2/livedocs/${archive.data.short_id}/resources/retrieve`, { query: "what changed since last week" }, ).then((res) => res.data.filter((hit) => hit.score >= 0.7));Example output
[ { "title": "Social listening — 2026-10-23", "content": "…", "score": 0.88 } ]
What's included
- A dated set of X posts about your watch terms, with handles and links
- A sourced web summary from Chat, with the resources it used
- The pages behind that answer extracted as Markdown when they matter
- A Markdown brief with themes, risks, and next steps
- A LiveDoc archive so week-over-week comparison is a retrieval call
What it doesn't do
- Public posts and pages only; private communities, DMs, and Slack are out of scope
- X coverage is incomplete and skewed toward active accounts — it is a signal, not a survey
- No sentiment scoring, reach estimation, or engagement prediction
- It does not reply, post, follow, or contact anyone
- Small samples are labeled as small instead of being presented as a trend
Questions
What counts as a source in the brief?
X posts returned by the search inside your time window, plus public pages returned by Chat or extracted with Web Fetch. Every item keeps its URL and date.
Can it track competitors as well as our own brand?
Yes. Add competitor names to the watch terms. The brief separates mentions of you from mentions of them and says who is being recommended, in which context.
Does it measure sentiment?
No numeric sentiment score. It groups posts into themes and quotes them, and it labels thin samples. A score would imply a representative sample the search results do not provide.
How far back does it look?
The time window is a placeholder you resolve to explicit start and end dates. The X search call takes a Unix timestamp, so the window is enforced in the request rather than trusted to the model.
Can it run every week?
The prompt runs once. Put the schedule in an agent or cron job and reuse the same LiveDoc, so week four can compare itself with weeks one to three.
How much does a brief cost?
Each X search, Chat call, and page extraction spends usage-based credits; current rates are on the pricing page. The prompt asks for approval before any of them run.
Related workflows
Turn one question into a sourced research brief
Ask the web, pull the pages behind the answer as Markdown, add what X is saying, then keep the evidence in a LiveDoc you can query again later.
Open workflowResearchBuild a competitor teardown your sales team can use
Collect a competitor's own pages, the coverage around them, and what buyers say on X, then assemble a teardown where every claim carries a source, a date, and a status.
Open workflowContentShip a daily AI news digest your team actually reads
Get a sourced roundup, add what practitioners are posting on X, pull one video transcript, then compose the issue in Markdown and archive it in a LiveDoc.
Open workflowMake it yours
The same recipe, adapted to the way you actually ship.
Run a launch-week pulse
Shrink the window to 24 hours and add the launch terms to the query. The same five steps become a daily pulse: one LiveDoc document per day, and the comparison step answers "what is new since yesterday" instead of "since last week".
Track one competitor closely
Replace the watch terms with the competitor's brand and product names, and ask the brief to show who gets recommended in reply threads. Keep your own brand out of the query so the themes do not mix customers talking about you with customers talking about them.
Feed repeated complaints into your issue tracker
The "feed to product" section is the handoff. When the same complaint appears in two or more themes, open one ticket and link the brief document instead of copying quotes into the tracker — the LiveDoc keeps the source posts attached as the thread evolves.
Keep going
The reference pages behind this recipe, and where to go next.