Integrations · Telegram Bot

Send /maskid @someone.
See who you’re talking to.

A small, open bot for your Telegram group or channel chat. Ask it about a Mask.ID member and it replies with their profile card — Trust Index, verified platforms, referrals — in the conversation. Public data only. Nothing stored. No dependencies.

What it does

Someone sends /maskid @eric_stanek — in a group, or in a private chat with the bot. The bot fetches that member’s public profile card from Mask.ID and replies with it as a photo, captioned with a link to the full profile. If there is no public profile under that name, it says so. That is the whole bot — about 80 lines of plain Node, talking to the Telegram Bot API directly. No framework, no database, no public URL needed.

It reads exactly what any visitor to Mask.ID can see. It never asks a member to share anything, never stores who asked about whom, and needs no Mask.ID account or API key of its own. A version that verifies members through Mask.ID and admits them to a group by Trust Index tier is the next step, built on the Attestation API.

Setup

  1. Create the bot with BotFatherIn Telegram, open @BotFather and send /newbot. Pick a display name and a username ending in bot. BotFather replies with the token — copy it and keep it private.
  2. Tell Telegram about the commandOptional but nice: send BotFather /setcommands, choose your bot, and paste maskid - Show a Mask.ID member's profile card. Telegram then autocompletes /maskid for everyone.
  3. Get the codeDownload the files below into a folder (say maskid-tg-bot/). Node 18 or newer — there is nothing to install.
  4. ConfigureCopy env.example to .env and paste the token.
  5. Start itnode --env-file=.env bot.js (Node 20+; on Node 18 export the variable first). Message the bot, or add it to a group and send /maskid @someone. To keep it running on a server, use the systemd unit at the bottom.

In groups, bots see only commands by default (Telegram’s privacy mode) — exactly what this bot needs, so leave it on.

The code

bot.js · the botdownload
// Mask.ID Telegram bot — answers `/maskid @someone` with that member's
// profile card. Reads only PUBLIC data (the same card any visitor sees at
// Mask.ID); nothing is stored. Zero dependencies: Node 18+ and the Bot API.

const TOKEN = process.env.TELEGRAM_TOKEN;
if (!TOKEN) { console.error("Set TELEGRAM_TOKEN."); process.exit(1); }

const MASKID = "https://app.mask.id";
const API = `https://api.telegram.org/bot${TOKEN}`;

// Usernames are letters, digits, and underscores. Anything else is rejected
// before it can reach a URL.
const USERNAME = /^[A-Za-z0-9_]{1,32}$/;

async function tg(method, body) {
  const res = await fetch(`${API}/${method}`, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify(body),
  });
  const data = await res.json();
  if (!data.ok) throw new Error(`${method}: ${data.description}`);
  return data.result;
}

const reply = (msg, text) =>
  tg("sendMessage", { chat_id: msg.chat.id, reply_to_message_id: msg.message_id, text });

async function handle(msg) {
  // In groups the command arrives as "/maskid@YourBot name"; strip the suffix.
  const m = /^\/maskid(?:@\w+)?(?:\s+(.*))?$/s.exec(msg.text ?? "");
  if (!m) return;

  const username = (m[1] ?? "").trim().replace(/^@/, "");
  if (!username) return reply(msg, "Usage: /maskid @username");
  if (!USERNAME.test(username)) return reply(msg, "That doesn't look like a Mask.ID username.");

  const cardUrl = `${MASKID}/card/${username}`;
  const profileUrl = `${MASKID}/u/${username}`;

  // The card is rendered on demand and cached server-side.
  let status;
  try {
    status = (await fetch(cardUrl, { method: "HEAD", redirect: "follow" })).status;
  } catch (err) {
    console.error("card fetch failed:", err);
    return reply(msg, "Couldn't reach Mask.ID just now. Try again in a minute.");
  }
  if (status === 404) return reply(msg, `No public Mask.ID profile for @${username}.`);
  if (status !== 200) return reply(msg, "Mask.ID is busy rendering cards — try again shortly.");

  // Telegram fetches the image itself from the URL.
  return tg("sendPhoto", {
    chat_id: msg.chat.id,
    reply_to_message_id: msg.message_id,
    photo: cardUrl,
    caption: `@${username} on Mask.ID\n${profileUrl}`,
  });
}

// Long polling: no public URL, no webhook, works from a laptop.
async function poll() {
  let offset = 0;
  const me = await tg("getMe", {});
  console.log(`Signed in as @${me.username}`);
  for (;;) {
    try {
      const updates = await tg("getUpdates", { offset, timeout: 30, allowed_updates: ["message"] });
      for (const u of updates) {
        offset = u.update_id + 1;
        if (u.message?.text) handle(u.message).catch((e) => console.error(e));
      }
    } catch (err) {
      console.error("poll error:", err.message);
      await new Promise((r) => setTimeout(r, 3000));
    }
  }
}

poll();
package.jsondownload
{
  "name": "maskid-telegram-bot",
  "version": "1.0.0",
  "private": true,
  "description": "Answers /maskid @username with the member's Mask.ID profile card.",
  "scripts": { "start": "node bot.js" },
  "engines": { "node": ">=18" }
}
env.example · copy to .envdownload
# Copy to .env and fill in. Never commit .env.
TELEGRAM_TOKEN=paste-the-token-from-BotFather
maskid-tg-bot.service · optional: keep it runningdownload
# /etc/systemd/system/maskid-tg-bot.service — keeps the bot running.
[Unit]
Description=Mask.ID Telegram bot
After=network-online.target

[Service]
User=maskidbot
WorkingDirectory=/opt/maskid-tg-bot
EnvironmentFile=/opt/maskid-tg-bot/.env
ExecStart=/usr/bin/node bot.js
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

How it behaves

Rate limits. Card rendering on Mask.ID is limited per IP address, and the bot checks every card from one address. A busy group with many lookups a minute will see “busy rendering” replies now and then; cards are cached for a while once rendered, so repeat lookups are cheap.

Privacy. The bot sends Mask.ID only the username that was asked about — not who asked, or which chat — and Mask.ID does not log IP addresses. Telegram fetches the image itself to deliver the photo.

Long polling. The bot asks Telegram for updates rather than receiving a webhook, so it runs anywhere with outbound internet — a laptop, a home server, a $5 VPS — with no domain or certificate.

License. The bot code on this page is public domain (CC0) — copy, change, and ship it however you like.

Next: admission by Trust Index tier

The card bot tells a group who someone is. The next bot asks people to verify through Mask.ID once, then approves join requests or lifts restrictions by their Trust Index tier — a bot-proof door that never asks for a phone number the group can see. That one uses the Attestation API and is on the Ways to integrate list. Want it first? The contact form reaches us.

Public data · Nothing stored · Zero dependencies

Running it? Tell us — your group gets listed.

Works with Mask.ID

Contact Us

Or message the developer directly on Vector.