Towns Protocol Skills
v2.0.0Use 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"
Security Scan
OpenClaw
Suspicious
high confidencePurpose & Capability
The SKILL.md and reference files consistently describe Towns Protocol bot capabilities (handlers, blockchain ops, interactions, deployment). The required capabilities (blockchain RPC, Bun, @towns-protocol/bot SDK) align with the described purpose. However the registry metadata declares no required env vars or primary credential while the runtime docs explicitly require APP_PRIVATE_DATA and JWT_SECRET (and recommend BASE_RPC_URL), creating an inconsistency between declared metadata and actual runtime needs.
Instruction Scope
Runtime instructions stay within the scope of building and deploying bots (message handling, interaction requests, verifying tx receipts, webhooks, deployment, debugging). They do instruct reading local files for chunked attachments, logging incoming messages, and exposing a health endpoint that prints the gas wallet address — these are reasonable for bot dev but can expose PII or wallet addresses if left enabled in production. There are no explicit instructions to read unrelated system secrets or exfiltrate data to unknown endpoints.
Install Mechanism
This is an instruction-only skill with no install spec and no code files to execute on install, which reduces installation risk. The SKILL.md assumes external dependencies (Bun runtime, @towns-protocol/bot SDK) but does not provide an automated installer — that is consistent with an instruction-only reference.
Credentials
The documentation requires sensitive environment values (APP_PRIVATE_DATA — base64 app credentials from app.towns.com; JWT_SECRET — min 32 chars; BASE_RPC_URL / RPC key; optional DATABASE_URL). The registry metadata however lists no required env vars or primary credential. Requiring APP_PRIVATE_DATA and JWT_SECRET is proportionate to a bot that authenticates to the Towns developer platform, but the omission from declared metadata and the unknown skill source are red flags. Also the guidance to log message contents and expose gasWallet in health endpoints can leak sensitive data.
Persistence & Privilege
The skill does not request always:true, does not modify other skills, and is user-invocable only. There is no claim of persistent platform-level privileges; autonomous invocation is allowed (platform default) but not combined here with other high-risk indicators.
What to consider before installing
This skill's instructions look like a legitimate Towns Protocol bot SDK reference, but take these precautions before installing or running it:
- Verify provenance: the skill has no homepage and an unknown source. Prefer an official repo or NPM package for @towns-protocol/bot and install only from trusted origins.
- Secrets: the SKILL.md requires APP_PRIVATE_DATA and JWT_SECRET. Treat these as sensitive: store them in a secrets manager, do not paste them into public places, and rotate them if exposed. Confirm APP_PRIVATE_DATA was issued by the official Towns developer portal.
- Minimize RPC key scope: use a dedicated RPC key (BASE_RPC_URL) with minimal privileges and rate limits. For read-only operations use a read-only key when possible; keep the funded gas wallet separate and funded with minimal ETH required for operations.
- Isolate runtime: run the bot in a sandboxed environment (separate VM/container) and avoid exposing debug endpoints in production. The health endpoint prints the gas wallet address — consider removing or restricting access to that endpoint.
- Audit logging and attachments: the docs suggest logging message content and reading local files for attachments; ensure logs do not capture PII and attachments are validated before upload.
- Confirm declared requirements: the registry metadata claims no env vars but the docs do — ask the publisher to correct metadata or provide the official package source. If you cannot verify the publisher, do not provide secrets to the skill.
- Code review: because this is instruction-only here, request or review the actual @towns-protocol/bot SDK source and any bot code you intend to run. Pin package versions and audit dependencies.
If you cannot confirm the skill's origin or reconcile the missing metadata, treat it as untrusted and avoid supplying APP_PRIVATE_DATA and JWT_SECRET to any code derived from this skill.Like a lobster shell, security has layers — review code before you run it.
latest
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 ANDmentionsarray in options - Two-wallet architecture:
bot.viem.account.address= Gas wallet (signs & pays fees) - MUST fund with Base ETHbot.appAddress= Treasury (optional, for transfers)
- Slash commands DON'T trigger onMessage - They're exclusive handlers
- Interactive forms use
typeproperty - Notcase(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) → boolean | |
ban / unban | (userId, spaceId) | Needs ModifyBanning permission |
Bot Properties
| Property | Description |
|---|---|
bot.viem | Viem client for blockchain |
bot.viem.account.address | Gas wallet - MUST fund with Base ETH |
bot.appAddress | Treasury wallet (optional) |
bot.botId | 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 |
|---|---|
insufficient funds for gas | Fund 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 failing | Use 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
- 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)
Comments
Loading comments...
