Module 8

Advanced Bot Features

Master media attachments, external integrations, and advanced utilities for building production-ready bots with rich features.

6 sections
Advanced patterns

What you'll learn

Send Media

Images, videos, and dynamically generated content

External Integrations

GitHub webhooks, cron jobs, and API polling

Smart Accounts

Convert user IDs to wallet addresses

Error Handling

Graceful failure patterns and user feedback

Prerequisites

Complete Modules 1-7 before starting this advanced module. These features require a solid understanding of bot fundamentals and the SDK.

1

Media Attachments

Send images, videos, and dynamically generated media to enhance your bot's responses.

URL Attachments

Send images and videos from public URLs:

// Image URL attachment (simple)
await handler.sendMessage(channelId, "Check this out!", {
  attachments: [{
    type: 'image',
    url: 'https://example.com/image.png',
    alt: 'Description for accessibility'
  }]
})

// Video URL attachment
await handler.sendMessage(channelId, "Video tutorial:", {
  attachments: [{
    type: 'video',
    url: 'https://example.com/video.mp4'
  }]
})

// Multiple attachments
await handler.sendMessage(channelId, "Product showcase:", {
  attachments: [
    {
      type: 'image',
      url: 'https://example.com/product1.jpg',
      alt: 'Product 1'
    },
    {
      type: 'image',
      url: 'https://example.com/product2.jpg',
      alt: 'Product 2'
    }
  ]
})
Attachment Types:
  • image: JPG, PNG, GIF, WebP images
  • video: MP4, WebM videos
  • link: URL preview cards
  • chunked: Binary data uploads
  • miniapp: Interactive web apps
2

Chunked Media (Binary Uploads)

Generate and upload images, PDFs, and other binary content programmatically.

import { makeTownsBot } from '@towns-protocol/bot'
import { createCanvas } from '@napi-rs/canvas'

// Generate dynamic chart
bot.onSlashCommand("chart", async (handler, { channelId, args }) => {
  const value = parseInt(args?.[0] || '50')

  // Create canvas
  const canvas = createCanvas(400, 300)
  const ctx = canvas.getContext('2d')

  // Draw chart
  ctx.fillStyle = '#667eea'
  ctx.fillRect(50, 300 - value * 2, 300, value * 2)
  ctx.fillStyle = '#fff'
  ctx.font = '24px sans-serif'
  ctx.fillText(`Value: ${value}`, 150, 50)

  // Export as PNG Blob
  const blob = await canvas.encode('png')

  await handler.sendMessage(channelId, "Your chart:", {
    attachments: [{
      type: 'chunked',
      data: blob,              // Blob (no mimetype needed)
      filename: 'chart.png',
      width: 400,              // Optional for images
      height: 300              // Optional for images
    }]
  })
})

// Raw binary data (Uint8Array)
bot.onSlashCommand("screenshot", async (handler, { channelId }) => {
  const screenshotBuffer = await captureScreen() // Your screenshot library

  await handler.sendMessage(channelId, "Current screen:", {
    attachments: [{
      type: 'chunked',
      data: screenshotBuffer,    // Uint8Array or Buffer
      filename: 'screenshot.png',
      mimetype: 'image/png'      // Required for Uint8Array
    }]
  })
})
Important
  • Uint8Array requires mimetype parameter
  • Blob objects include mimetype automatically
  • Image dimensions are auto-detected for image/* types
  • Specify width/height manually for videos
3

External Integrations

Connect your bot to webhooks, APIs, and scheduled tasks for powerful automations.

Use bot.getHandler() to send messages outside of event handlers:

// GitHub webhook integration
import express from 'express'
import { makeTownsBot } from '@towns-protocol/bot'

const app = express()
const bot = makeTownsBot({
  privateData: process.env.APP_PRIVATE_DATA!,
  webhookSecret: process.env.JWT_SECRET!,
  mnemonic: process.env.MNEMONIC!
})

// GitHub webhook endpoint
app.post('/github-webhook', express.json(), async (req, res) => {
  const { action, pull_request, repository } = req.body

  if (action === 'opened' && pull_request) {
    // Get handler outside of event context
    const handler = bot.getHandler()

    // Send notification to specific channel
    const channelId = process.env.GITHUB_CHANNEL_ID!

    await handler.sendMessage(channelId,
      `New PR opened: #${pull_request.number} - ${pull_request.title}`,
      {
        attachments: [{
          type: 'link',
          url: pull_request.html_url,
          title: pull_request.title,
          description: pull_request.body?.substring(0, 200)
        }]
      }
    )
  }

  res.status(200).send('OK')
})

// Scheduled tasks with cron
import cron from 'node-cron'

// Daily summary at 9 AM
cron.schedule('0 9 * * *', async () => {
  const handler = bot.getHandler()
  const channelId = process.env.DAILY_CHANNEL_ID!

  // Fetch data from your database
  const stats = await fetchDailyStats()

  await handler.sendMessage(channelId,
    `Daily Summary: ${stats.users} users, ${stats.messages} messages`
  )
})

app.listen(3000)
Integration Patterns:
  • GitHub/GitLab webhooks for PR notifications
  • Cron jobs for scheduled messages
  • API polling for external data
  • Database triggers for alerts
4

Smart Account Utilities

Convert user IDs to wallet addresses for on-chain operations and token airdrops.

import { makeTownsBot } from '@towns-protocol/bot'
import { getSmartAccountFromUserId } from '@towns-protocol/bot/getSmartAccountFromUserId'

bot.onSlashCommand("wallet", async (handler, { channelId, mentions, userId }) => {
  // Get wallet for mentioned user or sender
  const targetUserId = mentions[0]?.userId || userId

  try {
    // Convert Towns userId to smart account address
    const walletAddress = await getSmartAccountFromUserId(targetUserId)

    await handler.sendMessage(channelId,
      `Wallet address: ${walletAddress}`
    )

    // Use address to check balances, NFTs, etc
    const balance = await bot.viem.getBalance({ address: walletAddress })

    await handler.sendMessage(channelId,
      `Balance: ${formatEther(balance)} ETH`
    )
  } catch (error) {
    await handler.sendMessage(channelId,
      "Could not retrieve wallet address"
    )
  }
})

// Airdrop tokens to multiple users
bot.onSlashCommand("airdrop", async (handler, { channelId, mentions, args }) => {
  if (mentions.length === 0) {
    await handler.sendMessage(channelId, "Please mention users to airdrop")
    return
  }

  const amount = parseEther(args?.[0] || '0.01')

  // Get all wallet addresses
  const addresses = await Promise.all(
    mentions.map(m => getSmartAccountFromUserId(m.userId))
  )

  // Build batch transfer
  const calls = addresses.map(address => ({
    to: tokenContract,
    abi: erc20Abi,
    functionName: 'transfer',
    args: [address, amount]
  }))

  // Execute in single transaction
  const hash = await execute(bot.viem, {
    address: bot.appAddress,
    account: bot.viem.account,
    calls
  })

  await handler.sendMessage(channelId,
    `Airdropped to ${addresses.length} users! Tx: ${hash}`
  )
})
Use Cases:
  • Check user balances and NFT holdings
  • Airdrop tokens to multiple users
  • Verify on-chain permissions
  • Build user portfolio trackers
5

Error Handling Best Practices

Handle failures gracefully with user-friendly messages and proper logging.

// Robust error handling pattern
bot.onSlashCommand("transfer", async (handler, event) => {
  try {
    // Validate inputs
    if (!event.args?.[0] || !event.mentions?.[0]) {
      await handler.sendMessage(event.channelId,
        "Usage: /transfer @user <amount>"
      )
      return
    }

    const amount = parseEther(event.args[0])
    const recipient = await getSmartAccountFromUserId(
      event.mentions[0].userId
    )

    // Check balance before transfer
    const balance = await bot.viem.getBalance({
      address: bot.appAddress
    })

    if (balance < amount) {
      await handler.sendMessage(event.channelId,
        "Insufficient balance for transfer"
      )
      return
    }

    // Execute transfer
    const hash = await execute(bot.viem, {
      address: bot.appAddress,
      account: bot.viem.account,
      calls: [{
        to: recipient,
        value: amount
      }]
    })

    // Wait for confirmation
    const receipt = await waitForTransactionReceipt(bot.viem, { hash })

    if (receipt.status === 'success') {
      await handler.sendMessage(event.channelId,
        `Transfer successful! Tx: ${hash}`
      )
    } else {
      await handler.sendMessage(event.channelId,
        "Transfer failed - transaction reverted"
      )
    }

  } catch (error) {
    console.error('Transfer error:', error)

    // User-friendly error messages
    if (error.message.includes('insufficient funds')) {
      await handler.sendMessage(event.channelId,
        "Bot wallet has insufficient funds"
      )
    } else if (error.message.includes('user rejected')) {
      await handler.sendMessage(event.channelId,
        "Transaction was cancelled"
      )
    } else {
      await handler.sendMessage(event.channelId,
        "Transfer failed. Please try again later."
      )
    }
  }
})

Common Error Scenarios:

  • • Network errors: Retry with exponential backoff
  • • Insufficient funds: Check balance before transactions
  • • Invalid inputs: Validate and provide usage examples
  • • Rate limits: Implement request queuing
  • • Permission denied: Check permissions before actions
✨

Course Complete!

Congratulations! You've completed the Towns Protocol Bot Development course.

🎉 Achievement Unlocked

You've completed the Towns Protocol Bot Development course. You've learned everything from bot fundamentals to production deployment. Your bot is now live and responding in Towns!

Skills Mastered:

  • • Bot SDK fundamentals and setup
  • • Slash commands system
  • • Event handling and mentions
  • • Admin permissions
  • • Database integration
  • • Media attachments
  • • External integrations
  • • Production deployment

You Can Now Build:

  • • Community engagement bots
  • • Moderation systems
  • • Analytics and leaderboards
  • • Token gating and airdrops
  • • Interactive mini-apps
  • • Custom trading bots
  • • DAO governance tools
  • • Production-ready applications

What You've Built:

A production-ready Towns Protocol bot with event handling, slash commands, admin systems, database integration, media support, and live deployment. Your bot is running 24/7 on Render and responding to users in Towns!

Next Steps:

  • • Join the Towns developer community for support
  • • Explore advanced patterns in the documentation
  • • Build and deploy more sophisticated bots
  • • Share your creations with the community