Bot platform
Build the bot for your server.
Polls, games, welcomes, announcements and whatever else your crew needs. Write the code yourself or ask an AI to write it for you.
A bot never becomes an administrator and only gets the permissions granted by whoever installs it.
What a bot does on Falafy
Slash commands
Type / and the bot shows what it can do, with options and autocomplete.
Buttons and menus
Readers click and the bot reacts right away, in the same message.
Forms
Opens a small window with fields for the person to fill in.
Just for you
Replies that only the person who used the command can see.
Webhook alerts
Other services post in the channel, with no bot running.
Start in three steps
- 1
Create the app
In Falafy, open Settings > Developer and click Create application. Save the token: it is shown only once.
- 2
Install it in a server
Generate the install link, pick the permissions and choose your test server.
- 3
Pick your path
Write the code from the example below, or paste the prompt into an AI and run the file.
Two paths, the same bot
Manual
// bot.mjs: Node 22 and the ws package, no SDK
// npm i ws
// FALAFY_TOKEN=... SERVER_ID=... node bot.mjs
import WebSocket from 'ws';
const TOKEN = process.env.FALAFY_TOKEN; // Settings > Developer
const SERVER_ID = process.env.SERVER_ID;
const API = 'https://falafy.com.br/api/v1';
const api = (method, path, body) =>
fetch(API + path, {
method,
headers: { authorization: `Bot ${TOKEN}`, 'content-type': 'application/json' },
body: body && JSON.stringify(body),
}).then((r) => (r.status === 204 ? null : r.json()));
// answers an interaction (no Authorization: the interaction token is the credential)
const reply = (i, body) =>
fetch(`${API}/interactions/${i.id}/${i.token}/callback`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(body),
});
// registers the commands only in this server (they work at once)
const me = await api('GET', '/users/@me');
await api('PUT', `/applications/${me.id}/servers/${SERVER_ID}/commands`, [
{ name: 'ping', description: 'Replies pong' },
{ name: 'dado', description: 'Rolls a die',
options: [{ type: 4, name: 'lados', description: 'How many sides', minValue: 2, maxValue: 100 }] },
]);
const roll = (lados) => ({
content: `🎲 ${1 + Math.floor(Math.random() * lados)} (d${lados})`,
components: [{ type: 1, components: [{ type: 2, style: 1, label: 'Roll again', customId: `dado:${lados}` }] }],
});
const { url } = await api('GET', '/gateway/bot');
function connect() {
const ws = new WebSocket(url);
let ping;
ws.on('open', () => {
ws.send(JSON.stringify({ op: 'IDENTIFY', token: `Bot ${TOKEN}`, intents: 1 })); // SERVERS
ping = setInterval(() => ws.send(JSON.stringify({ op: 'PING' })), 30_000);
});
ws.on('close', () => { clearInterval(ping); setTimeout(connect, 5_000); }); // dropped? reconnect
ws.on('message', (raw) => {
const f = JSON.parse(raw);
if (f.op === 'READY') console.log('online as', f.d.user.displayName);
if (f.op !== 'EVENT' || f.event.t !== 'INTERACTION_CREATE') return;
const i = f.event.d;
// flags 64 = ephemeral: only the person who used the command sees it
if (i.type === 2 && i.data.name === 'ping') reply(i, { type: 4, data: { content: 'pong', flags: 64 } });
if (i.type === 2 && i.data.name === 'dado') {
const lados = i.data.options.find((o) => o.name === 'lados')?.value ?? 6;
reply(i, { type: 4, data: roll(lados) });
}
// button click: type 7 edits the same message
if (i.type === 3 && i.data.customId.startsWith('dado:')) reply(i, { type: 7, data: roll(Number(i.data.customId.slice(5))) });
});
}
connect();
How to run it
- Install Node 22 or newer and, in a new folder, run
npm i ws. - Save the code as
bot.mjs. - Copy the server ID: right-click the server icon and choose Copy server ID.
- Run
FALAFY_TOKEN=your_token SERVER_ID=server_id node bot.mjs. On Windows (PowerShell):$env:FALAFY_TOKEN="your_token"; $env:SERVER_ID="id"; node bot.mjs. - In the server, type
/pingor/dado.
The official TypeScript SDK (@falafy/bot) is not published on npm yet. This example does not need it.
With AI
Write a bot for Falafy in a single bot.mjs file (Node 22, ESM, only the ws dependency; use the built-in fetch). The token lives in process.env.FALAFY_TOKEN and never appears in the code or in logs. Falafy is not Discord: use only the API described here and do not invent routes.
Falafy API
- REST: base https://falafy.com.br/api/v1, header Authorization: Bot <token>, JSON body, camelCase fields. IDs are UUID strings. 204 responses have no body.
- GET /users/@me returns the bot user. Its id is also the applicationId (appId).
- GET /gateway/bot returns { url } of the gateway WebSocket.
- Slash commands: PUT /applications/{appId}/servers/{serverId}/commands with a list that replaces all of the bot's commands in that server (they work at once). Command: { name (a-z, 0-9, _ and -, up to 32), description (up to 100), options?: [{ type, name, description, required?, choices?: [{ name, value }], minValue?, maxValue?, minLength?, maxLength?, autocomplete? }] }. Option types: 3 string, 4 integer, 5 boolean, 6 user, 7 channel, 8 role, 10 number. Required options come before optional ones.
- Gateway: connect to url and, on open, send {"op":"IDENTIFY","token":"Bot <token>","intents":N}. You get {"op":"READY","d":{ user, servers, channels, roles, resumeId }}. Send {"op":"PING"} every 30 s (the answer is {"op":"PONG"}). Events arrive as {"op":"EVENT","event":{"t":"NAME","d":{...}},"seq":n}. If the connection drops, wait a few seconds, reconnect and send IDENTIFY again.
- Intents (add the values): SERVERS 1, SERVER_MEMBERS 2 (privileged), SERVER_MESSAGES 512, SERVER_MESSAGE_REACTIONS 1024, DIRECT_MESSAGES 4096, MESSAGE_CONTENT 32768 (privileged). Privileged intents must be enabled first in the portal (Settings > Developer). Without MESSAGE_CONTENT the bot only sees the text of DMs and of messages that mention it. INTERACTION_CREATE always arrives, no intent needed.
- Useful events: MESSAGE_CREATE (d: { id, channelId, author: { id, username, displayName }, content, mentions }), MEMBER_JOIN (d: { serverId, user }), INTERACTION_CREATE.
- INTERACTION_CREATE (d): { id, token, type (2 command, 3 component, 4 autocomplete, 5 modal), applicationId, serverId, channelId, user: { id, username, displayName }, member: { roleIds, permissions } or null, data, message? }. Command data: { name, options: [{ name, type, value }] }. Component data: { customId, values? }. Modal data: { customId, fields: [{ customId, value }] }.
- Replying: POST /interactions/{id}/{token}/callback, without Authorization, only once and within 3 seconds. Body { type, data }: 4 message; 5 "thinking…" (then edit with PATCH /webhooks/{appId}/{token}/messages/@original); 6 acknowledge the click without changing anything; 7 edit the component's message; 8 autocomplete choices ({ choices: [{ name, value }] }); 9 open a modal ({ customId, title, components: [{ type: 1, components: [{ type: 4, customId, label, style: 1 short or 2 paragraph }] }] }). data.flags = 64 makes the reply ephemeral (only the user who used it sees it). Follow-ups: POST /webhooks/{appId}/{token} for up to 15 minutes.
- Message: content (up to 2000), embeds (up to 10), components (up to 5 rows). Embed: { title, description, url, color (integer), fields: [{ name, value, inline }], footer: { text }, image: { url }, thumbnail: { url } }, https URLs only. Row { type: 1, components } with up to 5 buttons or 1 select menu. Button { type: 2, style, label }: style 1 primary, 2 secondary, 3 success, 4 danger (with customId) or 5 link (with url and no customId; it does not create an interaction). Select menu { type: 3, customId, placeholder, options: [{ label, value, description? }], minValues?, maxValues? }.
- Messages: POST /channels/{channelId}/messages { content, embeds, components, replyToId }; PATCH and DELETE /channels/{channelId}/messages/{messageId}; reaction: PUT /channels/{channelId}/messages/{messageId}/reactions/{emoji}/@me.
- Roles: GET /servers/{serverId}/roles; PUT and DELETE /servers/{serverId}/members/{userId}/roles/{roleId}. The bot needs the Manage roles permission and a role above the role it gives.
- DM: POST /users/@me/channels { recipientId } returns the channel; then POST /channels/{id}/messages. Only works with people who share a server with the bot.
- Limits: on 429, wait body.retryAfter seconds and try again. Errors come as { code, message, status }.
- Does not exist (do not try): joining a voice channel, kicking, banning or muting members, creating channels or threads, sending files or attachments.
Requirements: register the commands in SERVER_ID on startup; handle errors without crashing the process; at the top of the file, explain how to run it (npm i ws and FALAFY_TOKEN=... node bot.mjs) and which permissions and intents to enable in the portal.
The bot I want: /poll with a question and options, one button per option and the count updating live.
SERVER_ID: [paste the server ID here]
How to use it
- Copy the prompt and replace the text after "The bot I want" with your request.
- Paste the server ID at the end and send it to the AI.
- Save the answer as
bot.mjsand run it as in the Manual path. - Got an error? Paste the error message in the same chat and ask for a fix.
Never paste your token into the AI chat. It stays on your computer only, in the FALAFY_TOKEN variable.
New
A store in your server
Sell VIP roles, keys, tickets or whatever your community wants. The bot shows the catalog, creates the payment in your account and delivers on its own once the payment clears.
1Catalog
/store posts the products with a menu or one button for each.
2Payment
The bot creates the charge in your account (Mercado Pago with Pix, or Stripe) and sends the link only to whoever clicked.
3Confirmation
The provider notifies your bot's server when the payment is approved.
4Delivery
The bot gives the role, sends the item by DM and logs the sale in the staff channel.
The heart of the store
// loja.mjs (excerpt): combine with the bot.mjs from the Manual path
// variables: MP_ACCESS_TOKEN, MP_WEBHOOK_SECRET, ROLE_VIP, STAFF_CHANNEL
import http from 'node:http';
import crypto from 'node:crypto';
const MP = 'https://api.mercadopago.com';
const mpAuth = { authorization: `Bearer ${process.env.MP_ACCESS_TOKEN}`, 'content-type': 'application/json' };
const PRODUCTS = { vip30: { title: 'VIP 30 days', price: 15, roleId: process.env.ROLE_VIP } };
const delivered = new Set(); // in production, keep this in a file or database: deliver once per payment
// 1) click on "Buy": creates the charge in the seller's account
async function onBuy(i, productId) {
await reply(i, { type: 5, data: { flags: 64 } }); // "thinking…" only for whoever clicked (creating the charge can take more than 3 s)
const p = PRODUCTS[productId];
const pref = await fetch(`${MP}/checkout/preferences`, {
method: 'POST',
headers: mpAuth,
body: JSON.stringify({
items: [{ title: p.title, quantity: 1, unit_price: p.price, currency_id: 'BRL' }],
external_reference: `${i.user.id}:${productId}`,
}),
}).then((r) => r.json());
await api('PATCH', `/webhooks/${me.id}/${i.token}/messages/@original`, {
content: `${p.title}: R$ ${p.price.toFixed(2)}`,
components: [{ type: 1, components: [{ type: 2, style: 5, label: 'Pay', url: pref.init_point }] }],
});
}
// 2) Mercado Pago webhook: checks the signature and fetches the payment from its API
http.createServer(async (req, res) => {
const id = new URL(req.url, 'http://bot').searchParams.get('data.id');
if (req.method !== 'POST' || !id || !validSignature(req, id)) return res.writeHead(401).end();
res.writeHead(200).end();
const pay = await fetch(`${MP}/v1/payments/${id}`, { headers: mpAuth }).then((r) => r.json());
const [userId, productId] = String(pay.external_reference).split(':');
const p = PRODUCTS[productId];
if (pay.status !== 'approved' || !p || pay.transaction_amount !== p.price) return;
if (delivered.has(pay.id)) return;
delivered.add(pay.id);
await deliver(userId, p, pay.id);
}).listen(8080);
function validSignature(req, dataId) {
const sig = Object.fromEntries(String(req.headers['x-signature'] ?? '').split(',').map((kv) => kv.trim().split('=')));
const manifest = `id:${dataId};request-id:${req.headers['x-request-id']};ts:${sig.ts};`;
const want = crypto.createHmac('sha256', process.env.MP_WEBHOOK_SECRET).update(manifest).digest('hex');
return typeof sig.v1 === 'string' && sig.v1.length === want.length && crypto.timingSafeEqual(Buffer.from(sig.v1), Buffer.from(want));
}
// 3) delivery: role, DM with the item and a log for the staff
async function deliver(userId, p, paymentId) {
await api('PUT', `/servers/${SERVER_ID}/members/${userId}/roles/${p.roleId}`);
const dm = await api('POST', '/users/@me/channels', { recipientId: userId });
await api('POST', `/channels/${dm.id}/messages`, { content: `Payment confirmed! You got: ${p.title}.` });
await api('POST', `/channels/${process.env.STAFF_CHANNEL}/messages`, { content: `Sale ${paymentId}: ${p.title} for ${userId}` });
}
Uses the api() and reply() functions and the me and SERVER_ID constants from the Manual example. Set your bot's public URL (for example https://your.domain/) as the Payments webhook in the Mercado Pago dashboard.
Before you sell
- The money goes straight to your Mercado Pago or Stripe account. Falafy does not process, receive or hold payments.
- You are responsible for what you sell and for delivering it, and you must follow Falafy's Terms of use and your payment provider's rules.
- Never ask for or store card data: people pay on the provider's page.
- Keys and tokens only in environment variables, never in the code, in screenshots or in the AI chat.
- Validate the signature of every webhook and confirm the payment with the provider's API before delivering.
- Deliver only once per payment id: the provider may send the same notice more than once.
- The bot's role must be above the roles it sells, and the bot needs the Manage roles permission.
- Want a notice for every sale without touching the bot? Create an incoming webhook in the staff channel and post to it.
Store prompt
Write a bot for Falafy in a single bot.mjs file (Node 22, ESM, only the ws dependency; use the built-in fetch). The token lives in process.env.FALAFY_TOKEN and never appears in the code or in logs. Falafy is not Discord: use only the API described here and do not invent routes.
Falafy API
- REST: base https://falafy.com.br/api/v1, header Authorization: Bot <token>, JSON body, camelCase fields. IDs are UUID strings. 204 responses have no body.
- GET /users/@me returns the bot user. Its id is also the applicationId (appId).
- GET /gateway/bot returns { url } of the gateway WebSocket.
- Slash commands: PUT /applications/{appId}/servers/{serverId}/commands with a list that replaces all of the bot's commands in that server (they work at once). Command: { name (a-z, 0-9, _ and -, up to 32), description (up to 100), options?: [{ type, name, description, required?, choices?: [{ name, value }], minValue?, maxValue?, minLength?, maxLength?, autocomplete? }] }. Option types: 3 string, 4 integer, 5 boolean, 6 user, 7 channel, 8 role, 10 number. Required options come before optional ones.
- Gateway: connect to url and, on open, send {"op":"IDENTIFY","token":"Bot <token>","intents":N}. You get {"op":"READY","d":{ user, servers, channels, roles, resumeId }}. Send {"op":"PING"} every 30 s (the answer is {"op":"PONG"}). Events arrive as {"op":"EVENT","event":{"t":"NAME","d":{...}},"seq":n}. If the connection drops, wait a few seconds, reconnect and send IDENTIFY again.
- Intents (add the values): SERVERS 1, SERVER_MEMBERS 2 (privileged), SERVER_MESSAGES 512, SERVER_MESSAGE_REACTIONS 1024, DIRECT_MESSAGES 4096, MESSAGE_CONTENT 32768 (privileged). Privileged intents must be enabled first in the portal (Settings > Developer). Without MESSAGE_CONTENT the bot only sees the text of DMs and of messages that mention it. INTERACTION_CREATE always arrives, no intent needed.
- Useful events: MESSAGE_CREATE (d: { id, channelId, author: { id, username, displayName }, content, mentions }), MEMBER_JOIN (d: { serverId, user }), INTERACTION_CREATE.
- INTERACTION_CREATE (d): { id, token, type (2 command, 3 component, 4 autocomplete, 5 modal), applicationId, serverId, channelId, user: { id, username, displayName }, member: { roleIds, permissions } or null, data, message? }. Command data: { name, options: [{ name, type, value }] }. Component data: { customId, values? }. Modal data: { customId, fields: [{ customId, value }] }.
- Replying: POST /interactions/{id}/{token}/callback, without Authorization, only once and within 3 seconds. Body { type, data }: 4 message; 5 "thinking…" (then edit with PATCH /webhooks/{appId}/{token}/messages/@original); 6 acknowledge the click without changing anything; 7 edit the component's message; 8 autocomplete choices ({ choices: [{ name, value }] }); 9 open a modal ({ customId, title, components: [{ type: 1, components: [{ type: 4, customId, label, style: 1 short or 2 paragraph }] }] }). data.flags = 64 makes the reply ephemeral (only the user who used it sees it). Follow-ups: POST /webhooks/{appId}/{token} for up to 15 minutes.
- Message: content (up to 2000), embeds (up to 10), components (up to 5 rows). Embed: { title, description, url, color (integer), fields: [{ name, value, inline }], footer: { text }, image: { url }, thumbnail: { url } }, https URLs only. Row { type: 1, components } with up to 5 buttons or 1 select menu. Button { type: 2, style, label }: style 1 primary, 2 secondary, 3 success, 4 danger (with customId) or 5 link (with url and no customId; it does not create an interaction). Select menu { type: 3, customId, placeholder, options: [{ label, value, description? }], minValues?, maxValues? }.
- Messages: POST /channels/{channelId}/messages { content, embeds, components, replyToId }; PATCH and DELETE /channels/{channelId}/messages/{messageId}; reaction: PUT /channels/{channelId}/messages/{messageId}/reactions/{emoji}/@me.
- Roles: GET /servers/{serverId}/roles; PUT and DELETE /servers/{serverId}/members/{userId}/roles/{roleId}. The bot needs the Manage roles permission and a role above the role it gives.
- DM: POST /users/@me/channels { recipientId } returns the channel; then POST /channels/{id}/messages. Only works with people who share a server with the bot.
- Limits: on 429, wait body.retryAfter seconds and try again. Errors come as { code, message, status }.
- Does not exist (do not try): joining a voice channel, kicking, banning or muting members, creating channels or threads, sending files or attachments.
Requirements: register the commands in SERVER_ID on startup; handle errors without crashing the process; at the top of the file, explain how to run it (npm i ws and FALAFY_TOKEN=... node bot.mjs) and which permissions and intents to enable in the portal.
The bot I want: a store. /store posts an embed with the products (name, price and description, defined in a list at the top of the file) and a select menu. When someone picks a product, reply with type 5 and flags 64, create the payment in MY Mercado Pago account (Checkout Pro, POST https://api.mercadopago.com/checkout/preferences with MP_ACCESS_TOKEN, external_reference = "<userId>:<product>") and edit the reply with a link button (style 5) to pay. Start an HTTP server with node:http on port 8080 for the Mercado Pago payments webhook: validate the x-signature header with MP_WEBHOOK_SECRET, fetch the payment from GET https://api.mercadopago.com/v1/payments/{id}, check status approved and the amount, and deliver only once per payment id (keep delivered ids in a JSON file). Deliver = give the product's role, open a DM with the person and send the item or code, and post a log in the STAFF_CHANNEL channel. Never store card data. Keys only in environment variables. If I prefer Stripe: a Checkout Session with STRIPE_SECRET_KEY and a checkout.session.completed webhook validated with STRIPE_WEBHOOK_SECRET.
SERVER_ID: [paste the server ID here]
Never paste your token into the AI chat. It stays on your computer only, in the FALAFY_TOKEN variable.
What you can build
Ready-made ideas: open one and copy the complete prompt for your AI.
Live pollWorks today One button per option and the count going up live. See prompt
Prompt
/poll question:<text> options:<comma-separated text>. Posts an embed with the question and one button per option (up to 5). Each click counts one vote per person (votes can be changed) and updates the same message with type 7, showing text bars and the number of votes.
Quick gamesWorks today Dice, rock-paper-scissors, a quiz with a menu. See prompt
Prompt
/dice sides:<integer>, /rps with three buttons (rock, paper, scissors) against the bot, and /quiz with a question from a list in the code and a select menu with the answers; on pick, edits the message saying whether it was right.
WelcomeWorks today A welcome message and an automatic role. See prompt
Prompt
when someone joins the server (MEMBER_JOIN event, SERVER_MEMBERS intent enabled in the portal), sends a welcome embed in the WELCOME_CHANNEL channel with the person's name and gives the MEMBER_ROLE role. IDs in environment variables.
Pick your rolesWorks today Buttons for each person to pick their own roles. See prompt
Code
// click on the "cargo:<roleId>" button: gives or removes the role
if (i.type === 3 && i.data.customId.startsWith('cargo:')) {
const roleId = i.data.customId.slice(6);
const has = i.member.roleIds.includes(roleId);
await api(has ? 'DELETE' : 'PUT', `/servers/${i.serverId}/members/${i.user.id}/roles/${roleId}`);
reply(i, { type: 4, data: { content: has ? 'Role removed.' : 'Role added!', flags: 64 } });
}
Prompt
/roles posts a message with one button per role from a list in the code (name and roleId). Clicking gives the role if the person does not have it and removes it if they do, and replies ephemerally saying what changed.
Levels and XPWorks today People level up by chatting; /rank shows it. See prompt
Prompt
counts messages per person (MESSAGE_CREATE event, SERVER_MESSAGES intent; no need to read the text), gives XP with a minimum gap of 60 s per person, keeps everything in a JSON file and has /rank (your level and XP, ephemeral) and /top (the top 10 in an embed).
Automatic alertsWorks today Deploys, store, live: it lands in the channel by webhook. See prompt
Code
curl -X POST "$FALAFY_WEBHOOK_URL" \
-H "content-type: application/json" \
-d '{"username":"CI","embeds":[{"title":"Deploy ok · main","color":13168730}]}'
Prompt
a script that receives alerts from another service (for example my CI) and posts them in the channel through a Falafy incoming webhook, with an embed with title, color and fields. The webhook URL lives in the FALAFY_WEBHOOK_URL variable.
AI assistantWorks today /ask answers with the AI you choose. See prompt
Prompt
/ask text:<text>. Replies with type 5 (thinking…), calls the AI API I use (key in AI_API_KEY) with a limit of 1 question every 20 s per person and edits the original reply with the text (cut at 2000 characters).
Support ticketsComing soon A form that opens a private channel. See prompt
Prompt
/ticket opens a modal with subject and description. Bots cannot create channels yet, so for now the bot posts the request as an embed in the STAFF_CHANNEL channel with a "Take it" button and tells the person by DM when someone on the staff takes it.
StoreWorks today Catalog, payment in your account and automatic delivery. See prompt
Prompt
a store. /store posts an embed with the products (name, price and description, defined in a list at the top of the file) and a select menu. When someone picks a product, reply with type 5 and flags 64, create the payment in MY Mercado Pago account (Checkout Pro, POST https://api.mercadopago.com/checkout/preferences with MP_ACCESS_TOKEN, external_reference = "<userId>:<product>") and edit the reply with a link button (style 5) to pay. Start an HTTP server with node:http on port 8080 for the Mercado Pago payments webhook: validate the x-signature header with MP_WEBHOOK_SECRET, fetch the payment from GET https://api.mercadopago.com/v1/payments/{id}, check status approved and the amount, and deliver only once per payment id (keep delivered ids in a JSON file). Deliver = give the product's role, open a DM with the person and send the item or code, and post a log in the STAFF_CHANNEL channel. Never store card data. Keys only in environment variables. If I prefer Stripe: a Checkout Session with STRIPE_SECRET_KEY and a checkout.session.completed webhook validated with STRIPE_WEBHOOK_SECRET.
Want the details?
Full reference for the API, gateway, interactions, webhooks and limits.