Build a Farcaster Bot for On-Chain Alerts
In this guide, you will learn how to create a Farcaster bot that sends human-readable alerts about transactions happening on-chain. You can customize this bot for any EVM-compatible blockchain, and you don’t need any specific knowledge about EVM transaction decoding and interpretation.

Guide
Step 0: Prerequisites
- Bun installed (see installation guide here)
- Alchemy account (sign up here)
- Basescan API Key (sign up here)
- Farcaster account (can be yours or a separate one for your bot)
Step 1: Clone the Repository
Clone the bot repository and install dependencies:
git clone https://github.com/3loop/farcaster-onchain-alerts-botcd farcaster-onchain-alerts-botbun iStep 2: Configure Environment Variables
Copy the .env.example file to .env and add your API keys:
cp .env.example .envvim .envFor the Farcaster bot you need to specify:
ALCHEMY_API_KEY- Alchemy API key to monitor new transactions via WebSocketETHERSCAN_API_KEY- Basescan API key, used to fetch and cache ABIsARCHIVE_RPC_URL- Archive RPC URL for Base (required for transaction tracing)SIGNER_PRIVATE_KEYandACCOUNT_FID- Farcaster credentials (see Step 3)
Step 3: Create a Farcaster Account Key (Signer)
A Farcaster signer is a separate Ed25519 public and private key pair connected to your Farcaster account that you need for posting messages on your behalf. To connect the key pair, you have to send a transaction from your Farcaster wallet to the Key Registry Farcaster smart contract. At the moment of writing this guide, there was no simple way to create and connect the signer without using 3rd party APIs. So we made a script to generate the required transaction, and to run it you need to do the following:
- Fund your Farcaster custody wallet on Optimism:: You need some ETH on the Optimism chain to pay for the gas. A few dollars would be enough. Click on the 3 dots near your profile, press “About,” and there you will find your custody address.
- Get your Farcaster recovery phrase: On your phone, go to settings -> advanced -> recovery phrase, and write this recovery phrase into the
MNEMONICvariable in thescripts/create-signer.tsfile. - Run the script: Run the following command
bun run scripts/create-signer.ts. The result of this script will be an Optimism transaction like this, and a public and private key printed in the console. Do not share the private key. - Add env variables: Add the private key generated from the script and the bot’s account FID into the
SIGNER_PRIVATE_KEYandACCOUNT_FIDvariables.
Step 4: Setup the Transaction Decoder
Loop Decoder requires three components: an RPC provider, ABI store, and contract metadata store. Let’s set up each one:
RPC Provider
Configure your RPC provider in constants.ts for Base Mainnet (chain ID 8453). We use traceAPI: 'geth' for transaction tracing:
export const RPC = { 8453: { archiveUrl: process.env.ARCHIVE_RPC_URL, traceAPI: 'geth', },}const getPublicClient = (chainId: number) => { const rpc = RPC[chainId as keyof typeof RPC] if (!rpc) throw new Error(`Missing RPC provider for chain ID ${chainId}`)
return { client: createPublicClient({ transport: http(rpc.archiveUrl) }), config: { traceAPI: rpc.traceAPI }, }}ABI Store
Set up an in-memory ABI cache with Basescan and 4byte.directory strategies:
import { EtherscanStrategyResolver, FourByteStrategyResolver, VanillaAbiStore, ContractABI,} from '@3loop/transaction-decoder'
// Create an in-memory cache for the ABIsconst abiCache = new Map<string, ContractABI>()
const abiStore: VanillaAbiStore = { strategies: [ // List of stratagies to resolve new ABIs EtherscanV2StrategyResolver({ apikey: process.env.ETHERSCAN_API_KEY || '', }), FourByteStrategyResolver(), ],
// Get ABI from memory by address, event or signature // Can be returned the list of all possible ABIs get: async ({ address, event, signature }) => { const key = address?.toLowerCase() || event || signature if (!key) return []
const cached = abiCache.get(key) return cached ? [ { ...cached, id: key, source: 'etherscan', status: 'success', }, ] : [] },
set: async (_key, abi) => { const key = abi.type === 'address' ? abi.address.toLowerCase() : abi.type === 'event' ? abi.event : abi.type === 'func' ? abi.signature : null
if (key) abiCache.set(key, abi) },}Contract Metadata Store
Set up contract metadata resolution for token or NFT information (name, decimals, symbol):
import type { ContractData, VanillaContractMetaStore } from '@3loop/transaction-decoder'import { ERC20RPCStrategyResolver, NFTRPCStrategyResolver } from '@3loop/transaction-decoder'
// Create an in-memory cache for the contract meta-informationconst contractMetaCache = new Map<string, ContractData>()
const contractMetaStore: VanillaContractMetaStore = { strategies: [ERC20RPCStrategyResolver, NFTRPCStrategyResolver],
get: async ({ address, chainID }) => { const key = `${address}-${chainID}`.toLowerCase() const cached = contractMetaCache.get(key) return cached ? { status: 'success', result: cached } : { status: 'empty', result: null } },
set: async ({ address, chainID }, result) => { if (result.status === 'success') { contractMetaCache.set(`${address}-${chainID}`.toLowerCase(), result.result) } },}Create Decoder Instance
Combine all components into a TransactionDecoder instance:
import { TransactionDecoder } from '@3loop/transaction-decoder'
const decoder = new TransactionDecoder({ getPublicClient, abiStore, contractMetaStore,})Step 5: Decode and Interpret Transactions
With the decoder set up, you can now decode transactions and make them human-readable:
// 1. Decode the transactionconst decoded = await decoder.decodeTransaction({ chainID: CHAIN_ID, hash: txHash,})
// 2. Interpret it (make it human-readable)const interpreted = interpretTx(decoded)
// 3. Use the resultconsole.log(interpreted.action) // e.g., "Alice bought 5 shares of Bob for 0.1 ETH"View a decoded AAVE transaction example in our playground. You can test the interpretTx function by pasting it into the Interpretation field.
Step 6: Monitor AAVE Transactions
Set up real-time monitoring for AAVE trades. Configure the contract address in constants.ts:
export const CONTRACT_ADDRESS = '0xa238dd80c259a72e81d7e4664a9801593f98d1c5'export const CHAIN_ID = 8453Subscribe to new transactions and process them:
const wsClient = createPublicClient({ transport: webSocket(ALCHEMY_WS_RPC_URL),})
// Subscribe to AAVE transactionswsClient.transport.subscribe({ method: 'eth_subscribe', params: [ 'alchemy_minedTransactions', { addresses: [{ to: CONTRACT_ADDRESS }], includeRemoved: false, hashesOnly: true, }, ], onData: (data: any) => { const hash = data?.result?.transaction?.hash if (hash) handleTransaction(hash) },})
// Process each transactionasync function handleTransaction(txHash: string) { try { // 1. Decode const decoded = await decoder.decodeTransaction({ chainID: CHAIN_ID, hash: txHash, })
if (!decoded) return
// 2. Interpret const interpreted = interpretTx(decoded)
// 3. Format message const text = `New trade: ${interpreted.trader} ${interpreted.isBuy ? 'Bought' : 'Sold'} ${ interpreted.shareAmount } shares of ${interpreted.subject} for ${interpreted.price} ETH`
// 4. Post to Farcaster await publishToFarcaster({ text, url: `https://basescan.org/tx/${txHash}`, }) } catch (e) { console.error(e) }}Step 7: Publish to Farcaster
Use the @standard-crypto/farcaster-js-hub-rest package to publish casts:
async function publishToFarcaster(cast: { text: string; url: string }) { await client.submitCast( { text: cast.text, embeds: [{ url: cast.url }], }, Number(fid), signerPrivateKey, )}Step 8: Run the Bot
Start the bot locally:
bun run src/index.tsThe bot will now monitor AAVE transactions and post casts to your Farcaster account.
Next Steps
You’ve built a Farcaster bot that:
- Monitors specific contracts in real-time
- Decodes transactions automatically
- Generates human-readable descriptions
- Posts alerts to Farcaster
Customize it further:
- Track different contracts by updating
CONTRACT_ADDRESS - Modify the cast format in
handleTransaction - Add filters for specific transaction types or amounts
- Deploy to a server for 24/7 monitoring
Need help? Reach out on X/Twitter @3loop_io or check the full code example.