Skip to main content
Glama

English · Русский

Keys

Small smart functions, each callable in one line. Live at https://monoblock.casa/keys/

pip install monokeys
from 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: weather

No 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")     # True

Understands 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 Wikipedia

The 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 ★29798

Over 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 166

The 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 map

How wiki differs from wikidata

They are different things, useful in different situations:

what it is

for "Haifa"

wiki

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…

wikidata

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 LLC

The 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 behind

A 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')   # False

url — 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:  True

The 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: 13871714

Checks 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 instantly

An 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').text

Everything 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

Local

Keys (our server)

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

Timers(path)

storage in a sqlite file; ':memory:' for the process only

t.set(id, when, note)

set one; the same id moves it instead of doubling

t.due()

what has fired; delivered once

t.peek()

the same, without taking

t.pending()

who is still waiting and for how long

t.cancel(id)

drop it early — an unmute by hand

t.stream()

async for: waits on its own

t.wait(id)

wait for one particular timer

Three decisions that make it behave under a bot:

  • The same id moves 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

token

the public key from the server

your own access key, if you want one

base

https://monoblock.casa/keys

server address

timeout

20.0

how long to wait for an answer, seconds

retries

1

retries on a dropped connection

only

—

return just this field

fmt

json

json, text (a line for humans) or bool

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 winner

lang= 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/clear

An 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) -> dict

ctx 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 seconds

Production

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 only

The 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.0

Tests 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

X-Real-IP is read only from proxies listed in KEYS_TRUSTED_PROXIES

Leaking a key into logs

token= is cut out of the request line before it is written

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 KEYS_ADMIN_TOKEN

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 t.me page

~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.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    C
    maintenance
    A 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
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables AI assistants like Claude to read, search, and send messages through your personal Telegram account via MCP tools.
    MIT