@teleton-agent/sdk

July 12, 2026 · View on GitHub

Plugin SDK for Teleton Agent — TypeScript types and utilities for building plugins that interact with Telegram and the TON blockchain.

npm license TypeScript


Install

npm install @teleton-agent/sdk@^2

The package ships type definitions and the PluginSDKError class. It has an optional peer dependency on better-sqlite3 (used only if your plugin needs a database).

Quick Start

A Teleton plugin is a module that exports a tools function and, optionally, manifest, start, and migrate.

import type { PluginSDK, SimpleToolDef, PluginManifest } from "@teleton-agent/sdk";

export const manifest: PluginManifest = {
  name: "greeting",
  version: "1.0.0",
  description: "Sends a greeting with the bot's TON balance",
};

export const tools = (sdk: PluginSDK): SimpleToolDef[] => [
  {
    name: "greeting_hello",
    description: "Greet the user and show the bot wallet balance",
    parameters: {
      type: "object",
      properties: {
        name: { type: "string", description: "User's name" },
      },
      required: ["name"],
    },
    async execute(params, context) {
      const balance = await sdk.ton.getBalance();
      const text = `Hello ${params.name}! Bot balance: ${balance?.balance ?? "unknown"} TON`;
      await sdk.telegram.sendMessage(String(context.chatId), text);
      return { success: true, data: { greeting: text } };
    },
  },
];

Place the compiled plugin at ~/.teleton/plugins/<name>/index.js and register it in config.yaml:

plugins:
  greeting:
    enabled: true

Plugin Lifecycle

The core platform loads plugins in a defined order. Each export is optional except tools.

ExportSignatureWhen CalledPurpose
manifestPluginManifestLoad timeDeclares name, version, dependencies, default config
migrate(db: Database) => voidBefore tools, onceCreate/alter tables in the plugin's isolated SQLite DB
toolsSimpleToolDef[] | (sdk: PluginSDK) => SimpleToolDef[]After migrateRegister tools the LLM can invoke
start(ctx) => Promise<void>After bridge connectsRun background tasks, set up intervals
stop() => Promise<void>On shutdown / hot-reloadCleanup timers, close connections
onMessage(event: PluginMessageEvent) => Promise<void>Every incoming messageReact to messages without LLM involvement
onCallbackQuery(event: PluginCallbackEvent) => Promise<void>Inline button pressHandle callback queries from inline keyboards

The tools export can be either a static array or a factory function receiving the SDK. The start function receives a context object with sdk, db, config, pluginConfig, and log. The SDK object passed to plugins is frozen -- plugins cannot modify or extend it. Each plugin receives its own isolated database and a sanitized config object with no API keys.

Event Hooks

Plugins can export onMessage and onCallbackQuery to react to Telegram events directly, without going through the LLM agentic loop. These hooks are fire-and-forget — errors are caught per plugin and logged, so a failing hook never blocks message processing or other plugins.

onMessage

Called for every incoming message (DMs and groups), after the message is stored to the feed database. This fires regardless of whether the agent will respond to the message.

import type { PluginMessageEvent } from "@teleton-agent/sdk";

export async function onMessage(event: PluginMessageEvent) {
  // Auto-moderation example: delete messages containing banned words
  if (event.isGroup && /spam|scam/i.test(event.text)) {
    console.log(`Flagged message ${event.messageId} from ${event.senderId}`);
  }
}

onCallbackQuery

Called when a user presses an inline keyboard button. The data string is split on : into action (first segment) and params (remaining segments). You must call event.answer() to dismiss the loading spinner on the user's client.

import type { PluginCallbackEvent } from "@teleton-agent/sdk";

export async function onCallbackQuery(event: PluginCallbackEvent) {
  // Button data format: "myplugin:action:param1:param2"
  if (event.action !== "myplugin") return; // Not for this plugin

  const [subAction, ...args] = event.params;

  if (subAction === "confirm") {
    await event.answer("Confirmed!", false); // Toast notification
    // ... handle the confirmation
  } else {
    await event.answer("Unknown action", true); // Alert popup
  }
}

Tip: Namespace your callback data with your plugin name (e.g. "casino:bet:100") so multiple plugins can coexist without action collisions. All registered onCallbackQuery hooks receive every callback event — filter by event.action to handle only your own buttons.

API Reference

Core

PluginSDK

Root SDK object passed to plugin functions.

PropertyTypeDescription
versionstringSDK version (semver)
tonTonSDKTON blockchain operations
telegramTelegramSDKTelegram messaging and user operations
secretsSecretsSDKSecure access to plugin secrets (API keys, tokens)
storageStorageSDK | nullSimple key-value storage (null if no DB)
dbDatabase | nullIsolated SQLite database (null only if database initialization failed)
configRecord<string, unknown>Sanitized app config (no secrets)
pluginConfigRecord<string, unknown>Plugin-specific config from config.yaml
logPluginLoggerPrefixed logger
botBotSDK | nullBot inline mode SDK (null if not configured — see Bot SDK)

PluginLogger

All methods auto-prefix output with the plugin name.

MethodDescription
info(...args)Informational message
warn(...args)Warning
error(...args)Error
debug(...args)Debug (visible only when DEBUG or VERBOSE is set)

PluginSDKError

import { PluginSDKError } from "@teleton-agent/sdk";

Extends Error with a code property for programmatic handling.

PropertyTypeDescription
name"PluginSDKError"Always "PluginSDKError"
codeSDKErrorCodeMachine-readable error code
messagestringHuman-readable description

SDKErrorCode

type SDKErrorCode =
  | "BRIDGE_NOT_CONNECTED"   // Telegram bridge not ready
  | "WALLET_NOT_INITIALIZED" // TON wallet not configured
  | "INVALID_ADDRESS"        // Malformed TON address
  | "INVALID_INPUT"          // Invalid SDK method argument
  | "NOT_AVAILABLE"          // Capability unavailable in this runtime mode
  | "RATE_LIMITED"           // Platform rate limit reached
  | "TRANSACTION_FAILED"     // Blockchain transaction failed
  | "PERMISSION_DENIED"      // Operation is not authorized
  | "SECRET_NOT_FOUND"       // Required secret not configured
  | "OPERATION_FAILED";      // Generic failure

SDK_VERSION

import { SDK_VERSION } from "@teleton-agent/sdk";
// "2.1.0"

TON

TonSDK

MethodReturnsDescription
getAddress()string | nullBot's wallet address
getBalance(address?)Promise<TonBalance | null>Balance for an address (defaults to bot)
getPrice()Promise<TonPrice | null>Current TON/USD price (cached 30s)
sendTON(to, amount, comment?)Promise<TonSendResult>Send TON (irreversible)
getTransactions(address, limit?)Promise<TonTransaction[]>Transaction history (max 50)
verifyPayment(params)Promise<SDKPaymentVerification>Verify incoming payment with replay protection
getJettonBalances(ownerAddress?)Promise<JettonBalance[]>Jetton balances (defaults to bot wallet)
getJettonInfo(jettonAddress)Promise<JettonInfo | null>Jetton metadata (name, symbol, decimals)
sendJetton(jettonAddress, to, amount, opts?)Promise<JettonSendResult>Transfer jetton tokens (irreversible)
getJettonWalletAddress(ownerAddress, jettonAddress)Promise<string | null>Get jetton wallet address for owner
getNftItems(ownerAddress?)Promise<NftItem[]>NFTs owned by address (defaults to bot)
getNftInfo(nftAddress)Promise<NftItem | null>NFT item metadata
toNano(amount)bigintConvert TON to nanoTON
fromNano(nano)stringConvert nanoTON to TON string
validateAddress(address)booleanValidate TON address format
getJettonPrice(jettonAddress)Promise<JettonPrice | null>Jetton USD/TON price with 24h/7d/30d changes
getJettonHolders(jettonAddress, limit?)Promise<JettonHolder[]>Top holders ranked by balance (max 100)
getJettonHistory(jettonAddress)Promise<JettonHistory | null>Market analytics: volume, FDV, market cap
dexDexSDKDEX quotes and swaps (STON.fi + DeDust)
dnsDnsSDK.ton domain management and auctions
highloadHighloadSDKCore-managed Highload Wallet v3 batches

TonBalance

FieldTypeDescription
balancestringHuman-readable (e.g. "12.50")
balanceNanostringBalance in nanoTON

TonPrice

FieldTypeDescription
usdnumberPrice in USD
sourcestring"TonAPI" or "CoinGecko"
timestampnumberFetch time (ms since epoch)

TonSendResult

FieldTypeDescription
txRefstringReference: seqno_timestamp_amount
amountnumberAmount sent in TON

TonTransaction

FieldTypeDescription
typeTransactionTypeTransaction type
hashstringBlockchain tx hash (hex)
amountstring?e.g. "1.5 TON"
fromstring?Sender address
tostring?Recipient address
commentstring | null?Transaction memo
datestringISO 8601 date
secondsAgonumberAge in seconds
explorerstringTonviewer link
jettonAmountstring?Raw jetton amount
jettonWalletstring?Jetton wallet address
nftAddressstring?NFT address
transfersTonTransaction[]?Sub-transfers (for multi_send)

TransactionType

type TransactionType =
  | "ton_received" | "ton_sent"
  | "jetton_received" | "jetton_sent"
  | "nft_received" | "nft_sent"
  | "gas_refund" | "bounce"
  | "contract_call" | "multi_send";

JettonBalance

FieldTypeDescription
jettonAddressstringJetton master contract address
walletAddressstringOwner's jetton wallet address
balancestringRaw balance (string to avoid precision loss)
balanceFormattedstringHuman-readable (e.g. "100.50")
symbolstringToken ticker (e.g. "USDT")
namestringToken name (e.g. "Tether USD")
decimalsnumberToken decimals (e.g. 6 for USDT)
verifiedbooleanWhether verified on TonAPI
usdPricenumber?USD price per token (if available)

JettonInfo

FieldTypeDescription
addressstringJetton master contract address
namestringToken name
symbolstringToken ticker
decimalsnumberToken decimals
totalSupplystringTotal supply in raw units
holdersCountnumberNumber of unique holders
verifiedbooleanWhether verified on TonAPI
descriptionstring?Token description
imagestring?Token image URL

JettonSendResult

FieldTypeDescription
successbooleanWhether transaction was sent
seqnonumberWallet sequence number used

NftItem

FieldTypeDescription
addressstringNFT item contract address
indexnumberIndex within collection
ownerAddressstring?Current owner address
collectionAddressstring?Collection contract address
collectionNamestring?Collection name
namestring?NFT name
descriptionstring?NFT description
imagestring?NFT image URL
verifiedbooleanWhether verified

SDKVerifyPaymentParams

FieldTypeDescription
amountnumberExpected amount in TON
memostringExpected comment (e.g. username)
gameTypestringReplay protection group
maxAgeMinutesnumber?Time window (default: 10)

SDKPaymentVerification

FieldTypeDescription
verifiedbooleanWhether payment was found and valid
txHashstring?Transaction hash (replay protection)
amountnumber?Verified amount
playerWalletstring?Sender wallet (for payouts)
datestring?ISO 8601 date
secondsAgonumber?Age in seconds
errorstring?Failure reason

JettonPrice

FieldTypeDescription
priceUSDnumber | nullPrice in USD
priceTONnumber | nullPrice in TON
change24hstring | null24h change (e.g. "-2.5%")
change7dstring | null7d change
change30dstring | null30d change

JettonHolder

FieldTypeDescription
ranknumberRank (1 = top holder)
addressstringHolder's TON address
namestring | nullKnown name (e.g. "Binance")
balancestringFormatted balance (e.g. "1,234.56")
balanceRawstringRaw balance in smallest units

JettonHistory

FieldTypeDescription
symbolstringToken symbol
namestringToken name
currentPricestringPrice in USD
currentPriceTONstringPrice in TON
changes{ "24h", "7d", "30d" }Price change percentages
volume24hstring24h trading volume (USD)
fdvstringFully diluted valuation
marketCapstringMarket cap
holdersnumberNumber of holders

DEX — sdk.ton.dex

Dual DEX aggregator supporting STON.fi and DeDust. Compares quotes in parallel and recommends the best execution.

DexSDK

MethodReturnsDescription
quote(params)Promise<DexQuoteResult>Compare quotes from both DEXes
quoteSTONfi(params)Promise<DexSingleQuote | null>Quote from STON.fi only
quoteDeDust(params)Promise<DexSingleQuote | null>Quote from DeDust only
swap(params)Promise<DexSwapResult>Swap via best DEX (or forced)
swapSTONfi(params)Promise<DexSwapResult>Swap on STON.fi
swapDeDust(params)Promise<DexSwapResult>Swap on DeDust
// Get best quote for swapping 10 TON → USDT
const usdt = "EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs";
const quote = await sdk.ton.dex.quote({
  fromAsset: "ton",
  toAsset: usdt,
  amount: 10,
  slippage: 0.01, // 1%
});
console.log(`Best: ${quote.recommended} → ${quote.stonfi?.expectedOutput ?? "N/A"} USDT`);

// Execute the swap
const result = await sdk.ton.dex.swap({ fromAsset: "ton", toAsset: usdt, amount: 10 });

DexQuoteParams

FieldTypeDescription
fromAssetstring"ton" or jetton master address
toAssetstring"ton" or jetton master address
amountnumberAmount in human-readable units
slippagenumber?Tolerance (0.01 = 1%, default: 0.01)

DexQuoteResult

FieldTypeDescription
stonfiDexSingleQuote | nullSTON.fi quote
dedustDexSingleQuote | nullDeDust quote
recommended"stonfi" | "dedust"Best DEX for this trade
savingsstringSavings vs the other DEX

DexSingleQuote

FieldTypeDescription
dex"stonfi" | "dedust"DEX name
expectedOutputstringExpected output amount
minOutputstringMinimum after slippage
ratestringExchange rate
priceImpactstring?Price impact percentage
feestringFee amount
poolTypestring?Pool type (DeDust: "volatile" or "stable")

DexSwapParams

Extends DexQuoteParams with:

FieldTypeDescription
dex"stonfi" | "dedust"?Force a specific DEX (omit for auto)

DexSwapResult

FieldTypeDescription
dex"stonfi" | "dedust"DEX used
fromAssetstringSource asset
toAssetstringDestination asset
amountInstringAmount sent
expectedOutputstringExpected output
minOutputstringMinimum after slippage
slippagestringSlippage used

Highload Wallet v3 — sdk.ton.highload

MethodReturnsDescription
getInfo()Promise<HighloadWalletInfo>Address, balance, deployment, query sequence, and on-chain settings
fund(amount)Promise<TonTransferResult>Fund the derived Highload wallet from the main wallet
sendMessages(messages, opts?)Promise<HighloadBatchResult>Submit 1–254 messages with core-managed signing and query IDs

The Highload address uses the standard subwallet ID 0x10ad, preserving compatibility with existing Multisend deposits. Query IDs are advanced in private core state before broadcast to prevent cross-plugin collisions and replay after a process crash.

DNS — sdk.ton.dns

Manage .ton domains: check availability, resolve addresses, participate in auctions, and link domains to wallets.

DnsSDK

MethodReturnsDescription
check(domain)Promise<DnsCheckResult>Check availability, owner, auction status
resolve(domain)Promise<DnsResolveResult | null>Resolve domain to wallet address
getAuctions(limit?)Promise<DnsAuction[]>List active auctions
startAuction(domain)Promise<DnsAuctionResult>Start auction for an available domain
bid(domain, amount)Promise<DnsBidResult>Place bid on active auction
link(domain, address)Promise<void>Link domain to wallet address
unlink(domain)Promise<void>Remove wallet link
setSiteRecord(domain, adnlAddress)Promise<void>Set TON Site (ADNL) record on a domain
// Check if a domain is available
const info = await sdk.ton.dns.check("mybot.ton");
if (info.available) {
  const result = await sdk.ton.dns.startAuction("mybot.ton");
  sdk.log.info(`Auction started for ${result.domain}`);
} else {
  sdk.log.info(`Domain owned by ${info.owner}`);
}

// Resolve a domain
const resolved = await sdk.ton.dns.resolve("alice.ton");
if (resolved) {
  await sdk.ton.sendTON(resolved.walletAddress!, 1, "Hello from plugin");
}

// Set a TON Site ADNL record
await sdk.ton.dns.setSiteRecord("mysite.ton", "aabbccdd...64hex");

DnsCheckResult

FieldTypeDescription
domainstringDomain name (e.g. "example.ton")
availablebooleanWhether the domain is available
ownerstring?Current owner address
nftAddressstring?NFT address of the domain
walletAddressstring?Linked wallet address
auctionobject?Active auction: { bids, lastBid, endTime } (reserved, not yet populated)

DnsResolveResult

FieldTypeDescription
domainstringDomain name
walletAddressstring | nullLinked wallet address
nftAddressstringNFT address of the domain
ownerstring | nullOwner address
expirationDatenumber?Expiration (unix timestamp)

DnsAuction

FieldTypeDescription
domainstringDomain name
nftAddressstringNFT address
ownerstringCurrent highest bidder
lastBidstringHighest bid in TON
endTimenumberAuction end (unix timestamp)
bidsnumberNumber of bids

DnsAuctionResult

FieldTypeDescription
domainstringDomain name
successbooleanWhether auction started
bidAmountstringInitial bid in TON

DnsBidResult

FieldTypeDescription
domainstringDomain name
bidAmountstringBid amount in TON
successbooleanWhether bid was placed

Telegram

TelegramSDK

Core

MethodReturnsDescription
getMode()"user" | "bot"Active Telegram runtime mode
sendMessage(chatId, text, opts?)Promise<number>Send message, returns message ID
editMessage(chatId, messageId, text, opts?)Promise<number>Edit existing message
sendDice(chatId, emoticon, replyToId?)Promise<DiceResult>Send dice/slot animation
sendReaction(chatId, messageId, emoji)Promise<void>React to a message
getMessages(chatId, limit?)Promise<SimpleMessage[]>Fetch recent messages (default 50)
sendInlineBotResult(chatId, botUsername, query, index?)Promise<InlineBotResult>Query a third-party inline bot and send one result (user mode only)
getMe()TelegramUser | nullBot's user info
isAvailable()booleanWhether the bridge is connected

Messages

MethodReturnsDescription
deleteMessage(chatId, messageId, revoke?)Promise<void>Delete a message
forwardMessage(from, to, messageId)Promise<number | null>Forward message to another chat
pinMessage(chatId, messageId, opts?)Promise<void>Pin/unpin a message
searchMessages(chatId, query, limit?)Promise<SimpleMessage[]>Search messages in a chat
scheduleMessage(chatId, text, scheduleDate)Promise<number | null>Schedule message for later
getScheduledMessages(chatId)Promise<SimpleMessage[]>Get scheduled messages in a chat
deleteScheduledMessage(chatId, messageId)Promise<void>Delete a scheduled message
sendScheduledNow(chatId, messageId)Promise<void>Send a scheduled message immediately
getReplies(chatId, messageId, limit?)Promise<SimpleMessage[]>Get thread replies

Media

MethodReturnsDescription
sendPhoto(chatId, photo, opts?)Promise<number>Send a photo
sendVideo(chatId, video, opts?)Promise<number>Send a video
sendVoice(chatId, voice, opts?)Promise<number>Send a voice message
sendFile(chatId, file, opts?)Promise<number>Send a document/file
sendGif(chatId, gif, opts?)Promise<number>Send an animated GIF
sendSticker(chatId, sticker)Promise<number>Send a sticker
downloadMedia(chatId, messageId)Promise<Buffer | null>Download media (max 50MB)

Chat & Users

MethodReturnsDescription
getChatInfo(chatId)Promise<ChatInfo | null>Get chat/group/channel info
getUserInfo(userId)Promise<UserInfo | null>Get user information
resolveUsername(username)Promise<ResolvedPeer | null>Resolve @username to peer
getParticipants(chatId, limit?)Promise<UserInfo[]>Get group/channel members
getDialogs(limit?)Promise<Dialog[]>Get all conversations (max 100)
getHistory(chatId, limit?)Promise<SimpleMessage[]>Get message history (max 100)

Interactive

MethodReturnsDescription
createPoll(chatId, question, answers, opts?)Promise<number | null>Create a poll
createQuiz(chatId, question, answers, correctIndex, explanation?)Promise<number | null>Create a quiz

Moderation

MethodReturnsDescription
banUser(chatId, userId)Promise<void>Ban user from group
unbanUser(chatId, userId)Promise<void>Unban user
muteUser(chatId, userId, untilDate)Promise<void>Mute user (0 = forever)
kickUser(chatId, userId)Promise<void>Kick user (ban + immediate unban)

Stars & Gifts

MethodReturnsDescription
getStarsBalance()Promise<number>Get Telegram Stars balance
sendGift(userId, giftId, opts?)Promise<void>Send a star gift
getAvailableGifts()Promise<StarGift[]>Get gift catalog
getMyGifts(limit?)Promise<ReceivedGift[]>Get received gifts
getResaleGifts(giftId, limit?)Promise<StarGift[]>Get resale gifts from a collection
buyResaleGift(giftId)Promise<void>Buy a resale gift
getStarsTransactions(limit?)Promise<StarsTransaction[]>Stars transaction history
transferCollectible(msgId, toUserId)Promise<TransferResult>Transfer a collectible gift
setCollectiblePrice(msgId, price)Promise<void>Set/remove resale price (0 = unlist)
getCollectibleInfo(slug)Promise<CollectibleInfo | null>Fragment collectible info
getUniqueGift(slug)Promise<UniqueGift | null>NFT gift details by slug
getUniqueGiftValue(slug)Promise<GiftValue | null>NFT gift market valuation
sendGiftOffer(userId, giftSlug, price, opts?)Promise<void>Make buy offer on an NFT gift
// Gift marketplace flow: browse → check value → make offer
const gifts = await sdk.telegram.getMyGifts(10);
for (const gift of gifts) {
  sdk.log.info(`Gift ${gift.id} from user ${gift.fromId}, worth ${gift.starsAmount} stars`);
}

// Check NFT gift value
const value = await sdk.telegram.getUniqueGiftValue("CryptoBot-42");
if (value?.floorPrice) {
  sdk.log.info(`Floor: ${value.floorPrice} ${value.currency}`);
}

// Transfer a collectible
const result = await sdk.telegram.transferCollectible(gift.messageId!, targetUserId);
sdk.log.info(`Transferred to ${result.transferredTo}, paid: ${result.paidTransfer}`);

Stories & Advanced

MethodReturnsDescription
sendStory(mediaPath, opts?)Promise<number | null>Post a story
setTyping(chatId)Promise<void>Show typing indicator

InlineButton

FieldTypeDescription
textstringButton label text
callback_datastringCallback data sent when pressed

SendMessageOptions

FieldTypeDescription
replyToIdnumber?Message ID to reply to
inlineKeyboardInlineButton[][]?Inline keyboard rows

EditMessageOptions

FieldTypeDescription
inlineKeyboardInlineButton[][]?Updated keyboard (omit to keep)

DiceResult

FieldTypeDescription
valuenumberResult value (range depends on emoticon)
messageIdnumberMessage ID of the dice

TelegramUser

FieldTypeDescription
idnumberTelegram user ID
usernamestring?Username (without @)
firstNamestring?First name
isBotbooleanWhether the user is a bot

SimpleMessage

FieldTypeDescription
idnumberMessage ID
textstringMessage text
senderIdnumberSender user ID
senderUsernamestring?Sender username
timestampDateMessage timestamp

ChatInfo

FieldTypeDescription
idstringChat ID
titlestringChat title or user's first name
type"private" | "group" | "supergroup" | "channel"Chat type
membersCountnumber?Number of members
usernamestring?Chat username (if public)
descriptionstring?Chat/channel description

UserInfo

FieldTypeDescription
idnumberTelegram user ID
firstNamestringFirst name
lastNamestring?Last name
usernamestring?Username without @
isBotbooleanWhether the user is a bot

ResolvedPeer

FieldTypeDescription
idnumberEntity ID
type"user" | "chat" | "channel"Entity type
usernamestring?Username if available
titlestring?Title or first name

MediaSendOptions

FieldTypeDescription
captionstring?Media caption text
replyToIdnumber?Message ID to reply to
inlineKeyboardInlineButton[][]?Inline keyboard
durationnumber?Duration in seconds (video/voice)
widthnumber?Width in pixels (video)
heightnumber?Height in pixels (video)

PollOptions

FieldTypeDescription
isAnonymousboolean?Anonymous voters (default: true)
multipleChoiceboolean?Allow multiple answers (default: false)

StarGift

FieldTypeDescription
idstringGift ID
starsAmountnumberCost in Telegram Stars
availableAmountnumber?Remaining available
totalAmountnumber?Total supply

ReceivedGift

FieldTypeDescription
idstringGift ID
fromIdnumber?Sender user ID
datenumberUnix timestamp
starsAmountnumberStars value
savedbooleanWhether saved to profile
messageIdnumber?Associated message ID

Dialog

FieldTypeDescription
idstring | nullChat ID
titlestringChat title or name
type"dm" | "group" | "channel"Chat type
unreadCountnumberUnread messages
unreadMentionsCountnumberUnread mentions
isPinnedbooleanWhether pinned
isArchivedbooleanWhether archived
lastMessageDatenumber | nullLast message (unix timestamp)
lastMessagestring | nullLast message preview

StarsTransaction

FieldTypeDescription
idstringTransaction ID
amountnumberAmount (+received, -spent)
datenumberUnix timestamp
peerstring?Peer info
descriptionstring?Description

TransferResult

FieldTypeDescription
msgIdnumberMessage ID of transferred gift
transferredTostringRecipient identifier
paidTransferbooleanWhether it cost Stars
starsSpentstring?Stars spent (if paid)

CollectibleInfo

FieldTypeDescription
type"username" | "phone"Collectible type
valuestringUsername or phone number
purchaseDatestringISO 8601 date
currencystringFiat currency
amountstring?Fiat amount
cryptoCurrencystring?Crypto currency (e.g. "TON")
cryptoAmountstring?Crypto amount
urlstring?Fragment URL

UniqueGift

FieldTypeDescription
idstringGift ID
giftIdstringCollection gift ID
slugstringURL slug
titlestringGift title
numnumberNumber in collection
ownerobject{ id?, name?, address?, username? }
giftAddressstring?TON address of the NFT
attributesArray[{ type, name, rarityPercent? }]
availabilityobject?{ total, remaining }
nftLinkstringLink to NFT page

GiftValue

FieldTypeDescription
slugstringNFT slug
initialSaleDatestring?First sale (ISO 8601)
initialSaleStarsstring?First sale price in Stars
lastSaleDatestring?Last sale (ISO 8601)
lastSalePricestring?Last sale price
floorPricestring?Floor price
averagePricestring?Average price
listedCountnumber?Number listed
currencystring?Currency

GiftOfferOptions

FieldTypeDescription
durationnumber?Offer validity in seconds (default: 86400, min: 21600)

Secrets

SecretsSDK

Secure access to plugin secrets (API keys, tokens, credentials). Resolution order: declared environment variable override (or derived TELETON_PLUGIN_PLUGINNAME_KEY) > secrets store (/plugin set) > pluginConfig.

MethodReturnsDescription
get(key)string | undefinedGet secret value
require(key)stringGet secret, throws SECRET_NOT_FOUND if missing
has(key)booleanCheck if a secret is configured
const apiKey = sdk.secrets.get("api_key");
if (!apiKey) return { success: false, error: "API key not configured" };

SecretDeclaration

Used in PluginManifest.secrets to declare required secrets.

FieldTypeDescription
requiredbooleanWhether the plugin needs this secret to function
descriptionstringHuman-readable description
envstring?Namespaced environment override starting with TELETON_PLUGIN_PLUGIN_NAME_

Storage

StorageSDK

Simple key-value storage for plugins. Uses an auto-created _kv table in the plugin's isolated DB. No migrate() export needed. Values are JSON-serialized with optional TTL.

MethodReturnsDescription
get<T>(key)T | undefinedGet value (undefined if missing or expired)
set<T>(key, value, opts?)voidSet value (optional { ttl: ms } for expiration)
delete(key)booleanDelete a key (true if existed)
has(key)booleanCheck if key exists and is not expired
clear()voidDelete all keys
// Simple counter
const count = sdk.storage.get<number>("visits") ?? 0;
sdk.storage.set("visits", count + 1);

// Cache with 5-minute TTL
sdk.storage.set("api_result", data, { ttl: 300_000 });

Bot SDK (sdk.bot)

The Bot SDK enables plugins to handle Telegram inline queries and button callbacks. It is lazy-loadedsdk.bot is null unless the plugin declares bot capabilities in its manifest.

To enable the Bot SDK, add a bot field to your manifest:

export const manifest: PluginManifest = {
  name: "my-inline-bot",
  version: "1.0.0",
  bot: {
    inline: true,      // Enable inline query handling
    callbacks: true,    // Enable callback button handling
    rateLimits: {
      inlinePerMinute: 30,   // Default: 30
      callbackPerMinute: 60, // Default: 60
    },
  },
};

BotSDK

Property / MethodReturnsDescription
isAvailablebooleanWhether the bot client is connected (getter)
usernamestringBot's username (getter, empty string if unavailable)
onInlineQuery(handler)voidRegister handler for inline queries
onCallback(pattern, handler)voidRegister handler for button callbacks (glob pattern)
onChosenResult(handler)voidRegister handler for chosen inline results
editInlineMessage(inlineMessageId, text, opts?)Promise<void>Edit an inline message
keyboard(rows)BotKeyboardBuild a keyboard with auto-prefixed callback data

onInlineQuery(handler)

Register a handler for inline queries. The handler receives the query text (with plugin prefix already stripped) and must return an array of InlineResult objects.

ParameterTypeRequiredDescription
handler(ctx: InlineQueryContext) => Promise<InlineResult[]>YesHandler function
sdk.bot.onInlineQuery(async (ctx) => {
  const results = await searchItems(ctx.query);
  return results.map((item) => ({
    id: item.id,
    type: "article",
    title: item.title,
    description: item.description,
    content: { text: item.body, parseMode: "HTML" },
    keyboard: [
      [{ text: "Open", url: item.url }],
      [{ text: "Select", callback: `pick:${item.id}` }],
    ],
  }));
});

onCallback(pattern, handler)

Register a handler for button callback queries. The pattern uses glob syntax and is matched against the callback data (prefix already stripped).

ParameterTypeRequiredDescription
patternstringYesGlob pattern to match callback data (e.g. "pick:*", "menu:*:*")
handler(ctx: CallbackContext) => Promise<void>YesHandler function
sdk.bot.onCallback("pick:*", async (ctx) => {
  const itemId = ctx.match[1]; // Captured from glob
  await ctx.answer(`Selected item ${itemId}`);
  await ctx.editMessage(`You picked: ${itemId}`, {
    keyboard: [[{ text: "Back", callback: "menu" }]],
  });
});

onChosenResult(handler)

Register a handler that fires when a user selects an inline result. Requires inline feedback to be enabled in BotFather.

ParameterTypeRequiredDescription
handler(ctx: ChosenResultContext) => Promise<void>YesHandler function
sdk.bot.onChosenResult(async (ctx) => {
  sdk.log.info(`User chose result ${ctx.resultId} for query "${ctx.query}"`);
  // Track analytics, update state, etc.
});

editInlineMessage(inlineMessageId, text, opts?)

Edit an inline message. Tries GramJS first (supports styled/colored buttons), falls back to Grammy Bot API on error.

ParameterTypeRequiredDescription
inlineMessageIdstringYesInline message ID (from callback context)
textstringYesNew message text (HTML supported)
opts.keyboardButtonDef[][]NoUpdated keyboard (auto-prefixed)
opts.parseModestringNoParse mode: "HTML" (default) or "MarkdownV2"
await sdk.bot.editInlineMessage(ctx.inlineMessageId!, "Updated text", {
  keyboard: [
    [{ text: "Confirm", callback: "confirm", style: "success" }],
    [{ text: "Cancel", callback: "cancel", style: "danger" }],
  ],
});

keyboard(rows)

Build an inline keyboard with auto-prefixed callback data. Returns a BotKeyboard object with dual output formats.

ParameterTypeRequiredDescription
rowsButtonDef[][]YesArray of button rows
const kb = sdk.bot.keyboard([
  [
    { text: "Buy", callback: "buy:123", style: "success" },
    { text: "Sell", callback: "sell:123", style: "danger" },
  ],
  [{ text: "Details", url: "https://example.com" }],
]);

// Use with GramJS (styled colors)
const tlMarkup = kb.toTL();

// Use with Grammy Bot API (no colors, but wider compatibility)
const grammyKb = kb.toGrammy();

BotManifest

FieldTypeDescription
inlineboolean?Enable inline query handling
callbacksboolean?Enable callback query handling
rateLimitsobject?{ inlinePerMinute?: number, callbackPerMinute?: number }

BotKeyboard

Field / MethodTypeDescription
rowsButtonDef[][]Raw button definitions (with prefixed callbacks)
toGrammy()unknownGrammy InlineKeyboard (Bot API, no colors)
toTL()unknownGramJS TL ReplyInlineMarkup (MTProto, with colors)

ButtonDef

FieldTypeDescription
textstringButton label text
callbackstring?Callback data (auto-prefixed with plugin name)
urlstring?URL to open when pressed
copystring?Text to copy on click (native copy-to-clipboard)
styleButtonStyle?Button color: "success" (green), "danger" (red), "primary" (blue). GramJS only, graceful fallback on Bot API.

ButtonStyle

type ButtonStyle = "success" | "danger" | "primary";

InlineResult

FieldTypeDescription
idstringUnique result ID
type"article" | "photo" | "gif"Result type
titlestringResult title
descriptionstring?Short description
thumbUrlstring?Thumbnail URL
contentInlineResultContentMessage content to send
keyboardButtonDef[][]?Inline keyboard rows

InlineResultContent

type InlineResultContent =
  | { text: string; parseMode?: "HTML" | "Markdown" }
  | { photoUrl: string; thumbUrl?: string; caption?: string }
  | { gifUrl: string; thumbUrl?: string; caption?: string };

InlineQueryContext

FieldTypeDescription
querystringQuery text (plugin prefix already stripped)
queryIdstringTelegram query ID
userIdnumberUser who triggered the query
offsetstringPagination offset

CallbackContext

Field / MethodTypeDescription
datastringCallback data (prefix already stripped)
matchstring[]Regex match groups from glob pattern
userIdnumberUser who clicked
usernamestring?Username of the user
inlineMessageIdstring?Inline message ID (if from inline message)
chatIdstring?Chat ID (if from regular message)
messageIdnumber?Message ID (if from regular message)
answer(text?, alert?)Promise<void>Answer the callback query (toast or alert)
editMessage(text, opts?)Promise<void>Edit the message containing the button

ChosenResultContext

FieldTypeDescription
resultIdstringThe result ID that was chosen
inlineMessageIdstring?Inline message ID (if bot has inline feedback enabled)
querystringThe query that was used

Complete Inline Bot Example

import type { PluginSDK, SimpleToolDef, PluginManifest } from "@teleton-agent/sdk";

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

export const tools = (sdk: PluginSDK): SimpleToolDef[] => {
  // Set up inline handlers
  sdk.bot!.onInlineQuery(async (ctx) => {
    const query = ctx.query.trim();
    if (!query) return [];
    const price = await sdk.ton.getJettonPrice(query);
    if (!price) return [];
    return [{
      id: query,
      type: "article",
      title: `${query} Price`,
      description: `$${price.priceUSD ?? "N/A"} | 24h: ${price.change24h ?? "N/A"}`,
      content: { text: `<b>${query}</b>\nUSD: $${price.priceUSD}\n24h: ${price.change24h}`, parseMode: "HTML" },
      keyboard: [[
        { text: "Refresh", callback: `refresh:${query}`, style: "primary" },
        { text: "Buy", callback: `buy:${query}`, style: "success" },
      ]],
    }];
  });

  sdk.bot!.onCallback("refresh:*", async (ctx) => {
    const token = ctx.match[1];
    const price = await sdk.ton.getJettonPrice(token);
    await ctx.editMessage(`<b>${token}</b>\nUSD: $${price?.priceUSD}\n24h: ${price?.change24h}`, {
      keyboard: [[
        { text: "Refresh", callback: `refresh:${token}`, style: "primary" },
        { text: "Buy", callback: `buy:${token}`, style: "success" },
      ]],
    });
    await ctx.answer("Price updated!");
  });

  // Regular tools still work alongside inline mode
  return [{
    name: "price_check",
    description: "Check a token's price",
    parameters: {
      type: "object",
      properties: { token: { type: "string", description: "Jetton address" } },
      required: ["token"],
    },
    async execute(params) {
      const price = await sdk.ton.getJettonPrice(params.token);
      return { success: true, data: price };
    },
  }];
};

Plugin Definitions

SimpleToolDef

FieldTypeDescription
namestringUnique tool name (e.g. "casino_spin")
descriptionstringDescription for the LLM
parametersRecord<string, unknown>?JSON Schema for params
execute(params, context) => Promise<ToolResult>Tool handler
scopeToolScope?Visibility scope (default: "always")
categoryToolCategory?Masking category
requiresApprovalboolean?Deprecated compatibility field; ignored by the runtime

PluginManifest

FieldTypeDescription
namestringPlugin name (lowercase, hyphens, 1-64 chars)
versionstringSemver string
authorstring?Author name
descriptionstring?Short description (max 256 chars)
dependenciesstring[]?Required built-in modules
defaultConfigRecord<string, unknown>?Default config values
sdkVersionstring?Required SDK version range (e.g. ">=2.0.0")
secretsRecord<string, SecretDeclaration>?Secrets required by this plugin
botBotManifest?Bot capabilities — enables sdk.bot (see Bot SDK)
hooksPluginHookDeclaration[]?Hooks the plugin is allowed to register

ToolResult

FieldTypeDescription
successbooleanWhether execution succeeded
dataunknown?Result data (serialized for LLM)
errorstring?Error message

ToolScope

type ToolScope =
  | "open"
  | "always"
  | "dm-only"
  | "group-only"
  | "admin-only"
  | "allowlist"
  | "disabled";

ToolCategory

type ToolCategory = "data-bearing" | "action";

data-bearing tool results are subject to observation masking (token reduction on older results). action tool results are always preserved in full.

External action tools require a trusted Telegram identity by default and execute directly once authorized. The legacy requiresApproval field is accepted for manifest compatibility but ignored at runtime.

StartContext

Context passed to the start(ctx) lifecycle hook.

FieldTypeDescription
sdkPluginSDKCapability-scoped SDK for background work
dbDatabase | nullPlugin's isolated SQLite database
configRecord<string, unknown>Sanitized app config (no API keys)
pluginConfigRecord<string, unknown>Plugin-specific config from config.yaml
logPluginLoggerPrefixed logger

PluginToolContext

Runtime context passed to tool execute functions.

FieldTypeDescription
chatIdstringTelegram chat ID where tool was invoked
senderIdnumberTelegram user ID of the sender
isGroupbooleanWhether this is a group chat
dbDatabase | nullPlugin's isolated SQLite database
configRecord<string, unknown>?Sanitized bot config

Event Hook Types

PluginMessageEvent

FieldTypeDescription
chatIdstringTelegram chat ID
senderIdnumberSender's user ID
senderUsernamestring?Sender's @username (without @)
textstringMessage text
isGroupbooleanWhether this is a group chat
hasMediabooleanWhether the message contains media
messageIdnumberMessage ID
timestampDateMessage timestamp

PluginCallbackEvent

FieldTypeDescription
datastringRaw callback data string
actionstringFirst segment of data.split(":")
paramsstring[]Remaining segments after action
chatIdstringChat ID where the button was pressed
messageIdnumberMessage ID the button belongs to
userIdnumberUser ID who pressed the button
answer(text?: string, alert?: boolean) => Promise<void>Answer the callback query (dismisses spinner)

Error Handling

All SDK methods that perform I/O throw PluginSDKError on failure. Use the code property for control flow:

import { PluginSDKError } from "@teleton-agent/sdk";

async execute(params, context) {
  try {
    await sdk.ton.sendTON(params.address, params.amount);
    return { success: true };
  } catch (err) {
    if (err instanceof PluginSDKError) {
      switch (err.code) {
        case "WALLET_NOT_INITIALIZED":
          return { success: false, error: "Bot wallet not configured" };
        case "INVALID_ADDRESS":
          return { success: false, error: "Bad address format" };
        default:
          return { success: false, error: err.message };
      }
    }
    throw err; // Re-throw unexpected errors
  }
}

Always check ctx.sdk.telegram.isAvailable() before calling Telegram methods in start(), since the bridge may not be connected yet.

License

MIT -- see LICENSE.

Copyright 2025-2026 Digital Resistance.