Skip to main content

Twitch Rewards Bot Module

A Twitch-only bot module that handles Channel Points reward redemptions. Demonstrates event-based triggers with twitch:reward, a reward queue using ctx.cache, and auto-responses.

Project structure

twitch-rewards/
├── lumio.config.json
├── server/
│ ├── schema.ts
│ └── functions.ts
├── package.json
└── tsconfig.json

lumio.config.json

{
"$schema": "https://lumio.vision/schemas/lumio.config.schema.json",
"extension_id": "ext_placeholder",
"name": "Twitch Rewards Bot",
"category": "bot_module",
"version": "1.0.0",
"platforms": ["twitch"],
"server": true,
"permissions": ["chat:send", "events:read"],
"triggers": {
"commands": [
{
"name": "queue",
"description": "Show the current reward queue",
"cooldown_global": 10,
"cooldown_user": 30,
"min_role": "everyone"
},
{
"name": "clearqueue",
"description": "Clear the reward queue",
"cooldown_global": 5,
"min_role": "moderator"
}
],
"events": ["twitch:reward"]
},
"config_schema": [
{
"key": "maxQueueSize",
"type": "number",
"label": "Maximum reward queue size",
"default_value": 20,
"required": true
},
{
"key": "announceRewards",
"type": "boolean",
"label": "Announce reward redemptions in chat",
"default_value": true,
"required": true
}
]
}

server/schema.ts

import { defineSchema, defineTable, column } from "@zaflun/lumio-sdk/server";

export default defineSchema({
reward_history: defineTable({
user_id: column.string(),
user_name: column.string(),
reward_title: column.string(),
reward_cost: column.number(),
user_input: column.string(),
timestamp: column.number(),
}),
});

server/functions.ts

import { command, event } from "@zaflun/lumio-sdk/server";

// Event handler — fires on Channel Points reward redemptions
export const rewardHandler = event("twitch:reward", async (ctx, evt) => {
const rewardTitle = (evt.reward_title as string) ?? "Unknown Reward";
const userName = evt.user_name as string;
const userInput = (evt.user_input as string) ?? "";
const rewardCost = (evt.reward_cost as number) ?? 0;

// Add to the reward queue in cache
const maxQueueSize = (ctx.config.maxQueueSize as number) ?? 20;
const queueLength = await ctx.cache.increment("queue_length", 1);

if (queueLength > maxQueueSize) {
// Queue is full — decrement back and notify
await ctx.cache.increment("queue_length", -1);
return { reply: `Sorry ${userName}, the reward queue is full! Try again later.` };
}

// Store the reward in the queue
await ctx.cache.set(`queue:${queueLength}`, JSON.stringify({
user: userName,
reward: rewardTitle,
input: userInput,
cost: rewardCost,
time: Date.now(),
}), 7200); // 2-hour TTL

// Log to database for history
ctx.defer(async () => {
await ctx.db.insert("reward_history", {
user_id: evt.user_id,
user_name: userName,
reward_title: rewardTitle,
reward_cost: rewardCost,
user_input: userInput,
timestamp: Date.now(),
});
});

const announceRewards = (ctx.config.announceRewards as boolean) ?? true;
if (announceRewards) {
const inputPart = userInput ? ` — "${userInput}"` : "";
return {
reply: `${userName} redeemed "${rewardTitle}" (${rewardCost} pts)${inputPart} [Queue #${queueLength}]`,
};
}
return null;
});

// !queue — Show the current reward queue
export const queue = command("queue", async (ctx, args) => {
const queueLength = ((await ctx.cache.get("queue_length")) as number) ?? 0;

if (queueLength === 0) {
return { reply: "The reward queue is empty!" };
}

const items: string[] = [];
for (let i = 1; i <= Math.min(queueLength, 5); i++) {
const raw = (await ctx.cache.get(`queue:${i}`)) as string | null;
if (raw) {
const entry = JSON.parse(raw);
items.push(`#${i}: ${entry.user}${entry.reward}`);
}
}

const moreText = queueLength > 5 ? ` (+${queueLength - 5} more)` : "";
return { reply: `Reward queue: ${items.join(" | ")}${moreText}` };
});

// !clearqueue — Clear the reward queue (moderator only)
export const clearqueue = command("clearqueue", async (ctx, args) => {
const queueLength = ((await ctx.cache.get("queue_length")) as number) ?? 0;

// Delete all queue entries
for (let i = 1; i <= queueLength; i++) {
await ctx.cache.delete(`queue:${i}`);
}
await ctx.cache.delete("queue_length");

return { reply: `Reward queue cleared (${queueLength} item(s) removed).` };
});

Key concepts demonstrated

  • Platform-specific eventstwitch:reward fires only on Twitch Channel Points redemptions
  • Single-platform extensionplatforms: ["twitch"] restricts this module to Twitch only
  • Cache-based queuectx.cache stores the reward queue with a 2-hour TTL, using sequential keys
  • Atomic counterctx.cache.increment() manages the queue length without race conditions
  • Role-gated commands!clearqueue requires moderator role via min_role: "moderator"
  • Background history loggingctx.defer() persists reward redemptions to the database for long-term history