Skip to main content
Slash commands are special message types that:
  • Start with / (e.g., /help, /poll, /weather)
  • Show up in autocomplete when users type / in Towns
  • Display descriptions to help users understand what they do
  • Are handled separately from regular messages (don’t trigger onMessage)

Example Commands

/help                         → Show bot commands
/ban @john                    → Ban a user
/weather San Francisco        → Get weather for a location
/remind 5m Check the oven     → Set a reminder

Creating commands

Commands are defined in a TypeScript file (typically src/commands.ts):
src/commands.ts
import type { BotCommand } from '@towns-protocol/bot'

export const commands = [
  {
    name: 'help',
    description: 'Show available bot commands'
  },
  {
    name: 'ping',
    description: 'Check if bot is responding'
  },
  {
    name: 'greet',
    description: 'Greet a user'
  },
  {
    name: 'weather',
    description: 'Get current weather for a location'
  }
] as const satisfies BotCommand[]

export default commands

Handling Commands

You can use bot.onSlashCommand() to handle each slash command:
src/index.ts
import { makeTownsBot } from '@towns-protocol/bot'
import commands from './commands'

const bot = await makeTownsBot(appPrivateData, jwtSecret, { commands })

// Handle /help command
bot.onSlashCommand('help', async (handler, event) => {
  const commandList = commands
    .map(cmd => `• \`/${cmd.name}\` - ${cmd.description}`)
    .join('\n')

  await handler.sendMessage(
    event.channelId,
    `**Available Commands**\n${commandList}`
  )
})

// Handle /ping command
bot.onSlashCommand('ping', async (handler, event) => {
  const latency = Date.now() - event.createdAt.getTime()
  await handler.sendMessage(event.channelId, `Pong! 🏓 (${latency}ms)`)
})

Working with Arguments

Slash commands can receive arguments after the command name. You can get them from the args property of the event.
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>\nExample: /weather San Francisco'
    )
    return
  }

  const weather = await callWeatherAPI(location)
  await handler.sendMessage(
    channelId,
    `Weather in ${location}: ${weather.temp}°F, ${weather.conditions}`
  )
})

Working with Mentions

Users can mention other users or the bot in slash commands. You can get the mentions from the mentions property of the event.
// /greet @john @jane
bot.onSlashCommand('greet', async (handler, { channelId, mentions}) => {
  if (mentions.length === 0) {
    await handler.sendMessage(
      channelId,
      'Usage: /greet @user1 @user2 ...'
    )
    return
  }

  const greetings = mentions
    .map(m => `Hello <@${m.userId}>!`)
    .join('\n')

  await handler.sendMessage(channelId, greetings, { mentions })
})

Checking Permissions

You may want to check if the user has the necessary permissions to use a command. You can use the hasAdminPermission method to check if the user is an admin. This is a shortcut for checking if the user has the ModifyBanning permission. If you want to check for a specific permission, you can use the checkPermission method.
bot.onSlashCommand('ban', async (handler, { userId, spaceId, channelId, mentions }) => {
  // Check if user is admin
  const isAdmin = await handler.hasAdminPermission(userId, spaceId)
  if (!isAdmin) {
    await handler.sendMessage( channelId, '❌ Only admins can use this command')
    return
  }
  const userToBan = mentions[0]?.userId
  if (!userToBan) {
    await handler.sendMessage( channelId, 'Usage: /ban @user')
    return
  }
  try {
    await handler.ban(userToBan, spaceId)
    await handler.sendMessage(
      channelId,
      `✓ Banned <@${userToBan}>`
    )
  } catch {
    await handler.sendMessage( channelId, '❌ Failed to ban user')
  }
})
You can create commands that require USDC payments in any network. Payments are processed through the x402 protocol.

Defining Paid Commands

Add a paid property to your command definition with a price in USDC:
src/commands.ts
import type { BotCommand } from '@towns-protocol/bot'

export const commands = [
  {
    name: 'generate',
    description: 'Generate AI content',
    paid: { price: '$0.20' }
  }
] as const satisfies BotCommand[]

export default commands

How it works

When a user invokes a paid command:
  1. Bot sends a signature request for USDC transfer authorization
  2. User signs the request in their wallet
  3. Payment is verified and settled via the x402 facilitator
  4. Your command handler executes after successful payment
src/commands.ts
export const commands = [
  {
    name: 'generate',
    description: 'Generate AI content',
    paid: { price: '$0.20' }
  }
]
// Handler only runs after payment succeeds
bot.onSlashCommand('generate', async (handler, event) => {
  // Payment already processed at this point
  await handler.sendMessage(
    event.channelId,
    'Thank you for your purchase! Here is your generated content...'
  )
})

Updating Commands

Commands are automatically synced with Towns when your bot starts. To update your bot’s slash commands:
  1. Modify your command definitions in src/commands.ts
  2. Restart your bot
The new commands will be automatically registered and appear in Towns’ autocomplete.

Next Steps