The Stellarcade backend provides a RESTful API for interacting with games, users, wallets, and service health endpoints.
Most endpoints require a JWT token in the Authorization header.
Authorization: Bearer <your_token>
The API supports idempotent requests to prevent duplicate operations when retrying failed requests. This is required for wallet and financial operations, and optional for game plays.
- Client generates a unique idempotency key (UUID or similar) for each operation
- Include the key in the
Idempotency-Keyheader with your request - Server caches successful responses for 24 hours
- Duplicate requests with the same key return the cached response instead of re-executing
| Endpoint Type | Idempotency | Reason |
|---|---|---|
POST /api/wallet/deposit |
Required | Prevents duplicate deposits |
POST /api/wallet/withdraw |
Required | Prevents duplicate withdrawals |
POST /api/games/play |
Optional | Prevents accidental double-plays |
GET endpoints |
Not applicable | Read operations are inherently idempotent |
Idempotency-Key: <unique-key-per-operation>Key Requirements:
- Must be unique per distinct operation
- Recommended format: UUID v4 or similar (e.g.,
550e8400-e29b-41d4-a716-446655440000) - Maximum length: 256 characters
- Allowed characters: alphanumeric, hyphens, underscores
- Reuse with same body: Returns cached response (idempotent replay)
- Reuse with different body: Returns
409 Conflict(key collision detected)
// Example: Making an idempotent withdrawal request
const makeWithdrawal = async (amount, address) => {
// Generate a unique key for this specific operation
const idempotencyKey = crypto.randomUUID();
try {
const response = await fetch("/api/wallet/withdraw", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
"Idempotency-Key": idempotencyKey,
},
body: JSON.stringify({ amount, address }),
});
if (response.status === 409) {
// Key was reused with different request body
console.error(
"Idempotency conflict - key already used with different data",
);
return;
}
return await response.json();
} catch (error) {
// Safe to retry with the SAME idempotency key
// Server will return cached response if original succeeded
console.log("Retrying with same idempotency key...");
}
};When a request fails (network error, timeout, 5xx), retry with the same idempotency key:
const retryWithIdempotency = async (endpoint, body, maxRetries = 3) => {
const idempotencyKey = crypto.randomUUID();
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(endpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
"Idempotency-Key": idempotencyKey, // Same key for all retries
},
body: JSON.stringify(body),
});
// Success - return the result (may be from cache)
if (response.ok) return await response.json();
// Client error - don't retry
throw new Error(`Request failed: ${response.status}`);
} catch (error) {
if (attempt === maxRetries) throw error;
// Wait before retry (exponential backoff recommended)
await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));
}
}
};| Scenario | Server Response |
|---|---|
| First request with key | Executes operation, caches response (2xx), returns result |
| Retry with same key + same body | Returns cached response with 200 OK |
| Retry with same key + different body | Returns 409 Conflict with error message |
| Request without key (mutation) | Executes normally (no idempotency guarantee) |
{
"error": "Idempotency Conflict",
"message": "The provided Idempotency-Key was already used with a different request payload."
}- Cache duration: 24 hours from first successful response
- Cache key:
idempotency:{userId}:{clientKey}(scoped to user) - Cached data: Request body hash, status code, response body
- Only 2xx responses are cached
The frontend provides a built-in service for managing idempotency in transaction flows. See Idempotency Transaction Handling for implementation details.
- Generate keys client-side using a reliable UUID generator
- Store keys temporarily during request lifecycle for retry scenarios
- Use one key per logical operation - don't reuse across different operations
- Handle 409 errors gracefully - they indicate a programming error (key reuse)
- Don't use idempotency for read operations - GET requests skip idempotency checks
- GET
/api/games- Retrieve a list of available games. - GET
/api/games/:id- Get details of a specific game. - POST
/api/games/play- Initiate a game play request.- Body:
{ "gameType": "coin-flip", "betAmount": "10", "choice": "heads" }
- Body:
- GET
/api/games/recent- Retrieve recent games with metadata for pagination.- Query:
page(positive integer, default1),limit(positive integer, default10),cursor(optional next-page cursor),gameType,status,sortBy,sortDir. - Response keeps
itemsand adds:pagination.nextCursor: string cursor for the next page, ornullif none.pagination.hasNextPage: boolean flag.
- Query:
Example response:
{
"items": [],
"page": 1,
"pageSize": 10,
"total": 0,
"totalPages": 0,
"pagination": {
"nextCursor": null,
"hasNextPage": false
}
}- GET
/api/users/profile- Get the current user's profile. - POST
/api/users/create- Create a new user account linked to a Stellar address. - GET
/api/users/balance- Get the user's on-platform balance. - GET
/api/users/audit-logs- Retrieve audit logs with optional filtering.- Query:
actor(optional exact actor id),action(optional exact action),limit(optional positive integer, default50). actionsupports:wallet.deposit,wallet.withdraw,game.play.- Unmatched filters return an empty
itemsarray.
- Query:
- POST
/api/wallet/deposit- Get instructions for depositing Stellar assets. - POST
/api/wallet/withdraw- Withdraw assets to a Stellar address. - GET
/api/wallet/transactions- List all deposit and withdrawal transactions.
Wallet endpoints normalize network mismatch failures with a stable error shape. Clients must pass x-wallet-network matching backend STELLAR_NETWORK.
Example network mismatch response:
{
"error": {
"message": "Wallet network mismatch: expected testnet, received public. Please switch your wallet network and try again.",
"code": "NETWORK_MISMATCH",
"status": 400,
"expectedNetwork": "testnet",
"receivedNetwork": "public",
"correlationId": "<request-correlation-id>"
}
}- GET
/api/health- Check the status of the API service. - GET
/api/health/deep- Run deep dependency diagnostics for PostgreSQL, Redis, and Stellar Horizon.
The deep health response includes a top-level status, an ISO-8601 timestamp, and a dependencies object keyed by db, redis, and stellar.
Each dependency entry always includes:
status-healthyorunhealthylatency_ms- observed latency for the dependency probetimeout_ms- timeout budget applied to that probetimed_out-truewhen the probe exceeded its timeout budget
Unhealthy dependency entries also include:
failure_type- stable failure category for dashboards and alertserror- compact diagnostic message from the failed probe
Example deep health response:
{
"status": "degraded",
"timestamp": "2026-03-25T12:00:00.000Z",
"dependencies": {
"db": {
"status": "healthy",
"latency_ms": 4,
"timeout_ms": 5000,
"timed_out": false
},
"redis": {
"status": "unhealthy",
"latency_ms": 5001,
"timeout_ms": 5000,
"timed_out": true,
"failure_type": "timeout",
"error": "redis check timed out after 5000ms"
},
"stellar": {
"status": "healthy",
"latency_ms": 12,
"timeout_ms": 5000,
"timed_out": false
}
}
}400 Bad Request: Invalid input parameters.401 Unauthorized: Missing or invalid authentication token.403 Forbidden: Insufficient permissions or balance.429 Too Many Requests: Rate limit exceeded.500 Internal Server Error: Unexpected server error.
- Default limit: 60 requests per minute per IP.
- Authenticated limit: 200 requests per minute per user.