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

@starspay/sdk

v0.3.0

Published

Telegram Stars + Telegram Payments SDK - subscriptions, paywalls, and analytics for Telegram Mini Apps

Downloads

678

Readme

@starspay/sdk

npm version license types

Telegram payments for Mini Apps and bots — Telegram Stars, Telegram Payments (cards), and TronDealer (USDT/USDC) — with subscriptions, paywalls, and webhooks in a few lines of code.

@starspay/sdk is the official client + server SDK for StarsPay — a managed backend that abstracts Telegram payment providers behind one typed, framework-friendly subscription platform. Think of it as RevenueCat for Telegram Mini Apps.

  • Four payment providers — Telegram Stars (XTR), Telegram Payments API (Stripe-via-TG, fiat cards via the gateway you configure in @BotFather), TronDealer (USDT/USDC on BSC/Polygon/Ethereum via EVM, or TRC-20 USDT on TRON — deposit-address model), and TON (keyless TON Connect in a Telegram Mini App — native Gram, non-custodial; USDT-on-TON coming soon). Pick per product/price.
  • Subscription state machine — six states (pending, active, canceled, past_due, expired, revoked) with enforced transitions and a 3-day grace period. Fiat-renewal scheduler available for non-Stars subscriptions.
  • Auto-handled pre_checkout_query — middleware answers Telegram inside the 10-second deadline so you never miss a payment
  • Browser-safe clientsp_pub_* publishable-key enforcement; server keys throw on construction
  • React components<PaywallGate>, <SubscriptionButton>, <PurchaseButton>, <ProductCheckout>, <PaymentMethodSelector>, <TronDealerPayment>, useSubscription()
  • Product deep links — generate t.me/yourbot?start=buy_<priceId> URLs that open straight into checkout
  • Init-data validation — constant-time HMAC verification for Mini App initData
  • Refund + cancel — wraps editUserStarSubscription and refundStarPayment (Stars); Telegram Payments refunds are handled by the upstream PSP
  • Test mode — bypass entitlement checks in development without touching production data
  • Zero runtime deps for shared/server code; React is an optional peer

Get an API key at app.starspay.dev. Full guides at docs.starspay.dev.


Install

npm install @starspay/sdk

Requires Node.js 18+ for the server entry. React >=18 is an optional peer dependency for the React entry.

Entry points

The package ships four entry points; import only what you need.

| Import | Use it in | Ships | | ------------------------------- | ---------------------------------- | ----------------------------------------------- | | @starspay/sdk | Anywhere (shared) | Types, constants, product-link helpers | | @starspay/sdk/server | Node.js bots / webhook handlers | createStarsPay, validateInitData, API clients, StarsProvider + TelegramPaymentsProvider | | @starspay/sdk/client | Browsers / Mini Apps (vanilla JS) | StarsPayClient, isEntitled | | @starspay/sdk/react | React Mini Apps | Provider, hooks, gate + button components |

Both ESM and CommonJS builds are emitted with full .d.ts types.


Server quickstart

Express

import express from 'express';
import { createStarsPay } from '@starspay/sdk/server';

const starspay = createStarsPay({
  apiKey: process.env.STARSPAY_API_KEY!,        // sp_live_... or sp_test_...
  botToken: process.env.BOT_TOKEN!,
  webhookSecret: process.env.WEBHOOK_SECRET!,    // required in production
  onEvent: async (event, data) => {
    // event: 'payment.one_time' | 'subscription.created' | 'subscription.renewed' | 'payment.refunded'
    console.log(event, data.telegramUserId, data.amount);
  },
});

const app = express();

app.post(
  '/webhook',
  express.json(),
  starspay.middleware(),  // intercepts pre_checkout + successful_payment + refund
  (req, res) => {
    // your bot logic for non-payment updates
    res.sendStatus(200);
  },
);

The middleware returns 200 after a successful payment is recorded and 403 when the X-Telegram-Bot-Api-Secret-Token header doesn't match. Configure that header by passing secret_token to Telegram's setWebhook call.

Cloudflare Workers / Fastify / anywhere else

If you don't have an Express-style middleware chain, drive the SDK directly:

const starspay = createStarsPay({ apiKey, botToken, webhookSecret });

export default {
  async fetch(req: Request) {
    const secretHeader = req.headers.get('x-telegram-bot-api-secret-token') ?? undefined;
    const update = await req.json();
    await starspay.handleUpdate(update, secretHeader); // throws on invalid secret
    return new Response('OK');
  },
};

Create a subscription invoice

const invoiceUrl = await starspay.createInvoice({
  title: 'Premium',                            // ≤32 bytes UTF-8
  description: 'Unlock all features',          // ≤255 bytes UTF-8
  payload: `sub:premium:${userId}:${Date.now()}`,
  amount: 100,                                 // Stars (1–10,000)
  subscription: true,                          // 30-day recurring
});

await bot.sendMessage(chatId, `Subscribe: ${invoiceUrl}`);

Check entitlement, cancel, refund

const isActive = await starspay.isActive(telegramUserId);

await starspay.cancelSubscription(telegramUserId, telegramPaymentChargeId); // disable auto-renew
await starspay.refund(telegramUserId, telegramPaymentChargeId);             // full Stars refund
await starspay.refundPayment({ provider: 'stars', chargeId, telegramUserId }); // provider-aware refund

Validate Mini App initData

import { validateInitData } from '@starspay/sdk/server';

const data = validateInitData(req.body.initData, process.env.BOT_TOKEN!);
const userId = data.user?.id; // verified

Handle product link /start payloads

// Inside your bot's /start handler
const handled = await starspay.handleBotStart(chatId, ctx.startPayload);
if (!handled) {
  // not a StarsPay link — fall back to your normal welcome flow
}

Payment providers

StarsPay supports the following buyer-side providers. The provider is chosen per-price in your dashboard (provider column on the prices table); the SDK + backend dispatch automatically.

| Provider | Currency | Invoice mechanism | Webhook receiver | Refunds | |----------|----------|-------------------|-----------------|---------| | stars | XTR (Telegram Stars) | createInvoiceLink (currency=XTR) | bot-webhook | Yes — refundStarPayment | | telegram_payments | EUR / USD / ... (fiat cards) | createInvoiceLink + provider_token from BotFather | bot-webhook | No (handled by upstream PSP) | | trondealer | USDT / USDC (EVM: BSC, Polygon, Ethereum) · USDT TRC-20 (TRON) | Deposit address (no URL) — buyer sends funds, settlement via webhook | crypto-webhook?provider=trondealer | No (on-chain) | | ton | TON (native Gram; USDT-on-TON coming soon) | TON Connect in a Telegram Mini App; buyer connects a TON wallet, the Mini App builds + signs the transfer client-side | ton-watcher cron (on-chain) | No (on-chain) |

Configure providers on the server

When using createStarsPay, pass the keys your merchants saved in the dashboard. Only the providers you configure are dispatchable.

import { createStarsPay } from '@starspay/sdk/server';

const starspay = createStarsPay({
  apiKey: process.env.STARSPAY_API_KEY!,
  botToken: process.env.BOT_TOKEN!,
  webhookSecret: process.env.WEBHOOK_SECRET!,
  payments: {
    providers: {
      // The key is the merchant-defined identifier; token comes from @BotFather.
      // Optional `testToken` is used when testMode is enabled.
      stripe: { token: process.env.TG_PAYMENTS_TOKEN!, testToken: process.env.TG_PAYMENTS_TEST_TOKEN },
    },
  },
});

Create a Telegram Payments invoice

const invoice = await starspay.createProviderInvoice({
  provider: 'telegram_payments',
  title: 'Premium',
  description: 'Premium plan',
  amount: 999,                     // smallest unit (cents)
  currency: 'USD',
  payload: `sub:premium:${userId}:${Date.now()}`,
  needEmail: true,                 // optional — collect email at checkout
});

console.log(invoice.payUrl);
console.log(invoice.providerInvoiceId);

TronDealer (USDT / USDC)

TronDealer is a deposit-address provider: there's no hosted checkout. The buyer sends stablecoins to a per-order address and settlement arrives via a webhook. TronDealer is one-time only. The merchant picks a concrete network per price ("bsc", "polygon", "ethereum", or "tron") and the buyer pays on exactly that chain — no on-checkout picker. ("evm" is a legacy value meaning "any V2 EVM chain".)

  • EVM ("bsc" / "polygon" / "ethereum") — USDT or USDC. Deposit address is 0x…. The checkout Mini App shows an Open in wallet button (an EIP-681 deep link that opens the buyer's wallet prefilled with the transfer), a QR code, and a copyable address — no wallet-connect step. (Polygon uses native USDC — bridged USDC.e is not credited.)
  • TRON ("tron") — TRC-20 USDT only. Deposit address is T… (base58, case-sensitive). The Mini App shows the address + QR; the buyer sends from any wallet or exchange. Buyer's wallet handles the TRON energy/TRX fee.

Configure it per-merchant in the dashboard (Settings → Payment Providers → TronDealer) with an api_key and a webhook_secret, both stored encrypted. Once enabled, creating a TronDealer invoice (POST {webhook-ingest}/v1/invoices/create with priceId + telegramUserId) returns a deposit object instead of a URL:

// EVM price (network: "evm")
{
  "url": null,
  "provider": "trondealer",
  "provider_invoice_id": "0xabc…def",
  "deposit": { "address": "0xabc…def", "amount": "10.00", "asset": "USDT", "chains": ["bsc", "polygon"] }
}

// TRON price (network: "tron")
{
  "url": null,
  "provider": "trondealer",
  "provider_invoice_id": "tabc…xyz",
  "deposit": { "address": "T…base58address", "amount": "10.00", "asset": "USDT", "chains": ["tron"] }
}

In a React Mini App, render the deposit screen with <TronDealerPayment> — it assigns the address, shows a chain toggle + EIP-681 deeplink + QR, and polls getDepositStatus() until the payment confirms:

import { TronDealerPayment } from '@starspay/sdk/react';

<TronDealerPayment priceId="price_pro_usdt" onSuccess={() => unlock()} />

For buyers arriving via a product link, StarsPay opens its hosted payment Mini App with an opaque checkout token and polls GET {webhook-ingest}/v1/checkout?token=<checkout_token> for the deposit + status.

Settlement: TronDealer posts a transaction.confirmed webhook to {crypto-webhook}?app=<appId>&provider=trondealer, signed with HMAC-SHA256 over the raw body in the X-Signature-256 header (sha256=<hex>) using your webhook_secret. StarsPay verifies the signature, matches the deposit address, reconciles asset + amount, records the payment, flips the invoice to paid, and DMs the buyer. Invoice statuses: pending | paid | expired | canceled.

Test mode (canary only): Toggle Settings → TronDealer → Test mode in the dashboard to validate the full buyer flow without real funds. Invoice creation skips the live /wallets/assign call and returns a simulated deposit address; the hosted Mini App shows a "Simulate payment" button that synthesises a real transaction.confirmed event and runs the genuine settlement path. Test mode is blocked in production by the STARSPAY_ALLOW_TEST_PAYMENTS server flag and requires both the invoice and app config to carry test_mode: true. See the TronDealer docs for details.

TON (TON Connect)

The ton provider settles non-custodially in a Telegram Mini App: the buyer connects a TON wallet via TON Connect, the Mini App builds the transfer client-side (a comment carrying the order id), and the wallet signs it. Funds arrive directly in the merchant's own TON wallet — StarsPay never holds them, and there is no API key or settlement webhook. TON prices are one-time only and denominated in TON (native Gram); USDT-on-TON (jetton) is a planned follow-up.

Configure it per-merchant in the dashboard (Settings → Payment Providers → TON) with a single TON wallet address (wallet_address, stored encrypted) — that's the only field. Create a one-time price denominated in TON and assign it the ton provider. Like TronDealer, TON is dispatched by the StarsPay backend — POST {webhook-ingest}/v1/invoices/create with priceId + telegramUserId returns a checkout_token, and the bot opens the TON Mini App with it.

// Buyer flow (bot → TON Mini App → TON Connect → sign → on-chain watcher confirms):
// 1. POST {webhook-ingest}/v1/invoices/create { priceId, telegramUserId }
//    → { url: null, provider: 'ton', checkout_token: '…' }   (pending row)
// 2. Mini App: GET {webhook-ingest}/v1/checkout?token=<checkout_token>
//    → { …, ton: { to, amount_nano, comment } }
//    Buyer connects a TON wallet (TON Connect); the Mini App builds the comment
//    cell client-side and calls tonConnectUI.sendTransaction({ messages: [{ to, amount, payload }] }).
// 3. Mini App polls GET {webhook-ingest}/v1/checkout until status === 'paid'.

Confirmation is on-chain: the ton-watcher cron polls the public toncenter indexer, matches the on-chain comment to the pending invoice, then records the payment, flips the invoice to paid, and DMs the buyer. There is no webhook to register. An optional ops-level TONCENTER_API_KEY only raises toncenter rate limits — keyless operation works fine. Invoice statuses: pending | paid | expired | canceled. See the TON Payments docs.

Provider classes

If you need lower-level control (custom invoice flows, refund logic), the provider classes are exported directly:

import {
  StarsProvider,
  TelegramPaymentsProvider,
  TelegramApiClient,
} from '@starspay/sdk/server';

const telegram = new TelegramApiClient(process.env.BOT_TOKEN!);
const stars = new StarsProvider(telegram);

const invoice = await stars.createInvoice({
  title: 'Premium',
  description: 'Monthly premium',
  payload: 'sub:premium:123',
  amount: 100,
  subscription: true,
});

Browser quickstart

import { StarsPayClient } from '@starspay/sdk/client';

const client = new StarsPayClient({
  apiKey: import.meta.env.VITE_STARSPAY_PUB_KEY, // must start with sp_pub_
});

const isActive = await client.isActive(telegramUserId);

const url = await client.createInvoiceLink({
  priceId: 'price_monthly_premium',
  telegramUserId,
});

const status = await client.openPayment(url); // 'paid' | 'cancelled' | 'failed' | 'pending'

The client enforces publishable keys (sp_pub_*) and throws on sp_live_* / sp_test_* / sk_* to prevent accidentally shipping a server key to the browser. Subscription queries are memoized for 30 seconds (configurable via cacheTtl).

For deposit-style providers (e.g. TronDealer), call client.createDepositInvoice({ priceId, telegramUserId }) — it returns a DepositInvoice (bare deposit address + amount + chains) instead of a hosted-checkout URL; poll client.getDepositStatus() until it settles.

Plan-limit error handling

import { TxLimitExceededError } from '@starspay/sdk';

try {
  await client.createInvoiceLink({ priceId, telegramUserId });
} catch (err) {
  if (err instanceof TxLimitExceededError) {
    // err.tier, err.txCount, err.txLimit — show an upgrade prompt
  }
}

React quickstart

import {
  StarsPayProvider,
  PaywallGate,
  SubscriptionButton,
} from '@starspay/sdk/react';

function App() {
  const userId = window.Telegram.WebApp.initDataUnsafe.user?.id;

  return (
    <StarsPayProvider apiKey={import.meta.env.VITE_STARSPAY_PUB_KEY} telegramUserId={userId}>
      <PaywallGate
        loading={<Spinner />}
        fallback={
          <SubscriptionButton
            priceId="price_monthly_premium"
            onSuccess={() => location.reload()}
          >
            Subscribe — 100 Stars/month
          </SubscriptionButton>
        }
        errorFallback={<p>Couldn't verify subscription. Try again.</p>}
      >
        <PremiumApp />
      </PaywallGate>
    </StarsPayProvider>
  );
}

PaywallGate is fail-closed: errors render errorFallback (or fallback when omitted), never children.

Security note: PaywallGate is a UX convenience, not a security boundary. Always re-check entitlement server-side with starspay.isActive() or the middleware before serving premium data.

Hooks

import { useSubscription, useProductLink } from '@starspay/sdk/react';

function Status() {
  const { isActive, subscription, isLoading, error, refresh } = useSubscription();
  // ...
}

Other components

  • <PurchaseButton priceId={...} /> — one-time purchases
  • <ProductCheckout priceId={...} /> — full checkout card with price + product details
  • <TronDealerPayment priceId={...} /> — USDT/USDC deposit screen for the trondealer provider
  • <PaymentMethodSelector availableProviders={['stars', 'telegram_payments', 'trondealer', 'ton']} value={...} onChange={...} /> — selector grid for users who can pick a method

Subscription state machine

pending ──► active ──┬─► canceled ──► expired ──► (terminal)
                     ├─► past_due ──► expired
                     └─► revoked  ──► (terminal)

States that grant access (ENTITLED_STATUSES): active, canceled, past_due. canceled users keep access until current_period_end; past_due users keep access during the grace window (default 3 days, configurable per app).

Transitions are validated via the VALID_TRANSITIONS map exported from @starspay/sdk. Invalid transitions throw and are never written to the backend.


Product deep links

Generate shareable Telegram links that drop the user straight into checkout — no Mini App code required.

import { generateProductLink } from '@starspay/sdk';

const link = generateProductLink({
  botUsername: 'mybot',
  priceId: 'price_monthly_premium',
  type: 'bot_start',                  // or 'mini_app' with appShortName
});
// → https://t.me/mybot?start=buy_price_monthly_premium

Pair with starspay.handleBotStart(chatId, startParam) on the server to send the invoice automatically.


API key types

| Prefix | Where it goes | Enforcement | | ------------ | -------------------------- | -------------------------------------------------- | | sp_live_* | Server (production) | Required for live payments | | sp_test_* | Server (development) | Use against the staging API URL | | sp_pub_* | Browser / Mini App | Browser client throws if any other prefix is used |

Get keys at app.starspay.dev → Settings.

Test mode

const starspay = createStarsPay({ apiKey, botToken, testMode: true });
await starspay.isActive(123);  // → true (no API call)

testMode requires NODE_ENV to be development or test, and refuses to run against api.starspay.dev. The browser client also accepts testMode: true and logs a warning when combined with a publishable key.

Webhook secret (production)

createStarsPay throws if webhookSecret is missing outside of test mode. To use it:

  1. Pass webhookSecret: '...' when constructing createStarsPay.
  2. Pass the same value as secret_token to Telegram's setWebhook.

The middleware verifies the X-Telegram-Bot-Api-Secret-Token header in constant time and returns 403 on mismatch. When you call handleUpdate directly, pass the header value as the second argument.


Constants

Exported from @starspay/sdk:

| Constant | Value | Meaning | | --------------------------------- | ---------- | ---------------------------------------- | | STARS_CURRENCY | 'XTR' | Telegram Stars currency code | | SUBSCRIPTION_PERIOD_SECONDS | 2592000 | 30 days — only period Telegram supports | | DEFAULT_GRACE_PERIOD_SECONDS | 259200 | 3 days | | MIN_INVOICE_AMOUNT | 1 | Minimum invoice in Stars | | MAX_SUBSCRIPTION_AMOUNT | 10000 | Maximum subscription per period in Stars | | STARS_TO_USD_RATE | 0.013 | Approximate conversion rate |


Links

License

MIT © Alejandro Iglesias