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.
Base URL
https://astechapi.cloudAuth headers
X-Api-Key + X-SignatureQuick start
- Open Panel → API Details and copy your API key and secret key.
- Set your server IP, your domain and your callback URL there. Without all three every API call is rejected with
403. - Sign every request body with HMAC-SHA256 using your secret key.
- Call
/api/public/v1/games, then/api/public/v1/session/opento launch a game. - Seamless mode: implement the callback endpoint below. Transfer mode: use
/walletto fund players.
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.
X-Api-Key: <your api key>
X-Signature: <hex hmac_sha256(secret_key, raw_body)>
Content-Type: application/jsonimport 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;
}• 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.
// 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
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.
{ "currency": "INR", "provider": "Pragmatic", "category": "slots" }{ "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
Creates the player if it does not exist and returns a launch URL valid for a single session.
{
"player_login": "john", // your unique player id
"game_id": "0f1c...uuid", // from /games
"currency": "USD",
"language": "en",
"exit_url": "https://your-site.com/lobby"
}{ "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.
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.
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
Transfer mode only. All cash operations are idempotent on transaction_id — retrying a timed-out call is safe.
{ "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" }{ "content": { "id": "john", "currency": "USD", "cash": "100.00" } }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 a round against the wallet we hold for the player. Idempotent on transaction_id, and it feeds the daily GGR aggregate automatically.
{ "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" }{ "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.
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" }{ "status": "success", "error": "", "content": { "cash": "99.00" } }
// not enough money
{ "status": "fail", "error": "INSUFFICIENT_FUNDS", "content": null }// 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);• 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.
<?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);
<?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
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 offlineEvery 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.