Своя игра в сети «Стрекоза»
Разместите браузерную игру под доменом dragonfly-flash.com: игроки входят своим аккаунтом Стрекозы, игра сразу появляется на сайте и в приложении Android, покупки внутри игры принимаем через Robokassa, а ваша доля копится в кабинете и выплачивается вам.
Как это устроено
Игра работает на вашем сервере. У нас — страница https://dragonfly-flash.com/play/<адрес-игры>,
которая показывает вашу игру в рамке (iframe) и даёт ей мост к Стрекозе. Вы обновляете игру у себя когда угодно —
нам ничего перезапускать не нужно.
Почему рамка. Код вашей игры не исполняется на нашем домене и не видит сессии игроков, а наш код не видит вашу игру. Общаемся только через SDK — это безопасно для обеих сторон.
Быстрый старт
- Создайте игру в кабинете: название, адрес на Стрекозе (например
my-game→/play/my-game), адрес страницы игры на вашем сервере (https://…). Игра сразу появится в тестовом режиме на сайте и в приложении — видите её только вы и ваши тестеры. - Выпустите тестовый ключ (вкладка «Интеграция»). Ключ вида
dfk_test_…показывается один раз — положите его в настройки сервера игры (переменная окружения), никогда не в код страницы. - Подключите SDK на странице игры:
<script src="https://dragonfly-flash.com/static/devdocs/dragonfly-games.js"></script> - Вход:
DragonflyGames.login()→ билет → ваш сервер →POST /api/dev/v1/auth/verify→ игрок. - Покупки: ваш сервер создаёт заказ
POST /api/dev/v1/orders, страница вызываетDragonflyGames.pay(order.id), вы выдаёте покупку по вебхукуorder.paid. - Проверьте всё в тестовом режиме (деньги не списываются) и нажмите «Отправить на публикацию». После проверки игра
появится в каталоге у всех, а в кабинете станет доступен боевой ключ
dfk_live_….
Тестовый режим и отладка
- Пока игра не опубликована, она в тестовом режиме: видят только автор, тестеры (добавляются по нику во вкладке «Тестеры»)
и администрация. Все платежи — тестовые: вместо Robokassa окно «Оплатить (тест)», деньги не списываются,
а ваш сервер получает настоящий вебхук
order.paidс"mode": "test". - У тестового режима свои ключи (
dfk_test_…), свои заказы и свои билеты. Тестовый билет принимается только тестовым ключом, боевой — только боевым. Так тестовая покупка никогда не выдаст боевой товар. - Отладка с вашего компьютера. В поле «Тестовый адрес» можно указать
http://localhost:3000/— в тестовом режиме страница/play/my-gameоткроет в рамке игру прямо с вашего компьютера. Сервер Стрекозы при этом к вам не ходит: ваш локальный сервер сам вызывает наш API по ключу. - После публикации тестовый режим остаётся:
https://dragonfly-flash.com/play/my-game?test=1— для автора и тестеров. - Вебхуки на
localhostи во внутренние сети не отправляются. Для локальной отладки используйте туннель (ngrok, cloudflared) или опросGET /api/dev/v1/orders?status=paid. - Страница игры получает в адресе параметры:
df_mode(test/live),df_platform(web/android),df_game(адрес игры).
SDK на странице игры
<script src="https://dragonfly-flash.com/static/devdocs/dragonfly-games.js"></script>
<script>
DragonflyGames.environment; // 'site' | 'android' | 'standalone'
const info = await DragonflyGames.init();
// {sdk: 1, mode: 'test', platform: 'web', game: 'my-game', logged_in: true, nick: 'Вася', lang: 'ru'}
const auth = await DragonflyGames.login();
// {ticket: 'dft1.…', mode: 'test', player_id: 'p3f2a…', nick: 'Вася', expires_in: 300}
// Не входил на сайт — Стрекоза сама предложит войти. Отказался — ошибка 'not_logged_in'.
const res = await DragonflyGames.pay(orderId);
// {status: 'paid' | 'canceled' | 'pending' | 'expired', order: {...}}
DragonflyGames.fullscreen(); // «во весь экран»
DragonflyGames.exit(); // выйти обратно в Стрекозу
DragonflyGames.on('pause', () => {}); // приложение свернули
DragonflyGames.on('resume', () => {});
DragonflyGames.on('back', () => {}); // Android: нажали «Назад»
</script>
pay() — только для интерфейса. Покупку выдаёт ваш сервер по вебхуку
(или по статусу заказа из API), а не страница: страницу игрок может подделать. Статус pending значит, что игрок
закрыл окно ожидания, а оплата может прийти позже — вебхук придёт сам.Открыли страницу игры напрямую (не на Стрекозе) — environment === 'standalone', вход и оплата отвечают ошибкой
'standalone'. Покажите в этом случае ссылку на https://dragonfly-flash.com/play/my-game.
Вход игрока
Билет из login() отправьте своему серверу, а он проверит его у Стрекозы:
POST https://dragonfly-flash.com/api/dev/v1/auth/verify
Authorization: Bearer dfk_test_…
Content-Type: application/json
{"ticket": "dft1.…"}
→ 200
{
"player_id": "p3f2a9c01d4e5b6a7", // постоянный id игрока в ВАШЕЙ игре
"nick": "Вася",
"avatar_url": "https://dragonfly-flash.com/api/avatars/…jpg", // или null, если аватара нет
"mode": "test", // test | live — совпадает с режимом ключа
"platform": "web", // web | android
"role": "tester", // owner | tester | admin | player
"expires_at": 1792995532
}
player_id— обезличенный и постоянный: у одного человека в разных играх разные id, а id аккаунта Стрекозы вы не видите. Храните прогресс поplayer_id.- Билет живёт 5 минут. После проверки выдайте игроку свою сессию (cookie или токен вашего сервера).
- Ник и аватар могут меняться:
GET /api/dev/v1/players/{player_id}вернёт свежие. avatar_urlбываетnull: покажите свою заглушку. Обычный<img src>работает сразу; для canvas, WebGL, Phaser, Pixi или Three.js загружайте картинку сcrossOrigin = 'anonymous', иначе браузер не отдаст её в текстуру:const img = new Image(); img.crossOrigin = 'anonymous'; img.src = player.avatar_url;
Покупки
1. Ваш сервер создаёт заказ
POST https://dragonfly-flash.com/api/dev/v1/orders
Authorization: Bearer dfk_test_…
Content-Type: application/json
{
"player_id": "p3f2a9c01d4e5b6a7",
"order_id": "gems-100-8f1c", // ВАШ уникальный id покупки: латиница, цифры, _ . : - (до 64)
"amount": 149, // рубли: тест — от 1, боевой — от 10 до 15000
"title": "100 кристаллов", // увидит игрок в окне оплаты и в чеке
"payload": {"item": "gems", "qty": 100} // необязательно: ваши данные (до 2000 символов), вернутся в вебхуке
}
→ 200
{"id": 42, "order_id": "gems-100-8f1c", "player_id": "p3f2…", "mode": "test", "title": "100 кристаллов",
"amount": 149.0, "currency": "RUB", "status": "created", "created_at": "…Z", "expires_at": "…Z", "paid_at": null,
"dev_amount": 104.3, "fee_pct": 30.0, "payload": "{\"item\": \"gems\", \"qty\": 100}",
"webhook": {"state": "none", "attempts": 0, "last_error": null}}
- Повтор с тем же
order_id, игроком и суммой вернёт тот же заказ — можно безопасно повторять запрос при сбое сети. Тот жеorder_idс другой суммой — ошибка 409. - Заказ живёт 24 часа, потом его статус —
expired. dev_amount— ваша доля с этого платежа.
2. Страница игры открывает оплату
const res = await DragonflyGames.pay(order.id); // id — число из ответа сервера, не ваш order_id
if (res.status === 'paid') showSpinner('Покупка уже в пути…');
В тестовом режиме игрок увидит окно «Тестовый платёж» с кнопкой «Оплатить (тест)». В боевом — окно с суммой и переход на защищённую страницу Robokassa (карта, СБП), чек придёт игроку на почту.
3. Ваш сервер выдаёт покупку
По вебхуку order.paid (см. ниже) или, если вебхук не настроен, опросом
GET /api/dev/v1/orders/{order_id} / GET /api/dev/v1/orders?status=paid&since_id=N.
Выдавайте покупку один раз: запоминайте у себя order_id выданных заказов.
Вебхук и подпись
Укажите адрес вебхука в кабинете (вкладка «Интеграция», только https). Когда заказ оплачен, Стрекоза отправит:
POST https://game.example.com/dragonfly/webhook
Content-Type: application/json
X-Dragonfly-Event: order.paid
X-Dragonfly-Signature: t=1792995532,v1=5c1e…(hex)
{"event": "order.paid", "mode": "live", "game": "my-game", "sent_at": "2026-10-26T12:00:00Z",
"order": {"id": 42, "order_id": "gems-100-8f1c", "player_id": "p3f2…", "status": "paid", "amount": 149.0,
"paid_at": "…Z", "payload": "…", …}}
Проверка подписи: v1 = HMAC_SHA256(секрет, t + "." + тело_запроса_как_есть) в hex.
Секрет whsec_… — в кабинете. Отклоняйте запросы с неверной подписью и с t старше 5 минут.
- Ответ 2xx — доставлено. Любой другой ответ или нет ответа за 10 секунд — повторим через 10 с, 30 с, 2 мин, 10 мин, 30 мин, 1 ч, 3 ч, 6 ч, 12 ч, 24 ч. Повторную доставку можно запросить в кабинете (вкладка «Заказы»).
- Вебхук может прийти дважды — выдавайте покупку идемпотентно по
order_id. - Кнопка «Проверить связь» шлёт событие
ping— на него тоже отвечайте 2xx. - Режим заказа — в поле
mode: тестовые покупки не путайте с боевыми.
Приложение Android
- Каталог «Игры» в приложении «Стрекоза XP» берётся с сервера: новая игра появляется в приложении сразу, без его обновления. Тестовая — только у автора и тестеров (с пометкой «Тест»).
- В приложении игра открывается без рамки: страница вашей игры загружается в окне приложения, а SDK работает через встроенный мост
(
DragonflyGames.environment === 'android'). Код игры тот же — ничего отдельно делать не нужно. - Ориентация экрана — настройка «Ориентация в приложении» в кабинете (любая, альбомная, книжная).
- Кнопка «Назад»: приложение шлёт событие
backи спрашивает игрока, выйти ли из игры. - Боевая оплата в приложении открывает страницу Robokassa в браузере телефона; после оплаты игрок возвращается в приложение, а покупка приходит вашим вебхуком.
- Входы из приложения видны в кабинете («Последний вход из приложения») — так вы проверите, что игра работает на телефоне.
Справочник API
Базовый адрес: https://dragonfly-flash.com/api/dev/v1. Авторизация: Authorization: Bearer <ключ>.
Ответы — JSON, деньги — рубли, время — ISO 8601 в UTC (…Z). Вызывайте API только со своего сервера.
| Запрос | Что делает |
|---|---|
GET /ping | Проверка ключа: {ok, game, mode, status, server_time} |
POST /auth/verify | Проверить билет входа {ticket} → игрок |
GET /players/{player_id} | Ник и аватар игрока |
POST /orders | Создать заказ {player_id, order_id, amount, title, payload?} |
GET /orders/{order_id} | Заказ по вашему order_id |
GET /orders?status=paid&since_id=0&limit=100 | Список заказов режима ключа (сверка, опрос). since_id — последний полученный id |
Статусы заказа: created → paid | canceled (игрок отказался) | expired (прошло 24 ч).
Оплаченный заказ остаётся paid навсегда.
Лимит частоты — 20 запросов в секунду с одного адреса (с запасом 60). Превышение — HTTP 429, повторите через секунду.
Ошибки
Ошибка — HTTP-код и {"detail": "понятный текст по-русски"}.
| Код | Когда |
|---|---|
| 400 | Неверные данные (сумма, order_id, title); боевой ключ до публикации |
| 401 | Нет ключа, ключ отозван; билет неверный или истёк |
| 403 | Билет другой игры или другого режима; игра заблокирована или не опубликована (для боевого ключа) |
| 404 | Игрок или заказ не найден |
| 409 | order_id уже использован для другой покупки |
| 429 | Слишком часто |
| 5xx | Сбой у нас — повторите запрос позже |
Готовые примеры
Минимальная игра целиком: страница с SDK и сервер, который проверяет вход, создаёт заказ и выдаёт покупку по вебхуку.
Запустите сервер на localhost:3000, укажите в кабинете тестовый адрес http://localhost:3000/ и откройте
/play/<ваша-игра> на Стрекозе.
Страница игры (index.html)
<!DOCTYPE html>
<html lang="ru"><head><meta charset="utf-8"><title>Моя игра</title>
<script src="https://dragonfly-flash.com/static/devdocs/dragonfly-games.js"></script></head>
<body>
<p id="who">Загрузка…</p>
<p>Кристаллы: <b id="gems">0</b></p>
<button id="buy" disabled>Купить 100 кристаллов за 149 ₽</button>
<script>
const $ = (id) => document.getElementById(id);
let session = null; // токен НАШЕГО сервера после входа
async function api(path, body) {
const r = await fetch(path, {method: 'POST', headers: {'Content-Type': 'application/json',
'Authorization': session ? 'Bearer ' + session : ''}, body: JSON.stringify(body || {})});
const d = await r.json();
if (!r.ok) throw new Error(d.detail || r.status);
return d;
}
async function start() {
if (DragonflyGames.environment === 'standalone') {
$('who').textContent = 'Откройте игру на Стрекозе: https://dragonfly-flash.com/play/my-game';
return;
}
const auth = await DragonflyGames.login(); // билет от Стрекозы
const me = await api('/api/login', {ticket: auth.ticket});
session = me.session;
$('who').textContent = 'Привет, ' + me.nick + (auth.mode === 'test' ? ' (тестовый режим)' : '');
$('gems').textContent = me.gems;
$('buy').disabled = false;
}
$('buy').onclick = async () => {
const order = await api('/api/buy', {item: 'gems100'}); // сервер создаёт заказ в Стрекозе
const res = await DragonflyGames.pay(order.id);
if (res.status === 'paid') setTimeout(refresh, 1500); // покупку выдаст сервер по вебхуку
};
async function refresh() { const me = await api('/api/me'); $('gems').textContent = me.gems; }
start().catch((e) => { $('who').textContent = 'Ошибка: ' + e.message; });
</script>
</body></html>
Сервер на Python (FastAPI)
pip install fastapi uvicorn httpx, затем DF_KEY=dfk_test_… DF_WEBHOOK_SECRET=whsec_… uvicorn game_server:app --port 3000
# game_server.py — пример сервера игры для Стрекозы
import hashlib, hmac, json, os, secrets, time
import httpx
from fastapi import FastAPI, Header, HTTPException, Request
from fastapi.responses import FileResponse
DF_API = os.getenv("DF_API", "https://dragonfly-flash.com/api/dev/v1")
DF_KEY = os.environ["DF_KEY"] # dfk_test_… или dfk_live_…
DF_WEBHOOK_SECRET = os.environ["DF_WEBHOOK_SECRET"] # whsec_… из кабинета
PRICES = {"gems100": (149, "100 кристаллов", 100)}
app = FastAPI()
players = {} # player_id -> {"nick", "gems"} (в настоящей игре — база данных)
sessions = {} # токен сессии -> player_id
granted = set() # order_id уже выданных покупок — выдаём один раз
def df(method, path, body=None):
r = httpx.request(method, DF_API + path, json=body, timeout=15,
headers={"Authorization": "Bearer " + DF_KEY})
if r.status_code >= 400:
raise HTTPException(r.status_code, r.json().get("detail", "Ошибка Стрекозы"))
return r.json()
def me(authorization):
pid = sessions.get((authorization or "").removeprefix("Bearer "))
if not pid:
raise HTTPException(401, "Нет входа")
return pid
@app.get("/")
def page():
return FileResponse("index.html")
@app.post("/api/login")
async def login(request: Request):
ticket = (await request.json())["ticket"]
p = df("POST", "/auth/verify", {"ticket": ticket})
pl = players.setdefault(p["player_id"], {"gems": 0})
pl["nick"] = p["nick"]
token = secrets.token_urlsafe(24)
sessions[token] = p["player_id"]
return {"session": token, "nick": p["nick"], "gems": pl["gems"]}
@app.post("/api/me")
def get_me(authorization: str = Header(None)):
return players[me(authorization)]
@app.post("/api/buy")
async def buy(request: Request, authorization: str = Header(None)):
pid = me(authorization)
item = (await request.json())["item"]
price, title, _ = PRICES[item]
order_id = f"{item}-{secrets.token_hex(6)}"
return df("POST", "/orders", {"player_id": pid, "order_id": order_id, "amount": price,
"title": title, "payload": {"item": item}})
@app.post("/dragonfly/webhook")
async def webhook(request: Request, x_dragonfly_signature: str = Header("")):
body = await request.body()
parts = dict(x.split("=", 1) for x in x_dragonfly_signature.split(",") if "=" in x)
good = hmac.new(DF_WEBHOOK_SECRET.encode(), parts.get("t", "").encode() + b"." + body,
hashlib.sha256).hexdigest()
if not hmac.compare_digest(good, parts.get("v1", "")) or abs(time.time() - int(parts.get("t", 0))) > 300:
raise HTTPException(401, "bad signature")
event = json.loads(body)
if event["event"] == "order.paid":
o = event["order"]
if o["order_id"] not in granted: # выдаём ровно один раз
item = json.loads(o["payload"])["item"]
players.setdefault(o["player_id"], {"gems": 0, "nick": "?"})["gems"] += PRICES[item][2]
granted.add(o["order_id"])
return {"ok": True}
Сервер на Node.js (Express)
npm i express (Node 18+), затем DF_KEY=… DF_WEBHOOK_SECRET=… node server.js
// server.js — пример сервера игры для Стрекозы
const express = require('express');
const crypto = require('crypto');
const DF_API = process.env.DF_API || 'https://dragonfly-flash.com/api/dev/v1';
const { DF_KEY, DF_WEBHOOK_SECRET } = process.env;
const PRICES = { gems100: { amount: 149, title: '100 кристаллов', qty: 100 } };
const players = new Map(), sessions = new Map(), granted = new Set();
async function df(method, path, body) {
const r = await fetch(DF_API + path, { method, body: body && JSON.stringify(body),
headers: { 'Authorization': 'Bearer ' + DF_KEY, 'Content-Type': 'application/json' } });
const d = await r.json();
if (!r.ok) throw Object.assign(new Error(d.detail), { status: r.status });
return d;
}
const app = express();
// вебхук — ДО express.json(): подпись считается по сырому телу
app.post('/dragonfly/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const sig = Object.fromEntries(String(req.get('X-Dragonfly-Signature') || '').split(',').map(x => x.split('=')));
const good = crypto.createHmac('sha256', DF_WEBHOOK_SECRET).update(sig.t + '.').update(req.body).digest('hex');
const v1 = String(sig.v1 || '');
if (v1.length !== good.length || !crypto.timingSafeEqual(Buffer.from(good), Buffer.from(v1))
|| Math.abs(Date.now() / 1000 - Number(sig.t)) > 300) return res.status(401).end();
const ev = JSON.parse(req.body);
if (ev.event === 'order.paid' && !granted.has(ev.order.order_id)) {
const item = JSON.parse(ev.order.payload).item;
const p = players.get(ev.order.player_id) || { gems: 0 };
p.gems += PRICES[item].qty; players.set(ev.order.player_id, p); granted.add(ev.order.order_id);
}
res.json({ ok: true });
});
app.use(express.json());
app.get('/', (req, res) => res.sendFile(__dirname + '/index.html'));
const me = (req) => sessions.get(String(req.get('Authorization') || '').replace('Bearer ', ''));
app.post('/api/login', async (req, res) => {
try {
const p = await df('POST', '/auth/verify', { ticket: req.body.ticket });
const pl = players.get(p.player_id) || { gems: 0 }; pl.nick = p.nick; players.set(p.player_id, pl);
const token = crypto.randomBytes(24).toString('base64url'); sessions.set(token, p.player_id);
res.json({ session: token, nick: p.nick, gems: pl.gems });
} catch (e) { res.status(e.status || 500).json({ detail: e.message }); }
});
app.post('/api/me', (req, res) => me(req) ? res.json(players.get(me(req))) : res.status(401).end());
app.post('/api/buy', async (req, res) => {
const pid = me(req); if (!pid) return res.status(401).end();
const it = PRICES[req.body.item];
try {
res.json(await df('POST', '/orders', { player_id: pid, order_id: req.body.item + '-' + crypto.randomBytes(6).toString('hex'),
amount: it.amount, title: it.title, payload: { item: req.body.item } }));
} catch (e) { res.status(e.status || 500).json({ detail: e.message }); }
});
app.listen(3000, () => console.log('http://localhost:3000'));
Деньги и выплаты
- Игрок платит Стрекозе через Robokassa (карта, СБП), чек ему присылает Robokassa.
- Ваша доля считается с полной суммы платежа: стандартно 70% вам, 30% — площадке (персональная доля, если она согласована, видна в кабинете). Комиссию Robokassa площадка платит из своей доли.
- Кабинет → «Финансы»: оборот, начислено, выплачено и сколько к выплате, по каждой игре. Тестовые платежи в расчётах не участвуют.
- Укажите реквизиты для выплат в «Финансах». Выплаты делает администрация; каждая выплата отмечается в кабинете.
- Возвраты и спорные платежи решаются с администрацией площадки.
Публикация и правила
- Игра должна работать в рамке на сайте и в окне приложения, на телефоне и на компьютере.
- Своя графика, звуки и названия; без чужих торговых марок, взрослого контента, азартных игр на деньги и сбора данных игроков сверх
player_id, ника и аватара. - Продавать можно только внутриигровые товары и услуги вашей игры. Цены — в рублях, через SDK Стрекозы, без сторонних платёжек внутри игры.
- Смена боевого адреса опубликованной игры возвращает её на проверку.
- Администрация может снять игру с публикации или заблокировать её при нарушениях; вы получите уведомление с причиной.
Частые вопросы
Нужно ли мне просить вас что-то обновить на сервере, когда я выпускаю новую версию? Нет. Игра живёт у вас: обновили у себя — игроки видят новую версию. Названия, описание, значок, адреса — правятся в кабинете и применяются сразу.
Можно ли открыть игру не через Стрекозу? Можно, но вход и оплата работают только на Стрекозе (на сайте или в приложении).
Мой сервер не принимает запросы из вашего iframe. Не запрещайте встраивание: уберите X-Frame-Options или
поставьте Content-Security-Policy: frame-ancestors https://dragonfly-flash.com https://www.dragonfly-flash.com.
Cookie вашего сервера внутри рамки — сторонние: используйте токен в заголовке, как в примере, или cookie с SameSite=None; Secure.
Ключ утёк. Отзовите его в кабинете и выпустите новый — старый перестанет работать сразу.
Где взять помощь? Напишите администрации в личные сообщения на Стрекозе.