auto-thread
Val Town is a collaborative website to build and scale JavaScript apps.
Deploy APIs, crons, & store data – all from the browser, and deployed in milliseconds.
Viewing readonly version of main branch: v3View latest version
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.
- 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
- 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!
- Parse response headers to prevent hitting limits, handle 429s accordingly
- On 429 response, Discord sends
retry_afterfield indicating when to retry - Headers to check:
X-RateLimit-Remaining,X-RateLimit-Reset,Retry-After
- 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
- Endpoint:
GET /channels/{channel_id}/messages - Params:
limit(1-100),after(message_id),before,around - Cursor approach: Store
afterID from previous poll, use it next time - Bot needs:
MESSAGE_CONTENTintent to read message content
- 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
- Official guide: https://docs.val.town/integrations/discord/how-to-make-a-discord-bot-hosted-24-7-for-free-in-/
- Use
@discordjs/restpackage (REST-only, no WebSockets) OR raw fetch calls - SQLite for persistence (store last message IDs per channel)
// 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";
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
- Request Pacing: Add 200ms delay between Discord API calls
- Backoff: On 429, respect
Retry-Afterheader - Monitoring: Log rate limit headers
- Fallback: If hit hard limits, skip cycle and retry next minute
- ⚠️ 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 TOKENto use bot token limits (higher than IP limits)
- Discord API Reference: https://discord.com/developers/docs/reference
- Rate Limits Deep Dive: https://discord.com/developers/docs/topics/rate-limits
- Val Town Discord Guide: https://docs.val.town/integrations/discord/how-to-make-a-discord-bot-hosted-24-7-for-free-in-/
- Turso/SQLite in Val: https://docs.val.town/reference/std/sqlite/usage/
