npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@toptl/sdk

v1.0.1

Published

Official TypeScript SDK for the TOP.TL Telegram directory API

Readme

npm version npm downloads MIT License

toptl

Official TypeScript SDK for the TOP.TL Telegram directory API.

  • Zero dependencies (uses native fetch)
  • Full TypeScript types
  • Built-in autoposter with on-change detection and server-driven intervals
  • grammY plugin for automatic stat tracking
  • Webhook setup and testing
  • Batch stat posting for multi-listing bots
  • Works with Node.js 18+, Deno, Bun, and edge runtimes

Installation

npm install toptl
yarn add toptl
pnpm add toptl

Quick Start

import { TopTL } from 'toptl';

const toptl = new TopTL('toptl_your_api_key');

// Get listing info
const listing = await toptl.getListing('mybot');
console.log(listing.title, listing.votes);

Authentication

Get your API key from top.tl/settings/api. Pass it to the constructor:

const toptl = new TopTL('toptl_xxx');

All requests are authenticated via Authorization: Bearer toptl_xxx header.

Usage

Get Listing Info

const listing = await toptl.getListing('mybot');

console.log(listing.title);       // "My Bot"
console.log(listing.memberCount); // 12500
console.log(listing.votes);       // 342
console.log(listing.verified);    // true

Get Votes

// Get all active voters
const votes = await toptl.getVotes('mybot');
console.log(votes.totalVotes); // 342
console.log(votes.voters);     // [{ telegramId, firstName, votedAt }]

// Limit results
const top10 = await toptl.getVotes('mybot', 10);

Check if a User Voted

const result = await toptl.hasVoted('mybot', 123456789);

if (result.hasVoted) {
  console.log(`User voted at ${result.votedAt}`);
} else {
  console.log('User has not voted');
}

Post Stats

await toptl.postStats('mybot', {
  memberCount: 12500,
  groupCount: 35,
  channelCount: 12,
  botServes: ['en', 'ru', 'es'],
});

Batch Post Stats

Post stats for multiple listings in a single request. Useful for bots that manage several listings:

await toptl.batchPostStats([
  { username: 'mybot', memberCount: 12500, groupCount: 35 },
  { username: 'mygroup', memberCount: 8400, channelCount: 3 },
]);

Get Global Stats

const stats = await toptl.getStats();
console.log(`${stats.totalListings} listings, ${stats.totalVotes} votes`);

Webhooks

Set up webhooks to receive real-time vote notifications.

Set Webhook

await toptl.setWebhook('mybot', 'https://example.com/webhook');

// With a reward title shown to voters
await toptl.setWebhook('mybot', 'https://example.com/webhook', 'Premium Access');

Test Webhook

Send a test event to your configured webhook to verify it works:

const result = await toptl.testWebhook('mybot');
console.log(result.statusCode); // 200

Autoposter

The SDK includes a built-in autoposter that reports your bot's stats to TOP.TL. It posts immediately on start, then repeats on an interval.

Basic Usage

import { TopTL } from 'toptl';

const toptl = new TopTL('toptl_xxx');

// Start auto-posting every 30 minutes (default)
toptl.startAutopost('mybot', async () => {
  return { memberCount: await getUserCount() };
});

// Stop when needed (e.g., on shutdown)
process.on('SIGINT', () => {
  toptl.stopAutopost();
  process.exit(0);
});

Custom Interval

// Post every 10 minutes
toptl.startAutopost('mybot', getStats, { interval: 10 * 60 * 1000 });

On-Change Detection

Skip posting when stats haven't changed since the last post:

toptl.startAutopost('mybot', getStats, { onlyOnChange: true });

Server-Driven Intervals

When the server responds with a retryAfter value, the autoposter automatically uses it as the next interval instead of the configured default. This lets the server throttle or accelerate posting as needed.

grammY Plugin

The SDK provides a built-in grammY middleware that automatically tracks unique chat IDs from incoming updates and posts them as memberCount stats.

import { Bot } from 'grammy';
import { TopTL } from 'toptl';

const bot = new Bot('BOT_TOKEN');
const toptl = new TopTL('toptl_xxx');

// One line: tracks chats and auto-posts stats
bot.use(toptl.grammy('mybot'));

bot.start();

With options:

bot.use(toptl.grammy('mybot', { interval: 10 * 60 * 1000, onlyOnChange: true }));

Manual grammY Integration

If you need more control, use the autoposter directly:

import { Bot } from 'grammy';
import { TopTL } from 'toptl';

const bot = new Bot('BOT_TOKEN');
const toptl = new TopTL('toptl_xxx');

toptl.startAutopost('mybot', async () => {
  const count = await bot.api.getChatMemberCount('@mybot');
  return { memberCount: count };
});

// Vote-lock command: only allow access if the user voted
bot.command('premium', async (ctx) => {
  const { hasVoted } = await toptl.hasVoted('mybot', ctx.from!.id);

  if (!hasVoted) {
    return ctx.reply(
      'Please vote for us on TOP.TL to unlock this feature!\nhttps://top.tl/mybot'
    );
  }

  return ctx.reply('Welcome, premium user!');
});

bot.start();

API Reference

new TopTL(apiKey, options?)

Create a new client.

| Parameter | Type | Description | |-----------|------|-------------| | apiKey | string | Your TOP.TL API key (starts with toptl_) | | options.baseUrl | string | Override API base URL (default: https://top.tl/api/v1) |

getListing(username)

Get listing info. Requires scope listing:read.

Returns: Promise<Listing>

getVotes(username, limit?)

Get votes with active voters. Requires scope votes:read.

| Parameter | Type | Description | |-----------|------|-------------| | username | string | Listing username | | limit | number | Max voters to return (optional) |

Returns: Promise<VotesResponse>

hasVoted(username, telegramId)

Check if a user has voted. Requires scope votes:check.

Returns: Promise<HasVotedResponse>

postStats(username, stats)

Post stats for a listing. Requires scope listing:write.

| Parameter | Type | Description | |-----------|------|-------------| | username | string | Listing username | | stats.memberCount | number | Current member count (optional) | | stats.groupCount | number | Current group count (optional) | | stats.channelCount | number | Current channel count (optional) | | stats.botServes | string[] | Languages or regions the bot serves (optional) |

Returns: Promise<PostStatsResponse>

batchPostStats(stats)

Post stats for multiple listings in one request. Requires scope listing:write.

| Parameter | Type | Description | |-----------|------|-------------| | stats | BatchStatsEntry[] | Array of { username, memberCount?, groupCount?, channelCount?, botServes? } |

Returns: Promise<BatchStatsResponse>

getStats()

Get global TOP.TL platform stats. Requires scope listing:read.

Returns: Promise<GlobalStats>

setWebhook(username, url, rewardTitle?)

Set or update the webhook URL for a listing. Requires scope listing:write.

| Parameter | Type | Description | |-----------|------|-------------| | username | string | Listing username | | url | string | Webhook endpoint URL | | rewardTitle | string | Reward title shown to voters (optional) |

Returns: Promise<WebhookResponse>

testWebhook(username)

Send a test event to the configured webhook. Requires scope listing:write.

Returns: Promise<WebhookTestResponse>

startAutopost(username, statsFn, options?)

Start auto-posting stats. Posts immediately, then on interval.

| Parameter | Type | Description | |-----------|------|-------------| | username | string | Listing username | | statsFn | () => PostStatsBody \| Promise<PostStatsBody> | Function returning current stats | | options.interval | number | Interval in ms (default: 30 min). Overridden by server retryAfter. | | options.onlyOnChange | boolean | Skip posting if stats are unchanged (default: false) |

Returns: Promise<void>

stopAutopost()

Stop the autoposter.

grammy(username, options?)

Returns grammY-compatible middleware that tracks unique chat IDs and auto-posts stats.

| Parameter | Type | Description | |-----------|------|-------------| | username | string | Listing username | | options | AutopostOptions | Same options as startAutopost (optional) |

Returns: (ctx: any, next: () => Promise<void>) => Promise<void>

Types

interface Listing {
  username: string;
  title: string;
  description: string;
  category: string;
  memberCount: number;
  votes: number;
  verified: boolean;
  featured: boolean;
  createdAt: string;
  updatedAt: string;
}

interface VotesResponse {
  username: string;
  totalVotes: number;
  voters: Voter[];
}

interface Voter {
  telegramId: number;
  firstName: string;
  votedAt: string;
}

interface HasVotedResponse {
  hasVoted: boolean;
  votedAt: string | null;
}

interface PostStatsBody {
  memberCount?: number;
  groupCount?: number;
  channelCount?: number;
  botServes?: string[];
}

interface PostStatsResponse {
  success: boolean;
  retryAfter?: number;
}

interface GlobalStats {
  totalListings: number;
  totalVotes: number;
  totalUsers: number;
  categories: number;
}

interface WebhookResponse {
  success: boolean;
  url: string;
  rewardTitle?: string;
}

interface WebhookTestResponse {
  success: boolean;
  statusCode: number;
  body: unknown;
}

interface BatchStatsEntry {
  username: string;
  memberCount?: number;
  groupCount?: number;
  channelCount?: number;
  botServes?: string[];
}

interface BatchStatsResponse {
  success: boolean;
  processed: number;
}

interface AutopostOptions {
  interval?: number;
  onlyOnChange?: boolean;
}

Error Handling

All API errors throw a TopTLError with the HTTP status and response body:

import { TopTL, TopTLError } from 'toptl';

try {
  await toptl.getListing('nonexistent');
} catch (err) {
  if (err instanceof TopTLError) {
    console.log(err.status); // 404
    console.log(err.body);   // API error response
  }
}

License

MIT - see LICENSE for details.