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
| Method | Returns | Description |
|---|---|---|
get(key) | string | undefined | Get a secret value by key. Returns undefined if not configured. |
require(key) | string | Get a secret value, throwing PluginSDKError with code SECRET_NOT_FOUND if missing. |
has(key) | boolean | Check 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:
- Environment variable —
${PLUGIN_NAME_UPPER}_${KEY_UPPER}(e.g.WEATHER_REPORT_API_KEY) - Secrets store — Set via the Telegram admin command
/plugin set <plugin> <key> <value> - pluginConfig — From the
plugins:section inconfig.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:
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.
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,
},
};
},
},
];// 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
| Method | Returns | Description |
|---|---|---|
get<T>(key) | T | undefined | Get a value by key. Returns undefined if not found or expired. |
set<T>(key, value, opts?) | void | Set a value. Optional { ttl: number } in milliseconds for auto-expiration. |
delete(key) | boolean | Delete a key. Returns true if the key existed. |
has(key) | boolean | Check if a key exists and is not expired. |
clear() | void | Delete all keys in this plugin's storage. |
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 };
}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 },
};
}// 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
| Method | Returns | Description |
|---|---|---|
info(...args) | void | Log an informational message. Always visible. |
warn(...args) | void | Log a warning. Always visible. |
error(...args) | void | Log an error. Always visible. |
debug(...args) | void | Log a debug message. Only visible when DEBUG or VERBOSE environment variables are set. |
// 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" }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:
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 },
};
},
},
];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 } };
}