Discord Auto-Thread Bot on Val Town - Research Report
Architecture Overview
Constraint: Val Town has no WebSockets. Must use polling via REST API.
Solution: Cron job that runs every minute, polling Discord API every 2 seconds for new messages.
Key Findings
1. Polling Strategy ✅
Pipedream successfully polls Discord API every 15 seconds to 1 minute
Approach: Store last message ID, then poll for messages sent after that ID
Perfect for Val Town: Use interval val (cron) + internal polling loop
2. Rate Limiting (CRITICAL!)
Global: 50 requests/sec for authenticated bot requests
IP-based: IP addresses have separate 50 req/sec limit if no Authorization header
Invalid Request Ban: 10,000 invalid requests per 10 min = 24-hour IP ban (401/403/429 count as invalid)
Your situation: Val Town shares IP addresses across all users. Risk of hitting shared limits!
3. Rate Limit Handling
Parse response headers to prevent hitting limits, handle 429s accordingly
On 429 response, Discord sends retry_after field indicating when to retry
Headers to check: X-RateLimit-Remaining, X-RateLimit-Reset, Retry-After
4. Proxy Consideration
IP rotation proxies distribute load across multiple IPs, preventing single IP rate limits
Options: Twilight HTTP-Proxy, custom Nginx setup, or simple proxy services
For Val Town: Recommended if scaling, but for single bot: careful request pacing sufficient
5. Discord API for Messages
Endpoint:GET /channels/{channel_id}/messages
Params:limit (1-100), after (message_id), before, around
Cursor approach: Store after ID from previous poll, use it next time
Bot needs: MESSAGE_CONTENT intent to read message content
Use @discordjs/rest package (REST-only, no WebSockets) OR raw fetch calls
SQLite for persistence (store last message IDs per channel)
Recommended Implementation
Tech Stack
// Dependenciesimport { sqlite } from"https://esm.town/v/std/sqlite@14-main/main.ts";
// Lightweight REST - use native fetch (no discord.js)// Reason: discord.js includes gateway code we don't needconstTOKEN = Deno.env.get("DISCORD_BOT_TOKEN");
constAPI_BASE = "https://discord.com/api/v10";
File Structure
main.ts (interval - runs every minute)
├─ Polls Discord API every 2 seconds for 55 seconds
├─ Stores state in SQLite (last_message_id per channel)
└─ Calls createThread.ts for each new message
createThread.ts (script - utility)
├─ Calls OpenAI/Claude for thread title
└─ Creates thread via Discord API
config.ts (script - configuration)
└─ Channel IDs, API endpoints
Rate Limit Strategy
Request Pacing: Add 200ms delay between Discord API calls
Backoff: On 429, respect Retry-After header
Monitoring: Log rate limit headers
Fallback: If hit hard limits, skip cycle and retry next minute
Gotchas to Address
⚠️ Message Filtering: Bot sees its own messages; filter by author.id
⚠️ Thread Duplication: Store processed message IDs in SQLite to prevent duplicates
⚠️ Cold Starts: Interval vals start fresh; store state in DB not memory
⚠️ IP Sharing: Multiple bots on Val Town = shared IP rate limits
⚠️ Authorization: Always include Authorization: Bot TOKEN to use bot token limits (higher than IP limits)