CassieahBoT (formerly CassidySpectra, forked from CassidyRedux, originally CassidyBoT) is a multi-platform chatbot framework for Facebook, Discord, and web. Maintained by Liane Cagara (lianecagara on GitHub), it uses TypeScript for robust tooling and now includes napi-rs canvas for image generation because text-only output is outdated. This is the 4.0+ dev branch, so expect rough edges. If you break something, that’s on you.
CassieahBoT is a framework for building bots that work across Facebook (personal accounts and pages), Discord (partially), and web interfaces. It leverages TypeScript for type safety and includes napi-rs canvas for generating images dynamically. It’s extensible but not foolproof. If you don’t follow instructions, don’t expect it to work.
- TypeScript Tooling: Strong typing and autocomplete. Use a proper IDE or deal with the consequences.
- Image Generation: Uses napi-rs canvas to create images like memes or charts. Text output alone won’t cut it anymore.
- Command Management: Define and manage commands with a clear structure.
- Multi-Platform: Supports Facebook (personal and pages), Discord (limited), and web.
- Premade Components: Templates like
UTShopandGameSimulatorfor common tasks. - Auto-Styling: Commands get formatted titles and lines automatically.
- MongoDB Abstraction: Simplified database interaction. Still requires a valid MongoDB URI.
- Idle Games: Built-in support for idle game mechanics.
- Plugins: Parallel plugin execution with promises. Better than the old system.
- User Management: Handles permissions, roles, and inventory systems.
- Custom Fonts: Unicode fonts styled like Markdown.
- API Integration: Connect to external APIs for additional functionality.
- 4.0+ (Dev):
- Added napi-rs canvas for image generation.
- Improved plugin system with proper promise handling.
- Simplified MongoDB abstraction.
- Performance optimizations.
- Discord support remains partial and undertested.
- Fixed some bugs. New ones are probably lurking.
Follow these steps exactly. If the bot doesn’t work, check the logs first before asking for help.
- GitHub account.
- Facebook Page (required for Messenger integration).
- Dummy Facebook account (don’t use your main one).
- Browser with extension support (e.g., Chrome, Edge, Kiwi).
- Cookie Editor or C3C FBState extension for exporting cookies.
- Railway account (railway.app).
- Node.js 23.7.0 or higher. Older versions will fail.
- MongoDB URI (e.g., from MongoDB Atlas).
- Basic understanding of JavaScript/TypeScript and command-line tools.
-
Fork or Generate the Repository:
- Go to https://github.com/lianecagara/CassieahBoT.
- Fork or use the template to create your own repo (
https://github.com/your-username/CassieahBoT). - Template is better for private repos.
-
Clone the Repository:
git clone https://github.com/your-username/CassieahBoT cd CassieahBoT -
Run Update Command:
- Before touching any files, run:
npm run update
- This ensures dependencies and configs are up to date. If you skip this and modify files, updates will break. Don’t say I didn’t warn you.
- Before touching any files, run:
-
Install Dependencies:
npm install
- If this fails, verify Node.js version (23.7.0+).
-
Install napi-rs Canvas:
npm install @napi-rs/canvas
- Linux users may need additional packages (
libcairo2-dev,libpango1.0-dev). Search for solutions if it fails.
- Linux users may need additional packages (
-
Set Up Cookies:
- Log in to your dummy Facebook account.
- Use Cookie Editor or C3C FBState to export cookies in JSON format.
- Replace the contents of
cookie.jsonwith your cookies. - Run
node hidestateif using an.envfile to secure appstate. - Warning: Never commit
cookie.jsonto a public repo. Use environment variables.
-
Facebook Messenger Setup:
- Go to developers.facebook.com.
- Create a Business app.
- Add the Messenger product.
- Link your Facebook Page and generate a Page Access Token.
- Configure webhooks:
- Callback URL:
https://your-railway-url/webhook(set after deployment). - Verify Token: Create one (e.g.,
pagebot) and note it. - Subscribe to
messages,messaging_optins,messaging_postbacks.
- Callback URL:
- Update
settings.json:{ "pageAccessToken": "your-page-access-token", "pageVerifyToken": "your-verify-token", "discordBotToken": "", "discordClientID": "" } - Warning: Keep tokens secure. Use environment variables.
-
Discord (Optional):
- Go to Discord Developer Portal.
- Create a bot and copy its token.
- Add to
settings.json. - Discord support is partial. Test thoroughly or stick to Facebook.
-
Database:
- Add your MongoDB URI to
.env:MONGO_URI="your-mongodb-uri"
- Get a URI from MongoDB Atlas if you don’t have one.
- Add your MongoDB URI to
-
Local Testing:
npm start
- If it fails, check
cookie.json,settings.json, or logs.
- If it fails, check
-
Railway Deployment:
- Log in to railway.app.
- Create a new project and select your CassieahBoT repo.
- Ensure the Dockerfile uses Node.js 23.7.0+.
- Set environment variables:
MONGO_URI: Your MongoDB URI.APPSTATE: Contents ofcookie.json(or use the file).
- Deploy to USA Oregon region. Other regions won’t work.
- Get the Railway URL (e.g.,
https://your-project-name.up.railway.app). - Update Facebook webhook with
/webhook(e.g.,https://your-project-name.up.railway.app/webhook).
-
Testing:
- Message your Facebook Page with “help” from a non-dummy account.
- No response? Check Railway logs. It’s usually a misconfigured token or role.
- Assign roles in developers.facebook.com under App Roles to allow specific accounts to interact.
Commands are defined in JavaScript/TypeScript files. Here’s the format:
export const meta: CommandMeta = {
name: "example",
otherNames: ["ex", "test"],
author: "Your Name",
version: "1.0.0",
description: "Basic command example.",
usage: "{prefix}{name} [arg]",
category: "Misc",
role: 0,
waitingTime: 5,
};
export async function entry({ input, output, args }: CommandContext) {
output.reply(`You said: ${args[0] || "nothing"}`);
}See the LICENSE file for details.
- MrKimstersDev (Symer Steve Berondo): Original tutorial.
- Liane Kaye Cagara: Creator and maintainer.
Important: Always run npm run update before editing any files, or updates will break. If the bot isn’t responding, check the logs (Railway or local). Most issues are due to misconfigured tokens, cookies, or roles. Don’t skip steps, and don’t expect it to work if you half-read this.
(Current as of December 24, 2025 – Modern Best Practices Only)
This is the final, production-hardened reference.
Every pattern below is used daily in the largest CassieahBoT economies.
Prioritized from most critical to advanced.
ctx is passed to every command handler. It contains everything.
Always destructure directly in parameters (mandatory style):
async handler({ input, output, money }) {
// Now use input, output, money directly
}This is the cleanest, fastest, and most consistent way.
Do not do this:
async handler(ctx) {
const { input, output, money } = ctx; // Verbose, unnecessary
}Do not access via ctx.input.body — always destructure in params.
export const meta: CommandMeta = {
name: "vault",
otherNames: ["v", "safe"],
description: "Secure storage with deposit/withdrawal fees",
usage: "{prefix}{name} [deposit|withdraw|list]",
category: "Economy",
version: "4.0.0",
icon: "🔒",
waitingTime: 3, // cooldown in seconds per user
role: 0, // 0=everyone, 1=thread admin+, 1.5=moderator+, 2=bot admin
};waitingTime → automatic per-user cooldown
role → automatic block if insufficient
Read current user role:
input.role // returns 0, 1, 1.5, or 2export const style: CommandStyle = {
title: "🔒 Vault System", // string = auto-styled title
contentFont: "fancy",
lineDeco: "altar", // or "x"
};If style is exported, output.reply(text) and output.send(text) automatically apply it.
await output.reply("Styled automatically")
await output.send("Also styled", "thread123")Only use replyStyled when overriding.
export const entry = defineEntry({
async deposit({ input, output, money }) {
// logic
},
async withdraw({ input, output, money }) {
// logic
},
async list({ input, output, money }) {
// logic
},
});const home = new SpectralCMDHome({}, [
{
key: "deposit",
aliases: ["dep"],
async handler({ input, output, money }) {
// logic
}
},
{
key: "withdraw",
aliases: ["wd"],
async handler({ input, output, money }) {
// logic
}
},
]);
return home.runInContext(ctx);input.body // full message
input.words // ["vault", "deposit", "1000"]
input.args // ["deposit", "1000"]
input.senderID // user ID
input.messageReply?.senderID // replied user
input.mentions // { "@Name": "uid" }
Object.values(input.mentions)[0] // first @ UID
input.hasMentions // true/false
input.role // 0–2
input.isGroup // group chat?input.test(/all/i) // regex
input.isMessage() // text message
input.hasWordOR("all", "max") // any word
input.hasWordAND("send", "money") // all words
input.equal("yes") // exact
input.lower().body // lowercase// Detect target user (used in transfers, trades, duels)
const target = input.messageReply?.senderID || Object.values(input.mentions)[0];await output.reply("Auto-styled")
await output.send("To thread", "thread123")
await output.attach("Caption", "https://img.jpg")
await output.edit("Update", "mid123")
await output.unsend("mid123")
await output.reaction("❤️", "mid123")
await output.wentWrong()
await output.error(new Error("Fail"))const msg = await output.reply("Question?");
msg.editSelf("Updated")
msg.unsendSelf()
msg.atReply(async ({ input, output }) => {
// handle reply
})await output.confirm("Proceed?", async ({ yes }) => {
if (yes) await output.reply("Done");
})const user = await money.getCache(input.senderID)
await money.setItem(input.senderID, {
money: user.money + 5000,
lastUsed: Date.now(),
})// Top 10 richest (used for leaderboards)
const top = (await money.getAllCache())
.sort((a, b) => (b.money ?? 0) - (a.money ?? 0))
.slice(0, 10);const inv = new Inventory(user.items ?? []);
inv.has("sword")
inv.getAmount("potion")
inv.addOne({ key: "gem_123", name: "Ruby", icon: "💎" })
inv.toss("potion", 5)
inv.deleteOne("temp_456")
await money.setItem(input.senderID, {
items: Array.from(inv),
})// Display inventory (used in !inv, !vault list)
inv.toUnique()
.map(i => `${i.icon} **${i.name}** ×${inv.getAmount(i.key)}`)
.join("\n")| Bad Practice | Why It's Bad |
|---|---|
async handler(ctx) { const { ... } = ctx; } |
Verbose — destructure in parameters |
ctx.input.body instead of destructured |
Pollutes code, harder to read |
| Using arrow functions for handlers | Breaks context in rare cases |
Using function() syntax |
Outdated, verbose |
Manual role checks (if (input.role < ...)) |
Framework handles it automatically |
Using replyStyled when style is exported |
Redundant — reply auto-applies style |
Not using Array.from(inventory) on save |
Data loss — required for persistence |
Using deprecated fields (permissions, etc.) |
Ignored by framework |
Not exporting style |
Misses auto-styling benefit |
This is how real vault/bank deposit commands are written in large economies.
{
key: "deposit",
async handler({ input, output, money }) {
const amount = parseBet(input.words[1], Infinity);
if (!amount) return output.reply("Invalid amount — use number or 'all'");
const user = await money.getCache(input.senderID);
if ((user.money ?? 0) < amount) return output.reply("Not enough money in wallet");
const fee = Math.floor(amount * 0.03); // 3% deposit fee
const net = amount - fee;
const msg = await output.reply(
`Deposit **${formatCash(amount)}**?\n` +
`Fee: **${formatCash(fee)}** (3%)\n` +
`Net stored: **${formatCash(net)}**\n\n` +
`Reply **yes** to confirm`
);
msg.atReply(async ({ input: confirmInput, output: confirmOutput }) => {
if (confirmInput.body.toLowerCase() !== "yes") {
return confirmOutput.reply("Deposit cancelled");
}
const updated = await money.getCache(input.senderID);
if ((updated.money ?? 0) < amount) {
return confirmOutput.reply("Balance changed — insufficient funds");
}
const vault = new Inventory(updated.vaultItems ?? []);
vault.addOne({
key: `dep_${Date.now()}`,
name: "Vault Deposit",
icon: "💰",
value: net,
});
await money.setItem(input.senderID, {
money: updated.money - amount,
vaultItems: Array.from(vault),
});
await confirmOutput.reply(`Deposited **${formatCash(net)}** after fee!`);
});
}
}This pattern is used for:
- Bank deposits
- Item storage
- Tax payments
- Trade confirmations
- Any high-value transaction
CassieahBoT — December 24, 2025
Destructure in parameters. Export style. Use output.reply().
Clean. Fast. Scalable.
Now go build the next trillion-dollar virtual economy.
💰🔒
