Splitting Telegram Alerts Into a User Channel and a Developer Channel
Wiring Telegram notifications into a bot takes about thirty minutes. Get a bot token, call sendMessage, done. The trouble starts afterward: a single notification channel stops being usable very quickly.
Buy filled, sell filled, rotation complete — all fine. But then API token expiry warnings, order rejection payloads, exception stacks and trigger failures land in the same room. Buried under messages they cannot interpret, users miss the fill notifications that actually matter. Then they turn notifications off.
This post is about splitting alerts into a user channel and a developer channel. Structurally it is two functions, but where you draw that line ended up shaping how the whole thing is operated.
The line between them
One test: "is there anything the user can do with this message?"
- User channel — buy and sell fills, rotations, liquidations, bot start/stop. Things that happened in the account. A person can look and decide
- Developer channel — API errors, exceptions, bad parameters, exhausted retries. Things the code has to fix. Sending these to users only creates anxiety
Some events are ambiguous. Where does "the order was rejected" go? Both. The user gets "your buy did not go through"; the developer gets the rejection code and the raw response. It is the same event told twice in two languages, not something to force into one lane.
Which property scope holds the token and chat_id
This is the important decision. The two channels differ starting from the bot token itself.
// User channel - shared bot token, per-user chat_id
function SendBOTUser(body) {
if (getchatIdTel() == null || getchatIdTel() == "0") {
return; // not registered: exit quietly
}
var formattedDate = Utilities.formatDate(new Date(), "GMT+9",
"yyyy:MM:dd_hh:mm:ss");
const botToken = PropertiesService.getScriptProperties()
.getProperty("BOT_TOKEN");
var message = "StockBot(" + formattedDate + ")[" + who() + "]\n" + body;
var url = "https://api.telegram.org/bot" + botToken + "/sendMessage";
var payload = {
chat_id: getchatIdTel(), // from UserProperties
text: message
};
...
}
// Developer channel - both token and chat_id are fixed and shared
function SendBOTDev(body) {
var formattedDate = Utilities.formatDate(new Date(), "GMT+9",
"yyyy:MM:dd_hh:mm:ss");
const devToken = PropertiesService.getScriptProperties()
.getProperty("DEV_BOT_TOKEN");
const devchatid = PropertiesService.getScriptProperties()
.getProperty("DEV_BOT_CHATID");
var message = "StockBot(" + formattedDate + ")[" + who() + "]\n" + body;
var url = "https://api.telegram.org/bot" + devToken + "/sendMessage";
var payload = {
chat_id: devchatid, // fixed in ScriptProperties
text: message
};
...
}
The differences reveal the structure.
The user chat_id lives in UserProperties because it has to differ per person. Deploy the web app to execute as the accessing user and that scope splits automatically. There is not one line of user-partitioning logic in the code, yet everyone gets their own notifications.
The developer chat_id is fixed in ScriptProperties — whoever is running it, errors have to reach me.
Using an entirely different bot token is deliberate too. Share one bot and a user blocking it can also cut off developer alerts, and more importantly it gives users a path to discover the developer chat. Separate tokens remove that path.
How do you get a chat_id
A Telegram bot cannot message someone first. A chat_id only exists after the user has written to the bot. So registration is required — and telling people to "find your chat_id and paste it here" loses most of them.
Instead: ask the user to send their own account id to the bot, and let the server find and match it.
function TelBOTChatID(userid) {
const botToken = PropertiesService.getScriptProperties()
.getProperty("BOT_TOKEN");
const url = "https://api.telegram.org/bot" + botToken
+ "/getUpdates?limit=10";
try {
const response = UrlFetchApp.fetch(url);
const data = JSON.parse(response.getContentText());
if (!data.ok) {
Logger.log("TelBOTChatID API call failed");
return;
}
const updates = data.result;
let isFound = false;
// Newest first - if the same person sent several, take the latest
for (let i = updates.length - 1; i >= 0; i--) {
const msg = updates[i].message;
if (msg && msg.text && msg.text.includes(userid)) {
setchatIdTel(msg.chat.id.toString());
isFound = true;
break;
}
}
if (!isFound) {
Logger.log("TelBOTChatID [" + userid + "] user not found.");
}
} catch (e) {
Logger.log("TelBOTChatID error: " + e.toString());
}
}
The user only has to send their id to the bot once in Telegram and press the register button in the UI. The server does the rest.
Three caveats.
getUpdates only returns recent items — here, limit=10. If the user writes to the bot and presses the button much later, other people's messages will have pushed theirs out. The instructions have to say "press it right after you send."
The reverse iteration exists so that when the same person sent several messages, the most recent one wins. A chat_id can change when someone switches devices; scanning forward would grab the stale one.
getUpdates and webhooks are mutually exclusive. With a webhook registered, getUpdates returns an empty array. This approach requires not using webhooks.
A failed notification must not stop trading
This is the rule that matters most. Notifications are a side effect, not part of trading. If the Telegram API is down, orders still have to go out.
So both functions wrap the send in try/catch and, on failure, drop a log line and return.
try {
var response = UrlFetchApp.fetch(url, options);
Utilities.sleep(50);
Logger.log(response.getContentText());
} catch (e) {
Logger.log("An error occurred in SendBOT: " + e.message);
return; // do not rethrow
}
Not rethrowing is the point. Rethrow and a notification failure climbs the trading loop and kills the entire cycle. "Could not tell you" and "could not trade" are wildly different severities.
But it must still log. Swallow it with a bare catch {} and there is no way to investigate the eventual "I stopped getting alerts" report. With a line in the Apps Script execution log you can at least tell whether a send was attempted.
The Utilities.sleep(50) has a reason as well. Telegram rate-limits sends, and a bot can emit several messages in one cycle. Fifty milliseconds is enough to space consecutive sends without delaying trading.
The user channel stays quiet when unregistered
Look again at the first line of SendBOTUser:
if (getchatIdTel() == null || getchatIdTel() == "0") {
return;
}
For an unregistered user, nothing happens. Not an error, not a warning. Notifications are optional, so not using them has to be a normal state.
Checking both null and the string "0" is necessary because PropertiesService stores strings only. If initialization writes 0, it reads back as "0". This trips people up repeatedly with that store.
Once you ship it to other people, the split is mandatory
A bot you run alone survives with one channel. The moment someone else uses it, the situation changes completely.
First, developer alerts must not reach users. Exception messages carry partial account numbers, tickers and raw responses. In a multi-user setup, A's error message arriving at B is a data leak. Separate channels make that failure structurally impossible.
Second, user alerts flooding the developer channel make it useless. Ten users means hundreds of fill notifications a day, and no way to spot a real error inside them.
Third, users need to toggle their notifications while the developer channel stays permanently on. Split scopes give you that for free.
The [who()] prefix earns its keep here too: it tells you whose account an error came from. The user channel does not strictly need it — you only ever receive your own — but keeping both functions on one message format was worth more than trimming a field.
Summary
- One test: "is there anything the user can do with this?"
- Ambiguous events go to both channels, in different language — do not force them into one
- User chat_id in
UserProperties, developer chat_id inScriptProperties— scope does the partitioning for you - Split the bot tokens too, so users have no path to the developer channel
- Register chat_id by having the user message the bot and the server match it from
getUpdates(incompatible with webhooks) - On send failure, log and do not rethrow — notifications must never stop trading
- In multi-user deployments this split becomes a structural guard against data leaks
The next post covers FCM push notifications from GAS, which run alongside Telegram.
Note: this is an engineering write-up, not investment advice. Automated trading can lose money through software bugs and network failures; trading decisions and their outcomes are your own responsibility.
댓글
댓글 쓰기