Skills bots
install
source · Clone the upstream repo
git clone https://github.com/openclaw/skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/openclaw/skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/andreyz/towns-protocol" ~/.claude/skills/openclaw-skills-bots && rm -rf "$T"
OpenClaw · Install into ~/.openclaw/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/openclaw/skills "$T" && mkdir -p ~/.openclaw/skills && cp -r "$T/skills/andreyz/towns-protocol" ~/.openclaw/skills/openclaw-skills-bots && rm -rf "$T"
manifest:
skills/andreyz/towns-protocol/SKILL.mdsource content
Towns Protocol Bot SDK Reference
Critical Rules
MUST follow these rules - violations cause silent failures:
- User IDs are Ethereum addresses - Always
format, never usernames0x... - Mentions require BOTH -
format in text AND<@{userId}>
array in optionsmentions - Two-wallet architecture:
= Gas wallet (signs & pays fees) - MUST fund with Base ETHbot.viem.account.address
= Treasury (optional, for transfers)bot.appAddress
- Slash commands DON'T trigger onMessage - They're exclusive handlers
- Interactive forms use
property - Nottype
(e.g.,case
)type: 'form' - Never trust txHash alone - Verify
before granting accessreceipt.status === 'success'
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 |
|---|---|---|
| | opts: |
| | Bot's own messages only |
| | Bot's own messages only |
| | |
| | Forms, transactions, signatures |
| | |
/ | | Needs ModifyBanning permission |
Bot Properties
| Property | Description |
|---|---|
| Viem client for blockchain |
| Gas wallet - MUST fund with Base ETH |
| Treasury wallet (optional) |
| Bot 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
Bot Setup
Project Initialization
bunx towns-bot init my-bot cd my-bot bun install
Environment Variables
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
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) => { // event: { userId, spaceId, channelId, eventId, message, isMentioned, threadId?, replyId? } 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 }) => { // /weather San Francisco → args: ['San', 'Francisco'] const location = args.join(' ') if (!location) { await handler.sendMessage(channelId, 'Usage: /weather <location>') return } // ... fetch weather })
onReaction
bot.onReaction(async (handler, event) => { // event: { reaction, messageId, channelId } 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) => { // event: { senderAddress, receiverAddress, amount (bigint), currency } 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) { // 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 } })
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 } // Safe to proceed })
Common Mistakes
| Mistake | Fix |
|---|---|
| Fund with Base ETH |
| Mention not highlighting | Include BOTH in text AND array |
| Slash command not working | Add to array in makeTownsBot |
| Handler not triggering | Check message forwarding mode in Developer Portal |
failing | Use for external contracts |
| Granting access on txHash | Verify first |
| Message lines overlapping | Use (double newlines), not |
| Missing event context | Validate / before using |
Resources
- 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)