Build a Discord FAQ Bot with Pinecone Free Tier & Gemini
What We’re Building
A Discord bot that turns your product docs, internal wikis, or any Markdown knowledge base into a vector-search FAQ machine. Users type /ask in a designated channel, the bot retrieves the most relevant chunks from Pinecone, ships them to Google Gemini with a strict prompt, and returns a concise, sourced answer — no hallucinations, no invented URLs.
Feature list:
/ask <question>slash command that responds in-thread.- Document ingestion pipeline: chunk Markdown files, embed with Hugging Face
all-MiniLM-L6-v2, upsert into Pinecone. - RAG-style prompt that forces the model to cite source file names.
- Fully free-tier: Discord bot (unlimited messages), Pinecone Starter (1 pod, 100K vectors), Google Gemini free tier (15 RPM), Hugging Face Inference API (free rate-limited endpoint).
- Single Python script for ingestion, single script for the bot — zero orchestration overhead.
Architecture & Data Flow
The ingestion side runs once (or on a cron). The runtime side is the Discord bot: it receives a question, embeds it with the same model, hits Pinecone for top-k matches, constructs a prompt that includes those chunks plus a system instruction, calls Gemini, and posts the result.
Prerequisites & Free Tier Setup
You need four free accounts. Grab them now — all sign-ups take under 5 minutes.
| Service | Free Tier Limit | Sign-Up Link |
|---|---|---|
| Discord Developer Portal | Unlimited bots, 50 slash commands per app | https://discord.com/developers/applications |
| Pinecone | 1 pod index, ~100K vectors, 1 project | https://www.pinecone.io |
| Google AI Studio (Gemini) | 15 requests/minute, 1,500/day for gemini-1.5-flash | https://aistudio.google.com |
| Hugging Face | Free Inference API for sentence-transformers/all-MiniLM-L6-v2 | https://huggingface.co |
Discord setup: Create a new application, go to Bot → Add Bot, copy the token. Under OAuth2 → URL Generator, select bot and applications.commands, then Send Messages, Read Message History, Use Slash Commands. Paste the generated URL into a browser to invite the bot to a test server.
Pinecone setup: Create a new index named faq-bot with dimension 384 (matching all-MiniLM-L6-v2), metric cosine. Choose the free Starter plan. Copy your API key and environment (e.g., us-east-1-aws).
Gemini setup: In Google AI Studio, click "Get API Key" and copy it. You'll use the gemini-1.5-flash model — fast, free, and more than adequate for RAG responses.
Hugging Face: You don't need a token for the public Inference API if you stay under the rate limit. But if you hit it, create a free account and generate a read token at https://huggingface.co/settings/tokens.
Step 1: Scaffold the Discord Bot
Create a project directory and install dependencies:
mkdir discord-faq-bot && cd discord-faq-bot
python -m venv venv && source venv/bin/activate # Windows: venv\Scripts\activate
pip install discord.py python-dotenv pinecone-client sentence-transformers google-generativeai requests
Create a .env file:
DISCORD_TOKEN=your_discord_bot_token
PINECONE_API_KEY=your_pinecone_api_key
PINECONE_ENV=us-east-1-aws
GEMINI_API_KEY=your_gemini_api_key
INDEX_NAME=faq-bot
Create bot.py with the minimal slash-command skeleton:
import os
import discord
from discord import app_commands
from dotenv import load_dotenv
load_dotenv()
TOKEN = os.getenv("DISCORD_TOKEN")
GUILD_ID = discord.Object(id=1234567890) # Replace with your test server ID
class FAQBot(discord.Client):
def __init__(self):
intents = discord.Intents.default()
intents.message_content = True
super().__init__(intents=intents)
self.tree = app_commands.CommandTree(self)
async def setup_hook(self):
self.tree.copy_global_to(guild=GUILD_ID)
await self.tree.sync(guild=GUILD_ID)
bot = FAQBot()
@bot.tree.command(name="ask", description="Ask a question about our documentation")
async def ask(interaction: discord.Interaction, question: str):
await interaction.response.defer(thinking=True)
answer = "placeholder — retrieval and LLM call coming in Step 3"
await interaction.followup.send(f"**Q:** {question}\n**A:** {answer}")
if __name__ == "__main__":
bot.run(TOKEN)
Run it with python bot.py. Type /ask in your test server to confirm the command registers.
Quick tip: To get your server (guild) ID, enable Developer Mode in Discord (User Settings → Advanced), right-click your server icon, and select "Copy ID".
Step 2: Ingest Documents into Pinecone
We'll chunk Markdown files, embed them, and push to Pinecone. Create a docs/ folder and drop in a few .md files — real product docs or lorem ipsum for testing.
Create ingest.py:
import os
import re
from dotenv import load_dotenv
from pinecone import Pinecone, ServerlessSpec
from sentence_transformers import SentenceTransformer
load_dotenv()
PINECONE_API_KEY = os.getenv("PINECONE_API_KEY")
INDEX_NAME = os.getenv("INDEX_NAME")
# Initialize Pinecone
pc = Pinecone(api_key=PINECONE_API_KEY)
if INDEX_NAME not in pc.list_indexes().names():
pc.create_index(
name=INDEX_NAME,
dimension=384,
metric="cosine",
spec=ServerlessSpec(cloud="aws", region="us-east-1")
)
index = pc.Index(INDEX_NAME)
# Load embedding model (runs locally, no API call needed after download)
embedder = SentenceTransformer("all-MiniLM-L6-v2")
def chunk_markdown(filepath: str, max_chars: int = 800) -> list[dict]:
"""Split a markdown file into overlapping chunks. Returns list of {text, metadata}."""
with open(filepath, "r", encoding="utf-8") as f:
text = f.read()
# Split on double newlines (paragraphs), then merge small ones
paragraphs = [p.strip() for p in re.split(r"\n\n+", text) if p.strip()]
chunks = []
current_chunk = ""
for para in paragraphs:
if len(current_chunk) + len(para) < max_chars:
current_chunk += para + "\n\n"
else:
if current_chunk:
chunks.append({"text": current_chunk.strip(), "source": os.path.basename(filepath)})
current_chunk = para + "\n\n"
if current_chunk:
chunks.append({"text": current_chunk.strip(), "source": os.path.basename(filepath)})
return chunks
def ingest_docs(docs_dir: str = "docs"):
vectors = []
for filename in os.listdir(docs_dir):
if filename.endswith(".md"):
filepath = os.path.join(docs_dir, filename)
chunks = chunk_markdown(filepath)
for i, chunk in enumerate(chunks):
embedding = embedder.encode(chunk["text"]).tolist()
vector_id = f"{filename}-chunk{i}"
vectors.append({
"id": vector_id,
"values": embedding,
"metadata": {"text": chunk["text"], "source": chunk["source"]}
})
print(f"Embedded {vector_id}")
# Batch upsert in groups of 100
batch_size = 100
for i in range(0, len(vectors), batch_size):
batch = vectors[i:i+batch_size]
index.upsert(vectors=batch)
print(f"Upserted batch {i//batch_size + 1}")
print(f"Ingestion complete. {len(vectors)} vectors in index.")
if __name__ == "__main__":
ingest_docs()
Run python ingest.py. You'll see each chunk get embedded and upserted. In the Pinecone console, your index should now show vector count > 0.
Why this embedding model?
all-MiniLM-L6-v2produces 384-dimensional vectors, runs fast on CPU, and is free. For a production bot with thousands of docs, you'd swap in a stronger model, but this keeps us inside every free tier.
Step 3: Wire Up the Q&A Command
Now replace the placeholder in bot.py with real retrieval and Gemini call. Add these functions above the FAQBot class:
import google.generativeai as genai
from pinecone import Pinecone
from sentence_transformers import SentenceTransformer
# Initialize shared clients
pc = Pinecone(api_key=os.getenv("PINECONE_API_KEY"))
index = pc.Index(os.getenv("INDEX_NAME"))
embedder = SentenceTransformer("all-MiniLM-L6-v2")
genai.configure(api_key=os.getenv("GEMINI_API_KEY"))
model = genai.GenerativeModel("gemini-1.5-flash")
def retrieve_context(query: str, top_k: int = 5) -> list[str]:
"""Embed the query, fetch top-k chunks from Pinecone."""
query_embedding = embedder.encode(query).tolist()
results = index.query(vector=query_embedding, top_k=top_k, include_metadata=True)
contexts = []
for match in results["matches"]:
source = match["metadata"].get("source", "unknown")
text = match["metadata"]["text"]
contexts.append(f"[Source: {source}]\n{text}")
return contexts
def generate_answer(question: str, contexts: list[str]) -> str:
"""Send question + contexts to Gemini with a strict system prompt."""
context_block = "\n\n---\n\n".join(contexts)
prompt = f"""You are a precise documentation assistant. Answer the user's question using ONLY the provided context.
If the answer is not in the context, say "I couldn't find that in the documentation."
Always cite the source file name in brackets when you use information from it.
Context:
{context_block}
Question: {question}
Answer:"""
response = model.generate_content(prompt)
return response.text
Then update the /ask command:
@bot.tree.command(name="ask", description="Ask a question about our documentation")
async def ask(interaction: discord.Interaction, question: str):
await interaction.response.defer(thinking=True)
try:
contexts = retrieve_context(question)
answer = generate_answer(question, contexts)
# Discord messages have a 2000-char limit; truncate if needed
if len(answer) > 1900:
answer = answer[:1900] + "..."
await interaction.followup.send(f"**Q:** {question}\n**A:** {answer}")
except Exception as e:
await interaction.followup.send(f"Something went wrong: {str(e)}")
That's the full bot. Restart it and test with /ask How do I reset my password? — assuming your docs contain that info.
Step 4: Run It Locally
# Terminal 1: one-time ingestion (or re-run when docs change)
python ingest.py
# Terminal 2: the bot
python bot.py
Keep the bot running. For a persistent setup, wrap it in a systemd service or a free-tier cloud VM (Oracle Cloud Always Free, Fly.io free allowance). But local is fine for testing and small teams.
Quick validation checklist:
-
/askcommand appears in Discord. - Bot defers, retrieves, and responds within 2-3 seconds.
- Answers cite source file names.
- Questions outside the docs return "I couldn't find that."
Extensions That Add Real Value
Once the core loop works, these upgrades turn a demo into a tool people actually rely on:
- Re-ingestion webhook. Add a simple HTTP endpoint (Flask on a side thread) that listens for a
POST /refreshand re-runsingest.py. Trigger it from a GitHub Action on docs push. - Thread-awareness. If someone asks a follow-up in a thread, pull the parent message context and include it in the prompt so Gemini understands conversational continuity.
- Feedback buttons. Add 👍/👎 reaction handlers. Log low-rated answers to a
feedback.jsonlfile so you can spot retrieval gaps and improve chunking. - Multi-source support. Extend
ingest.pyto accept PDFs or web pages. For PDFs, usepymupdf(free, MIT license). For web pages,requests+BeautifulSoup. This is the same pattern we explore in OCR It: Building a Document-to-LLM Pipeline When Copy-Paste Is Blocked. - Channel gating. Only respond in a
#docs-qachannel. Checkinteraction.channel.namebefore processing. - Rate limiting per user. Track
user_id→ last request time in a dict; if under 5 seconds, reply with a cooldown message. Protects your free-tier Gemini quota.
Common Pitfalls & How to Avoid Them
Pinecone index dimension mismatch. If you change embedding models, you must delete and recreate the index. all-MiniLM-L6-v2 = 384. text-embedding-004 (Gemini) = 768. They are not interchangeable. Stick with one.
Hugging Face rate limiting. The free Inference API throttles aggressively. The code above uses sentence-transformers locally, so embeddings run on your machine — no API calls, no rate limits. Don't accidentally swap to requests.post("https://api-inference.huggingface.co/...") unless you add retry logic.
Gemini safety filters. gemini-1.5-flash can refuse seemingly innocuous prompts if safety filters trigger. If you get empty responses, lower the safety thresholds:
response = model.generate_content(
prompt,
safety_settings={
"HARASSMENT": "block_none",
"HATE_SPEECH": "block_none",
"SEXUALLY_EXPLICIT": "block_none",
"DANGEROUS": "block_none",
}
)
Slash command sync delay. Discord can take up to an hour to propagate global commands. During development, use guild=GUILD_ID for instant registration (as shown above).
Chunk overlap. The simple paragraph splitter doesn't create overlapping chunks. For dense technical docs, add a 100-character overlap between chunks to avoid splitting critical context across boundaries.
Environment variables not loading. If os.getenv returns None, make sure .env is in the same directory as your script and you call load_dotenv() before accessing variables.
FAQ
Q: Can I use this in a server with 10,000 members? Yes, the architecture scales. Pinecone free tier handles ~100 queries/minute easily. The bottleneck is Gemini's 15 RPM free limit. For higher volume, add a simple in-memory cache (question → answer) or upgrade to Gemini's pay-as-you-go tier.
Q: What if my docs are 50MB of Markdown? The 100K vector limit on Pinecone free tier is your ceiling. At ~800 characters per chunk, that's roughly 80MB of text — plenty for most documentation sets. If you exceed it, prune old chunks or upgrade to the $70/month Standard plan.
Q: Why not use Discord's built-in AutoMod for FAQs? AutoMod does keyword matching. This bot does semantic search. "How do I cancel?" and "I want to stop my subscription" return the same docs. That's the vector DB advantage.
Q: How do I deploy this so it runs 24/7? The cheapest path: Oracle Cloud Always Free ARM VM (4 cores, 24GB RAM) runs this comfortably. Alternatively, a Raspberry Pi on your home network with a Cloudflare Tunnel. Both are $0/month. For a deeper look at always-on deployment patterns, check out What a Forward Deployed Engineer Actually Does in a Week: A Concrete Workflow.
Q: Can I swap Gemini for a fully local model?
Absolutely. Replace the genai.GenerativeModel call with a local Ollama instance running llama3.2 or mistral. This is the same pattern as Build a Job-Application Autofill Browser Extension with Local LLM. You'd lose the free-tier angle but gain zero rate limits and full privacy.
Q: My bot works in DMs but not in channels. Why?
Check the bot's OAuth2 scopes. It needs applications.commands for slash commands and Send Messages for the channel. Also confirm the bot has Read Message History and Use Slash Commands permissions in the server role settings.
Q: What's the latency breakdown? Embedding (local): ~50ms. Pinecone query: ~100ms. Gemini API: ~800ms. Total round-trip: ~1 second. If it's slower, check your network latency to Pinecone's region — pick the one closest to you during index creation.
Want to build like a Forward Deployed Engineer?
FDE Coach is a cohort-based program in frontend, backend, AWS, and AI. Build real products and get referred to 200+ hiring partners.
Explore the program