Skip to main content
Interactive messages let your bot create buttons, request transactions, and get signatures from users.

Forms with Buttons

Create interactive UI elements like buttons for games, polls, and confirmations.
import { hexToBytes } from 'viem'

bot.onSlashCommand('play', async (handler, event) => {
  await handler.sendInteractionRequest(
    event.channelId,
    {
      case: 'form',
      value: {
        id: 'game-menu',
        title: '🎮 Game Menu',
        subtitle: 'Choose your action:',
        components: [
          {
            id: 'start-button',
            component: {
              case: 'button',
              value: { label: '▶️ Start Game' }
            }
          },
          {
            id: 'help-button',
            component: {
              case: 'button',
              value: { label: '❓ Help' }
            }
          }
        ]
      }
    },
    hexToBytes(event.userId as `0x${string}`)  // recipient
  )
})

Handling Button Clicks

bot.onInteractionResponse(async (handler, event) => {
  if (event.response.payload.content?.case !== 'form') return
  
  const form = event.response.payload.content?.value
  
  for (const component of form.components) {
    if (component.component.case === 'button') {
      if (component.id === 'start-button') {
        await handler.sendMessage(event.channelId, '🎮 Starting game...')
      } else if (component.id === 'help-button') {
        await handler.sendMessage(event.channelId, '❓ Help...')
      }
    }
  }
})

Transaction Requests

Prompt users to sign and execute blockchain transactions. Perfect for payments, NFT minting, token swaps, and contract interactions. Any Wallet:
bot.onSlashCommand('send-usdc', async (handler, event) => {
  const usdcAddress = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' // Base
  const recipient = '0x1234567890123456789012345678901234567890'
  const amount = '50000000' // 50 USDC (6 decimals)
  
  // Encode ERC20 transfer: transfer(address,uint256)
  const recipientPadded = recipient.slice(2).padStart(64, '0')
  const amountPadded = parseInt(amount).toString(16).padStart(64, '0')
  const data = `0xa9059cbb${recipientPadded}${amountPadded}`
  
  await handler.sendInteractionRequest(event.channelId, {
    case: 'transaction',
    value: {
      id: 'usdc-transfer',
      title: 'Send USDC',
      subtitle: 'Send 50 USDC to recipient',
      content: {
        case: 'evm',
        value: {
          chainId: '8453',
          to: usdcAddress,
          value: '0',
          data: data,
          signerWallet: undefined // User chooses wallet
        }
      }
    }
  })
})
Restrict to Smart Account:
import { getSmartAccountFromUserId } from '@towns-protocol/bot'

bot.onSlashCommand('send-usdc-sm', async (handler, event) => {
  const smartAccount = await getSmartAccountFromUserId(bot, {
    userId: event.userId
  })
  
  if (!smartAccount) {
    await handler.sendMessage(event.channelId, "No smart account found")
    return
  }
  
  // ... same transaction setup ...
  
  await handler.sendInteractionRequest(event.channelId, {
    case: 'transaction',
    value: {
      // ...
      content: {
        case: 'evm',
        value: {
          // ...
          signerWallet: smartAccount // Only this wallet can sign
        }
      }
    }
  })
})

Handle Transaction Response

bot.onInteractionResponse(async (handler, event) => {
  if (event.response.payload.content?.case === 'transaction') {
    const txData = event.response.payload.content.value
    
    await handler.sendMessage(
      event.channelId,
      `✅ Transaction Confirmed!

Request ID: ${txData.requestId}
Transaction Hash: \`${txData.txHash}\`

View on explorer: https://basescan.org/tx/${txData.txHash}`
    )
  }
})

Signature Requests

Request cryptographic signatures without executing transactions. Perfect for authentication, permissions, off-chain agreements, and gasless interactions.
import { InteractionRequestPayload_Signature_SignatureType } from '@towns-protocol/proto'

bot.onSlashCommand('sign', async (handler, event) => {
  // EIP-712 Typed Data Structure
  const typedData = {
    domain: {
      name: 'My Towns Bot',
      version: '1',
      chainId: 8453,
      verifyingContract: '0x0000000000000000000000000000000000000000'
    },
    types: {
      Message: [
        { name: 'from', type: 'address' },
        { name: 'content', type: 'string' },
        { name: 'timestamp', type: 'uint256' }
      ]
    },
    primaryType: 'Message',
    message: {
      from: event.userId,
      content: 'I agree to the terms',
      timestamp: Math.floor(Date.now() / 1000)
    }
  }
  
  await handler.sendInteractionRequest(event.channelId, {
    case: 'signature',
    value: {
      id: 'message-signature',
      title: 'Sign Message',
      subtitle: `Sign: "${typedData.message.content}"`,
      chainId: '8453',
      data: JSON.stringify(typedData),
      type: InteractionRequestPayload_Signature_SignatureType.TYPED_DATA,
      signerWallet: undefined // User chooses wallet
    }
  })
})

Handle Signature Response

bot.onInteractionResponse(async (handler, event) => {
  if (event.response.payload.content?.case === 'signature') {
    const signatureData = event.response.payload.content.value
    
    await handler.sendMessage(
      event.channelId,
      `✅ Signature Received!

Request ID: ${signatureData.requestId}

Signature:
\`\`\`
${signatureData.signature}
\`\`\`

You can now verify this signature on-chain or use it for authentication.`
    )
  }
})

Complete Response Handler

bot.onInteractionResponse(async (handler, event) => {
  const { response } = event
  
  switch (response.payload.content?.case) {
    case 'form':
      const formData = response.payload.content.value
      // Handle button clicks
      for (const component of formData.components) {
        if (component.component.case === 'button') {
          // Route based on component.id
        }
      }
      break
      
    case 'transaction':
      const txData = response.payload.content.value
      await handler.sendMessage(
        event.channelId,
        `Transaction confirmed: ${txData.txHash}`
      )
      break
      
    case 'signature':
      const signatureData = response.payload.content.value
      await handler.sendMessage(
        event.channelId,
        `Signature received: ${signatureData.signature}`
      )
      break
  }
})