Stacks Wars
Dev

API Reference

Complete HTTP and WebSocket API reference for the Stacks Wars backend.

The Stacks Wars backend exposes an HTTP REST API and two WebSocket endpoints. The base URL defaults to http://localhost:3001 in development.


Authentication

Most read endpoints are public. Write endpoints require a JWT token set as an auth_token HTTP-only cookie. The token is issued on user creation and lasts 14 days by default.

Route GroupAuthRate LimitDescription
GET /, GET /healthNoneNonePublic health check
GET /api/*NoneStandardPublic read-only API
POST/PATCH /api/*JWT requiredAuthAuthenticated writes
POST /api/userNoneStrictUser creation (heavily rate-limited)
POST/PUT /api/* (admin)JWT + Admin walletAuthAdmin-only operations

HTTP Endpoints

Health & Info

GET /

Returns API metadata.

{ "name": "Stacks Wars API", "version": "0.1.0", "status": "running" }

GET /health

Returns "OK" (200). Use for uptime monitoring.


Users

POST /api/user

Create a new user account. Returns the user and sets an auth_token cookie.

Body:

{ "walletAddress": "SP...", "emailAddress": "user@example.com" }

Response: User object + Set-Cookie: auth_token=...


GET /api/me

Get the authenticated user's profile. Requires JWT.

Response: User


GET /api/user/{identifier}

Get any user's public profile.

Path: identifier — UUID, wallet address, or username.

Response: User


PATCH /api/user/username

Update the authenticated user's username (must be unique). Requires JWT.

Body:

{ "username": "newname" }

PATCH /api/user/display-name

Update the authenticated user's display name. Requires JWT.

Body:

{ "displayName": "New Name" }

PATCH /api/user/profile

Partial profile update. Requires JWT.

Body:

{ "username": "optional", "displayName": "optional" }

Response: Full updated User object.


GET /api/unclaimed-reward

Get unclaimed prize rewards for the authenticated user. Requires JWT.

Response:

[{ "lobbyInfo": { ... }, "prize": 1.5 }]

GET /api/player-lobby/{user_id}

Get all lobbies a user is participating in.

Path: user_id — UUID

Query:

ParamTypeDefaultDescription
statusstringallComma-separated statuses
limitinteger6Page size
offsetinteger0Pagination offset

Response: [LobbyInfo[], total]


POST /api/logout

Revoke the JWT token and clear the auth cookie. Requires JWT.

Response: 204 No Content


Games

GET /api/games

List all game types.

Query:

ParamTypeDefaultDescription
pageinteger1Page number
limitinteger20Page size
orderstring"asc" or "desc"

Response: Game[]


GET /api/game/{identifier}

Get a game by UUID or path slug (e.g., "lexi-wars").

Response: Game


GET /api/game/by-creator/{creator_id}

Get all games created by a user.

Path: creator_id — UUID

Response: Game[]


POST /api/game

Create a new game type. Requires JWT (admin only).

Body:

{
	"name": "My Game",
	"path": "my-game",
	"description": "A fun game",
	"imageUrl": "/games/my-game.png",
	"minPlayers": 2,
	"maxPlayers": 6,
	"category": ["Strategy", "Competitive"]
}

Response: Game (includes the generated UUID)


Lobbies

GET /api/lobbies

List all lobbies with pagination.

Query:

ParamTypeDefaultDescription
limitinteger20Page size (max 100)
offsetinteger0Pagination offset

Response:

{ "data": [LobbyInfo], "total": 42, "limit": 20, "offset": 0 }

GET /api/lobby/{identifier}

Get a lobby by UUID or path.

Response: Lobby


GET /api/game/{game_identifier}/lobbies

List lobbies for a game, filtered by status.

Path: game_identifier — game UUID or path

Query:

ParamTypeDefaultDescription
statusesstringallComma-separated: "waiting,starting,in_progress,finished"
limitinteger20Page size (max 100)
offsetinteger0Pagination offset

Response: PaginatedResponse<LobbyInfo>


GET /api/lobby/my

List lobbies created by the authenticated user. Requires JWT.

Query: limit, offset

Response: PaginatedResponse<Lobby>


POST /api/lobby

Create a new lobby. Requires JWT.

Body:

{
	"name": "My Lobby",
	"description": "Optional description",
	"entryAmount": 1.5,
	"currentAmount": 1.5,
	"tokenSymbol": "STX",
	"tokenContractId": "stx",
	"contractAddress": "SP...",
	"isPrivate": false,
	"isSponsored": false,
	"gameId": "5eb61ff4-...",
	"gamePath": "lexi-wars"
}

For sponsored lobbies, set isSponsored: true, entryAmount: null, and currentAmount to the sponsor's pool amount.

Response: 201 Created + Lobby


Seasons

GET /api/season/current

Get the currently active season.

Response: Season (or 404 if no active season)


GET /api/season

List all seasons.

Query: limit (default 10, max 100), offset (default 0)

Response: Season[]


POST /api/season

Create a new season. Requires JWT + admin wallet.

Body:

{
	"name": "Season 1",
	"description": "The first season",
	"startDate": "2026-01-01 00:00:00",
	"endDate": "2026-03-31 23:59:59"
}

PUT /api/season/{season_id}

Update a season. Requires JWT + admin wallet.

Path: season_id — integer

Body: Partial — name, description, startDate, endDate (all optional)


Leaderboard

GET /api/leaderboard

Get paginated leaderboard rankings.

Query:

ParamTypeDefaultDescription
seasonIdintegercurrentSeason to query
limitinteger10Page size (max 100)
offsetinteger0Pagination offset
sortBystring"points""points", "matches", "win_rate", or "pnl"
orderstring"desc""asc" or "desc"

Response:

{ "total": 150, "leaderboard": [LeaderBoard] }

GET /api/leaderboard/{user_id}

Get a single player's leaderboard entry.

Path: user_id — UUID

Query: seasonId (optional)

Response: LeaderBoard


Platform Ratings

GET /api/platform-rating

List all platform ratings, optionally filtered by star value.

Query: rating — integer 1–5 (optional)

Response: PlatformRating[]


GET /api/platform-rating/{user_id}

Get a user's rating.

Response: PlatformRating


POST /api/platform-rating

Create or replace the authenticated user's rating. Requires JWT.

Body:

{ "rating": 5, "comment": "Great platform!" }

PATCH /api/platform-rating

Update the authenticated user's rating. Requires JWT.


DELETE /api/platform-rating

Delete the authenticated user's rating. Requires JWT.


Stacks Blockchain

GET /api/balance/{wallet_address}

Get STX and fungible token balances for a wallet.

Response:

[{ "name": "STX", "balance": 150.5, "contractId": "stx" }]

Amounts are in human-readable units (not micro-units).


GET /api/token/{contract_address}

Get token USD price and minimum wager amount (~$10 equivalent).

Response:

{ "price": 1.25, "minimumAmount": 8.0 }

Contracts

GET /api/contract

Generate a vault contract Clarity source with baked-in creator and entry fee.

Query:

ParamTypeDescription
gameCreatorIdUUIDCreator's user ID
entryFeeintegerEntry fee in micro-units
contractIdstring"stx" for STX vault, or a token contract address for FT vault

Response: Clarity contract source code (string)


GET /api/sponsored-contract

Generate a sponsored vault contract.

Query:

ParamTypeDescription
gameCreatorIdUUIDCreator's user ID
poolSizeintegerPool size in micro-units
contractIdstring"stx" or token contract address

Response: Clarity contract source code (string)


WebSocket Endpoints

GET /ws/lobbies — Lobby List

Subscribe to the global lobby list with real-time updates. No authentication required.

Query parameters:

ParamTypeDefaultDescription
statusstringallComma-separated statuses to filter
limitinteger6Page size

Client → Server

Message TypeFieldsDescription
subscribestatus?: string[], limit?: integerChange status filter
loadMoreoffset: integer, limit: integerPaginate for more lobbies

Server → Client

Message TypeFieldsDescription
lobbyListlobbyInfo: LobbyInfo[], total: integerFull lobby page
lobbyCreatedlobbyInfo: LobbyInfoNew lobby created (live)
lobbyUpdatedlobby: LobbyInfoLobby changed (live)
lobbyRemovedlobbyId: stringLobby removed (live)
errorcode: string, message: stringError

GET /ws/room/{lobby_path} — Game Room

Join a specific lobby room for game and chat communication. Supports unauthenticated spectators and authenticated participants.

Path: lobby_path — the URL-friendly lobby path

Authentication: Optional JWT via cookie/header. Unauthenticated users can spectate.

On Connect

The server sends a lobbyBootstrap message containing the lobby info, players, join requests, and chat history. If a game is in progress and the user is a participant, a gameState message is also sent for reconnection.

Client → Server

Message TypeFieldsAuthDescription
joinYesJoin the lobby
leaveYesLeave the lobby
updateLobbyStatusstatus: stringYes (creator)Change lobby status
joinRequestYesRequest to join a private lobby
approveJoinuserId: stringYes (creator)Accept a join request
rejectJoinuserId: stringYes (creator)Reject a join request
kickuserId: stringYes (creator)Kick a participant
sendMessagecontent: string, replyTo?: stringYesSend a chat message
addReactionmessageId: string, emoji: stringYesAdd emoji reaction
removeReactionmessageId: string, emoji: stringYesRemove emoji reaction
claimRewardtxId: stringYesClaim prize after a game ends
toggleParticipationparticipate: booleanYes (creator)Toggle sponsor participation
pingts: integerNoHeartbeat (unix ms timestamp)

Game actions are sent wrapped in a game envelope:

{ "game": { "type": "submitWord", "word": "hello" } }

The special action { "game": { "type": "quit" } } triggers player quit handling.

Server → Client

Message TypeFieldsScopeDescription
lobbyBootstraplobbyInfo, players, joinRequests, chatHistoryPersonalInitial state on connect
lobbyStatusChangedstatus, participantCount, currentAmount?RoomLobby status update
startCountdownsecondsRemaining?RoomPre-game countdown
playerJoinedplayer: PlayerStateRoomPlayer joined
playerLeftplayer: PlayerStateRoomPlayer left
playerKickedplayer: PlayerStateRoomPlayer kicked
joinRequestsUpdatedjoinRequests: JoinRequest[]RoomUpdated join requests
joinRequestStatususerId, acceptedPersonalYour join request result
messageReceivedmessage: ChatMessageRoomChat message
reactionAddedmessageId, userId, emojiRoomReaction added
reactionRemovedmessageId, userId, emojiRoomReaction removed
pongelapsedMs: integerPersonalHeartbeat response
playerUpdatedplayers: PlayerState[]RoomUpdated player list
gameStategameState: objectPersonalGame state (reconnection)
gameStartedRoomGame has started
gameStartFailedreason: stringRoomGame failed to start
finalStandingstandings: PlayerState[]RoomFinal rankings
gameOverrank, prize?, warsPointPersonalYour game result
claimSuccessPersonalPrize claimed
participationToggleduserId, participatingRoomSponsor toggled
errorcode, messagePersonalError

Game-specific events are wrapped in a game envelope:

{ "game": { "type": "turn", "player": { ... }, "timeoutSecs": 15 } }

Key Types

User

{
	"id": "uuid",
	"walletAddress": "SP...",
	"username": "alice",
	"displayName": "Alice",
	"email": "alice@example.com",
	"emailVerified": false,
	"trustRating": 10.0,
	"createdAt": "2026-01-01T00:00:00",
	"updatedAt": "2026-01-01T00:00:00"
}

Game

{
	"id": "uuid",
	"name": "Lexi Wars",
	"path": "lexi-wars",
	"description": "...",
	"imageUrl": "/games/lexi-wars.png",
	"minPlayers": 2,
	"maxPlayers": 20,
	"category": ["Word Games", "Competitive"],
	"creatorId": "uuid",
	"isActive": true
}

LobbyStatus

"waiting" | "starting" | "in_progress" | "finished"

PlayerState

{
	"userId": "uuid",
	"walletAddress": "SP...",
	"username": "alice",
	"displayName": "Alice",
	"trustRating": 10.0,
	"lobbyId": "uuid",
	"status": "Joined",
	"state": "Accepted",
	"rank": 1,
	"prize": 2.5,
	"warsPoint": 10.0,
	"claimState": null,
	"isCreator": false
}

PaginatedResponse<T>

{
	"data": [],
	"total": 42,
	"limit": 20,
	"offset": 0
}

On this page