Frontend Integration Guide
Complete reference for integrating a frontend client with the BattlesBit GraphQL API. All examples use the actual schema — copy-paste ready.
0. Connection Setup
HTTP (Queries & Mutations)
| Setting | Value |
|---|---|
| Endpoint | /graphql |
| Method | POST |
| Content-Type | application/json |
| Auth header | Authorization: Bearer {accessToken} |
WebSocket (Subscriptions)
| Setting | Value |
|---|---|
| Endpoint | /graphql (use ws:// or wss://) |
| Protocol | graphql-ws |
| Auth | Pass token in connectionParams |
import { createClient } from "graphql-ws";
const wsClient = createClient({
url: "wss://api.battlesbit.com/graphql",
connectionParams: {
authorization: "Bearer " + accessToken,
},
});Reconnect Strategy
Use exponential backoff for both HTTP retries and WebSocket reconnections:
1s → 2s → 4s → 8s → 16s → 30s (cap)const wsClient = createClient({
url: "wss://api.battlesbit.com/graphql",
connectionParams: {
authorization: "Bearer " + accessToken,
},
retryAttempts: Infinity,
retryWait: (retries) => {
const delay = Math.min(1000 * Math.pow(2, retries), 30000);
return new Promise((resolve) => setTimeout(resolve, delay));
},
});1. Authentication
Request Email Verification Code
mutation RequestCode($email: String!) {
requestEmailVerificationCode(email: $email) {
expiresIn
}
}Returns expiresIn (seconds until the code expires) — not a success boolean.
Login with Email (OTP)
mutation LoginEmail($email: String!, $code: String!) {
loginWithEmail(email: $email, verificationCode: $code) {
accessToken
refreshToken
expiresIn
}
}Login with Password
mutation LoginPassword($email: String!, $password: String!) {
loginWithPassword(email: $email, password: $password) {
accessToken
refreshToken
expiresIn
}
}Refresh Token
mutation Refresh($refreshToken: String!) {
refreshToken(refreshToken: $refreshToken) {
accessToken
refreshToken
expiresIn
}
}Token Storage
| Token | Storage | Reason |
|---|---|---|
accessToken | In-memory variable | Short-lived, never persisted to disk |
refreshToken | Secure storage (Keychain / EncryptedSharedPreferences) | Long-lived, used to obtain new access tokens |
Set up an HTTP interceptor that automatically calls refreshToken when the access token expires and retries the failed request.
2. Profile
Get Current Profile
query Profile {
profile {
id
nickName
avatar
email
level
exp
cup
balance
isEmailVerified
isSuspended
wallets {
id
currency
balance
locked
}
}
}Update Profile
mutation UpdateProfile($input: UpdateProfileInput!) {
updateProfile(updateProfileInput: $input) {
id
nickName
avatar
}
}Variables:
{
"input": {
"nickName": "TraderJoe",
"avatar": "avatar_03"
}
}3. Browse Challenges
Challenges (games) are templates that define the rules for a match room.
query BrowseChallenges($page: Pagination, $order: GameOrder, $filter: GameFilter) {
gamesPage(paginateBy: $page, orderBy: $order, filterBy: $filter) {
data {
id
title
description
entryFee
rewardPrice
currency
duration
minPlayers
maxPlayers
difficulty
cover
allowedSymbols
maxLeverage
allowedDirections
maxDailyDrawdownPct
maxTotalDrawdownPct
profitTargetPct
maxLossPerTradePct
maxPositions
startVBalance
cup
winExp
loseExp
minTrades
maxTradesPerMinute
}
total
}
}Variables:
{
"page": { "limit": 20, "offset": 0 },
"order": { "field": "CREATED_AT", "direction": "DESC" },
"filter": { "isActive": true }
}Key Fields
| Field | Description |
|---|---|
entryFee | Cost to join (deducted from wallet) |
rewardPrice | Prize for the winner |
currency | USDT or SOL |
duration | Match length (e.g. "30m", "1h") |
difficulty | beginner, intermediate, expert |
allowedSymbols | Tradable assets (empty = all) |
maxLeverage | Max leverage allowed (null = global default) |
allowedDirections | both, long_only, short_only |
maxDailyDrawdownPct | Daily drawdown limit as % |
maxTotalDrawdownPct | Total drawdown limit as % |
profitTargetPct | Auto-finish when profit target reached |
4. Browse Match Rooms
Match rooms are instances of a challenge where players compete.
query BrowseMatches($page: Pagination, $order: GameMatchOrder, $filter: GameMatchFilter) {
gameMatchesPage(paginateBy: $page, orderBy: $order, filterBy: $filter) {
data {
id
status
startTime
endTime
gameID
tradingLocked
winnerID
game {
title
entryFee
rewardPrice
currency
duration
minPlayers
maxPlayers
}
participants {
id
userID
vBalance
isDisqualified
user {
nickName
avatar
}
}
}
total
}
}Match Statuses
| Status | Meaning | Joinable? |
|---|---|---|
waiting | Waiting for players | Yes |
open | Trading in progress | No |
pending | Legacy / transitional | No |
closed | Match finished, results available | No |
canceled | Match canceled, fees refunded | No |
archived | Archived match | No |
Display rooms with status: "waiting" as joinable.
5. Join a Match
mutation JoinMatch($matchID: ID!) {
joinMatch(matchID: $matchID) {
id
userID
vBalance
gameMatchID
currency
}
}The entry fee is automatically deducted from the player’s wallet.
Error Handling
| Error message | Cause | UI suggestion |
|---|---|---|
"insufficient balance" | Wallet balance too low | Show deposit prompt |
"match is full" | maxPlayers reached | Refresh room list |
"not in waiting status" | Match already started | Refresh room list |
"user already in match" | Duplicate join attempt | Navigate to match |
"suspended" | Account is suspended | Show suspension notice |
6. Live Trading
Open a Position
mutation OpenPosition($input: CreateGameMatchPositionInput!) {
createGameMatchPosition(createGameMatchPositionInput: $input) {
id
status
baseToken
entryPrice
margin
leverage
side
type
size
liquidationPrice
stopLoss
targetPoint
}
}Variables:
{
"input": {
"matchID": "match-uuid",
"baseToken": "bitcoin",
"entryPrice": 95000.50,
"margin": 10.0,
"leverage": 5,
"side": "buy",
"type": "market",
"stopLoss": 90000.0,
"targetPoint": 100000.0
}
}Important: The argument name is
createGameMatchPositionInput:— notinput:.
Position Input Fields
| Field | Type | Required | Description |
|---|---|---|---|
matchID | ID! | Yes | The match to trade in |
baseToken | String! | Yes | Asset ID (e.g. "bitcoin") |
entryPrice | Float! | Yes | Desired entry price |
margin | Float! | Yes | Amount of virtual balance to use |
leverage | Uint8! | Yes | Leverage multiplier |
side | buy | sell | Yes | Trade direction |
type | market | limit | Yes | Order type |
stopLoss | Float | No | Stop loss price |
targetPoint | Float | No | Take profit price |
Position Creation Errors
| Error message | Cause |
|---|---|
"insufficient balance" | Not enough vBalance |
"max positions exceeded" | Hit maxPositions limit |
"symbol not allowed" | baseToken not in game’s allowedSymbols |
"leverage exceeds" | Over game’s maxLeverage |
"direction not allowed" | Wrong side for long_only / short_only game |
"trading is locked" | Admin locked trading on this match |
"disqualified" | Participant was disqualified by risk rules |
"max loss per trade" | Margin exceeds maxLossPerTradePct of starting balance |
"trading frequency" | Exceeded maxTradesPerMinute |
Update Stop Loss / Take Profit
mutation UpdatePosition($id: ID!, $input: UpdateGameMatchPositionInput!) {
updateGameMatchPosition(id: $id, updateGameMatchPositionInput: $input) {
id
stopLoss
targetPoint
}
}Variables:
{
"id": "position-uuid",
"input": {
"stopLoss": 92000.0,
"targetPoint": 105000.0
}
}Important: The argument name is
updateGameMatchPositionInput:— notinput:.
To clear a stop loss or target point, use the clear fields:
{
"id": "position-uuid",
"input": {
"clearStopLoss": true,
"clearTargetPoint": true
}
}Close a Position (Full or Partial)
mutation ClosePosition($id: ID!, $percentage: Float!) {
closeGameMatchPosition(id: $id, percentage: $percentage) {
id
status
profit
closePrice
closedAt
}
}| Percentage | Result |
|---|---|
100.0 | Full close |
50.0 | Partial close — original position stays open with reduced size, a new closed position record is created |
1.0 — 99.0 | Partial close at that percentage |
Cancel a Pending Order
Cancel a limit order that has not yet been filled:
mutation CancelOrder($id: ID!) {
cancelGameMatchPosition(id: $id)
}Returns Boolean! — true on success.
Get Participant Equity
query Equity($matchID: ID!) {
participantEquity(matchID: $matchID) {
vBalance
unrealizedPnL
equity
usedMargin
freeMargin
}
}| Field | Description |
|---|---|
vBalance | Current virtual balance |
unrealizedPnL | PnL from open positions |
equity | vBalance + unrealizedPnL |
usedMargin | Margin locked in open positions |
freeMargin | equity - usedMargin |
Poll this query or update locally when you receive PnL notifications.
7. Real-Time Subscriptions
Global Notifications
Receive account-level notifications (wallet updates, achievement unlocks, app announcements):
subscription GlobalNotifications {
NotificationGlobal {
target
id
message
type
action
event
data
createdAt
}
}Note: The subscription name is PascalCase:
NotificationGlobal.
Match Notifications
Receive match-specific notifications (position PnL updates, position closed, match events):
subscription MatchNotifications($matchId: ID!) {
NotificationMatch(matchId: $matchId) {
target
id
message
type
action
event
data
createdAt
}
}Note: The parameter is
matchId(lowercased) — notmatchID.
Notification Events
| Event | Description |
|---|---|
MATCH_CREATED | New match room available |
MATCH_CANCELED | Match was canceled |
MATCH_CLOSED | Match finished |
POSITION_UPDATED | Position PnL changed |
POSITION_CANCELED | Limit order canceled |
POSITION_LIQUIDATED | Position hit liquidation price |
POSITION_CLOSED | Position was closed |
WALLET_BALANCE_UPDATED | Wallet balance changed |
APP_NOTIFICATION | General app notification |
ACHIEVEMENT_UNLOCKED | Player unlocked an achievement |
Parsing the data Field
The data field is a JSON string. Parse it to extract PnL and other dynamic values:
const notification = subscriptionData.NotificationMatch;
if (notification.event === "POSITION_UPDATED") {
const data = JSON.parse(notification.data);
// data.pnl — absolute PnL value
// data["pnl%"] — PnL percentage
updatePositionPnL(notification.id, data.pnl, data["pnl%"]);
}Live Asset Prices
Subscribe to real-time price updates for a single asset:
subscription AssetPrice($id: String!) {
assetUpdated(id: $id) {
id
priceUSD
}
}8. Post-Match Results
Match Summary (Player-Facing)
Use matchSummary for player-facing post-match stats:
query MatchSummary($matchID: ID!) {
matchSummary(matchID: $matchID) {
matchID
totalTrades
totalParticipants
bestTradePnL
worstTradePnL
maxDrawdown
duration
winnerID
}
}Note:
gameMatch(id)requires theread:gameMatchespermission (admin only). UsematchSummaryfor regular players.
Access Match Data via Participants
Since gameMatch is admin-only, players can still access match details through the gameMatchesPage query (which is public) to see participants and final standings:
query MatchResults($filter: GameMatchFilter) {
gameMatchesPage(
paginateBy: { limit: 1 }
filterBy: $filter
) {
data {
id
status
winnerID
game { title rewardPrice currency }
participants {
userID
vBalance
peakVBalance
isDisqualified
disqualificationReason
user { nickName avatar }
}
}
}
}9. Wallet Management
Create a Wallet
First-time users need to create a wallet:
mutation CreateWallet($input: CreateWalletInput!) {
createWallet(createWalletInput: $input) {
id
currency
balance
locked
address
}
}Variables:
{
"input": {
"currency": "USDT"
}
}Important: The argument name is
createWalletInput:— notinput:.
Supported currencies: USDT, SOL.
Check Balance
Wallets are available on the profile query:
query MyWallets {
profile {
wallets {
id
currency
balance
locked
address
}
}
}Note:
walletsPagerequires theread:walletspermission (admin only). Useprofile.walletsfor regular players.
Request Withdrawal
mutation Withdraw($input: CreateWithdrawalInput!) {
createWithdrawRequest(createWithdrawalInput: $input) {
id
createdAt
}
}Variables:
{
"input": {
"amount": 50000,
"currency": "USDT",
"address": "0x..."
}
}Withdrawals are created as pending transactions and require admin approval.
10. Market Data
List All Tradable Assets
query AllAssets {
assets {
id
name
symbol
priceUSD
changePercent24Hr
marketCapUSD
volumeUSD24Hr
iconUrl
rank
}
}Get a Single Asset
query SingleAsset($id: String!) {
asset(id: $id) {
id
name
symbol
priceUSD
changePercent24Hr
supply
maxSupply
marketCapUSD
volumeUSD24Hr
vwap24Hr
iconUrl
rank
}
}Subscribe to Live Prices
Single asset:
subscription AssetPrice($id: String!) {
assetUpdated(id: $id) {
id
priceUSD
}
}All assets (bulk updates):
subscription AllAssetPrices {
assetsUpdated {
id
priceUSD
}
}Favorite Assets
# Add to favorites
mutation { createFavoriteAsset(assetID: "bitcoin") }
# Remove from favorites
mutation { deleteFavoriteAsset(assetID: "bitcoin") }Both return Boolean!.
11. Social Features
Send a Friend Request
mutation SendRequest($friendID: ID!) {
sendFriendRequest(friendID: $friendID) {
id
status
userID
friendID
}
}Accept a Friend Request
mutation AcceptRequest($requestID: ID!) {
acceptFriendRequest(requestID: $requestID) {
id
status
}
}Reject a Friend Request
mutation RejectRequest($requestID: ID!) {
rejectFriendRequest(requestID: $requestID) {
id
status
}
}Remove a Friend
mutation RemoveFriend($friendshipID: ID!) {
removeFriend(friendshipID: $friendshipID)
}Returns Boolean!.
List Friends
query MyFriends($first: Int) {
friends(first: $first) {
edges {
node {
id
userID
friendID
status
user { nickName avatar }
friend { nickName avatar }
}
}
}
}List Pending Friend Requests
query PendingRequests($first: Int) {
friendRequests(first: $first) {
edges {
node {
id
userID
friendID
status
user { nickName avatar }
}
}
}
}12. Notifications
Notification Center (Persistent)
query MyNotifications($first: Int) {
myNotifications(first: $first) {
edges {
node {
id
message
event
type
target
targetID
isRead
createdAt
}
}
}
}| Field | Description |
|---|---|
target | What the notification is about (e.g. "MATCH", "POSITION", "WALLET") |
targetID | UUID of the related entity |
isRead | Whether the user has seen it |
event | Event type string for icon/color mapping |
type | INFO, WARNING, or ERROR |
Unread Count (Badge)
query {
unreadNotificationCount
}Mark as Read
# Single notification
mutation MarkRead($id: ID!) {
markNotificationRead(id: $id) {
id
isRead
}
}
# All notifications
mutation {
markAllNotificationsRead
}13. Leaderboard
query Leaderboard($page: Pagination, $period: String) {
leaderboard(paginateBy: $page, period: $period) {
data {
userID
nickName
avatar
cup
exp
level
}
total
}
}Period Values
| Value | Meaning |
|---|---|
null | All-time leaderboard |
"weekly" | Current week |
"monthly" | Current month |
Variables example:
{
"page": { "limit": 50, "offset": 0 },
"period": "weekly"
}14. Store
Browse Store Items
query StoreItems {
storeItems(first: 50, filterBy: { isActive: true }) {
edges {
node {
id
title
description
price
paymentType
currency
isActive
consumable {
id
name
description
avatar
type
value
}
}
}
}
}Get a Single Store Item
query StoreItem($id: ID!) {
storeItem(id: $id) {
id
title
description
price
paymentType
currency
consumable { id name avatar type value }
}
}Purchase a Store Item
mutation BuyItem($id: ID!) {
requestPurchaseStoreItem(id: $id)
}Returns Boolean!.
Browse Store Packages
query StorePackages {
storePackages(first: 50, filterBy: { isActive: true }) {
edges {
node {
id
title
description
avatar
price
paymentType
currency
isActive
items {
id
quantity
consumable { id name avatar type value }
}
}
}
}
}Get a Single Store Package
query StorePackage($id: ID!) {
storePackage(id: $id) {
id
title
description
avatar
price
paymentType
currency
items {
id
quantity
consumable { id name avatar type value }
}
}
}Purchase a Store Package
mutation BuyPackage($id: ID!) {
requestPurchaseStorePackage(id: $id)
}Returns Boolean!.
15. Achievements
Browse Achievements
query AllAchievements {
achievements(first: 100) {
edges {
node {
id
key
name
description
avatar
isActive
isHidden
expiresAt
rules {
id
name
field
target
comparator
weight
}
rewards {
id
name
avatar
type
value
}
}
}
}
}Get a Single Achievement
query Achievement($id: ID!) {
achievement(id: $id) {
id
key
name
description
avatar
isActive
isHidden
rules { id name field target comparator weight }
rewards { id name avatar type value }
}
}Set Favorite Achievements (Profile Showcase)
mutation SetFavorites($favorites: [String!]!) {
updateProfileFavoriteAchievement(favoriteAchievement: $favorites) {
id
achievementID
}
}Variables:
{
"favorites": ["achievement-key-1", "achievement-key-2", "achievement-key-3"]
}16. Push Notifications
Register Device Token
mutation RegisterDevice($input: CreateFCMTokenInput!) {
createFCMToken(createFCMTokenInput: $input) {
id
token
platform
}
}Variables:
{
"input": {
"token": "fcm-device-token-here",
"platform": "ios"
}
}Important: The argument name is
createFCMTokenInput:— notinput:.
Supported Platforms
| Value | Platform |
|---|---|
ios | Apple iOS |
android | Android |
web | Web browser (PWA) |
Register the FCM token after login and whenever it refreshes.
Full Game Flow Summary
┌──────────────────────────────────────────────────────────────────────┐
│ AUTHENTICATION │
│ requestEmailVerificationCode ──► loginWithEmail ──► Token │
│ OR loginWithPassword ──► Token │
│ Store accessToken (memory) + refreshToken (secure storage) │
└──────────────────────┬───────────────────────────────────────────────┘
│
┌──────────────┼──────────────┐
▼ ▼ ▼
┌───────────┐ ┌──────────────┐ ┌──────────────┐
│ profile │ │ createWallet │ │ createFCMToken│
│ (setup) │ │ (first time) │ │ (push notif) │
└───────────┘ └──────────────┘ └───────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ BROWSE & DISCOVER │
│ gamesPage ──► See available challenges (rules, fees, prizes) │
│ gameMatchesPage ──► See match rooms (status: waiting = joinable) │
│ assets ──► Browse tradable assets + assetUpdated subscription │
│ leaderboard ──► See top players │
│ storeItems / storePackages ──► Browse the store │
└──────────────────────┬──────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ JOIN MATCH │
│ joinMatch(matchID) ──► GameMatchParticipant │
│ Entry fee auto-deducted from wallet │
│ Subscribe: NotificationMatch(matchId) for live updates │
└──────────────────────┬──────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ LIVE TRADING │
│ │
│ createGameMatchPosition ──► Open position (market or limit) │
│ updateGameMatchPosition ──► Adjust SL/TP │
│ closeGameMatchPosition ──► Full or partial close │
│ cancelGameMatchPosition ──► Cancel pending limit order │
│ participantEquity ──► Check vBalance, PnL, margin │
│ │
│ Subscriptions: │
│ NotificationMatch ──► PnL updates, position events │
│ assetUpdated ──► Live price feed for charts │
└──────────────────────┬──────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ POST-MATCH │
│ matchSummary(matchID) ──► Stats, winner, trade counts │
│ gameMatchesPage (filter by ID) ──► Participants, final balances │
│ NotificationGlobal ──► Achievement unlocked, wallet updated │
└──────────────────────────────────────────────────────────────────────┘Quick Reference: Argument Names
A common source of bugs is using input: instead of the full argument name. Here is the correct mapping:
| Mutation | Argument Name |
|---|---|
createGameMatchPosition | createGameMatchPositionInput: |
updateGameMatchPosition | updateGameMatchPositionInput: |
updateProfile | updateProfileInput: |
createWallet | createWalletInput: |
createWithdrawRequest | createWithdrawalInput: |
createFCMToken | createFCMTokenInput: |
Never use input: as a shorthand — the server will reject the request.