Steam Trade Bot
1. Summary
Goal: Автоматизированный вывод виртуальных предметов (скинов) из инвентаря пользователя в его Steam аккаунт через trade offers. Бот работает без участия администратора — от запроса до получения скина в Steam.
User Value: Получение реальных скинов Rust в Steam без ожидания ручной обработки администратором. Пользователь нажимает "Вывести" → принимает трейд в Steam → скин в инвентаре.
2. Business Logic
Withdrawal Lifecycle
Жизненный цикл вывода проходит через 5 стадий:
PENDING → PROCESSING → SENT → COMPLETED
↓
FAILED
- 1. PENDING
- 2. PROCESSING
- 3. SENT
- 4. COMPLETED
- FAILED
Резервация предмета
- Пользователь запрашивает вывод через
POST /api/withdraw - Создаётся запись
Withdrawalсо статусомPENDING - Предмет НЕ удаляется из виртуального инвентаря (protection)
- Job добавляется в очередь обработки
Виртуальный предмет НЕ удаляется до подтверждения Steam. Это защита от double-spend: если трейд упадёт — предмет останется.
Поиск предмета на боте
- Бот запрашивает свой инвентарь Steam (с кешированием)
- Ищет предмет по
marketHashName(exact match, case-insensitive) - Проверяет
tradable=1Иmarketable=1 - Если предмет в trade hold → возвращает дату разблокировки
Если предмет недавно получен ботом, Steam блокирует его на 7 дней. Вывод невозможен до разблокировки.
Trade Offer отправлен
- Бот создаёт trade offer через Steam API
- Добавляет предмет по
assetid - Отправляет на
tradeUrlпользователя - Автоматически подтверждает через 2FA (identitySecret)
- Статус →
SENT, сохраняетсяtradeOfferId
Важно: Виртуальный предмет всё ещё в инвентаре! Ждём ответа Steam.
Trade принят пользователем
- Steam отправляет событие
sentOfferChanged(state=3 Accepted) TradeStatusTrackerServiceобрабатывает событие- Статус →
COMPLETED - Виртуальный предмет УДАЛЯЕТСЯ из
UserInventory - Создаётся событие
ITEM_WITHDRAWNв Feed
Предмет удаляется ТОЛЬКО здесь — когда Steam подтвердил приём. Это гарантирует консистентность.
Trade отклонён/истёк
Steam state codes, приводящие к FAILED:
| State | Причина | Сообщение пользователю |
|---|---|---|
| 7 | Declined | "Трейд был отклонён" |
| 5 | Expired | "Истёк срок действия трейда" |
| 6 | Canceled | "Трейд был отменён" |
| 8 | InvalidItems | "Предметы больше недоступны" |
| 10 | CanceledBySecondFactor | "Трейд был отменён через мобильное подтверждение" |
Виртуальный предмет остаётся в инвентаре — пользователь может повторить вывод.
Withdraw Readiness Check
Перед выводом система выполняет 7-точечную проверку готовности:
| # | Проверка | Описание | Блокирует |
|---|---|---|---|
| 1 | hasTradeUrl | Trade URL установлен в профиле | ✓ |
| 2 | isSteamVerified | Steam аккаунт верифицирован | ✓ |
| 3 | isInventoryPublic | Инвентарь Steam публичный | ✓ |
| 4 | noActiveWithdrawal | Нет активного вывода этого предмета | ✓ |
| 5 | itemExists | Предмет есть в виртуальном инвентаре | ✓ |
| 6 | itemWithdrawable | Тип предмета — SKIN | ✓ |
| 7 | itemAvailableOnBot | Предмет tradable на боте | ✓ |
Результат: canWithdraw: boolean + детальный breakdown для UI.
Protection
| Действие | Rate Limit | Auth | Validation | Atomic |
|---|---|---|---|---|
| Создать вывод | mutations (5/min) | telegram | WithdrawalSchema | ✓ |
| Проверить готовность | general (100/min) | telegram | ReadinessSchema | — |
| Отменить вывод | — | telegram | — | ✓ |
См. Security Matrix для полного обзора защит.
Incoming Trades (Пополнение бота)
Бот автоматически принимает входящие трейды для пополнения инвентаря. Только от доверенных аккаунтов, только подарки (бот ничего не отдаёт).
Decision Logic:
Новый входящий трейд
├── Отправитель НЕ в whitelist → DECLINE
├── Бот должен отдать предметы → DECLINE (safety)
├── Пустой трейд (0 предметов) → DECLINE
└── Gift от admin → ACCEPT + 2FA confirm + Telegram notify
Бот никогда не отдаёт предметы через авто-принятие. Даже если отправитель в whitelist — трейд, где itemsToGive > 0, будет отклонён. Это защита от компрометации admin-аккаунта.
Конфигурация:
| Переменная | Формат | Пример |
|---|---|---|
STEAM_ADMIN_IDS | Comma-separated SteamID64 | 76561198175539447,76561198000000000 |
Если STEAM_ADMIN_IDS не задан — все входящие трейды отклоняются.
Telegram уведомления:
При успешном принятии трейда бот отправляет уведомление в топик TRADES:
- Trade ID
- Steam ID отправителя
- Количество и список предметов (до 20)
Edge Cases
| Ситуация | UI поведение |
|---|---|
| ❌ Trade URL не установлен | Кнопка "Вывести" → редирект в настройки профиля |
| ❌ Steam не верифицирован | Кнопка disabled, tooltip "Требуется верификация Steam" |
| ❌ Инвентарь приватный | Кнопка disabled, tooltip "Сделайте инвентарь публичным" |
| ⏱️ Предмет в trade hold | Показать дату разблокировки "Доступен через 5д 12ч" |
| 🔄 Уже есть активный вывод | Показать статус текущего вывода вместо кнопки |
| ✅ Трейд отправлен | Кнопка "Открыть Steam" → ссылка на trade offer |
Backend Error Codes
| Код | HTTP | Описание |
|---|---|---|
NO_TRADE_URL | 400 | Trade URL не установлен |
NOT_VERIFIED | 400 | Steam аккаунт не верифицирован |
ITEM_NOT_FOUND | 404 | Предмет не найден в инвентаре |
ITEM_NOT_WITHDRAWABLE | 400 | Тип предмета не поддерживает вывод |
ACTIVE_WITHDRAWAL_EXISTS | 400 | Уже есть активный вывод |
ITEM_NOT_ON_BOT | 400 | Предмет отсутствует на боте |
TRADE_HOLD | 400 | Предмет в trade hold |
BOT_UNAVAILABLE | 503 | Бот не авторизован в Steam |
3. ADR (Architectural Decisions)
Почему Item Reservation Pattern?
Проблема: При выводе нужно синхронизировать два источника правды — виртуальный инвентарь (БД) и реальный Steam инвентарь. Если удалить предмет сразу при запросе, а трейд упадёт — предмет потерян.
Решение: Предмет удаляется ТОЛЬКО после подтверждения Steam (state=3 Accepted). До этого — резервация: предмет помечен как "в процессе вывода", но физически остаётся в UserInventory.
Альтернативы (отклонены):
- Удалять сразу + восстанавливать при ошибке → race conditions, сложная логика отката
- Не удалять никогда, полагаться на Steam → рассинхронизация данных
Последствия: Надёжность выше, но код сложнее. Нужен отдельный confirmWithdrawal() метод.
Никогда не удалять виртуальный предмет до TradeStatusTrackerService.handleAccepted(). Все пути ошибок должны вызывать releaseItemOnError().
Почему 3-Layer Smart Caching?
Проблема: Steam API имеет жёсткие лимиты. При частых запросах инвентаря бота — бан IP.
Решение: Трёхуровневое кеширование:
- Cache Hit — возврат кеша если TTL валиден (5 мин)
- Smart Freshness — если
forceRefreshно кеш моложе 3 сек, всё равно возврат кеша - In-Flight Deduplication — одновременные запросы получают один Promise
Техническая реализация
getInventory(appid, contextid, forceRefresh):
1. Check cache → HIT? return cached
2. forceRefresh? Check age < 3sec → return cached anyway
3. Check in-flight Map → same request pending? return same Promise
4. Fetch from Steam API → store in cache → return
| Параметр | Значение | Описание |
|---|---|---|
CACHE_TTL | 5 мин | Время жизни кеша инвентаря |
MIN_INTERVAL | 3 сек | Минимальный интервал между запросами Steam |
Почему In-Memory Queue?
Проблема: Trade jobs нужно обрабатывать последовательно (Steam rate limits).
Решение: In-memory очередь с интервалом 1 сек между jobs.
Альтернативы (отклонены):
- Redis/PostgreSQL queue → overhead для простой задачи
- BullMQ → лишняя зависимость
Последствия:
- ✅ Простота, нет внешних зависимостей
- ❌ Очередь теряется при рестарте сервера
- ❌ Не подходит для горизонтального масштабирования
Для текущего объёма (один бот) in-memory достаточно. При масштабировании — миграция на Redis.
Почему Lazy Initialization?
Проблема: Steam логин занимает время и может блокировать старт сервера.
Решение: TradeManager создаётся не при старте, а по событию webSession от SteamBotService. Сервер стартует мгновенно, бот авторизуется в фоне.
Последствия:
- API
/api/withdrawдоступен сразу, но возвращаетBOT_UNAVAILABLEпока бот не готов isBotReady()check в каждом методе
4. Architecture
Trade Flow
Key Components
| Компонент | Путь | Описание |
|---|---|---|
| SteamBotService | backend/src/domains/steam-trade-bot/services/steam-bot.service.ts | Сессия Steam, логин, 2FA, reconnect |
| TradeManagerService | backend/src/domains/steam-trade-bot/services/trade-manager.service.ts | Trade offers, 3-layer кеширование инвентаря |
| TradeQueueService | backend/src/domains/steam-trade-bot/services/trade-queue.service.ts | Последовательная обработка jobs |
| ItemReservationService | backend/src/domains/steam-trade-bot/services/item-reservation.service.ts | Жизненный цикл предмета при выводе |
| TradeStatusTrackerService | backend/src/domains/steam-trade-bot/services/trade-status-tracker.service.ts | Синхронизация статусов со Steam |
| BotInventoryCacheService | backend/src/domains/steam-trade-bot/services/bot-inventory-cache.service.ts | Кеширование инвентаря бота |
| IncomingTradeService | backend/src/domains/steam-trade-bot/services/incoming-trade.service.ts | Авто-принятие входящих gift-трейдов от admin whitelist |
| WithdrawReadinessService | backend/src/domains/steam-trade-bot/services/withdraw-readiness.service.ts | 7-точечная проверка готовности |
| TradeController | backend/src/domains/steam-trade-bot/controllers/trade.controller.ts | API эндпоинты |
| Routes | backend/src/domains/steam-trade-bot/routes/trade.routes.ts | Роутинг |
Circuit Breaker (Reconnection)
При потере сессии Steam бот использует exponential backoff:
Attempt 1: wait 1 sec → retry
Attempt 2: wait 2 sec → retry
Attempt 3: wait 4 sec → retry
Attempt 4: wait 8 sec → retry
Attempt 5: wait 16 sec → retry
After 5 fails: emit 'reconnectFailed', stop trying
5. Database Schema
Models
| Модель | Описание | Ключевые поля |
|---|---|---|
| Withdrawal | Запись о выводе предмета | userId, itemId, status, tradeUrl, tradeOfferId |
| User | Связь с пользователем | steamTradeUrl, steamId, steamVerified |
| Item | Связь с предметом | marketHashName, itemType |
Withdrawal Fields
| Поле | Тип | Описание |
|---|---|---|
id | String | CUID |
userId | String | FK → User |
itemId | String | FK → Item |
status | WithdrawalStatus | PENDING, PROCESSING, SENT, COMPLETED, FAILED, CANCELLED |
tradeUrl | String | Snapshot Trade URL на момент запроса |
tradeOfferId | String? | ID трейд-оффера Steam |
failureReason | String? | Причина ошибки (показывается пользователю) |
adminNote | String? | Внутренняя заметка (только админка) |
requestedAt | DateTime | Когда запросили |
completedAt | DateTime? | Когда завершили |
Relationships
6. API Endpoints
- User API
- Admin API
7. Operations
Переменные окружения
| Переменная | Обязательна | Описание |
|---|---|---|
STEAM_USERNAME | Да | Логин бот-аккаунта Steam |
STEAM_PASSWORD | Да | Пароль бот-аккаунта |
STEAM_SHARED_SECRET | Да | 2FA shared secret (из SDA .maFile) — для генерации одноразовых кодов |
STEAM_IDENTITY_SECRET | Да | 2FA identity secret (из SDA .maFile) — для подтверждения трейдов |
STEAM_ADMIN_IDS | Нет | SteamID64 через запятую — whitelist для авто-принятия входящих трейдов |
STEAM_DEVICE_ID | Нет | Device ID (генерируется автоматически если не задан) |
STEAM_API_KEY | Нет | Steam Web API Key |
STEAM_ENABLED | Нет | false для явного отключения бота |
STEAM_REQUIRED | Нет | true если бот обязателен (на prod — обязателен по умолчанию) |
Генерация 2FA кода вручную
Для ручного входа в бот-аккаунт Steam (например, чтобы скопировать Trade URL или проверить инвентарь) нужен одноразовый 2FA код. Сгенерировать его из STEAM_SHARED_SECRET:
cd ~/goloot/backend && node -e "require('dotenv').config(); console.log(require('steam-totp').generateAuthCode(process.env.STEAM_SHARED_SECRET))"
Как это работает:
dotenvзагружаетSTEAM_SHARED_SECRETизbackend/.envsteam-totpгенерирует TOTP код (аналог Steam Guard) из этого секрета- Код действует ~30 секунд — вводить сразу после генерации
Когда нужно:
- Ручной вход в бот-аккаунт через браузер/клиент Steam
- Копирование Trade URL бота
- Проверка инвентаря бота вручную
- Изменение настроек аккаунта
shared_secret и identity_secret берутся из .maFile — файла, который создаёт SDA при привязке 2FA. Этот файл содержит всё необходимое для генерации кодов и подтверждения трейдов без мобильного приложения Steam.
8. Related
- Inventory — источник виртуальных предметов для вывода
- Profile — настройка Trade URL и Steam верификация
- Live Feed — события
ITEM_WITHDRAWNв ленте - Withdraw Readiness — детали 7-точечной проверки