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 (ctx, event) => {
  await ctx.sendInteractionRequest(
    ctx.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(ctx.userId as `0x${string}`)  // recipient
  )
})

Handling Button Clicks

bot.onInteractionResponse(async (ctx, event) => {
  if (ctx.response.payload.content?.case !== 'form') return

  const form = ctx.response.payload.content?.value

  for (const component of form.components) {
    if (component.component.case === 'button') {
      if (component.id === 'start-button') {
        await ctx.send('🎮 Starting game...')
      } else if (component.id === 'help-button') {
        await ctx.send('❓ 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 (ctx, 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 ctx.sendInteractionRequest(ctx.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 (ctx, event) => {
  const smartAccount = await getSmartAccountFromUserId(bot, {
    userId: ctx.userId
  })

  if (!smartAccount) {
    await ctx.send("No smart account found")
    return
  }

  // ... same transaction setup ...

  await ctx.sendInteractionRequest(ctx.channelId, {
    case: 'transaction',
    value: {
      // ...
      content: {
        case: 'evm',
        value: {
          // ...
          signerWallet: smartAccount // Only this wallet can sign
        }
      }
    }
  })
})

Handle Transaction Response

bot.onInteractionResponse(async (ctx, event) => {
  if (ctx.response.payload.content?.case === 'transaction') {
    const txData = ctx.response.payload.content.value

    await ctx.send(
      `✅ 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 (ctx, 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: ctx.userId,
      content: 'I agree to the terms',
      timestamp: Math.floor(Date.now() / 1000)
    }
  }

  await ctx.sendInteractionRequest(ctx.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 (ctx, event) => {
  if (ctx.response.payload.content?.case === 'signature') {
    const signatureData = ctx.response.payload.content.value

    await ctx.send(
      `✅ 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 (ctx, event) => {
  const { response } = ctx

  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 ctx.send(`Transaction confirmed: ${txData.txHash}`)
      break

    case 'signature':
      const signatureData = response.payload.content.value
      await ctx.send(`Signature received: ${signatureData.signature}`)
      break
  }
})