Agent API
The same catalogue the site runs on, addressed by a program. There is no account, no key and no invoice: the request pays for itself, so an agent that has never contacted us before can call it on its first try.
Two endpoints. Search finds entities you cannot name; fetch reads the latest posts and profile of entities you can, from Telegram, at the moment you ask. On both, parameters go in the query string, not in a body: the price is quoted by the payment layer, which sees the URL and nothing else.
Search
POST /api/agent/search?q=<query>&limit=<n>&lang=<code>&kind=<type>&min_users=<n>&max_users=<n> | Parameter | Required | Default | Meaning |
|---|---|---|---|
q | yes | — | The query. Matched by meaning as well as by name; @username returns that entity and similar ones. |
limit | no | 100 | Results wanted, 1 to 1000. Above 1000 is clamped, and priced at the clamped figure. |
lang | no | — | ISO 639-1 code, e.g. en, ru. Entities whose language could not be detected are kept. |
kind | no | — | channel, group or bot. |
min_users | no | — | Lowest subscriber, member or monthly user count. |
max_users | no | — | Highest subscriber, member or monthly user count. |
Fetch
POST /api/agent/fetch?usernames=<name1,name2,…> | Parameter | Required | Default | Meaning |
|---|---|---|---|
usernames | yes | — | Telegram usernames, comma-separated, 1 to 100. @name and t.me/name are accepted. Above 100 is refused with 400 and charged nothing. |
Each name is read from Telegram when you call, so the posts are as fresh as Telegram's own page. One entry comes back per
distinct name, in the order asked. A name Telegram holds nothing under comes back as {"type": "absent"} and is
charged for: the lookup is the product, and a miss costs the same to answer as a hit. If Telegram does not answer in
time, the entry comes from our catalogue instead and carries "stale": true.
Posts exist for channels. A group and a bot come back with their profile and no messages field at
all, because Telegram publishes no readable history for either.
Paying per request
Payment is the x402 protocol — HTTP 402, no keys and no subscription.
- Send the request with no payment header.
-
The reply is
402 Payment Requiredwith aPayment-Requiredheader: base64 of a JSON document naming the recipient, the token, the amount and the networks accepted. -
Authorise a payment matching one of them and repeat the request with a
Payment-Signatureheader. The authorisation is good for five minutes. - On
200the payment settles on chain. On any error nothing is charged.
An x402 client library does all four steps for you: x402-fetch for JavaScript, x402-reqwest for Rust.
Search is a tier per hundred results asked for, rounded up, never fewer than one: ceil(limit / 100) × base. Fetch is
linear in the names: names × base, with duplicates collapsed before pricing. Both in USDC, on Base and on Solana.
The base amount is quoted in the 402 itself, and in /.well-known/x402 and openapi.json, both free to read.
Search response
200, application/json, an array in ranked order. Empty is [], never null.
Fields with nothing in them are omitted.
[
{
"id": 123456,
"kind": "channel",
"username": "example_channel",
"name": "Example Channel",
"bio": "An example Telegram channel",
"avatar_url": "https://semagram.io/api/avatar/example_channel.jpg",
"user_count": 15000,
"has_chat": "example_chat"
}
] | Field | Type | Meaning |
|---|---|---|
id | number | Stable identifier. Survives a rename; username does not. |
kind | string | channel, group or bot. |
username | string | Telegram username, without the @. |
name | string? | Display name. |
bio | string? | Short description from the Telegram page. |
description | string? | A bot's full description. |
lang | string? | Language, ISO 639-1. Absent when it could not be detected. |
avatar_url | string? | Avatar, served by us rather than by Telegram. |
user_count | number? | Subscribers of a channel, members of a group, monthly users of a bot. |
has_chat | string | true? | The channel's linked chat: its username when the chat is public, true when the channel has one but it is private or its username is not known yet. |
Fetch response
200, application/json, one entry per name in the order asked. messages is there only for
a channel, newest first, and is then an array — never null.
[
{
"type": "channel",
"username": "example_channel",
"id": 123456,
"name": "Example Channel",
"bio": "Short channel bio",
"lang": "en",
"user_count": 15000,
"has_chat": "example_chat",
"fetched_at": "2026-09-28T10:14:00Z",
"messages": [
{
"id": 412,
"text": "An example post",
"posted_at": "2026-09-28T09:04:00Z",
"forwarded_from": "telegram",
"link": "https://t.me/example_channel/412",
"view_count": 91024
}
]
},
{ "type": "absent", "username": "no_such_name" }
] | Field | Type | Meaning |
|---|---|---|
type | string | channel, group, bot, absent, or invalid. |
username | string | The name asked for, lowercased. |
id | number? | The identifier search returns for the same entity, when it is in the catalogue. |
name, bio, lang, avatar_url, user_count, has_chat | — | As in a search result. |
description, commands | string?, object? | A bot's description, and its commands as name to description. |
fetched_at | string | When the data was read from Telegram. |
stale | boolean? | Present and true when Telegram did not answer and the entry comes from our catalogue. |
messages | array? | The latest posts. Only a channel has this field. |
A message carries id, posted_at, link, and optionally text,
forwarded_from and view_count. forwarded_from has three states: the origin's
username when the origin is public, the literal true when the post is a forward
whose origin is hidden, and absent when the post is original. Text only, never media: a photo posted with no
caption is a message with no text.
Errors
| Status | Meaning |
|---|---|
400 | Search: q is missing or empty, or kind is not one of the three. Fetch: no usernames, or more than 100. |
402 | No payment, or one that did not settle. Read the Payment-Required header. |
502 | The request could not be answered. Retriable; nothing is charged. |
Example
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();
// Then read what those channels are posting.
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 serves the same catalogue as two tools, search and fetch, over streamable HTTP. They
take the arguments above as a structured object — fetch takes its usernames as an array — and return the same rows.
It is behind OAuth 2.0: the first connection sends you through a browser once, and the client keeps the token afterwards. Clients are registered by hand, so give the client id along with the address:
claude mcp add --transport http --client-id semagram-mcp semagram https://mcp.semagram.io/mcp Access is granted on request. Write to [email protected] with the client you will connect with.
Also
- agent-api.md — this page as one Markdown file, for a model to read.
- openapi.json — the OpenAPI description.
- /.well-known/x402 — the payment discovery document.
- x402 documentation — the protocol itself.