- PRICING-LOCATION-ANALYSIS.md (45KB): технический анализ Marzban API - IMPLEMENTATION-SUMMARY.md (12KB): краткая выжимка для разработки - MINI_APP_ARCHITECTURE.md: обновленная архитектура Ключевые решения: ✅ Скидки за период: -10% (3м), -15% (6м), -20% (год) ✅ Выбор локаций из реальных нод Marzban ✅ Мультишаговая воронка: тариф → период → локации → оплата ❌ Динамическое ценообразование отклонено ⏸️ Семейные планы отложены на V2.0 (требуют Device Monitor) ✅ Реферальная система уже работает MVP план: 2 недели (20 часов) - Week 1: Backend API (8h) - /api/nodes + /api/create-user updates - Week 2: Frontend UI (12h) - мультишаговый /app/plans/page.tsx
45 KiB
🎯 Анализ: Система тарифов с выбором локаций + скидки за период
📊 Текущее состояние
Что есть сейчас:
-
✅ Тарифы определены в
/app/plans/page.tsx:- Trial (7 дней бесплатно)
- Start (100₽/мес, 1 локация)
- Plus (299₽/мес, 3 локации)
- Max (350₽/мес, 15+ локаций)
-
✅ SetupWizard (
/components/SetupWizard.tsx):- Есть UI для выбора локаций
- Но используется ПОСЛЕ покупки (в wizard'е настройки)
- НЕ влияет на создание пользователя
-
✅ API создания пользователя (
/app/api/create-user/route.ts):- Создает пользователя с фиксированными inbounds
- НЕТ привязки к нодам/локациям
- Используется hardcoded список inbounds:
inbounds: { vless: ['VLESS TCP', 'VLESS Reality'], vmess: ['VMess WS'], trojan: ['Trojan TCP'] } -
❌ Что НЕ работает:
- Выбор локаций НЕ сохраняется в Marzban
- Нет связи между тарифом и доступными нодами
- Нет скидок за длительные периоды (3 мес, 6 мес, год)
🔍 Анализ Marzban API
Доступные endpoints (из https://panel.umbrix.net/docs):
1. Управление нодами (/api/nodes):
GET /api/nodes # Список всех нод с их параметрами
GET /api/node/{node_id} # Детали конкретной ноды
POST /api/node # Создать ноду
PUT /api/node/{node_id} # Изменить ноду
DELETE /api/node/{node_id} # Удалить ноду
GET /api/nodes/usage # Статистика по нодам
Структура ноды (NodeResponse):
{
"id": 1,
"name": "🇺🇸 USA Node",
"address": "194.113.210.187",
"port": 62050,
"api_port": 62051,
"status": "connected",
"message": null,
"xray_version": "1.8.21",
"usage": {
"uplink": 123456789,
"downlink": 987654321
}
}
2. Inbounds (/api/inbounds):
GET /api/inbounds # Список всех inbounds (протоколов)
Структура inbound:
{
"tag": "VLESS TCP",
"protocol": "vless",
"network": "tcp",
"tls": "reality",
"port": 443
}
3. Создание пользователя (POST /api/user):
{
"username": "test_user",
"status": "active",
"expire": 1735689600, // Unix timestamp
"data_limit": 107374182400, // Bytes
"data_limit_reset_strategy": "no_reset",
"proxies": {
"vmess": {"id": "uuid-here"},
"vless": {},
"trojan": {}
},
"inbounds": {
"vless": ["VLESS TCP", "VLESS Reality"],
"vmess": ["VMess WS"],
"trojan": ["Trojan TCP"]
},
"note": "Тариф Plus, TG: 12345"
}
🎨 Новая воронка с выбором локаций
Идеальный User Flow:
1. Пользователь открывает /plans
↓
2. Выбирает тариф (Start/Plus/Max)
↓
3. Видит варианты периода оплаты:
┌─────────────────────────────────────┐
│ ⭕ 1 месяц - 299₽ (без скидки) │
│ ⭕ 3 месяца - 807₽ (-10% = 897₽) │
│ ⭕ 6 месяцев - 1523₽ (-15% = 1794₽)│
│ ⭕ 1 год - 2870₽ (-20% = 3588₽)│
└─────────────────────────────────────┘
↓
4. Выбирает локации (в зависимости от тарифа):
┌─────────────────────────────────────┐
│ Start: 1 локация из списка │
│ Plus: 3 локации из списка │
│ Max: Все локации │
└─────────────────────────────────────┘
Список локаций (из Marzban API):
☐ 🇳🇱 Нидерланды (ping: 15ms)
☐ 🇩🇪 Германия (ping: 20ms)
☐ 🇺🇸 США (ping: 120ms)
☐ 🇸🇬 Сингапур (ping: 180ms)
☑ 🇯🇵 Япония (ping: 160ms)
☐ 🇬🇧 Великобритания (ping: 35ms)
↓
5. Нажимает "Оплатить" → Payment gateway
↓
6. После оплаты → API создает пользователя с выбранными локациями
🚀 Техническая реализация
Этап 1: Получить список нод из Marzban
Создать новый API endpoint /api/nodes:
// app/api/nodes/route.ts
import { NextResponse } from 'next/server';
const MARZBAN_API = process.env.MARZBAN_PANEL_URL;
const ADMIN_USERNAME = process.env.MARZBAN_ADMIN_USERNAME;
const ADMIN_PASSWORD = process.env.MARZBAN_ADMIN_PASSWORD;
export async function GET() {
try {
// 1. Получить токен админа
const tokenResponse = await fetch(`${MARZBAN_API}/api/admin/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `grant_type=password&username=${ADMIN_USERNAME}&password=${ADMIN_PASSWORD}`,
});
const { access_token } = await tokenResponse.json();
// 2. Получить список нод
const nodesResponse = await fetch(`${MARZBAN_API}/api/nodes`, {
headers: { 'Authorization': `Bearer ${access_token}` },
});
const nodes = await nodesResponse.json();
// 3. Форматировать для фронтенда
const locations = nodes
.filter(node => node.status === 'connected')
.map(node => ({
id: node.id,
name: node.name, // "🇺🇸 USA Node"
address: node.address,
ping: calculatePing(node.address), // TODO: реальный ping
country: extractCountry(node.name), // "US"
}));
return NextResponse.json({ success: true, locations });
} catch (error) {
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
}
}
function extractCountry(nodeName: string): string {
// Извлечь код страны из emoji флага
const match = nodeName.match(/🇦-🇿{2}/);
return match ? getFlagCode(match[0]) : 'Unknown';
}
function calculatePing(address: string): string {
// TODO: Реальный ping check (или хардкод по регионам)
const mockPings = {
'194.113.210.187': '120ms', // USA
'193.168.175.128': '15ms', // NL
// ...
};
return mockPings[address] || '50ms';
}
Этап 2: Страница выбора тарифа с периодом
Обновить /app/plans/page.tsx:
'use client';
import { useState, useEffect } from 'react';
import { Check, ChevronRight } from 'lucide-react';
type Period = '1month' | '3months' | '6months' | '1year';
type PlanType = 'start' | 'plus' | 'max';
interface Location {
id: number;
name: string;
country: string;
ping: string;
}
export default function Plans() {
const [selectedPlan, setSelectedPlan] = useState<PlanType | null>(null);
const [selectedPeriod, setSelectedPeriod] = useState<Period>('1month');
const [selectedLocations, setSelectedLocations] = useState<number[]>([]);
const [availableLocations, setAvailableLocations] = useState<Location[]>([]);
// Загрузить локации из API
useEffect(() => {
async function fetchLocations() {
const response = await fetch('/api/nodes');
const data = await response.json();
if (data.success) {
setAvailableLocations(data.locations);
}
}
fetchLocations();
}, []);
// Тарифы с ценами за разные периоды
const plans = {
start: {
name: 'Start',
locations: 1,
prices: {
'1month': 100,
'3months': 270, // -10%
'6months': 510, // -15%
'1year': 960, // -20%
}
},
plus: {
name: 'Plus',
locations: 3,
prices: {
'1month': 299,
'3months': 807, // -10%
'6months': 1523, // -15%
'1year': 2870, // -20%
}
},
max: {
name: 'Max',
locations: 999, // Все
prices: {
'1month': 350,
'3months': 945, // -10%
'6months': 1785, // -15%
'1year': 3360, // -20%
}
}
};
const handlePlanSelect = (plan: PlanType) => {
setSelectedPlan(plan);
};
const handlePeriodSelect = (period: Period) => {
setSelectedPeriod(period);
};
const handleLocationToggle = (locationId: number) => {
if (!selectedPlan) return;
const maxLocations = plans[selectedPlan].locations;
if (selectedLocations.includes(locationId)) {
setSelectedLocations(selectedLocations.filter(id => id !== locationId));
} else if (maxLocations === 999 || selectedLocations.length < maxLocations) {
setSelectedLocations([...selectedLocations, locationId]);
}
};
const handlePurchase = async () => {
if (!selectedPlan || selectedLocations.length === 0) {
alert('Выберите тариф и хотя бы одну локацию');
return;
}
// Отправить на payment gateway
const response = await fetch('/api/create-payment', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
plan: selectedPlan,
period: selectedPeriod,
locations: selectedLocations,
telegramId: getTelegramId(),
}),
});
const data = await response.json();
if (data.success) {
window.location.href = data.paymentUrl;
}
};
const getDiscountPercent = (period: Period): number => {
const discounts = {
'1month': 0,
'3months': 10,
'6months': 15,
'1year': 20,
};
return discounts[period];
};
const calculateSavings = (plan: PlanType, period: Period): number => {
const basePrices = plans[plan].prices;
const monthlyPrice = basePrices['1month'];
const months = { '1month': 1, '3months': 3, '6months': 6, '1year': 12 }[period];
const fullPrice = monthlyPrice * months;
const discountedPrice = basePrices[period];
return fullPrice - discountedPrice;
};
return (
<div className="min-h-screen bg-slate-900 text-white p-4">
<h1 className="text-2xl font-bold mb-6">Выбор тарифа</h1>
{/* Шаг 1: Выбор тарифа */}
{!selectedPlan && (
<div className="space-y-4">
{Object.entries(plans).map(([key, plan]) => (
<button
key={key}
onClick={() => handlePlanSelect(key as PlanType)}
className="w-full p-4 bg-slate-800 rounded-lg text-left hover:bg-slate-700"
>
<h3 className="text-xl font-bold">{plan.name}</h3>
<p className="text-slate-400">
{plan.locations === 999 ? 'Все локации' : `${plan.locations} локаций`}
</p>
<p className="text-blue-400 mt-2">От {plan.prices['1month']}₽/мес</p>
</button>
))}
</div>
)}
{/* Шаг 2: Выбор периода */}
{selectedPlan && !selectedLocations.length && (
<div className="space-y-4">
<button onClick={() => setSelectedPlan(null)} className="text-blue-400">← Назад</button>
<h2 className="text-xl font-bold mb-4">Выберите период</h2>
{Object.entries(plans[selectedPlan].prices).map(([period, price]) => {
const discount = getDiscountPercent(period as Period);
const savings = calculateSavings(selectedPlan, period as Period);
return (
<button
key={period}
onClick={() => {
handlePeriodSelect(period as Period);
// Переходим к выбору локаций
}}
className="w-full p-4 bg-slate-800 rounded-lg text-left hover:bg-slate-700"
>
<div className="flex justify-between items-center">
<div>
<h3 className="text-lg font-bold">
{period === '1month' && '1 месяц'}
{period === '3months' && '3 месяца'}
{period === '6months' && '6 месяцев'}
{period === '1year' && '1 год'}
</h3>
{discount > 0 && (
<p className="text-green-400 text-sm">
💰 Экономия {savings}₽ (-{discount}%)
</p>
)}
</div>
<div className="text-right">
<p className="text-2xl font-bold">{price}₽</p>
{discount > 0 && (
<p className="text-slate-400 line-through text-sm">
{plans[selectedPlan].prices['1month'] *
({ '3months': 3, '6months': 6, '1year': 12 }[period] || 1)}₽
</p>
)}
</div>
</div>
</button>
);
})}
</div>
)}
{/* Шаг 3: Выбор локаций */}
{selectedPlan && selectedPeriod && (
<div className="space-y-4">
<button onClick={() => setSelectedPeriod('1month')} className="text-blue-400">← Назад</button>
<h2 className="text-xl font-bold mb-2">Выберите локации</h2>
<p className="text-slate-400 mb-4">
{plans[selectedPlan].locations === 999
? 'Доступны все локации'
: `Выберите до ${plans[selectedPlan].locations} локаций`}
</p>
<div className="space-y-2">
{availableLocations.map(location => (
<button
key={location.id}
onClick={() => handleLocationToggle(location.id)}
disabled={
!selectedLocations.includes(location.id) &&
plans[selectedPlan].locations !== 999 &&
selectedLocations.length >= plans[selectedPlan].locations
}
className={`w-full p-4 rounded-lg border transition-colors ${
selectedLocations.includes(location.id)
? 'bg-blue-600 border-blue-400'
: 'bg-slate-800 border-slate-700 hover:border-slate-600'
}`}
>
<div className="flex justify-between items-center">
<div className="flex items-center gap-3">
{selectedLocations.includes(location.id) && (
<Check className="w-5 h-5 text-white" />
)}
<span>{location.name}</span>
</div>
<span className="text-slate-400 text-sm">{location.ping}</span>
</div>
</button>
))}
</div>
{selectedLocations.length > 0 && (
<button
onClick={handlePurchase}
className="w-full py-4 bg-blue-600 hover:bg-blue-700 rounded-lg font-bold mt-6"
>
Перейти к оплате {plans[selectedPlan].prices[selectedPeriod]}₽
<ChevronRight className="inline ml-2" />
</button>
)}
</div>
)}
</div>
);
}
function getTelegramId() {
return (window as any).Telegram?.WebApp?.initDataUnsafe?.user?.id || null;
}
Этап 3: Обновить API создания пользователя
Модифицировать /app/api/create-user/route.ts:
export async function POST(request: NextRequest) {
const {
planType,
period, // NEW: '1month' | '3months' | '6months' | '1year'
locationIds, // NEW: [1, 3, 5] - выбранные ID нод
telegramId,
telegramUsername,
firstName,
lastName
} = await request.json();
// 1. Получить токен админа (same as before)
// 2. Определить параметры тарифа с учетом периода
const planConfig = getPlanConfig(planType, period);
// 3. Получить список нод и их inbounds
const nodesResponse = await fetch(`${MARZBAN_API}/api/nodes`, {
headers: { 'Authorization': `Bearer ${access_token}` },
});
const allNodes = await nodesResponse.json();
// 4. Фильтровать только выбранные ноды
const selectedNodes = allNodes.filter(node => locationIds.includes(node.id));
// 5. Получить inbounds выбранных нод
const inboundsResponse = await fetch(`${MARZBAN_API}/api/inbounds`, {
headers: { 'Authorization': `Bearer ${access_token}` },
});
const allInbounds = await inboundsResponse.json();
// 6. Создать mapping inbounds для выбранных нод
const userInbounds = {
vless: [],
vmess: [],
trojan: []
};
selectedNodes.forEach(node => {
// Добавить inbounds этой ноды в конфиг пользователя
allInbounds
.filter(inbound => inbound.node_id === node.id)
.forEach(inbound => {
if (inbound.protocol === 'vless') {
userInbounds.vless.push(inbound.tag);
} else if (inbound.protocol === 'vmess') {
userInbounds.vmess.push(inbound.tag);
} else if (inbound.protocol === 'trojan') {
userInbounds.trojan.push(inbound.tag);
}
});
});
// 7. Создать пользователя с кастомными inbounds
const createUserResponse = await fetch(`${MARZBAN_API}/api/user`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${access_token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
username: username,
status: 'active',
expire: planConfig.expireTimestamp,
data_limit: planConfig.dataLimitBytes,
data_limit_reset_strategy: 'no_reset',
proxies: {
vmess: { id: randomUUID() },
vless: {},
trojan: {}
},
inbounds: userInbounds, // <-- ГЛАВНОЕ ИЗМЕНЕНИЕ!
note: `Тариф: ${planConfig.name}, Период: ${period}, Локации: ${selectedNodes.map(n => n.name).join(', ')}, TG: ${telegramId}`,
}),
});
// Rest of the code...
}
function getPlanConfig(planType: PlanType, period: Period) {
const basePlans = {
trial: { name: 'Trial', dataLimitGB: 10, durationDays: 7 },
start: { name: 'Start', dataLimitGB: 50, basePriceRub: 100 },
plus: { name: 'Plus', dataLimitGB: 299, basePriceRub: 299 },
max: { name: 'Max', dataLimitGB: 999999, basePriceRub: 350 },
};
const periodMultipliers = {
'1month': { months: 1, discount: 0 },
'3months': { months: 3, discount: 0.10 },
'6months': { months: 6, discount: 0.15 },
'1year': { months: 12, discount: 0.20 },
};
const plan = basePlans[planType];
const periodConfig = periodMultipliers[period];
const durationDays = periodConfig.months * 30;
const fullPrice = plan.basePriceRub * periodConfig.months;
const discountedPrice = Math.round(fullPrice * (1 - periodConfig.discount));
return {
name: plan.name,
dataLimitBytes: plan.dataLimitGB * 1024 * 1024 * 1024,
expireTimestamp: Math.floor(Date.now() / 1000) + (durationDays * 24 * 60 * 60),
priceRub: discountedPrice,
periodDays: durationDays,
};
}
Этап 4: Payment gateway integration
Создать /app/api/create-payment/route.ts:
// Интеграция с платежным шлюзом (Stripe / YooKassa / Coinbase)
export async function POST(request: NextRequest) {
const { plan, period, locations, telegramId } = await request.json();
const planConfig = getPlanConfig(plan, period);
// 1. Создать заказ в БД (pending payment)
const orderId = createOrder({
telegramId,
plan,
period,
locations,
amount: planConfig.priceRub,
status: 'pending',
});
// 2. Создать платежную ссылку
const paymentUrl = await createStripeCheckout({
amount: planConfig.priceRub,
currency: 'RUB',
metadata: { orderId, telegramId },
successUrl: `https://app.umbrix.net/payment-success?order=${orderId}`,
cancelUrl: `https://app.umbrix.net/plans`,
});
return NextResponse.json({ success: true, paymentUrl });
}
Этап 5: Webhook для обработки оплаты
Создать /app/api/payment-webhook/route.ts:
// Webhook от платежного провайдера
export async function POST(request: NextRequest) {
const payload = await request.json();
// 1. Verify signature (Stripe/YooKassa)
// 2. Проверить статус оплаты
if (payload.status === 'succeeded') {
const { orderId } = payload.metadata;
// 3. Получить данные заказа из БД
const order = getOrder(orderId);
// 4. Создать пользователя в Marzban
await fetch('/api/create-user', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
planType: order.plan,
period: order.period,
locationIds: order.locations,
telegramId: order.telegramId,
}),
});
// 5. Обновить статус заказа
updateOrder(orderId, { status: 'completed' });
// 6. Отправить уведомление в Telegram
sendTelegramMessage(order.telegramId, '✅ Оплата прошла! Ваш VPN активирован.');
}
return NextResponse.json({ success: true });
}
🤖 Обновление n8n бота
Добавить новые шаги в workflow:
{
"nodes": [
{
"name": "Show Plans Menu",
"type": "n8n-nodes-base.telegram",
"parameters": {
"chatId": "={{ $json.userId }}",
"text": "💎 Выберите тариф:",
"replyMarkup": {
"inline_keyboard": [
[{"text": "🌍 Start (100₽/мес)", "web_app": {"url": "https://app.umbrix.net/plans?plan=start"}}],
[{"text": "🌎 Plus (299₽/мес)", "web_app": {"url": "https://app.umbrix.net/plans?plan=plus"}}],
[{"text": "🌏 Max (350₽/мес)", "web_app": {"url": "https://app.umbrix.net/plans?plan=max"}}]
]
}
}
}
]
}
ВАЖНО: Все тарифы теперь открывают Mini App с pre-selected планом → выбор периода → выбор локаций → оплата.
📋 Чеклист внедрения
Неделя 1: Backend API
- Создать
/api/nodes- список доступных локаций из Marzban - Обновить
/api/create-user- добавить параметрыperiodиlocationIds - Добавить функцию
getPlanConfig(planType, period)с расчетом скидок - Реализовать mapping нод → inbounds для создания пользователя
Неделя 2: Frontend UI
- Переработать
/app/plans/page.tsx- мультишаговый выбор:- Шаг 1: Тариф (Start/Plus/Max)
- Шаг 2: Период (1м/3м/6м/1год) с показом скидок
- Шаг 3: Локации (чекбоксы с ограничением по тарифу)
- Добавить компонент
PeriodSelectorс расчетом экономии - Добавить компонент
LocationSelectorс ping и флагами стран - Стилизация под Telegram Design System
Неделя 3: Payment Integration
- Выбрать платежный шлюз (YooKassa для RU, Stripe для международных)
- Создать
/api/create-payment- генерация платежных ссылок - Создать
/api/payment-webhook- обработка успешных оплат - Создать БД таблицу
ordersдля хранения pending платежей - Добавить страницу
/payment-success- редирект после оплаты
Неделя 4: n8n Bot Update
- Добавить inline кнопки с pre-selected тарифами
- Обновить WebApp URLs с query параметрами
?plan=start - Добавить уведомления о успешной оплате
- Добавить команду
/renewдля продления подписки
Неделя 5: Testing & QA
- E2E тесты полной воронки (выбор → оплата → создание пользователя)
- Тестирование на разных тарифах и периодах
- Проверка корректности создания inbounds для выбранных нод
- Load testing (100 одновременных покупок)
- Security audit payment webhook
💡 Кардинальные предложения
❌ Предложение 1: Динамическое ценообразование - ОТКЛОНЕНО
Вместо фиксированных цен - учитывать нагрузку на ноды
Причина отклонения: Усложняет ценообразование для пользователей, все локации должны быть одинаковы по цене.
Предложение 2: Гибкие пакеты трафика
Разделить тарифы на "Устройства" и "Трафик":
Base Plan (1 устройство):
├── 50 ГБ - 100₽/мес
├── 200 ГБ - 250₽/мес
└── ∞ ГБ - 350₽/мес
+100₽ за каждое доп. устройство
+50₽ за каждую доп. локацию
⚠️ Предложение 3: Семейные планы - ТРЕБУЕТ АНАЛИЗА
Family Plan (5 устройств, безлимит):
- 499₽/мес для семьи
- Каждый член семьи выбирает свои локации
- Общий аккаунт с sub-users в Marzban
Проблема: Marzban не имеет встроенного механизма ограничения количества устройств на уровне API.
Варианты решения:
Вариант 1: Мониторинг online_at (рекомендуемый)
// GET /api/user/{username} возвращает:
{
"online_at": "2026-02-08T10:30:00Z", // Последняя активность
"sub_last_user_agent": "v2rayNG/1.8.5"
}
// Логика:
// - online_at обновляется при каждом подключении
// - Если разница между текущим временем и online_at < 5 минут - устройство активно
// - Подсчитываем активные сессии через периодический poll
Ограничения:
- ❌ НЕТ прямого поля
active_connectionsилиdevice_count - ❌ НЕТ поля
ip_limitв UserCreate - ✅ ЕСТЬ
online_at- последняя активность - ✅ ЕСТЬ
sub_last_user_agent- информация об устройстве
Реализация (см. детали ниже в разделе "Семейные планы: Детальный анализ")
Вариант 2: Ограничение на уровне Xray (требует кастомных патчей)
- Модифицировать
xray_config.jsonс ограничением по IP - Требует fork Marzban с кастомной логикой
- НЕ рекомендуется для MVP
Вариант 3: IP-based limiting (приблизительный)
- Собирать статистику подключений через
/api/user/{username}/usage - Отслеживать уникальные IP адреса
- Ограничение: один пользователь может иметь несколько устройств с одним IP (домашняя сеть)
Решение: Отложить семейные планы на V2.0 до реализации мониторинга устройств.
✅ Предложение 4: Gamification - УЖЕ РЕАЛИЗОВАНО
Текущая система рефералов:
// /api/referral/track - отслеживание приглашений
// /api/referral/stats - статистика пользователя
Бонусы:
- +7 дней за каждого друга
- +30 дней milestone bonus за каждые 5 друзей
Формула:
bonusDays = (referralCount * 7) + (Math.floor(referralCount / 5) * 30)
Примеры:
1 друг → +7 дней
5 друзей → +35 дней (+30 milestone)
10 друзей → +70 дней (+60 milestone) = 130 дней
Что можно улучшить:
- Добавить визуальные badges (Bronze/Silver/Gold)
- Добавить leaderboard на странице
/referral - Уведомления в Telegram при достижении milestone
🔥 Критические улучшения воронки
1. A/B Testing периодов оплаты
// Показывать разным пользователям разные default периоды
const defaultPeriod = userId % 2 === 0 ? '3months' : '6months';
// Трекинг конверсии:
// - Какой период чаще выбирают?
// - Какая скидка конвертирует лучше?
2. Urgency триггеры
⏰ Специальное предложение!
🔥 -30% на годовую подписку (осталось 2 часа)
💰 При оплате сегодня - бонус +7 дней
3. Social proof
✅ Уже 1,234 пользователей выбрали тариф Plus
⭐ 4.9/5 - средняя оценка
🌍 Топ-3 локации: 🇳🇱 🇩🇪 🇺🇸
4. Упрощенная воронка для новичков
Вместо 3 шагов (тариф → период → локации):
"Быстрый старт" кнопка:
→ Plus (3 месяца) + Авто-выбор 3 лучших локаций по ping
→ Сразу на оплату (экономия 807₽ вместо 897₽)
🎯 Priority Roadmap
MVP (2 недели):
- ✅ API
/api/nodes- список локаций - ✅ Обновить
/app/plans/page.tsx- мультишаговый выбор - ✅ Обновить
/api/create-user- кастомные inbounds - ⚠️ YooKassa integration (payment gateway)
V1.1 (4 недели):
- Скидки за период (3м/6м/1год)
- Показ экономии при выборе периода
- n8n bot updates
V1.2 (6 недель):
Динамическое ценообразование❌ ОТКЛОНЕНО- A/B testing периодов
- Analytics dashboard (конверсия по воронке)
V2.0 (8 недель):
- Device Monitor Service - мониторинг устройств через Xray логи
- Семейные планы (после реализации п.11)
- Gamification улучшения (badges, leaderboard)
- Автоматическое продление подписки
❓ Вопросы для обсуждения
-
Какой payment gateway использовать?
- YooKassa (для РФ) ✅
- Stripe (международный) ✅
- Crypto (Coinbase Commerce) ?
-
Какие скидки оптимальны?
- 3 мес: -10% ✅
- 6 мес: -15% ✅
- 1 год: -20% ✅
-
Нужна ли локация-специфичная цена?❌ ОТКЛОНЕНО- Все локации одинаковы ✅
-
Trial период - как ограничить abuse?
- 1 trial на Telegram ID ✅
- Требовать номер телефона? ⚠️
- Требовать payment method (0₽ charge)? ⚠️
-
Семейные планы - реализовывать в MVP?
- ❌ Отложить на V2.0
- ⚠️ Требует Device Monitor Service (40 часов разработки)
- ✅ Для MVP - добавить в ToS ограничение без технического enforcement
📞 Следующие шаги
- ✅ Проанализировать текущее состояние
- ✅ Изучить Marzban API
- ✅ Спроектировать новую воронку
- ✅ Проанализировать ограничения устройств
- ✅ Изучить реферальную систему
- ⏳ Согласовать со stakeholder'ами
- ⏳ Начать разработку MVP
Готово к обсуждению! 🚀
🔬 ДОПОЛНЕНИЕ: Семейные планы - Детальный анализ ограничения устройств
Проблема:
Marzban НЕ имеет встроенного механизма ограничения количества одновременных подключений (devices/connections).
Анализ Marzban API:
❌ Что НЕ доступно в API:
// POST /api/user - UserCreate schema НЕ имеет:
{
"ip_limit": 5, // ❌ Нет такого поля
"device_limit": 5, // ❌ Нет такого поля
"max_connections": 5, // ❌ Нет такого поля
"active_sessions": [] // ❌ Нет такого поля
}
✅ Что ДОСТУПНО в API:
// GET /api/user/{username} - UserResponse:
{
"username": "test_user",
"status": "active",
"online_at": "2026-02-08T10:30:00Z", // ✅ Последняя активность
"sub_last_user_agent": "v2rayNG/1.8.5", // ✅ User-Agent устройства
"used_traffic": 123456789,
"data_limit": 107374182400
}
// GET /api/user/{username}/usage - UsageResponse:
{
"username": "test_user",
"usages": [
{
"date": "2026-02-08",
"upload": 12345,
"download": 67890,
"total": 80235
}
]
}
Варианты решения:
Вариант 1: Server-side мониторинг через online_at (Рекомендуемый для MVP)
Как работает:
- Периодически (каждые 30 секунд) опрашиваем
GET /api/user/{username} - Проверяем
online_at- если разница < 2 минуты → устройство активно - Если активных устройств > лимита → временно блокируем (
status: 'disabled')
Реализация:
// app/api/family-plans/monitor-devices/route.ts
import { NextRequest, NextResponse } from 'next/server';
const MARZBAN_API = process.env.MARZBAN_PANEL_URL;
const ADMIN_USERNAME = process.env.MARZBAN_ADMIN_USERNAME;
const ADMIN_PASSWORD = process.env.MARZBAN_ADMIN_PASSWORD;
interface FamilyPlanUser {
username: string;
deviceLimit: number;
lastCheck: Date;
}
// В production - хранить в Redis или БД
const familyPlanUsers: Map<string, FamilyPlanUser> = new Map();
// Cron job - запускать каждые 30 секунд
export async function POST(request: NextRequest) {
try {
// 1. Получить токен админа
const tokenResponse = await fetch(`${MARZBAN_API}/api/admin/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `grant_type=password&username=${ADMIN_USERNAME}&password=${ADMIN_PASSWORD}`,
});
const { access_token } = await tokenResponse.json();
// 2. Проверить каждого пользователя Family Plan
for (const [username, config] of familyPlanUsers.entries()) {
const userResponse = await fetch(`${MARZBAN_API}/api/user/${username}`, {
headers: { 'Authorization': `Bearer ${access_token}` },
});
const user = await userResponse.json();
// 3. Проверить активность
const onlineAt = new Date(user.online_at);
const now = new Date();
const minutesSinceActive = (now.getTime() - onlineAt.getTime()) / 1000 / 60;
// Если пользователь активен (подключался менее 2 минут назад)
if (minutesSinceActive < 2) {
// ПРОБЛЕМА: Мы НЕ МОЖЕМ узнать сколько устройств подключено СЕЙЧАС!
// online_at показывает только ПОСЛЕДНЮЮ активность
// Вариант решения: собирать статистику подключений
// Но это требует доступа к логам Xray на уровне нод
}
}
return NextResponse.json({ success: true, message: 'Device monitoring completed' });
} catch (error) {
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
}
}
Проблема: online_at показывает только последнюю активность, а НЕ количество активных сессий.
Вариант 2: IP-based мониторинг (Приблизительный)
Идея: Отслеживать уникальные IP адреса через usage API.
Проблема:
- Несколько устройств могут быть за одним IP (домашний роутер)
- Один телефон может менять IP (мобильный интернет)
- НЕ точный метод
Вариант 3: Кастомизация Xray config (Профессиональный)
Идея: Модифицировать xray_config.json для ограничения по IP.
{
"inbounds": [
{
"tag": "VLESS TCP",
"protocol": "vless",
"settings": {
"clients": [
{
"id": "uuid-here",
"email": "test_user@marzban",
"level": 0,
"maxIps": 5 // ← Кастомное поле (требует патч Xray)
}
]
}
}
]
}
Проблема:
- Требует fork Xray с кастомной логикой
- Нужно пропатчить Marzban для поддержки
maxIps - Сложно для MVP
Вариант 4: Использовать sub-users (Встроенная функция Marzban)
Идея: Создать родительский аккаунт + 4 дочерних аккаунта.
// Семейный план:
// 1. Создать главного пользователя: family_john_doe
// 2. Создать 4 sub-users:
// - family_john_doe_device_1
// - family_john_doe_device_2
// - family_john_doe_device_3
// - family_john_doe_device_4
// - family_john_doe_device_5
// Каждый sub-user = отдельное устройство с собственной подпиской
Преимущества:
- ✅ Работает "из коробки" в Marzban
- ✅ Каждое устройство = отдельная подписка
- ✅ Легко контролировать (5 username'ов)
Недостатки:
- ❌ Пользователю нужно выдавать 5 разных subscription URLs
- ❌ Неудобно для конечного пользователя
- ❌ Не настоящий "Family Plan" (просто 5 отдельных аккаунтов)
Рекомендация для V1.0:
MVP подход:
- Отказаться от ограничения устройств на техническом уровне
- Полагаться на честность пользователей
- Добавить в ToS: "Семейный план предназначен для использования в одной семье (до 5 устройств)"
- Мониторить аномальный трафик: если один аккаунт генерирует 1TB/день - вручную проверить
V2.0 подход (после MVP):
- Реализовать Server-side webhook для мониторинга Xray логов
- Парсить логи подключений в реальном времени:
# Xray лог показывает: [Info] accepted tcp:xxx.xxx.xxx.xxx:12345 [email:test_user@marzban] - Собирать статистику активных IP за последние 5 минут
- Если > deviceLimit → отправить уведомление или временно заблокировать
Архитектура для V2.0:
┌─────────────────────────────────────────┐
│ Marzban Panel (API) │
│ - Создание пользователей │
│ - Управление подписками │
└────────────┬────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Xray Core (на каждой ноде) │
│ - Обработка трафика │
│ - Генерация логов подключений │
└────────────┬────────────────────────────┘
│
│ (Webhooks или log streaming)
▼
┌─────────────────────────────────────────┐
│ Device Monitor Service (NEW!) │
│ - Парсинг логов Xray │
│ - Подсчет активных IP per user │
│ - Проверка лимитов устройств │
│ - Автоблокировка при превышении │
└────────────┬────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ PostgreSQL / Redis │
│ - Хранение активных сессий │
│ - Хранение семейных планов │
└─────────────────────────────────────────┘
Стоимость разработки: ~40 часов (Device Monitor Service)
Итоговое решение:
| Вариант | Точность | Сложность | Время реализации | Рекомендация |
|---|---|---|---|---|
1. online_at мониторинг |
❌ Низкая | Средняя | 8 часов | ⚠️ Не точно |
| 2. IP-based | ❌ Низкая | Низкая | 4 часа | ⚠️ Не рекомендуется |
| 3. Xray патчи | ✅ Высокая | Очень высокая | 80+ часов | ❌ Слишком сложно |
| 4. Sub-users | ✅ Высокая | Средняя | 12 часов | ⚠️ Неудобно для пользователей |
| 5. Без ограничений + ToS | N/A | Очень низкая | 0 часов | ✅ Для MVP |
| 6. Log-based monitor (V2.0) | ✅ Высокая | Высокая | 40 часов | ✅ Для V2.0 |
Вывод: Для MVP - НЕ реализовывать техническое ограничение устройств. Добавить в ToS и мониторить аномальный трафик вручную.
Для V2.0 - разработать Device Monitor Service с парсингом Xray логов.
Готово к обсуждению! 🚀