Advanced Bot Features
Master media attachments, external integrations, and advanced utilities for building production-ready bots with rich features.
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.
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'
}
]
})image: JPG, PNG, GIF, WebP imagesvideo: MP4, WebM videoslink: URL preview cardschunked: Binary data uploadsminiapp: Interactive web apps
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
}]
})
})- Uint8Array requires
mimetypeparameter - Blob objects include mimetype automatically
- Image dimensions are auto-detected for image/* types
- Specify width/height manually for videos
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)- GitHub/GitLab webhooks for PR notifications
- Cron jobs for scheduled messages
- API polling for external data
- Database triggers for alerts
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}`
)
})- Check user balances and NFT holdings
- Airdrop tokens to multiple users
- Verify on-chain permissions
- Build user portfolio trackers
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