Scenario
Feature requests in#feature-requests or #ideas channels are gold — but they’re unstructured and difficult to prioritize. This job extracts and clusters requests, creates user personas based on the types of requestors, then runs a Mavera Focus Group to pressure-test each feature’s appeal across user segments and generate a ranked priority list.
Flow: Discord messages → Extract feature requests → Mavera POST /mave/chat (cluster) → POST /personas → POST /focus-groups → Ranked priorities
Code
import os, requests, time
DC_TOKEN = os.environ["DISCORD_BOT_TOKEN"]
DC_BASE = "https://discord.com/api/v10"
DC_H = {"Authorization": f"Bot {DC_TOKEN}"}
MV = os.environ["MAVERA_API_KEY"]
MV_BASE = "https://app.mavera.io/api/v1"
MV_H = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}
FEATURE_CHANNEL = "CHANNEL_ID_FEATURES"
# 1. Fetch feature request messages
msgs = []
after = "0"
while len(msgs) < 300:
r = requests.get(f"{DC_BASE}/channels/{FEATURE_CHANNEL}/messages", headers=DC_H,
params={"limit": 100, "after": after})
if r.status_code == 429:
time.sleep(r.json().get("retry_after", 1))
continue
if not r.ok:
break
batch = r.json()
if not batch:
break
for m in batch:
if not m.get("author",{}).get("bot",False) and len(m.get("content","")) > 15:
reactions = sum(r.get("count",0) for r in m.get("reactions", []))
msgs.append({"text": m["content"][:400], "reactions": reactions,
"author_roles": [r.get("name","") for r in m.get("member",{}).get("roles",[])]})
after = batch[0]["id"]
time.sleep(0.5)
print(f"Feature requests: {len(msgs)}")
# 2. Cluster via Mave
corpus = "\n".join(
f"- (reactions: {m['reactions']}) {m['text'][:250]}"
for m in sorted(msgs, key=lambda x: -x["reactions"])[:80]
)
clustering = requests.post(f"{MV_BASE}/mave/chat", headers=MV_H, json={
"message": f"Product analyst. Cluster {len(msgs)} Discord feature requests into top 8 themes.\n\n"
f"{corpus[:6000]}\n\n"
"For each:\n- Feature name (3-5 words)\n- Request count\n- Total reactions (proxy for community votes)\n"
"- Representative request (exact quote)\n- Complexity estimate (low/medium/high)\n"
"Rank by reactions × frequency."
}).json()
clusters = clustering.get("content", "")
print(f"\nClusters:\n{clusters[:1200]}")
# 3. Create community-segment personas
SEGMENTS = [
{"name": "Free Tier Power User", "desc": "Uses daily, hits limits constantly. Won't pay but evangelizes to others. Cares about functionality over polish."},
{"name": "Pro Subscriber", "desc": "Paying $15/month. Expects reliability and premium features. Compares to competitors. Values ROI."},
{"name": "Community Moderator", "desc": "Manages a 500+ member server using the product. Cares about admin tools, permissions, analytics. Loyal but demanding."},
{"name": "Developer / API User", "desc": "Builds integrations. Cares about API quality, documentation, rate limits, webhooks. Will leave for better DX."},
]
persona_ids = []
for seg in SEGMENTS:
p = requests.post(f"{MV_BASE}/personas", headers=MV_H, json={
"name": f"Discord: {seg['name']}", "description": seg["desc"],
}).json()
persona_ids.append(p["id"])
time.sleep(0.3)
# 4. Focus Group
fg = requests.post(f"{MV_BASE}/focus-groups", headers=MV_H, json={
"name": "Discord Feature Request Validation",
"persona_ids": persona_ids,
"questions": [
f"Here are the top community feature requests:\n{clusters[:2000]}\n\nRank your top 3 and explain why.",
"Which feature would make you upgrade (or stay on your current plan)?",
"Is there a request NOT on this list that matters more to you?",
"If we shipped your #1 request but it was behind a paywall, would you pay?",
],
"responses_per_persona": 2,
}).json()
for _ in range(25):
time.sleep(5)
data = requests.get(f"{MV_BASE}/focus-groups/{fg['id']}", headers=MV_H).json()
if data.get("status") == "completed":
break
print(f"\n{'='*60}\nFEATURE REQUEST FOCUS GROUP\n{'='*60}")
for resp in data.get("responses", []):
idx = persona_ids.index(resp.get("persona_id")) if resp.get("persona_id") in persona_ids else -1
name = SEGMENTS[idx]["name"] if 0 <= idx < len(SEGMENTS) else "Unknown"
print(f"\n[{name}] Q: {resp.get('question','')[:80]}")
print(f" A: {resp.get('answer','')[:350]}")
const DC_TOKEN = process.env.DISCORD_BOT_TOKEN;
const DC_BASE = "https://discord.com/api/v10";
const DC_H = { Authorization: `Bot ${DC_TOKEN}` };
const MV = process.env.MAVERA_API_KEY;
const MV_BASE = "https://app.mavera.io/api/v1";
const MV_H = { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" };
const FEATURE_CHANNEL = "CHANNEL_ID_FEATURES";
// 1. Fetch
const msgs = [];
let after = "0";
while (msgs.length < 300) {
const r = await fetch(`${DC_BASE}/channels/${FEATURE_CHANNEL}/messages?limit=100&after=${after}`, { headers: DC_H });
if (r.status === 429) { await new Promise(res => setTimeout(res, (await r.json()).retry_after*1000||1000)); continue; }
const batch = await r.json();
if (!batch.length) break;
for (const m of batch) {
if (!m.author?.bot && (m.content||"").length > 15) {
const reactions = (m.reactions||[]).reduce((s,r) => s + (r.count||0), 0);
msgs.push({ text: (m.content||"").slice(0,400), reactions });
}
}
after = batch[0].id;
await new Promise(r => setTimeout(r, 500));
}
console.log(`Feature requests: ${msgs.length}`);
// 2. Cluster
const corpus = msgs.sort((a,b) => b.reactions-a.reactions).slice(0,80)
.map(m => `- (reactions: ${m.reactions}) ${m.text.slice(0,250)}`).join("\n");
const clustering = await fetch(`${MV_BASE}/mave/chat`, { method: "POST", headers: MV_H,
body: JSON.stringify({ message: `Cluster ${msgs.length} feature requests into top 8.\n\n${corpus.slice(0,6000)}\n\nPer theme: name, count, reactions, quote, complexity. Rank by reactions × frequency.` }),
}).then(r => r.json());
const clusters = clustering.content || "";
// 3. Personas
const SEGMENTS = [
{ name: "Free Power User", desc: "Daily user, hits limits. Won't pay but evangelizes." },
{ name: "Pro Subscriber", desc: "$15/mo. Expects premium. Compares competitors." },
{ name: "Community Mod", desc: "Manages 500+ server. Admin tools, analytics." },
{ name: "Developer / API", desc: "Builds integrations. API quality, DX." },
];
const personaIds = [];
for (const seg of SEGMENTS) {
const p = await fetch(`${MV_BASE}/personas`, { method: "POST", headers: MV_H,
body: JSON.stringify({ name: `Discord: ${seg.name}`, description: seg.desc }),
}).then(r => r.json());
personaIds.push(p.id);
await new Promise(r => setTimeout(r, 300));
}
// 4. Focus Group
const fg = await fetch(`${MV_BASE}/focus-groups`, { method: "POST", headers: MV_H,
body: JSON.stringify({ name: "Feature Request Validation", persona_ids: personaIds,
questions: [`Requests:\n${clusters.slice(0,2000)}\n\nRank top 3.`,
"Which feature would make you upgrade?",
"Missing request that matters more?",
"Pay for #1 behind a paywall?"],
responses_per_persona: 2 }),
}).then(r => r.json());
let data;
for (let i = 0; i < 25; i++) {
await new Promise(r => setTimeout(r, 5000));
data = await fetch(`${MV_BASE}/focus-groups/${fg.id}`, { headers: MV_H }).then(r => r.json());
if (data.status === "completed") break;
}
console.log(`\n${"=".repeat(60)}\nFEATURE REQUEST FOCUS GROUP`);
for (const resp of data.responses || []) {
const idx = personaIds.indexOf(resp.persona_id);
console.log(`\n[${idx>=0?SEGMENTS[idx].name:"?"}] Q: ${(resp.question||"").slice(0,80)}`);
console.log(` A: ${(resp.answer||"").slice(0,350)}`);
}
Example Output
Feature requests: 243
Clusters:
1. Dark Mode (48 requests, 312 reactions) — "dark mode please my eyes"
2. API Webhooks (35 requests, 287 reactions) — "webhook support for events"
3. Mobile App Rebuild (29 requests, 245 reactions) — "mobile app is unusable"
4. Custom Dashboards (22 requests, 198 reactions) — "let us build our own dashboards"
5. Keyboard Shortcuts (19 requests, 156 reactions) — "vim keybindings when?"
FEATURE REQUEST FOCUS GROUP
============================================================
[Free Power User] Q: Rank top 3
A: 1. Dark mode — I use this at night, it's actually painful.
2. Keyboard shortcuts — efficiency matters when you're in it 8 hours.
3. Mobile app — would use on commute but it's too broken.
[Developer / API] Q: Which feature would make you upgrade?
A: Webhooks, easily. I'm polling your API every 30 seconds which is
wasteful for both of us. Real-time webhooks = I'd upgrade to Pro
immediately because my integration quality jumps dramatically.
[Pro Subscriber] Q: Pay for #1 behind a paywall?
A: Depends. Dark mode behind a paywall would feel insulting — it's
a basic accessibility feature. Custom dashboards behind Pro? Sure,
that's clearly a power feature. Know the difference.
Error Handling
Reaction data
Reaction data
Reactions serve as community votes. Some servers use custom emojis for upvoting — check
emoji.name to identify vote reactions vs. casual emoji use.Duplicate requests
Duplicate requests
Users often post the same request in different channels. The clustering step in Mave handles deduplication by theme, but consider cross-referencing channel IDs.
Focus Group bias
Focus Group bias
Personas may over-index on vocal segments. Balance by including a “silent majority” persona who uses the product but never posts in Discord.