# MIDNIGHTAI Web Game Leaderboard API
This specification helps coding agents connect leaderboards to web games registered on MIDNIGHTAI. Games and the API share the origin `https://www.midnightai.net`. Never put API secrets in game source.
## CORS and allowed origins
- Register each game’s allowed Origins in the admin game editor, one per line.
- Defaults are `https://www.midnightai.net`, `https://midnightai.net`, `https://crazygames.com`, and `https://*.crazygames.com`.
- `https://*.crazygames.com` permits every HTTPS subdomain. The root domain is permitted separately by `https://crazygames.com`.
- `localhost`, `127.0.0.1`, and `::1` are automatically allowed over HTTP/HTTPS on every port for development.
- Local development Origins undergo start and submission validation, but scores are not saved to production leaderboards. Successful submissions include `testMode:true`, `saved:false`, and `rank:null`.
- Add `?gameId=GAME_SLUG` to POST URLs so CORS preflight can check game-specific settings.
- Browser CORS does not apply to native apps or server-to-server requests. An embedded WebView may send an Origin; add that Origin to the allowlist.
- CORS is browser access control, not authentication. Play tokens, rate limits, and score validation always apply.
## Core rules
- Use the English slug registered in the admin as `gameId`, for example `dokkaebi-fire`.
- Obtain a server-issued `playToken` when a run starts.
- A token lasts 30 minutes and permits one submission.
- Submit the score once at the end of the game.
- Minimum play time is 3 seconds. `durationMs` is rejected if it differs substantially from actual elapsed time between token issuance and submission.
- Do not ask users to enter nicknames. Unauthenticated players use a server-issued `guest-number`.
- Every MIDNIGHTAI game shares the same guest-account `localStorage` key.
- Scores are integers from 0 to 1,000,000,000.
- Start requests are limited to 10 per minute per IP; submissions to 6 per minute.
- Gameplay must remain possible even when the API fails.
## Registering a game during development
1. Register basic game information in the admin when development begins.
2. Save with public visibility disabled. Hidden games do not appear in the main game list.
3. Use the registered slug as the public `gameId` when integrating guest issuance and leaderboards.
4. Verify on localhost in test mode, which does not save production scores.
5. Once files, cover image, and description are complete, update the existing game.
6. Enable public visibility after final testing. Changing visibility does not change `gameId` or the leaderboard integration.
## 1. Shared player identity
### Purpose
All MIDNIGHTAI games in one browser must share the same user. Display only a safe public name; verify actual score ownership with a hard-to-guess secret token.
```json
{
"displayName": "guest-482193",
"playerToken": "추측할 수 없는 긴 무작위 키"
}
```
- `displayName`: public display name issued as `guest-` plus digits. Never use it as authentication.
- `playerToken`: a secret authentication key using sufficiently long cryptographic randomness.
- Store only a hash such as SHA-256 on the server, never the original token.
- Use internal `playerId`, not the display name, to manage ownership and best scores.
### Shared localStorage key
Every game must use this exact key:
```text
midnightai_player_identity_v1
```
Storage format:
```json
{
"displayName": "guest-482193",
"playerToken": "서버에서 발급한 원문 토큰",
"provider": "guest",
"version": 1
}
```
Do not create separate keys per game. Games under `/play/...` share `https://www.midnightai.net` and can read the same value.
Games registered as external links on other domains cannot directly share MIDNIGHTAI localStorage because of browser security rules. They need verified platform login or a separate account-linking process.
### Guest issuance contract
Server endpoint:
`POST /api/player/guest?gameId=dokkaebi-fire`
No request body. Successful response:
```json
{
"displayName": "guest-482193",
"playerToken": "64자 이상의 추측 불가능한 무작위 토큰",
"provider": "guest",
"version": 1
}
```
Client initialization:
```js
const PLAYER_STORAGE_KEY = 'midnightai_player_identity_v1';
async function getOrCreatePlayerIdentity() {
const saved = localStorage.getItem(PLAYER_STORAGE_KEY);
if (saved) {
try {
const identity = JSON.parse(saved);
if (identity.version === 1 && identity.displayName && identity.playerToken) {
return identity;
}
} catch {}
localStorage.removeItem(PLAYER_STORAGE_KEY);
}
const response = await fetch(`/api/player/guest?gameId=${encodeURIComponent(GAME_ID)}`, {method: 'POST'});
if (!response.ok) throw new Error('guest_issue_failed');
const identity = await response.json();
localStorage.setItem(PLAYER_STORAGE_KEY, JSON.stringify(identity));
return identity;
}
```
Clearing browser storage creates a new guest number. Recovery on another device is not guaranteed at this stage.
### Platform-authenticated players
For login platforms such as CrazyGames, the server verifies an authentication token from the platform SDK before linking `provider` and `providerUserId`. Never trust only a platform ID string sent by the game. Use the platform display name only after verification; otherwise use the shared guest account.
## 2. Starting a run
`POST /api/ranking?gameId=dokkaebi-fire`
```json
{"action":"start","gameId":"dokkaebi-fire","playerToken":"공통 플레이어 토큰"}
```
Successful `200` response:
```json
{"playToken":"...","expiresAt":"2026-07-13T12:30:00.000Z","minPlayMs":3000}
```
Call when the player starts the game. Keep the returned token and `performance.now()` value only in memory. Never hardcode tokens or put them in URLs.
```js
async function startRankedRun(gameId) {
const response = await fetch(`/api/ranking?gameId=${encodeURIComponent(gameId)}`, {
method: 'POST', headers: {'content-type': 'application/json'},
body: JSON.stringify({action: 'start', gameId, playerToken: identity.playerToken})
});
if (!response.ok) throw new Error('ranking_start_failed');
const data = await response.json();
return {playToken: data.playToken, startedAt: performance.now()};
}
```
## 3. Submitting a score
`POST /api/ranking?gameId=dokkaebi-fire`
```json
{
"action":"submit",
"gameId":"dokkaebi-fire",
"playToken":"start 응답의 토큰",
"playerToken":"공통 플레이어 토큰",
"score":12500,
"durationMs":83420,
"submissionId":"crypto.randomUUID()"
}
```
Successful `200` response:
```json
{"ok":true,"scoreId":123,"rank":7}
```
```js
async function submitRankedScore({gameId, run, identity, score}) {
const response = await fetch(`/api/ranking?gameId=${encodeURIComponent(gameId)}`, {
method: 'POST', headers: {'content-type': 'application/json'},
body: JSON.stringify({
action: 'submit', gameId, playToken: run.playToken,
playerToken: identity.playerToken,
score: Math.max(0, Math.floor(score)),
durationMs: Math.floor(performance.now() - run.startedAt),
submissionId: crypto.randomUUID()
})
});
const data = await response.json();
if (!response.ok) throw new Error(data.error || 'ranking_submit_failed');
return data;
}
```
## 4. Reading leaderboards
`GET /api/ranking?gameId=dokkaebi-fire&scope=all&limit=20`
- `scope`: `all`, `weekly`, `daily`
- `limit`: 1–100
- Each `playerId` appears once, with its best score during the period.
```json
{"gameId":"dokkaebi-fire","scope":"all","entries":[{"displayName":"guest-482193","score":12500,"achieved_at":"2026-07-13T12:00:00.000Z","rank":1}]}
```
## Error codes
- `invalid_game_id`, `game_not_found`: invalid game identifier
- `origin_not_allowed`: web Origin not allowed by the admin
- `rate_limited`: request limit exceeded; retry after `retryAfter` seconds
- `invalid_player_token`: missing or invalid player key
- `invalid_score`, `invalid_duration`: invalid submission values
- `invalid_or_expired_token`, `token_already_used`: expired, mismatched, or reused token
- `play_time_mismatch`: client play duration differs from server elapsed time
- `duplicate_submission`: repeated `submissionId`
## Coding-agent implementation checklist
1. Declare only the public `GAME_ID` as a game constant.
2. Read shared identity from `midnightai_player_identity_v1`; call guest issuance if absent.
3. Call the start endpoint with `playerToken` immediately before gameplay begins.
4. Fall back to ordinary, unranked gameplay if starting fails.
5. Display `displayName`; do not request a nickname.
6. Include both `playerToken` and `playToken` in score submissions.
7. Disable submission after one press to prevent duplicate requests.
8. Show the successful response’s `rank`.
9. Provide all-time, weekly, and daily leaderboard tabs.
10. Never expose `playerToken` in URLs, logs, or screens outside its designated localStorage key.
11. Normalize score calculations to integers.
12. Do not repeatedly submit fake scores to production, even during development.
## Privacy processing and notice requirements
Every coding agent integrating leaderboards must also apply these general privacy rules. Refer to `https://www.midnightai.net/privacy-collection` and `https://www.midnightai.net/privacy` for operating-site notices. If actual processing differs for a game, give more specific notice where collection occurs and obtain any required separate consent.
### Purposes and information
Process only the minimum information needed:
| Purpose | Information | Baseline retention |
| --- | --- | --- |
| Guest identification and a shared user across games | `displayName`, internal `playerId`, hashed `playerToken` | While the service operates or until a deletion request is completed |
| Score storage and leaderboards | `gameId`, score, play duration, creation and update timestamps | While leaderboards operate or until a deletion request is completed |
| Preventing duplication, manipulation, and abuse | Hashed `playToken`, `submissionId`, hashed IP address, request times, and error records | Minimum period needed for security |
| Inquiries and rights requests | Email address and inquiry provided by the user | Minimum period needed after resolution |
- Because `displayName` may be public, do not use real names, email addresses, or phone numbers.
- Never store original `playerToken`, IP addresses, or platform authentication tokens in the DB or ordinary logs. Verify or one-way hash them on the server.
- Do not collect passwords, national identification numbers, payment information, precise location, contacts, advertising IDs, or other information unnecessary for leaderboards.
- If analytics or ad SDKs process separate information, do not bundle it into leaderboard consent. Apply the advertising/cookie notice and SDK policies separately.
### Required game UI notice
Before enabling leaderboards for the first time or sending personal information to the server, display this information somewhere easy to find:
```text
랭킹 등록을 위해 게스트 식별정보, 게임 식별자, 점수, 플레이 시간 및
부정 이용 방지 기록이 처리됩니다. 자세한 내용은 개인정보 수집·이용 안내를 확인하세요.
```
For English interfaces, the same notice reads: “Guest identity, game ID, score, play duration, and abuse-prevention records are processed to register leaderboard scores. See the Information Collection and Use notice for details.”
Link the Information Collection and Use notice in a new window or in-game modal to:
```text
https://www.midnightai.net/privacy-collection
```
- Refusing required processing restricts only leaderboard registration and shared guest identification. Continue ordinary gameplay where possible.
- If adding information requiring separate legal consent, do not preselect the checkbox. State purpose, information, retention period, the right to refuse, and affected features together.
- Do not expand collection from users under 14 to information requiring guardian consent. Design a separate consent and verification process first if needed.
### Client and server restrictions
1. Store original `playerToken` only under `midnightai_player_identity_v1`, never in screens, URLs, analytics events, errors, or `console.log`.
2. Keep `playToken` only in memory during that run; discard after submission or expiry.
3. Do not add IP addresses, platform tokens, inquiry information, or other personal data to localStorage alongside the identity object.
4. Public API responses contain only necessary `displayName`, score, rank, and achievement time. Do not return internal `playerId`, token hashes, IP hashes, or abuse records.
5. Mask request bodies and authentication headers in server logs. Prevent player and platform tokens from reaching observability tools.
6. Do not save development test scores in production. Do not copy production data containing real personal information into local tests.
7. CORS is not a substitute for privacy protection or authentication. Validate tokens, rate limits, and inputs on every write request.
8. When adding information or purposes, update this document, the collection notice, and Privacy Policy together with code.
### Deletion, correction, and browser reset
- Provide `midnightai@midnightai.net` for access, correction, deletion, or restriction requests.
- For deletion, remove or irreversibly anonymize unnecessary records linked to the `playerId`, not only public scores. Keep minimum legally or security-required records separate from public leaderboards.
- Explain that clearing browser storage removes the original token and may prevent automatic guest-account recovery.
- If providing a guest-reset control, confirm that the user may lose the connection to existing leaderboard records before execution.
### Completion criteria for coding agents
Leaderboard integration is complete only when:
- A privacy notice link is visible on the game’s leaderboard or settings screen.
- Tokens and internal identifiers do not appear in URLs, screens, logs, or analytics.
- Public leaderboards display only necessary information.
- Ordinary gameplay remains possible where feasible if the API fails or processing is refused.
- New information collected is reflected in both notices and UI.
> Existing records from legacy nickname integrations remain accepted for compatibility. Newly integrated or modified games must use `/api/player/guest` and `playerToken`.
Leaderboard API
Integration Specification
Includes game IDs, shared guest identity, one-time play tokens, score submission, and privacy requirements.
Open Markdown for coding agents ↗