Business
Plans & pricing
src/routes/_authed.plans.tsx) are the sellable packages Telegram users buy through the bot. Every plan is a bundle of protocol access, limits, and a fixed price — this page covers every field, the pricing model, and how a plan drives the bot's purchase flow.Plans define the VPN packages sold via the Telegram storefront. Every plan is prepaid only — mode: "prepaid" means a fixed price for a fixed duration; there is no metered or postpaid billing mode (MODE_LABEL only ever maps prepaid).
The route is /_authed/plans and supports a ?open=<planId> search param that auto-opens a plan's detail dialog — used for deep-linking from the Invoices page so an admin can jump straight from an invoice to the plan that generated it.
Fields
Plan data model#
| Field | Meaning |
|---|---|
| id / name | Row id and the display name shown in the bot's Buy flow. |
| mode | Always prepaid today — fixed price, fixed duration. |
| priceCents / priceUsdCents | Authoritative USD price in cents is priceUsdCents. The panel always keeps priceCents === priceUsdCents on save — priceCents is a legacy mirror field. |
| pricePkrCents | Optional PKR price in cents, for local pricing display alongside USD. |
| durationDays | Subscription length in days. |
| quotaGb | Lifetime data cap; 0 means unlimited. |
| speedDnKbps / speedUpKbps | Per-direction throttle; 0 means unlimited. The UI shows and edits these in Mbps (kbps ÷ 1000). |
| ipLimit | Concurrent device/IP cap granted to accounts created from this plan. |
| active | Whether the plan is purchasable — “On sale” vs “Hidden”. Toggled inline via a Switch with optimistic UI (optimisticPatch). |
| protocol | Comma-separated subset of ssh,xray,zivpn — the account types the purchase bundles. Picking more than one bundles multiple protocol accounts into a single purchase. At least one is required, enforced client-side. |
| tier | Optional cosmetic badge (bronze/silver/gold) — amber/slate/yellow styling only, no functional effect. |
Node/region scoping is client-side
getPlanAcl/savePlanAcl (src/lib/plan-node-acl-store) and getPlanPricing/savePlanPricing (src/lib/plan-pricing-store) — these are NodePricing/RegionPricing records, not part of the core plan row.Only when protocol includes xray
Xray config modes#
allowConfigChooser— “User decides”: the buyer freely picks exactly one Xray protocol + transport combo in the bot.allowedCombos— array of"<protocol>:<transport>"strings, e.g.vless:ws,trojan:xhttp. Protocols:vless,vmess,trojan. Transports:ws,httpupgrade,xhttp,grpc.maxProtocols— if> 0, “cherry-pick” mode: the buyer chooses up to N of the pinned combos. If0with combos pinned, it is “locked” mode: the buyer gets every pinned combo automatically, no picker shown.allowedTransports,allowedXhttpModes— extra transport-level restriction data (XHTTP mode variants), shown only when a pinned combo ends in:xhttp.
Creating a plan
- 1Go to Plans → click New plan.
- 2Enter a Name.
- 3Set Price USD (and optionally Price PKR for local pricing display).
- 4Set Duration (days), Quota GB (0 = unlimited), IP limit, and Down/Up Mbps (0 = unlimited).
- 5Under Account types included, tick at least one of SSH / Xray / ZIVPN. Selecting more than one bundles multiple protocol accounts into a single purchase.
- 6If Xray is included, choose an Xray config mode: User decides, Choose combos for user (locked), or Set a limit for user (cherry-pick).
- 7Click Save — creates or updates the plan via a single call,
api.plans.save(plan)(id decides create vs update).
Editing and publishing
- Toggle the Purchasable switch in the list to publish/hide the plan from the bot (“On sale” vs “Hidden”) without deleting it — an easy way to soft-retire a plan while keeping its history and existing subscribers intact.
- Open a plan row to edit any field and re-save; the dialog is the same for create and edit.
Archiving / deleting
Deleting a plan asks for confirmation, then calls api.plans.remove(id). There is no soft-archive state distinct from toggling active off — untick Purchasable if you want to stop new sales while keeping the plan around for reporting and existing accounts that reference it.
Plans surface in the bot's Buy flow (user_buy → user_plan_pick in backend/agent/bot.py), which looks up the plan's allowed protocols and Xray combos to build the purchase flow: username prompt, transport picker (if applicable), port picker, then order creation.
- Only active ("On sale") plans are offered to buyers.
- A plan with multiple protocols in the protocol field results in one order that provisions multiple sibling accounts.
- Attaching a plan to an account creation form (in Accounts) pre-fills quota, device limit, speed, duration and CDN defaults from the plan.
- The plan's price is what an invoice line item and a payment record reference (planId/planName on Payment and Invoice).
Cross-cutting (§15)
Currency & ledger conventions#
- All money amounts in the JS layer are integers in cents (
priceCents/priceUsdCents/pricePkrCents/amountCents/moneyCents). Always divide by 100 for display;formatMoney()does this and formats as USD viaIntl.NumberFormat. - Points are a separate integer currency; conversion is configurable in Referrals → Points per PKR (
pointsPerUnit, e.g. 300 = 1 PKR). The PKR/USD rate (pkrPerUsd, default 280) is a Settings-driven display rate, not live FX. - Prepaid is a count of plan-units, not currency — “Prepaid: 3” means the user has 3 pre-paid plan-purchases credited, redeemable against actual VPN accounts.
- A plan with zero protocols selected is rejected by the UI before save — always tick at least one account type.
- priceCents and priceUsdCents must stay in sync; only edit price via the Price USD field so the panel keeps them mirrored.
- Node/region scoping and per-node pricing live in browser localStorage, not the plan row itself — they do not travel with an export/import of the plan list.
- Hiding a plan (Purchasable off) does not affect accounts already provisioned from it; it only stops new purchases in the bot.
