Skip to content

Build a Telegram Bot for human-readable alerts

In this guide, you will learn how to create a Telegram 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.

Final result

Guide

Step 0: Prerequisites

  • An installed Bun (see installation guide here)
  • An Alchemy account (sign up here)
  • An Etherscan account (sign up here)
  • A Telegram account

Step 1: Clone the Repository

Clone the Bot repository from GitHub and install project dependecies:

Terminal window
git clone https://github.com/3loop/example-tg-bot
cd example-tg-bot
bun i

Step 2: Add Etherescan and Alchemy API Keys

Copy and rename the .env.example file to .env, then paste the Alchemy and Etherescan API keys into the ALCHEMY_API_KEY and ETHERSCAN_API_KEY variables.

Terminal window
cp .env.example .env
vim .env

We use the Alchemy API key to monitor new transactions happening on the blockchain, and the Etherscan API key (from the free plan) to fetch contract ABIs and avoid hitting rate limits. The Etherscan API could be optional if the transactions you are interested in do not interact with many contracts, but since we are testing AAVE V3 it will have many interactions.

Step 3: Create a New Bot on Telegram

  1. Obtain a bot token: Start a chat with the BotFather bot in Telegram, write /newbot into the chat, follow the instructions, and copy the bot token. Paste its value into the TELEGRAM_BOT_TOKEN variable in the .env file.
  2. Obtain a chat ID: Get the chat ID of the chat where the bot should send notifications. Start a chat with your bot by pressing the /start command. Then open to the link https://api.telegram.org/bot<YourBOTToken>/getUpdates, where YourBotToken is the token you copied from the BotFather. From the chat object, copy the id and put it into the TELEGRAM_CHAT_ID variable in the .env file. Check this StackOverflow answer for more details.

Step 4: Setup the Decoder

Now, let’s go through the code. To begin using the Loop Decoder, we need to set up the following components:

RPC Provider

We’ll create a function that returns an object with aPublicClient based on the chain ID. In this example, we are going to support the Ethereum Mainnet.

The constants.ts file contains the RPC URL and configuration:

src/constants.ts
export const RPC = {
1: {
url: `wss://eth-mainnet.g.alchemy.com/v2/${process.env.ALCHEMY_API_KEY}`,
},
}

The getPublicClient returns an RPC client based on the chaind ID and the config from constants.ts:

index.ts
import { createPublicClient, http } from 'viem'
// Create a public client for the Ethereum Mainnet network
const getPublicClient = (chainId: number) => {
return {
client: createPublicClient({
transport: http('https://rpc.ankr.com/eth'),
}),
}
}

ABI Loader

To avoid making unecessary calls to third-party APIs, Loop Decoder uses an API that allows cache. For this example, we will keep it simple and use an in-memory cache. We will also use some strategies to download contract ABIs from Etherscan and 4byte.directory. You can find more information about the strategies in the Strategies reference.

Create a cache for contract ABI:

index.ts
import {
EtherscanStrategyResolver,
FourByteStrategyResolver,
VanillaAbiStore,
ContractABI,
} from '@3loop/transaction-decoder'
// Create an in-memory cache for the ABIs
const abiCache = new Map<string, ContractABI>()
const abiStore: VanillaAbiStore = {
// Define the strategies to use for fetching the ABIs
strategies: [
EtherscanStrategyResolver({
apikey: 'YourApiKeyToken',
}),
FourByteStrategyResolver(),
],
// Get the ABI from the cache
// Get it by contract address, event name or signature hash
get: async ({ address, event, signature }) => {
const value = abiCache.get(address)
if (value) {
return {
status: 'success',
result: value,
}
} else if (event) {
const value = abiCache.get(event)
if (value) {
return {
status: 'success',
result: value,
}
}
} else if (signature) {
const value = abiCache.get(signature)
if (value) {
return {
status: 'success',
result: value,
}
}
}
return {
status: 'empty',
result: null,
}
},
// Set the ABI in the cache
// Store it by contract address, event name or signature hash
set: async (_key, value) => {
if (value.status === 'success') {
if (value.result.type === 'address') {
abiCache.set(value.result.address, value.result)
} else if (value.result.type === 'event') {
abiCache.set(value.result.event, value.result)
} else if (value.result.type === 'func') {
abiCache.set(value.result.signature, value.result)
}
}
},
}

Contract Metadata Loader

Create an in-memory cache for contract meta-information. Using ERC20RPCStrategyResolver we will automatically retrieve token meta information from the contract such as token name, decimals, symbol, etc.

index.ts
import type { ContractData, VanillaContractMetaStore } from '@3loop/transaction-decoder'
import { ERC20RPCStrategyResolver } from '@3loop/transaction-decoder'
// Create an in-memory cache for the contract meta-information
const contractMetaCache = new Map<string, ContractData>()
const contractMetaStore: VanillaContractMetaStore = {
// Define the strategies to use for fetching the contract meta-information
strategies: [ERC20RPCStrategyResolver],
// Get the contract meta-information from the cache
get: async ({ address, chainID }) => {
const key = `${address}-${chainID}`.toLowerCase()
const value = contractMetaCache.get(key)
if (value) {
return {
status: 'success',
result: value,
}
}
return {
status: 'empty',
result: null,
}
},
// Set the contract meta-information in the cache
set: async ({ address, chainID }, result) => {
const key = `${address}-${chainID}`.toLowerCase()
if (result.status === 'success') {
contractMetaCache.set(key, result.result)
}
},
}

Loop Decoder instance

Finally, we’ll create a new instance of the LoopDecoder class.

src/decoder/decoder.ts
import { TransactionDecoder } from '@3loop/transaction-decoder'
const decoder = new TransactionDecoder({
getPublicClient: getPublicClient,
abiStore: abiStore,
contractMetaStore: contractMetaStore,
})

Step 5: Decode and Interpret a Transaction

After creating the decoder object in the decoder.ts file, we can use its decodeTransaction method to decode transactions by hash:

const decoded = await decoder.decodeTransaction({
chainID: chainID,
hash: txHash,
})

The decodeTransaction method returns a DecodedTransaction object, which you can inspect using our playground. Here is an example of an AAVE V3 transaction.

To interpret a decoded transaction, the interpreter.ts file contains a transformEvent function that transforms the DecodedTransaction and adds an action description. You can test the transformEvent function by putting it into the Interpretation field on the playground with the AAVE V3 example, and pressing “Interpret”.

Playground

Step 6: Create a Contract Subscription

The bot is set to monitor AAVE V3 transactions on the Ethereum mainnet by default. Update the contract address and chain ID in the constants.ts file:

src/constants.ts
export const CONTRACT_ADDRESS = '0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2'
export const CHAIN_ID = 1

In the program’s entry point, we start a WebSocket subscription for new transactions using Alchemy’s WebSocket RPC.

src/index.ts
async function createSubscription(address: string) {
await publicClient.transport.subscribe({
method: 'eth_subscribe',
params: [
//@ts-ignore
'alchemy_minedTransactions',
{
addresses: [{ to: address }],
includeRemoved: false,
hashesOnly: true,
},
],
onData: (data: any) => {
const hash = data?.result?.transaction?.hash
if (hash) handleTransaction(hash)
},
//...
})
}
createSubscription(CONTRACT_ADDRESS)

Step 7: Handle a new Transaction

The handleTransaction function is responsible for decoding incoming alerts and sending a Telegram message. You can customize the bot message or add extra information by tweaking the botMessage variable.

async function handleTransaction(txHash?: string) {
try {
//...
const decoded = await decoder.decodeTransaction({
chainID: CHAIN_ID,
hash: txHash,
})
if (!decoded) return;
const interpreted = interpretTx(decoded as DecodedTransaction);
if (!interpreted.action) {
console.log("No defined action for this transaction.", txHash);
return;
}
const botMessage = `${interpreted.action} <https://etherscan.io/tx/${txHash}`>;
bot.sendMessage(chatId, botMessage);
} catch (e) {
console.error(e);
}
}

Step 8: Start the Bot

Use the following command to start the server locally:

Terminal window
bun run src/index.ts

Your Telegram bot is now set up and will monitor blockchain transactions and send alerts to the specified chat or channel.

Conculsion

In this guide, you’ve learned how to create a Telegram bot that monitors blockchain transactions and sends human-readable alerts. By using the Loop Decoder library, you can easily set up a bot to track specific contract addresses and chains without needing in-depth knowledge of EVM transaction decoding.

Let us know on X/Twitter (@3loop_io) if you encounter any problems or have any questions, we’d love to help you!

Happy coding!