Telegram
Send Telegram messages from your handlers via the Bot API. No SDK required β just fetch.
Bot setupβ
- Open @BotFather in Telegram, run
/newbot, and copy the bot token. - Add the bot to a chat (or DM it) and grab the chat ID. The simplest way is to send any message and visit
https://api.telegram.org/bot<token>/getUpdatesβchat.idis in the JSON. - Set
TELEGRAM_BOT_TOKENandTELEGRAM_CHAT_IDin your.env.
Define the effectβ
Bot token and chat ID are static β bake them in. The handler hands the effect raw values; the message string is built inside the effect body.
import { createEffect, S } from "envio";
const ENDPOINT = `https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}/sendMessage`;
const CHAT_ID = process.env.TELEGRAM_CHAT_ID!;
export const notifyTransfer = createEffect(
{
name: "notifyTransfer",
input: {
from: S.string,
to: S.string,
value: S.bigint,
contract: S.string,
txHash: S.string,
},
rateLimit: { calls: 25, per: "second" }, // Telegram limits ~30 msg/sec globally
mode: "orderedAfterCommit", // human-readable feed β keep order
},
async ({ input, context }) => {
const text = [
`*New RETH Transfer Event*`,
`from: ${input.from}`,
`to: ${input.to}`,
`amount: ${formatUnits(input.value, 18)}`,
`RETH contract: ${input.contract}`,
`[etherscan](https://etherscan.io/tx/${input.txHash})`,
].join("\n");
const res = await fetch(ENDPOINT, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
chat_id: CHAT_ID,
text,
parse_mode: "Markdown",
disable_web_page_preview: true,
}),
});
if (!res.ok) {
context.log.error(`Telegram failed: ${res.status} ${await res.text()}`);
throw new Error(`Telegram ${res.status}`);
}
}
);
const formatUnits = (value: bigint, decimals = 18) => {
const base = 10n ** BigInt(decimals);
const whole = value / base;
const frac = value % base;
if (frac === 0n) return whole.toString();
return `${whole}.${frac.toString().padStart(decimals, "0").replace(/0+$/, "")}`;
};
Call it from a handlerβ
The rindexer configβ¦
chat:
telegram:
- bot_token: ${TELEGRAM_BOT_TOKEN}
chat_id: -4223616270
messages:
- event_name: Transfer
filter_expression: "value >= 10 && value <= 2000000000000000000"
template_inline: |
*New RETH Transfer Event*
from: {{from}}
to: {{to}}
amount: {{format_value(value, 18)}}
RETH contract: {{transaction_information.address}}
[etherscan](https://etherscan.io/tx/{{transaction_information.transaction_hash}})
β¦becomes a regular handler with a template literal:
import { indexer } from "envio";
import { notifyTransfer } from "../effects/telegram";
indexer.onEvent(
{ contract: "RocketPoolETH", event: "Transfer" },
async ({ event, context }) => {
const { from, to, value } = event.params;
if (value < 10n || value > 2_000_000_000_000_000_000n) return;
context.effect(notifyTransfer, {
from,
to,
value,
contract: event.srcAddress,
txHash: event.transaction.hash,
});
},
);
Lower latencyβ
If a delay between the on-chain event and the Telegram message is a problem (e.g. you're driving an alerting bot), switch the effect to mode: "ordered" β the runtime fires it inline within the batch instead of waiting for the DB commit. The tradeoff: a failed batch may still produce a delivered message, so retries on the next run are duplicates.
Multiple chats / multiple alertsβ
If you need to route to different chat IDs, define one effect per destination β each bakes in its own chat ID and message template β and pick the right one in the handler. A small factory keeps this DRY:
const sendTo = (chatId: string, label: string) =>
createEffect(
{
name: `telegram:${label}`,
input: { value: S.bigint },
rateLimit: { calls: 25, per: "second" },
mode: "orderedAfterCommit",
},
async ({ input }) => {
await fetch(ENDPOINT, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
chat_id: chatId,
text: `${label}: ${formatUnits(input.value)} RETH`,
parse_mode: "Markdown",
}),
});
},
);
export const whaleAlert = sendTo(process.env.TELEGRAM_WHALE_CHAT_ID!, "π Whale");
export const notableAlert = sendTo(process.env.TELEGRAM_NOTABLE_CHAT_ID!, "Heads up");
if (value >= 1_000_000_000_000_000_000_000n) {
context.effect(whaleAlert, { value });
} else if (value >= 100_000_000_000_000_000_000n) {
context.effect(notableAlert, { value });
}
Keeping each destination in its own effect avoids passing chatId through input (which would defeat dedup and clutter the call site).