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 Account for reading/writing contracts
- bot.appAddress- The bot’s app contract address (SimpleAccount)
Your bot client wallet will need to be funded with Base ETH to execute transactions.
You receive the wallet credentials after creating your bot.
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
  }
)
.env file:
BASE_RPC_URL=https://base-mainnet.g.alchemy.com/v2/YOUR_API_KEY
Reading from Contracts
Use readContract to read from any contract without making a transaction:
import { readContract } from 'viem/actions'
// Read from any contract
const balance = await readContract(bot.viem, {
  address: '0x00000000A22C618fd6b4D7E9A335C4B96B189a38',
  abi: erc20Abi,
  functionName: 'balanceOf',
  args: [userAddress]
})
Writing to Contracts
ERC-7821
execute
Use execute for any onchain interaction - swapping, staking, NFTs, DeFi, etc.
Single Operation:
import { execute } from 'viem/experimental/erc7821'
import { parseEther } from 'viem'
// Transfer tokens
const hash = await execute(bot.viem, {
  address: bot.appAddress,
  account: bot.viem.account,
  calls: [{
    to: tokenAddress,
    abi: erc20Abi,
    functionName: 'transfer',
    args: [recipient, parseEther('100')]
  }]
})
// Approve and transfer tokens
const hash = await execute(bot.viem, {
  address: bot.appAddress,
  account: bot.viem.account,
  calls: [
    {
      to: tokenAddress,
      abi: erc20Abi,
      functionName: 'approve',
      args: [spenderAddress, amount]
    },
    {
      to: tokenAddress,
      abi: erc20Abi,
      functionName: 'transferFrom',
      args: [bot.appAddress, recipientAddress, amount]
    }
  ]
})
executeBatch
Use 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 writeContract only for your bot’s own contract operations. Can be helpful if you have a custom contract for your bot.
Error Handling
Always wrap contract interactions in try-catch blocks:
bot.onSlashCommand('swap', async (handler, { channelId, args }) => {
  try {
    const hash = await execute(bot.viem, {
      address: bot.appAddress,
      account: bot.viem.account,
      calls: [/* ... */]
    })
    await waitForTransactionReceipt(bot.viem, { hash })
    await handler.sendMessage(channelId, `✅ Success! Tx: ${hash}`)
    
  } catch (error) {
    console.error('Transaction failed:', error)
    await handler.sendMessage(
      channelId, 
      `❌ Transaction failed: ${error.message}`
    )
  }
})
Next Steps