Skip to content
teleproto

Quick Start

Build an end-to-end userbot that logs in, listens for messages, replies, and persists the session — all in about ten minutes.

Set up#

A fresh project, the package, and a runner that can execute TypeScript directly. tsx is the path of least resistance; node --import tsx or ts-node work equally well.

Terminal
mkdir my-bot && cd my-bot
npm init -y
npm install teleproto
npm install -D tsx typescript @types/node

Drop your credentials into .env (and add .env to .gitignore before you forget):

.env
TG_API_ID=1234567
TG_API_HASH=0123456789abcdef0123456789abcdef

Sign in once#

The full login flow lives in a single client.start() call. teleproto walks you through the SMS-code prompt, asks for the 2FA password if you have one, and registers the device. We're using StoreSession here instead of StringSession — it writes the auth key to disk under ./bot-session/, which is what you want for ongoing development. (Use StringSession when you need to ship the session as an env var to a one-off container.)

bot.ts
import "node:process";
import { TelegramClient } from "teleproto";
import { StoreSession } from "teleproto/sessions";
import { NewMessage } from "teleproto/events";
import { createInterface } from "node:readline/promises";

const apiId = Number(process.env.TG_API_ID);
const apiHash = process.env.TG_API_HASH!;

// Disk-backed session — persists under ./bot-session/
const session = new StoreSession("bot-session");

const client = new TelegramClient(session, apiId, apiHash, {
  connectionRetries: 5,
});

const rl = createInterface({ input: process.stdin, output: process.stdout });

await client.start({
  phoneNumber: () => rl.question("Phone: "),
  phoneCode:   () => rl.question("Code from Telegram: "),
  password:    () => rl.question("2FA password (if set): "),
  onError: (err) => {
    console.error(err);
    return false;
  },
});

rl.close();
console.log("Signed in as", (await client.getMe()).username);

Listen + reply#

Handlers are plain callbacks registered through addEventHandler. Pair the callback with an event builder like NewMessage to filter what reaches you. Below: only incoming messages that start with !echo, with an early exit on our own outgoing messages to avoid replying to ourselves.

bot.ts (continued)
client.addEventHandler(async (event) => {
  // ignore our own outgoing messages
  if (event.message.out) return;

  const text = (event.message.message || "").trim();
  await event.message.reply({ message: `you said: ${text}` });
}, new NewMessage({ pattern: /^!echo\b/ }));

// Keep the process alive. client.disconnected is a boolean getter,
// not a Promise — don't await it.
await new Promise<void>((resolve) => {
  process.once("SIGINT", () => resolve());
});

await client.disconnect();

There is no middleware pipeline, no next(), no dispatcher. Register more handlers for more events; each one runs independently when its builder matches.

Run it#

Load the env file and run the script:

Terminal
# load env vars from .env (use dotenv-cli, direnv, or your shell)
export $(grep -v '^#' .env | xargs)

npx tsx bot.ts

First run: answer the phone-number and code prompts. The bot then sits idle and replies to anything matching !echo. Hit Ctrl-C to exit cleanly.

One footgun worth naming: client.disconnected is a boolean getter, not a Promise. Writing await client.disconnected resolves immediately and drops you out of the process. Keep the process alive with a real wait — a new Promise(() => {}), a SIGINT listener like the one above, or whatever your process supervisor expects.

Where to next#