Skip to content

Latest commit

 

History

History
596 lines (506 loc) · 31.6 KB

File metadata and controls

596 lines (506 loc) · 31.6 KB

Build Specification — "xui-seller"

A self-hosted Telegram bot + dashboard for selling 3x-ui VPN configs with crypto (USDT — TRC-20 first, multi-chain by design)

Audience: This document is the blueprint for Claude Code. It describes a complete, single-tenant product to be built from scratch in Go (backend) + React (frontend). Build it phase by phase. Each phase ends in something runnable.


1. What we are building

A single-tenant product that a 3x-ui operator installs on their own server, next to their own 3x-ui panel. It gives them:

  1. A Telegram bot their customers talk to — get a free trial config, browse plans, pay in crypto (USDT on a network they choose), and receive a working vless:// link + QR code.
  2. A web dashboard where the operator configures the bot token, their receive wallet(s) per network, plans & prices, which 3x-ui inbound to provision from, and trial limits — and views users, orders, and revenue.
  3. A one-line installer (Docker) so the install experience mirrors installing 3x-ui itself.

Explicit non-goals (do NOT build these)

  • No modification of 3x-ui itself. We never fork it or inject a sidebar into its UI. We talk to it only through its HTTP API. 3x-ui stays a completely untouched, separate process.
  • No multi-tenant SaaS. One install serves one operator. No central hosting, no custody of other people's wallets. (May be revisited far in the future; architect cleanly but do not build.)
  • No custom payment-processor integrations (Cryptomus etc.). Payment is crypto straight to the operator's own wallet(s), watched on-chain. The system is multi-chain by design (see §7): TRC-20 is the first and default network, but the payment layer is an abstraction so additional networks (BEP-20, others) can be added without touching the bot or order logic.

2. Tech stack (decided — do not substitute)

Layer Choice Reason
Backend (bot + API) Go One static binary, tiny footprint, same language as 3x-ui, easy to ship.
Telegram github.com/go-telegram/bot (or telebot v3) Idiomatic Go, webhook + long-poll support.
3x-ui client github.com/digilolnet/client3xui Maintained Go wrapper for the 3x-ui API (login/session/addClient handled).
Payment watch Pluggable ChainWatcher interface (§7); first impl = TronGrid HTTP API; future = BEP-20 via BSC RPC / BscScan No full node needed; multi-chain by design.
DB PostgreSQL (via pgx / sqlc) Reliable; sqlc gives type-safe queries. SQLite acceptable as a lighter default if preferred — make it swappable via interface.
Frontend React + Vite + TypeScript, Tailwind, TanStack Query Light, fast, matches operator's React familiarity.
Auth (dashboard) Single operator account, session cookie or JWT, password set at install Single-tenant — no signup flow needed.
Packaging Docker + docker-compose, one-line install script Mirror 3x-ui's bash <(curl ...) UX.

QR codes: github.com/skip2/go-qrcode. Config: env vars + DB-stored settings (see Phase 3).


3. High-level architecture

                  ┌──────────────────────────────────────────────┐
                  │                Operator's VPS                  │
                  │                                                │
  Telegram  ──────┼──► [ Bot service ]──┐                          │
  users           │         │           │  reads/writes           │
                  │         │           ▼                          │
                  │         │      [ PostgreSQL ] ◄──┐             │
                  │         │           ▲            │             │
  Operator  ──────┼──► [ Dashboard API ]┘            │             │
  (browser)       │         ▲                        │             │
                  │         │                  [ Payment watcher ]  │
                  │   [ React SPA ]              (runs 1 ChainWatcher│
                  │                               per enabled network)│
                  │                                   │  polls       │
                  │                                   ▼              │
                  │            TronGrid (TRC-20)  •  BSC RPC (BEP-20) •  …
                  │                                                │
                  │   [ 3x-ui panel ] ◄── client3xui API calls ────┘
                  │   (UNTOUCHED, separate process, own port)      │
                  └──────────────────────────────────────────────┘

Three logical services, one Go binary with sub-commands / goroutines (simpler to ship than microservices):

  • bot — Telegram update handler.
  • api — REST API for the dashboard + serves the built React SPA.
  • watcher — supervises one ChainWatcher goroutine per enabled network (Tron first), each polling its chain, matching payments, and handing confirmed payments to provisioning.

All three share the DB and a settings layer. Run them as one process with internal goroutines, or allow xui-seller bot|api|watcher sub-commands for separate supervisor units — design the cmd/ layout to allow both.


4. Repository layout

xui-seller/
├── cmd/
│   └── xui-seller/main.go        # entrypoint; flags: --mode=all|bot|api|watcher
├── internal/
│   ├── config/                   # env loading, validation
│   ├── db/                       # pgx pool, migrations, sqlc-generated queries
│   │   ├── migrations/
│   │   └── queries/
│   ├── settings/                 # operator settings stored in DB (token, wallet, etc.)
│   ├── xui/                      # wrapper around client3xui: provision/revoke clients
│   ├── chains/                   # MULTI-CHAIN payment layer
│   │   ├── chain.go              # ChainWatcher interface + Payment/Network types + registry
│   │   ├── tron/                 # TRC-20 implementation (TronGrid) — first & default
│   │   └── bsc/                  # BEP-20 implementation (BSC RPC / BscScan) — added later
│   ├── bot/                      # Telegram handlers, menus (Persian), FSM for purchase flow
│   ├── api/                      # HTTP handlers for dashboard, auth middleware
│   ├── watcher/                  # supervises one ChainWatcher per enabled network; reconciliation
│   ├── orders/                   # order lifecycle / domain logic (chain-agnostic)
│   └── notify/                   # send messages to users + operator (admin alerts)
├── web/                          # React + Vite + TS dashboard
│   ├── src/
│   └── ...
├── deploy/
│   ├── docker-compose.yml
│   ├── Dockerfile
│   └── install.sh                # one-line installer
├── migrations/                   # if not under internal/db
├── Makefile
├── .env.example
└── README.md

5. Data model (PostgreSQL)

Money handling (multi-chain note): store all prices and amounts as integer micro-USDT (1 USDT = 1_000_000, i.e. 6 decimals) as the internal canonical unit, regardless of chain. Each chain reports raw amounts in its own decimals — TRC-20 USDT uses 6 decimals, BEP-20 USDT uses 18 decimals — so every ChainWatcher is responsible for normalizing its on-chain raw value to micro-USDT before it reaches the order-matching logic. The orders layer never sees chain-specific decimals; it only ever compares micro-USDT.

-- The single operator (set at install). One row.
operator(
  id              serial primary key,
  username        text not null,
  password_hash   text not null,          -- bcrypt/argon2
  created_at      timestamptz default now()
)

-- Global settings the dashboard edits (one row). Network/wallet config moved to payment_networks.
settings(
  id                  int primary key default 1,    -- single row
  bot_token           text,
  bot_username        text,
  xui_base_url        text,                          -- e.g. https://panel.example.com:port/path
  xui_username        text,
  xui_password        text,
  xui_inbound_id      int,                           -- which inbound to provision from
  xui_sub_port        int default 2096,
  xui_sub_path        text default '/user/',
  trial_enabled       bool default true,
  trial_gb            int default 1,                 -- data cap for trial (GB)
  trial_days          int default 3,
  trial_once_per_user bool default true,
  currency_label      text default 'USDT',
  payment_window_min  int default 30,                -- minutes to pay before order expires
  updated_at          timestamptz default now()
)

-- One row per payment network the operator enables. THIS is the multi-chain core.
-- TRC-20 is seeded by default; BEP-20 (and others) are added by inserting rows.
payment_networks(
  id              serial primary key,
  code            text unique not null,   -- 'TRC20' | 'BEP20' | ... (stable machine key)
  display_name    text not null,          -- 'USDT (Tron / TRC-20)'
  asset           text not null default 'USDT',
  wallet_address  text not null,          -- operator's receive address ON THIS NETWORK
  contract_address text not null,         -- token contract on this chain
  decimals        int not null,           -- 6 for TRC-20 USDT, 18 for BEP-20 USDT
  required_confs  int not null default 19,-- confirmations to accept on this chain
  rpc_url         text,                   -- node/explorer endpoint (e.g. TronGrid / BSC RPC)
  api_key         text,                   -- provider API key if needed (e.g. TronGrid key)
  poll_seconds    int default 20,         -- polling interval for this watcher
  enabled         bool default true,
  sort_order      int default 0,          -- display order at checkout
  created_at      timestamptz default now()
)
-- Seed example (Phase 2): TRC20 USDT, contract TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t, decimals 6.

-- Subscription plans the operator defines.
plans(
  id          serial primary key,
  name        text not null,            -- e.g. "1 month / 50GB"
  days        int not null,             -- duration; 0 = unlimited time
  gb          int not null,             -- data cap; 0 = unlimited
  device_limit int default 1,           -- maps to limitIp
  price_micro_usdt bigint not null,     -- price in micro-USDT
  sort_order  int default 0,
  enabled     bool default true,
  created_at  timestamptz default now()
)

-- Telegram users who interact with the bot.
users(
  id              bigserial primary key,
  tg_id           bigint unique not null,
  tg_username     text,
  language        text default 'fa',
  trial_claimed   bool default false,
  created_at      timestamptz default now()
)

-- Orders: one per purchase attempt. Now carries which network the user chose to pay on.
orders(
  id                  bigserial primary key,
  user_id             bigint references users(id),
  plan_id             int references plans(id),
  network_id          int references payment_networks(id),  -- chosen at checkout
  status              text not null,    -- pending|paid|provisioned|expired|failed
  price_micro_usdt    bigint not null,  -- nominal plan price (canonical micro-USDT)
  expected_micro_usdt bigint not null,  -- UNIQUE per-order amount (price + tiny offset) for matching
  pay_to_address      text not null,    -- snapshot of the network's wallet at order time
  tx_hash             text,             -- filled when matched
  paid_at             timestamptz,
  expires_at          timestamptz not null,  -- now + payment_window_min
  created_at          timestamptz default now()
)
-- Uniqueness of the matching amount must be scoped PER NETWORK (same amount can be pending on
-- two different chains simultaneously without colliding).
create unique index on orders(network_id, expected_micro_usdt) where status = 'pending';

-- Configs issued to users (trial or paid).
configs(
  id            bigserial primary key,
  user_id       bigint references users(id),
  order_id      bigint references orders(id),   -- null for trial
  kind          text not null,                  -- trial|paid
  xui_email     text not null,                  -- the client "email"/identifier in 3x-ui
  xui_uuid      text not null,
  sub_id        text,
  vless_link    text not null,
  expiry_time   bigint,                         -- ms epoch, matches 3x-ui expiryTime
  total_gb      int,
  created_at    timestamptz default now()
)

-- Seen transactions, for idempotency. Scoped per network (tx hashes can theoretically repeat
-- across chains, and the same hash means different things per chain).
seen_tx(
  network_id        int references payment_networks(id),
  tx_hash           text,
  to_address        text,
  amount_micro_usdt bigint,            -- already normalized by the ChainWatcher
  block_ts          bigint,
  processed_at      timestamptz default now(),
  primary key (network_id, tx_hash)
)

6. 3x-ui integration (internal/xui)

Authentication: client3xui logs in with xui_username/xui_password against xui_base_url and manages the session cookie. Wrap it so re-login on session expiry is automatic.

Provisioning a client (trial or paid) calls the 3x-ui addClient endpoint. The payload shape (confirmed from the 3x-ui Postman collection / Go model) is:

// POST {base}/panel/api/inbounds/addClient
{
  "id": <inbound_id>,              // from settings.xui_inbound_id
  "settings": "{\"clients\":[{    // NOTE: settings is a JSON *string*
    \"id\": \"<generated-uuid>\",
    \"flow\": \"xtls-rprx-vision\",   // for VLESS+Reality; leave \"\" otherwise
    \"email\": \"<unique-identifier>\",
    \"limitIp\": <device_limit>,
    \"totalGB\": <bytes>,             // GB * 1024^3; 0 = unlimited
    \"enable\": true,
    \"tgId\": \"\",
    \"subId\": \"<random-subid>\",
    \"expiryTime\": <ms_epoch>        // 0 = never; else now_ms + days*86400_000
  }]}"
}

Key correctness notes for Claude Code:

  • settings is a stringified JSON, not a nested object. client3xui handles this — verify.
  • totalGB field is named "GB" but the value 3x-ui stores is bytes. Multiply GB × 1024³. (Double-check against the running panel during Phase 1 — some versions take GB directly. Test empirically.)
  • expiryTime is milliseconds since epoch. 0 = no expiry.
  • email must be unique per client — use tg{tg_id}_{short_random}.
  • Known issue: addClient occasionally returns an empty response body even on success (see 3x-ui issues #3052, #3236). Implement retry with backoff (3 attempts) and then verify by listing clients / fetching by email rather than trusting the response.
  • Building the vless:// link: 3x-ui can return a subscription URL ({sub_port}{sub_path}{subId}), OR construct the link from inbound stream settings + client UUID. Prefer the subscription URL when the panel's subscription service is enabled; fall back to constructing the raw vless:// link from the inbound config. Support both.

Revoking / expiry is mostly handled by 3x-ui itself via expiryTime and totalGB. Provide a DeleteClient(inboundID, uuid) for admin use.


7. Payment layer — multi-chain by design (internal/chains, internal/watcher)

This is the section that makes the product chain-agnostic. The bot, orders, and provisioning code never know which chain a payment came from — they only deal in canonical micro-USDT and a network_id. All chain-specific logic lives behind one interface.

7.1 The ChainWatcher interface (internal/chains/chain.go)

// Payment is a normalized, chain-agnostic incoming transfer.
type Payment struct {
    TxHash        string
    ToAddress     string
    AmountMicro   int64   // ALREADY normalized to micro-USDT by the watcher
    Confirmations int
    BlockTime     int64   // unix seconds
}

// ChainWatcher is implemented once per network (tron, bsc, ...).
type ChainWatcher interface {
    Code() string                         // "TRC20", "BEP20", ...
    // FetchIncoming returns recent transfers TO the given wallet for the configured token,
    // normalized to micro-USDT. Implementation handles its own decimals, pagination, API key.
    FetchIncoming(ctx context.Context, wallet string, sinceBlockTs int64) ([]Payment, error)
    // ValidateConfig checks wallet/contract/rpc/api_key are usable (used by dashboard "Test").
    ValidateConfig(ctx context.Context, n NetworkConfig) error
}

// NetworkConfig is built from a payment_networks row.
type NetworkConfig struct {
    Code, WalletAddress, ContractAddress, RPCURL, APIKey string
    Decimals, RequiredConfs, PollSeconds int
}

// Registry maps a network code to a constructor, so adding a chain = registering one factory.
var registry = map[string]func(NetworkConfig) ChainWatcher{}
func Register(code string, f func(NetworkConfig) ChainWatcher) { registry[code] = f }
func New(code string, cfg NetworkConfig) (ChainWatcher, error) { /* look up + construct */ }

Adding a new network later = create internal/chains/<name>/, implement ChainWatcher, call Register("<CODE>", ...) in its init(). No other code changes.

7.2 The watcher supervisor (internal/watcher)

On startup (and on settings change), load all enabled rows from payment_networks, construct a ChainWatcher for each via the registry, and run one polling goroutine per network. Each goroutine:

  1. Every poll_seconds, call FetchIncoming(wallet, sinceBlockTs).
  2. For each Payment not already in seen_tx (keyed by network_id, tx_hash):
    • Insert into seen_tx (idempotency).
    • Find a pending order on this network_id with matching expected_micro_usdt.
    • If matched and Confirmations ≥ network.required_confs: mark order paid, set tx_hash + paid_at, then hand off to the shared provisioning routine (§7.4).
  3. Independently, a single sweeper expires pending orders past expires_atexpired.

The matching + provisioning code is shared across all chains — only FetchIncoming differs.

7.3 Unique-amount matching (chain-agnostic)

  • When an order is created, set expected_micro_usdt = plan_price + unique_offset, where the offset is a small per-order delta (e.g. order id mod 10000 → up to 0.009999 USDT) so concurrent pending orders on the same network never collide. Enforced by the partial unique index (network_id, expected_micro_usdt) where status='pending'.
  • The same nominal amount can be pending on Tron AND on BSC at once without conflict, because uniqueness is scoped per network.
  • Show the user: "send exactly X.XXXXXX USDT on to <address> within N minutes."

7.4 Provisioning on payment (ordersconfigs) — shared, idempotent

  1. Generate UUID, subId, unique email.
  2. Call xui.ProvisionClient(plan).
  3. Build vless link + QR.
  4. Insert configs row, set order provisioned.
  5. Send the user their link + QR via the bot; notify operator of the sale. If the order is already provisioned, resend the link and do not create a second client.

7.5 First implementation — TRC-20 (internal/chains/tron), default & built in Phase 2

  • USDT TRC-20 contract: TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t, 6 decimals.
  • FetchIncoming: GET https://api.trongrid.io/v1/accounts/{wallet}/transactions/trc20?only_confirmed=true&contract_address={contract}&limit=50 with header TRON-PRO-API-KEY: {api_key}. Parse value (string base units) → divide by 10^(decimals-6) to get micro-USDT (for 6 decimals this is identity).
  • Free TronGrid API key required; back off on HTTP 429.
  • Reorg safety: only_confirmed=true and/or enforce required_confs.

7.6 Second implementation — BEP-20 (internal/chains/bsc), Phase 5 / when wanted

  • USDT BEP-20 uses 18 decimals — normalize raw value to micro-USDT by dividing by 10^(18-6)=10^12. (This is exactly why normalization lives in the watcher, not the order layer.)
  • Watch via a BSC RPC provider (Ankr/QuickNode/public RPC) polling Transfer event logs to the wallet, or via a BscScan-style API analogous to TronGrid. Confirmations from block height.
  • Hard rule for the UI: each network shows its own address, clearly labeled, and the bot must warn the user to send only on the selected network. Sending USDT on the wrong network can lose funds irrecoverably. Never show one address as if it works for multiple chains.

7.7 Common edge cases (handled in the shared layer, all chains)

  • Underpayment: do not provision; notify user + operator; leave order open or mark failed.
  • Overpayment: provision; log the surplus; optionally notify operator.
  • Late payment (after expiry): if a tx matches an expired order's amount, flag for operator review rather than silently provisioning.
  • Rate limits: per-network API key; exponential backoff on 429.
  • Reorg safety: act only on confirmed transfers / enforce required_confs per network.

8. Telegram bot (internal/bot)

Default language Persian (fa), casual natural tone, with an English fallback. All user-facing strings in a locale/fa.json + locale/en.json so they're editable.

Commands / flow:

  • /start → welcome + main menu (inline keyboard):
    • 🎁 «دریافت کانفیگ تست رایگان» (Free trial) — only if trial_enabled and (if trial_once_per_user) user hasn't claimed.
    • 🛒 «خرید اشتراک» (Buy) → list enabled plans with prices.
    • 📦 «اشتراک‌های من» (My configs) → list issued configs + remaining usage/expiry.
    • 🆘 «پشتیبانی» (Support) → operator contact.
  • Trial flow: tap → provision trial client (trial_gb, trial_days) → return vless link + QR → set users.trial_claimed = true.
  • Buy flow (FSM):
    1. User picks a plan.
    2. User picks a payment network from the enabled payment_networks (skip this step automatically if only one is enabled). Each option shows its label, e.g. «USDT — شبکه ترون».
    3. Create order (status pending, network_id, expected_micro_usdt, pay_to_address snapshot, expires_at).
    4. Reply with: the selected network's address (copyable), exact amount, a clear "only send on " warning, countdown, and a «پرداخت کردم» (I've paid) button.
    5. The per-network watcher confirms on-chain → bot pushes the config automatically. The "I've paid" button just triggers an immediate re-check, not manual trust.
  • My configs: query configs + live usage via 3x-ui getClientTrafficsByEmail.

Use the bot library's FSM/state or store flow state in DB keyed by tg_id. Handle the bot running behind a SOCKS5 proxy (operator may be in a censored region) — make proxy configurable, since the operator's server may need it to reach api.telegram.org.


9. Dashboard (internal/api + web/)

Auth: single operator login (username + password set at install). Session cookie (HttpOnly, Secure) or JWT. No registration. Add simple rate-limiting on login.

API endpoints (REST, JSON):

POST   /api/login
POST   /api/logout
GET    /api/settings            GET/PUT settings
PUT    /api/settings
GET    /api/plans               CRUD plans
POST   /api/plans
PUT    /api/plans/:id
DELETE /api/plans/:id
GET    /api/users               list bot users (paginated)
GET    /api/orders              list orders (filter by status, network)
GET    /api/configs             list issued configs
GET    /api/stats               revenue, counts, trial conversions
GET    /api/networks            CRUD payment networks (TRC20, BEP20, ...)
POST   /api/networks
PUT    /api/networks/:id
DELETE /api/networks/:id
POST   /api/test/xui            test 3x-ui connection (login + list inbounds)
POST   /api/test/network/:id    validate a network's wallet/contract/rpc/api_key (ChainWatcher.ValidateConfig)
GET    /api/inbounds            proxy 3x-ui inbound list (to pick inbound_id in UI)

Frontend pages (React):

  • Login.
  • Dashboard/overview: revenue (today/7d/30d), active configs, pending orders, trial→paid conversion, recent sales.
  • Settings: bot token + "set webhook" status, 3x-ui connection (base url / user / pass, with "Test" → then a dropdown of inbounds to pick xui_inbound_id), subscription port/path, trial settings (GB + days + once-per-user toggle), payment window.
  • Payment Networks: table to add/edit/enable/disable networks. Each row: code (TRC20/BEP20/…), display name, wallet address, contract address, decimals, required confirmations, RPC/endpoint, API key, poll interval, sort order. A "Test" button per row calls ChainWatcher.ValidateConfig. TRC-20 is seeded by default; the operator just fills in their wallet + TronGrid key. Adding BEP-20 later is a new row once the bsc implementation ships.
  • Plans editor: table with add/edit/delete; fields name, days, GB, device limit, price (USDT), enabled, sort order.
  • Users: list, search by tg id/username, see their configs.
  • Orders: filter by status and network; see amount, network, tx hash (link to the relevant explorer — Tronscan for TRC-20, BscScan for BEP-20), timestamps.
  • Configs: list with usage + expiry; admin revoke button.

Serve the built SPA from the Go binary (embed with embed.FS) so it's one artifact.


10. Installer & packaging (deploy/)

  • Dockerfile: multi-stage — build Go binary + build React, final minimal image (distroless or alpine) containing the binary with embedded SPA.
  • docker-compose.yml: two services — app (the Go binary, all modes) + postgres. Volumes for DB. Env from .env.
  • install.sh: the one-liner. Mirrors 3x-ui UX:
    bash <(curl -Ls https://raw.githubusercontent.com/<you>/xui-seller/main/deploy/install.sh)
    
    It should: check Docker (install if missing), clone/pull, generate .env (prompt for operator username/password, or generate random and print them — like 3x-ui does), pick a dashboard port, docker compose up -d, then print the dashboard URL + credentials.
  • First-run: run DB migrations automatically; create the single operator row from .env.
  • Document putting the dashboard behind a reverse proxy (Nginx) with SSL, and the option to run the bot via webhook (needs public HTTPS) vs long-polling (no inbound port needed — default to long-polling for simplicity and censorship-resilience).

11. Build order (phases)

Each phase must end runnable and testable. Do not start a phase before the previous compiles and runs.

Phase 0 — Skeleton. Repo layout, Go module, main.go with mode flags, config loader, Postgres connection + migration runner, .env.example, Makefile, empty Docker setup. go build passes.

Phase 1 — Core bot engine (config-file driven).

  • internal/xui: login + provision client + build vless link + QR. Test against the operator's real panel. Resolve the GB-vs-bytes and expiryTime units empirically.
  • internal/bot: /start, Persian menu, trial flow end-to-end (provision + return link/QR).
  • Plans hardcoded/config for now. Deliverable: a bot that hands out working trial configs.

Phase 2 — Payments + persistence (multi-chain foundation, TRC-20 first).

  • Full DB schema + sqlc queries (including payment_networks).
  • internal/chains: define the ChainWatcher interface + registry (§7.1). Implement only the tron (TRC-20) watcher now, registered via the factory — but build the supervisor (internal/watcher) to run one watcher per enabled network, not hardcoded to Tron.
  • Order matching + provisioning written chain-agnostically (micro-USDT, network_id); only FetchIncoming is Tron-specific.
  • Buy flow in the bot: pick plan → pick network (auto-skip if one) → show amount/address → auto-detect → provision. Order expiry, under/overpayment handling, operator notifications.
  • Deliverable: a customer can buy a paid config with USDT on Tron, fully automatically — and the architecture already supports dropping in a second network with no refactor.

Phase 3 — Dashboard.

  • internal/api: auth + all endpoints above. Move settings/plans/networks from config into DB.
  • React SPA: login, overview, settings (with 3x-ui test + inbound picker), payment networks page with per-row test, plans editor, users, orders, configs. Embed SPA in the binary.
  • Deliverable: operator manages everything from a browser; bot reads settings live from DB.

Phase 4 — Packaging.

  • Dockerfile, docker-compose, install.sh one-liner, README with full setup + Nginx/SSL guide.
  • Deliverable: another operator can install alongside their 3x-ui with one command.

Phase 5 — Optional polish + more networks (only if wanted later).

  • BEP-20 watcher (internal/chains/bsc) — second ChainWatcher impl (18 decimals; BSC RPC / BscScan). Should require no changes outside internal/chains/bsc/ + one Register call.
  • Multi-server / inbound pool, referral system, promo codes, fuller i18n, DB backup/export, usage-warning notifications, admin broadcast.

12. Critical correctness checklist (tell Claude Code to verify, not assume)

  • addClient settings field is a stringified JSON. Confirm with client3xui.
  • totalGB units: test whether the panel wants bytes or GB on the operator's version.
  • expiryTime is ms epoch; 0 = never.
  • addClient may return an empty body on success — retry + verify by lookup.
  • Money is integer micro-USDT internally; each watcher normalizes its chain's decimals (TRC-20 = 6, BEP-20 = 18) to micro-USDT before the order layer sees it.
  • Chain-specific code lives only behind ChainWatcher; bot/orders/provisioning are chain-agnostic. Adding a network = one new package + one Register call, nothing else.
  • Each network has its own wallet/contract/decimals/confs row; per-network API key.
  • Per-network rate limits handled with backoff (429).
  • Only act on confirmed transactions (enforce per-network required_confs).
  • Payment matching + provisioning must be idempotent (seen_tx keyed by (network_id, tx_hash) + order status guards).
  • Unique per-order amount enforced by partial unique index scoped (network_id, amount).
  • Never present one address as valid for multiple chains. Bot warns: send only on the selected network. Wrong-network sends can be unrecoverable.
  • Bot may need a SOCKS5 proxy to reach Telegram; default to long-polling.
  • Dashboard secrets (bot token, xui password, per-network API keys) stored server-side only; never exposed to the SPA beyond what's needed; serve over HTTPS.
  • Never log full secrets.

13. Verified references (current as of build)

  • 3x-ui API shape (addClient, client model): MHSanaei/3x-ui Postman collection + Go model package.
  • 3x-ui Go client: github.com/digilolnet/client3xui.
  • addClient empty-response issue: 3x-ui issues #3052, #3236 (implement retries).
  • TronGrid TRC-20 history endpoint: GET /v1/accounts/{address}/transactions/trc20?contract_address=TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t.
  • USDT TRC-20 contract: TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t, 6 decimals.
  • TRON Go SDK option: github.com/distributed-lab/tron-sdk.
  • BEP-20 (later): USDT on BSC uses 18 decimals; watch via BSC RPC (Ankr/QuickNode/public) or a BscScan-style API; confirmations from block height.
  • QR: github.com/skip2/go-qrcode.

End of specification. Build single-tenant, Go + React, phase by phase, verifying the 3x-ui quirks against a live panel in Phase 1.