Interactive Features & Web3
Build rich interactive experiences with buttons, forms, transaction requests, signatures, and miniapps. Learn how to integrate Web3 functionality and create engaging bot experiences.
Learning Objectives
By the end of this module, you will be able to:
Transaction Requests
Request users to sign and execute blockchain transactions for payments, NFTs, and more.
Get User's Smart Account
First, get the user's smart account address to interact with their wallet.
import { getSmartAccountFromUserId } from '@towns-protocol/bot'
bot.onSlashCommand('wallet', async (handler, event) => {
// Get user's smart account (wallet) address
const walletAddress = await getSmartAccountFromUserId(bot, {
userId: event.userId
})
await handler.sendMessage(
event.channelId,
`Your wallet address: ${walletAddress}`
)
})Request Transaction
Prompt users to sign transactions for payments, swaps, or contract interactions.
bot.onSlashCommand('tip', async (handler, event) => {
const amount = event.args[0] || "0.001"
const recipient = event.mentions[0]?.userId || bot.botId
// Request ETH transfer transaction
await handler.sendInteractionRequest(event.channelId, {
case: "transaction",
value: {
id: "tip-transaction",
title: "Send Tip",
subtitle: `Send ${amount} ETH to user`,
content: {
case: "evm",
value: {
chainId: "8453", // Base mainnet
to: recipient,
value: (parseFloat(amount) * 1e18).toString(), // Convert to wei
data: "0x", // No data for simple transfer
signerWallet: undefined, // Any wallet (user chooses)
}
}
}
})
})
// 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!
Hash: \`${txData.txHash}\`
View on Basescan: https://basescan.org/tx/${txData.txHash}`
)
}
})Transaction Use Cases
- • 💸 Request payments from users
- • 🎨 NFT minting flows
- • 🔄 Token swaps and DeFi interactions
- • 🎮 In-game purchases
- • 📝 Contract deployments
Signature Requests
Request cryptographic signatures for authentication, agreements, and gasless operations.
EIP-712 Typed Signatures
Request structured signatures that are human-readable and secure.
import { InteractionRequestPayload_Signature_SignatureType } from "@towns-protocol/proto"
bot.onSlashCommand('verify', async (handler, event) => {
// EIP-712 Typed Data
const typedData = {
domain: {
name: "My Bot",
version: "1",
chainId: 8453, // Base
verifyingContract: "0x0000000000000000000000000000000000000000"
},
types: {
Message: [
{ name: "from", type: "address" },
{ name: "content", type: "string" },
{ name: "timestamp", type: "uint256" }
]
},
primaryType: "Message",
message: {
from: event.userId,
content: "I verify my account ownership",
timestamp: Math.floor(Date.now() / 1000)
}
}
await handler.sendInteractionRequest(event.channelId, {
case: "signature",
value: {
id: "verify-signature",
title: "Verify Account",
subtitle: "Sign to prove you own this account",
chainId: "8453",
data: JSON.stringify(typedData),
type: InteractionRequestPayload_Signature_SignatureType.TYPED_DATA,
signerWallet: undefined // Any wallet
}
})
})
// Handle signature response
bot.onInteractionResponse(async (handler, event) => {
if (event.response.payload.content?.case === "signature") {
const sigData = event.response.payload.content.value
// You can now verify this signature on-chain or off-chain
await handler.sendMessage(
event.channelId,
`✅ Account verified!
Signature: \`${sigData.signature.slice(0, 20)}...\``
)
}
})Signature Use Cases
- • 🔐 Authentication and login
- • ✅ Agreement verification
- • 🎫 Ticket validation
- • 📋 Gasless permissions
- • 🤝 Off-chain commitments
Miniapps
Embed interactive web applications directly in Towns chat.
Sending Miniapp Attachments
Launch web apps that users can interact with inside Towns.
bot.onSlashCommand('app', async (handler, event) => {
const publicUrl = process.env.PUBLIC_URL || 'https://my-app.com'
const cacheBust = Date.now() // Prevent caching during dev
await handler.sendMessage(event.channelId, 'Launch My App', {
attachments: [{
type: 'miniapp', // Must be 'miniapp', NOT 'link'
url: `${publicUrl}/miniapp?v=${cacheBust}`
}]
})
})Miniapp HTML Template
Your miniapp HTML must include specific meta tags and SDK integration.
<!DOCTYPE html>
<html>
<head>
<!-- REQUIRED: Miniapp metadata -->
<meta name="fc:miniapp" content='{
"version":"1",
"imageUrl":"https://my-app.com/preview.png",
"button":{
"title":"Launch App",
"action":{
"type":"launch_miniapp",
"name":"My App",
"url":"https://my-app.com",
"splashImageUrl":"https://my-app.com/splash.png",
"splashBackgroundColor":"#667eea"
}
}
}' />
</head>
<body>
<script type="module">
import { sdk } from 'https://esm.sh/@farcaster/miniapp-sdk@0.2.3'
async function init() {
// ALWAYS call ready() first
await sdk.actions.ready()
// Access Towns context
const context = await sdk.context
// Towns user data
const userId = context.towns.user.userId
const address = context.towns.user.address
const spaceId = context.towns.spaceId
const channelId = context.towns.channelId
document.getElementById('user-id').textContent = userId
}
init()
</script>
<h1>Welcome to My Miniapp!</h1>
<p>User ID: <span id="user-id">Loading...</span></p>
</body>
</html>Miniapp Tips
- • Always call sdk.actions.ready() before accessing context
- • Use cache-busting during development: ?v=1766159421092
- • Don't use emojis in button titles
- • Test on both Alpha and Omega environments
- • Must use HTTPS in production
Batch Operations
Execute multiple blockchain operations in a single atomic transaction.
Using execute() for Batch Operations
The execute function from ERC-7821 lets you batch multiple operations atomically.
import { execute } from 'viem/experimental/erc7821'
import { parseEther } from 'viem'
bot.onSlashCommand('airdrop', async (handler, event) => {
if (event.mentions.length === 0 || !event.args[0]) {
await handler.sendMessage(
event.channelId,
'Usage: /airdrop @user1 @user2 ... <amount-each>'
)
return
}
const amountEach = parseEther(event.args[0])
// Build batch calls - one tip per user
const calls = event.mentions.map((mention) => ({
to: tippingContractAddress,
abi: tippingAbi,
functionName: 'tip',
value: amountEach,
args: [
{
receiver: mention.userId,
tokenId: tokenId,
currency: ETH_ADDRESS,
amount: amountEach,
messageId: event.eventId,
channelId: event.channelId,
},
],
}))
// Execute all tips in one transaction
const hash = await execute(bot.viem, {
address: bot.appAddress,
account: bot.viem.account,
calls
})
await waitForTransactionReceipt(bot.viem, { hash })
await handler.sendMessage(
event.channelId,
`Airdropped ${event.args[0]} ETH to ${event.mentions.length} users!
Transaction: ${hash}`
)
})Complex Multi-Step Operations
Combine approve, swap, stake, and more in one atomic transaction.
bot.onSlashCommand('defi-combo', async (handler, event) => {
const amount = parseEther(event.args[0] || '100')
// Approve + Swap + Stake in one transaction
const hash = await execute(bot.viem, {
address: bot.appAddress,
account: bot.viem.account,
calls: [
{
to: tokenAddress,
abi: erc20Abi,
functionName: 'approve',
args: [dexAddress, amount]
},
{
to: dexAddress,
abi: dexAbi,
functionName: 'swapExactTokensForTokens',
args: [amount, minOut, [tokenIn, tokenOut], bot.appAddress]
},
{
to: stakingAddress,
abi: stakingAbi,
functionName: 'stake',
args: [amount]
}
]
})
await waitForTransactionReceipt(bot.viem, { hash })
await handler.sendMessage(
event.channelId,
`✅ Swapped and staked ${event.args[0]} tokens in one transaction!`
)
})Batch Benefits
- • Atomic execution - all succeed or all fail
- • Gas optimized - single transaction fee
- • Perfect for airdrops, multi-step DeFi, bulk operations
- • Works with any contract interaction
- • Fund bot.appAddress for gas costs
Module 7 Complete!
Amazing! You've learned advanced interactive features including buttons, transactions, signatures, miniapps, and batch operations. Your bot can now create rich, interactive Web3 experiences!