ESC
Start typing to search...

sdk.bot — Bot SDK

Build inline bots with styled buttons, callback handlers, and inline query responses using the Bot SDK.

Quick Example

Register an inline query handler that returns results with colored buttons:

Inline query with styled buttons
import type { PluginSDK, SimpleToolDef, PluginManifest } from "@teleton-agent/sdk";

export const manifest: PluginManifest = {
  name: "my-inline-bot",
  version: "1.0.0",
  description: "Demo inline bot with styled buttons",
  bot: { inline: true, callbacks: true },
};

export const start = async (sdk: PluginSDK) => {
  if (!sdk.bot) return;

  sdk.bot.onInlineQuery(async (ctx) => {
    return [{
      id: "1",
      type: "article",
      title: "Hello World",
      description: "Send a greeting",
      content: { text: "Hello from the inline bot!" },
      keyboard: [[
        { text: "Like", callback: "like:1", style: "success" },
        { text: "Dislike", callback: "dislike:1", style: "danger" },
      ]],
    }];
  });

  sdk.bot.onCallback("like:*", async (ctx) => {
    await ctx.answer("Thanks for the like!");
  });
};

Prerequisites

The Bot SDK is only available to plugins that declare bot capabilities in their manifest. If your manifest does not include bot: { inline: true }, then sdk.bot will be null.

Required manifest declaration
export const manifest: PluginManifest = {
  name: "my-plugin",
  version: "1.0.0",
  description: "Plugin with bot capabilities",
  bot: {
    inline: true,     // Enable inline query handling
    callbacks: true,  // Enable callback query handling
    rateLimits: {
      inlinePerMinute: 30,   // Default: 30
      callbackPerMinute: 60, // Default: 60
    },
  },
};

Register handlers in your start() hook, not in tools(). The bot handlers are event-driven and persist for the plugin's lifetime.

Properties

PropertyTypeDescription
isAvailablebooleanWhether the bot client is connected and ready. Always check before using other methods.
usernamestringThe bot's Telegram username (e.g. "my_bot"). Used to construct inline links like @my_bot query.

Methods

onInlineQuery(handler)

Register a handler for inline queries. When a user types @yourbot query in any chat, this handler is called with the query context. Return an array of InlineResult objects to display as results.

ParameterTypeDescription
handler(ctx: InlineQueryContext) => Promise<InlineResult[]>Async function that receives the query context and returns results.
Inline query handler
sdk.bot.onInlineQuery(async (ctx) => {
  // ctx.query  — the search text (prefix already stripped)
  // ctx.queryId — Telegram query ID
  // ctx.userId — user who triggered the query
  // ctx.offset — pagination offset string

  const results = await searchDatabase(ctx.query);

  return results.map((item, i) => ({
    id: String(i),
    type: "article" as const,
    title: item.name,
    description: item.summary,
    content: { text: item.fullText, parseMode: "HTML" as const },
    keyboard: [[
      { text: "View Details", callback: `view:${item.id}`, style: "primary" },
    ]],
  }));
});

onCallback(pattern, handler)

Register a handler for callback queries matching a glob pattern. Patterns use * for wildcard matching. The callback data is automatically stripped of the plugin namespace prefix before matching.

ParameterTypeDescription
patternstringGlob pattern to match (e.g. "vote:*", "action:*:confirm").
handler(ctx: CallbackContext) => Promise<void>Async function called when a matching callback is received.
Callback handler with glob pattern
sdk.bot.onCallback("vote:*", async (ctx) => {
  // ctx.data  — raw callback data (prefix stripped)
  // ctx.match — regex match groups from the pattern
  // ctx.userId — user who clicked
  // ctx.username — optional username
  // ctx.inlineMessageId — if from an inline message
  // ctx.chatId — if from a regular message
  // ctx.messageId — if from a regular message

  const choice = ctx.data.split(":")[1]; // e.g. "yes" from "vote:yes"

  await ctx.answer(`You voted: ${choice}`);
  await ctx.editMessage(`Vote recorded: ${choice}`, {
    keyboard: [[
      { text: "Vote recorded", callback: "noop", style: "success" },
    ]],
  });
});

onChosenResult(handler)

Register a handler for when a user selects an inline result. Requires inline feedback to be enabled via @BotFather (/setinlinefeedback).

ParameterTypeDescription
handler(ctx: ChosenResultContext) => Promise<void>Async function called when a user picks an inline result.
Chosen result tracking
sdk.bot.onChosenResult(async (ctx) => {
  // ctx.resultId — the ID of the chosen result
  // ctx.inlineMessageId — for editing the sent message later
  // ctx.query — the original query text

  sdk.log.info(`User chose result ${ctx.resultId} for query "${ctx.query}"`);

  // You can edit the inline message after the user selects it
  if (ctx.inlineMessageId) {
    await sdk.bot.editInlineMessage(ctx.inlineMessageId, "Thanks for choosing this result!");
  }
});

editInlineMessage(inlineMessageId, text, opts?)

Edit a message that was sent via inline mode. You need the inlineMessageId from a callback or chosen result context.

ParameterTypeDescription
inlineMessageIdstringThe inline message ID to edit.
textstringNew message text.
opts{ keyboard?: ButtonDef[][]; parseMode?: string }Optional keyboard and parse mode.
Edit inline message
await sdk.bot.editInlineMessage(ctx.inlineMessageId, "Updated content!", {
  parseMode: "HTML",
  keyboard: [[
    { text: "Refresh", callback: "refresh:123", style: "primary" },
    { text: "Close", callback: "close:123", style: "danger" },
  ]],
});

keyboard(rows)

Build a keyboard object from button definitions. Returns a BotKeyboard with methods for both GramJS (MTProto, with colors) and Grammy (Bot API, no colors) formats. Callback data is automatically prefixed with the plugin name.

ParameterTypeDescription
rowsButtonDef[][]Array of rows, each containing button definitions.
ReturnsDescription
BotKeyboardObject with .toTL(), .toGrammy(), and .rows properties.
Building a keyboard
const kb = sdk.bot.keyboard([
  [
    { text: "Confirm", callback: "confirm:42", style: "success" },
    { text: "Cancel", callback: "cancel:42", style: "danger" },
  ],
  [
    { text: "Visit Website", url: "https://example.com" },
  ],
]);

// For GramJS (MTProto) — supports colored buttons
const tlMarkup = kb.toTL();

// For Grammy (Bot API) — standard buttons, no colors
const grammyKeyboard = kb.toGrammy();

// Access raw rows (callbacks already prefixed)
console.log(kb.rows);

Styled Buttons

The Bot SDK supports colored inline keyboard buttons via GramJS Layer 222. Buttons are defined using the ButtonDef interface:

ButtonDef

PropertyTypeRequiredDescription
textstringYesButton label text displayed to the user.
callbackstringNoCallback data sent when pressed. Auto-prefixed with plugin name.
urlstringNoURL to open when pressed.
copystringNoText to copy to clipboard when pressed.
styleButtonStyleNoButton color: "success" (green), "danger" (red), or "primary" (blue).

Button Styles

StyleColorUse Case
"success"GreenConfirmations, positive actions, "yes" votes
"danger"RedDeletions, cancellations, "no" votes, warnings
"primary"BluePrimary actions, navigation, neutral selections

Rendering Formats

Use .toTL() for GramJS Layer 222 (MTProto) which renders colored buttons natively. Use .toGrammy() for the standard Bot API fallback where colors are silently ignored.

Styled button examples
// Green confirmation + red cancel
const confirmKeyboard = sdk.bot.keyboard([
  [
    { text: "Confirm Payment", callback: "pay:confirm", style: "success" },
    { text: "Cancel", callback: "pay:cancel", style: "danger" },
  ],
]);

// Blue navigation row + copy button
const navKeyboard = sdk.bot.keyboard([
  [
    { text: "Previous", callback: "page:prev", style: "primary" },
    { text: "Next", callback: "page:next", style: "primary" },
  ],
  [
    { text: "Copy Address", copy: "EQDrjaLahLkMB-hMCmkzOyBuHJ186F-g..." },
  ],
]);

Gotcha: Callback data is automatically prefixed with the plugin name to prevent collisions between plugins. If your plugin is named "dice-game" and you set callback: "roll:6", the actual callback data sent to Telegram is "dice-game:roll:6". The prefix is stripped back before your handler receives it, so your handler always sees "roll:6".

Type Definitions

BotManifest
interface BotManifest {
  /** Enable inline query handling */
  inline?: boolean;
  /** Enable callback query handling */
  callbacks?: boolean;
  /** Rate limits */
  rateLimits?: {
    /** Max inline answers per minute (default: 30) */
    inlinePerMinute?: number;
    /** Max callback answers per minute (default: 60) */
    callbackPerMinute?: number;
  };
}
ButtonDef & ButtonStyle
type ButtonStyle = "success" | "danger" | "primary";

interface ButtonDef {
  text: string;
  callback?: string;
  url?: string;
  copy?: string;
  style?: ButtonStyle;
}
InlineResult & InlineResultContent
type InlineResultContent =
  | { text: string; parseMode?: "HTML" | "Markdown" }
  | { photoUrl: string; thumbUrl?: string; caption?: string }
  | { gifUrl: string; thumbUrl?: string; caption?: string };

interface InlineResult {
  id: string;
  type: "article" | "photo" | "gif";
  title: string;
  description?: string;
  thumbUrl?: string;
  content: InlineResultContent;
  keyboard?: ButtonDef[][];
}
InlineQueryContext
interface InlineQueryContext {
  /** The query text (prefix already stripped) */
  query: string;
  /** Telegram query ID */
  queryId: string;
  /** User who triggered the query */
  userId: number;
  /** Pagination offset */
  offset: string;
}
CallbackContext
interface CallbackContext {
  /** Raw callback data (prefix already stripped) */
  data: string;
  /** Regex match groups (if pattern was used) */
  match: string[];
  /** User who clicked */
  userId: number;
  /** Username of the user */
  username?: string;
  /** Inline message ID (if from inline message) */
  inlineMessageId?: string;
  /** Chat ID (if from regular message) */
  chatId?: string;
  /** Message ID (if from regular message) */
  messageId?: number;
  /** Answer the callback query (toast/alert) */
  answer(text?: string, alert?: boolean): Promise<void>;
  /** Edit the message that contains the button */
  editMessage(text: string, opts?: { keyboard?: ButtonDef[][]; parseMode?: string }): Promise<void>;
}
ChosenResultContext
interface ChosenResultContext {
  /** The result ID that was chosen */
  resultId: string;
  /** Inline message ID (available if bot has inline feedback enabled) */
  inlineMessageId?: string;
  /** The query that was used */
  query: string;
}
BotKeyboard
interface BotKeyboard {
  /** Get Grammy InlineKeyboard (Bot API, no colors) */
  toGrammy(): unknown;
  /** Get GramJS TL ReplyInlineMarkup (MTProto, with colors) */
  toTL(): unknown;
  /** Raw button definitions (with prefixed callbacks) */
  rows: ButtonDef[][];
}
BotSDK
interface BotSDK {
  readonly isAvailable: boolean;
  readonly username: string;
  onInlineQuery(handler: (ctx: InlineQueryContext) => Promise<InlineResult[]>): void;
  onCallback(pattern: string, handler: (ctx: CallbackContext) => Promise<void>): void;
  onChosenResult(handler: (ctx: ChosenResultContext) => Promise<void>): void;
  editInlineMessage(
    inlineMessageId: string,
    text: string,
    opts?: { keyboard?: ButtonDef[][]; parseMode?: string }
  ): Promise<void>;
  keyboard(rows: ButtonDef[][]): BotKeyboard;
}

Rate Limiting

The Bot SDK enforces per-plugin rate limits to prevent abuse. Configure limits in your manifest's bot.rateLimits:

LimitDefaultDescription
inlinePerMinute30Maximum inline query responses per minute.
callbackPerMinute60Maximum callback query responses per minute.

When a limit is exceeded, the handler is silently skipped. The user sees no response (inline results disappear, callback gets no answer). Set higher limits for high-traffic bots, but be mindful of Telegram's own API rate limits.

Complete Example: Dice Game Bot

A fully functional inline dice game where users roll dice via inline mode, see results with colored buttons, and can reroll.

dice-game/index.ts
import type { PluginSDK, SimpleToolDef, PluginManifest } from "@teleton-agent/sdk";

export const manifest: PluginManifest = {
  name: "dice-game",
  version: "1.0.0",
  description: "Inline dice game with styled buttons",
  bot: {
    inline: true,
    callbacks: true,
    rateLimits: { inlinePerMinute: 60, callbackPerMinute: 120 },
  },
};

function rollDice(): number {
  return Math.floor(Math.random() * 6) + 1;
}

function diceEmoji(n: number): string {
  return ["\u2680", "\u2681", "\u2682", "\u2683", "\u2684", "\u2685"][n - 1];
}

export const start = async (sdk: PluginSDK) => {
  if (!sdk.bot) {
    sdk.log.warn("Bot SDK not available, skipping inline setup");
    return;
  }

  // Handle inline queries — show a "Roll Dice" option
  sdk.bot.onInlineQuery(async (ctx) => {
    const sides = parseInt(ctx.query) || 6;
    const clamped = Math.min(Math.max(sides, 2), 20);

    return [{
      id: `roll-${clamped}`,
      type: "article",
      title: `Roll a d${clamped}`,
      description: `Roll a ${clamped}-sided die and see the result`,
      content: { text: `Rolling a d${clamped}...` },
      keyboard: [[
        { text: `Roll d${clamped}`, callback: `roll:${clamped}`, style: "primary" },
      ]],
    }];
  });

  // Handle the Roll button press
  sdk.bot.onCallback("roll:*", async (ctx) => {
    const sides = parseInt(ctx.data.split(":")[1]) || 6;
    const result = Math.floor(Math.random() * sides) + 1;
    const emoji = sides === 6 ? ` ${diceEmoji(result)}` : "";

    await ctx.editMessage(`You rolled a d${sides}: **${result}**${emoji}`, {
      parseMode: "Markdown",
      keyboard: [
        [
          { text: `Roll again (d${sides})`, callback: `roll:${sides}`, style: "success" },
          { text: "Change dice", callback: "pick", style: "primary" },
        ],
        [
          { text: `Share result: ${result}`, copy: `I rolled a ${result} on a d${sides}!` },
        ],
      ],
    });

    await ctx.answer(`You rolled ${result}!`);
  });

  // Handle dice picker
  sdk.bot.onCallback("pick", async (ctx) => {
    await ctx.editMessage("Choose your dice:", {
      keyboard: [
        [
          { text: "d4", callback: "roll:4", style: "primary" },
          { text: "d6", callback: "roll:6", style: "primary" },
          { text: "d8", callback: "roll:8", style: "primary" },
        ],
        [
          { text: "d10", callback: "roll:10", style: "primary" },
          { text: "d12", callback: "roll:12", style: "primary" },
          { text: "d20", callback: "roll:20", style: "success" },
        ],
      ],
    });
    await ctx.answer();
  });

  // Track which results users select
  sdk.bot.onChosenResult(async (ctx) => {
    sdk.log.info(`User chose "${ctx.resultId}" from query "${ctx.query}"`);
  });

  sdk.log.info(`Dice game inline bot ready: @${sdk.bot.username}`);
};

// Optionally export agent tools alongside the inline bot
export const tools = (sdk: PluginSDK): SimpleToolDef[] => [
  {
    name: "dice_roll",
    description: "Roll dice (for use in chat without inline mode)",
    parameters: {
      type: "object",
      properties: {
        sides: { type: "number", description: "Number of sides (default: 6)" },
      },
    },
    async execute(params) {
      const sides = params.sides || 6;
      const result = Math.floor(Math.random() * sides) + 1;
      return { result, sides, display: `Rolled d${sides}: ${result}` };
    },
  },
];