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.
A single-tenant product that a 3x-ui operator installs on their own server, next to their own 3x-ui panel. It gives them:
- 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. - 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.
- A one-line installer (Docker) so the install experience mirrors installing 3x-ui itself.
- 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.
| 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).
┌──────────────────────────────────────────────┐
│ 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
ChainWatchergoroutine 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.
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
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)
)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:
Key correctness notes for Claude Code:
settingsis a stringified JSON, not a nested object.client3xuihandles this — verify.totalGBfield 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.)expiryTimeis milliseconds since epoch.0= no expiry.emailmust be unique per client — usetg{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 rawvless://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.
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.
// 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.
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:
- Every
poll_seconds, callFetchIncoming(wallet, sinceBlockTs). - For each
Paymentnot already inseen_tx(keyed bynetwork_id, tx_hash):- Insert into
seen_tx(idempotency). - Find a
pendingorder on thisnetwork_idwith matchingexpected_micro_usdt. - If matched and
Confirmations ≥ network.required_confs: mark orderpaid, settx_hash+paid_at, then hand off to the shared provisioning routine (§7.4).
- Insert into
- Independently, a single sweeper expires
pendingorders pastexpires_at→expired.
The matching + provisioning code is shared across all chains — only FetchIncoming differs.
- 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."
- Generate UUID, subId, unique email.
- Call
xui.ProvisionClient(plan). - Build vless link + QR.
- Insert
configsrow, set orderprovisioned. - 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.
- USDT TRC-20 contract:
TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t, 6 decimals. FetchIncoming: GEThttps://api.trongrid.io/v1/accounts/{wallet}/transactions/trc20?only_confirmed=true&contract_address={contract}&limit=50with headerTRON-PRO-API-KEY: {api_key}. Parsevalue(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=trueand/or enforcerequired_confs.
- 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
Transferevent 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.
- 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
expiredorder'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_confsper network.
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_enabledand (iftrial_once_per_user) user hasn't claimed. - 🛒 «خرید اشتراک» (Buy) → list enabled plans with prices.
- 📦 «اشتراکهای من» (My configs) → list issued configs + remaining usage/expiry.
- 🆘 «پشتیبانی» (Support) → operator contact.
- 🎁 «دریافت کانفیگ تست رایگان» (Free trial) — only if
- Trial flow: tap → provision trial client (
trial_gb,trial_days) → return vless link + QR → setusers.trial_claimed = true. - Buy flow (FSM):
- User picks a plan.
- 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 — شبکه ترون». - Create
order(statuspending,network_id,expected_micro_usdt,pay_to_addresssnapshot,expires_at). - Reply with: the selected network's address (copyable), exact amount, a clear "only send on " warning, countdown, and a «پرداخت کردم» (I've paid) button.
- 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-uigetClientTrafficsByEmail.
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.
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 thebscimplementation 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.
- 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:
It should: check Docker (install if missing), clone/pull, generate
bash <(curl -Ls https://raw.githubusercontent.com/<you>/xui-seller/main/deploy/install.sh).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
operatorrow 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).
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 theChainWatcherinterface + registry (§7.1). Implement only thetron(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); onlyFetchIncomingis 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) — secondChainWatcherimpl (18 decimals; BSC RPC / BscScan). Should require no changes outsideinternal/chains/bsc/+ oneRegistercall. - Multi-server / inbound pool, referral system, promo codes, fuller i18n, DB backup/export, usage-warning notifications, admin broadcast.
-
addClientsettingsfield is a stringified JSON. Confirm withclient3xui. -
totalGBunits: test whether the panel wants bytes or GB on the operator's version. -
expiryTimeis 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 + oneRegistercall, 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_txkeyed 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.
- 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.