Business

Plans & pricing

Plans (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 onlymode: "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.

FieldMeaning
id / nameRow id and the display name shown in the bot's Buy flow.
modeAlways prepaid today — fixed price, fixed duration.
priceCents / priceUsdCentsAuthoritative USD price in cents is priceUsdCents. The panel always keeps priceCents === priceUsdCents on save — priceCents is a legacy mirror field.
pricePkrCentsOptional PKR price in cents, for local pricing display alongside USD.
durationDaysSubscription length in days.
quotaGbLifetime data cap; 0 means unlimited.
speedDnKbps / speedUpKbpsPer-direction throttle; 0 means unlimited. The UI shows and edits these in Mbps (kbps ÷ 1000).
ipLimitConcurrent device/IP cap granted to accounts created from this plan.
activeWhether the plan is purchasable — “On sale” vs “Hidden”. Toggled inline via a Switch with optimistic UI (optimisticPatch).
protocolComma-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.
tierOptional cosmetic badge (bronze/silver/gold) — amber/slate/yellow styling only, no functional effect.

Node/region scoping is client-side

Which nodes/regions a plan can provision on, and per-node/region price overrides, are stored client-side via 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. If 0 with 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

  1. 1
    Go to Plans → click New plan.
  2. 2
    Enter a Name.
  3. 3
    Set Price USD (and optionally Price PKR for local pricing display).
  4. 4
    Set Duration (days), Quota GB (0 = unlimited), IP limit, and Down/Up Mbps (0 = unlimited).
  5. 5
    Under Account types included, tick at least one of SSH / Xray / ZIVPN. Selecting more than one bundles multiple protocol accounts into a single purchase.
  6. 6
    If Xray is included, choose an Xray config mode: User decides, Choose combos for user (locked), or Set a limit for user (cherry-pick).
  7. 7
    Click 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_buyuser_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 via Intl.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.