keys
Allows checking whether Telegram channels, groups, and bots are alive, returning data such as subscriber count, online count, and kind, without requiring an account or Bot API.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@keyscheck if @durov is alive on Telegram and how many subscribers it has"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
English · Русский
Keys
Small smart functions, each callable in one line. Live at https://monoblock.casa/keys/
pip install monokeysfrom monokeys import Keys
k = Keys() # nothing to configure
print(k.alive.text("@durov"))
# alive · channel · Pavel Durov · 10 998 851 subscribers
print(k.answer.text("weather in Haifa"))
# Haifa: 27.4°C, wind 8.0 m/s, humidity 68% · source: weatherNo signup, no access key, no quota: the client picks up the public key itself, and that key never expires and is never metered.
alive — is this Telegram link alive?
No account, no Bot API, no token: it reads the public t.me preview page — the
same one messengers use to draw a link card.
k.alive("@durov") # the whole answer as an object
k.alive.members_count("@durov") # 11005185 — just the number, already an int
k.alive.kind("ru_python") # group
k.alive.verified("@BotFather") # TrueUnderstands every shape a link comes in: @durov, t.me/durov,
t.me/durov/123, t.me/s/durov, t.me/+AbCdEf…, t.me/joinchat/…, DUROV.
Returns: is_alive, kind (channel / group / bot / user), title,
description, members_count, members_label, online_count, verified,
is_private, needs_request, restricted, has_preview, avatar_url,
deep_link, action, username, url, error.
The subtlety that makes naive checks lie: Telegram answers 200 for a deleted
channel and renders a placeholder. Alive and dead differ in the markup, not in
the status code.
Related MCP server: Telegram Bot MCP Server
answer — one question, many sources at once
k.answer.text("who is Pavel Durov")
# Pavel Valeryevich Durov is a technology entrepreneur… · source: ddg
k.answer.text("weather in Haifa") # Haifa: 27.4°C, wind 8.0 m/s
k.answer.text("100 usd to eur") # 100 USD = 86.23 EUR · rate 0.86
k.answer("Haifa", sources="osm,wiki") # only the map and WikipediaThe question goes to several places in parallel, each answers in its own
field, and the best one is named separately along with links you can check it
against. Leave sources out and the key reads the question and decides for
itself: a city goes to the map, a currency to exchange rates, a package to the
package registries. General reference is always asked.
Thirteen sources, not a single paid key: Wikipedia, Wikidata, Wiktionary, DuckDuckGo, OpenStreetMap, Open-Meteo, exchange rates, Crossref, arXiv, Open Library, PyPI, npm, GitHub.
What is deliberately absent: language models — every free one runs into a quota, and there is no way to check what a model says; and search engine results — scraping them is against their terms, and the server's IP is shared by a dozen other products, so one ban would take everything down.
Ask exactly the source you want
Every source is a method of its own. Same word, different answers:
k.answer.weather("Haifa") # Haifa: 27.4°C, wind 8.0 m/s, humidity 68%
k.answer.osm("Haifa") # Haifa, Haifa Subdistrict, Haifa District, Israel
k.answer.wiki("Haifa") # Haifa is the third-largest city in Israel…
k.answer.wikidata("Haifa") # Haifa — city in northern Israel
k.answer.rates("100 usd to eur") # 100 USD = 86.23 EUR
k.answer.github("telegram") # DrKLO/Telegram ★29798Over a plain link: ?only=weather or ?sources=weather.
Asking for a source's field is asking that source: the word "weather" may not appear in your question, and you still want the weather.
Exchange rates that do the math
k.answer.rates("100 dollars to shekels")
# 100 USD = 299.24 ILS · rate 2.99 · as of Mon, 31 Aug 2026
k.answer.rates("50 eur to ils and usd")
# 50 EUR = 173.06 ILS, 58.22 USD · rate 3.46
k.answer.rates("100 UZS to KGS") # three-letter codes work for all 166The amount, the currencies and their order are parsed out of the question: "100 dollars to shekels" means USD into ILS, not the other way round. There can be several targets. 166 currencies, including ones the European Central Bank does not publish at all.
Works in two languages
The language is detected from the question itself: Cyrillic goes to Russian sources, Latin to English ones. Hints and topic words are covered on both sides:
k.answer.text("погода в Хайфе") == k.answer.text("weather in Haifa") # both go to weather
k.answer.text("где находится Хайфа") == k.answer.text("where is Haifa") # both go to the mapHow wiki differs from wikidata
They are different things, useful in different situations:
what it is | for "Haifa" | |
| an article summary written by people — connected prose, a few sentences | Haifa is the third-largest city in Israel, after Jerusalem and Tel Aviv, with a mixed Jewish-Arab population… |
| a structured fact: a label plus one line of "what kind of thing this is", plus a Q-code | Haifa — city in northern Israel, third-largest in the country |
Put simply: wiki is for reading, wikidata is for parsing. The second one is
shorter, identical across languages, and useful when you need the type of a
thing rather than its description.
dev — a vulnerability, a domain or an address
k.dev.text("CVE-2021-44228")
# cve CVE-2021-44228 · critical · Apache Log4j2 2.0-beta9 through 2.15.0 …
k.dev.text("github.com")
# domain github.com · MarkMonitor Inc. · expires 2026-10-09 · 140.82.121.3
k.dev.text("8.8.8.8")
# ip 8.8.8.8 · Ashburn United States · Google LLCThe key works out what you gave it: a CVE number, a domain and an IP address look nothing like each other. Three things a developer looks up constantly, opening three different sites every time.
Open sources, no keys: CIRCL (a CVE database mirror), RDAP instead of the retired whois, DNS over Cloudflare, the Wayback Machine, ip-api.
crypto — what a coin costs
k.crypto.text("bitcoin") # Bitcoin · 78 293 USD · 24h +0.08% · rank 1
k.crypto("ETH", vs="usd")Deliberately separate from ordinary exchange rates: a currency has one central bank number per day, a coin has a price in several currencies at once, a 24-hour move and a market cap — and all of it changes by the minute.
time — what time it is there, and what time yours is there
k.time('Haifa')
# Haifa: 00:15, 08.09.2026 · Tuesday · Asia/Jerusalem
k.time('Haifa', to='New York', at='15:00')
# Haifa: 15:00, 08.09.2026 · Tuesday · Asia/Jerusalem · New York: 08:00 · 7 h behindA question about time almost never ends at «what time is it». The real one is «my call is at 15:00, what is that for them» — and you do that arithmetic by hand every single time. Daylight saving is already accounted for, and if the answer lands on the next day there, the key says so:
k.time('Haifa', to='Tokyo', at='23:00', only='to_time,next_day')
# '05:00 · True'weather — weather in words
k.weather('Haifa')
# Haifa: partly cloudy · 27.6°C ≈32.8°C · 27.2°C…31.4°C · ☔0%
k.weather('Berlin', when='tomorrow', only='description,umbrella')
# 'overcast · True'Numbers answer the wrong question. People ask «do I need an umbrella» and get code 61 and 83% humidity. Here it is the other way round: weather in words, apparent temperature next to the real one, chance of rain and a straight answer about the umbrella. Forecast up to seven days. «Tomorrow» is counted in the place's own timezone, not our server's.
read — a page as text
article = k.read('https://peps.python.org/pep-0008/')
article.text # 'Indentation\\n\\nUse 4 spaces per indentation level...'
article.words # 1010
article.title # 'PEP 8 – Style Guide for Python Code'
article.published # '2001-07-05'118 KB of markup become 1010 words of text. Menus, banners, the footer and the «read next» block stay out. The encoding is taken from the page itself — plenty of sites still ship cp1251, and trusting the HTTP header alone gives you mojibake.
When there is no connected text, the key says so instead of passing a list of links off as an article:
k.read('https://news.ycombinator.com/', only='is_article') # Falseurl — a link card
k.url('bit.ly/3xYz')
# url: 'https://example.com/article' ← where it actually leads
# title: 'Page title'
# image: 'https://example.com/og.png'
# alive: True
# redirected: TrueThe same thing a messenger shows when you paste a link into a chat. Separate
from read on purpose: the reader needs text and pays for it by parsing the
whole page, while a card only needs the first few kilobytes.
email — address validation
k.email('ivan@gmial.com')
# valid: False · suggestion: 'ivan@gmail.com'Syntax, a live mail server (MX), disposable domain, role mailbox — and, above
all, a typo suggestion. ZeroBounce and Hunter charge for this; here it is
DNS-over-HTTPS plus a bundled disposable-domain list, no third-party keys. With
check_mx='0' it answers instantly and offline.
pwned — has this password leaked
k.pwned('qwerty123') # pwned: True · count: 13871714Checks Have I Been Pwned (900M+ passwords) without sending the password.
The SHA-1 is computed where the key runs, and only the first five hex characters
leave — you can't tell which password was asked (k-anonymity). Run it locally and
the password never leaves your machine; through our server it reaches us in the
clear (we don't log it) — for a real password use it locally or pass a ready
SHA-1 in hash=.
password — generate or rate a password
k.password(length=20).password # strong, via secrets, no network
k.password(check='qwerty123') # weak · 10 bits · cracked instantlyAn honest rating — entropy in bits, not «has an uppercase letter» checkboxes.
With no server of ours at all
A key is code, not a service. The library can run it on your side: the request goes to the source straight from your machine, with us out of the chain.
from monokeys import Local
k = Local() # no address, no access key
k.weather('Berlin').description # 'overcast'
k.time('Haifa', to='Tokyo', at='23:00').to_time
k.read('https://example.com/article').textEverything is the same, field names and short lines included: it is literally the same key code that runs on the server — it lives in the package, and the server takes it from there. Two copies would drift apart on the first edit.
For async code there is AsyncLocal with the same calls:
from monokeys import AsyncLocal
k = AsyncLocal()
weather = await k.weather('Berlin')A separate class rather than guesswork: a call that returns a dict in one place and a promise in another bites late and hard.
Which to take
|
| |
speed | one hop instead of two | plus the round trip to us |
privacy | we never see your queries | we do |
our outages | do not matter | do |
cache | in process memory | shared, survives restarts |
needs outbound internet | yes | no, reaching us is enough |
other languages | Python only | any, it is plain HTTP |
Take Local when in doubt. Keys is for when you have no direct way out, when
a cache shared across machines helps, or when the language is not Python.
Still zero dependencies: the local fetch is written on the standard library. If
certifi happens to be around, the root certificate list is taken from it — on
machines with a stale system list a perfectly alive site otherwise answers
«certificate has expired».
Works with no network: timers
Not everything needs a request. A Telegram bot mutes someone for half an hour and has to remember them in exactly half an hour — keeping that count on someone else's server makes their uptime your correctness. So timers live on your side: a plain sqlite file, no network, no key, no limits.
from monokeys import Timers
t = Timers('bans.db')
t.set(f'unmute:{chat}:{user}', '30m', note='flood')
async for fired in t.stream(): # waits on its own, no poll loop
chat, user = fired.id.split(':')[1:]
await bot.restrict_chat_member(chat, user, permissions=ALL_ALLOWED)Write the deadline however you like: 30, '30m', '2h', '1h30m',
'15:00', '2026-09-09 15:00', datetime(...).
call | what it does |
| storage in a sqlite file; |
| set one; the same |
| what has fired; delivered once |
| the same, without taking |
| who is still waiting and for how long |
| drop it early — an unmute by hand |
|
|
| wait for one particular timer |
Three decisions that make it behave under a bot:
The same
idmoves the timer. Extending a mute means replacing the deadline; otherwise the unmute happens twice, the first time too early.A fired timer is delivered once. Two handlers of one bot would otherwise unban a person twice — the second time after a fresh ban.
Downtime is not lost. The bot was down for an hour, three unmutes came due in that hour — it gets all three at once instead of missing them.
Library arguments
Keys(token=…, base=…, timeout=…, retries=…, user_agent=…)
k.alive(value, only=…, fmt=…, timeout=…, **params)Argument | Default | What it does |
| the public key from the server | your own access key, if you want one |
|
| server address |
|
| how long to wait for an answer, seconds |
|
| retries on a dropped connection |
| — | return just this field |
|
|
|
Typos do not stay silent: k.alive.members_cout(...) says immediately that
there is no such field and lists the real ones. Key and field names come from
the server, so a new key is available at once, without updating the package.
Full reference with examples: https://monoblock.casa/keys/client
Arguments do exactly what you ask
only= takes as many fields as you want — an application usually needs two or
three out of twenty, not the lot:
/alive/@durov?only=title,members_count,kind
→ Pavel Durov · 10971256 · channel
/alive/@durov?only=title,verified&fmt=json
→ {"title":"Pavel Durov","verified":true}A single field stays a bare value, ready to drop straight into a variable. A typo in any of the names is refused with the list of real fields.
For answer, the fields you name also say who to ask:
/answer/Haifa?only=weather,osm
/answer/Haifa?sources=osm,wikidata # both answers, not just the winnerlang= picks the source language. Do not ask an English source in Russian:
English Wikipedia answers "Хайфа" with a footballer of the same name, so such
an answer is thrown away and the key honestly says nothing.
You can skip the library entirely
A key is just a link — anything can open it:
https://monoblock.casa/keys/alive/@durov -> alive · channel · Pavel Durov · …echo file_get_contents("https://monoblock.casa/keys/alive/@durov");Add ?fmt=json for all the fields, ?only=members_count for a single number,
?fmt=bool for a bare true/false. Every key's page carries ready-made code
for curl, Python, JavaScript, C++, Go and PHP.
Inside an assistant
One address, and every key shows up inside Claude, Cursor or your editor as an ordinary tool:
{ "mcpServers": { "keys": { "url": "https://monoblock.casa/keys/mcp" } } }Access keys
You do not need one: the public key works for everybody and is never metered,
and the library fetches it itself from /public-token. It is deliberately not
baked into the package — that way it can be rotated with one command and
everyone picks up the new one without a release.
Your own key is only for those who want a kill switch of their own. The owner issues them:
curl -X POST -H "X-Admin-Token: $KEYS_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"label": "forum overclockers.ru"}' https://monoblock.casa/keys/token
curl -H "X-Admin-Token: $KEYS_ADMIN_TOKEN" https://monoblock.casa/keys/tokens
curl -X POST -H "X-Admin-Token: $KEYS_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"id": "a11ea8e0ea55"}' https://monoblock.casa/keys/token/revoke
# forget cached answers after changing a key
curl -X POST -H "X-Admin-Token: $KEYS_ADMIN_TOKEN" https://monoblock.casa/keys/cache/clearAn issued key never expires and is never counted — nobody here intends to meter anyone's requests. Instead of a quota there is a kill switch: revoke by public id, instantly, without touching the other keys.
If you want to add a key of your own
How it is put together
A key is one folder. Out of its manifest come, by themselves: the HTTP
endpoint, the MCP tool, the documentation page, openapi.json, llms.txt and
code examples in six languages. None of that is written by hand.
keys/
my-key/
key.json # what it does, what it takes, what it returns, how long to cache
handler.py # async def run(params, ctx) -> dictctx is the only door to the outside world: await ctx.fetch(url) already goes
through a per-host rate limiter. The optional canonical(params) brings input
to a canonical form before the cache — without it durov, DUROV and
t.me/durov/123 would be three separate entries and three trips to the network.
The loader checks: id matches the folder name, parameter types come from a
fixed list, there is a short summary, and the field used for the one-line answer
is described in returns.
Note for contributors: the source comments are written in Russian, the same language the project is developed in. Everything a user reads — this file, the package page, the site — is English.
Running it
pip install -r requirements.txt
python run.py # http://127.0.0.1:8110
pytest tests/ -q # 131 tests, offline, under two secondsProduction
Docker on 8105, nginx serves it under /keys/, databases on the keys-data
volume.
tar czf keys.tgz . && scp keys.tgz ubuntu@SERVER:/tmp/
ssh ubuntu@SERVER 'rm -rf ~/keys && mkdir ~/keys && tar xzf /tmp/keys.tgz -C ~/keys \
&& cd ~/keys && docker build -t keys:latest . && docker rm -f keys \
&& docker run -d --name keys --restart unless-stopped -p 127.0.0.1:8105:8105 \
--env-file ~/keys.env -v keys-data:/app/data keys:latest'Secrets live in ~/keys.env on the server and never reach the repository:
KEYS_ADMIN_TOKEN=… # without it, key issuing is closed
KEYS_TRUSTED_PROXIES=127.0.0.1 # nginx address; without it we trust no headers
KEYS_REQUIRE_HTTPS=1 # access keys over HTTPS onlyThe app knows its own prefix through --root-path /keys, so links and code
samples on the pages come out correct by themselves.
Releasing the library
No tokens involved: PyPI trusts GitHub directly over OIDC.
# bump version in clients/python/pyproject.toml, then
git tag v0.3.0 && git push origin v0.3.0Tests run before the upload — a version on PyPI cannot be replaced, and broken code can only be fixed by a new number.
What protects an access key
Against | How |
Spoofing the address via a header |
|
Leaking a key into logs |
|
A leaked database | sha256 is stored; only the public key is kept in the clear |
A stolen key | self-service revocation, effective immediately, not resurrected from cache |
Silent downgrade | an unknown or revoked key gets a 401, not a quiet drop to anonymous |
Anyone handing out keys | issuing requires |
Clogging the queue | waiting on the shared tap is bounded: busy means 503, not a hanging connection |
What the server can take
Measurements on the production machine (2 cores, 3.8 GB, a dozen other containers alongside):
What was measured | Result |
One | ~10 KB, 75 ms |
Eight in parallel | 171 ms for the whole batch |
Parsing a page | 0.085 ms → ~11 700 pages/sec on one core |
The CPU is not the issue — parsing is 900 times cheaper than the network trip.
There is exactly one bottleneck: how much t.me tolerates from our IP. So we go
out through a tap of 5 requests per second — that is not a per-user limit but
insurance against a ban. The cache relieves it: a live channel is kept for six
hours, a dead one for an hour.
Who wrote this
The code was written by Claude (Opus 5) under the direction of the repository owner: the tasks, the design decisions and every check against live data are his; the implementation is the model's. The Telegram preview parsing came out of his own earlier project, TG Catalog.
Commits carry Co-Authored-By where this applies.
This server cannot be deployed
Maintenance
Related MCP Connectors
Build, validate, deploy — HTTP APIs, cron jobs, webhooks and MCP tools — from your AI client.
The OpenRouter for tools. One MCP connection gives any AI agent 254 hosted tools, pay per call.
Free public MCP for AI agents — 193 tools, 44 workflows. No API key.
Give any MCP-compatible AI assistant a builder for live, hosted web tools and workflows.
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceEnables deployment of MCP servers over HTTP, providing ready-to-use tools for text processing, math calculations, and social media content generation, accessible to AI assistants like GitHub Copilot and Claude.-
- AlicenseAqualityDmaintenanceEnables AI assistants to publish, edit, search, and manage messages in Telegram channels via a set of MCP tools.8MIT
- AlicenseNot gradedqualityCmaintenanceA Telegram integration for Claude, Cursor, and other MCP-compatible clients. It exposes over 80 tools for account, chat, message, contact, media, folder, and admin operations through the Model Context Protocol.Apache 2.0
- AlicenseNot gradedqualityCmaintenanceEnables AI assistants like Claude to read, search, and send messages through your personal Telegram account via MCP tools.MIT