Всё, что ниже — из живого пет-проекта: бот начисляет участникам голосовых каналов баллы лояльности (LP), а на эти баллы люди устраивают пари в стиле Twitch Predictions. Читать далее
Ставочная система для Discord‑сервера на Java 21 + Spring Boot + JDA: почему множитель ×2 ломает экономику, как посчитать коэффициенты честно, что происходит, когда восемь одновременных кликов по кнопке пытаются потратить один и тот же баланс, и как сделать выплаты, переживающие падение сервиса на середине расчёта.
Фиксированный множитель выигрыша (ставка × 2, как в наивных реализациях) создаёт валюту из воздуха. Экономика сервера умирает за неделю.
Правильный ответ — тотализатор (pari‑mutuel): все ставки в общий пул, комиссия организатора, остаток делится между победителями пропорционально. Сумма выплат всегда равна пулу минус комиссия. Ноль эмиссии.
Кнопки Discord — это источник гонок. Спам‑клик по «Поставить» без блокировок = двойное списание и баланс в минусе.
Расчёт выплат обязан быть идемпотентным и переживать рестарт: итоги розыгрыша фиксируются один раз, каждая ставка считается в своей транзакции под флагом settled.
Тесты на всё это пишутся: Testcontainers + восемь потоков, дерущихся за один баланс.
Всё, что ниже — из живого пет‑проекта: бот начисляет участникам голосовых каналов баллы лояльности (LP), а на эти баллы люди устраивают пари в стиле Twitch Predictions.
Откуда взялась задачаУ меня есть Discord‑сервер, где люди сидят в голосовых каналах. Бот раз в 5 минут начисляет за это баллы: обычному участнику 100 LP, зрителю стрима 150, самому стримеру 200 (если есть кто‑то, кто его слушает). Баллы можно тратить на бесполезные, но приятные вещи: отключить кого‑нибудь от голосового канала за 10 000 LP или замьютить за 50 000 LP.
Дальше случилось предсказуемое: у людей накопились баллы, и им захотелось на них спорить. «Скинем ли мы этого босса с первой попытки?», «Придёт ли Вася сегодня в войс?». Так появилась команда /lp-pari <название>, которая публикует в канал сообщение‑опрос с кнопками «Да» и «Нет»:
🎲 Скинем босса с первой попытки?
Автор: @user
✅ Да ❌ Нет Итого
3 ставки 1 ставка 4 ставки
500 LP 500 LP Пул: 1000 LP
×1.90 ×1.90 Комиссия: 50 LP
Приём ставок открыт
Участник жмёт кнопку, вводит сумму в модальном окне, сумма немедленно списывается с баланса. Автор пари ставить не может, менять выбор нельзя, поставить на оба исхода нельзя. Управление («Остановить ставки», «Завершить: ДА», «Завершить: НЕТ», «Отменить») доступно только автору.
Выглядит как задача на вечер. На практике вечер ушёл на UI, а следующие несколько — на то, чтобы система не разваливалась от арифметики, гонок и рестартов.
Ошибка № 1: множитель ×2, или как напечатать деньгиПервая версия выплат была такой, какой её пишут все:
case FINISHED -> {
boolean won = Objects.equals(pari.getWinningOption(), bet.getOption());
payout = won ? bet.getAmount() * PariService.WIN_MULTIPLIER : 0L; // WIN_MULTIPLIER = 2
reason = TransactionReason.BET_WIN;
}
Угадал — получи вдвое, не угадал — ставка сгорела. Интуитивно кажется, что это честно и сбалансированно: одни теряют, другие получают.
Посчитаем. Пусть на «Да» поставили 900 LP, на «Нет» — 100 LP. Побеждает «Да»:
Собрано с проигравших | 100 LP |
Выплачено победителям | 900 × 2 = 1800 LP |
Эмиссия из воздуха | 1700 LP |
Система обязана выплатить 1800 LP, а «сгорело» всего 100. Разницу бот берёт… ниоткуда. Он просто дописывает число в колонку balance.
И это не редкий случай, а типичный: люди ставят на очевидный исход. Именно очевидные исходы и выигрывают чаще всего. То есть система печатает деньги не в исключительных ситуациях, а по умолчанию, в большинстве розыгрышей.
Обратный случай не лучше: если на победивший вариант поставили 100 LP, а на проигравший 900 LP, то 700 LP просто исчезают из экономики. Ни игрокам, ни организатору.
Итог: денежная масса на сервере ходит ходуном, накопленные за месяцы сидения в войсе баллы обесцениваются за пару крупных пари, и «замьютить кого‑то за 50 000 LP» перестаёт быть достижением. Экономика, в которой единственный источник эмиссии должен быть трудовым (сиди в войсе — получай), внезапно получает второй источник, работающий в тысячу раз быстрее.
Решение: тотализаторВзять схему со скачек мне подсказал @Z3R0ing — за что ему отдельное спасибо. Это pari‑mutuel, он же тотализатор, которому сто лет на ипподромах. Никаких заранее известных коэффициентов: все ставки складываются в общий котёл, организатор удерживает комиссию, а остаток делится между победителями пропорционально их ставкам.
total_pool = сумма всех ставок пари
commission = ⌊total_pool × commission_rate⌋
prize_pool = total_pool − commission
коэффициент варианта = prize_pool / пул этого варианта
выплата победителю = ⌊ставка × prize_pool / winning_sum⌋
Ключевое свойство: сумма всех выплат равна общему пулу за вычетом комиссии. Всегда. Игра строго с нулевой суммой (точнее — с отрицательной на величину комиссии, что как раз делает пари лёгким стоком лишней валюты, а не источником).
Весь расчёт — в одном классе без состояния и без похода в базу:
/** Комиссия организатора, удерживаемая из общего пула. */
public static long commission(long totalPool, BigDecimal rate) {
if (totalPool <= 0 || rate == null || rate.signum() <= 0) {
return 0L;
}
return BigDecimal.valueOf(totalPool)
.multiply(rate)
.setScale(0, RoundingMode.FLOOR)
.longValueExact();
}
/**
* Выплата по выигравшей ставке: доля призового фонда, пропорциональная размеру ставки.
* Считается как amount * prizePool / winningSum без промежуточного округления
* коэффициента, поэтому результат не «плывёт» от порядка обработки ставок.
*/
public static long payout(long betAmount, long prizePool, long winningSum) {
if (betAmount <= 0 || prizePool <= 0 || winningSum <= 0) {
return 0L;
}
return BigDecimal.valueOf(betAmount)
.multiply(BigDecimal.valueOf(prizePool))
.divide(BigDecimal.valueOf(winningSum), 0, RoundingMode.FLOOR)
.longValueExact();
}
Три детали, каждая из которых лечит отдельный класс багов:
Никакой плавающей точки. Балансы — целые long, вся арифметика на BigDecimal с явным RoundingMode. double в денежных расчётах — это когда пользователь получает 569.9999999999999 LP и открывает тикет.
Округление вниз, всегда. Сумма выплат из‑за этого никогда не превышает призовой фонд. Неразделённый остаток (не более 1 LP на победителя) остаётся вместе с комиссией. Округление вверх при 200 победителях подарило бы им 200 LP из ниоткуда — то есть ровно ту проблему, ради которой всё затевалось.
Умножаем до деления. amount × prizePool / winningSum, а не amount × коэффициент. Если сначала округлить коэффициент до 4 знаков, а потом умножать, суммарная выплата разъедется с призовым фондом, и разница будет тем больше, чем больше участников.
Комиссия 5%. На «Да» поставили 300 и 200 LP, на «Нет» — 500 LP.
Величина | Значение |
|---|---|
Общий пул | 1000 LP |
Комиссия (5%) | 50 LP |
Призовой фонд | 950 LP |
Коэффициент «Да» | 950 / 500 = 1.90 |
Коэффициент «Нет» | 950 / 500 = 1.90 |
Победило «Да»: поставивший 300 получает 300 × 950 / 500 = 570 LP, поставивший 200 — 380 LP. Итого выплачено 950 — ровно призовой фонд, ни баллом больше.
Пока пари открыто, коэффициент пересчитывается с каждой новой ставкой: чем больше поставлено на вариант, тем ниже выплата на единицу ставки. В опросе показывается текущий коэффициент, а итоговым становится тот, что зафиксирован в момент объявления исхода.
Отсюда следует свойство, которое игроки узнают тяжело и обычно в комментариях к боту: коэффициент может оказаться меньше единицы. Если 95% пула поставлено на «Да» и «Да» побеждает — делить, кроме собственных денег этих же людей за вычетом комиссии, нечего. Ставишь на очевидное — получаешь обратно свою ставку минус комиссия.
Это не баг, это ровно тот механизм, который в схеме ×2 отсутствовал и из‑за отсутствия которого печатались деньги. Поэтому в подтверждении ставки бот прямым текстом предупреждает:
return "Ставка принята: **" + bet.getAmount() + "** LP на вариант «"
+ PariMessageService.optionName(option) + "». Текущий коэффициент: **"
+ PariMessageService.formatCoefficient(odds.coefficient(option))
+ "**, он изменится с новыми ставками.";
Комиссия фиксируется при созданииСтавка комиссии берётся из настройки PARI_COMMISSION_RATE (по умолчанию 5%), но записывается в строку пари в момент создания:
ALTER TABLE paris ADD COLUMN commission_rate NUMERIC(5, 4) NOT NULL DEFAULT 0;
Иначе админ, поправивший конфиг на горячую, изменил бы правила уже идущей игры — люди ставили при 5%, а получили при 20%. Некорректное значение (отрицательное или ≥ 1) трактуется как ноль с предупреждением в лог: лучше бесплатное пари, чем упавший бот.
Особые случаи, без которых всё разваливаетсяНа победивший вариант никто не поставил (winning_sum = 0). Делить призовой фонд не между кем. Наивная реализация здесь делит на ноль или тихо съедает весь пул. Правильное поведение — вернуть ставки всем участникам полностью, без комиссии.
Пари отменено — полный возврат всем.
Пари забыли. Открытое пари висит вечно, и ставки в нём заморожены навсегда. Планировщик раз в минуту отменяет пари старше PARI_TIMEOUT_HOURS (по умолчанию 24 ч) с полным возвратом.
Дальше начинается самое интересное, и оно уже не про экономику, а про конкурентный доступ.
Пользователь с балансом 1000 LP нажимает «Да», вводит 900 LP — и в этот момент у него лагает клиент. Он жмёт ещё раз. И ещё. Discord честно доставляет боту все взаимодействия, JDA обрабатывает их параллельно, в разных потоках. Наивный код
проверить баланс → списать → записать ставку
в восьми потоках даст восемь ставок по 900 LP с баланса в 1000 LP и итоговый баланс −6200.
Правильный приём ставки — одна транзакция со строгим порядком действий:
@Transactional
public PariBet placeBet(Long pariId, Guild guild, User user, boolean option, long amount) {
// 1. Блокируем пари: пока принимается ставка, статус не сможет измениться.
Pari pari = pariRepository.findByIdForUpdate(pariId)
.orElseThrow(() -> new PariException("Пари не найдено."));
// 2. Проверки: та же гильдия, статус OPEN, вызывающий не автор.
if (pari.getStatus() != PariStatus.OPEN) {
throw new PariException("Приём ставок по этому пари уже закрыт.");
}
if (pari.getAuthorId().equals(user.getId())) {
throw new PariException("Автор не может делать ставки в собственном пари.");
}
GuildMember member = guildMemberService.getOrCreateMember(guild, user);
// 3. Повторное чтение под блокировкой: с этого момента баланс не изменит никто другой.
GuildMember lockedMember = guildMemberRepository.findByIdForUpdate(member.getId())
.orElseThrow(() -> new PariException("Участник не найден."));
// 4. Проверку существующей ставки делаем уже под блокировкой баланса, иначе два
// одновременных клика могли бы оба увидеть, что ставки ещё нет.
if (pariBetRepository.findByPariIdAndMemberId(pariId, lockedMember.getId()).isPresent()) {
throw new PariException("Вы уже сделали ставку в этом пари. Изменить выбор нельзя.");
}
if (lockedMember.getBalance() < amount) {
throw new PariException("Недостаточно поинтов...");
}
// 5. Списание, запись ставки и транзакции BET_HOLD.
lockedMember.setBalance(lockedMember.getBalance() - amount);
...
}
Здесь важен каждый пункт, но особенно два.
Блокировка пари (шаг 1) — не про баланс, а про статус. Без неё возможен сценарий: участник начал ставить, автор в этот момент нажал «Завершить», расчёт прошёл по ставкам, существовавшим на тот момент, — и следом закоммитилась ставка, которая уже никогда не будет рассчитана. Деньги списаны, ставка в статусе settled = false, пари закрыто. Блокировка строки пари заставляет кнопку «Завершить» подождать.
Порядок шагов 3 и 4 принципиален. Если проверять «а нет ли уже ставки» до захвата блокировки, два одновременных клика успеют оба увидеть, что ставки нет, и оба пройдут дальше. Классическая TOCTOU‑гонка: проверка и действие обязаны быть по одну сторону блокировки.
Блокировки всегда берутся в одном порядке — пари → баланс, поэтому дедлоков не возникает. Это то самое правило, которое легко нарушить, когда через полгода добавляешь новую операцию и берёшь блокировки в удобном для неё порядке.
Сами блокировки — обычный пессимистичный SELECT ... FOR UPDATE через Spring Data:
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT m FROM GuildMember m WHERE m.id = :id")
Optional<GuildMember> findByIdForUpdate(@Param("id") Long id);
Оборона в глубинуЛогика в сервисе — не единственный рубеж. За ней стоит база:
CONSTRAINT uk_pari_bet_pari_member UNIQUE (pari_id, member_id),
CONSTRAINT ck_pari_bet_amount_positive CHECK (amount > 0)
ALTER TABLE guild_members ADD CONSTRAINT ck_guild_members_balance_non_negative
CHECK (balance >= 0) NOT VALID;
Уникальный индекс (pari_id, member_id) — последний рубеж против гонки: если приложение всё‑таки прозевало, вставка упадёт, и DataIntegrityViolationException превратится в понятное пользователю сообщение вместо стектрейса:
try {
// saveAndFlush, а не save: исключение нужно поймать здесь, а не при коммите
bet = pariBetRepository.saveAndFlush(bet);
} catch (DataIntegrityViolationException e) {
throw new PariException("Вы уже сделали ставку в этом пари. Изменить выбор нельзя.");
}
А CHECK (balance >= 0) гарантирует, что никакой код в приложении — ни ставки, ни будущая фича, которую я напишу через год в три часа ночи, — не уведёт баланс в минус. Это инвариант данных, и жить он должен там, где данные.
Пари с сотней участников — это сто начислений. Наивно это делается одной транзакцией на весь розыгрыш, и наивно это плохо по трём причинам: длинная транзакция держит блокировки, падение на 87-м участнике откатывает первых 86, а рестарт контейнера посреди расчёта оставляет половину людей без выплат.
Решение — разделить объявление исхода и начисление.
Шаг 1: фиксируем итоги ровно один раз@Transactional
public Pari finish(Long pariId, String actorId, boolean winningOption) {
Pari pari = lockForAuthor(pariId, actorId); // SELECT ... FOR UPDATE + проверка авторства
requireActive(pari);
PariStats stats = getStats(pariId);
long totalPool = stats.totalPool();
long winningSum = winningOption ? stats.yesPool() : stats.noPool();
long prizePool = PariPayoutCalculator.prizePool(totalPool, pari.getCommissionRate());
pari.setStatus(PariStatus.FINISHED);
pari.setWinningOption(winningOption);
pari.setTotalPool(totalPool);
pari.setPrizePool(prizePool);
pari.setWinningSum(winningSum);
pari.setWinningCoefficient(PariPayoutCalculator.coefficient(prizePool, winningSum));
pari.setClosedAt(Instant.now());
return pariRepository.save(pari);
}
Здесь важно не то, что итоги считаются, а что они сохраняются в строку пари:
ALTER TABLE paris ADD COLUMN total_pool BIGINT;
ALTER TABLE paris ADD COLUMN prize_pool BIGINT;
ALTER TABLE paris ADD COLUMN winning_sum BIGINT;
ALTER TABLE paris ADD COLUMN winning_coefficient NUMERIC(18, 4);
Выплата по каждой ставке считается только из этих зафиксированных чисел и никогда не пересчитывается по текущему состоянию пула. Иначе результат зависел бы от момента запуска расчёта — а расчёт, как мы увидим, может запуститься дважды и с разницей в минуту. Это ровно та ошибка, которую в схеме с фиксированным множителем не заметить: там результат от состояния пула не зависит вовсе.
Шаг 2: каждая ставка — своя транзакция@Transactional
public boolean settleBet(Long betId) {
PariBet bet = pariBetRepository.findByIdForUpdate(betId).orElse(null);
if (bet == null) { return false; }
if (bet.isSettled()) {
return false; // Уже рассчитана — повторное начисление исключено.
}
Payout payout = resolvePayout(bet); // из зафиксированных итогов пари
if (payout.amount() > 0) {
GuildMember member = guildMemberRepository.findByIdForUpdate(bet.getMember().getId())...;
member.setBalance(member.getBalance() + payout.amount());
// + запись в журнал транзакций
}
bet.setSettled(true); // флаг и начисление коммитятся вместе
bet.setPayout(payout.amount());
bet.setSettledAt(now);
return true;
}
settleBet живёт в отдельном бине — не потому что так красивее, а потому что @Transactional в Spring работает через прокси: вызов приватного метода того же класса не создаст новую транзакцию, и вся идея развалится молча.
Начисление и установка флага коммитятся вместе — значит, либо участник получил выплату и ставка помечена, либо не произошло ничего. Третьего состояния не существует.
Шаг 3: батч и восстановлениеБатч перебирает нерассчитанные ставки порциями по 200 и, если за проход не удалось рассчитать ни одной, останавливается, чтобы не крутиться вхолостую при недоступной базе:
if (processedInBatch == 0) {
log.warn("Расчёт пари {} не продвигается: осталось {} нерассчитанных ставок",
pariId, betIds.size());
return processed;
}
Пари помечается рассчитанным (settled_at) только когда не осталось ни одной ставки с settled = false. Пока это не так, его подбирает планировщик:
@Scheduled(fixedDelay = 60_000L)
public void recoverPendingSettlements() {
List<Pari> pending = pariRepository.findByStatusInAndSettledAtIsNull(
List.of(PariStatus.FINISHED, PariStatus.CANCELED), PageRequest.of(0, 20));
for (Pari pari : pending) {
settle(pari.getId());
pariMessageService.refresh(pari.getId());
}
}
Итоговые свойства расчёта:
повторный запуск безопасен — рассчитанные ставки пропускаются по флагу;
сбой на одном участнике не откатывает остальных — у каждого своя транзакция;
расчёт переживает рестарт — недоделанное подберёт планировщик в течение минуты;
результат не зависит от того, когда именно прошёл расчёт — он берётся из зафиксированных итогов.
После расчёта бот публикует в канал сводку — ответом на сообщение‑опрос:
🏆 Победители
@user1 — 300 LP → 570 LP (+270)
@user2 — 200 LP → 380 LP (+180)
💀 Проигравшие
@user3 — 500 LP → 0 LP (−500)
И тут же вылезает проблема: расчёт может запустить кнопка «Завершить», а через минуту — восстановительный планировщик. Оба честно дойдут до конца и оба захотят опубликовать итоги. Пользователь получит сводку дважды.
Городить synchronized бессмысленно (в нескольких экземплярах сервиса он не работает), проверять if (resultsPostedAt == null) — та же TOCTOU‑гонка, что и со ставками. Право на публикацию берётся атомарным UPDATE:
@Modifying(clearAutomatically = true)
@Query("UPDATE Pari p SET p.resultsPostedAt = :now WHERE p.id = :id AND p.resultsPostedAt IS NULL")
int markResultsPosted(@Param("id") Long id, @Param("now") Instant now);
Вернулась единица — публикуешь ты. Ноль — кто‑то успел раньше. Одна строка SQL вместо распределённой блокировки.
Пара деталей, которые дались опытом:
отметка ставится только после того, как канал найден: недоступный канал не должен «съедать» право на публикацию;
участники упоминаются через <@id> — внутри эмбеда это рендерится как ник, но не рассылает пинги, поэтому сводка на 50 человек не превращается в ковровую бомбардировку уведомлениями;
список обрезается по лимиту поля эмбеда (1024 символа) в строку «…и ещё N участников», иначе Discord просто отклонит сообщение;
ответ на сообщение пари отправляется с failOnInvalidReply(false): если опрос удалили, сводка всё равно уйдёт в канал обычным сообщением;
ошибки отправки только логируются: доставка сообщения не влияет на движение средств. Деньги уже у людей, а сообщение — это всего лишь сообщение.
Взаимодействие с Discord мокается целиком: JDA‑объекты (Guild, Member, User, события) — интерфейсы, а асинхронные RestAction проверяются через захват success/failure‑колбэков и их ручной вызов. Арифметика тотализатора покрыта юнит‑тестами до последнего округления.
А вот гонки юнит‑тестами не проверишь. Для них — Testcontainers с настоящим PostgreSQL и восемь потоков, стартующих одновременно по CountDownLatch:
@Test
void concurrentClicksProduceExactlyOneBet() throws Exception {
User spammer = user("spammer");
giveBalance(guild, spammer, 1_000L);
Pari pari = pariService.createPari(guild, author, "Спам-клики");
int threads = 8;
CountDownLatch start = new CountDownLatch(1);
AtomicInteger accepted = new AtomicInteger();
for (int i = 0; i < threads; i++) {
pool.submit(() -> {
start.await();
pariService.placeBet(pari.getId(), guild, spammer, true, 900L);
accepted.incrementAndGet();
});
}
start.countDown();
// ...
assertThat(accepted.get()).isEqualTo(1);
assertThat(balanceOf(spammer)).isEqualTo(100L); // 1000 − 900, а не минус шесть тысяч
}
Рядом — тест, который проверяет, что база сама не даст записать отрицательный баланс, и тест на повторный расчёт: settleAll() вызывается дважды, балансы сверяются один раз.
Про H2 вместо Postgres здесь можно сразу забыть: SELECT ... FOR UPDATE, CHECK‑констрейнты и поведение при конфликте уникального индекса — это ровно те вещи, ради которых и нужен настоящий движок. Контейнер поднимается один раз на всю JVM и переиспользуется всеми интеграционными тестами.
Экономика игровой валюты — это инженерная задача, а не UX‑мелочь. Прежде чем писать код выплат, полезно ответить на вопрос «откуда берутся деньги и куда деваются». Если ответ «из воздуха» — переписывать придётся, вопрос только когда.
Кнопка в мессенджере — это распределённая система. Пользователь кликнет дважды, сеть задублирует, клиент отправит ретрай. Любой обработчик, трогающий деньги, должен переживать повторную доставку.
Проверка и действие обязаны быть по одну сторону блокировки. Это правило нарушается незаметно и ломается редко — то есть на проде и в самый неудачный момент.
Идемпотентность дешевле компенсаций. Флаг settled + зафиксированные итоги + планировщик дорасчёта — три простые вещи, которые заменяют собой целый пласт разбирательств «а почему Васе начислили дважды».
Инварианты денег живут в базе. CHECK (balance >= 0) и уникальный индекс не заменяют логику в сервисе, но страхуют её от всего кода, который будет написан после.
Java 21, Spring Boot (WebMVC, Data JPA, Thymeleaf), JDA 6, PostgreSQL, Flyway, Gradle, JUnit 5 + Mockito + AssertJ + Testcontainers, JaCoCo.
Отдельно в проекте живёт веб‑дашборд с балансами и временем в голосовых каналах — причём время нигде не хранится, а восстанавливается из журнала начислений: баллы капают фиксированными порциями раз в 5 минут, значит Σ (баллы по причине / начисление за интервал) × 5 минут даёт время. Но это уже тема для отдельной статьи.
| # | Наименование новости | Тональность | Информативность | Дата публикации |
|---|---|---|---|---|
| 1 | Почему LLM никогда не пройдут тест Тьюринга в Tinder: изнанка дейтинг-аутстаффа | 0 | 9.37 | 22-09-2026 |
| 2 | Мой бот назвали «очередным поделием с ИИ». Комментатор прав | -2 | 6 | 07-07-2026 |
| 3 | Интеграция платёжных систем в high-risk вертикалях: архитектура, риски и практический опыт | 0 | 7 | 07-07-2026 |
| 4 | Одна карточка на всех без сервера: детерминированная генерация, RLS и флаг retired в шахматном бинго | 0 | 5.67 | 22-09-2026 |
| 5 | Кнопка нажата, а ответа нет: как не потерять действие пользователя и не наделать дублей | 0 | 8.2 | 19-09-2026 |
| 6 | Результаты на 30 августа: 💚Основа: VIP 4900р 19:30 Кф=1.6!✅ NOSTR ... | 0 | 42.51 | 30-08-2026 |
| 7 | Результаты на 20.07.26: 💚Основа: VIP 4900р 19:30 Кф=1.6!✅ NOSTR 3900р ... | 0 | 5 | 21-07-2026 |
| 8 | ЛДПР запустила официальный Telegram-бот цифрового алгоритма "Жириновский" | 0 | 0 | 19-08-2023 |
| 9 | Мошенники начали добавлять жертв в чаты с "соседями" для хищения денег | 0 | 10 | 28-07-2026 |