Operator API

One REST integration for the whole catalogue. Every request is authenticated with your API key and an HMAC-SHA256 signature over the exact raw request body. Copy-paste clients for Node.js and PHP are included below.

Quick start

  1. Open Panel → API Details and copy your API key and secret key.
  2. Set your server IP, your domain and your callback URL there. Without all three every API call is rejected with 403.
  3. Sign every request body with HMAC-SHA256 using your secret key.
  4. Call /api/public/v1/games, then /api/public/v1/session/open to launch a game.
  5. Seamless mode: implement the callback endpoint below. Transfer mode: use /wallet to fund players.
Wallet modes

Seamless — the player balance stays on your side. We call your callback URL for balance, bets, wins and rollbacks.

Transfer — we hold a wallet per player. You push credits with /wallet and read the balance back.

Authentication (HMAC-SHA256)

Send these headers on every request. The signature is the lowercase hex HMAC-SHA256 of the exact raw request body (empty string for GET) keyed with your secret key. Sign the same string you send — do not re-serialise the JSON.

Headers
X-Api-Key: <your api key>
X-Signature: <hex hmac_sha256(secret_key, raw_body)>
Content-Type: application/json
Node.js
import crypto from "node:crypto";

const API_BASE = "https://astechapi.cloud";
const API_KEY = process.env.ASTECH_API_KEY;
const SECRET  = process.env.ASTECH_SECRET_KEY;

export async function astechCall(path, payload = null) {
  const body = payload === null ? "" : JSON.stringify(payload);
  const signature = crypto.createHmac("sha256", SECRET).update(body).digest("hex");

  const res = await fetch(API_BASE + path, {
    method: payload === null ? "GET" : "POST",
    headers: {
      "X-Api-Key": API_KEY,
      "X-Signature": signature,
      "Content-Type": "application/json",
    },
    ...(payload === null ? {} : { body }),
  });

  const json = await res.json();
  if (json.status !== "success") throw new Error(json.error || "API error");
  return json.content;
}
Mandatory security settings

IP whitelist — one server IP. Calls from any other IP return 403.

Domain whitelist — one domain. Requests with a different Origin/Referer return 403.

Callback URL — must be set even in transfer mode.

• Never expose the secret key in browser code — call our API only from your backend.

Ready-made client

Drop this into your project and you are integrated. Both files use only the standard library.

Node.js
// astech.js  — Node 18+ (no dependencies)
import crypto from "node:crypto";

export class ASTechAPI {
  constructor({ apiKey, secretKey, baseUrl = "https://astechapi.cloud" }) {
    this.apiKey = apiKey; this.secretKey = secretKey; this.baseUrl = baseUrl;
  }
  sign(body) {
    return crypto.createHmac("sha256", this.secretKey).update(body).digest("hex");
  }
  async request(path, payload) {
    const body = payload ? JSON.stringify(payload) : "";
    const res = await fetch(this.baseUrl + path, {
      method: payload ? "POST" : "GET",
      headers: {
        "X-Api-Key": this.apiKey,
        "X-Signature": this.sign(body),
        "Content-Type": "application/json",
      },
      ...(payload ? { body } : {}),
    });
    const json = await res.json();
    if (json.status !== "success") throw new Error(json.error || "API error");
    return json.content;
  }
  games(filters = {})    { return this.request("/api/public/v1/games", filters); }
  openGame(data)         { return this.request("/api/public/v1/session/open", data); }
  wallet(data)           { return this.request("/api/public/v1/wallet", data); }
  round(data)            { return this.request("/api/public/v1/round", data); }
  verifyCallback(rawBody, signature) {
    const expected = this.sign(rawBody);
    return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
  }
}

// usage
const astech = new ASTechAPI({ apiKey: process.env.ASTECH_API_KEY, secretKey: process.env.ASTECH_SECRET_KEY });
const { games } = await astech.games();
const session = await astech.openGame({
  player_login: "john",
  game_id: games[0].gameId,
  currency: "USD",
  language: "en",
  exit_url: "https://your-site.com/lobby",
});
console.log(session.gameUrl);

Game list

POST
/api/public/v1/games

Returns the catalogue enabled for your account (only the providers your admin allowed). Send an empty JSON object {} — or the optional filters below — and sign that exact body.

Request (all fields optional)
{ "currency": "INR", "provider": "Pragmatic", "category": "slots" }
Response
{ "status": "success", "error": "", "content": {
  "count": 1,
  "games": [
    {
      "gameId": "0f1c...uuid",
      "title": "Book of Gold",
      "provider": "Pragmatic",
      "category": "slots",
      "imageUrl": "https://cdn.example/book-of-gold.png",
      "currencies": ["INR", "USD"]
    }
  ]
} }

Cache this list on your side and refresh it a few times per day. Always launch with the gameId returned here (send it as game_id to /session/open).

Launch a game

POST
/api/public/v1/session/open

Creates the player if it does not exist and returns a launch URL valid for a single session.

Request
{
  "player_login": "john",          // your unique player id
  "game_id": "0f1c...uuid",        // from /games
  "currency": "USD",
  "language": "en",
  "exit_url": "https://your-site.com/lobby"
}
Response
{ "content": {
  "sessionId": "9a2f...",
  "gameUrl": "https://provider.example/launch?token=...",
  "mode": "seamless",
  "demo": false
} }

The API returns the URL but a JSON response cannot navigate the browser by itself. Your frontend must open gameUrl, or submit directly to the redirect launcher below.

Node.js
const session = await astech.openGame({
  player_login: user.id,
  game_id: gameId,
  currency: user.currency,
  language: "en",
  exit_url: "https://your-site.com/lobby",
});
res.redirect(session.gameUrl);

Recommended instant launch: submit a temporary form to GetGameUrl.phpdirectly inside the click event. Browser GET/form requests now return a 302 redirect automatically, so the new tab opens immediately with no asynchronous popup-blocker problem and no intermediate JSON page.

Browser — direct 302 launch
btn.onclick = () => {
  const form = document.createElement("form");
  form.method = "POST";
  form.action = "/GetGameUrl.php";
  form.target = "_blank";

  const fields = { gameCode: gameId, token: jwt };
  for (const [name, value] of Object.entries(fields)) {
    const input = document.createElement("input");
    input.type = "hidden";
    input.name = name;
    input.value = value;
    form.appendChild(input);
  }

  document.body.appendChild(form);
  form.submit(); // synchronous user-click: popup-blocker safe
  form.remove();
};

Same-tab option: navigate to /GetGameUrl.php?gameCode=XYZ&token=JWT. For production, prefer the POST form so the JWT is not stored in browser history or server URL logs. If an AJAX caller needs JSON, send Content-Type: application/json or add response=json.

Transfer wallet

POST
/api/public/v1/wallet

Transfer mode only. All cash operations are idempotent on transaction_id — retrying a timed-out call is safe.

Commands
{ "cmd": "userCreate", "user_login": "john", "currency": "USD" }

{ "cmd": "userInfo",   "user_login": "john", "currency": "USD" }

{ "cmd": "userCash",   "user_login": "john", "currency": "USD",
  "operation": "in",              // "in" = deposit, "out" = withdraw
  "cash": "100.00",
  "transaction_id": "dep-001" }
Response
{ "content": { "id": "john", "currency": "USD", "cash": "100.00" } }
Node.js
await astech.wallet({ cmd: "userCreate", user_login: "john", currency: "USD" });
await astech.wallet({
  cmd: "userCash", user_login: "john", currency: "USD",
  operation: "in", cash: "100.00", transaction_id: "dep-001",
});
const info = await astech.wallet({ cmd: "userInfo", user_login: "john", currency: "USD" });
console.log(info.cash);

Rounds — bet, win, rollback

POST
/api/public/v1/round

Post a round against the wallet we hold for the player. Idempotent on transaction_id, and it feeds the daily GGR aggregate automatically.

Request
{ "action": "bet", "player_login": "john", "currency": "USD",
  "transaction_id": "tx-9001", "round_id": "r-51",
  "session_id": "9a2f...", "game_id": "0f1c...uuid", "bet": "1.00" }

{ "action": "win", "player_login": "john", "transaction_id": "tx-9002",
  "round_id": "r-51", "win": "3.50", "round_finished": "1" }

{ "action": "betwin", "player_login": "john", "transaction_id": "tx-9003",
  "bet": "1.00", "win": "3.50", "round_finished": "1" }

{ "action": "rollback", "player_login": "john", "transaction_id": "tx-9001" }
Response
{ "content": { "id": "john", "currency": "USD", "balance": "99.00", "transactionId": "tx-9001" } }

Seamless callbacks

In seamless mode we POST to your callback URL for every wallet event, signed with the same HMAC scheme in the X-Signature header. Verify the signature over the raw body before processing, de-duplicate on transaction_id, and always reply with the envelope below within 8 seconds.

Requests we send you
POST <your callback url>
X-Api-Key: <your api key>
X-Signature: <hex hmac_sha256(secret_key, raw_body)>

{ "cmd": "getBalance", "user_login": "john", "currency": "USD" }

{ "cmd": "writeBet", "user_login": "john", "currency": "USD",
  "bet": "1.00", "win": "0.00", "transaction_id": "tx-9001",
  "round_id": "r-51", "session_id": "9a2f...", "game_id": "0f1c...uuid",
  "round_finished": true }

{ "cmd": "rollback", "user_login": "john", "transaction_id": "tx-9001" }
Reply we expect
{ "status": "success", "error": "", "content": { "cash": "99.00" } }

// not enough money
{ "status": "fail", "error": "INSUFFICIENT_FUNDS", "content": null }
Node.js
// Express — raw body is required for signature verification
import express from "express";
const app = express();

app.post("/astech/callback",
  express.raw({ type: "*/*" }),
  async (req, res) => {
    const raw = req.body.toString("utf8");
    if (!astech.verifyCallback(raw, req.get("X-Signature") || "")) {
      return res.json({ status: "fail", error: "Invalid signature", content: null });
    }
    const p = JSON.parse(raw);

    try {
      if (p.cmd === "getBalance") {
        const cash = await db.getBalance(p.user_login, p.currency);
        return res.json({ status: "success", error: "", content: { cash: cash.toFixed(2) } });
      }

      if (p.cmd === "writeBet") {
        // idempotent: ignore a transaction_id you have already stored
        const cash = await db.applyBet({
          login: p.user_login,
          bet: Number(p.bet || 0),
          win: Number(p.win || 0),
          txId: p.transaction_id,
          roundId: p.round_id,
        });
        return res.json({ status: "success", error: "", content: { cash: cash.toFixed(2) } });
      }

      if (p.cmd === "rollback") {
        const cash = await db.rollback(p.transaction_id);
        return res.json({ status: "success", error: "", content: { cash: cash.toFixed(2) } });
      }

      return res.json({ status: "fail", error: "Unknown cmd", content: null });
    } catch (e) {
      return res.json({ status: "fail", error: e.message, content: null });
    }
  });

app.listen(3000);
Callback rules

• Always answer HTTP 200 with the JSON envelope — never an HTML error page.

• Respond within 8 seconds — after that we abort the call and the round is rejected.

• Same transaction_id twice = return the stored balance, do not debit again.

• A rollback for an unknown transaction should succeed and return the current balance.

• Your callback URL must be HTTPS and reachable from the public internet.

Drop-in PHP files (launcher + callback)

Ye do files aap sidha apne server par daal sakte ho. Sirf API_KEY, API_SECRET aur database details badalni hain — baaki integration ready hai. Ek baar callback file ka HTTPS URL Panel → API Details → Callback URL me save kar do, aur apna server IP + domain wahin whitelist kar do.

1
GetGameUrl.php — game launch
GetGameUrl.php
<?php
/**
 * GetGameUrl.php - AS Tech API Game Launcher
 *
 * Ye file aapke apne server par rahegi. Player jab game kholna chahe,
 * ye file JWT se user verify karti hai, phir AS Tech API API ko
 * HMAC-signed request bhejke game URL return karti hai.
 *
 * SETUP (sirf 4 line badalni hai):
 *  1. conn.php / functions2.php ka path sahi karo
 *  2. API_KEY + API_SECRET apne panel -> API Details se paste karo
 *  3. EXIT_URL me apni site ka lobby URL do
 *  4. Ye file usi domain par rakho jo panel me whitelist kiya hai,
 *     aur server ka IP bhi panel me whitelist karo.
 */

// ============================================
// CONFIGURATION
// ============================================
include_once "../../conn.php";
include_once "../../functions2.php";

define('API_BASE',   'https://astechapi.cloud');
define('API_KEY',    'YOUR_API_KEY');       // Panel -> API Details
define('API_SECRET', 'YOUR_SECRET_KEY');    // Panel -> API Details
define('CURRENCY',   'INR');                // Operator currency
define('EXIT_URL',   'https://yourwebsite.com/');
// ============================================

header('Strict-Transport-Security: max-age=31536000');
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, Authorization');
header('Access-Control-Allow-Credentials: true');
$origin = isset($_SERVER['HTTP_ORIGIN']) ? $_SERVER['HTTP_ORIGIN'] : '';
header('Access-Control-Allow-Origin: ' . $origin);
header('vary: Origin');

date_default_timezone_set("Asia/Kolkata");
$nowTime = date("Y-m-d H:i:s");

$res = [
    'code' => 11,
    'msg'  => 'Method not allowed',
    'serviceNowTime' => $nowTime,
];

/**
 * Signed POST to AS Tech API. Signature = hex HMAC-SHA256 of the RAW body
 * using your secret key. Body must be sent byte-for-byte as it was signed.
 */
function astech_request($path, array $payload)
{
    $raw = json_encode($payload, JSON_UNESCAPED_SLASHES);
    $sig = hash_hmac('sha256', $raw, API_SECRET);

    $ch = curl_init(API_BASE . $path);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $raw);
    curl_setopt($ch, CURLOPT_TIMEOUT, 30);
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        'Content-Type: application/json',
        'X-Api-Key: ' . API_KEY,
        'X-Signature: ' . $sig,
        'Origin: https://' . ($_SERVER['HTTP_HOST'] ?? 'yourwebsite.com'),
    ]);

    $body = curl_exec($ch);
    $err  = curl_error($ch);
    curl_close($ch);

    if ($err !== '') {
        return ['status' => 'fail', 'error' => 'Connection error: ' . $err, 'content' => null];
    }
    $decoded = json_decode($body, true);
    return is_array($decoded)
        ? $decoded
        : ['status' => 'fail', 'error' => 'Bad response from API', 'content' => null];
}

$body = file_get_contents("php://input");
$post = json_decode($body, true);
if (!is_array($post)) $post = [];

// JSON/AJAX callers ko JSON response milta hai. Normal browser GET ya HTML form
// submission ko game par direct 302 redirect milta hai — redirect=1 ki zaroorat nahi.
$contentType = strtolower((string)($_SERVER['CONTENT_TYPE'] ?? ''));
$accept = strtolower((string)($_SERVER['HTTP_ACCEPT'] ?? ''));
$isJsonRequest = strpos($contentType, 'application/json') !== false;
$forceJson = (isset($_GET['response']) && $_GET['response'] === 'json')
    || (isset($_POST['response']) && $_POST['response'] === 'json');
$forceRedirect = (isset($_GET['redirect']) && $_GET['redirect'] === '1')
    || (isset($_POST['redirect']) && $_POST['redirect'] === '1');
$isBrowserNavigation = !$isJsonRequest && (isset($_GET['gameCode']) || isset($_POST['gameCode'])
    || strpos($accept, 'text/html') !== false);
$isRedirect = !$forceJson && ($forceRedirect || $isBrowserNavigation);

// Common operator frontend field names supported.
foreach (['gameCode', 'game_id', 'gameId', 'game_code'] as $field) {
    if (!isset($post['gameCode']) && isset($_POST[$field])) $post['gameCode'] = $_POST[$field];
    if (!isset($post['gameCode']) && isset($_GET[$field])) $post['gameCode'] = $_GET[$field];
}

if (isset($post['gameCode'])) {

    $gameId = trim((string)$post['gameCode']);   // AS Tech API /games ka game_id

    // ---- JWT AUTH -------------------------------------------------------
    $bearer = explode(" ", $_SERVER['HTTP_AUTHORIZATION'] ?? '');
    $token  = $bearer[1] ?? ($_POST['token'] ?? ($_GET['token'] ?? ''));
    $auth   = json_decode(is_jwt_valid($token), true);

    if (($auth['status'] ?? '') === 'Success') {

        $user_id = $auth['payload']['id'];

        // ---- USER CHECK -------------------------------------------------
        $stmt = $conn->prepare("SELECT id FROM shonu_subjects WHERE id = ? LIMIT 1");
        $stmt->bind_param("s", $user_id);
        $stmt->execute();
        $userRes = $stmt->get_result();
        $stmt->close();

        if ($userRes && $userRes->num_rows === 1) {

            // ---- OPEN SESSION ON AS TECH API -----------------------------
            $api = astech_request('/api/public/v1/session/open', [
                'player_login' => (string)$user_id,   // aapka unique player id
                'game_id'      => $gameId,
                'currency'     => CURRENCY,
                'language'     => 'en',
                'exit_url'     => EXIT_URL,
            ]);

            if (($api['status'] ?? '') === 'success' && !empty($api['content']['gameUrl'])) {
                $gameUrl = $api['content']['gameUrl'];

                if ($isRedirect && filter_var($gameUrl, FILTER_VALIDATE_URL)
                    && in_array(strtolower((string)parse_url($gameUrl, PHP_URL_SCHEME)), ['http', 'https'], true)) {
                    header_remove('Content-Type');
                    header('Location: ' . $gameUrl, true, 302);
                    exit;
                }

                // Alias fields: har frontend chal jaye (url / gameUrl / game_url / launchUrl)
                $res = [
                    'code' => 0,
                    'msg'  => 'Success',
                    'url'       => $gameUrl,
                    'gameUrl'   => $gameUrl,
                    'data' => [
                        'url'       => $gameUrl,
                        'gameUrl'   => $gameUrl,
                        'game_url'  => $gameUrl,
                        'launchUrl' => $gameUrl,
                        'sessionId' => $api['content']['sessionId'] ?? '',
                    ],
                    'serviceNowTime' => $nowTime,
                ];
            } else {
                $res = [
                    'code' => 8,
                    'msg'  => $api['error'] ?: 'Game launch failed',
                    'serviceNowTime' => $nowTime,
                ];
            }
        } else {
            $res = ['code' => 4, 'msg' => 'User not found', 'serviceNowTime' => $nowTime];
        }
    } else {
        $res = ['code' => 3, 'msg' => 'Invalid token', 'serviceNowTime' => $nowTime];
    }
} else {
    $res = ['code' => 2, 'msg' => 'gameCode required', 'serviceNowTime' => $nowTime];
}

header('Content-Type: application/json; charset=utf-8');
echo json_encode($res, JSON_UNESCAPED_SLASHES);

2
Callback.php — seamless wallet
Callback.php
<?php
/**
 * Callback.php - AS Tech API Seamless Wallet Handler
 *
 * AS Tech API is URL par bet / win / rollback bhejta hai.
 * Player ka paisa aapke hi database me rehta hai.
 *
 * SETUP:
 *  1. API_SECRET me panel wali SECRET KEY dalo (wahi jo launch me use hoti hai).
 *  2. USE_CONN_FILE true rakho agar conn.php use karna hai, warna DB values bharo.
 *  3. Is file ka public HTTPS URL panel -> API Details -> Callback URL me save karo.
 *  4. Niche diya CREATE TABLE chala lo (idempotency ke liye zaroori hai).
 *
 * Har response HTTP 200 + is envelope me hona chahiye:
 *   { "status": "success", "error": "", "content": { "cash": "123.45" } }
 */

error_reporting(E_ALL);
ini_set('display_errors', 0);
ini_set('log_errors', 1);
header('Content-Type: application/json; charset=utf-8');
mysqli_report(MYSQLI_REPORT_OFF);

// ============================================
// CONFIGURATION
// ============================================
define('API_SECRET', 'YOUR_SECRET_KEY');   // Panel -> API Details -> Secret key

define('USE_CONN_FILE', false);
define('CONN_FILE', __DIR__ . '/../../conn.php');

define('DB_HOST', 'localhost');
define('DB_PORT', 3306);
define('DB_NAME', 'your_db_name');
define('DB_USER', 'your_db_user');
define('DB_PASS', 'your_db_password');

// Aapki wallet table / column names
define('WALLET_TABLE',  'shonu_kaichila');
define('WALLET_USER',   'balakedara');
define('WALLET_AMOUNT', 'motta');
// ============================================

$conn = null;
if (USE_CONN_FILE && file_exists(CONN_FILE)) {
    include_once CONN_FILE;
}
if (!isset($conn) || !($conn instanceof mysqli) || $conn->connect_error) {
    $conn = @new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME, DB_PORT);
}

$log_dir = __DIR__ . '/apilogs';
if (!file_exists($log_dir)) { mkdir($log_dir, 0777, true); }

function writeLog($msg) {
    global $log_dir;
    file_put_contents($log_dir . "/log_" . date('Y-m-d') . ".txt",
        "[" . date('H:i:s') . "] " . $msg . PHP_EOL, FILE_APPEND);
}

function okReply($cash) {
    http_response_code(200);
    echo json_encode([
        'status'  => 'success',
        'error'   => '',
        'content' => ['cash' => number_format((float)$cash, 2, '.', '')],
    ]);
    exit;
}

function failReply($error, $cash = null) {
    http_response_code(200);
    echo json_encode([
        'status'  => 'fail',
        'error'   => $error,
        'content' => $cash === null ? null : ['cash' => number_format((float)$cash, 2, '.', '')],
    ]);
    exit;
}

function signatureHeader() {
    $headers = function_exists('getallheaders') ? getallheaders() : [];
    foreach ($headers as $k => $v) {
        if (strtolower($k) === 'x-signature') return trim($v);
    }
    return trim($_SERVER['HTTP_X_SIGNATURE'] ?? '');
}

function playerBalance(mysqli $conn, $login) {
    $sql = "SELECT " . WALLET_AMOUNT . " AS bal FROM " . WALLET_TABLE . " WHERE " . WALLET_USER . " = ? LIMIT 1";
    $st = $conn->prepare($sql);
    $st->bind_param("s", $login);
    $st->execute();
    $r = $st->get_result();
    $row = $r ? $r->fetch_assoc() : null;
    $st->close();
    return $row === null ? null : round((float)$row['bal'], 2);
}

writeLog("------------------------------------------------");

if (!($conn instanceof mysqli) || $conn->connect_error) {
    writeLog("CRITICAL: DB connection failed");
    failReply('DB connection error');
}

// Create the idempotency ledger automatically. Without this table getBalance
// works, but the first writeBet would otherwise fail with an HTTP 500.
$ledgerSql = "CREATE TABLE IF NOT EXISTS astech_bet_logs (
  id INT AUTO_INCREMENT PRIMARY KEY,
  user_id VARCHAR(100) NOT NULL,
  game_id VARCHAR(120),
  bet_amount DECIMAL(15,2) DEFAULT 0.00,
  win_amount DECIMAL(15,2) DEFAULT 0.00,
  balance_before DECIMAL(15,2) DEFAULT 0.00,
  balance_after DECIMAL(15,2) DEFAULT 0.00,
  serial_number VARCHAR(200) NOT NULL UNIQUE,
  game_round VARCHAR(200),
  currency_code VARCHAR(10) DEFAULT 'INR',
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  INDEX idx_user_id (user_id),
  INDEX idx_serial_number (serial_number)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4";
if (!$conn->query($ledgerSql)) {
    writeLog("CRITICAL: astech_bet_logs setup failed: " . $conn->error);
    failReply('Bet ledger setup failed');
}

$raw  = file_get_contents('php://input');
$sig  = signatureHeader();
$calc = hash_hmac('sha256', $raw, API_SECRET);

writeLog("Payload: " . $raw);

if ($sig === '' || !hash_equals($calc, strtolower($sig))) {
    writeLog("ERROR: signature mismatch. got=" . $sig . " calc=" . $calc);
    failReply('Invalid signature');
}

$p = json_decode($raw, true);
if (!is_array($p)) failReply('Invalid JSON');

$cmd      = trim((string)($p['cmd'] ?? ''));
$login    = trim((string)($p['user_login'] ?? ($p['login'] ?? '')));
$currency = trim((string)($p['currency'] ?? 'INR'));
$bet      = round((float)($p['bet'] ?? 0), 2);
$win      = round((float)($p['win'] ?? 0), 2);
$txnId    = trim((string)($p['transaction_id'] ?? ''));
$roundId  = trim((string)($p['round_id'] ?? ''));
$gameId   = trim((string)($p['game_id'] ?? ''));

if ($login === '') failReply('user_login required');

$balance = playerBalance($conn, $login);
if ($balance === null) failReply('Player not found');

// -------------------- getBalance --------------------
if ($cmd === 'getBalance') {
    writeLog("getBalance " . $login . " = " . $balance);
    okReply($balance);
}

// -------------------- writeBet ----------------------
if ($cmd === 'writeBet') {
    if ($txnId === '') failReply('transaction_id required', $balance);

    // idempotency: same transaction_id dobara aaye to stored balance wapas do
    $dup = $conn->prepare("SELECT balance_after FROM astech_bet_logs WHERE serial_number = ? LIMIT 1");
    if (!$dup) {
        writeLog("writeBet prepare failed: " . $conn->error);
        failReply('Bet ledger unavailable', $balance);
    }
    $dup->bind_param("s", $txnId);
    $dup->execute();
    $dupRes = $dup->get_result();
    if ($dupRes && $dupRes->num_rows > 0) {
        $row = $dupRes->fetch_assoc();
        $dup->close();
        writeLog("duplicate " . $txnId);
        okReply((float)$row['balance_after']);
    }
    $dup->close();

    if ($bet <= 0 && $win <= 0) failReply('bet or win must be greater than 0', $balance);
    if ($bet > 0 && $balance < $bet) {
        writeLog("insufficient funds " . $login . " bal=" . $balance . " bet=" . $bet);
        failReply('Insufficient balance', $balance);
    }

    $conn->begin_transaction();
    try {
        $sql = "UPDATE " . WALLET_TABLE . " SET " . WALLET_AMOUNT . " = " . WALLET_AMOUNT . " - ? + ? "
             . "WHERE " . WALLET_USER . " = ? AND " . WALLET_AMOUNT . " >= ?";
        $up = $conn->prepare($sql);
        if (!$up) throw new Exception('Wallet update prepare failed: ' . $conn->error);
        $up->bind_param("ddsd", $bet, $win, $login, $bet);
        $up->execute();
        if ($up->affected_rows <= 0) throw new Exception('Balance update rejected');
        $up->close();

        $newBalance = playerBalance($conn, $login);

        $log = $conn->prepare(
            "INSERT INTO astech_bet_logs (user_id, game_id, bet_amount, win_amount, balance_before, balance_after,
             serial_number, game_round, currency_code, created_at) VALUES (?,?,?,?,?,?,?,?,?,NOW())"
        );
        if (!$log) throw new Exception('Bet log prepare failed: ' . $conn->error);
        $log->bind_param("ssddddsss", $login, $gameId, $bet, $win, $balance, $newBalance, $txnId, $roundId, $currency);
        $log->execute();
        $log->close();

        $conn->commit();
        writeLog("writeBet ok " . $login . " " . $balance . " -> " . $newBalance);
        okReply($newBalance);
    } catch (Throwable $e) {
        $conn->rollback();
        writeLog("writeBet failed: " . $e->getMessage());
        failReply('Balance update failed', playerBalance($conn, $login));
    }
}

// -------------------- rollback ----------------------
if ($cmd === 'rollback') {
    if ($txnId === '') failReply('transaction_id required', $balance);

    $done = $conn->prepare("SELECT balance_after FROM astech_bet_logs WHERE serial_number = ? LIMIT 1");
    $rbId = $txnId . ':rollback';
    $done->bind_param("s", $rbId);
    $done->execute();
    $doneRes = $done->get_result();
    if ($doneRes && $doneRes->num_rows > 0) {
        $row = $doneRes->fetch_assoc();
        $done->close();
        okReply((float)$row['balance_after']);   // already rolled back
    }
    $done->close();

    $orig = $conn->prepare("SELECT bet_amount, win_amount FROM astech_bet_logs WHERE serial_number = ? LIMIT 1");
    $orig->bind_param("s", $txnId);
    $orig->execute();
    $origRes = $orig->get_result();
    $origRow = $origRes ? $origRes->fetch_assoc() : null;
    $orig->close();

    // unknown transaction -> success + current balance (spec)
    if (!$origRow) okReply($balance);

    $delta = round((float)$origRow['bet_amount'] - (float)$origRow['win_amount'], 2);

    $conn->begin_transaction();
    try {
        $sql = "UPDATE " . WALLET_TABLE . " SET " . WALLET_AMOUNT . " = " . WALLET_AMOUNT . " + ? WHERE " . WALLET_USER . " = ?";
        $up = $conn->prepare($sql);
        $up->bind_param("ds", $delta, $login);
        $up->execute();
        $up->close();

        $newBalance = playerBalance($conn, $login);
        $zero = 0.00;

        $log = $conn->prepare(
            "INSERT INTO astech_bet_logs (user_id, game_id, bet_amount, win_amount, balance_before, balance_after,
             serial_number, game_round, currency_code, created_at) VALUES (?,?,?,?,?,?,?,?,?,NOW())"
        );
        $log->bind_param("ssddddsss", $login, $gameId, $zero, $zero, $balance, $newBalance, $rbId, $roundId, $currency);
        $log->execute();
        $log->close();

        $conn->commit();
        writeLog("rollback ok " . $txnId . " -> " . $newBalance);
        okReply($newBalance);
    } catch (Throwable $e) {
        $conn->rollback();
        writeLog("rollback failed: " . $e->getMessage());
        failReply('Rollback failed', playerBalance($conn, $login));
    }
}

failReply('Unknown cmd: ' . $cmd, $balance);

/*
CREATE TABLE IF NOT EXISTS astech_bet_logs (
  id INT AUTO_INCREMENT PRIMARY KEY,
  user_id VARCHAR(100) NOT NULL,
  game_id VARCHAR(120),
  bet_amount DECIMAL(15,2) DEFAULT 0.00,
  win_amount DECIMAL(15,2) DEFAULT 0.00,
  balance_before DECIMAL(15,2) DEFAULT 0.00,
  balance_after DECIMAL(15,2) DEFAULT 0.00,
  serial_number VARCHAR(200) UNIQUE,
  game_round VARCHAR(200),
  currency_code VARCHAR(10) DEFAULT 'INR',
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  INDEX idx_user_id (user_id),
  INDEX idx_serial_number (serial_number)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
*/

Errors & rate limits

HTTP codes
401  Missing / invalid API key or signature
403  IP not whitelisted, domain not allowed, or callback URL not configured
404  Player or game not found
400  Insufficient funds or bad parameters
409  Wrong wallet mode for this endpoint
429  Rate limit exceeded (default 120 req/min per key)
503  Platform maintenance or provider offline

Every response carries an x-request-id header — quote it in support tickets and we can pull the exact request, timing and response from the API log. Retry 429 and 503 with exponential backoff; never retry 4xx validation errors.

Go-live checklist

✔ API key and secret stored in server environment variables only.

✔ Production server IP whitelisted in API Details.

✔ Production domain whitelisted in API Details.

✔ HTTPS callback URL saved and answering the envelope format.

✔ Signature verified on every callback.

transaction_id stored with a unique index for idempotency.

✔ Game list cached and refreshed on a schedule.

✔ Tested one full cycle: launch → bet → win → rollback.