ESC
Start typing to search...

SDK Utilities Reference

Secrets management, key-value storage, and structured logging for Teleton plugins.

sdk.secrets

The SecretsSDK namespace provides secure, read-only access to plugin credentials. Secrets are never exposed in logs or tool output — they are managed separately from application config.

Methods

MethodReturnsDescription
get(key)string | undefinedGet a secret value by key. Returns undefined if not configured.
require(key)stringGet a secret value, throwing PluginSDKError with code SECRET_NOT_FOUND if missing.
has(key)booleanCheck if a secret is configured (without retrieving the value).

Secret Resolution Order

When you call sdk.secrets.get("api_key"), the SDK checks these sources in order:

  1. Environment variable${PLUGIN_NAME_UPPER}_${KEY_UPPER} (e.g. WEATHER_REPORT_API_KEY)
  2. Secrets store — Set via the Telegram admin command /plugin set <plugin> <key> <value>
  3. pluginConfig — From the plugins: section in config.yaml

The first non-empty value wins. This allows you to use environment variables in production while falling back to config during development.

Declaring Secrets in Manifest

Declare required secrets in your PluginManifest so the agent warns the admin if they are missing:

SecretDeclaration Interface
interface SecretDeclaration {
  required: boolean;       // Is this secret mandatory?
  description: string;     // Human-readable description for admin
  env?: string;            // Environment variable name (IGNORED at runtime)
}

Gotcha: The env field in SecretDeclaration is defined in the Zod schema but is ignored at runtime. The actual environment variable name is always auto-derived from the plugin name and key: ${PLUGIN_NAME_UPPER}_${KEY_UPPER}. The env field exists only for documentation purposes.

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

export const manifest: PluginManifest = {
  name: "weather-report",
  version: "1.0.0",
  secrets: {
    api_key: {
      required: true,
      description: "OpenWeatherMap API key (get one at openweathermap.org)",
    },
  },
};

export const tools = (sdk: PluginSDK): SimpleToolDef[] => [
  {
    name: "get_weather",
    description: "Get current weather for a city",
    parameters: {
      type: "object",
      properties: {
        city: { type: "string", description: "City name" },
      },
      required: ["city"],
    },
    async execute(params) {
      // Throws SECRET_NOT_FOUND if WEATHER_REPORT_API_KEY is not set
      const apiKey = sdk.secrets.require("api_key");
      const city = params.city as string;

      const response = await fetch(
        `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${apiKey}&units=metric`
      );
      const data = await response.json();

      return {
        success: true,
        data: {
          city: data.name,
          temp: `${data.main.temp}C`,
          description: data.weather[0].description,
        },
      };
    },
  },
];
Graceful Secret Handling
// Prefer get() + null check for optional features
const webhookUrl = sdk.secrets.get("webhook_url");
if (webhookUrl) {
  await fetch(webhookUrl, {
    method: "POST",
    body: JSON.stringify({ event: "payment_received", amount }),
  });
}

// Use has() for conditional logic
if (sdk.secrets.has("premium_key")) {
  sdk.log.info("Premium features enabled");
}

sdk.storage

The StorageSDK namespace provides simple key-value persistence. It auto-creates a _kv table in the plugin's isolated database — no migrate() export required.

Note: sdk.storage is almost always available since the _kv table is auto-created. Unlike sdk.db which requires migrate(), storage works out of the box. It is only null if the core platform cannot create the plugin database at all (rare edge case). Always check for null before use in safety-critical paths.

Methods

MethodReturnsDescription
get<T>(key)T | undefinedGet a value by key. Returns undefined if not found or expired.
set<T>(key, value, opts?)voidSet a value. Optional { ttl: number } in milliseconds for auto-expiration.
delete(key)booleanDelete a key. Returns true if the key existed.
has(key)booleanCheck if a key exists and is not expired.
clear()voidDelete all keys in this plugin's storage.
Caching API Responses with TTL
async execute(params) {
  const city = params.city as string;
  const cacheKey = `weather:${city.toLowerCase()}`;

  // Check cache first (5-minute TTL)
  const cached = sdk.storage?.get<WeatherData>(cacheKey);
  if (cached) {
    sdk.log.debug(`Cache hit for ${city}`);
    return { success: true, data: cached };
  }

  // Fetch fresh data
  const apiKey = sdk.secrets.require("api_key");
  const response = await fetch(
    `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${apiKey}`
  );
  const weather = await response.json();

  const result = {
    city: weather.name,
    temp: weather.main.temp,
    humidity: weather.main.humidity,
  };

  // Cache for 5 minutes (300,000 ms)
  sdk.storage?.set(cacheKey, result, { ttl: 300_000 });

  return { success: true, data: result };
}
Persistent Counter
async execute(params, context) {
  if (!sdk.storage) {
    return { success: false, error: "Storage unavailable" };
  }

  // Increment a per-user visit counter
  const key = `visits:${context.senderId}`;
  const count = sdk.storage.get<number>(key) ?? 0;
  sdk.storage.set(key, count + 1);

  return {
    success: true,
    data: { visits: count + 1 },
  };
}
Managing Storage State
// Check before accessing
if (sdk.storage?.has("user_preferences")) {
  const prefs = sdk.storage.get<UserPrefs>("user_preferences");
  // Use prefs...
}

// Delete a single key
const existed = sdk.storage?.delete("temp_data");
sdk.log.info(`Cleanup: key existed = ${existed}`);

// Reset all plugin storage
sdk.storage?.clear();
sdk.log.info("All storage cleared");

sdk.log

The PluginLogger namespace provides structured logging. All messages are automatically prefixed with [plugin:<name>] and routed through the agent's Pino logger with proper log levels.

Methods

MethodReturnsDescription
info(...args)voidLog an informational message. Always visible.
warn(...args)voidLog a warning. Always visible.
error(...args)voidLog an error. Always visible.
debug(...args)voidLog a debug message. Only visible when DEBUG or VERBOSE environment variables are set.
Structured Logging Examples
// Basic logging
sdk.log.info("Plugin initialized successfully");
sdk.log.warn("API rate limit approaching", { remaining: 5 });
sdk.log.error("Payment verification failed", { txHash, error: err.message });

// Debug logging (only visible with DEBUG=1 or VERBOSE=1)
sdk.log.debug("Raw API response", { status: response.status, body: data });

// Output format:
// [2026-03-02 14:30:00] INFO [plugin:weather-report] Plugin initialized successfully
// [2026-03-02 14:30:01] WARN [plugin:weather-report] API rate limit approaching { remaining: 5 }
// [2026-03-02 14:30:02] ERROR [plugin:weather-report] Payment verification failed { txHash: "abc...", error: "timeout" }
Logging in Tool Execution
async execute(params, context) {
  const address = params.address as string;
  sdk.log.info(`Balance check requested for ${address}`);

  try {
    const balance = await sdk.ton.getBalance(address);
    if (!balance) {
      sdk.log.warn(`Balance fetch returned null for ${address}`);
      return { success: false, error: "Could not fetch balance" };
    }

    sdk.log.debug("Balance result", { address, balance: balance.balance });
    return { success: true, data: balance };
  } catch (err) {
    sdk.log.error("Balance check failed", {
      address,
      error: err instanceof Error ? err.message : String(err),
    });
    return { success: false, error: "Balance check failed" };
  }
}

sdk.db

The sdk.db property exposes a better-sqlite3 Database instance for plugins that need full SQL capabilities beyond key-value storage.

Important: sdk.db is null unless your plugin exports a migrate() function. Each plugin gets its own isolated SQLite database file — plugins cannot access each other's data.

The migrate() Pattern

Export a migrate function from your plugin entry point. It receives the raw better-sqlite3 database instance and runs once when the plugin is loaded:

Plugin with Database Migration
import type { PluginSDK, SimpleToolDef } from "@teleton-agent/sdk";
import type Database from "better-sqlite3";

// Called once when plugin is loaded — create your tables here
export function migrate(db: Database.Database): void {
  db.exec(`
    CREATE TABLE IF NOT EXISTS leaderboard (
      user_id INTEGER PRIMARY KEY,
      username TEXT,
      score INTEGER DEFAULT 0,
      last_played TEXT
    )
  `);

  db.exec(`
    CREATE TABLE IF NOT EXISTS used_transactions (
      tx_hash TEXT PRIMARY KEY,
      game_type TEXT NOT NULL,
      used_at TEXT DEFAULT (datetime('now'))
    )
  `);
}

export const tools = (sdk: PluginSDK): SimpleToolDef[] => [
  {
    name: "update_score",
    description: "Update a player's score on the leaderboard",
    parameters: {
      type: "object",
      properties: {
        score: { type: "number", description: "Points to add" },
      },
      required: ["score"],
    },
    async execute(params, context) {
      if (!sdk.db) {
        return { success: false, error: "Database not available" };
      }

      const score = params.score as number;
      const userId = context.senderId;

      const stmt = sdk.db.prepare(`
        INSERT INTO leaderboard (user_id, score, last_played)
        VALUES (?, ?, datetime('now'))
        ON CONFLICT(user_id) DO UPDATE SET
          score = score + excluded.score,
          last_played = excluded.last_played
      `);
      stmt.run(userId, score);

      const row = sdk.db.prepare(
        "SELECT score FROM leaderboard WHERE user_id = ?"
      ).get(userId) as { score: number } | undefined;

      sdk.log.info(`Score updated for user ${userId}: +${score}`);

      return {
        success: true,
        data: { totalScore: row?.score ?? score },
      };
    },
  },
];
Querying the Database
async execute(params) {
  if (!sdk.db) {
    return { success: false, error: "Database not available" };
  }

  // Fetch top 10 players
  const topPlayers = sdk.db.prepare(`
    SELECT user_id, username, score
    FROM leaderboard
    ORDER BY score DESC
    LIMIT 10
  `).all() as Array<{ user_id: number; username: string; score: number }>;

  // Use transactions for batch operations
  const insertMany = sdk.db.transaction((players: Array<{ id: number; score: number }>) => {
    const stmt = sdk.db!.prepare(
      "INSERT OR REPLACE INTO leaderboard (user_id, score) VALUES (?, ?)"
    );
    for (const player of players) {
      stmt.run(player.id, player.score);
    }
  });

  return { success: true, data: { leaderboard: topPlayers } };
}