Telegram Premium paid with Stars
This implementation provides a Layer 228 Premium storefront, direct Premium
gifts, and the HTTP Bot API giftPremiumSubscription method on top of the
existing users, messages, updates, and Stars ledger.
Supported entry points
help.getPremiumPromoandhelp.getAppConfigadvertise the configured built-in Premium bot. The Stars-only catalog is opened inside that bot.@premiumbotsupports/start,/premium,/gift,/status,/history,/terms, and/help./giftopens a regular-user peer picker, then presents the current server catalog and an invoice. A fallback command has the form/gift <user_id> <months> [greeting]. Menu and plan buttons are callback buttons handled by the local server; they do not navigate to the publict.meservice. Every incoming message and callback also carries the physical auth-key, session id,lang_code,system_lang_code, andlang_packfrom the exactinitConnectionrequest. The copy is Russian forru, English for all other reported languages, and retains Russian only for legacy internal calls that have no client metadata.payments.getPremiumGiftCodeOptionsreturns ordered, enabled, one-user XTR plans. A suppliedboost_peeris validated and then rejected withBOOST_PEER_INVALID; Premium giveaways and multi-user gift codes are not implemented.payments.getPaymentFormacceptsinputInvoicePremiumGiftStars, an@premiumbotinvoice message, and the native Premium subscription store purpose. Because that constructor has no product/month field, a normal native purchase maps to the shortest enabled plan and itsupgradevariant maps to the longest enabled plan. An unverifiable external-storerestorerequest is rejected instead of minting Premium locally.payments.sendStarsFormsettles a form and returns the existingpayments.paymentResultupdate container.- The local HTTP Bot API exposes
giftPremiumSubscriptionwithuser_id,month_count,star_count, optionaltext,text_parse_mode, andtext_entities. It validatesmonth_countandstar_countagainst the same catalog used by MTProto.
Bot API callers can supply an Idempotency-Key HTTP header or the local
request_id extension. Reusing the same key with the same request returns the
stored successful result; reusing it with different recipient, plan, price, or
text is rejected. Calls without either field remain compatible with the
official method but receive a new server-generated request key, so an
application that retries requests should always send Idempotency-Key.
Layer 228 wire contract
The implementation uses constructors from the vendored Layer 228 schema:
| Constructor | ID | Relevant flags |
|---|---|---|
inputInvoicePremiumGiftStars |
0xdabab2ef |
message: bit 0 |
payments.paymentFormStars |
0x7bf6b15c |
schema-generated |
premiumGiftCodeOption |
0x257e962b |
store product: bit 0; quantity: bit 1 (both absent for XTR) |
messageActionGiftPremium |
0x48e91302 |
crypto pair: bit 0; message: bit 1 |
starsTransactionPeerPremiumBot |
0x250dbaf8 |
no fields |
inputStorePaymentPremiumSubscription |
0xa6751e66 |
restore: bit 0 |
The direct gift parser verifies InputUser.access_hash, rejects self-gifts and
bot/deleted/system recipients, bounds the greeting to 128 Unicode characters,
allows only bold, italic, underline, strikethrough, spoiler, and valid custom
emoji entities, and verifies every entity against UTF-16 boundaries, including
surrogate-pair boundaries. The recipient's persisted
disallow_premium_gifts setting is checked both while creating the form and
again during settlement.
Catalog and configuration
The authoritative catalog is premium_plans. A client never supplies the
charged price. Every form snapshots the amount, duration, and plan version, and
settlement checks the snapshot against the current enabled catalog again.
TELESRV_PREMIUM_PLANS seeds and synchronizes config-owned rows. Saving a row
through the protected admin UI/API changes its ownership to admin, so a
restart does not overwrite the operator's price, duration, label, order, or
enabled state. Plans are disabled rather than deleted, and the last enabled
plan cannot be disabled. Existing checkout forms retain their original
snapshot but fail settlement after a plan version changes.
| Environment variable | Default | Meaning |
|---|---|---|
TELESRV_PREMIUM_BOT_USERNAME |
premiumbot |
Reserved built-in storefront username, without @. |
TELESRV_PREMIUM_BOT_USER_ID |
1250000015 |
Stable positive system-user ID that must not collide with another built-in account. |
TELESRV_PREMIUM_PLANS |
3:90:750,6:180:1300,12:365:2400 |
Comma-separated months:duration_days:amount_stars catalog. |
TELESRV_PREMIUM_SWEEP_INTERVAL |
1m |
Expiration worker interval. |
TELESRV_PREMIUM_SWEEP_BATCH |
500 |
Expired entitlement rows processed per transaction. |
Durations use the catalog's fixed duration_days everywhere: invoice,
entitlement, service message, and expiration. Extension is
max(now, premium_until) + duration_days.
Atomic settlement
Before issuing a form, the application reads the existing Stars account through
the normal Stars service, which also applies the configured one-time starting
grant. @premiumbot shows the resulting balance alongside the exact catalog
price. Settlement then runs in one PostgreSQL transaction:
- locks buyer and recipient users in stable ID order through the existing private-message state machine;
- locks the PaymentIntent and revalidates owner, status, expiry, invoice snapshot, plan version, recipient, and the latest gift-privacy restriction;
- locks and debits the buyer's existing Stars balance without allowing it to become negative;
- inserts one immutable negative Stars transaction linked to the PaymentIntent;
- inserts one entitlement and extends
users.premium_expires_at; - marks the intent paid and stores transaction/message IDs;
- inserts the purchase audit event;
- writes both private message boxes, dialogs, PTS events, and dispatch outbox rows through the existing message transaction hooks.
Gift purchases create messageActionGiftPremium between buyer and recipient.
Self purchases create a confirmation from @premiumbot. Stars are burned by the
platform: the buyer receives a negative ledger entry whose peer projects as
starsTransactionPeerPremiumBot; the Premium bot is not credited.
form_id, PaymentIntent idempotency keys, entitlement payment IDs, and linked
transaction IDs are unique. Concurrent retries therefore cannot double debit,
extend twice, or create a second service message.
Storage
Migration 0167_premium_stars adds:
premium_plans;premium_payment_intents;premium_entitlements;premium_audit_events;- Premium linkage columns on
stars_transactions; users.premium_updated_atand the Layer 228 disallowed-gift switches inaccount_settings;- the built-in Premium bot user, bot profile, reserved username, and read-model versions.
Migration 0168_premium_plan_management records whether a catalog row is
managed by startup configuration or by an operator. Apply both migrations
before starting a build that includes the Premium catalog editor.
Indexes cover plan order, form/idempotency/transaction uniqueness, pending form expiry, recipient history, entitlement user/expiry/status, payment and transaction uniqueness, and audit lookup by target/payment/command.
Expiration is lazy on every User projection and durable through a restart-safe
worker. The worker expires rows in batches, recomputes the maximum remaining
active entitlement, writes audit events, invalidates User/UserFull caches, and
pushes updateUser.
Refunds preserve the original debit and add a compensating Stars credit. The associated entitlement becomes refunded, later entitlement windows are compacted without destroying time from unrelated grants, the aggregate expiration is recomputed, and the command is idempotent.
Protected operator API
All Premium endpoints require a master token or a scoped token with
premium.manage:
GET /v1/premium/users/{id}/entitlements?limit=50GET /v1/premium/payments/{id}— returns the PaymentIntent, linked entitlement, and original ledger transactionGET /v1/premium/plansPOST /v1/premium/plans/upsertPOST /v1/accounts/grant-premium—months: 0revokesPOST /v1/accounts/refund-premium
Mutations require the existing command metadata (command_id, actor,
reason, and optional dry_run) and are written to both the admin command log
and Premium audit log.
The browser admin exposes one Stars & Premium (/monetization) workspace.
It shows the native/bot channels, supports manual Stars and Premium grants, and
edits the shared Premium catalog: Stars price, fixed duration, label, sort
order, and enabled state. The legacy /premium address remains an alias.
Browser reads and writes are protected by premium.manage; users with a
wildcard permission also have access.
Stars-only promo compatibility
premiumSubscriptionOption.currency is an ISO 4217 fiat currency field. XTR
is valid for Premium gift-code options and Stars payment forms, but not for
that constructor. Publishing XTR in help.premiumPromo.period_options makes
official clients render duplicate Star icons or missing-glyph boxes. The
Stars-only server therefore leaves period_options empty and relies on
premium_bot_username; the bot displays the live Stars catalog without
ambiguous symbols.
Verification
Run unit tests:
go test ./...
Run the real PostgreSQL bootstrap/concurrency/privacy/refund/expiry tests
against a dedicated
database whose name contains test:
$env:TELESRV_TEST_POSTGRES_DSN = "postgres://user:[email protected]:5432/telesrv_test?sslmode=disable"
go test ./internal/store/postgres -run "TestPremiumPurchaseAtomicConcurrentIdempotentRefundAndExpiry|TestAccountSettingsRoundTripPostgres" -count=1 -v
Manual Telegram Desktop check:
- apply migrations and start the server;
- open Settings → Telegram Premium and confirm there are no malformed fiat rows or duplicate Star glyphs;
- open
@premiumbot, press each menu callback, buy one plan, and verify the Stars balance/history and Premium badge; - use
/gift, choose a regular user, pay the invoice, and verifymessageActionGiftPremiumin both dialog views; - disable Premium gifts in the recipient's gift privacy, verify
USER_PRIVACY_RESTRICTED, then re-enable them; - retry the same
sendStarsFormand confirm there is no second debit, extension, or message; - disconnect the recipient before gifting, reconnect, and confirm
updates.getDifferencerestores the service message; - restart the server and confirm Premium persists;
- edit a plan in Stars & Premium, restart the server, and confirm the operator-managed value remains;
- configure a short test plan or admin grant, wait for expiry, and confirm the badge disappears after the worker or immediately on a fresh User read.
Deliberate non-goals
boost_peer, Premium giveaways, multi-user gift codes, fiat/store purchases, recurring renewal, and external payment providers are not implemented.- Premium promo videos remain optional and use the existing
TELESRV_PREMIUM_PROMO_SEED_DIR; an absent seed keeps the no-video response. - No server-side
inputStickerSetPremiumGiftsasset is seeded; official clients may use their own built-in rendering formessageActionGiftPremium. - Built-in bot copy currently ships in Russian and English; unsupported client languages fall back to English. Protocol responses and purchase data remain language-neutral, and the request-scoped session metadata path is reusable by future service bots.
Protocol references:
Layer schema,
Premium API,
Stars API,
payment flow,
inputInvoicePremiumGiftStars,
premiumSubscriptionOption,
payments.getPremiumGiftCodeOptions,
payments.getPaymentForm,
payments.sendStarsForm, and
messageActionGiftPremium.
The HTTP surface follows the official
giftPremiumSubscription
parameter set and adds only the optional local idempotency key described above.