Оба файла описывали состояние, которого больше нет, и содержали ошибки, из-за которых внешний разработчик не собрал бы рабочий фронт: - README описывал Phantom, swap ввод/вывод (POST /api/pump/swap), STUB-ключи и Фазу 1 — ничего этого в index.html нет. Указан нерабочий API base (smart-updown.binom.pw вместо testfront.binom.pw) и адрес контракта FMM…. «Ставка через поллинг» — поллинга нет, события по WS. - API.md держал раздел «часть ставок зависает без исхода» со статусом «не исправлено» — баг закрыт 14.09.2026 в контракте. По этой доке писали бы обходной код под проблему, которой нет. README переписан под фактический index.html: HTTP-шлюз без Phantom, сценарий кошелёк→фаусет→ставка→WS-исход, три исхода, правило 409. Секция API.md заменена на «Ничья при exitPrice == entryPrice» (штатный исход, доля ~трети), историческая сноска на SEC-9 сохранена. Проверено: полный сценарий снаружи по новой доке проходит (кошелёк → фаусет 10000 → ставка id=44 → исход по балансу).
9.3 KiB
API тестового стенда — контракт для клиента
Всё, что нужно, чтобы подключиться к шлюзу игры «ставка на направление цены SOL»
и получать события в реальном времени. Источник — код шлюза
(smart-updown-token/relayer/src/gateway.ts, settle.ts).
База: https://testfront.binom.pw
Пути нормализуются: /api/state и /state — одно и то же.
CORS открыт (Access-Control-Allow-Origin: *) — можно дёргать прямо из браузера.
Стенд тестовый (SMART_MODE=test): фантики фейковые, SOL из фаусета, контракт наш.
Короткий рецепт: «поставить и получить события»
const BASE = 'https://testfront.binom.pw';
// 1. Кошелёк + деньги (только test-mode)
const { address } = await (await fetch(`${BASE}/wallet`, { method: 'POST' })).json();
await fetch(`${BASE}/faucet/${address}`); // наливает 5 SOL + 10 000 фантиков
// 2. Подписка на события
const ws = new WebSocket(`${BASE.replace('https', 'wss')}/api/ws`);
ws.onmessage = (e) => {
const m = JSON.parse(e.data);
if (m.type === 'price') console.log('курс ', m.price); // "101.02"
if (m.type === 'bet_open') console.log('ставка открыта', m.id, m.side, m.entryPrice);
if (m.type === 'bet_closed') console.log('ставка закрыта', m.id, m.exitPrice, m.statusName);
};
// 3. Ставка. ВНИМАНИЕ: поле называется amountUnits, НЕ amount!
await fetch(`${BASE}/bet`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ address, side: 'UP', amountUnits: 500000000 }), // 500 фантиков
});
// Закрывать не нужно — шлюз закроет сам по таймеру (см. «Авто-закрытие»).
HTTP-ручки
GET /state — состояние контракта
{
"counter": "3", "nextBetId": "3",
"minAmount": "100000000", "maxAmount": "10000000000",
"minAmountHuman": "100.000000", "maxAmountHuman": "10000.000000",
"multiplierBps": "13000", "expirySeconds": "15", "paused": 0,
"admin": "E7Hg55…", "relayer": "E7Hg55…",
"vaultBalance": "26000000000", "vaultBalanceHuman": "26000.000000"
}
multiplierBps: 13000 = выплата 1.3× при выигрыше. expirySeconds: 15 = окно ставки.
paused: 1 = приём ставок остановлен.
POST /wallet — создать кошелёк (только test-mode)
{} → {"address": "DxfxzWYF…"}. Денег не наливает — отдельно через /faucet.
GET /faucet/{address} — налить SOL + фантиков (только test-mode)
Работает на произвольный адрес (Phantom и т.п.):
{"address":"…","sol":5,"solLamports":5000000000,"tokenUnits":"10000000000","tokenHuman":"10000.000000"}.
Вне test-mode → 403.
GET /balance/{address} — балансы
{"sol":5,"solLamports":5000000000,"tokenUnits":"10000000000","tokenHuman":"10000.000000"}
POST /bet — поставить
Тело: {"address": "…", "side": "UP" | "DOWN", "amountUnits": 500000000}
address— обязателен, получен из/wallet(или любой пополненный через/faucet).side— регистр не важен (up/UP).amountUnits— целое в базовых единицах токена (у нашего минтаdecimals=6:500000000= 500 фантиков). Минимум100000000(100 фантиков). ⚠️ ПолеamountвместоamountUnits→400 {"error":"amountUnits must be a positive integer (token base units)"}.
Ответ 200: {"tx":"4y2HYuu…","id":"0","bettor":"…","entryPrice":10112000000}
Закрывать не нужно — шлюз закроет сам по экспирации.
Ошибки:
| Код | Когда |
|---|---|
400 |
нет/неверный amountUnits, неизвестный side, сумма вне min/max |
403 |
ставки приостановлены (paused=1) |
409 |
у адреса уже есть открытая ставка — {"error":"you already have an open bet: close it before placing a new one","openBetId":"0"} |
409 |
контракт не инициализирован |
500 |
RPC недоступен |
Правило: одна открытая ставка на адрес. Проверка идёт запросом в цепочку, не в память процесса — обойти перезапуском клиента нельзя.
GET /price — текущая цена из NATS (отладка)
{"symbol":"solusdt","price":"101.45000000","ts":1789334371122}. Пока цены нет → 503.
POST /close — НЕ СУЩЕСТВУЕТ (404)
Ручки закрытия нет намеренно: закрытие делает сервер сам по таймеру.
WebSocket: GET /api/ws
wss://testfront.binom.pw/api/ws
При подключении сервер сразу шлёт снапшот цены (если она уже есть), дальше — поток.
Все события — широковещательные: один сокет видит ставки всех игроков.
Нужны только свои — фильтруй по полю bettor.
1. price — курс SOL
{"type":"price","symbol":"solusdt","price":"101.02000000","ts":1789326824749}
price — строка, чтобы не терять точность. Тик — по мере прихода из NATS (медиана ~0.5 с,
провалы до нескольких секунд).
2. bet_open — ставку поставили (игра началась)
{"type":"bet_open","id":"2","bettor":"8KbUFCHV…","side":"DOWN",
"amountUnits":"500000000","entryPrice":10105000000,"expireTime":1789326835}
entryPrice— цена входа, целое с масштабом 1e8 (10105000000= 101.05).expireTime— unix-секунды, момент закрытия окна.
3. bet_closed — ставку рассчитали (игра закончилась)
{"type":"bet_closed","id":"2","bettor":"8KbUFCHV…",
"exitPrice":10104000000,"status":1,"statusName":"payout_done"}
{"type":"bet_closed","id":"3","bettor":"8KbUFCHV…",
"exitPrice":10105000000,"status":3,"statusName":"refunded"}
exitPrice— цена выхода, тот же масштаб 1e8.statusName:payout_done(игрок выиграл, выплата с множителем) |house_won(выиграл дом, ставка остаётся в ваулте) |refunded(ничья приexitPrice == entryPrice: залог возвращён игроку ровно суммой ставки, без множителя) |open(ещё открыта).
Исход считает контракт: UP выигрывает при exitPrice > entryPrice, DOWN — при <.
Авто-закрытие
Планировщик в шлюзе каждые 2 с (SMART_SETTLE_INTERVAL_MS, дефолт 2000) обходит
ставки программы, для просроченных берёт цену из локальной истории и зовёт close_bet.
При успехе всем в сокет уходит bet_closed. Клиент не должен ничего закрывать сам.
Ничья при exitPrice == entryPrice
Если цена на момент экспирации в точности равна цене входа, ставка не зависает:
контракт возвращает игроку ровно сумму ставки без множителя, ставка переходит в
статус 3, по WS уходит bet_closed со statusName: "refunded".
Доля таких окон — около трети ставок (замер 33.6 % на 34 769 окнах). Это нормальный штатный исход, а не ошибка: клиенту надо просто показать третий вид результата.
Рекомендация фронту: обрабатывать три исхода bet_closed (payout_done / house_won
/ refunded) и не показывать refunded как проигрыш.
Исторически (до 14.09.2026) такие ставки зависали навсегда; разбор —
pump-game-ops/docs/SECURITY.md, пунктSEC-9.
Адреса стенда
| Что | Значение |
|---|---|
| Шлюз | https://testfront.binom.pw (/api/…) |
| WS | wss://testfront.binom.pw/api/ws |
| Программа | 9ALsnxXNzDBv3mokngCHbruRTWUZiWR7pf7vswS1fCpf |
| Минт фантиков | 6fxySAjTQTkzyyZ7u8EQqv3SyJtv2Ydr6hphquHbkGS9 (decimals 6) |
| RPC localnet | http://192.168.76.181:8899 (изнутри сети; рестарт возможен) |
| Источник цены | NATS market.price.solusdt (Binance SOL/USDT) |