# Discord Bot Development

> Discord Bot Development

- Skill: `lucadominguez/discord-bot-development` (Agent Skill)
- Install (CLI): `npx skillmds@latest add lucadominguez/discord-bot-development`
- Raw SKILL.md: https://api.skillmd.com/api/skills/lucadominguez/discord-bot-development/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: lucadominguez (https://skillmd.com/u/lucadominguez)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/lucadominguez/discord-bot-development

---

# Discord Bot Development

Build production Discord bots with discord.py, Hermes-powered AI filtering, multi-source data ingestion, and autonomous maintenance cron jobs.

## Quick Architecture

```
┌──────────────────────────────────────────┐
│           discord.py Bot                 │
├──────────────────────────────────────────┤
│  @tasks.loop()  │  @bot.command()        │
│  Reddit (PRAW)  │  !check, !status       │
│  arXiv/PubMed   │                        │
│  Patent APIs    │                        │
└────────┬─────────┴──────────┬───────────┘
         │                    │
         ▼                    ▼
   Hermes AI Filter      cron job (every N days)
   "is this novel?"      checks liveness, logs,
   → posts to channel     restarts if crashed
```

## Project Structure

```
~/discord-bots/
├── venv/                          # isolated Python environment
├── run.sh                         # start/stop/restart/status launcher
└── bot-name/
    ├── bot.py                     # Discord client, commands, scheduled tasks
    ├── config.py                  # tokens, channel IDs, intervals, API keys
    ├── filter.py                  # Hermes AI filtering (subprocess)
    ├── requirements.txt
    ├── fetchers/                  # data source modules
    │   ├── __init__.py
    │   ├── reddit.py              # PRAW-based Reddit scraper
    │   ├── papers.py              # arXiv + PubMed/EuropePMC
    │   └── patents.py             # Google Patents / USPTO
    └── data/
        └── state.json             # seen-item dedup (auto-created)
```

## Bot Core (bot.py)

### Scheduled Task Pattern

Use `@tasks.loop(minutes=N)` with a `bot.wait_until_ready()` guard:

```python
from discord.ext import commands, tasks

bot = commands.Bot(command_prefix="!", intents=intents)

@tasks.loop(minutes=30)
async def check_source():
    await bot.wait_until_ready()
    items = fetch_and_filter()
    await post_to_channel(items, "channel-key")

@bot.event
async def on_ready():
    if not check_source.is_running():
        check_source.start()
```

### Posting with Embeds

Use `discord.Embed` for formatted posts. Assign distinct colors per source type for visual scanning:

```python
def format_post(item: dict, item_type: str) -> discord.Embed:
    colors = {"reddit": 0xFF4500, "paper": 0x3498DB, "patent": 0xE74C3C}
    embed = discord.Embed(
        title=item["title"][:256],
        url=item["url"],
        color=colors[item_type],
        description=item.get("abstract", item.get("content", ""))[:1500]
    )
    embed.add_field(name="AI Verdict", value=item.get("reason", "relevant")[:1024], inline=False)
    return embed
```

**Pitfall:** `embed.add_field` values are capped at 1024 chars. Long AI reasons or abstracts must be truncated. Also, `title` is capped at 256 chars — slice it when constructing.

### Manual Trigger Commands

Give admins a `!check` command for on-demand runs:

```python
@bot.command(name="check")
@commands.has_permissions(administrator=True)
async def manual_check(ctx, source: str = ""):
    # source: reddit|papers|patents|all
    await ctx.send(f"⚡ Running {source or 'all'} check...")
    # ... fetch, filter, post ...
```

## AI Filtering via Hermes (filter.py)

Call Hermes as a subprocess from the bot. Each call is a fresh session — include all context in the prompt:

```python
import json, subprocess

HERMES_CMD = ["hermes", "chat", "-q", "--quiet"]

PROMPTS = {
    "reddit": """Evaluate this Reddit post for novelty. Title: {title}. Content: {content}.
    Return ONLY JSON: {{"relevant": true/false, "reason": "one sentence"}}""",
    # ... per-source prompts ...
}

def filter_content(content_type: str, data: dict) -> dict:
    if not data.get("title"):
        return {"relevant": False, "reason": "no title"}
    prompt = PROMPTS[content_type].format(**data)
    result = subprocess.run(HERMES_CMD + [prompt], capture_output=True, text=True, timeout=90)
    # Extract JSON from response (may be wrapped in markdown)
    output = result.stdout.strip()
    start = output.find("{")
    end = output.rfind("}") + 1
    if start >= 0 and end > start:
        return json.loads(output[start:end])
    # Fallback: keyword check
    if any(w in output.lower() for w in ["true", "relevant", "yes", "novel"]):
        return {"relevant": True, "reason": output[:100]}
    return {"relevant": False, "reason": output[:100]}
```

**Key design decisions:**
- Each item gets its own Hermes call (not batch) — avoids overwhelming context
- `time.sleep(0.5)` between calls to rate-limit Hermes
- JSON extraction handles Hermes wrapping output in markdown code blocks
- Fallback keyword check catches non-JSON responses

### Per-Source Prompt Design

The prompt is the most important piece. It must define "relevant" with concrete rules:

```python
FILTER_PROMPTS = {
    "reddit": """Evaluate: Is this post offering truly novel, useful, or esoteric knowledge?
    NO: beginner questions, sourcing requests, repeated topics, personal anecdotes without insight
    YES: novel mechanisms, under-discussed compounds, detailed experience reports with unique observations""",

    "paper": """Evaluate: Is this paper offering truly novel knowledge?
    NO: well-known mechanisms, incremental results, replication of established findings
    YES: novel compounds, unexpected mechanisms, under-studied pathways, surprising results""",

    "patent": """Evaluate: Is this patent describing a truly novel compound/technology?
    NO: obvious combinations, minor formulation tweaks, patents on well-known compounds
    YES: novel chemical structures, unexpected synergistic combinations, genuinely new mechanisms""",
}
```

## State Tracking (Avoid Reposts)

Maintain a `data/state.json` file tracking seen item IDs per source:

```python
def load_seen() -> set:
    if not os.path.exists(STATE_FILE):
        return set()
    with open(STATE_FILE) as f:
        return set(json.load(f).get("seen_reddit", []))

def save_seen(seen: set):
    state = {}
    if os.path.exists(STATE_FILE):
        with open(STATE_FILE) as f:
            state = json.load(f)
    state["seen_reddit"] = list(seen)
    state["last_reddit_check"] = datetime.now(timezone.utc).isoformat()
    os.makedirs(os.path.dirname(STATE_FILE), exist_ok=True)
    with open(STATE_FILE, "w") as f:
        json.dump(state, f, indent=2)
```

Use the source + item ID as the dedup key (e.g., `arxiv:2103.12345`, post IDs from PRAW, patent IDs).

## Data Fetchers

### Reddit (PRAW)

```python
import praw

reddit = praw.Reddit(
    client_id=REDDIT_CLIENT_ID,
    client_secret=REDDIT_CLIENT_SECRET,
    user_agent="BotName/1.0 (by /u/username)",
)

for sub in ["nootopics", "nootropics"]:
    for post in reddit.subreddit(sub).new(limit=15):
        if post.score < MIN_SCORE or post.id in seen_ids:
            continue
        # ... filter and collect ...
```

**Credentials:** Create at https://www.reddit.com/prefs/apps → "script" app.

### arXiv API

Free, no auth. XML-based API at `http://export.arxiv.org/api/query`:

```python
import urllib.request, xml.etree.ElementTree as ET
from urllib.parse import urlencode

params = {
    "search_query": '(cognitive OR nootropic) AND (cat:q-bio.NC)',
    "max_results": 20,
    "sortBy": "submittedDate",
    "sortOrder": "descending",
}
url = f"http://export.arxiv.org/api/query?{urlencode(params)}"
# Parse XML response via ElementTree
# Extract: title, summary (abstract), authors, id, published
```

### PubMed / EuropePMC

Free, no auth. REST API at `https://www.ebi.ac.uk/europepmc/webservices/rest/search`:

```python
params = {
    "query": '(nootropic OR "cognitive enhancement")',
    "resultType": "core",
    "pageSize": 10,
    "format": "json",
    "sort": "FIRST_PUBLICATION_DATE desc",
}
# Returns JSON with resultList.result[] containing title, abstractText, authorString
```

### Google Patents

Free, no auth. Returns JSON (with a `)]}'` prefix to strip):

```python
query = quote('"nootropic" cognitive')
url = f"https://patents.google.com/?q={query}&language=ENGLISH&format=json&num=10"
# Strip ")]}'" prefix before JSON parsing
# Navigate: data["results"]["cluster"][0]["result"][0]["patent"]
```

## Bot Launcher (run.sh)

A bash script for lifecycle management. Avoids the agent needing to remember the venv activation and nohup pattern:

```bash
#!/bin/bash
BOT_DIR="$HOME/discord-bots/bot-name"
VENV="$HOME/discord-bots/venv"
PID_FILE="$BOT_DIR/.bot.pid"

start_bot() {
    if [ -f "$PID_FILE" ] && kill -0 $(cat "$PID_FILE") 2>/dev/null; then
        echo "Already running (PID: $(cat $PID_FILE))"
        return
    fi
    cd "$BOT_DIR"
    source "$VENV/bin/activate"
    nohup python bot.py >> bot.log 2>&1 &
    echo $! > "$PID_FILE"
    echo "Started (PID: $!)"
}
```

Supports: `start`, `stop`, `restart`, `status`.

## Auto-Maintenance Cron Job

Set up a Hermes cron job that checks bot health every few days. The prompt must be self-contained with exact paths:

```
You are the maintenance agent for the Discord bot at /home/user/discord-bots/bot-name/

Your job:
1. Check if the bot process is running: ps aux | grep "bot.py"
2. Read last 30 lines of bot.log for ERROR lines
3. Check state.json — when was the last data check? >12h since last check = warning
4. If bot is NOT running, restart it: /home/user/discord-bots/run.sh start
5. Report status concisely: "RUNNING/STOPPED. Errors: X. Last activity: Y. Action: ..."
```

Create it:
```
cronjob action=create schedule="0 9 */3 * *" name="Bot Maintenance" prompt="..."
```

This runs every 3 days at 9 AM. Adjust frequency based on bot criticality.

## Setup Checklist

1. **Discord bot token:** https://discord.com/developers/applications → New App → Bot → Copy Token
2. **Invite bot to server:** OAuth2 → URL Generator → bot + Send Messages + Embed Links → paste URL in browser
3. **Channel IDs:** Enable Developer Mode in Discord → right-click channel → Copy ID
4. **Reddit API creds (if needed):** https://www.reddit.com/prefs/apps → script app
5. **Python venv:** `python3 -m venv venv && source venv/bin/activate && pip install discord.py praw aiohttp`
6. **Test:** `./run.sh start` → check `bot.log` → `!status` in Discord
7. **Cron:** create maintenance job

## Pitfalls

- **`embed.add_field` value cap:** 1024 chars. AI-filter reasons and abstracts must be sliced.
- **`embed.title` cap:** 256 chars. Slice when constructing.
- **Rate limiting Hermes:** Space AI filter calls by 0.5+ seconds to avoid overwhelming the model.
- **Hermes JSON extraction:** Hermes often wraps JSON in markdown code blocks. Extract `{...}` substring rather than expecting raw JSON.
- **State file race:** The bot writes `state.json` after each check. If multiple sources run concurrently, they'll clobber each other. Use per-source state files or a single-threaded check loop if sources share a state file.
- **WSL cold-start for maintenance cron:** If the maintenance cron runs when WSL isn't warmed up, `hermes chat -q` may take 5-15 seconds to start. Not an issue for a 3-day cron — the delay is negligible.
- **Google Patents JSON prefix:** Responses start with `)]}'` — strip before `json.loads()`.
- **arXiv rate limiting:** Be polite — space requests by 3+ seconds. arXiv asks for this in their API terms.
