Delete a Telegram bot and https://t.me/your_deleted_bot keeps returning HTTP 200
with a page that looks completely normal. Every link checker I know of — CI actions,
directory scripts, monitoring cron jobs — reports it as healthy forever.
If you maintain anything that lists Telegram bots, some fraction of your list is
already dead and your checks are telling you it is fine.
Reproducing it
Pick a username that has never existed:
curl -s -o /dev/null -w "%{http_code}\n" https://t.me/nonexistent_test_bot_77712
# 200
Enter fullscreen mode Exit fullscreen mode
Two hundred. No redirect, no 404, no soft-404 marker in the body that a status check
would catch.
Where the truth is
The status code is useless here, but the Open Graph title is not. I measured four
usernames — two live bots, two that do not exist:
og:title
t.me/BookClassBot (live)
BookClass
t.me/instanavy_bot (live)
StoryViewer - anonymous instagram story viewer tool
t.me/nonexistent_test_bot_77712
Telegram: Contact @nonexistent_test_bot_77712
t.me/zzz_definitely_not_a_real_bot_9182
Telegram – a new era of messaging
A live bot puts its own display name in og:title. A dead one gets one of two
Telegram placeholders: Telegram: Contact @<username>, or — if the username is not
even syntactically valid — the generic Telegram – a new era of messaging.
That is the whole signal.
curl -s https://t.me/some_bot | grep -o '<meta property="og:title" content="[^"]*"'
Enter fullscreen mode Exit fullscreen mode
The check
Standard library only, no dependencies:
import re
import urllib.request
UA = "Mozilla/5.0 (compatible; linkcheck/1.0)"
DEAD_EXACT = {"Telegram – a new era of messaging", "Telegram"}
DEAD_PREFIX = "Telegram: Contact @"
def telegram_bot_exists(url: str) -> bool:
"""True if the bot behind a t.me URL still exists.
HTTP status is not usable here: Telegram serves 200 with a placeholder page
for usernames that were deleted or never existed. The Open Graph title is
what actually differs.
"""
req = urllib.request.Request(url, headers={"User-Agent": UA})
with urllib.request.urlopen(req, timeout=20) as resp:
html = resp.read(200_000).decode("utf-8", "replace")
m = re.search(r'<meta\s+property="og:title"\s+content="([^"]*)"', html)
title = m.group(1).strip() if m else ""
if not title:
return False
return title not in DEAD_EXACT and not title.startswith(DEAD_PREFIX)
Enter fullscreen mode Exit fullscreen mode
Note the em dash in Telegram – a new era of messaging. It is U+2013, not a hyphen.
I lost a few minutes to that one.
It survives the URL variants
Mini Apps get linked in several shapes, and the check holds for all of them — the
title still resolves to the app’s real name:
t.me/DefyTONBot/app -> "DefyTON"
t.me/arena_defense_bot/arena_defense -> "ARENA DEFENSE"
t.me/lexicon_snap_bot?startapp=cat_awesome -> "LexiCon"
t.me/playful_mind_bot/play?startapp=cat_awesome_tma -> "Playful Mind"
Enter fullscreen mode Exit fullscreen mode
So you can point the check at whatever URL someone submitted, without normalising it
to the bare username first.
Why bother
Because directories rot silently, and silent rot is the thing that kills them.
A list of bots that returns 200 across the board looks maintained. Every entry is
green. Meanwhile a chunk of them lead to a Telegram page saying nothing, and the only
person who finds out is the reader who clicks — once, and then never trusts the list
again.
I hit this while building a catalog of Telegram Mini Apps. Every entry passed a
conventional link check. Only after adding the title check could I say the bots were
actually there. It now runs weekly in CI and flags anything that has quietly
disappeared.
Caveats worth stating
- This depends on Telegram’s markup, which is not an API contract. If they change the placeholder strings, the check goes stale. Pin the strings somewhere obvious and test against a known-dead username occasionally.
- A bot that exists but is broken, banned, or unresponsive still passes. This tells you the username resolves — nothing more.
- Fetch politely. This is one request per entry, on a schedule, not a crawl.
The general shape
The reusable lesson is not about Telegram. It is that HTTP 200 means the server
answered, not that the thing exists. Plenty of platforms serve a decorated
“nothing here” page rather than a 404 — it is better for humans and worse for every
automated check pointed at them.
Whenever you are checking a resource on someone else’s platform, the question to ask
is: what does this host return for something that definitely is not there? Find out
first, then write the check. Assuming a 404 you never verified is how a monitor ends
up reporting green for months.
The catalog this came out of is
here — the check lives in
scripts/check.py, public domain, take it. I also build
StoriesFly, which is where the Instagram-flavoured test
usernames above came from.
답글 남기기