> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mavera.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Feature Request Mining → Product Focus Group

### 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

<CodeGroup>
  ```python Python theme={"dark"}
  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]}")
  ```

  ```javascript JavaScript theme={"dark"}
  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)}`);
  }
  ```
</CodeGroup>

### Example Output

```text theme={"dark"}
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

<AccordionGroup>
  <Accordion title="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.</Accordion>
  <Accordion title="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.</Accordion>
  <Accordion title="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.</Accordion>
</AccordionGroup>
