Skip to content

Рецепты ​

Готовые скрипты под частые задачи байера. Каждый собран из методов, описанных на соседних страницах, - скопируйте, впишите свои значения в начале скрипта и запускайте.

Во всех рецептах замените ВАШ_КЛЮЧ и panel.example.com на свои. Как получить ключ, см. раздел «Введение в API».

Даты - по UTC

Статистика и конверсии считаются по дням UTC. «Вчера» в скриптах ниже - это вчера по UTC, а не по вашему часовому поясу.

Статистика за вчера в Telegram ​

Каждое утро присылает в Telegram итоги за вчера: визиты, установки, регистрации, депозиты, выплаты и конверсию.

Что понадобится:

  1. Свой бот: напишите @BotFather команду /newbot и скопируйте токен.
  2. ID чата, куда слать: напишите боту любое сообщение, откройте https://api.telegram.org/bot<ТОКЕН>/getUpdates и возьмите chat.id из ответа. Для группы - добавьте бота в группу, напишите в ней сообщение и возьмите chat.id группы (он отрицательный).
python
# pip install requests
import datetime
import requests

API = "https://panel.example.com/api"
HEADERS = {"Authorization": "Bearer ВАШ_КЛЮЧ", "Accept": "application/json"}
BOT_TOKEN = "ТОКЕН_БОТА"
CHAT_ID = "ID_ЧАТА"

day = (datetime.datetime.now(datetime.timezone.utc).date() - datetime.timedelta(days=1)).isoformat()

response = requests.get(f"{API}/stats/dashboard", headers=HEADERS, params={
    "date_from": day,
    "date_to": day,
}, timeout=30)
response.raise_for_status()
metrics = response.json()["data"]["metrics"]

payouts = ", ".join(f'{a["amount"]} {a["currency"]}' for a in metrics["total_deposits"]["amounts"]) or "0"
text = "\n".join([
    f"PWA Hub за {day}",
    f"Визиты: {metrics['total_visits']['value']} ({metrics['total_visits']['trend']}%)",
    f"Установки: {metrics['total_installs']['value']} ({metrics['total_installs']['trend']}%)",
    f"Регистрации: {metrics['total_registrations']['value']}",
    f"Депозиты: {metrics['total_deposits']['value']} - {payouts}",
    f"CR: {metrics['avg_cr']['value']}%",
])

requests.post(f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage", json={
    "chat_id": CHAT_ID,
    "text": text,
}, timeout=30).raise_for_status()
print("Отправлено:\n" + text)
js
// Node.js 18+, без зависимостей; сохраните как stats-telegram.mjs
const API = 'https://panel.example.com/api';
const headers = { Authorization: 'Bearer ВАШ_КЛЮЧ', Accept: 'application/json' };
const BOT_TOKEN = 'ТОКЕН_БОТА';
const CHAT_ID = 'ID_ЧАТА';

const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000);
const day = yesterday.toISOString().slice(0, 10);

const response = await fetch(`${API}/stats/dashboard?${new URLSearchParams({ date_from: day, date_to: day })}`, {
  headers,
  signal: AbortSignal.timeout(30000),
});
if (!response.ok) throw new Error(`PWA Hub ответил ${response.status}`);
const { metrics } = (await response.json()).data;

const payouts = metrics.total_deposits.amounts.map((a) => `${a.amount} ${a.currency}`).join(', ') || '0';
const text = [
  `PWA Hub за ${day}`,
  `Визиты: ${metrics.total_visits.value} (${metrics.total_visits.trend}%)`,
  `Установки: ${metrics.total_installs.value} (${metrics.total_installs.trend}%)`,
  `Регистрации: ${metrics.total_registrations.value}`,
  `Депозиты: ${metrics.total_deposits.value} - ${payouts}`,
  `CR: ${metrics.avg_cr.value}%`,
].join('\n');

const sent = await fetch(`https://api.telegram.org/bot${BOT_TOKEN}/sendMessage`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ chat_id: CHAT_ID, text }),
  signal: AbortSignal.timeout(30000),
});
if (!sent.ok) throw new Error(`Telegram ответил ${sent.status}`);
console.log(`Отправлено:\n${text}`);
php
<?php
// composer require guzzlehttp/guzzle
require 'vendor/autoload.php';

$api = new GuzzleHttp\Client([
    'base_uri' => 'https://panel.example.com/api/',
    'headers' => ['Authorization' => 'Bearer ВАШ_КЛЮЧ', 'Accept' => 'application/json'],
    'timeout' => 30,
]);
$botToken = 'ТОКЕН_БОТА';
$chatId = 'ID_ЧАТА';

$day = (new DateTimeImmutable('yesterday', new DateTimeZone('UTC')))->format('Y-m-d');

$response = $api->get('stats/dashboard', ['query' => ['date_from' => $day, 'date_to' => $day]]);
$metrics = json_decode($response->getBody(), true)['data']['metrics'];

$payouts = implode(', ', array_map(
    fn($a) => "{$a['amount']} {$a['currency']}",
    $metrics['total_deposits']['amounts']
)) ?: '0';
$text = implode("\n", [
    "PWA Hub за {$day}",
    "Визиты: {$metrics['total_visits']['value']} ({$metrics['total_visits']['trend']}%)",
    "Установки: {$metrics['total_installs']['value']} ({$metrics['total_installs']['trend']}%)",
    "Регистрации: {$metrics['total_registrations']['value']}",
    "Депозиты: {$metrics['total_deposits']['value']} - {$payouts}",
    "CR: {$metrics['avg_cr']['value']}%",
]);

(new GuzzleHttp\Client(['timeout' => 30]))->post("https://api.telegram.org/bot{$botToken}/sendMessage", [
    'json' => ['chat_id' => $chatId, 'text' => $text],
]);
echo "Отправлено:\n{$text}" . PHP_EOL;

По расписанию. На сервере с Linux добавьте строку в crontab -e - скрипт будет срабатывать каждый день в 07:00 по времени сервера:

bash
0 7 * * * /usr/bin/python3 /home/user/stats-telegram.py

Для Node.js - /usr/bin/node /home/user/stats-telegram.mjs, для PHP - /usr/bin/php /home/user/stats-telegram.php. Запускайте не раньше 00:10 UTC: свежие события попадают в статистику с задержкой до 5 минут.

trend - изменение к предыдущему дню в процентах. Подробно о показателях - в разделе «Статистика».

Клонировать PWA, повесить домен и включить ​

Делает новое PWA из готового за один запуск: клонирует его, привязывает к копии домен и включает её. Удобно, когда под каждую связку нужно своё приложение со своим доменом.

Что понадобится: id PWA-образца (из списка PWA или адресной строки панели) и домен для копии.

Это реальные действия в Cloudflare

Привязка домена создаёт зону и DNS-записи в вашем аккаунте Cloudflare. Поддомен (go.example.com) привязывается, только если его корневой домен (example.com) уже добавлен в панель - иначе придёт 404. Подробности - в разделе «Домены».

python
# pip install requests
import requests

API = "https://panel.example.com/api"
HEADERS = {"Authorization": "Bearer ВАШ_КЛЮЧ", "Accept": "application/json"}
SOURCE_ID = "WZAX"                # PWA-образец
NAME = "Promo EN - связка 12"    # название копии
DOMAIN = "fit-tracker-12.example"  # домен для копии


def call(method, path, **kwargs):
    response = requests.request(method, f"{API}/{path}", headers=HEADERS, timeout=30, **kwargs)
    if not response.ok:
        raise SystemExit(f"{method} /{path}: {response.status_code} {response.json().get('message')}")
    return response.json()["data"]


# Команда образца нужна для PUT (team_lead и admin передают team_id всегда)
team_id = call("GET", f"pwas/{SOURCE_ID}")["pwa"]["team"]["id"]

clone = call("POST", f"pwas/{SOURCE_ID}/clone", json={"names": [NAME]})["clones"][0]
print("Копия:", clone["id"], clone["name"])

domain = call("POST", "domains", json={"domain": DOMAIN, "pwa_id": clone["id"]})["domain"]
print("Домен:", domain["domain"])

call("PUT", f"pwas/{clone['id']}", json={"active": True, "team_id": team_id})
print("PWA включено")
js
// Node.js 18+, без зависимостей; сохраните как clone-pwa.mjs
const API = 'https://panel.example.com/api';
const headers = { Authorization: 'Bearer ВАШ_КЛЮЧ', Accept: 'application/json', 'Content-Type': 'application/json' };
const SOURCE_ID = 'WZAX'; // PWA-образец
const NAME = 'Promo EN - связка 12'; // название копии
const DOMAIN = 'fit-tracker-12.example'; // домен для копии

async function call(method, path, body) {
  const response = await fetch(`${API}/${path}`, {
    method,
    headers,
    body: body ? JSON.stringify(body) : undefined,
    signal: AbortSignal.timeout(30000),
  });
  const json = await response.json();
  if (!response.ok) throw new Error(`${method} /${path}: ${response.status} ${json.message}`);
  return json.data;
}

// Команда образца нужна для PUT (team_lead и admin передают team_id всегда)
const teamId = (await call('GET', `pwas/${SOURCE_ID}`)).pwa.team.id;

const [clone] = (await call('POST', `pwas/${SOURCE_ID}/clone`, { names: [NAME] })).clones;
console.log('Копия:', clone.id, clone.name);

const { domain } = await call('POST', 'domains', { domain: DOMAIN, pwa_id: clone.id });
console.log('Домен:', domain.domain);

await call('PUT', `pwas/${clone.id}`, { active: true, team_id: teamId });
console.log('PWA включено');
php
<?php
// composer require guzzlehttp/guzzle
require 'vendor/autoload.php';

$api = new GuzzleHttp\Client([
    'base_uri' => 'https://panel.example.com/api/',
    'headers' => ['Authorization' => 'Bearer ВАШ_КЛЮЧ', 'Accept' => 'application/json'],
    'timeout' => 30,
    'http_errors' => false,
]);
$sourceId = 'WZAX';               // PWA-образец
$name = 'Promo EN - связка 12';  // название копии
$domainName = 'fit-tracker-12.example'; // домен для копии

function call(GuzzleHttp\Client $api, string $method, string $path, ?array $body = null): array
{
    $response = $api->request($method, $path, $body === null ? [] : ['json' => $body]);
    $json = json_decode($response->getBody(), true);
    if ($response->getStatusCode() >= 400) {
        exit("{$method} /{$path}: {$response->getStatusCode()} {$json['message']}" . PHP_EOL);
    }
    return $json['data'];
}

// Команда образца нужна для PUT (team_lead и admin передают team_id всегда)
$teamId = call($api, 'GET', "pwas/{$sourceId}")['pwa']['team']['id'];

$clone = call($api, 'POST', "pwas/{$sourceId}/clone", ['names' => [$name]])['clones'][0];
echo "Копия: {$clone['id']} {$clone['name']}" . PHP_EOL;

$domain = call($api, 'POST', 'domains', ['domain' => $domainName, 'pwa_id' => $clone['id']])['domain'];
echo "Домен: {$domain['domain']}" . PHP_EOL;

call($api, 'PUT', "pwas/{$clone['id']}", ['active' => true, 'team_id' => $teamId]);
echo 'PWA включено' . PHP_EOL;

Копия получает настройки, картинки и аккаунты образца, но не поток фильтрации - если он нужен, задайте filter_stream_id в том же PUT (см. раздел «PWA»). Новое PWA начинает получать трафик в течение нескольких минут после включения.

Остановить PWA без депозитов ​

Проходит по всем включённым PWA и выключает те, у которых за последние 7 дней набралось достаточно установок, но нет ни одного депозита.

Что понадобится: решить, сколько установок считать достаточным (MIN_INSTALLS). По умолчанию скрипт работает в режиме проверки (DRY_RUN = True) и только печатает, что выключил бы. Убедитесь, что список верный, и поменяйте на False.

python
# pip install requests
import datetime
import time
import requests

API = "https://panel.example.com/api"
HEADERS = {"Authorization": "Bearer ВАШ_КЛЮЧ", "Accept": "application/json"}
MIN_INSTALLS = 50  # меньше установок - рано судить
DRY_RUN = True     # True - только показать, False - выключать

today = datetime.datetime.now(datetime.timezone.utc).date()
date_from = (today - datetime.timedelta(days=7)).isoformat()
date_to = (today - datetime.timedelta(days=1)).isoformat()


def get(path, params=None):
    response = requests.get(f"{API}/{path}", headers=HEADERS, params=params, timeout=30)
    if response.status_code == 401:
        raise SystemExit("Ключ не принят - проверьте его в профиле")
    response.raise_for_status()
    return response.json()["data"]


pwas, page = [], 1
while True:
    data = get("pwas", {"status": "active", "page": page, "excludes": "stats"})
    pwas += data["pwas"]
    if page >= data["meta"]["last_page"]:
        break
    page += 1

for pwa in pwas:
    totals = get("stats/funnel", {"date_from": date_from, "date_to": date_to, "pwa_id": pwa["id"]})["funnel"]["totals"]
    time.sleep(0.25)  # не больше 300 запросов в минуту
    if totals["installs"] < MIN_INSTALLS or totals["deps"] > 0:
        continue
    print(f"{pwa['id']} {pwa['name']}: {totals['installs']} установок, 0 депозитов")
    if not DRY_RUN:
        requests.put(f"{API}/pwas/{pwa['id']}", headers=HEADERS, json={
            "active": False,
            "team_id": pwa["team"]["id"],
        }, timeout=30).raise_for_status()
        print("  выключено")
js
// Node.js 18+, без зависимостей; сохраните как stop-pwas.mjs
const API = 'https://panel.example.com/api';
const headers = { Authorization: 'Bearer ВАШ_КЛЮЧ', Accept: 'application/json', 'Content-Type': 'application/json' };
const MIN_INSTALLS = 50; // меньше установок - рано судить
const DRY_RUN = true; // true - только показать, false - выключать

const day = (daysAgo) => new Date(Date.now() - daysAgo * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
const dateFrom = day(7);
const dateTo = day(1);
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function get(path, params = {}) {
  const response = await fetch(`${API}/${path}?${new URLSearchParams(params)}`, {
    headers,
    signal: AbortSignal.timeout(30000),
  });
  if (response.status === 401) throw new Error('Ключ не принят - проверьте его в профиле');
  if (!response.ok) throw new Error(`${path}: ${response.status}`);
  return (await response.json()).data;
}

const pwas = [];
for (let page = 1; ; page++) {
  const data = await get('pwas', { status: 'active', page, excludes: 'stats' });
  pwas.push(...data.pwas);
  if (page >= data.meta.last_page) break;
}

for (const pwa of pwas) {
  const { totals } = (await get('stats/funnel', { date_from: dateFrom, date_to: dateTo, pwa_id: pwa.id })).funnel;
  await sleep(250); // не больше 300 запросов в минуту
  if (totals.installs < MIN_INSTALLS || totals.deps > 0) continue;
  console.log(`${pwa.id} ${pwa.name}: ${totals.installs} установок, 0 депозитов`);
  if (!DRY_RUN) {
    const response = await fetch(`${API}/pwas/${pwa.id}`, {
      method: 'PUT',
      headers,
      body: JSON.stringify({ active: false, team_id: pwa.team.id }),
      signal: AbortSignal.timeout(30000),
    });
    if (!response.ok) throw new Error(`PUT /pwas/${pwa.id}: ${response.status}`);
    console.log('  выключено');
  }
}
php
<?php
// composer require guzzlehttp/guzzle
require 'vendor/autoload.php';

$api = new GuzzleHttp\Client([
    'base_uri' => 'https://panel.example.com/api/',
    'headers' => ['Authorization' => 'Bearer ВАШ_КЛЮЧ', 'Accept' => 'application/json'],
    'timeout' => 30,
]);
$minInstalls = 50; // меньше установок - рано судить
$dryRun = true;    // true - только показать, false - выключать

$today = new DateTimeImmutable('today', new DateTimeZone('UTC'));
$dateFrom = $today->modify('-7 days')->format('Y-m-d');
$dateTo = $today->modify('-1 day')->format('Y-m-d');

$pwas = [];
for ($page = 1; ; $page++) {
    $response = $api->get('pwas', ['query' => ['status' => 'active', 'page' => $page, 'excludes' => 'stats']]);
    $data = json_decode($response->getBody(), true)['data'];
    $pwas = [...$pwas, ...$data['pwas']];
    if ($page >= $data['meta']['last_page']) {
        break;
    }
}

foreach ($pwas as $pwa) {
    $response = $api->get('stats/funnel', ['query' => [
        'date_from' => $dateFrom,
        'date_to' => $dateTo,
        'pwa_id' => $pwa['id'],
    ]]);
    $totals = json_decode($response->getBody(), true)['data']['funnel']['totals'];
    usleep(250000); // не больше 300 запросов в минуту
    if ($totals['installs'] < $minInstalls || $totals['deps'] > 0) {
        continue;
    }
    echo "{$pwa['id']} {$pwa['name']}: {$totals['installs']} установок, 0 депозитов" . PHP_EOL;
    if (!$dryRun) {
        $api->put("pwas/{$pwa['id']}", ['json' => ['active' => false, 'team_id' => $pwa['team']['id']]]);
        echo '  выключено' . PHP_EOL;
    }
}

Скрипт делает по одному запросу статистики на каждое PWA и держит паузу 0,25 с, чтобы не упереться в лимит 300 запросов в минуту. Установки и депозиты здесь - по дате события, как в воронке (см. раздел «Статистика»). Команда участников PWA получит уведомление о каждом выключенном приложении.

Статистика в Google Таблицах ​

Каждое утро дописывает в Google Таблицу строку с итогами за вчера. За месяц получается готовая таблица для графиков и сводок - без серверов и cron, всё внутри Google.

Настройка:

  1. Откройте нужную Google Таблицу и выберите Extensions → Apps Script.
  2. Вставьте скрипт ниже вместо содержимого редактора и замените panel.example.com на адрес своей панели.
  3. Слева откройте Project Settings (шестерёнка) → Script Properties → Add script property: имя PWA_HUB_KEY, значение - ваш ключ. Так ключ не хранится в тексте скрипта.
  4. Сверху выберите функцию appendYesterday и нажмите Run. Google попросит разрешения - подтвердите. В таблице появится лист PWA Hub с первой строкой.
  5. Слева откройте Triggers (будильник) → Add Trigger: функция appendYesterday, источник Time-driven, Day timer, время - например, 7am to 8am. Сохраните.
js
// Google Таблица → Extensions → Apps Script
const API = 'https://panel.example.com/api';
const SHEET = 'PWA Hub';

function appendYesterday() {
  const key = PropertiesService.getScriptProperties().getProperty('PWA_HUB_KEY');
  if (!key) throw new Error('Добавьте PWA_HUB_KEY в Script Properties');

  const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000);
  const day = Utilities.formatDate(yesterday, 'UTC', 'yyyy-MM-dd');

  const response = UrlFetchApp.fetch(`${API}/stats/dashboard?date_from=${day}&date_to=${day}`, {
    headers: { Authorization: `Bearer ${key}`, Accept: 'application/json' },
    muteHttpExceptions: true,
  });
  if (response.getResponseCode() !== 200) {
    throw new Error(`PWA Hub ответил ${response.getResponseCode()}: ${response.getContentText()}`);
  }
  const metrics = JSON.parse(response.getContentText()).data.metrics;

  const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
  let sheet = spreadsheet.getSheetByName(SHEET);
  if (!sheet) {
    sheet = spreadsheet.insertSheet(SHEET);
    sheet.appendRow(['Дата', 'Визиты', 'Уникальные', 'Установки', 'Регистрации', 'Депозиты', 'CR, %']);
  }
  sheet.appendRow([
    day,
    metrics.total_visits.value,
    metrics.total_uniques.value,
    metrics.total_installs.value,
    metrics.total_registrations.value,
    metrics.total_deposits.value,
    metrics.avg_cr.value,
  ]);
}

Повторный запуск - повторная строка

Скрипт просто дописывает строку. Запустили его дважды за день - вчерашний день окажется в таблице два раза; лишнюю строку удалите вручную.

Если панель недоступна или ключ отозван, запуск по триггеру завершится ошибкой, а Google пришлёт письмо о сбое - строка за этот день не появится. Пропущенный день можно дописать, поменяв yesterday на нужную дату и запустив функцию вручную.