Skip to main content
Towns bots are onchain apps powered by ERC-4337 (Account Abstraction) that can interact with any smart contract using ERC-7821 batch execution. The bot exposes viem client and app address for direct blockchain interactions:
  • bot.viem - Viem client with your gas wallet account for signing transactions
  • bot.appAddress - Your bot’s treasury wallet address (SimpleAccount)
Your gas wallet (bot.viem.account) needs Base ETH to pay for gas fees when executing transactions. See Understanding Your Bot’s Wallet Architecture for details.

Configuration

Base RPC URL

Bots that interact with smart contracts require a reliable RPC endpoint for Base chain. The default public RPC (mainnet.base.org) has strict rate limits and will cause your bot to fail quickly under normal usage. You can configure a custom RPC URL by passing baseRpcUrl to makeTownsBot():
const bot = await makeTownsBot(
  process.env.APP_PRIVATE_DATA!,
  process.env.JWT_SECRET!,
  {
    baseRpcUrl: process.env.BASE_RPC_URL,
    commands
  }
)
Add the RPC URL to your .env file:
BASE_RPC_URL=https://base-mainnet.g.alchemy.com/v2/YOUR_API_KEY
We recommend getting a RPC URL from a reputable provider, like Alchemy.

Reading from Contracts

Read from any contract without making a transaction:
import { readContract } from 'viem/actions'
import simpleAppAbi from '@towns-protocol/bot/simpleAppAbi'

const owner = await readContract(bot.viem, {
  address: bot.appAddress,
  abi: simpleAppAbi,
  functionName: 'moduleOwner',
  args: []
})

Writing to Contracts

execute (Primary Method)

Use execute from ERC-7821 for any onchain interaction. This is your main tool for blockchain operations. Single Operation:
import { execute } from 'viem/experimental/erc7821'
import { parseEther } from 'viem'
import { waitForTransactionReceipt } from 'viem/actions'

bot.onSlashCommand('transfer', async (handler, { channelId, mentions, args }) => {
  const recipient = mentions[0]?.userId
  const amount = parseEther(args[0])

  const hash = await execute(bot.viem, {
    address: bot.appAddress,
    account: bot.viem.account,
    calls: [{
      to: tokenAddress,
      abi: erc20Abi,
      functionName: 'transfer',
      args: [recipient, amount]
    }]
  })

  await waitForTransactionReceipt(bot.viem, { hash })
  await handler.sendMessage(channelId, `Transferred! Tx: ${hash}`)
})
Batch Operations: Execute multiple operations atomically (all succeed or all fail):
import { execute } from 'viem/experimental/erc7821'
import { parseEther } from 'viem'
import { waitForTransactionReceipt } from 'viem/actions'

bot.onSlashCommand('defi', async (handler, { channelId, args }) => {
  const amount = parseEther(args[0])

  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: 'swap',
        args: [tokenIn, tokenOut, amount]
      },
      {
        to: stakingAddress,
        abi: stakingAbi,
        functionName: 'stake',
        args: [amount]
      }
    ]
  })

  await waitForTransactionReceipt(bot.viem, { hash })
  await handler.sendMessage(channelId, `Swapped and staked! Tx: ${hash}`)
})

executeBatch

For advanced use cases requiring batches of batches:
import { executeBatch } from 'viem/experimental/erc7821'

const hash = await executeBatch(bot.viem, {
  address: bot.appAddress,
  account: bot.viem.account,
  calls: [
    [/* first batch */],
    [/* second batch */],
    [/* third batch */]
  ]
})

writeContract

Use only for your bot’s SimpleAccount contract operations:
import { writeContract, waitForTransactionReceipt } from 'viem/actions'
import simpleAppAbi from '@towns-protocol/bot/simpleAppAbi'
import { parseEther, zeroAddress } from 'viem'

const hash = await writeContract(bot.viem, {
  address: bot.appAddress,
  abi: simpleAppAbi,
  functionName: 'sendCurrency',
  args: [recipientAddress, zeroAddress, parseEther('0.01')]
})

await waitForTransactionReceipt(bot.viem, { hash })

Utility Functions

getSmartAccountFromUserId

Get a user’s smart account address from their Towns user ID.
import { getSmartAccountFromUserId } from '@towns-protocol/bot'

bot.onSlashCommand('wallet', async (handler, event) => {
  const wallet = await getSmartAccountFromUserId(bot, {
    userId: event.userId
  })

  if (wallet) {
    await handler.sendMessage(event.channelId, `Your wallet: ${wallet}`)
  } else {
    await handler.sendMessage(event.channelId, 'No smart account found')
  }
})
Returns null if no smart account exists. Use cases:
  • Send tokens/NFTs to users
  • Check on-chain balances
  • Airdrop rewards
  • Verify asset ownership

When to Use Each Method

  • readContract - Read from any contract (no transaction, no gas)
  • writeContract - Bot’s SimpleAccount contract operations only
  • execute - Primary method for any onchain interaction
    • Tips, swaps, staking, NFT minting, DeFi
    • Single operations or batch operations
    • Atomic execution (all succeed or all fail)
    • Gas optimized for batches
  • executeBatch - Advanced batching (batches of batches)