Skip to main content

Kick Follow Bot Module

A Kick-only bot module that announces new followers and tracks follow streaks using ctx.cache for fast counter access and ctx.defer() for background database persistence.

Project structure

kick-follow-bot/
├── 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": "Kick Follow Bot",
"category": "bot_module",
"version": "1.0.0",
"platforms": ["kick"],
"server": true,
"permissions": ["chat:send", "events:read"],
"triggers": {
"commands": [
{
"name": "followers",
"description": "Show the follow streak count for this stream",
"cooldown_global": 10,
"cooldown_user": 30,
"min_role": "everyone"
},
{
"name": "recentfollows",
"description": "Show the most recent followers",
"cooldown_global": 10,
"cooldown_user": 30,
"min_role": "everyone"
}
],
"events": ["kick:follower"]
},
"config_schema": [
{
"key": "announceFollows",
"type": "boolean",
"label": "Announce new followers in chat",
"default_value": true,
"required": true
},
{
"key": "streakMilestone",
"type": "number",
"label": "Follow streak milestone for special message",
"default_value": 10,
"required": true
},
{
"key": "welcomeEmote",
"type": "string",
"label": "Emote to include in welcome message",
"default_value": "",
"required": false
}
]
}

server/schema.ts

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

export default defineSchema({
followers: defineTable({
user_id: column.string(),
user_name: column.string(),
followed_at: column.number(),
stream_session: column.string(),
}),
});

server/functions.ts

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

// Event handler — new Kick follower
export const followHandler = event("kick:follower", async (ctx, evt) => {
const userName = evt.user_name as string;

// Increment the follow streak counter
const streakCount = await ctx.cache.increment("follow_streak", 1);

// Store follower record in background
ctx.defer(async () => {
await ctx.db.insert("followers", {
user_id: evt.user_id,
user_name: userName,
followed_at: Date.now(),
stream_session: await getSessionId(ctx),
});
});

const announceFollows = (ctx.config.announceFollows as boolean) ?? true;
if (!announceFollows) {
return null;
}

const emote = (ctx.config.welcomeEmote as string) ?? "";
const emotePart = emote ? ` ${emote}` : "";
const milestone = (ctx.config.streakMilestone as number) ?? 10;

// Check for milestone
if (streakCount > 0 && streakCount % milestone === 0) {
return {
reply: `${streakCount} follow streak! Welcome ${userName}!${emotePart} We've hit ${streakCount} new followers this stream!`,
};
}

return {
reply: `Welcome ${userName}!${emotePart} (Follow #${streakCount} this stream)`,
};
});

// !followers — Show the current follow streak count
export const followers = command("followers", async (ctx, args) => {
const streakCount = ((await ctx.cache.get("follow_streak")) as number) ?? 0;

if (streakCount === 0) {
return { reply: "No new followers this stream yet!" };
}

return { reply: `${streakCount} new follower(s) this stream!` };
});

// !recentfollows — Show the most recent followers
export const recentfollows = command("recentfollows", async (ctx, args) => {
const allFollowers = await ctx.db.list("followers");

if (allFollowers.length === 0) {
return { reply: "No followers recorded yet!" };
}

// Sort by timestamp descending, take last 5
const recent = allFollowers
.sort((a, b) => (b.followed_at as number) - (a.followed_at as number))
.slice(0, 5);

const names = recent.map((f) => f.user_name).join(", ");
return { reply: `Recent followers: ${names}` };
});

// Helper: get or create a session ID for this stream
async function getSessionId(ctx: any): Promise<string> {
let sessionId = (await ctx.cache.get("session_id")) as string | null;
if (!sessionId) {
sessionId = `session_${Date.now()}`;
// 12-hour TTL — covers a long stream
await ctx.cache.set("session_id", sessionId, 43200);
}
return sessionId;
}

Key concepts demonstrated

  • Kick-specific eventskick:follower fires when a new viewer follows the Kick channel
  • Single-platform moduleplatforms: ["kick"] restricts this to Kick only
  • Follow streak trackingctx.cache.increment() counts new followers during the stream session
  • Milestone announcements — special messages at configurable streak intervals (every 10, 25, 50, etc.)
  • Session-scoped data — cache keys with 12-hour TTL naturally reset between streams
  • Background persistencectx.defer() stores follower records in the database while keeping the event handler fast
  • Config-driven behavior — announce toggle, milestone interval, and custom emote are all configurable