Status: Draft (2026-10-26), code-based. Behavior was read from the repository code and design documents as of 2026-10-25. The count of "corrected records" in the production DB is the value read on 2026-10-18. Operational statistics such as user counts are not included.Writing rule: Measured data is the default; unmeasured items are marked as "Unmeasured", and items whose basis cannot be found are marked as "Unconfirmed".
Table of Contents
- 1. When a Match Ends
- 2. "Record This Match"
- 3. Record Screens
- 4. Live Sharing: The Server Recalculates Scores
- 5. Corrected Records: Only With Consent, and Still 0 Count
- 6. Footage Never Leaves the Device, and the Exception
- 7. Nicknames Instead of Real Names
- 8. What Tables Exist on the Server
- 9. Conclusion
- References
- Further Reading
1. When a Match Ends
A best-of-five match ended 3:1. The camera scoreboard shows a "Match Over" screen, and the game scores (11:7, 9:11, 11:5, 13:11) are displayed side by side. Where is this match right now?
It is only on the phone. As seen in Part 2, the camera scoreboard stores the entire match history in the browser's localStorage (ttplay:cam). Every entry in the history also includes its source. These are tap (screen-tapped score), key (remote control key), adj (score adjustment), and auto (accepted auto-judgment). The server knows nothing about this match.
In this state, there are only three paths chosen by the user to leave something on the server.
| Path | What Goes to the Server | Where It Stays |
|---|---|---|
| Record This Match | Game scores, match duration, opponent | Match records, ratings |
| Share Live (during match) | Score, undo, end events | Live match and event columns |
| Auto Score (during match) | Frames (not stored), records of human responses to chips | Corrected records (no video) |
2. "Record This Match"
What is Transferred
When clicking "Record This Match" in the "Match Over" sheet (backend/static/tt/play/lib/cam-sheets.mjs), the screen temporarily stores the match summary {best_of, games, source:'scorekeeper'} in sessionStorage and moves to the "My Records" screen (/tt/me#match). If not logged in, it goes through the login process. Selecting an opponent on the records screen and clicking "Record" sends the data to the server at that moment (POST /api/tt/matches).
What gets transferred is only the game score. Rally-by-rally history, who scored when and how, and traces of auto-judgments are not transferred. Certain cases cannot be recorded.
- Unfinished matches (no winner).
- Games raised manually via score adjustments. As seen in Part 2, since the scoreboard does not know what the score of this game was, it is excluded from records and left for humans to enter manually. Fabricated 11:0 scores must not enter the ratings.
Login and Display Names
Writing records requires a Google login. Guests only have read access. Write privileges act as a gateway similar to the site community.
There is an important design choice here: Google account names are never exported anywhere. Google names are usually real names, and if they were exposed as-is to match opponents or leaderboards, people met at the table tennis club would learn your real name. Therefore, no responses or screens in the table tennis section use Google names (backend/app/tt_records.py header "Google names are not exported"). Before setting a display name, players are listed as "Pong Player #ID", and to record matches, a display name (2–20 characters) must be set first. This is because matches appearing only with numbers on the opponent's screen make it impossible to recognize who recorded them.
The nickname field (up to 12 characters) in the camera scoreboard also bears the notice "Nicknames instead of real names".
Ratings Change Only After Opponent Confirmation
If ratings could be increased by solo-recorded wins, the leaderboard would simply become a table of who recorded the most. Therefore, matches against opponents with accounts enter Pending Confirmation, and the rating changes only once when the opponent clicks "Confirm", changing its status to Confirmed. Opponents can also raise an "Objection". If not confirmed within 7 days , it does not enter the ratings and remains only as a personal record. Expiration does not modify rows via batch jobs; instead, it is judged by the timestamp generated upon reading (measured using a single DB clock).
Matches against opponents without accounts (neighborhood friends) can also be recorded. In this case, the opponent's name is visible only to the person who recorded the match , and ratings do not change.
Elo Rating
Ratings use the Elo system derived from chess.
# backend/app/tt_records.py 에서 줄임
def k_factor(rating, rated_games):
if rated_games < 10: return 40 # 처음 10경기: 제자리를 빨리 찾게
if rating >= 2000: return 16
return 24
def expected(ra, rb): # A 가 이길 기대값
return 1 / (1 + 10 ** ((rb - ra) / 400))
# 변화량 = K × (실제 결과 − 기대값). 두 사람이 각자 자기 K 를 쓴다.
- Everyone starts at 1500.
- Ratings look only at match wins and losses. They do not distinguish between 3:0 and 3:2. Code comments note that "the side easier to explain was chosen".
- If the two people's K values differ (e.g., only one is in their first 10 matches), the sum of the rating changes is not 0, so the change amounts for both people are stored separately in the match row.
- The leaderboard displays only people who have turned their profile public and have 3 or more confirmed matches. Profiles are private by default.
For example, if both are at 1500 and within their first 10 matches, the expected value is 0.5, the winner gets 40 × 0.5 = +20, and the loser gets −20. With the same K of 40, if a 1600-rated player beats a 1500-rated player, the expected value is about 0.64, so it increases by only about +14.4, and dropping a match decreases it by about −25.6.
Daily Limits and Concurrency
| Limit | Value | Reason |
|---|---|---|
| Daily match records | 30 | Prevent bulk entry |
| Same opponent per day | 10 | Slow down rating inflation via collusive trading |
| Daily practice records | 20 | — |
| Match date | Within 1 year | Narrow the path of bulk-injecting old records to manufacture ratings |
| Single-game score upper limit | 50 | Filter out typos (e.g., 110:9) |
Limits are counted in the DB. To prevent two requests from the same person from being concurrently counted as the "29th" and passing together, people are lined up using DB advisory locks. When confirming, the match row is locked first, and the two player rows are locked starting from the one with the smaller ID . This is because if Alice and Bob simultaneously confirm different matches involving each other and lock "starting from my row", they would wait for each other's row and fall into a deadlock.
3. Record Screens
| Screen | What is Visible | Who |
|---|---|---|
My Records /tt/me |
My matches, pending/objections, ratings, practice log | Self |
Leaderboard /tt/rank |
Public player rankings, club/this-month filters, this week's risers | Everyone |
History /tt/me/history |
Aggregated changes in applications, matches, and scores; CSV download | Self |
Match Share /tt/m/{코드} |
A single shared match | Link recipients |
Historydoes not accumulate new records; instead, it reads and aggregates what each feature has already left behind (backend/app/tt_history.py). Match records, live scoreboard events (recalculated by the server), recording analysis results, auto-score correction records, and rating changes are mixed together in a single line. Since live scoreboards and match records are not linked by foreign keys, they are connected by estimation for matches with "identical end scores and close timestamps". Because misconnections can occur, the screen always displays the source of the flow (live or video). There are no user IDs in URLs, and all queries are bound only to the logged-in user, making it impossible to view another person's history. CSVs are exported with defenses against Excel formula injection.
Match Shareuses a specially generated 11-character code instead of a match ID. This is because match IDs are sequential numbers, and using them in URLs would allow browsing other people's matches one by one. Confirmed matches can have sharing enabled by either player, while personal records can only be shared by the person who recorded them. Matches under pending confirmation or objection are not shared because it hasn't even been determined whether the match "actually took place". On the share screen as well, players who have not made their profiles public appear as "Pong Player", and the names of opponents without accounts are never exposed.
4. Live Sharing: The Server Recalculates Scores
If "Share Live" is turned on during a match, people who receive the viewing link see the scores in real-time (backend/app/tt_watch.py).
- Sends events only. ). If the browser sends "currently 7:5", any score could theoretically be broadcast. Therefore, only score, undo, and end events are received, and the server determines the score by replaying the live events from the beginning according to the rules . The "state is not history" concept from Part 2 remains intact on the server as well. The rules used here are the third copy mentioned in Part 1 (
LiveBoard), bound through random cross-validation with the same case files and engine. - Sequence numbers. Each event is assigned an incrementing number starting from 1. Since a phone's screen might lock immediately after a score or its signal might drop, causing it to resend the same batch, receiving an already-received number with identical content is acknowledged silently, while different content or gaps in numbers returns 409 along with the server's last number. The screen stops sending when it receives a 409.
- Writes require owner + token. You must be logged into the account that created the live session, and a token received only once upon creation must be sent in the header . Not including it in the URL or body is intended to prevent it from remaining in logs and share links, and only the hash of the token is kept in the DB.
- Adjustments are translated into events. If an entry from long ago is cleared on the manual scoreboard, corresponding undos are sent to the live session and re-sent.
- Doubles matches are not sent(Part 2).
- If there are no events for 3 hours, the match is considered finished. Cheer emojis do not extend this time limit.
The naming rules are the same. Links distributed directly by the recorder display the nickname set for that match, but on the home feedviewed by anyone, owners who have not enabled public profiles are masked as "Pong Player", and opponents are masked as "Opponent Player".
5. Corrected Records: Only With Consent, and Still 0 Count
As mentioned in Part 3, when humans respond to auto-judgment chips ("Correct", corrected to another player, "Invalid"), those responses become corrected records. Let's follow where these go.
What It Contains
The auto-score screen creates one row for each rally responded to by a human and sends it to POST /api/tt/corrections . The row contains the following (backend/app/tt_corrections.py).
- Original judgment (winner side, reason, confidence, confirmation requirement) and corrected judgment.
- Event summary seen by the engine at that moment (16 items or fewer, counts, sides, types only).
- Signal-specific evidence (ball confidence, what judged it, conflicting signals).
- Training usage consent status.
. No video footage is included. The server does not insert what the browser sent as-is; instead, it regenerates only recognized structures. There is almost no space for text input, leaving nowhere for profanity, URLs, or scripts to accumulate. The row key includes the logged-in user's ID, preventing anyone from overwriting another person's row. Limits are 30 requests per minute per person, and 2,000 rows per day.
Judgments that passed automatically over time do not leave a correction record, because they were not verified by a human.
Who takes what
- Even actions without consent are logged. This is because it is a service metric that measures 「how much humans correct」.
- Only consented actions go to exports for training and evaluation. Operator-only exports (
GET /api/tt/corrections/export) carry masked values created with a newly drawn salt for each exportinstead of human numbers. Within a single file, the same person can be grouped, but files cannot be linked to each other or to our DB. - My records can be deleted at any time(
DELETE /api/tt/corrections/mine).
Never used for training so far
When reading the operational DB on 2026-10-18, the correction records were 0. Therefore, correction records have not yet been used for training or evaluation even once. All learning models in Part 5 were trained using public data and synthetic images.
There are plans. As seen in Part 5, when retraining the learning-based modules with commercially usable data, correction judgments from people who agreed to learning usage can become ingredients for modules like 「Verification Needed」 correction. This is because a 「human-corrected judgment」 is the correct answer for where the engine was wrong. However, for that to happen, enough correction records must accumulate, and reviewing retention periods and consent notices when using them as training materials comes first.
6. Video Does Not Leave the Device, and Exceptions
The primary principle of the camera scoreboard is camera video does not leave the device(backend/static/tt/play/cam.js Introduction). Recording is also synthesized inside the phone and saved on the phone. Text-to-speech for scores uses the browser's speech synthesis.
The exceptions are two features enabled by the user.
Automatic Scoring. Since the GPU finding the ball is on the server, frames are sent. Instead, the scope was narrowed (Part 1).
- Only JPEGs reduced to 512×288, up to 30 frames per second.
- Only for Google-logged-in members, and only within their time quota.
- Web relay only passes frames through, and workers keep only the 3-frame window seen by the ball model in memory. No frames are saved.
- Worker logs do not store video, ball coordinates, or personal information.
- Audio is not requested when opening the camera.
Upload Recording for Analysis. This is a separate feature where the user directly selects and uploads a video file. The browser first reduces it to 360p60 500kbps and uploads it, and the original is deleted after 30 days. Analysis workers work only through the web's internal API without access to the DB and storage keys.
7. Nicknames Instead of Real Names
To summarize the privacy design in one line: 「Preventing people met at the table tennis court from knowing my real name」 .
| Location | Displayed Name |
|---|---|
| Camera Scoreboard Screen | Nickname set for that match (up to 12 chars, 「Nicknames instead of real names」 guide) |
| Match History / Opponent Screen | Display name (2–20 chars). Before setting, 「Table Tennis Player #ID」 |
| Leaderboard / Public Profile | Only for those who enabled public visibility. Default is private |
| Opponent Without an Account | Only to the person who logged that match |
| Live Home Feed | Owner who didn't set public: 「Table Tennis Player」, Opponent: 「Opponent Player」 |
| Correction Record Export | No name, masked value changing with each export |
| Google Account Name | Never exposed anywhere |
The same principle applies to demo videos. Names embedded in the original screen (broadcast subtitles) are left as-is, but text we draw over them is used only as 「Player A/B」.
8. What Tables Exist on the Server
Here is a collection of record-related server-side tables. Each table has a DB migration number, and each migration introduced a feature.
| Migration | Table | What It Stores | What It Does Not Store |
|---|---|---|---|
| 0036 | tt_players |
Display name (2–20 characters, defaults to "Table Tennis Player #number"), rating, number of matches contributing to the rating, visibility (private by default) | Google Name |
| 0036 | tt_matches |
Match duration, game scores, source (manual input, score helper, recorded video analysis), status (pending confirmation, confirmed, disputed, expired, personal record), rating change for both players | Rally-by-rally history |
| 0036 | tt_practice |
Practice log (visible only to the user) | — |
| 0039 | tt_live_matches·tt_live_events |
Live match and event columns (numbered points, undo, end). Adds an 11-character code for match sharing to the tt_matches in the same migration |
The scores themselves (recalculated by the server from events), video |
| 0040 | tt_corrections |
Original ruling, corrected ruling, event summary, rationale, consent status | Video |
| 0048 | tt_videos |
Analysis queue and results for user-uploaded videos | — (originals are deleted after 30 days) |
A key takeaway from these tables is that "what you choose not to store is half of the design". Match records do not know about rallies, live matches do not store scores but only events, and corrected records do not include videos. Each table stores only what is strictly necessary for its feature, and the history screen reads and aggregates these tables without creating a new one (Section 3). What is not stored cannot leak either.
9. Conclusion
- Completed matches basically remain on your phone only . Only the records, live matches, and automatic scores chosen by the user are sent to the server.
- Matches transmit only game scores, and the opponent must confirm within 7 days for the Elo rating (K 40, 24, 16) to update. Daily limits and DB locking prevent bulk entries and concurrent requests.
- Live matches receive only events, which the server recalculates according to the rules, and write operations require the owner's account and a header token.
- Corrected records are saved without video, and only consented ones are exported with masked values. There are currently 0 cases, so they have never been used for training.
- Camera footage never leaves the device, and automatic scoring frames only pass through the server. Google real names never go anywhere.
A word from TT. Today's match is remembered by your phone first. The server is only told "what the score was," and your name is used only as a nickname. Since the opponent needs to say "that's correct" for your score to go up, please make sure to confirm on days you win.
References
- A. E. Elo. The Rating of Chessplayers, Past and Present. Arco, 1978. (Original bibliography for Elo rating. The original text was not opened this time; our version can be checked in code
backend/app/tt_records.py) - International Table Tennis Federation. ITTF Statutes 2026, Chapter 2: The Laws of Table Tennis, §2.12 (Matches and Games). https://db.ittf.com/document
- Code and Design: Repository
backend/app/tt_records.py·tt_history.py·tt_watch.py·tt_corrections.py·tt_live.pyREADME,backend/static/tt/play/cam.js·lib/cam-sheets.mjs·lib/camauto.mjs.
Further Reading
- This Series Part 1: Overall Architecture: A table of what leaves the device.
- This Series Part 2: Manual Scoreboard and Match Rules Engine: "History, Not State" and the adjustment column.
- This Series Part 3: Automatic Judgment: The answer of the chip where corrected records originate.
- This Series Part 5: Data, Licensing, and Training Pipeline: Conditions under which corrected records can become training material and principles for training data tailored to commercial services.