Requirements
- Target platform
- OpenClaw
- Install method
- Manual import
- Extraction
- Extract archive
- Prerequisites
- OpenClaw
- Primary doc
- SKILL.md
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"
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"
Hand the extracted package to your coding agent with a concrete install brief instead of figuring it out manually.
I downloaded a skill package from Yavira. Read SKILL.md from the extracted folder and install it by following the included instructions. Tell me what you changed and call out any manual steps you could not complete.
I downloaded an updated skill package from Yavira. Read SKILL.md from the extracted folder, compare it with my current installation, and upgrade it while preserving any custom configuration unless the package docs explicitly say otherwise. Summarize what changed and any follow-up checks I should run.
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
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'
MethodSignatureNotessendMessage(channelId, text, opts?) → { eventId }opts: { threadId?, replyId?, mentions?, attachments? }editMessage(channelId, eventId, text)Bot's own messages onlyremoveEvent(channelId, eventId)Bot's own messages onlysendReaction(channelId, messageId, emoji)sendInteractionRequest(channelId, payload)Forms, transactions, signatureshasAdminPermission(userId, spaceId) → booleanban / unban(userId, spaceId)Needs ModifyBanning permission
PropertyDescriptionbot.viemViem client for blockchainbot.viem.account.addressGas wallet - MUST fund with Base ETHbot.appAddressTreasury wallet (optional)bot.botIdBot identifier For detailed guides, see references/: Messaging API - Mentions, threads, attachments, formatting Blockchain Operations - Read/write contracts, verify transactions Interactive Components - Forms, transaction requests Deployment - Local dev, Render, tunnels Debugging - Troubleshooting guide
bunx towns-bot init my-bot cd my-bot bun install
APP_PRIVATE_DATA=<base64_credentials> # From app.towns.com/developer JWT_SECRET=<webhook_secret> # Min 32 chars PORT=3000 BASE_RPC_URL=https://base-mainnet.g.alchemy.com/v2/KEY # Recommended
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()
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) }
Triggers on regular messages (NOT slash commands). bot.onMessage(async (handler, event) => { // event: { userId, spaceId, channelId, eventId, message, isMentioned, threadId?, replyId? } if (event.isMentioned) { await handler.sendMessage(event.channelId, 'You mentioned me!') } })
Triggers on /command. Does NOT trigger onMessage. bot.onSlashCommand('weather', async (handler, { args, channelId }) => { // /weather San Francisco → args: ['San', 'Francisco'] const location = args.join(' ') if (!location) { await handler.sendMessage(channelId, 'Usage: /weather <location>') return } // ... fetch weather })
bot.onReaction(async (handler, event) => { // event: { reaction, messageId, channelId } if (event.reaction === '👋') { await handler.sendMessage(event.channelId, 'I saw your wave!') } })
Requires "All Messages" mode in Developer Portal. bot.onTip(async (handler, event) => { // event: { senderAddress, receiverAddress, amount (bigint), currency } if (event.receiverAddress === bot.appAddress) { await handler.sendMessage(event.channelId, 'Thanks for ' + formatEther(event.amount) + ' ETH!') } })
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) { // IMPORTANT: Verify on-chain before granting access // See references/BLOCKCHAIN.md for full verification pattern await handler.sendMessage(event.channelId, 'TX: https://basescan.org/tx/' + tx.txHash) } break } })
Always validate context before using: bot.onSlashCommand('cmd', async (handler, event) => { if (!event.spaceId || !event.channelId) { console.error('Missing context:', { userId: event.userId }) return } // Safe to proceed })
MistakeFixinsufficient funds for gasFund bot.viem.account.address with Base ETHMention not highlightingInclude BOTH <@userId> in text AND mentions arraySlash command not workingAdd to commands array in makeTownsBotHandler not triggeringCheck message forwarding mode in Developer PortalwriteContract failingUse execute() for external contractsGranting access on txHashVerify receipt.status === 'success' firstMessage lines overlappingUse \n\n (double newlines), not \nMissing event contextValidate spaceId/channelId before using
Developer Portal: https://app.towns.com/developer Documentation: https://docs.towns.com/build/bots SDK: https://www.npmjs.com/package/@towns-protocol/bot Chain ID: 8453 (Base Mainnet)
Messaging, meetings, inboxes, CRM, and teammate communication surfaces.
Largest current source with strong distribution and engagement signals.