TypeScript/Node.js backend for the Xelma decentralized XLM price prediction market, built on the Stellar blockchain (Soroban).
- Overview
- Key Features
- Project Structure
- Architecture
- Prerequisites
- Installation
- Environment Setup
- Running the Server
- API Documentation
- Testing
- Migration Safety
- Scripts
- Troubleshooting
- Related Repositories
Xelma Backend is the server-side component of a blockchain-based prediction market platform where users predict XLM (Stellar Lumens) price movements. The backend orchestrates:
- Real-time price data from CoinGecko
- Blockchain integration with Soroban smart contracts on Stellar
- WebSocket updates for live round status and price changes
- JWT-based authentication with wallet signature verification
- PostgreSQL database for user profiles, rounds, predictions, and stats
- Role-based access control (User, Admin, Oracle) for secure operations
- Automated scheduling for round creation, locking, and resolution
The platform supports two game modes:
- UP_DOWN - Binary predictions (price goes up or down)
- LEGENDS - Range-based predictions (price lands in specific ranges)
- ✅ Wallet-Based Authentication: Users authenticate with Stellar wallet signatures (no passwords)
- ✅ Two Game Modes: UP_DOWN (binary) and LEGENDS (range-based) prediction markets
- ✅ Real-Time Price Oracle: Polls CoinGecko every 10 seconds for XLM/USD prices
- ✅ Soroban Integration: Creates and resolves rounds on-chain via
@tevalabs/xelma-bindings - ✅ WebSocket Support: Live updates for prices, rounds, chat, and notifications
- ✅ Leaderboard System: Tracks wins, earnings, and streaks across game modes
- ✅ Automated Schedulers: Cron jobs for round creation, locking, and resolution
- ✅ Transactional Outbox: Notification and WebSocket side-effects are written atomically with DB commits — guaranteed at-least-once delivery even across process crashes
- ✅ Dead-Letter Queue: Failed dispatches are persisted and replayable via admin endpoints
- ✅ OpenAPI Documentation: Auto-generated Swagger UI at
/api-docs - ✅ Rate Limiting: Protects endpoints from abuse
- ✅ Comprehensive Logging: Winston-based logging for debugging and monitoring
Xelma-Backend/
├── src/
│ ├── index.ts # Application entry point
│ ├── socket.ts # Socket.IO initialization with JWT auth
│ │
│ ├── routes/ # Express route handlers
│ │ ├── auth.routes.ts # Authentication (login, verify)
│ │ ├── user.routes.ts # User profile management
│ │ ├── rounds.routes.ts # Round creation & resolution (admin/oracle)
│ │ ├── predictions.routes.ts # Submit & claim predictions
│ │ ├── leaderboard.routes.ts # Leaderboard & user stats
│ │ ├── education.routes.ts # Educational tips
│ │ ├── chat.routes.ts # Chat message submission
│ │ └── notifications.routes.ts # User notifications
│ │
│ ├── services/ # Business logic layer
│ │ ├── oracle.ts # Price fetching from CoinGecko
│ │ ├── soroban.service.ts # Soroban contract interaction
│ │ ├── round.service.ts # Round lifecycle management
│ │ ├── prediction.service.ts # Prediction submission & validation
│ │ ├── resolution.service.ts # Round resolution & payout calculation
│ │ ├── leaderboard.service.ts # Leaderboard data aggregation
│ │ ├── websocket.service.ts # WebSocket event emissions
│ │ ├── notification.service.ts # Notification creation & delivery
│ │ ├── education-tip.service.ts# Educational content management
│ │ ├── chat.service.ts # Chat message handling
│ │ ├── scheduler.service.ts # General cron job scheduler
│ │ └── round-scheduler.service.ts # Round creation/locking scheduler
│ │
│ ├── middleware/ # Express middleware
│ │ ├── auth.middleware.ts # JWT verification & role checking
│ │ └── rateLimiter.middleware.ts # Rate limiting configuration
│ │
│ ├── utils/ # Utility functions
│ │ ├── logger.ts # Winston logger setup
│ │ ├── jwt.util.ts # JWT generation & verification
│ │ └── challenge.util.ts # Wallet challenge generation
│ │
│ ├── types/ # TypeScript type definitions
│ │ ├── auth.types.ts # Authentication types
│ │ ├── round.types.ts # Round & game mode types
│ │ ├── leaderboard.types.ts # Leaderboard types
│ │ ├── education.types.ts # Education tip types
│ │ ├── chat.types.ts # Chat message types
│ │ ├── prisma.types.ts # Prisma client extensions
│ │ └── xelma-bindings.d.ts # Xelma bindings type stubs
│ │
│ ├── lib/
│ │ └── prisma.ts # Prisma client instance
│ │
│ ├── docs/
│ │ └── openapi.ts # OpenAPI/Swagger configuration
│ │
│ ├── scripts/
│ │ ├── generate-openapi.ts # Generate OpenAPI JSON
│ │ └── export-postman.ts # Export Postman collection
│ │
│ └── tests/ # Jest test suites
│ ├── education-tip.service.spec.ts
│ ├── education-tip.route.spec.ts
│ └── round.spec.ts
│
├── prisma/
│ ├── schema.prisma # Prisma database schema
│ ├── migrations/ # Database migrations
│ └── seed.ts # Database seeding script
│
├── dist/ # Compiled JavaScript output
├── docs/ # Additional documentation
├── .env.example # Environment variables template
├── package.json # Project dependencies & scripts
├── tsconfig.json # TypeScript configuration
├── jest.config.ts # Jest testing configuration
└── README.md # This file
- Purpose: Fetches real-time XLM/USD price from CoinGecko
- Polling Interval: Every 10 seconds
- Singleton Pattern: Single instance across the application
- Used By: Round service, WebSocket service for price updates
- Purpose: Interfaces with Soroban smart contracts on Stellar blockchain
- Capabilities:
- Create new rounds on-chain
- Lock rounds for betting
- Resolve rounds with final prices
- Mint initial tokens for users
- Place bets and claim winnings
- Configuration: Requires
SOROBAN_CONTRACT_ID, admin & oracle keypairs - Failsafe: Gracefully disables if configuration is missing
- Purpose: Manages the complete lifecycle of prediction rounds
- Responsibilities:
- Start new rounds (UP_DOWN or LEGENDS mode)
- Lock rounds when betting period ends
- Fetch active, locked, and upcoming rounds
- Calculate pool sizes (UP vs DOWN pools)
- Integrations: Soroban service, WebSocket service, notification service
- Purpose: Handles user bet submissions
- Validations:
- Round is active and not locked
- User has sufficient balance
- No duplicate predictions per round
- Correct prediction format (side for UP_DOWN, range for LEGENDS)
- Actions:
- Deducts user balance
- Calls Soroban contract to place bet
- Updates round pool sizes
- Emits WebSocket events
- Purpose: Resolves completed rounds and distributes winnings
- Process:
- Fetch final price from oracle
- Update round status to RESOLVED
- Calculate payouts for winning predictions
- Update user stats (wins, earnings, streaks)
- Call Soroban contract to finalize round
- Send win/loss notifications
- Payout Formula: Proportional to bet size and total pool ratio
- Purpose: Aggregates and ranks user performance data
- Metrics:
- Total earnings
- Win/loss counts per game mode
- Current win streak
- Accuracy percentage
- Queries: Optimized database queries with pagination support
- Materialized sorted set: When Redis is available, a Redis sorted set
(
ZSET) stores every user'stotalEarningsas the score. Rank lookups become O(log N) instead of a full-tableCOUNT(*). The set is kept in sync after everyupdateUserStatsForRoundcall and invalidated whenever the leaderboard namespace is flushed. The DB path is always the fallback when Redis is unavailable.
- Purpose: Broadcasts real-time events to connected clients
- Events:
price_update- New XLM price every 5 secondsround_update- Round status changes (created, locked, resolved)user_balance_update- User balance changesnew_notification- New notificationsnew_message- New chat messages
- Authentication: JWT-based socket authentication
scheduler.service.ts: General-purpose cron job runnerround-scheduler.service.ts: Automated round management- Creates new rounds every 4 minutes (configurable)
- Locks rounds after 30 seconds (configurable)
- Controlled by
ROUND_SCHEDULER_ENABLEDenvironment variable
API-only mode: Set
API_ONLY=trueto start the HTTP server with all schedulers, oracle polling, and the WebSocket price ticker disabled. This is the recommended setup for split deployments — one dedicated worker process runs background jobs while one or more stateless processes serve HTTP — and for safer local debugging.
- Purpose: Guarantees at-least-once delivery of notification and WebSocket side-effects
- How it works:
- Business transactions (payout, prediction) write
OutboxEventrows inside the sameprisma.$transaction()call — atomically with the state change. - A background poller (cron, every
OUTBOX_POLL_INTERVAL_SECONDS) readsPENDINGrows and dispatches them. - On success the row is marked
PROCESSED. On failureattemptsis incremented; onceOUTBOX_MAX_ATTEMPTSis reached the row is markedFAILEDand escalated to the existing DLQ.
- Business transactions (payout, prediction) write
- Why this matters: Before this change, notifications fired after the transaction committed. A process crash between commit and notification call silently dropped the event. Now the event is durable from the moment the transaction commits.
- Env vars:
OUTBOX_POLL_INTERVAL_SECONDS,OUTBOX_BATCH_SIZE,OUTBOX_MAX_ATTEMPTS,OUTBOX_RETENTION_DAYS
- Purpose: Creates and delivers notifications to users
- Types: WIN, LOSS, ROUND_START, BONUS_AVAILABLE, ANNOUNCEMENT
- Channels: Database storage + WebSocket emission
- Filtering: Respects user notification preferences
- Purpose: Handles global chat message submission and retrieval
- Features:
- Message validation (max 500 characters)
- Automatic user info attachment
- WebSocket broadcasting
- Pagination support
- Purpose: Provides educational content for users
- Features:
- Daily tip delivery
- Random tip selection
- Category-based filtering
POST /challenge- Request a wallet authentication challenge (returns challenge string)POST /connect- Verify signed challenge and issue JWT token
GET /profile- [Auth] Get authenticated user's profileGET /balance- [Auth] Get current virtual balanceGET /stats- [Auth] Get detailed user statisticsPATCH /profile- [Auth] Update user preferences (nickname, avatar, preferences)GET /transactions- [Auth] Get paginated transaction historyGET /:walletAddress/public-profile- Get any user's public profile
POST /start- [Admin] Start a new roundGET /active- Get all active roundsGET /:id- Get specific round detailsPOST /:id/resolve- [Oracle] Resolve a round with final price
POST /submit- [Auth] Submit a prediction for a roundGET /user/:userId- Get user's prediction historyGET /round/:roundId- Get all predictions for a round
GET /- Get global leaderboard (paginated, optional auth for user position)
GET /guides- Get all educational guides grouped by categoryGET /tip?roundId=<uuid>- Generate contextual educational tip for a resolved round
POST /send- [Auth] Send a chat messageGET /history- Get recent chat messages (paginated, max 50)
GET /- [Auth] Get paginated notificationsGET /unread-count- [Auth] Get unread notification countGET /:id- [Auth] Get a specific notificationPATCH /:id/read- [Auth] Mark a notification as readPATCH /read-all- [Auth] Mark all notifications as readDELETE /:id- [Auth] Delete a notificationDELETE /- [Auth] Delete all read notifications
GET /- Health check with timestampGET /health- Detailed health check (uptime, status)GET /metrics- Prometheus metrics for HTTP, schedulers, oracle, predictions, WebSocket, rate limits, and DB pool settingsGET /api/price- Current XLM/USD price as a decimal string with staleness infoGET /api-docs- Swagger UI documentationGET /api-docs.json- OpenAPI specification
authenticateUser: Verifies JWT token and attaches user to requestrequireAdmin: Ensures user has ADMIN rolerequireOracle: Ensures user has ORACLE role
- Prevents API abuse with per-IP and per-user limits
- Single prediction submit: 10 requests/minute per user
- Batch prediction submit: 3 requests/minute per user (stricter; each batch may include up to 50 predictions)
- Batch leaderboard lookup: 10 requests/minute per user
- Auth, chat, admin round creation, and oracle resolve endpoints have tailored policies
- Rate-limit hits are recorded for the admin metrics dashboard (
GET /api/admin/metrics/rate-limits)
- Canonical list of API routes and required auth levels (
public,authenticated,admin,oracle) src/tests/security.spec.tsandsrc/tests/route-auth.registry.spec.tsfail CI when the registry drifts from implemented routes- Role middleware (
requireAdmin,requireOracle,authenticateUser) is built on a sharedrequireRolehelper inauth.middleware.ts
The application uses PostgreSQL via Prisma ORM. Key models:
- User: Wallet address, virtual balance, wins, streaks, roles
- Round: Game mode, status, prices, pools, timestamps
- Prediction: User bets with side/range, amounts, payouts
- Notification: User notifications with types and read status
- Message: Global chat messages
- UserStats: Aggregated performance metrics per game mode
- Transaction: Balance change history (bonus, win, loss, etc.)
- AuthChallenge: Wallet signature challenges for authentication
- AuditLog: Security audit trail for authentication and authorization events
The backend implements automated data retention policies to control storage growth while maintaining security audit trails.
All authentication and authorization events are logged for security monitoring and compliance:
- Events Logged: Challenge lifecycle (issued, verified, failed, expired, invalidated), authentication success/failure, user creation/login
- Storage: Audit events are persisted to the
AuditLogtable in the database - Configuration: Controlled by
AUDIT_LOG_DATABASE_ENABLED(default:true) - Fallback: When database persistence is disabled, events are only logged to Winston (files/console)
The retention service automatically cleans up old data based on configurable time-to-live (TTL) policies:
| Entity | Environment Variable | Default TTL | Purpose |
|---|---|---|---|
| Auth Challenges | RETENTION_AUTH_CHALLENGES_TTL_DAYS |
7 days | Remove expired and old authentication challenges |
| Chat Messages | RETENTION_CHAT_MESSAGES_TTL_DAYS |
90 days | Archive old chat messages |
| Audit Logs | RETENTION_AUDIT_LOGS_TTL_DAYS |
90 days | Maintain security audit trail for compliance |
Configuration:
- Enable/disable each policy via
RETENTION_*_ENABLED(default:true) - Batch size for deletion operations:
RETENTION_BATCH_SIZE(default: 1000) - Retention service can be run on-demand or via cron scheduler
Implementation: See src/services/retention.service.ts
See prisma/schema.prisma for full schema.
- Node.js 22.x or higher
- npm, pnpm, or yarn
- PostgreSQL database (local or cloud-hosted)
- Stellar account with testnet/mainnet keypairs (for admin & oracle roles)
- @tevalabs/xelma-bindings package (installed automatically)
git clone https://github.com/TevaLabs/Xelma-Backend.git
cd Xelma-Backendnpm install
# or
pnpm install
# or
yarn installThis will automatically:
- Install all dependencies including
@tevalabs/xelma-bindings - Run
postinstallscript to build the TypeScript code
cp .env.example .envThis application requires specific environment variables to run securely. Create a .env file in the root directory based on .env.example.
| Variable | Description | Default |
|---|---|---|
JWT_SECRET |
Cryptographic secret used to sign and verify JSON Web Tokens. App will refuse to start without this. | None |
Note: For production, JWT_SECRET must be a cryptographically strong, random string (e.g., generated via openssl rand -base64 32).
Open .env and set the following:
# Server Configuration
PORT=3000
NODE_ENV=development
CLIENT_URL=http://localhost:5173
# Database
DATABASE_URL=postgresql://username:password@localhost:5432/xelma_db
# Prisma / Postgres pool + timeout tuning (optional)
# If set, these values override/augment DATABASE_URL query params at startup.
# Defaults are production-safe and conservative.
DB_CONNECTION_LIMIT=10
DB_POOL_TIMEOUT_SECONDS=10
DB_CONNECT_TIMEOUT_SECONDS=10
DB_STATEMENT_TIMEOUT_MS=0
DB_PGBOUNCER=false
# JWT Authentication
JWT_SECRET=your-super-secret-jwt-key-change-this-in-production
JWT_EXPIRY=7d
# Xelma Bindings API Key (if required by your setup)
XELMA_API_KEY=your-xelma-api-key-here
# Soroban Configuration
SOROBAN_NETWORK=testnet # or 'mainnet'
SOROBAN_RPC_URL=https://soroban-testnet.stellar.org
SOROBAN_CONTRACT_ID=your-deployed-contract-id
# Stellar Keypairs (use Stellar Laboratory to generate)
# Admin keypair for creating rounds
SOROBAN_ADMIN_SECRET=S...your-admin-secret-key
# Oracle keypair for resolving rounds
SOROBAN_ORACLE_SECRET=S...your-oracle-secret-key
# Round Scheduler
ROUND_SCHEDULER_ENABLED=false # Set to 'true' to enable automated rounds
ROUND_SCHEDULER_MODE=UP_DOWN # or 'LEGENDS'
# API-only startup mode (skip oracle polling, schedulers, and price ticker)
API_ONLY=false # Set to 'true' to run as a stateless HTTP API only
# Price Oracle Configuration
ORACLE_POLLING_INTERVAL_MS=10000 # Interval between price updates (ms)
ORACLE_REQUEST_TIMEOUT_MS=5000 # Network timeout for requests (ms)
ORACLE_MAX_RETRIES=3 # Max retry attempts for failed requests
ORACLE_STALENESS_THRESHOLD_MS=60000 # Threshold for stale price data (ms)Operators can tune the oracle's behavior via environment variables to balance price freshness against API rate limits and network reliability:
| Variable | Description | Default |
|---|---|---|
ORACLE_POLLING_INTERVAL_MS |
How often to fetch the price from CoinGecko. | 10000 (10s) |
ORACLE_REQUEST_TIMEOUT_MS |
Network timeout for the API request. | 5000 (5s) |
ORACLE_MAX_RETRIES |
Number of retry attempts on failure. | 3 |
ORACLE_STALENESS_THRESHOLD_MS |
When to consider the local price data stale. | 60000 (60s) |
Prisma’s Postgres connector reads pool/timeouts via connection string query params. This backend exposes operational knobs as env vars and merges them into DATABASE_URL at startup (env vars win over existing query params):
| Variable | Purpose | Default |
|---|---|---|
DB_CONNECTION_LIMIT |
Max Prisma DB connections | 10 |
DB_POOL_TIMEOUT_SECONDS |
Wait for a pooled connection | 10 |
DB_CONNECT_TIMEOUT_SECONDS |
Timeout establishing a new connection | 10 |
DB_STATEMENT_TIMEOUT_MS |
Server-side statement timeout (0 disables) |
0 |
DB_PGBOUNCER |
Enable PgBouncer transaction-pooling mode | false |
Notes
- PgBouncer: if your stack uses PgBouncer in transaction pooling mode, set
DB_PGBOUNCER=true. - Visibility: scrape
/metricsand look fordb_pool_settings_infoto see the effective values. - Validation: invalid values are rejected at startup via config validation.
GET /metrics exposes Prometheus text-format metrics with only
low-cardinality labels. Labels intentionally avoid user IDs, wallet addresses,
round IDs, socket IDs, request bodies, and secrets.
Core application metrics include:
| Metric | Labels | Meaning |
|---|---|---|
http_requests_total |
method, route, status_code |
HTTP request volume by normalized Express route |
http_request_duration_seconds |
method, route, status_code |
HTTP latency histogram |
http_errors_total |
method, route, status_code |
HTTP 4xx/5xx responses |
predictions_placed_total |
none | Successful prediction submissions |
rounds_started_total |
mode |
Rounds created by game mode |
rounds_resolved_total |
mode |
Rounds resolved by game mode |
price_oracle_updates_total |
none | Successful oracle price refreshes |
price_oracle_fetch_failures_total |
reason |
Oracle refresh failures |
scheduler_runs_total |
job, outcome |
Scheduler executions |
scheduler_items_processed_total |
job, outcome |
Items processed by scheduler jobs |
socket_connections_active |
none | Current Socket.IO connections |
websocket_emits_total |
event, outcome |
WebSocket dispatch attempts |
websocket_connection_events_total |
event, authenticated |
Socket connect/disconnect events |
# Generate Prisma client and apply committed migrations
npm run db:prepare
# Create a new development migration when changing prisma/schema.prisma
npm run prisma:migrate
# (Optional) Seed database with sample data
npx prisma db seedNote: Never commit your
.envfile. It contains sensitive credentials.
npm run devThe server will start on http://localhost:3000 with auto-reload on file changes.
Use one command when you want local startup to perform the same Prisma preparation Render performs before booting the service:
npm run dev:render-parityThis runs prisma generate, applies committed migrations with
prisma migrate deploy, then starts the hot-reload dev server. It expects a
local .env with at least DATABASE_URL and JWT_SECRET; copy
.env.example to .env if you are starting from a fresh checkout.
# Build TypeScript to JavaScript
npm run build
# Start production server
npm startTo reproduce the runtime behavior of the Render deployment on your machine,
use the start:render-parity script. This sets NODE_ENV=production
before launching the built server so the same code paths Render hits
fire locally — CORS is strict (CLIENT_URL must be set, no wildcard
origin), error responses match production, and logging runs at
production verbosity.
# 1) Build first (start:render-parity expects dist/)
npm run build
# 2) Run with production-shaped environment
CLIENT_URL=http://localhost:5173 \
JWT_SECRET="$(openssl rand -base64 32)" \
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/xelma_local" \
npm run start:render-parityRequired env vars for parity (matches what Render's environment supplies):
| Variable | Why it matters in render-parity mode |
|---|---|
NODE_ENV=production |
Set by the script. Enables strict CORS and production logging. |
CLIENT_URL |
Required. Strict CORS will reject all origins if unset. |
ALLOWED_ORIGINS |
Optional comma-separated extra origins. |
JWT_SECRET |
Required for startup. Use a cryptographically strong value. |
DATABASE_URL |
Required. Point at a local Postgres. |
SOROBAN_CONTRACT_ID / SOROBAN_ADMIN_SECRET / SOROBAN_ORACLE_SECRET |
Optional; only needed if you want on-chain calls. |
If you hit a CORS error from your frontend in this mode, hit
GET /api/admin/cors-diagnostics?origin=<your-origin> with an admin
token to see exactly which origins this process accepts.
curl http://localhost:3000/healthExpected response:
{
"status": "healthy",
"uptime": 42.123,
"timestamp": "2026-02-23T12:00:00.000Z"
}Notification creation and WebSocket emits go through a dead-letter queue
(DLQ) so a transient DB blip, a not-yet-initialized socket layer, or a
runtime exception in emit does not silently drop a user-facing event.
How it works:
notificationService.createNotification(...)records aFailedDispatchrow onNOTIFICATION_CREATEerrors (the original error still rethrows so callers behave the same).websocketService.emit*(...)records aFailedDispatchrow whenever the socket layer is not initialized or the underlyingemitthrows. The emit itself is fire-and-forget — the caller's hot path is never broken by a DLQ persistence failure.- Rows have
attempts,lastError, andstatus(PENDING,RETRYING,RESOLVED,ABANDONED) so an operator can triage stuck dispatches.
Operator endpoints (admin-only, gated by requireAdmin):
GET /api/admin/dead-letter— list entries, newest first. Query params:status,channel,limit,offset.POST /api/admin/dead-letter/:id/retry— replay a single entry; setsRESOLVEDon success, bumpsattemptsand moves toABANDONEDonce the cap (default 5) is reached.POST /api/admin/dead-letter/retry-all— replay everyPENDING/RETRYINGentry (capped, oldest first). Returns a counts summary.
The backend provides auto-generated OpenAPI/Swagger documentation.
- Swagger UI: http://localhost:3000/api-docs
- OpenAPI JSON: http://localhost:3000/api-docs.json
POST /api/auth/challenge
Content-Type: application/json
{
"walletAddress": "GXXX...YOUR_STELLAR_ADDRESS"
}Response:
{
"challenge": "random-challenge-string",
"expiresAt": "2026-02-23T00:05:00.000Z"
}POST /api/auth/connect
Content-Type: application/json
{
"walletAddress": "GXXX...YOUR_STELLAR_ADDRESS",
"challenge": "random-challenge-string",
"signature": "BASE64_SIGNATURE_OF_CHALLENGE"
}Response:
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": "user-uuid",
"walletAddress": "GXXX...",
"createdAt": "2026-01-01T00:00:00.000Z",
"lastLoginAt": "2026-02-23T12:00:00.000Z"
},
"bonus": 100,
"streak": 1
}POST /api/rounds/start
Authorization: Bearer YOUR_JWT_TOKEN
Content-Type: application/json
{
"mode": 0, # 0 = UP_DOWN, 1 = LEGENDS
"startPrice": 0.1234,
"duration": 300 # Duration in seconds
}Response:
{
"success": true,
"round": {
"id": "round-uuid",
"mode": "UP_DOWN",
"status": "ACTIVE",
"startPrice": 0.1234,
"startTime": "2026-02-23T12:00:00Z",
"endTime": "2026-02-23T12:05:00Z",
"sorobanRoundId": "1",
"poolUp": 0,
"poolDown": 0
}
}GET /api/rounds/activeResponse:
{
"rounds": [
{
"id": "round-uuid",
"mode": "UP_DOWN",
"status": "ACTIVE",
"startPrice": 0.1234,
"startTime": "2026-02-23T12:00:00Z",
"endTime": "2026-02-23T12:05:00Z",
"poolUp": 150,
"poolDown": 200
}
]
}POST /api/predictions/submit
Authorization: Bearer YOUR_JWT_TOKEN
Content-Type: application/json
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
# For UP_DOWN mode:
{
"roundId": "round-uuid",
"amount": 10,
"side": "UP"
}
# For LEGENDS mode:
{
"roundId": "round-uuid",
"amount": 10,
"priceRange": {
"min": 0.12,
"max": 0.13
}
}Idempotency-Key is optional but recommended for clients that may retry a
submit request after network failure. The same authenticated user can retry the
same request body with the same key for 10 minutes and receive the cached
response. Reusing the same key with a different request body returns 409 with
code IDEMPOTENCY_KEY_CONFLICT; generate a fresh key for a new prediction
attempt.
Response:
{
"success": true,
"prediction": {
"id": "prediction-uuid",
"roundId": "round-uuid",
"amount": 10,
"side": "UP",
"priceRange": null,
"createdAt": "2026-02-23T12:01:00Z"
}
}GET /api/leaderboard?limit=100&offset=0Response:
{
"leaderboard": [
{
"rank": 1,
"userId": "user-uuid",
"walletAddress": "GXXX...XXXX",
"totalEarnings": 5432.10,
"totalPredictions": 60,
"accuracy": 75.0,
"modeStats": {
"upDown": { "wins": 30, "losses": 15, "earnings": 3000.0, "accuracy": 66.67 },
"legends": { "wins": 15, "losses": 0, "earnings": 2432.10, "accuracy": 100.0 }
}
}
],
"userPosition": null,
"totalUsers": 150,
"lastUpdated": "2026-02-23T12:00:00.000Z"
}Connect to the WebSocket server with JWT authentication:
import io from 'socket.io-client';
const socket = io('http://localhost:3000', {
auth: {
token: 'YOUR_JWT_TOKEN'
}
});
// Listen for price updates
socket.on('price_update', (data) => {
console.log('New price:', data);
// { asset: 'XLM', price: 0.1234, timestamp: '...' }
});
// Listen for round updates
socket.on('round_update', (data) => {
console.log('Round update:', data);
// { type: 'created'|'locked'|'resolved', round: {...} }
});
// Listen for balance updates
socket.on('user_balance_update', (data) => {
console.log('Balance update:', data);
// { userId: '...', balance: 1050 }
});
// Listen for notifications
socket.on('new_notification', (notification) => {
console.log('Notification:', notification);
});
// Listen for chat messages
socket.on('new_message', (message) => {
console.log('Chat:', message);
});Run the test suite with Jest:
# Run all tests
npm test
# Run unit tests with coverage thresholds
npm run test:unit:coverage
# Run the full local CI check
npm run ci
# Run tests in watch mode
npm run test:watch
# Repeatable load baselines for prediction throughput + websocket fanout (#21)
npm run test:loadnpm run test:load runs src/tests/performance.spec.ts, which exercises:
- Single-request latency baselines for auth, active rounds, and prediction submit (#152).
- Concurrent prediction throughput — N parallel
POST /api/predictions/submitrequests with aggregate RPS and p95 latency assertions. - WebSocket fanout — M clients join the
roundroom and must receiveprediction:placedwithin the configured p95 budget.
The harness lives in src/tests/load-test.harness.ts and uses mocked Prisma/Soroban so it stays repeatable in CI without a live database. Tune thresholds via env vars (see .env.example → “Load / performance test harness”):
| Variable | Default | Purpose |
|---|---|---|
LOAD_TEST_PREDICTION_CONCURRENCY |
10 |
Max in-flight prediction requests |
LOAD_TEST_PREDICTION_ITERATIONS |
30 |
Total prediction requests per run |
LOAD_TEST_PREDICTION_MIN_RPS |
5 |
Minimum acceptable throughput |
LOAD_TEST_PREDICTION_P95_MS |
500 |
Max p95 latency for predictions |
LOAD_TEST_WS_CLIENTS |
20 |
Connected sockets for fanout test |
LOAD_TEST_WS_MIN_DELIVERY_RATE |
1 |
Minimum delivery ratio (0–1) |
LOAD_TEST_WS_P95_MS |
250 |
Max p95 fanout delivery time |
Each run prints [LOAD] summary lines to stdout for before/after comparisons in PRs.
Coverage thresholds are enforced in jest.config.ts for lines, branches, functions, and statements. The current floor is intentionally conservative and excludes tests, mocks, generated files, scripts, and vendored bindings so the gate tracks application code. CI runs npm run test:unit:coverage, prints the Jest coverage summary, uploads coverage/, and fails when the thresholds are not met.
Current test coverage includes:
- Education tip service tests
- Education tip route tests
- Round service tests
Schema changes should follow the migration checklist in docs/migration-safety.md. Use it before opening PRs that edit prisma/schema.prisma, add files under prisma/migrations/, or require production backfills.
At minimum, migration PRs should include:
- A before/after behavior summary.
- Risk notes for locks, backfills, and compatibility with the previous application version.
- Verification output for Prisma generation, migration, and targeted tests.
- A rollback plan that preserves production data.
| Script | Description |
|---|---|
npm start |
Run production server (requires build) |
npm run dev |
Start development server with hot-reload |
npm run dev:render-parity |
Generate Prisma client, apply committed migrations, then start dev server |
npm run build |
Compile TypeScript to JavaScript |
npm test |
Run Jest test suite |
npm run test:coverage |
Run Jest with coverage reporting and thresholds |
npm run test:unit:coverage |
Run unit tests with coverage reporting and thresholds |
npm run test:watch |
Run tests in watch mode |
npm run test:load |
Run repeatable load baselines for prediction throughput and websocket fanout (#21) |
npm run ci |
Run lint, build, unit coverage, and integration tests |
npm run prisma:generate |
Generate Prisma client |
npm run prisma:migrate |
Run database migrations |
npm run prisma:migrate:deploy |
Apply committed migrations without creating new migration files |
npm run db:prepare |
Run Prisma generate and migrate deploy |
npm run docs:openapi |
Generate OpenAPI JSON spec to docs/openapi.json |
npm run docs:verify |
Regenerate OpenAPI and verify required paths are documented (CI gate) |
npm run docs:postman |
Export Postman collection |
npm run scorecard |
Run the production-readiness scorecard (see #197) |
Every error response from the API carries a stable machine-readable
code (in addition to the HTTP status) so clients can branch on the
specific failure without parsing prose. The canonical list lives in
src/utils/errors.ts as ERROR_CATALOG and is
also exposed as JSON at GET /api/errors for client codegen.
A drift test (src/tests/error-catalog.spec.ts) pins the catalog to
the ErrorCode enum, so adding a new code without a catalog entry
fails CI.
| HTTP | Code | Description |
|---|---|---|
| 400 | VALIDATION_ERROR |
Body / query / params failed schema validation. See error.details. |
| 401 | AUTHENTICATION_ERROR |
Missing / invalid credentials. Re-authenticate. |
| 401 | INVALID_CHALLENGE |
Signed challenge does not match a known issued challenge. |
| 401 | CHALLENGE_EXPIRED |
Challenge TTL elapsed. Request a new one. |
| 401 | CHALLENGE_USED |
Challenge already consumed (one-shot). |
| 401 | INVALID_SIGNATURE |
Signature does not verify against wallet + challenge. |
| 403 | AUTHORIZATION_ERROR |
Authenticated, not permitted. |
| 404 | NOT_FOUND |
Resource does not exist. |
| 409 | CONFLICT |
Generic state conflict. |
| 409 | ROUND_ALREADY_RESOLVED |
Round outcome already final. |
| 409 | DUPLICATE_PREDICTION |
User already predicted on this round. |
| 409 | ACTIVE_ROUND_EXISTS |
A round of the requested mode is already active. |
| 422 | BUSINESS_RULE_VIOLATION |
Generic domain rule violation. |
| 422 | INSUFFICIENT_FUNDS |
Not enough balance. |
| 422 | ROUND_NOT_ACTIVE |
Round is not in ACTIVE status. |
| 422 | ROUND_LOCKED |
Round is locked before resolution. |
| 500 | CONFIGURATION_ERROR |
Server misconfiguration. Operator action required. |
| 500 | INTERNAL_SERVER_ERROR |
Unexpected. Retry; include requestId if reporting. |
| 503 | EXTERNAL_SERVICE_ERROR |
Upstream (DB, RPC, oracle) failure. Retry with backoff. |
npm run scorecard runs a small, zero-dependency set of "is this repo
ready to deploy?" heuristics and prints a green / yellow / red
breakdown. CI runs the same script in its own job
(.github/workflows/ci.yml) and fails the
build only when a required check fails — soft "nice to have"
checks emit warnings without blocking merges. New checks live in
scripts/production-readiness-scorecard.js.
Error:
Soroban configuration or bindings missing. Soroban integration DISABLED.
Solution:
Ensure your .env contains valid values for:
SOROBAN_CONTRACT_IDSOROBAN_ADMIN_SECRETSOROBAN_ORACLE_SECRET
Verify the contract is deployed and accessible at SOROBAN_RPC_URL.
Error:
Cannot find module '@tevalabs/xelma-bindings'
Solution:
npm install @tevalabs/xelma-bindings
# or
npm installError:
Can't reach database server at localhost:5432
Solution:
- Verify PostgreSQL is running:
psql -U postgres - Check
DATABASE_URLin.envmatches your database credentials - Ensure database
xelma_dbexists or run migrations:npm run prisma:migrate
Cause: Token is missing, expired, or invalid.
Solution:
- Ensure you're including the token in the
Authorizationheader:Authorization: Bearer YOUR_JWT_TOKEN - If expired, log in again to get a fresh token
- Verify
JWT_SECRETin.envmatches the one used to generate the token
Cause: Your account doesn't have the required role.
Solution:
- Check your user's role in the database (should be
ADMINorORACLE) - Verify
SOROBAN_ADMIN_SECRETandSOROBAN_ORACLE_SECRETin.envmatch the keypairs registered in the smart contract - Ensure you're using the correct JWT token for the intended role
Cause: CoinGecko API rate limits or network issues.
Solution:
- Check server logs for error messages from the oracle service
- Verify internet connectivity
- Consider using a CoinGecko API key if hitting rate limits (update
oracle.ts)
Cause: Scheduler is disabled in configuration.
Solution:
Set ROUND_SCHEDULER_ENABLED=true in .env and restart the server.
The following are proposed issue drafts you can open in GitHub. They are based on the current backend code and prioritize security, correctness, reliability, and maintainability.
Context
Multiple files instantiate new PrismaClient() directly (for example middleware/services/socket), while src/lib/prisma.ts already provides a shared singleton. This can cause excess DB connections and inconsistent behavior across environments.
What Needs to Happen
- Replace direct
new PrismaClient()usage with imports fromsrc/lib/prisma.ts. - Ensure all services/middleware/socket paths use the same Prisma lifecycle.
- Add a lightweight check/test to prevent regressions.
Files to Create/Modify
src/middleware/auth.middleware.tssrc/services/round.service.tssrc/services/notification.service.tssrc/services/scheduler.service.tssrc/socket.ts
Acceptance Criteria
- No direct
new PrismaClient()remains outsidesrc/lib/prisma.ts. - App behavior is unchanged functionally.
- No Prisma connection warnings during local development under load.
How to Validate
- Run
npm run build. - Run
npm test. - Start app and verify no repeated Prisma client initialization/connection warnings.
PR Requirements
- PR title:
refactor: centralize prisma client usage - Include
Closes #[issue_id]in PR description
Context
src/index.ts starts polling, schedulers, WebSocket emission interval, and HTTP listen as import-time side effects. This makes integration testing harder and complicates graceful shutdown.
What Needs to Happen
- Introduce explicit
createApp()andstartServer()lifecycle functions. - Track interval/cron handles and close them on shutdown signals.
- Add shutdown hooks for HTTP server and Prisma disconnect.
Files to Create/Modify
src/index.tssrc/services/oracle.tssrc/services/scheduler.service.tssrc/services/round-scheduler.service.tssrc/lib/prisma.ts
Acceptance Criteria
- Importing app module does not automatically bind network ports.
- Server exits cleanly on
SIGINT/SIGTERM. - Test suites can initialize app without background jobs running unexpectedly.
How to Validate
- Run
npm test. - Start app and stop with Ctrl+C; ensure clean shutdown logs with no hanging process.
PR Requirements
- PR title:
refactor: isolate startup side effects and add graceful shutdown - Include
Closes #[issue_id]in PR description
Context
POST /api/rounds/start validates mode with if (!mode || mode < 0 || mode > 1) which incorrectly rejects valid mode 0 (UP_DOWN).
What Needs to Happen
- Replace falsy checks with explicit numeric validation.
- Add validation tests for
mode=0andmode=1.
Files to Create/Modify
src/routes/rounds.routes.tssrc/tests/round.spec.ts
Test Scenarios
mode=0accepted.mode=1accepted.- invalid values (
-1,2, string) rejected with400.
Acceptance Criteria
UP_DOWNrounds can be created via API.- Validation behavior is deterministic and covered by tests.
How to Validate
- Run
npm test -- --testPathPattern=round.
PR Requirements
- PR title:
fix: correct mode validation for round creation - Include
Closes #[issue_id]in PR description
Context
POST /api/predictions/submit currently accepts userId from request body despite requiring JWT auth. This allows user impersonation by submitting predictions for another user.
What Needs to Happen
- Remove
userIdfrom request body contract. - Use
req.user.userIdas the single source of identity. - Update OpenAPI docs and tests.
Files to Create/Modify
src/routes/predictions.routes.tssrc/docs/openapi.tssrc/tests/(add predictions route tests)
Acceptance Criteria
- Endpoint ignores/rejects external
userIdinput. - Authenticated user can only submit for self.
- Docs reflect updated request schema.
How to Validate
- Add test with mismatched body
userId; assert request fails or body field is ignored. - Run
npm test.
PR Requirements
- PR title:
security: bind prediction submissions to authenticated user - Include
Closes #[issue_id]in PR description
Context Prediction placement performs multiple writes (prediction insert, balance update, pool update, Soroban call) without transactional boundaries, risking partial state on failure or concurrency races.
What Needs to Happen
- Use Prisma transactions for DB writes.
- Define contract for external Soroban call ordering and rollback strategy.
- Add concurrency-aware tests for duplicate submissions and balance integrity.
Files to Create/Modify
src/services/prediction.service.tssrc/tests/(new prediction service tests)
Acceptance Criteria
- No partial DB updates when any step fails.
- User balance and round pools remain consistent under concurrent submissions.
How to Validate
- Run test suite including failure-injection scenarios.
- Run stress test script for concurrent submissions.
PR Requirements
- PR title:
fix: make prediction placement transactional and race-safe - Include
Closes #[issue_id]in PR description
Context Round creation paths (manual and scheduler) do not guard against overlapping active rounds. This can create ambiguous active state and inconsistent client behavior.
What Needs to Happen
- Enforce active-round guard by mode (or globally, per product rule).
- Add conflict response (for example
409) from API layer. - Ensure scheduler respects existing active rounds.
Files to Create/Modify
src/services/round.service.tssrc/services/round-scheduler.service.tssrc/routes/rounds.routes.tssrc/tests/round.spec.ts
Acceptance Criteria
- At most one active round per defined constraint.
- Scheduler does not create overlapping active rounds.
How to Validate
- Start round, attempt second creation immediately, assert conflict.
- Run scheduler simulation with existing active round.
PR Requirements
- PR title:
fix: enforce single active round constraint - Include
Closes #[issue_id]in PR description
Context Lock/resolve operations run in loops and cron contexts. Without strict state transition guards and idempotency, repeated jobs can cause noisy failures and inconsistent side effects.
What Needs to Happen
- Make
lockRoundandresolveRoundstate transitions conditional and idempotent. - Return explicit outcomes (
updated,already_locked,already_resolved). - Add retry-safe scheduler behavior.
Files to Create/Modify
src/services/round.service.tssrc/services/resolution.service.tssrc/services/scheduler.service.tssrc/services/round-scheduler.service.ts
Acceptance Criteria
- Re-running lock/resolve for same round is safe.
- Schedulers do not emit false errors on already-processed rounds.
How to Validate
- Trigger same operation twice and verify second pass is no-op.
- Run auto-resolve job repeatedly with same dataset.
PR Requirements
- PR title:
fix: make round lifecycle transitions idempotent - Include
Closes #[issue_id]in PR description
Context
Round resolve responses reference resolvedAt, but schema currently has no such field, producing undefined data and inconsistent API contracts.
What Needs to Happen
- Add
resolvedAtto PrismaRoundmodel via migration. - Populate it during resolution.
- Ensure API docs and response payloads are aligned.
Files to Create/Modify
prisma/schema.prismaprisma/migrations/(new migration)src/services/resolution.service.tssrc/routes/rounds.routes.ts
Acceptance Criteria
- Resolved rounds always include non-null
resolvedAt. - API response schema matches runtime output.
How to Validate
- Run
npm run prisma:migrate. - Resolve a round and verify
resolvedAtpersisted and returned.
PR Requirements
- PR title:
feat: persist resolvedAt for rounds - Include
Closes #[issue_id]in PR description
Context
Auth challenge lookup and isUsed update occur in separate operations, leaving a race window where the same challenge could be consumed by concurrent requests.
What Needs to Happen
- Use transaction or conditional update (
updateManywithisUsed=false) to consume challenge atomically. - Ensure only one request can successfully consume each challenge.
- Add concurrent auth tests.
Files to Create/Modify
src/routes/auth.routes.tssrc/tests/(new auth route race tests)
Acceptance Criteria
- Challenge replay via concurrent requests is prevented.
- Exactly one request succeeds for a single challenge.
How to Validate
- Run parallel connect requests with same challenge and signature.
- Assert one success, one auth failure.
PR Requirements
- PR title:
security: atomically consume auth challenges - Include
Closes #[issue_id]in PR description
Context JWT utility falls back to a weak default secret when env var is missing, creating a critical production risk.
What Needs to Happen
- Remove insecure default JWT secret fallback.
- Add startup config validation for required env vars.
- Fail fast with clear error messages.
Files to Create/Modify
src/utils/jwt.util.tssrc/index.tsREADME.md(env requirements)
Acceptance Criteria
- App refuses startup without
JWT_SECRET. - No hardcoded fallback secret remains.
How to Validate
- Start app without
JWT_SECRET; verify startup fails clearly. - Start with valid secret; verify normal auth flows.
PR Requirements
- PR title:
security: require explicit jwt secret configuration - Include
Closes #[issue_id]in PR description
Context
Codebase mixes console.log/error/warn with Winston logger, reducing observability consistency and log parsing quality.
What Needs to Happen
- Replace console statements with
loggerutility. - Standardize log fields and context objects.
- Ensure production-friendly log formatting.
Files to Create/Modify
src/services/oracle.tssrc/routes/auth.routes.tssrc/routes/user.routes.tssrc/routes/education.routes.tssrc/services/*(as needed)
Acceptance Criteria
- No direct
console.*usage in runtime paths. - Logs are structured and consistent across modules.
How to Validate
- Grep for
console.and confirm runtime files are clean. - Run app and verify consistent logger output.
PR Requirements
- PR title:
chore: standardize structured logging across backend - Include
Closes #[issue_id]in PR description
Context Oracle polling and price emit intervals are started without stop handles. In tests/restarts this can create duplicate timers and noisy behavior.
What Needs to Happen
- Return and manage interval handles for polling and broadcast loops.
- Add
start/stopsemantics to prevent duplicate starts. - Use lifecycle hooks from app bootstrap.
Files to Create/Modify
src/services/oracle.tssrc/index.tssrc/tests/(new timer lifecycle tests)
Acceptance Criteria
- Polling and emit loops can be started once and stopped cleanly.
- No duplicate interval activity after restart in process.
How to Validate
- Run lifecycle tests with fake timers.
- Manual restart scenario confirms single active loop.
PR Requirements
- PR title:
fix: add start-stop lifecycle for oracle and price broadcast - Include
Closes #[issue_id]in PR description
Context Rate limiting is strong on auth/chat but missing on several write-heavy endpoints such as prediction submission and round operations, increasing abuse/DoS risk.
What Needs to Happen
- Add per-user and per-IP rate limits for high-risk mutation routes.
- Add separate stricter policies for admin/oracle actions.
- Document limits in OpenAPI.
Files to Create/Modify
src/middleware/rateLimiter.middleware.tssrc/routes/predictions.routes.tssrc/routes/rounds.routes.tssrc/docs/openapi.ts
Acceptance Criteria
- Abuse-prone endpoints are rate-limited with tailored policies.
- OpenAPI docs reflect 429 behavior for affected routes.
How to Validate
- Hit endpoints in burst and verify
429responses. - Confirm normal usage remains unaffected.
PR Requirements
- PR title:
security: add rate limits for mutation endpoints - Include
Closes #[issue_id]in PR description
Context Price oracle currently fetches from one source with minimal resilience. Failures keep stale values silently and there is no explicit freshness metadata on served price.
What Needs to Happen
- Add request timeout and retry/backoff.
- Track
lastUpdatedAtand expose staleness in API. - Define behavior when data is stale (for example block round creation/resolution).
Files to Create/Modify
src/services/oracle.tssrc/index.ts(price endpoint)src/services/round-scheduler.service.tssrc/services/scheduler.service.ts
Acceptance Criteria
- Oracle fetch behavior is resilient to transient failures.
- API exposes freshness metadata.
- Scheduler decisions include staleness safeguards.
How to Validate
- Simulate API failures/timeouts and verify retries + stale handling.
- Confirm round creation/resolution behavior follows policy.
PR Requirements
- PR title:
feat: add resilient oracle fetching and freshness safeguards - Include
Closes #[issue_id]in PR description
Context
src/services/soroban.service.ts currently defines Client as undefined as any, while runtime methods depend on it. This can break critical blockchain flows silently at runtime.
What Needs to Happen
- Properly import and initialize client from
@tevalabs/xelma-bindings. - Add typed request/response handling and robust error mapping.
- Add integration tests/mocks for create/place/resolve flows.
Files to Create/Modify
src/services/soroban.service.tssrc/types/xelma-bindings.d.ts(if still needed)src/tests/(new soroban service tests)
Acceptance Criteria
- Soroban client initialization is fully functional and typed.
- No placeholder
undefined as anyclient code remains. - Core blockchain calls are covered by tests.
How to Validate
- Run targeted Soroban service tests.
- Perform manual test flow: create round, place bet, resolve.
PR Requirements
- PR title:
fix: wire real soroban bindings client with typed integration - Include
Closes #[issue_id]in PR description
Context Current README route tables include outdated paths and endpoint names that do not match implemented routes (for example auth, chat, education, rounds).
What Needs to Happen
- Reconcile README endpoint sections with route files.
- Ensure OpenAPI examples and operation summaries match real behavior.
- Add a lightweight docs verification checklist.
Files to Create/Modify
README.mdsrc/docs/openapi.tsdocs/openapi.json(regenerated)docs/postman-collection.json(regenerated)
Acceptance Criteria
- No stale endpoint names or paths in docs.
- Generated docs reflect current API contract.
How to Validate
- Run
npm run docs:openapiandnpm run docs:postman. - Spot-check a sample of endpoints from docs against running server.
PR Requirements
- PR title:
docs: align readme and openapi with implemented routes - Include
Closes #[issue_id]in PR description
Context Input validation is currently ad hoc and duplicated in routes, increasing inconsistency and missed edge cases.
What Needs to Happen
- Add a shared validation layer (for example Zod/Joi).
- Define schemas for auth, rounds, predictions, chat, and pagination query params.
- Standardize validation error shape.
Files to Create/Modify
src/middleware/(new validation middleware)src/routes/auth.routes.tssrc/routes/rounds.routes.tssrc/routes/predictions.routes.tssrc/routes/chat.routes.ts
Acceptance Criteria
- Major routes use centralized schema validation.
- Validation errors are consistent and documented.
How to Validate
- Add route tests for invalid payloads/types.
- Run
npm test.
PR Requirements
- PR title:
refactor: add centralized request schema validation - Include
Closes #[issue_id]in PR description
Context Current tests focus mainly on education and round service. Core auth, prediction, notification, and WebSocket paths lack meaningful automated coverage.
What Needs to Happen
- Add unit and route tests for auth challenge/connect and JWT guards.
- Add prediction route/service tests for success and failures.
- Add notification route/service tests including ownership checks.
- Add Socket.IO auth and room event tests.
Files to Create/Modify
src/tests/auth.routes.spec.ts(new)src/tests/prediction.service.spec.ts(new)src/tests/notifications.routes.spec.ts(new)src/tests/socket.spec.ts(new)
Acceptance Criteria
- Core user-critical flows are covered by automated tests.
- Regression risk for auth/prediction/socket paths is reduced.
How to Validate
- Run
npm test. - Confirm new suites pass consistently in CI/local.
PR Requirements
- PR title:
test: expand coverage for auth prediction notifications and sockets - Include
Closes #[issue_id]in PR description
Context Cron-driven behavior is difficult to reason about and currently under-tested. Round locking/resolution logic should be verified in time-driven scenarios.
What Needs to Happen
- Add scheduler tests using fake timers.
- Cover auto-lock and auto-resolve decision logic.
- Verify no duplicate processing and proper status transitions.
Files to Create/Modify
src/tests/scheduler.service.spec.ts(new)src/tests/round-scheduler.service.spec.ts(new)src/services/scheduler.service.ts(small testability hooks)src/services/round-scheduler.service.ts(small testability hooks)
Acceptance Criteria
- Scheduler behavior is deterministic under test.
- Time-based lifecycle transitions are covered.
How to Validate
- Run targeted scheduler tests and full
npm test.
PR Requirements
- PR title:
test: add deterministic coverage for cron schedulers - Include
Closes #[issue_id]in PR description
Context
Balances, pools, and payouts currently rely on Float values in Prisma/models, which can introduce rounding drift in financial calculations.
What Needs to Happen
- Migrate monetary fields to
Decimal(or integer minor units) in Prisma schema. - Update service calculations and serialization.
- Add tests to verify deterministic payout math.
Files to Create/Modify
prisma/schema.prismaprisma/migrations/(new migration)src/services/prediction.service.tssrc/services/resolution.service.tssrc/services/leaderboard.service.tssrc/tests/(new monetary precision tests)
Acceptance Criteria
- No float precision anomalies in balance/payout flows.
- Monetary calculations are deterministic across environments.
How to Validate
- Run migration and targeted payout tests with fractional edge cases.
- Verify balances reconcile after multi-round simulation.
PR Requirements
- PR title:
refactor: move monetary math to decimal-safe types - Include
Closes #[issue_id]in PR description
This project uses GitHub Actions for continuous integration and deployment. CI and CD are cleanly separated into two workflow files.
File: .github/workflows/ci.yml
CI runs automatically on every pull request and on pushes to main. It executes three independent jobs in parallel:
| Job | What it does |
|---|---|
| lint | Runs tsc --noEmit to check for type errors |
| build | Compiles TypeScript to dist/ via tsc |
| test | Spins up a PostgreSQL 16 service container, runs migrations, and executes the full test suite |
CI is fast, deterministic, and has no side effects. It is also used as a gate by the deployment workflow.
File: .github/workflows/deploy.yml
The deployment workflow calls CI as a prerequisite (reusable workflow) and only proceeds if all checks pass.
- Trigger: Automatic on push to
devorstagingbranches, or via manualworkflow_dispatch - Environment:
staging(configured in GitHub repository settings) - Process:
- CI suite runs and must pass
- Dependencies are installed and the project is built
- Database migrations run against the staging database
- Application is deployed to the staging environment
- Trigger: Push to
mainor manualworkflow_dispatchwithproductionselected - Environment:
production(configured in GitHub repository settings with required reviewers) - Approval Gate: Production deployments require manual approval through GitHub's environment protection rules. Configure this in Settings > Environments > production > Required reviewers.
- Process:
- CI suite runs and must pass
- A reviewer must approve the deployment in the GitHub Actions UI
- Dependencies are installed and the project is built
- Database migrations run against the production database
- Application is deployed to production
Both environments can be deployed manually via Actions > Deploy > Run workflow, selecting the target environment from the dropdown.
Each environment (staging, production) must have the following configured in GitHub Settings > Environments:
| Secret | Description |
|---|---|
DATABASE_URL |
PostgreSQL connection string for the target environment |
JWT_SECRET |
Strong random secret for JWT signing (must not be a placeholder) |
SOROBAN_CONTRACT_ID |
Deployed Soroban prediction market contract address |
SOROBAN_ADMIN_SECRET |
Stellar secret key for contract admin operations |
SOROBAN_ORACLE_SECRET |
Stellar secret key for oracle price settlement |
| Variable | Description | Example |
|---|---|---|
PORT |
Server listen port | 3000 |
CLIENT_URL |
CORS-allowed frontend origin | https://app.xelma.io |
SOROBAN_NETWORK |
Stellar network target | testnet or mainnet |
SOROBAN_RPC_URL |
Soroban RPC endpoint | https://soroban-testnet.stellar.org |
STAGING_URL |
Staging environment URL (display only) | https://staging.xelma.io |
PRODUCTION_URL |
Production environment URL (display only) | https://xelma.io |
- Go to your repository Settings > Environments
- Create
stagingandproductionenvironments - For
production, enable Required reviewers and add authorized approvers - Add all secrets and variables listed above to each environment
- Ensure no secrets contain placeholder values
If a deployment causes issues, use the following rollback process:
# 1. Identify the last known good commit
git log --oneline -10
# 2. Revert the problematic commit(s)
git revert <bad-commit-sha>
# 3. Push the revert (this triggers a new deployment)
git push origin main # for production
git push origin dev # for staging- Go to Actions > Deploy > Run workflow
- Select the target environment
- Optionally, create a branch from the known-good commit and push it to trigger deployment
If a migration caused the issue:
# Check migration status
npx prisma migrate status
# If needed, manually revert the migration in the target database
# Then redeploy the previous commitImportant: Always test rollbacks in staging before applying to production. Database migrations are not automatically reversed; plan migrations to be backward-compatible when possible.
The lightweight hackathon server (src/app.ts, default port 3001) applies per-IP throttling with express-rate-limit via src/middleware/rateLimiter.ts.
| Limiter | Scope | Window | Max requests |
|---|---|---|---|
apiRateLimiter |
All /api/* routes |
1 minute | 100 |
writeRateLimiter |
POST, PUT, PATCH, DELETE |
1 minute | 20 |
betRateLimiter |
POST /api/rounds/:id/bet |
1 minute | 5 |
When a client exceeds a limit, the API returns 429 with retry guidance:
{
"error": "Too Many Requests",
"message": "Too many bet submissions from this IP. Please wait before placing another bet.",
"retryAfter": 60
}The RateLimit-* and Retry-After response headers are also set (standardHeaders: true).
- Smart Contract: TevaLabs/Xelma-Blockchain
- TypeScript Bindings: @tevalabs/xelma-bindings
- Frontend: Coming soon
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
ISC
Built with ❤️ by the TevaLabs team on Stellar