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

6. Thread Creation

  • Endpoint: POST /channels/{channel_id}/messages/{message_id}/threads
  • Body: { name, auto_archive_duration }
  • Requires message ID and channel ID
  • Can use message content to generate AI thread name

7. Val Town Discord Resources

Tech Stack

// Dependencies import { 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 need const TOKEN = Deno.env.get("DISCORD_BOT_TOKEN"); const API_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

  1. Request Pacing: Add 200ms delay between Discord API calls
  2. Backoff: On 429, respect Retry-After header
  3. Monitoring: Log rate limit headers
  4. 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)