API для агентов
Тот же каталог, на котором работает сайт, только обращается к нему программа. Ни аккаунта, ни ключа, ни счёта: запрос оплачивает сам себя, поэтому агент, который никогда с нами не связывался, может вызвать его с первой попытки.
Два эндпойнта. Поиск находит то, чего вы не можете назвать; загрузка отдаёт сохранённые сообщения и данные профиля того, что назвать можете. У обоих параметры передаются в строке запроса, не в теле: цену называет платёжный слой, а он видит только URL, — из тела запрос был бы оценён по одной цене, а отвечал бы на другое.
Поиск
POST /api/agent/search?q=<запрос>&limit=<n>&lang=<код>&kind=<тип>&min_users=<n>&max_users=<n> | Параметр | Обязательный | По умолчанию | Значение |
|---|---|---|---|
q | да | — | Запрос. Ищем по смыслу, а не только по названию; @username вернёт этот канал и ближайшие к нему. |
limit | нет | 100 | Сколько результатов нужно, от 1 до 1000. Больше 1000 обрезается — и оплачивается по обрезанному числу. |
lang | нет | — | Код ISO 639-1, например en, ru. |
kind | нет | — | channel, group или bot. |
min_users | нет | — | Нижняя граница числа подписчиков или участников. |
max_users | нет | — | Верхняя граница. |
Загрузка по имени
POST /api/agent/fetch?usernames=<имя1,имя2,…> | Параметр | Обязательный | По умолчанию | Значение |
|---|---|---|---|
usernames | да | — | Юзернеймы через запятую, от 1 до 100. Понимаются @name и t.me/name. Больше ста — отказ 400, деньги не списываются. |
На каждое имя приходит ровно одна запись, в том порядке, в котором спрашивали, — чтобы агент,
которому счёт выставлен по именам, мог свести ответ с запросом. Имя, которого в каталоге нет
— несуществующий канал, аккаунт человека, имя, которое ещё ни разу не смотрели, — приходит
как {"type": "absent"} и оплачивается: продаётся
ответ на вопрос, а «такого у нас нет» стоит нам ровно столько же, сколько «вот оно».
Сообщения есть у каналов. Группа и бот приходят с данными профиля и вообще без поля messages: читаемой истории Telegram для них не
публикует, а пустой массив сказал бы «пока нет» — то есть заявление про наш обход, а не про
Telegram. У канала, постов которого мы не держим, массив как раз пустой: вот там «пока нет»
правда.
Оплата за запрос
Оплата идёт по протоколу x402 — HTTP 402, без ключей и без подписки.
- Отправьте запрос без платёжного заголовка.
-
В ответ придёт
402 Payment Requiredи заголовокPayment-Required: base64 от JSON, в котором получатель, токен, сумма и сети, которые мы принимаем. -
Подпишите платёж по одному из вариантов и повторите запрос с заголовком
Payment-Signature. Авторизация действует пять минут. -
При
200платёж проводится в сети. При любой ошибке не списывается ничего.
Клиентская библиотека x402 делает все четыре шага за вас: x402-fetch для JavaScript, x402-reqwest для Rust.
Поиск — по тарифу за каждую сотню запрошенных результатов, с округлением вверх, минимум
один: ceil(limit / 100) × базовая цена. Загрузка — линейно по именам: имена × базовая цена, причём повторы схлопываются до оценки, так что дважды
названный канал оплачивается один раз. И то и другое в USDC, в сетях Base и Solana. Базовую сумму
называет сам 402, а не эта страница, чтобы агент читал ровно ту сумму, которую с
него спишут. Она же есть в /.well-known/x402 и openapi.json — оба документа читаются бесплатно.
Ответ поиска
200, application/json, массив в порядке ранжирования — тот же вид,
что и у /api/search. Пустой результат — это [], а не null. Поля, в которых ничего нет, не отдаются вовсе.
[
{
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"kind": "channel",
"username": "example_channel",
"name": "Example Channel",
"description": "An example Telegram channel",
"avatar_url": "https://semagram.io/avatar/example_channel.jpg",
"user_count": 15000
}
] | Поле | Тип | Значение |
|---|---|---|
uuid | string | Постоянный идентификатор. Переживает переименование, в отличие от username. |
kind | string? | channel, group или bot. |
username | string | Юзернейм в Telegram, без @. |
name | string? | Отображаемое имя. |
bio | string? | Краткое описание. |
description | string? | Полное описание. |
avatar_url | string? | Аватарка. Её раздаём мы, а не Telegram. |
user_count | number? | Подписчики или участники на момент последнего обхода. |
Ответ загрузки
200, application/json, по записи на каждое имя в том порядке, в
котором спрашивали. messages есть только у канала и тогда всегда массив — не null, — и пуст, если постов мы не держим.
[
{
"type": "channel",
"username": "example_channel",
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"name": "Example Channel",
"bio": "Short channel bio",
"user_count": 15000,
"profile_scraped_at": "2026-07-31T02:14:00Z",
"messages_scraped_at": "2026-07-31T02:15:00Z",
"messages": [
{
"id": 412,
"kind": "photo",
"text": "An example post",
"posted_at": "2026-07-30T11:04:00Z",
"forwarded_from": "telegram",
"link": "https://t.me/example_channel/412",
"view_count": 91024,
"reaction_count": 318,
"image_count": 1
}
]
},
{ "type": "absent", "username": "no_such_name" }
] | Поле | Тип | Значение |
|---|---|---|
type | string | channel, group, bot либо absent, если в каталоге такого нет. |
username | string | Имя, которое спрашивали, в нижнем регистре. |
uuid | string? | Тот же идентификатор, что возвращает поиск. |
canonical_username | string? | Написание самого Telegram, если оно отличается. |
bio, name, lang, avatar_url | string? | Как в результате поиска. |
description | string? | Полное описание. Только у ботов. |
user_count | number? | Подписчики у канала, участники у группы. У бота не бывает. |
mau | number? | Месячная аудитория. Только у ботов — это то, что у них вместо подписчиков. |
commands | object? | Команды бота: имя — описание. |
profile_scraped_at | string? | Когда профиль в последний раз успешно прочитан с Telegram. Отсутствует, если это неизвестно. |
messages_scraped_at | string? | Когда история в последний раз успешно прочитана. Отсутствует, если это неизвестно. |
messages | array? | Сохранённые посты, свежие первыми. Поле есть только у канала. |
У сообщения есть id, kind, posted_at, link, а также, если есть что положить, text, forwarded_from, view_count, reaction_count и image_count.
forwarded_from — одно поле с тремя состояниями, потому что состояний три и два
из них значат «это репост»: юзернейм источника, если источник публичный;
литеральное true, если это репост, а источник себя скрыл; и отсутствие поля, если пост оригинальный. Проверять наличие ключа — значит
спрашивать «репост ли это»; проверять, что значение строка, — «можно ли пройти по цепочке».
Отдаётся текст, а не медиа. Хранится текст поста и подпись к медиа — сам
файл не хранится. Поэтому фотография без подписи — это сообщение вообще без text, и kind — единственное, что говорит, что это была
фотография, а не пустое сообщение. Он есть всегда и принимает одно из значений: text, photo, video, round_video, voice, sticker, document, media_group, poll, location, contact, invoice, service, unsupported, unknown.
Ошибки
| Код | Что означает |
|---|---|
400 | Поиск: q отсутствует или пуст, либо kind не один из трёх. Загрузка: usernames не названы или их больше ста. |
402 | Платежа не было или он не прошёл. Смотрите заголовок Payment-Required. |
502 | Запрос не отработал. Можно повторить; не списывается ничего. |
Пример
import { wrapFetch } from "x402-fetch";
const fetch402 = wrapFetch(fetch, wallet);
const found = await fetch402(
"https://semagram.io/api/agent/search?q=crypto+news&limit=200",
{ method: "POST" },
);
const results = await found.json();
// А потом прочитать, что эти каналы пишут.
const names = results.slice(0, 10).map((r) => r.username).join(",");
const fetched = await fetch402(
`https://semagram.io/api/agent/fetch?usernames=${names}`,
{ method: "POST" },
);
const entities = await fetched.json(); Model Context Protocol
/mcp отдаёт тот же каталог двумя инструментами — search и fetch — поверх streamable HTTP. Аргументы те же, что выше, но
структурированным объектом, а не строкой запроса: fetch принимает юзернеймы
массивом. Строки в ответе те же.
Вход через OAuth 2.0: при первом подключении вас один раз проведут через браузер, дальше клиент хранит токен сам. Наш сервер авторизации регистрирует клиентов вручную, а не по запросу, поэтому идентификатор клиента нужно указать вместе с адресом — без него клиент, рассчитывающий зарегистрироваться сам, скажет, что сервер несовместим:
claude mcp add --transport http --client-id semagram-mcp semagram https://mcp.semagram.io/mcp Любой MCP-клиент подключается так же, отличается только написание флага. Если ваш не умеет задавать идентификатор клиента, подключиться пока не выйдет — напишите нам, какой это клиент.
Доступ выдаётся по запросу: в отличие от платного эндпойнта, самостоятельной регистрации здесь нет. Напишите на [email protected], укажите клиент и его client id, — и вам вышлют учётные данные.
Ещё
- agent-api.md — эта же страница одним файлом Markdown, чтобы её читала модель.
- openapi.json — описание в формате OpenAPI.
- /.well-known/x402 — документ обнаружения для платёжных каталогов.
- Документация x402 — сам протокол.