# API тестового стенда — контракт для клиента Всё, что нужно, чтобы подключиться к шлюзу игры «ставка на направление цены SOL» и получать события в реальном времени. Источник — код шлюза (`smart-updown-token/relayer/src/gateway.ts`, `settle.ts`). **База:** `https://testfront.binom.pw/api` CORS открыт (`Access-Control-Allow-Origin: *`) — можно дёргать прямо из браузера. > ⚠️ **`/api` в базе обязателен.** GET-ручки (`/state`, `/price`, `/balance`, `/faucet`) > отвечают и без префикса — `…/state` и `…/api/state` одно и то же. Но **POST-ручки > (`/wallet`, `/bet`) без `/api` не работают**: `POST …/wallet` → `405 Method Not Allowed`. > Держите `/api` в базе, и всё будет единообразно. **Стенд тестовый** (`SMART_MODE=test`): фантики фейковые, SOL из фаусета, контракт наш. --- ## Короткий рецепт: «поставить и получить события» ```js const BASE = 'https://testfront.binom.pw/api'; // /api обязателен, см. выше const H = { 'Content-Type': 'application/json' }; // 1. Кошелёк + деньги (только test-mode) const { address } = await (await fetch(`${BASE}/wallet`, { method: 'POST', headers: H, body: '{}' })).json(); await fetch(`${BASE}/faucet/${address}`); // наливает 5 SOL + 10 000 фантиков // 2. Подписка на события const ws = new WebSocket('wss://testfront.binom.pw/api/ws'); // только wss: страница по HTTPS 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: H, body: JSON.stringify({ address, side: 'UP', amountUnits: '500000000' }), // 500 фантиков }); // Закрывать не нужно — шлюз закроет сам по таймеру (см. «Авто-закрытие»). ``` --- ## HTTP-ручки ### `GET /state` — состояние контракта ```json { "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 ```json {"type":"price","symbol":"solusdt","price":"101.02000000","ts":1789326824749} ``` `price` — строка, чтобы не терять точность. Тик — по мере прихода из NATS (медиана ~0.5 с, провалы до нескольких секунд). ### 2. `bet_open` — ставку поставили (игра началась) ```json {"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` — ставку рассчитали (игра закончилась) ```json {"type":"bet_closed","id":"2","bettor":"8KbUFCHV…", "exitPrice":10104000000,"status":1,"statusName":"payout_done"} ``` ```json {"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) |