Skip to main content

Ludicé API Documentation

Complete API reference for the Ludicé backend services and bot integration.

Table of Contents

  1. Overview
  2. Authentication
  3. Backend Game API
  4. Balance Service API
  5. Frontend Bot Integration
  6. Error Handling
  7. Rate Limiting
  8. Examples

Overview

Ludicé consists of three API layers:

ServicePortPurposeProtocol
Game API8080Game logic, user managementHTTP + HMAC
Balance Service8000User balance managementHTTP (REST)
BotN/ATelegram interfaceaiogram 3.x

Authentication

HMAC-SHA256 Signature

All requests to the Game API (port 8080) require HMAC-SHA256 authentication.

Request Signing Process

Step 1: Prepare Data

data = {
"username": "player123",
"bet": 50
}

Step 2: Create JSON String

import json

# CRITICAL: Sort keys and remove spaces
data_str = json.dumps(data, sort_keys=True, separators=(',', ':'))
# Result: '{"bet":50,"username":"player123"}'

Step 3: Generate Signature

import hmac
import hashlib

SECRET_KEY = "your-secret-key"

signature = hmac.new(
SECRET_KEY.encode(),
data_str.encode(),
hashlib.sha256
).hexdigest()

Step 4: Send Request

import requests

headers = {
"Content-Type": "application/json",
"X-Signature": signature
}

response = requests.post(
"http://localhost:8080/endpoint",
headers=headers,
json=data
)

Helper Function (Python)

async def send_to_backend(url: str, data: dict) -> dict:
"""
Send authenticated request to backend.

Args:
url: Full endpoint URL
data: Request payload

Returns:
Response JSON or None on error
"""
try:
# Create signature
data_str = json.dumps(data, sort_keys=True, separators=(',', ':'))
signature = hmac.new(
SECRET_KEY.encode(),
data_str.encode(),
hashlib.sha256
).hexdigest()

# Send request
headers = {
"Content-Type": "application/json",
"X-Signature": signature
}

response = requests.post(url, headers=headers, json=data)

if response.status_code == 200:
return response.json()
else:
print(f"Error: {response.status_code} - {response.text}")
return None

except Exception as e:
print(f"Request failed: {e}")
return None

Backend Game API

Base URL: http://localhost:8080

Endpoints

1. User Registration

Endpoint: POST /register

Description: Register a new user or update existing user info.

Request:

{
"username": "player123",
"id": 123456789
}

Response (Success):

{
"status": "success",
"message": "User registered successfully"
}

Response (Error):

{
"status": "error",
"message": "User already exists"
}

Example:

data = {
"username": "player123",
"id": 123456789
}
response = await send_to_backend("http://localhost:8080/register", data)

2. Start/Join Game

Endpoint: POST /start/game

Description: Start a new game or join existing lobby with matching bet.

Request:

{
"username": "player123",
"bet": 50
}

Response (Lobby Created):

{
"status": "waiting",
"message": "Searching for opponent...",
"lobby_id": "abc123",
"bet": 50
}

Response (Game Started):

{
"status": "game_started",
"message": "Opponent found!",
"game_id": "def456",
"opponent": "player456",
"bet": 50
}

Response (Error - Insufficient Balance):

{
"status": "error",
"message": "Insufficient balance"
}

Example:

data = {
"username": "player123",
"bet": 50
}
response = await send_to_backend("http://localhost:8080/start/game", data)

3. Submit Game Result

Endpoint: POST /write/res

Description: Submit dice roll result for active game.

Request:

{
"username": "player123",
"game_id": "def456",
"result": 6
}

Response (Waiting for Opponent):

{
"status": "waiting",
"message": "Waiting for opponent to roll..."
}

Response (Game Complete - Win):

{
"status": "win",
"message": "You won!",
"your_roll": 6,
"opponent_roll": 3,
"winnings": 100
}

Response (Game Complete - Loss):

{
"status": "loss",
"message": "You lost.",
"your_roll": 3,
"opponent_roll": 6,
"loss": 50
}

Response (Game Complete - Tie):

{
"status": "tie",
"message": "It's a tie! Bet refunded.",
"your_roll": 4,
"opponent_roll": 4
}

Example:

data = {
"username": "player123",
"game_id": "def456",
"result": 6
}
response = await send_to_backend("http://localhost:8080/write/res", data)

4. Leave Lobby

Endpoint: POST /leave

Description: Leave a game lobby before match starts.

Request:

{
"username": "player123"
}

Response:

{
"status": "success",
"message": "Left lobby successfully"
}

Example:

data = {
"username": "player123"
}
response = await send_to_backend("http://localhost:8080/leave", data)

Balance Service API

Base URL: http://localhost:8000

No authentication required (internal service only, should not be exposed publicly).

Endpoints

1. Create User

Endpoint: POST /create_user

Description: Create a new user with initial balance.

Request:

{
"username": "player123",
"initial_balance": 1000
}

Response (Success):

{
"status": "success",
"username": "player123",
"balance": 1000
}

Response (Error):

{
"error": "User already exists"
}

Example:

curl -X POST http://localhost:8000/create_user \
-H "Content-Type: application/json" \
-d '{"username": "player123", "initial_balance": 1000}'

2. Get Balance

Endpoint: GET /get_balance

Description: Query user's current balance.

Query Parameters:

  • username (string, required): User's username

Response (Success):

{
"username": "player123",
"balance": 750
}

Response (Error):

{
"error": "User not found"
}

Example:

curl http://localhost:8000/get_balance?username=player123

3. Modify Balance

Endpoint: POST /modify_balance

Description: Add or subtract from user balance.

Request:

{
"username": "player123",
"amount": 100,
"operation": "add"
}

Operations:

  • "add" - Add to balance
  • "subtract" - Subtract from balance

Response (Success):

{
"status": "success",
"username": "player123",
"new_balance": 850
}

Response (Error - Insufficient Funds):

{
"error": "Insufficient balance"
}

Example (Add):

curl -X POST http://localhost:8000/modify_balance \
-H "Content-Type: application/json" \
-d '{"username": "player123", "amount": 100, "operation": "add"}'

Example (Subtract):

curl -X POST http://localhost:8000/modify_balance \
-H "Content-Type: application/json" \
-d '{"username": "player123", "amount": 50, "operation": "subtract"}'

Frontend Bot Integration

Telegram Bot Handlers

1. Start Command

Trigger: /start

Flow:

User sends /start

Show Terms of Service

Set FSM State: LegalStates.waiting_for_acceptance

User clicks "Accept"

Call: POST /register

Show main menu

Code:

@start_router.message(CommandStart())
async def cmd_start(message: types.Message, state: FSMContext):
user_id = message.from_user.id

await state.set_state(LegalStates.waiting_for_acceptance)
await message.answer(
TERMS_FULL,
parse_mode="Markdown",
reply_markup=get_legal_nav_keyboard()
)

@start_router.callback_query(F.data == "accept_terms")
async def accept_terms_handler(callback: types.CallbackQuery, state: FSMContext):
user_id = callback.from_user.id
username = callback.from_user.username or f"user_{user_id}"

# Register user
data = {
"username": username,
"id": user_id
}
response = await send_to_backend(
"http://localhost:8080/register",
data
)

await state.clear()
await callback.message.answer(
"Welcome to Ludicé!",
reply_markup=start.start_kb
)

2. Game Start

Trigger: User clicks "Roll 🎲" → "Dice 🎲"

Flow:

User clicks "Dice 🎲"

Show gambling reminder

Ask for bet amount

Set FSM State: BetStates.waiting_for_bet

User enters amount

Validate (>= 10 stars)

Call: POST /start/game

Wait for opponent or start game

Code:

@game_router.message(F.text == "Dice 🎲")
async def play_dice(message: types.Message, state: FSMContext):
await show_gambling_reminder(message)
await message.answer("What amount are you willing to bet?")
await state.set_state(BetStates.waiting_for_bet)

@game_router.message(BetStates.waiting_for_bet)
async def process_bet(message: types.Message, state: FSMContext):
bet_amount = message.text

if not bet_amount.isdigit():
await message.answer("❌ Please enter a valid number.")
return

bet = int(bet_amount)

if bet < 10:
await message.answer("❌ Minimum bet is 10 stars.")
return

user_id = message.from_user.id
username = message.from_user.username or f"user_{user_id}"

# Start game
data = {
"username": username,
"bet": bet
}
response = await send_to_backend(
"http://localhost:8080/start/game",
data
)

if response:
await message.answer(
f"✅ Bet placed: {bet} ⭐\nSearching for opponent..."
)
else:
await message.answer("❌ Failed to place bet.")
await state.clear()

3. Payment Flow

Trigger: User clicks "Top up 🔝"

Flow:

User clicks "Top up 🔝"

Show payment keyboard

User selects amount (e.g., "100 ⭐")

Send invoice (Telegram Stars)

User pays

pre_checkout_query → answer(ok=True)

successful_payment → Update balance

Code:

@start_router.callback_query(F.data == "star100")
async def send_invoice(callback: types.CallbackQuery):
prices = [LabeledPrice(label="100 ⭐", amount=133)]

pay_kb = InlineKeyboardMarkup(
inline_keyboard=[[InlineKeyboardButton(text="Pay 133 ⭐", pay=True)]]
)

await callback.message.answer_invoice(
title="❖ Telegram Stars",
description="Your account will be credited with 100 stars.",
payload="topup_133",
provider_token="",
prices=prices,
currency="XTR",
reply_markup=pay_kb
)

@start_router.pre_checkout_query()
async def pre_checkout(pre_q: PreCheckoutQuery):
await pre_q.answer(ok=True)

@start_router.message(F.successful_payment)
async def payment_success(msg: types.Message):
sp = msg.successful_payment
amount = int(sp.invoice_payload.replace("topup_", ""))

# TODO: Update backend balance
# await send_to_backend("http://localhost:8000/modify_balance", {...})

await msg.answer("✅ Payment successful!")

Error Handling

HTTP Status Codes

CodeMeaningAction
200SuccessProcess response
400Bad RequestInvalid data format
401UnauthorizedInvalid signature
403ForbiddenRate limit exceeded
404Not FoundResource doesn't exist
500Server ErrorRetry later

Common Errors

1. Invalid Signature

Error:

{
"detail": "Invalid signature"
}

Cause: HMAC signature doesn't match

Solutions:

  • Check SECRET_KEY matches between frontend and backend
  • Verify JSON serialization (sorted keys, no spaces)
  • Check timestamp is within 5 minutes

2. Rate Limit Exceeded

Error:

{
"detail": "Rate limit exceeded"
}

Cause: Too many requests

Solutions:

  • Wait 1 second between user requests
  • Implement client-side throttling
  • Show user "Please wait..." message

3. Insufficient Balance

Error:

{
"status": "error",
"message": "Insufficient balance"
}

Cause: User doesn't have enough stars to bet

Solutions:

  • Check balance before allowing bet
  • Show "Top up" prompt
  • Set maximum bet based on balance

Rate Limiting

Game API (Port 8080)

IP-based: 10 requests/minute (slowapi)

User-based: 1 request/second minimum

Implementation:

# Backend
@limiter.limit("10/minute")
async def endpoint():
if not check_time_seciruty(username):
raise HTTPException(status_code=429, detail="Rate limit")
...

Balance Service (Port 8000)

No rate limiting (internal service only)

Should be behind firewall and not publicly accessible.


Examples

Complete Game Flow Example

1. Register User

# User accepts terms
data = {
"username": "alice",
"id": 123456
}
response = await send_to_backend(
"http://localhost:8080/register",
data
)
# Response: {"status": "success", ...}

2. Start Game

# Alice bets 50 stars
data = {
"username": "alice",
"bet": 50
}
response = await send_to_backend(
"http://localhost:8080/start/game",
data
)
# Response: {"status": "waiting", "lobby_id": "xyz123", ...}

3. Bob Joins

# Bob also bets 50 stars
data = {
"username": "bob",
"bet": 50
}
response = await send_to_backend(
"http://localhost:8080/start/game",
data
)
# Response: {"status": "game_started", "game_id": "abc456", "opponent": "alice", ...}

4. Alice Rolls

# Alice rolls a 5
data = {
"username": "alice",
"game_id": "abc456",
"result": 5
}
response = await send_to_backend(
"http://localhost:8080/write/res",
data
)
# Response: {"status": "waiting", ...}

5. Bob Rolls

# Bob rolls a 3
data = {
"username": "bob",
"game_id": "abc456",
"result": 3
}
response = await send_to_backend(
"http://localhost:8080/write/res",
data
)
# Response: {"status": "loss", "your_roll": 3, "opponent_roll": 5, ...}

Result: Alice wins 100 stars total (her 50 + Bob's 50)


Payment Integration Example

1. User Pays 133 Stars

# Telegram processes payment
# successful_payment event triggers

sp = msg.successful_payment
amount_paid = sp.total_amount # 133 stars
amount_credited = 100 # After ~25% fee

2. Credit User Balance

# Update in Balance Service
data = {
"username": "alice",
"amount": amount_credited,
"operation": "add"
}

response = requests.post(
"http://localhost:8000/modify_balance",
json=data
)
# Response: {"status": "success", "new_balance": 1100}

3. Record Transaction

# Save to payments.json
transaction = {
"id": "txn_123",
"user": "alice",
"amount_paid": 133,
"amount_credited": 100,
"timestamp": datetime.now().isoformat(),
"payment_provider": "telegram_stars"
}
# Append to payments.json

Testing

Test Signature Generation

import hmac
import hashlib
import json

SECRET_KEY = "test-secret-key"

data = {"username": "test", "bet": 10}
data_str = json.dumps(data, sort_keys=True, separators=(',', ':'))
signature = hmac.new(
SECRET_KEY.encode(),
data_str.encode(),
hashlib.sha256
).hexdigest()

print(f"Data: {data_str}")
print(f"Signature: {signature}")

Test API Endpoints

# Register user (with correct signature)
python scripts/test_api.py register alice 123456

# Start game
python scripts/test_api.py start_game alice 50

# Check balance
curl http://localhost:8000/get_balance?username=alice

Last Updated: January 2025