Skip to main content
bots Use when building Towns Protocol bots - covers SDK initialization, slash commands, message handlers, reactions, interactive forms, blockchain operations, and deployment. Triggers: "towns bot", "makeTownsBot", "onSlashCommand", "onMessage", "sendInteractionRequest", "webhook", "bot deployment", "@towns-protocol/bot"
Jump to install Skills Marketplace Discover and explore AI skills built by the community.
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Copy promptShow prompt details A direct command skips the review prompt. Inspect the source before running it.
npx skills add https://github.com/duclm1x1/Dive-Ai --skill botsThe command stays on one line. Scroll horizontally to inspect it before copying.
Prefer a local copy? Download the files currently available to SkillsMP.
Download Zip Downloading... Related occupations SOC
Based on SOC occupation classification
More from this repository
name bots description Use when building Towns Protocol bots - covers SDK initialization, slash commands, message handlers, reactions, interactive forms, blockchain operations, and deployment. Triggers: "towns bot", "makeTownsBot", "onSlashCommand", "onMessage", "sendInteractionRequest", "webhook", "bot deployment", "@towns-protocol/bot" license MIT compatibility Requires Bun runtime, Base network RPC access, @towns-protocol/bot SDK metadata {"author":"towns-protocol","version":"2.0.0"}
Towns Protocol Bot SDK Reference
Critical Rules
MUST follow these rules - violations cause silent failures:
User IDs are Ethereum addresses - Always 0x... format, never usernames
Mentions require BOTH - <@{userId}> format in text AND mentions array in options
Two-wallet architecture :
bot.viem.account.address = Gas wallet (signs & pays fees) - MUST fund with Base ETH
bot.appAddress = Treasury (optional, for transfers)
Slash commands DON'T trigger onMessage - They're exclusive handlers
Interactive forms use type property - Not case (e.g., type: 'form')
Never trust txHash alone - Verify receipt.status === 'success' before granting access
Quick Reference
Key Imports import { makeTownsBot, getSmartAccountFromUserId } from '@towns-protocol/bot'
import type { BotCommand , BotHandler } from '@towns-protocol/bot'
import { Permission } from '@towns-protocol/web3'
import { parseEther, formatEther, erc20Abi, zeroAddress } from 'viem'
import { readContract, waitForTransactionReceipt } from 'viem/actions'
import { execute } from 'viem/experimental/erc7821'
Handler Methods Method Signature Notes sendMessage(channelId, text, opts?) → { eventId }opts: { threadId?, replyId?, mentions?, attachments? } editMessage(channelId, eventId, text)Bot's own messages only removeEvent(channelId, eventId)Bot's own messages only sendReaction(channelId, messageId, emoji)sendInteractionRequest(channelId, payload)Forms, transactions, signatures hasAdminPermission(userId, spaceId) → booleanban / unban(userId, spaceId)Needs ModifyBanning permission
Bot Properties Property Description bot.viemViem client for blockchain bot.viem.account.addressGas wallet - MUST fund with Base ETH bot.appAddressTreasury wallet (optional) bot.botIdBot identifier
Bot Setup
Project Initialization bunx towns-bot init my-bot
cd my-bot
bun install
Environment Variables APP_PRIVATE_DATA=<base64_credentials>
JWT_SECRET=<webhook_secret>
PORT=3000
BASE_RPC_URL=https://base-mainnet.g.alchemy.com/v2/KEY
Basic Bot Template import { makeTownsBot } from '@towns-protocol/bot'
import type { BotCommand } from '@towns-protocol/bot'
const commands = [
{ name : 'help' , description : 'Show help' },
{ name : 'ping' , description : 'Check if alive' }
] as const satisfies BotCommand []
const bot = await makeTownsBot (
process.env .APP_PRIVATE_DATA !,
process.env .JWT_SECRET !,
{ commands }
)
bot.onSlashCommand ('ping' , async (handler, event) => {
const latency = Date .now () - event.createdAt .getTime ()
await handler.sendMessage (event.channelId , 'Pong! ' + latency + 'ms' )
})
export default bot.start ()
Config Validation import { z } from 'zod'
const EnvSchema = z.object ({
APP_PRIVATE_DATA : z.string ().min (1 ),
JWT_SECRET : z.string ().min (32 ),
DATABASE_URL : z.string ().url ().optional ()
})
const env = EnvSchema .safeParse (process.env )
if (!env.success ) {
console .error ('Invalid config:' , env.error .issues )
process.exit (1 )
}
Event Handlers
onMessage Triggers on regular messages (NOT slash commands).
bot.onMessage (async (handler, event) => {
if (event.isMentioned ) {
await handler.sendMessage (event.channelId , 'You mentioned me!' )
}
})
onSlashCommand Triggers on /command. Does NOT trigger onMessage.
bot.onSlashCommand ('weather' , async (handler, { args, channelId }) => {
const location = args.join (' ' )
if (!location) {
await handler.sendMessage (channelId, 'Usage: /weather <location>' )
return
}
})
onReaction bot.onReaction (async (handler, event) => {
if (event.reaction === '👋' ) {
await handler.sendMessage (event.channelId , 'I saw your wave!' )
}
})
onTip Requires "All Messages" mode in Developer Portal.
bot.onTip (async (handler, event) => {
if (event.receiverAddress === bot.appAddress ) {
await handler.sendMessage (event.channelId ,
'Thanks for ' + formatEther (event.amount ) + ' ETH!' )
}
})
onInteractionResponse bot.onInteractionResponse (async (handler, event) => {
switch (event.response .payload .content ?.case ) {
case 'form' :
const form = event.response .payload .content .value
for (const c of form.components ) {
if (c.component .case === 'button' && c.id === 'yes' ) {
await handler.sendMessage (event.channelId , 'You clicked Yes!' )
}
}
break
case 'transaction' :
const tx = event.response .payload .content .value
if (tx.txHash ) {
await handler.sendMessage (event.channelId ,
'TX: https://basescan.org/tx/' + tx.txHash )
}
break
}
})
Event Context Validation Always validate context before using:
bot.onSlashCommand ('cmd' , async (handler, event) => {
if (!event.spaceId || !event.channelId ) {
console .error ('Missing context:' , { userId : event.userId })
return
}
})
Common Mistakes Mistake Fix insufficient funds for gasFund bot.viem.account.address with Base ETH Mention not highlighting Include BOTH <@userId> in text AND mentions array Slash command not working Add to commands array in makeTownsBot Handler not triggering Check message forwarding mode in Developer Portal writeContract failingUse execute() for external contracts Granting access on txHash Verify receipt.status === 'success' first Message lines overlapping Use \n\n (double newlines), not \n Missing event context Validate spaceId/channelId before using
Resources