FairDealPlus API Docs

Server: http://SERVER_IP:5018
WS: ws://SERVER_IP:5018/ws

๐ŸŽฎ Player Test Access

Test Account Credentials (Ready to Use)

Endpoint http://SERVER_IP:5018/api/loginPlayer
Username player01
Password Player@123
Balance 1500 points
Token will auto-fill in all test forms below

๐Ÿ“‹ Overview

FairDealPlus Go Server runs on port 5018. It provides REST HTTP APIs and a WebSocket endpoint.
All protected routes require a JWT token in the Authorization: Bearer <token> header.
Base URL: http://SERVER_IP:5018
WebSocket: ws://SERVER_IP:5018/ws
Content-Type: application/json
โš ๏ธ Unity tip: Use UnityWebRequest for REST APIs and WebSocket or NativeWebSocket package for real-time game events.

๐Ÿ” Authentication

POST /api/login Public Admin / Agent login
FieldTypeNote
userNamestringrequired
passwordstringrequired

Response

{ "success": true, "token": "eyJhbGci..." }
โšก Live Test
POST /api/loginPlayer Public Player login (Unity uses this)
FieldTypeNote
userNamestringrequired
passwordstringrequired
devicenamestringoptional Android / iOS / PC

Response

{ "success": true, "token": "eyJhbGci..." }
โœ… Use this token in all subsequent requests as Authorization: Bearer TOKEN
โšก Live Test
POST /api/register ๐Ÿ”’ Auth Required Create new user (Admin only)
FieldTypeNote
userNamestringrequired unique
passwordstringrequired
referralIdUUIDrequired parent user UUID
rolestringoptional player/agent/distributor/superAdmin
creditPointnumberoptional starting balance
commissionPercentagenumberoptional
โšก Live Test
POST /api/setForgetPassword Public Reset password via OTP
FieldTypeNote
userNamestringrequired
mobilestringrequired
newPasswordstringrequired
otpstringoptional
{ "success": true, "message": "Password updated" }

๐Ÿ‘ค Player APIs

All routes under /api/player/* require JWT token. Send as: Authorization: Bearer TOKEN

GET /api/player/me ๐Ÿ”’ Auth Get my profile + balance

Response Fields

FieldTypeDescription
idUUIDPlayer UUID
userNamestringUsername
rolestringplayer / agent / superAdmin
creditPointnumber๐Ÿ’ฐ Current balance
isActiveboolAccount active status
typestringRV or TN (commission mode)
{
  "success": true,
  "data": {
    "id": "035e2653-50a1-4c68-977b-ffc25badcede",
    "userName": "player01",
    "role": "player",
    "creditPoint": 1500,
    "isActive": true,
    "isLogin": true,
    "type": "RV"
  }
}
โšก Live Test
GET /api/player/bets?page=1&limit=20 ๐Ÿ”’ Auth Player bet history
Query ParamTypeDefault
pageint1
limitint20 (max 100)
{
  "bets": [
    { "id": "uuid", "game": "rouletteTimer40", "betAmount": 100,
      "wonAmount": 3600, "winPosition": "7", "createdAt": "..." }
  ],
  "page": 1, "limit": 20
}
โšก Live Test
GET /api/player/wins?game=rouletteTimer40&limit=15 ๐Ÿ”’ Auth Last game results (hot/cold data)
Query ParamTypeNote
gamestringrequired game name
limitintdefault 15
{
  "game": "rouletteTimer40",
  "results": ["26", "3", "11", "0", "36"],
  "x": [1, 1, 1, 1, 1]
}
โšก Live Test
POST /api/player/point-request ๐Ÿ”’ Auth Request points (deposit/withdraw)
FieldTypeNote
pointnumberrequired
commentstringoptional reason
fromIdUUIDoptional agent UUID
{ "message": "Point request submitted", "requestId": "uuid" }
โšก Live Test
POST /api/player/updatePassword ๐Ÿ”’ Auth Change password
FieldType
oldPasswordstringrequired
newPasswordstringrequired
POST /api/player/logout ๐Ÿ”’ Auth Logout player
{ "success": true, "message": "Logged out" }

๐Ÿ›ก๏ธ Admin APIs

Requires admin/agent/distributor/superAdmin JWT token.

GET /api/admin/online ๐Ÿ”’ Admin Get online players list
{ "count": 5, "players": [{ "id":"...", "userName":"player01", "creditPoint": 1500 }] }
โšก Live Test
GET /api/admin/stats?game=spinToWin ๐Ÿ”’ Admin Game stats โ€” collection vs payment
{
  "gameName": "spinToWin",
  "totalCollection": 50000,
  "totalPayment": 43200,
  "lastResults": ["3","7","1"],
  "xs": [1,1,1]
}
โšก Live Test
GET /api/admin/users?role=player&page=1 ๐Ÿ”’ Admin List all users
ParamTypeNote
rolestringplayer/agent/distributor (optional filter)
pageintdefault 1, 50 per page
โšก Live Test
POST /api/admin/user/:id/balance ๐Ÿ”’ Admin Update player balance
FieldTypeNote
amountnumberrequired
opstring"add" (default) or "set"
POST /api/admin/user/:id/toggle ๐Ÿ”’ Admin Block/unblock player account
{ "isActive": false }

โšก WebSocket Connection

URL: ws://SERVER_IP:5018/ws

All WebSocket messages are JSON in this format:
Client โ†’ Server: { "event": "eventName", "data": { ... } }
Server โ†’ Client: { "event": "eventName", "data": { ... }, "status": 1 }
Unity Package: Use NativeWebSocket โ€” WebSocket ws = new WebSocket("ws://SERVER_IP:5018/ws");

Unity Connection Code Example

// 1. Connect
WebSocket ws = new WebSocket("ws://SERVER_IP:5018/ws");
await ws.Connect();

// 2. Send event
string msg = JsonConvert.SerializeObject(new {
    @event = "join",
    data = new { token = playerToken, gameName = "rouletteTimer40" }
});
await ws.SendText(msg);

// 3. Receive
ws.OnMessage += (bytes) => {
    var text = System.Text.Encoding.UTF8.GetString(bytes);
    var msg = JsonConvert.DeserializeObject<SocketMsg>(text);
    Debug.Log($"Event: {msg.@event}, Data: {msg.data}");
};

๐Ÿ“ค Client โ†’ Server Events

WS join Join a game room โ€” FIRST event to send
FieldTypeNote
tokenstringrequired JWT token from login
gameNamestringrequired see game list below
// Send:
{ "event": "join", "data": { "token": "eyJ...", "gameName": "rouletteTimer40" } }

// Response event: "join"
{
  "event": "join",
  "data": {
    "creditPoint": 1500,
    "gameName": "rouletteTimer40",
    "gamelastdata": ["26","3","11","7","0"],
    "x": [1,1,1,1,1],
    "gameState": { "positions": {}, "adminBalance": 0 }
  },
  "status": 1
}
WS checkLogin Verify token + auto-resume if reconnecting within 45s grace

Send this on every WebSocket open โ€” for fresh logins and reconnects. Server checks the reconnect-grace buffer; if the user dropped within the last 45s their previous game room is auto-restored. See Reconnect & Grace for full flow.

// Request
{ "event": "checkLogin", "data": { "token": "eyJ..." } }

Server emits ONE of the following sequences:

(A) Resume successful โ€” player was in a room and reconnected within 45s

{ "event": "state_snapshot", "data": {
    "room": "rouletteTimer40", "resumed": true,
    "creditPoint": 1500.0,
    "state": { /* current round state from GameEngine */ },
    "gamelastdata": ["7","26","11"], "x": [1,1,1]
} }
{ "event": "session_status", "data": { "resumed": true, "room": "rouletteTimer40" } }

Unity action: restore game UI from state_snapshot. Do NOT send join again.

(B) Fresh login OR grace expired (token still valid)

{ "event": "session_status", "data": { "resumed": false, "room": "" } }

Unity action: go to lobby / game-select screen. User picks a game โ†’ send join normally.

(C) Token expired or invalid

{ "event": "error", "data": { "message": "Invalid token" } }

Unity action: clear stored token โ†’ login screen.

(D) Another device hijacked the session

{ "event": "logout", "data": { "message": "Another device login" } }

Unity action: clear stored token โ†’ login screen with warning.

WS placeBet Place bet on timer games (roulette40/60, spinToWin, etc.)
FieldTypeNote
playerIdUUIDrequired
gameNamestringrequired
betPointnumberrequired total bet amount
positionobjectrequired { "7": 100, "red": 50 }
// Send:
{
  "event": "placeBet",
  "data": {
    "playerId": "035e2653-50a1-4c68-977b-ffc25badcede",
    "gameName": "rouletteTimer40",
    "betPoint": 150,
    "position": { "7": 100, "red": 50 }
  }
}

// Response event: "placeBet"
{ "event": "placeBet", "data": { "handId": "bet-uuid", "gameName": "rouletteTimer40", "result": "Bet placed successfully" }, "status": 1 }
WS placeBetAB Andar Bahar bet
FieldTypeNote
playerIdUUIDrequired
gameNamestring"andarBahar"
betPointnumberrequired
positionobject{ "side": "A" } or { "side": "B" }
roundint1 or 2
{ "event": "placeBetAB", "data": { "playerId": "...", "gameName": "andarBahar", "betPoint": 100, "position": { "side": "A" }, "round": 1 } }
WS placeBetRoulette Manual roulette โ€” instant result
// Send:
{ "event": "placeBetRoulette", "data": { "playerId": "...", "betPoint": 200, "position": { "7": 100, "red": 50, "even": 50 } } }

// Instant Response:
{ "event": "result", "data": { "handId": "uuid", "gameName": "roulette", "data": "7", "winAmount": 3600 } }
WS placeBetManualSpin Manual spin (1-9) โ€” instant result, 9x pay
{ "event": "placeBetManualSpin", "data": { "playerId": "...", "betPoint": 100, "position": { "5": 100 } } }

// Instant:
{ "event": "result", "data": { "handId": "uuid", "gameName": "manualSpin", "data": "5", "winAmount": 900 } }
WS joinAdmin Admin joins game admin room
{ "event": "joinAdmin", "data": { "adminId": "admin-uuid", "gameName": "rouletteTimer40" } }

// Response:
{ "event": "resAdmin", "data": { "numbers": [...], "totalCollection": 5000, "totalPayment": 4200 } }
WS winByAdmin Admin force sets winner number
{ "event": "winByAdmin", "data": { "gameName": "rouletteTimer40", "cardNumber": 7, "y": 1.0 } }
WS balanceupdate Request latest balance for a user
{ "event": "balanceupdate", "data": { "userid": "player-uuid" } }

// Response: "UPDATED_WALLET" event with new balance number

๐Ÿ“ฅ Server โ†’ Client Events

These events are pushed by the server. Unity should listen for all of them.

EventWhenKey Data Fields
session_statusAlways after checkLoginresumed (bool), room (string)
state_snapshotReconnect within 45s graceroom, resumed, creditPoint, state, gamelastdata[], x[]
joinAfter successful joincreditPoint, gameName, gamelastdata[], x[], gameState, gameId
newRoundNew round starts (timer games)gameName, gamelastdata[], x[], state
betClosedBetting window closedgameName
round2Andar Bahar round 2 startsgameName, round: 2
resultRound result announcedgameName, data (winning number), x, gamelastdata[]
winRound won (manual roulette / instant games)handId, gameName, data, winAmount
placeBetBet placed confirmationhandId (betID), gameName, result (message)
UPDATED_WALLETBalance changednew balance (number)
logoutAccount blocked / force-kick / another device / idlemessage
maintenanceServer entered maintenance modemessage
errorAny error (invalid token, bad bet, etc.)message
boopReply to beep keepalive(empty)
resAdminAdmin join responsenumbers[], totalCollection, totalPayment
resAdminResultRound result (admin room)gameName, result
resAdminBetDataBet placed (admin room)gameName, totalCollection, totalPayment
versionApp version infoandroid (url), exe (url), version

Timer Game Round Flow (rouletteTimer40 example)

  [0s]    Server โ†’ "newRound"   { gameName, gamelastdata, state }
  [0-45s] Player  โ†’ "placeBet"   { position, betPoint }
  [45s]   Server โ†’ "betClosed"  { gameName }
  [53s]   Server โ†’ "result"     { data: "7", x: 1, gamelastdata: ["7","26",...] }
          Server โ†’ "UPDATED_WALLET"  1400   (only to winner)
  [53s]   Next round begins โ†’ "newRound" again

Andar Bahar Round Flow

  [0s]    Server โ†’ "newRound"   { gameName: "andarBahar", round: 1 }
  [0-30s] Player โ†’ "placeBetAB" { side: "A", betPoint: 100 }
  [30s]   Server โ†’ "betClosed"  { round: 1 }
  [30s]   Server โ†’ "round2"     { gameName, round: 2 }
  [30-45s] Player โ†’ "placeBetAB" { side: "B", betPoint: 50 }
  [45s]   Server โ†’ "betClosed"  { round: 2 }
  [45s]   Server โ†’ "result"     { data: "A" or "B" }   pays 2x

๐Ÿ” Reconnect & Grace Period

When a mobile app goes to background the OS freezes / kills the WebSocket within ~30s. To avoid kicking the player out of an in-progress round, the server holds disconnected players in a 45-second reconnect-grace buffer before running the real cleanup. If the player reconnects within that window, the previous game room and session are restored seamlessly.

Grace duration: 45 seconds (constant reconnectGrace in internal/hub/hub.go)
What is buffered: only role=player sockets. Admins and unauthed sockets clean up instantly.
What is NOT buffered: ForceKick / KickAllForMaintenance / 15-minute idle logout โ€” these bypass the grace and clean up immediately.

Server-side behavior on disconnect

StepAction
1Player socket dies (background / network drop / pong timeout after 60s)
2Player removed from broadcast room (no point sending newRound/result to a dead socket)
3userClients[userID] entry kept; is_login=true in DB; is_online=true in Redis
4Pending entry created with 45s timer
5aReconnect within 45s: timer cancelled, new socket auto-rejoins previous room, state_snapshot + session_status emitted
5bTimer expires: full cleanup โ€” DelOnline, SetLogout, RecordLogout(reason=disconnect), removed from userClients

Unity reconnect flow

  [App background]
       โ†“ (close socket explicitly on OnApplicationPause(true))
  Server: buffer player โ†’ 45s grace timer started

  [App foreground, 30s later]
       โ†“ open new WebSocket
       โ†“ Send: { "event": "checkLogin", "data": { "token": "..." } }
       โ†“
  Server matches userID against pending buffer:
       โ”œโ”€โ”€ Found       โ†’ emit "state_snapshot" + "session_status" {resumed:true}
       โ”œโ”€โ”€ Not found   โ†’ emit "session_status" {resumed:false}
       โ”œโ”€โ”€ Bad token   โ†’ emit "error" {message:"Invalid token"}
       โ””โ”€โ”€ Hijacked    โ†’ emit "logout" {message:"Another device login"}

  [Unity reads session_status]
       โ”œโ”€โ”€ resumed:true  โ†’ restore game UI from state_snapshot โ€” DONE
       โ”œโ”€โ”€ resumed:false โ†’ go to lobby, send "join" event
       โ””โ”€โ”€ error/logout  โ†’ clear token, return to login

Unity SocketManager pseudocode

void OnApplicationPause(bool paused) {
    if (paused) {
        socket?.Close();  // clean signal to server
    } else {
        StartCoroutine(ReconnectWithBackoff());  // 1s, 2s, 4s, 8s, 15s
    }
}

void OnMessage(SocketMsg msg) {
    switch (msg.@event) {
        case "state_snapshot":
            GameUI.RestoreFromSnapshot(msg.data); break;
        case "session_status":
            if (!msg.data.resumed && wasInGame) GoToLobby(); break;
        case "error":
            if (msg.data.message == "Invalid token") ClearTokenAndGoToLogin(); break;
        case "logout":
            ClearTokenAndGoToLogin(msg.data.message); break;
        case "maintenance":
            ShowMaintenanceScreen(msg.data.message); break;
        // ... game events: newRound, betClosed, result, win, UPDATED_WALLET ...
    }
}
โš ๏ธ Unity rules
โ€ข On reconnect, send only checkLogin โ€” NOT join. The server will auto-rejoin the room and push state_snapshot.
โ€ข Always wait for session_status before deciding next screen โ€” it is the deterministic signal that resume succeeded or not.
โ€ข If session_status doesn't arrive within 3 seconds of checkLogin, treat as network failure and retry.
โ€ข Server is authoritative โ€” disable bet UI on betClosed, don't rely on client-side timers.

Server log signatures (for debugging)

// Disconnect โ†’ buffered:
Client disconnected (buffered 45s): <userID> room=rouletteTimer40

// Reconnect within grace:
Reconnect within grace: user=<userID> prev_room=rouletteTimer40

// Grace expired (no reconnect within 45s):
Reconnect grace expired: user=<userID> โ€” full cleanup done

Live monitor: journalctl -u fairdealplus-go -f | grep -E "buffered|grace|Reconnect"

๐Ÿงช WebSocket Live Tester

Disconnected
Quick Events
Custom JSON
Place Bet

Click to send pre-built events:

version
join room
checkLogin
leaveRoom
balanceupdate
beep (ping)
Token:
Game:
Message Log
โ€” WebSocket log will appear here โ€”

๐ŸŽฒ Game List

Game NameTypeTimerPositionsPayout
rouletteTimer40Timer53s round0-36 (37 nums) + colors/dozens36x single, 2x color
rouletteTimer60Timer73s round0-36 (37 nums) + colors/dozens36x single, 2x color
spinToWinTimer60s round1-99x
funTargetTimer60s round1-99x
funRouletteTimer60s round0-3636x / 2x
andarBaharTimer30s+15sside: "A" or "B"2x
rouletteManualInstant0-36 + red/black/even/odd/dozens/cols36x / 2x / 3x
manualSpinManualInstant1-99x

๐Ÿ’ฐ Roulette Position Reference

Position keys for roulette bets โ€” combine any of these in one bet object.
KeyDescriptionPayout
"0" to "36"Single number36x
"red"Red numbers2x
"black"Black numbers2x
"even"Even numbers (2,4,6...)2x
"odd"Odd numbers2x
"low"1-182x
"high"19-362x
"dozen1"1-123x
"dozen2"13-243x
"dozen3"25-363x
"col1"Column 1 (1,4,7...)3x
"col2"Column 2 (2,5,8...)3x
"col3"Column 3 (3,6,9...)3x

Example Multi-position Bet

{
  "event": "placeBet",
  "data": {
    "playerId": "035e2653-50a1-4c68-977b-ffc25badcede",
    "gameName": "rouletteTimer40",
    "betPoint": 350,
    "position": {
      "7":    100,   // 100 on number 7 โ†’ wins 3600 if 7 hits
      "red":  100,   // 100 on red โ†’ wins 200 if red hits
      "odd":  100,   // 100 on odd โ†’ wins 200 if odd hits
      "dozen1": 50   // 50 on 1-12 โ†’ wins 150 if 1-12 hits
    }
  }
}