Business

Wallet & tracks

Every Telegram user has three parallel wallets — Points, Money/Top-up and Prepaid — each with its own ledger. Two panel pages, Wallet and Wallet Tracks, present the same underlying ledger from different angles.

Each track is independently topped-up, spent, and giftable, and each has its own ledger of transactions fetched via api.wallet.txns(track) for "points" | "money" | "prepaid".

TrackFieldWhat it represents
PointspointsLoyalty/reward currency, mainly earned via referrals, gift/redeem codes, or admin grants; spendable toward plans.
Money / Top-upmoneyCentsReal cash balance in cents; topped up by the user (admin-approved) or gifted by an admin; spent buying plans/subscriptions directly.
PrepaidprepaidCountA count of prepaid plan-units/subscriptions credited to the user (not cash), redeemable against actual VPN accounts.

Transaction record

Every mutation of a track is a row in one append-only ledger table, the WalletTxn shape:

id, ts
Row id and timestamp.
telegram_id, track
Which user, which of points|money|prepaid.
delta
Signed change to the balance.
balance_after
Running balance after this transaction.
kind
Freeform, e.g. topup, gift, spend.
ref
Often order:<id>, linking back to a specific purchase/invoice.
note, admin_actor
Free text, and which staff member (if any) initiated it.

Real vs gifted vs coupon classification

The Wallet dashboard classifies money/prepaid inflows into buckets by inspecting tx metadata:

  • Real — genuine cash top-ups / plan purchases paid in cash.
  • Gifted — admin gifts or gift-code redemptions (isGift/isRedeemCode helpers).
  • Coupon — coupon-code discounted purchases (isCoupon).

A FIFO algorithm (moneyAttr) walks each user's money ledger chronologically, draining the “gifted” bucket first on spends, so the dashboard can correctly split “real spent” vs “gift spent” and “real held” vs “gift held”. This is purely informational reporting — it is not enforced by the backend, since the ledger has no dedicated column for it; the split is derived heuristically from kind/note/ref.

Money is displayed with formatMoney(cents) (Intl.NumberFormat USD). PKR conversion uses pkrPerUsd from Settings (default 280), purely for display on the Referrals page.

§4.2 — /_authed/wallet

Wallet page#

The newer “business dashboard”, sectioned by cash source. A SectionRail switches between seven sections, each backed by a dedicated component driven by the same underlying data:

SectionComponent
Top-upTopUpSection
PrepaidPrepaidSection
PointsPointsSection
Top-up giftedTopUpGiftSection
Prepaid giftedPrepaidGiftSection
Coupon top-upCouponTopUpSection
Coupon prepaidCouponPrepaidSection

A hero card at the top shows all-time added/spent/held tiles for whichever section is selected, plus overall Users and Total transactions counts. Data is fetched via api.wallet.summary() (per-user aggregate balances + pending counts), api.users.list(), api.invoices.list(), api.wallet.txns("money"|"prepaid"|"points"), api.accounts.list(), and api.referrals.overview(500).

§4.3 — /_authed/wallet-tracks

Wallet Tracks page & crypto tracking#

A tabbed alternative UI: Overview, Points, Top-up, Prepaid, Approvals (with a live badge count of pending top-up + prepaid requests). It is linked from Wallet via a Legacy view button.

Layout

  • Hero row — three BalanceCards (Points / Top-up / Prepaid totals across all users) plus pending counts from summary.pending.topup / summary.pending.prepaid.
  • ActionsExport XLSX (api.wallet.exportUrl()), Send monthly (api.wallet.monthlyClose() — generates and sends monthly wallet statements to users, toast reports sent/failed/users counts), and Gift wallet (opens the gift dialog).
  • Overview tab — top wallet holders table (sorted by combined weighted balance) and a recent-activity feed of the last 40 wallet transactions across all tracks.
  • Per-track tab (Points/Top-up/Prepaid) — Credited/Spent/Net summary cards plus a full paginated ledger table (up to 300 rows) with per-row XLSX export.
  • Approvals tab — pending top-up and prepaid requests submitted by users self-service in the bot, distinct from the Payments queue (which is for direct plan purchases). This is where crypto top-up confirmations typically get manually verified and approved before the balance lands on the user's Top-up track.

Endpoints (api.wallet)

text
summary()                                       per-user aggregate balances + pending counts
requests()                                       pending approvals
txns(track)                                      "points" | "money" | "prepaid"
gift(telegramId, track, amount, reason, planId?) instant admin credit
monthlyClose()                                   generate + send monthly wallet statements
exportUrl()                                      XLSX export
decideTopup(id, approve, note)                   approve/reject a self-service top-up request
decidePrepaid(id, approve, note)                 approve/reject a self-service prepaid request

Gifting a wallet balance

  1. 1
    Go to Wallet Tracks (or Users → a specific user → grant dialog).
  2. 2
    Click Gift wallet.
  3. 3
    Enter the recipient's numeric Telegram ID.
  4. 4
    Choose the track: Points, Top-up (money, in cents), or Prepaid (pick a plan + quantity).
  5. 5
    Enter the amount and a reason (shown in the ledger/audit trail).
  6. 6
    Click Send gift — this is instant and does not require the Payments approval workflow. It is the same primitive the Users page's grant/deduct dialog uses, calling api.wallet.gift(telegramId, track, amount, reason, planId?).

Approving a self-service top-up/prepaid request

  1. 1
    Go to Wallet Tracks → Approvals.
  2. 2
    Review the request (user, track, amount/plan).
  3. 3
    Optionally type a note.
  4. 4
    Click Approve or Reject — calls api.wallet.decideTopup(id, approve, note) or the parallel prepaid decision call. The wallet summary and requests list are invalidated/refetched afterward.

Two separate approval layers

Payments (plan purchases via proof-of-payment upload) and Wallet Tracks → Approvals (direct top-up/prepaid wallet-funding requests) are independent queues driving separate mutations — api.payments.decide vs api.wallet.decideTopup/decidePrepaid — but both notify the user in Telegram on decision.
  • All money amounts are integers in cents; always divide by 100 for display — formatMoney() does this and formats as USD.
  • Points are a separate integer currency; the conversion rate is configurable in Referrals → Points per PKR (pointsPerUnit). PKR/USD (pkrPerUsd, default 280) is a Settings-driven display rate, not live FX.
  • Prepaid is a count of plan-units, not currency, credited from admin gifts, coupons, or real cash top-up-to-prepaid conversion.
  • Every wallet mutation — top-up, spend, gift, coupon-discount, referral payout, admin grant/deduct — is a row in one append-only ledger table; there is no separate table per track, only a track field distinguishing them.