skills$openclaw/bots
andreyz5.6k

by andreyz

bots – OpenClaw Skill

bots is an OpenClaw Skills integration for coding workflows. >-

5.6k stars5.1k forksSecurity L1
Updated Feb 7, 2026Created Feb 7, 2026coding

Skill Snapshot

namebots
description>- OpenClaw Skills integration.
ownerandreyz
repositoryandreyz/towns-protocol
languageMarkdown
licenseMIT
topics
securityL1
installopenclaw add @andreyz/towns-protocol
last updatedFeb 7, 2026

Maintainer

andreyz

andreyz

Maintains bots in the OpenClaw Skills directory.

View GitHub profile
File Explorer
8 files
.
references
BLOCKCHAIN.md
2.7 KB
DEBUGGING.md
3.4 KB
DEPLOYMENT.md
2.3 KB
INTERACTIVE.md
2.6 KB
MESSAGING.md
1.7 KB
_meta.json
288 B
SKILL.md
7.5 KB
SKILL.md

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:

  1. User IDs are Ethereum addresses - Always 0x... format, never usernames
  2. Mentions require BOTH - <@{userId}> format in text AND mentions array in options
  3. Two-wallet architecture:
    • bot.viem.account.address = Gas wallet (signs & pays fees) - MUST fund with Base ETH
    • bot.appAddress = Treasury (optional, for transfers)
  4. Slash commands DON'T trigger onMessage - They're exclusive handlers
  5. Interactive forms use type property - Not case (e.g., type: 'form')
  6. 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

MethodSignatureNotes
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

PropertyDescription
bot.viemViem client for blockchain
bot.viem.account.addressGas wallet - MUST fund with Base ETH
bot.appAddressTreasury wallet (optional)
bot.botIdBot identifier

For detailed guides, see references/:


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

MistakeFix
insufficient funds for gasFund bot.viem.account.address with Base ETH
Mention not highlightingInclude BOTH <@userId> in text AND mentions array
Slash command not workingAdd to commands array in makeTownsBot
Handler not triggeringCheck message forwarding mode in Developer Portal
writeContract failingUse execute() for external contracts
Granting access on txHashVerify receipt.status === 'success' first
Message lines overlappingUse \n\n (double newlines), not \n
Missing event contextValidate spaceId/channelId before using

Resources

README.md

No README available.

Permissions & Security

Security level L1: Low-risk skills with minimal permissions. Review inputs and outputs before running in production.

Requirements

  • OpenClaw CLI installed and configured.
  • Language: Markdown
  • License: MIT
  • Topics:

Configuration

```typescript 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) } ``` ---

FAQ

How do I install bots?

Run openclaw add @andreyz/towns-protocol in your terminal. This installs bots into your OpenClaw Skills catalog.

Does this skill run locally or in the cloud?

OpenClaw Skills execute locally by default. Review the SKILL.md and permissions before running any skill.

Where can I verify the source code?

The source repository is available at https://github.com/openclaw/skills/tree/main/skills/andreyz/towns-protocol. Review commits and README documentation before installing.