English Tiếng Việt
Backlog · FLYER Test / ranking.flyer.us

Monthly Leaderboard Prize: standardised ranking and teacher-verified winners

Every month the top student in each of Starters, Movers, Flyers, KET and PET on ranking.flyer.us wins a bicycle. Part A is the lean version: a small monthly event run by hand from September 2026 with no development. Part B is the audit of today's leaderboard and the full design to build if the event is kept after the pilot.

Sprint
Backlog
Status
Part A ready to run · Part B after pilot
Priority
2, Medium
Updated
07/09/2026
Small monthly event first, system later
Draft for reviewSections 3 and 4 come from a direct read of the webs-ranking, exam and flyer-go repositories, so the “what exists today” claims can be checked against file paths. Sections 5 onward are a design proposal. Anything not yet decided is in Open questions, not silently assumed.

ALean version: a small monthly event, run by hand from September 2026

The decision is to start now with what exists, not to wait for a system. Nothing in Part A needs a developer. It needs a one-page rule text, a printable teacher letter, one CS owner, a Google Sheet and a budget line. The public board stays exactly as it is; FLYER adds the prize, the eligibility check and the announcement around it.

What runsThree pilot months: September, October and November 2026. Five bicycles a month, one per level, decided from the Monthly tab of ranking.flyer.us as it stands at 23:59 on the last day. The winner must be a real, named child in the level's age band, confirmed by a signed letter from their English teacher. Reserves are ranks 2 and 3. After three months the team decides whether to keep the event and, if so, whether to build Part B.
Needs no dev
Standings
Monthly tab, per level, screenshot by CS at 23:50 on the last day and again at 08:00 on the 1st (the view refreshes hourly).
Needs no dev
Eligibility
Checked by hand from the profile in admin (account type, birthday if present) and from the teacher letter.
Needs no dev
Claim
CS contacts the top three per level on Zalo, sends the letter template, receives a photo of the signed page.
Needs no dev
Record
One Google Sheet, one row per level per month: standings screenshot, claimant, letter, teacher call, decision, delivery.
Needs marketing
Announce
Rules post on the 1st of September, winner post on the 12th of each month, delivery photo after.

A1One-page rules, as published

This is the text a parent reads. It is deliberately short. Anything not here is decided by FLYER and explained to the family concerned.

RuleText
PrizeEvery month, the student ranked first on the Monthly tab of ranking.flyer.us in each of Starters, Movers, Flyers, KET and PET receives one bicycle. Five bicycles a month.
WhenStandings are taken at 23:59 on the last day of the month, Vietnam time. FLYER keeps a screenshot as the record. Tests submitted after that belong to the next month.
Who can winA student using their own personal account. Class, centre, teacher and family-shared accounts cannot win. The student's age on the last day of the month must be inside the level's band: Starters 6 to 8, Movers 8 to 10, Flyers 10 to 12, KET 11 to 14, PET 13 to 16.
Teacher letterWithin 7 days of being contacted, the family sends a photo of the confirmation letter signed by the student's current English teacher (school or centre), stating the student's name, date of birth, school and class, that they are in the age band, and that they sat the tests themselves. FLYER may call the teacher.
ReservesIf the first-ranked student cannot be reached, does not send the letter in time, or is not eligible, the prize passes to rank 2, then rank 3.
LimitsOne bicycle per student per month (if first in two levels, the higher level counts). One bicycle per student per calendar year.
Fair playFLYER may set aside a result that shows signs of abuse, for example several accounts on one device, more than 20 full tests in a day, or a birthday changed during the month, and pass the prize to the next reserve.
AnnouncementWinners are announced on the 12th of the following month on FLYER's Zalo and Facebook, with first name, school and province. Delivery within two weeks of the announcement.
ExclusionsFLYER staff, partner and school administrator accounts, and demonstration accounts are not eligible.
Why the running sum is accepted for the pilotThe board's current score rewards volume (Part B, section 4). For a three-month pilot that is tolerable: the teacher letter removes class and centre accounts, which are the worst distortion on today's board, and the fair-play clause covers the extreme cases. If two of the three pilot winners at a level turn out to be students sitting several hundred tests, switch to the best-ten query in Part B, section 9, which one person can run on the replica on the 1st without any product change.

A2The monthly routine for CS

One owner, about one working day per month. Times are Vietnam time.

WhenTaskTime
Last day, 23:50Open ranking.flyer.us, Monthly tab, each of the five levels. Screenshot the top 5 of each. Save to the Drive folder for the month.15 min
1st, 08:00 to 09:00Screenshot again (the hourly refresh may have moved a late submission). The 08:00 screenshot is the record if the two differ. Fill the Sheet: level, rank 1 to 3, display name, score, profile id.30 min
1stIn admin, open each of the 15 profiles: personal account or class account, birthday on file, school, parent phone. Note obvious class or centre names. Mark provisional winner and reserves.45 min
1st to 2ndZalo or call the parent of each provisional winner (message A3.1). Send the letter template as PDF. Tell reserves they are reserves (message A3.2).1 h
2nd to 8thReceive letters. Check name and date of birth against the profile and the age band. Call the teacher on the number given (script A3.3). Log the call in the Sheet.2 h
8th to 10thDecide: winner confirmed, or passed to reserve with a reason. Product lead signs off in the Sheet. Ask the winner's family for address, bicycle size, and consent to publish first name, school and photo.45 min
12thMarketing posts the five winners (message A3.4). CS orders the bicycles.30 min
By the 26thDelivery. Family sends a photo; posted with consent. Sheet row closed.30 min

The Sheet

One tab per month, one row per level and rank (15 rows). Columns: level, rank, screenshot link, display name, profile id, account type, birthday on file, age at month end, in band (yes/no), parent name, parent phone, contacted on, letter received on, letter link, teacher name, teacher phone, teacher called on, call outcome, decision, decided by, address, bicycle size, consent, ordered on, delivered on, photo link, notes.

The letter

A printable one-page bilingual form is at roadmap.flyer.vn/ranking-prize-letter. CS exports it to PDF once and sends the same file to every family. A phone photo of the signed page is accepted. Fields: student name, date of birth, school, grade and class, level and month, three statements (age band, learning at this level, sat the tests personally), teacher name, role, school or centre, phone, signature and date, parent name, phone, signature and consent to publish.

A3Message templates

A3.1 To the provisional winner's parent

Hello, this is FLYER. Congratulations: [student name] finished first in [level] on the FLYER monthly leaderboard for [month] and is the provisional winner of this month's bicycle. To confirm the prize, we need one thing within 7 days: a short confirmation letter signed by [student name]'s current English teacher (school or centre). The form is attached; a photo of the signed page sent here is enough. We may call the teacher to confirm. Please also tell us the student's date of birth so we can check the age band for [level] ([band]). Thank you!

A3.2 To a reserve

Hello, this is FLYER. [Student name] finished [rank] in [level] on the FLYER monthly leaderboard for [month] and is a reserve for this month's bicycle. If the first-ranked student's prize is not confirmed by the 10th, we will contact you and you will have 5 days to send a teacher confirmation letter. You can prepare it now with the attached form. Thank you!

A3.3 Call script for the teacher

Hello, I am [name] from FLYER. A student of yours, [student name], has won a prize on our monthly English leaderboard. We received a confirmation letter with your name and number. May I confirm three things: that [student name] is in your class this year, that the student is [age] years old and in grade [grade], and that you signed the letter? Thank you very much, that is all we need.

A3.4 Winner announcement (Zalo, Facebook)

FLYER Monthly Leaderboard, [month] winners. Five students, five bicycles: Starters, [first name], [school], [province]. Movers, [first name], [school], [province]. Flyers, [first name], [school], [province]. KET, [first name], [school], [province]. PET, [first name], [school], [province]. Congratulations! The [next month] race is already on at ranking.flyer.us. Rules: [link].

A4Risks handled by hand, and when to build

RiskHow the pilot handles it
Rank 1 is a class or centre account (as on the board today)Not eligible under “own personal account”. The prize passes to the reserve. CS can usually tell from the display name; the teacher letter settles the rest.
The winner is a student who sat several hundred testsAccepted for the pilot if they are a real child in the band; the fair-play clause covers extreme daily volumes. Two such winners at one level trigger the best-ten query for the next month.
No birthday on the profile (58% of August racers)The letter carries the date of birth and the teacher confirms it. Ask the family to add it to the profile as part of the claim.
Same student first in two levelsHigher level counts; the lower level's prize passes to its reserve.
The board moved between 23:59 and the 08:00 screenshotThe 08:00 screenshot on the 1st is the record, because it contains every test submitted before midnight. Say so in the rules post.
Dispute from a familyReply with the screenshot and the rule text. The rules say FLYER's decision is final.
Teacher unreachableTwo attempts on two days, then the school's office; if still nothing by the 10th, pass to the reserve and tell the family why.

When to build Part B

  • The event is kept after November and the team wants it to run without one CS day per month.
  • Disputes about the score itself, not about eligibility, appear more than once.
  • The board's number one is repeatedly a grinder rather than a strong student; then start with Part B section 9 (versioned formula with the championship test), which is the smallest piece with the largest effect.

The five backlog items on the board are ordered for that moment and stay parked until then.


Part B · Audit and full design, for after the pilot

1Context and problem

FLYER wants a recurring, visible reward on the public leaderboard: each month, the student with the top score in Starters, Movers, Flyers, KET and PET receives a bicycle. The request came with one condition: a winner must present a letter from their English teacher confirming they are within the Cambridge age range for the level they won.

The leaderboard already exists and is well used. It has Daily, Weekly, Monthly and Yearly tabs, a level selector, and student and school tables. What it does not have is any of the properties a physical prize needs: a formula that is written down and versioned, eligibility rules, a way to tell a real child from an anonymous or duplicate profile, an age on the profile, and any tooling to run a claim and verification process.

The risk of skipping this work is concrete. A bicycle is worth enough that families will optimise for it. Today a student can be ranked from an anonymous profile, one user can hold several profiles, staff and demo accounts are not excluded, and the score is a running sum that rewards volume over quality. The first month of prizes would produce disputes that the team cannot adjudicate, because the rule that produced the number is not in any repository.

Design positionKeep the public leaderboard as the single source of truth for the prize, and make that leaderboard defensible. Do not build a separate contest ranking that shows a different number from the board. Add a versioned formula, explicit eligibility, an age band per level, and a claim and verification flow around the Monthly tab. The teacher letter is the human check that catches what the data cannot.

2Goals and success metrics

Goals

  • Turn the Monthly tab into a monthly race with a prize students and parents understand and trust.
  • Raise the number of students who complete full mock tests every month at the five Cambridge levels.
  • Get a verified date of birth onto student profiles, which every future age-banded feature will need.
  • Establish a repeatable monthly operating cycle: freeze, claim, verify, announce, deliver.

Success metrics

MetricDefinitionType
Active racers per levelDistinct eligible students with at least one counted test in the month, per levelPrimary
Tests per racerMedian counted tests per active racer in the monthPrimary
Profiles with verified DOBShare of active student profiles with a date of birth entered and lockedPrimary, the lasting asset
Claim completionShare of provisional winners who upload a valid letter within the claim windowGuardrail
Verification turnaroundDays from freeze to announcement, target 12Guardrail
DisputesComplaints per cycle that require a rule interpretationGuardrail, target zero after month 2
RetentionShare of month N racers still active in month N+1The real test of whether a prize changes behaviour
Tests per student in August 2026, by levelShare of active students by number of distinct tests sat in the month. About one in five already reaches ten.
Tests per student in August 2026, by level0%25%50%75%100%Starters35%Starters · 1 test: 4,181 (35.0%)30%Starters · 2 to 4: 3,553 (29.7%)16%Starters · 5 to 9: 1,899 (15.9%)19%Starters · 10 or more: 2,317 (19.4%)Movers35%Movers · 1 test: 3,463 (34.8%)29%Movers · 2 to 4: 2,881 (28.9%)16%Movers · 5 to 9: 1,615 (16.2%)20%Movers · 10 or more: 1,995 (20.0%)Flyers37%Flyers · 1 test: 3,484 (37.4%)28%Flyers · 2 to 4: 2,635 (28.3%)15%Flyers · 5 to 9: 1,392 (14.9%)19%Flyers · 10 or more: 1,814 (19.5%)KET37%KET · 1 test: 2,227 (36.6%)29%KET · 2 to 4: 1,775 (29.2%)18%KET · 5 to 9: 1,085 (17.9%)16%KET · 10 or more: 990 (16.3%)PET41%PET · 1 test: 1,752 (41.1%)32%PET · 2 to 4: 1,344 (31.5%)16%PET · 5 to 9: 673 (15.8%)12%PET · 10 or more: 496 (11.6%)
1 test2 to 45 to 910 or more
View data
LevelActive students1 test2 to 45 to 910 or more
Starters11,9504,1813,5531,8992,317
Movers9,9543,4632,8811,6151,995
Flyers9,3253,4842,6351,3921,814
KET6,0772,2271,7751,085990
PET4,2651,7521,344673496
Source: FLYER read replica, full mock tests (type exam) submitted 1 to 31 August 2026, Vietnam time.
Baseline, August 2026 (read replica)29,200 students sat at least one full mock test at the five prize levels. At each level roughly one in five sat ten or more distinct tests. Only 42% have a birthday on file, and 17% raced from an anonymous profile. Targets for the first cycle should be set against these numbers; the query is reproducible from section 14.

3How the leaderboard works today

Read from the source, not assumed. The live app is flyer/webs-ranking (Next.js, last commit 27/08/2026) served at ranking.flyer.us; the older ranking-web repository is dead since October 2024. The ranking API is GraphQL v2 in flyer/exam.

The board today
ranking.flyer.us, Monthly tab, Starters

Monthly tab, Starters, 7 September 2026. Rank 1 is a class account, not a child, with 770 points a week into the month.

ranking.flyer.us, Daily tab, Starters

Daily tab, Starters, same day. 106 points in one day means eleven full tests before dinner.

AspectCurrent behaviourWhere
Time windowsDaily, Weekly, Monthly, Yearly. No all-time. Default tab is Daily.RankFrequency enum, pages/index.tsx
LevelsA dropdown of test levels of type CLASSIC: Starters (default), Movers, Flyers, KET, PET, plus IOE, SET, TOEFL Primary and Junior. IELTS is its own type and does not appear.TestLevelsWithFilter
TablesStudent table and School table. Search box switches to challenge code (digits), class code (CL…) or a name search.TableRanking
Score shownThe grade column: a sum of per-test grades normalised to 10 points each, over the window. Not a best score and not an average. A student who takes 40 tests outranks a student who takes 10 perfect ones.learner_test_level_ranks
Where the formula livesA materialized view created by hand on the production database. It is declared in Go only as a column list. No CREATE VIEW exists in any repository, so the exact tie-break cannot be read from git.ent/schema/views.go, module/mview
RefreshHourly via Temporal, Asia/Ho_Chi_Minh. A submitted test can take up to an hour to move the board.module/mview/mview.go
Which attempts countFull mock tests only (type_attempt = 'exam'), submitted, grade above zero. Practice, speaking, writing and placement attempts do not count. Teacher-assigned and self-served tests count the same. No sample or paid filter.sibling views in flyer-go
Tie-breakBy analogy with the versioned monthly recap view: score descending, then total execution time ascending. Unverified for the leaderboard view itself.0092_user_score_views.up.sql
FiltersProvince, school, class code, challenge code, display name. No age, grade or birth-year filter exists on the API.LearnerTestLevelRankFilter
Identity shownRank, avatar and frame, display name, province, school, score. Clicking any row opens a public profile modal with coins, total points, study time, completed tests, accuracy, achievements and full attempt history.ModalInfoProfileRanking
Date of birthFetched in the ranking fragment and never rendered. On the profile it is nullable, unvalidated and never read by any ranking, recap or badge job.learner_profiles.date_of_birth
EligibilityNone. Viewing needs no login. Anonymous profiles (no user id) rank. One user can hold several profiles and each ranks separately. No staff or test-account exclusion. Demo students have their attempt dates shifted forward hourly so sample classes look active, and they surface in Daily and Weekly rankings.AuthProvider, IsAnonymous, update_attempt_dates_by_students
Rewards todayOne badge, TOURNAMENT_STAR, granted every Monday 01:00 to every learner holding rank 1 in any level for the week. Wisdom-stone gift store and giveaway campaigns exist but are unrelated to rank. No prize system, no admin screen for prizes.AchievementWeeklyRankSt
Known UI defectMedal icons for the top three are keyed off the row index, not the server rank. With any filter applied, the first three rows of the filtered list get gold, silver and bronze even if their real rank is 5,000.TableRanking/index.tsx
ConsequenceThe Monthly tab is already the right surface for the prize. What it lacks is a formula anyone can defend, a definition of who is allowed to win, and a way to prove a winner's age. Those three are the work.

4What blocks a monthly prize

#GapWhy it matters for a bicycle
1Formula not in version controlAny dispute ends with “the database says so”. The team cannot show a parent the rule, cannot test it on dev (dev and prod already drift on the sibling monthly view), and cannot change it with a review trail.
2Volume beats qualityA running sum means the winner is whoever sits the most tests. In August 2026 the number one at KET submitted 849 attempts and the number one at Movers sat 602 distinct tests, roughly twenty a day. That is neither the behaviour FLYER wants nor a result Academic can stand behind.
3No eligibilityAnonymous profiles, duplicate profiles, staff, school demo accounts and artificially refreshed sample students are all rankable. At least one of these will be in a top three in month one.
4No age on recordThe condition “right Cambridge age for the level” cannot be checked, because date of birth is optional and unvalidated, and nothing in the API can filter on it.
5No claim or verification toolingThere is nowhere for a winner to upload a letter, nowhere for CS to approve it, and no record of who won, when, and on what evidence.
6Hourly lag and index-based medalsSmall on their own, but on the last evening of the month a student who sees themselves in first place at 23:30 and third at 00:30 will not accept “the view had not refreshed”.
What it took to be number one in August 2026Attempts and distinct tests by the student ranked first under the current running-sum formula, per level.
What it took to be number one in August 202602004006008001,000822Starters · Attempts: 822125Starters · Distinct tests: 125Starters680Movers · Attempts: 680602Movers · Distinct tests: 602Movers444Flyers · Attempts: 444353Flyers · Distinct tests: 353Flyers849KET · Attempts: 849549KET · Distinct tests: 549KET311PET · Attempts: 311204PET · Distinct tests: 204PET
AttemptsDistinct tests
View data
LevelAttemptsDistinct tests
Starters822125
Movers680602
Flyers444353
KET849549
PET311204
Source: FLYER read replica, full mock tests (type exam) submitted 1 to 31 August 2026, Vietnam time.

5The standard: programme rules

These are the rules as a parent should be able to read them on a public rules page. Everything in sections 9 to 14 exists to make these rules true. The numbers quoted come from the August 2026 read-replica baseline in section 2.

5.1 The prize

  • Five prizes a month, one per level: Starters, Movers, Flyers, KET, PET. Each prize is one bicycle.
  • The winner of a level is the eligible student ranked first on the Monthly tab for that level at the moment the month is frozen.
  • Two runners-up per level are held as reserves. If the winner does not complete the claim or fails verification, the prize passes to the next reserve.
  • A student can win at most one bicycle per month (if first in two levels, the higher level counts and the lower passes to its reserve) and at most one bicycle per calendar year.

5.2 The score

Monthly score, per level. Best-ten score plus championship score. Both parts are shown on the board.max 200
Best-ten score. Take the student's ten best distinct tests of that level submitted in the month. For each test, take the best attempt and normalise it to 10 points. Sum them.max 100
Championship score. One fresh full mock test per level each month, written by Academic and unpublished until its window: the last weekend of the month, Saturday 08:00 to Sunday 22:00 Vietnam time. One attempt, one sitting, normal timing. Normalised to 100.max 100
Fewer than ten tests. Missing slots count as zero. A student with nine perfect tests scores 90 and is outranked by a student with ten tests averaging 9.1.0 per slot
Counted attempts. Full mock tests only, submitted inside the month in Vietnam time. Practice drills, speaking, writing and placement tests do not count. Teacher-assigned and self-chosen tests count equally.exam only
Retakes. Only the best attempt on a given test counts, so repeating one test never adds points. Breadth across ten different tests is what scores.best of
Students with a perfect best-10 score, August 2026Under a plain best-ten rule these students all score 100. A tie-break, not the score, would choose the bicycle.
Students with a perfect best-10 score, August 20260200400600800641Starters: 641Starters410Movers: 410Movers372Flyers: 372Flyers163KET: 163KET61PET: 61PET
View data
LevelStudents at 100 / 100
Starters641
Movers410
Flyers372
KET163
PET61
Source: FLYER read replica, full mock tests (type exam) submitted 1 to 31 August 2026, Vietnam time.

5.3 Tie-break, in order

  1. Lower time on the championship test, capped at the level's allotted duration.
  2. More distinct tests completed in the month at that level.
  3. Lower total time on the ten counted tests, with each test's time capped at the level's allotted duration so an open tab never counts.
  4. Earlier moment at which the final score was reached.
Why best-ten plus a championship test, and not the running sumTen full tests at Starters is roughly four to five hours of work, enough to reward consistency and beyond the reach of a lucky afternoon, while the cap stops the race becoming a volume contest that a child cannot win against a family with more time. The cap alone is not enough: the chart above shows 641 Starters students would have scored a perfect 100 in August 2026, so a best-ten rule on its own hands the bicycle to a tie-break. One controlled sitting on content nobody has seen separates them in a way Academic can defend, and it is the strongest anti-cheat measure in the design, because a family cannot rehearse a test that does not exist yet.

5.4 Eligibility

  • A registered student account (not an anonymous profile) with a parent phone number verified.
  • A date of birth entered on the profile before the 20th of the month, and locked once entered; later corrections go through support with the same letter.
  • The student's age on the last day of the month falls inside the level's age band (section 6).
  • Not on the exclusion list: FLYER staff and their test accounts, school demo and sample accounts, partner admin accounts, and any profile flagged for integrity review in the last six months.
  • Where one user owns several student profiles, only the profile designated as primary is eligible.

5.5 Claiming

  • Provisional winners and reserves are notified on the 1st. They have seven days to submit the teacher confirmation letter (section 7) and a delivery address.
  • FLYER verifies within three working days and may call the teacher or the school.
  • Winners are announced on the 12th; delivery follows within two weeks; a delivery photo is posted on the winner wall with the family's consent.
  • FLYER's decision on eligibility and integrity is final; the rule text and the score breakdown are shown to the claimant so the decision is explainable.

6Age bands per level

Cambridge publishes the Young Learners suite (Pre A1 Starters, A1 Movers, A2 Flyers) for children aged roughly 6 to 12, with each paper pitched at a typical band inside that range. For A2 Key for Schools and B1 Preliminary for Schools it publishes no fixed age, only that the papers are written for school-aged learners. The bands below are FLYER's operational bands, aligned to that guidance and to Vietnamese school grades. Age is counted in completed years on the last day of the prize month.

LevelCambridge guidanceFLYER band (age at month end)Typical VN gradeBorn in, for a September 2026 race
Starters (Pre A1)YLE, children 6 to 12; Starters typically 7 to 86 to 8Grade 1 to 32018 to 2020
Movers (A1)Typically 8 to 118 to 10Grade 3 to 52016 to 2018
Flyers (A2)Typically 9 to 1210 to 12Grade 5 to 72014 to 2016
KET (A2 Key for Schools)School-aged; commonly lower secondary11 to 14Grade 6 to 92012 to 2015
PET (B1 Preliminary for Schools)School-aged; commonly upper secondary13 to 16Grade 8 to 112010 to 2013
  • Bands overlap on purpose. An eight-year-old may race in both Starters and Movers, and a thirteen-year-old in both KET and PET; the one-prize-per-month rule handles the double win.
  • The band applies both ways. A student older than the band cannot win that level even with the top score; a student younger than the band is also not eligible for that level. Whether to admit younger “prodigies” is an open question for Academic (section 16).
  • The board still shows every student. An ineligible student keeps their rank on the public table, marked “not eligible for the prize” with the reason, so the number on the board never changes for them; only the prize passes them by.
OwnerAcademic owns the band table. It should be confirmed before the first cycle and then changed at most once a year, because a band change mid-year retroactively changes who could have won.

7The teacher confirmation letter

The letter is the human check. The platform can compute an age from a date of birth, but it cannot know whether the date is true or whether the child sat the tests. A named English teacher who signs a short form does both, and gives CS one phone number to call.

7.1 Who may sign

  • The student's current English teacher at their school, or at an English centre the student attends. Homeroom teachers may sign for grades 1 to 3 where there is no separate English teacher.
  • A parent, relative or the student's own FLYER account holder may not sign. A teacher who is the parent may not sign for their own child.
  • If the teacher uses FLYER Teacher or Teado.ai and the student is in their class, the teacher can confirm in the product instead of on paper (phase 2, section 12.4). This is stronger evidence than a scan and should be preferred when available.

7.2 What the letter contains

FieldPurpose
Student full name, date of birth, school, grade and classMatched against the profile; a mismatch on date of birth is an automatic reject.
Level won, prize monthTies the letter to one cycle so it cannot be reused.
Statement 1: the student is within the FLYER age band for this levelThe condition the programme was built on.
Statement 2: the student is currently learning English at or around this levelCatches an eleven-year-old parked in Starters for the prize even if the age fits the overlap.
Statement 3: to the teacher's knowledge the student completed the tests personallyThe integrity check; makes the teacher a witness, not just a clerk.
Teacher full name, role, school or centre, mobile number, signature, date; school stamp if availableGives CS a person to call. The stamp is optional because centre teachers often cannot obtain one.
Parent or guardian name, phone, signature, and consent to publish the winner's first name, school and photoRequired by the winner wall and by the delivery.

The template is a one-page PDF, bilingual, downloadable from the claim page. A photo of the signed page taken with a phone is accepted; the upload is limited to 10 MB and to JPG, PNG, HEIC and PDF.

7.3 Verification by CS

  1. Open the claim in admin; the letter is shown next to the profile, the frozen standings row and the integrity signals.
  2. Check that name and date of birth match the profile and that the computed age is inside the band.
  3. Call the teacher on the number given. Confirm the student, the class and that the teacher signed. Log the call outcome on the claim.
  4. Approve, or reject with a reason chosen from a fixed list (date of birth mismatch, teacher unreachable, teacher denies signing, level not appropriate, integrity concern, incomplete letter). A rejection releases the prize to the next reserve automatically.

8The monthly cycle end to end

One cycle per level per month, all five running on the same calendar. Steps 1 and 2 are the existing leaderboard with the new formula; steps 3 to 7 are new.

Day 1 to last day
Race
Monthly tab shows the standardised score, refreshed hourly, with an eligibility badge on every row and a “what you need to be eligible” panel for the logged-in student.
Changed · FE + BE
Last day, 23:59
Close
Attempts submitted after midnight Vietnam time belong to the next month. The board shows “final standings pending” for the level.
New · BE job
1st, 02:00
Freeze
Standings snapshot written to a table, never recomputed. Provisional winner and two reserves per level are marked. Notifications sent in-app, by Zalo and by email to the parent.
New · BE job + notifications
1st to 7th
Claim
Winner opens the claim page, downloads the letter template, uploads the signed letter, confirms delivery address. Reserves are told they are reserves and may pre-submit.
New · FE + BE
8th to 10th
Verify
CS checks letters and calls teachers. Rejections and expiries pass the prize to the next reserve, who gets a fresh 5-day window.
New · Admin
12th
Announce
Winner wall updates, board banner names the five winners, Bingo bot posts in Discord, marketing posts on Zalo and Facebook.
New · FE + Marketing
12th to 26th
Deliver
Bicycle shipped; delivery photo attached to the claim; claim marked delivered. Cycle closed.
New · Ops
One month, seven stepsThe highlight advances automatically; it repeats every month.
  1. RaceDay 1 to 31
  2. CloseLast day 23:59
  3. Freeze1st, 02:00
  4. Claim1st to 7th
  5. Verify8th to 10th
  6. Announce12th
  7. Deliver12th to 26th
Why freeze into a tableThe leaderboard view is refreshed hourly and would silently change if a teacher deleted a class, a test was withdrawn, or the formula was revised. The prize must be decided on a number that can be shown a year later exactly as it was on the 1st.

9Requirement A: versioned ranking formula

Replace the hand-made monthly ranking for the five prize levels with a view whose definition lives in a migration file, computes the best-ten score in section 5.2, and carries a version number that is printed on the board.

What changes

  • A new materialized view monthly_prize_ranks (level, month, learner profile, score, distinct test count, capped total time, time of final score, rank, formula version), added to the hourly refresh list next to learner_test_level_ranks.
  • The Monthly tab for Starters, Movers, Flyers, KET and PET reads from this view. Daily, Weekly and Yearly, and Monthly for other levels, are unchanged in phase 1.
  • Rank is a position (row_number), never a percentile bucket. The sibling monthly recap view uses ntile(100) on production and row_number() on dev; that drift must not be repeated here.
  • Month boundaries are computed in Asia/Ho_Chi_Minh explicitly in the SQL, not left to the session time zone of whoever runs the refresh.
  • The public rules page and the board footer print “Formula v1, effective 01/10/2026”. A formula change bumps the version and takes effect only from the next month.
  • The championship test is an ordinary check point flagged championship_month and championship_level, hidden from the library and from search until its window opens, single attempt enforced server-side (a second start is refused, not silently ignored), and released into the normal library once the window closes. Academic delivers five such tests by the 20th of each month.

Reference SQL shape

WITH counted AS (                    -- one row per (student, test) = best attempt that month
  SELECT ua.learner_profile_id, ua.test_level_id, ua.check_point_id,
         bool_or(cp.championship_month IS NOT NULL)                       AS is_championship,
         max(round(10 * ua.grade / ua.max_grade, 2))                      AS best_grade,
         min(least(ua.execution_time, tl.execution_time))                 AS best_time,
         min(ua.submitted_at)                                             AS first_at
  FROM   user_attempts ua
  JOIN   test_level_exam_settings tl ON tl.test_level_id = ua.test_level_id
  JOIN   check_points cp ON cp.id = ua.check_point_id
  WHERE  ua.type_attempt = 'exam' AND ua.submitted_at IS NOT NULL
    AND  ua.grade > 0 AND ua.max_grade > 0
    AND  (ua.submitted_at AT TIME ZONE 'Asia/Ho_Chi_Minh') >= date_trunc('month', now() AT TIME ZONE 'Asia/Ho_Chi_Minh')
  GROUP  BY 1,2,3
), ranked AS (
  SELECT *, row_number() OVER (PARTITION BY learner_profile_id, test_level_id
                               ORDER BY best_grade DESC, first_at) AS k
  FROM counted WHERE NOT is_championship
), best10 AS (
  SELECT learner_profile_id, test_level_id,
         sum(best_grade) FILTER (WHERE k <= 10)   AS score10,
         count(*)                                  AS distinct_tests,
         sum(best_time)  FILTER (WHERE k <= 10)   AS counted_time,
         max(first_at)   FILTER (WHERE k <= 10)   AS final_score_at
  FROM ranked GROUP BY 1,2
), champ AS (                       -- the month's single-attempt championship test
  SELECT learner_profile_id, test_level_id, best_grade * 10 AS champ_score, best_time AS champ_time
  FROM counted WHERE is_championship
)
SELECT b.learner_profile_id, b.test_level_id,
       b.score10 + coalesce(c.champ_score, 0) AS score,
       b.score10, c.champ_score, b.distinct_tests, b.counted_time, c.champ_time, b.final_score_at,
       row_number() OVER (PARTITION BY b.test_level_id
                          ORDER BY b.score10 + coalesce(c.champ_score, 0) DESC,
                                   c.champ_time ASC NULLS LAST, b.distinct_tests DESC,
                                   b.counted_time ASC, b.final_score_at ASC) AS rank,
       1 AS formula_version
FROM best10 b LEFT JOIN champ c USING (learner_profile_id, test_level_id);

Acceptance

  • Given a student with twelve tests, the best-ten score equals the sum of their ten best normalised grades and their thirteenth test never changes it.
  • Given two students on 200, the one with the lower capped time on the championship test ranks first; then more distinct tests; then lower capped time on the ten counted tests; then the earlier final score.
  • A second start of the championship test is refused by the API with a message the student can read; only the first attempt is ever counted. The test is not visible in the library, in search, or by direct link before its window opens.
  • The definition is in a migration in the exam repository and produces the same rank on dev and production for the same fixture data.
  • Medals on the board are keyed to the server rank, not the row index.

10Requirement B: eligibility and profile

Who raced in August 202629,200 students sat at least one full test at the five prize levels. Fewer than half could prove their age today.
Who raced in August 2026Registered, birthday on file: 12,261 (42.0%)Registered, no birthday: 11,921 (40.8%)Anonymous profile: 5,018 (17.2%)42%birthday on file
  • 42% Registered, birthday on file 12,261
  • 41% Registered, no birthday 11,921
  • 17% Anonymous profile 5,018
Source: FLYER read replica, full mock tests (type exam) submitted 1 to 31 August 2026, Vietnam time.

Date of birth

  • Add a date-of-birth prompt in the student app and on ranking.flyer.us for any logged-in student without one. Copy: “Add your birthday to be eligible for the monthly bicycle.”
  • Once saved, the field is locked (dob_locked_at). Changing it requires a support ticket with a teacher letter or birth certificate. This is what makes the age check meaningful.
  • Validation: a date between 3 and 20 years before today; anything else is rejected at the API.
  • Cut-off: a date of birth saved on or after the 20th of a month does not count for that month's prize. This stops a family adding a convenient birthday on the 31st after seeing the standings.

Eligibility computation

A nightly job (and again at freeze) writes an eligibility row per active student and level: eligible or not, with a list of reasons. Reasons are a fixed enum so the UI can explain them.

Reason codeMeaningShown to the student as
ANONYMOUSProfile has no user accountCreate an account to be eligible
NO_PARENT_PHONENo verified parent phoneAdd a parent's phone number
NO_DOBNo date of birth, or added after the 20thAdd your birthday before the 20th
AGE_OUT_OF_BANDAge at month end outside the level's bandThis level is for ages 8 to 10; you can race in Flyers
EXCLUDEDOn the exclusion list (staff, demo, partner admin, integrity hold)Not eligible for prizes
SECONDARY_PROFILEUser has another profile marked primaryOnly your main profile can win
WON_THIS_YEARAlready received a bicycle this calendar yearYou already won in March, congratulations; next year again

Exclusion list

  • Seeded from: every learner profile linked to a @flyer.us or @flyer.vn user, the configured demo student ids that the sample-class job shifts dates for, and profiles owned by partner admin accounts.
  • Maintained in admin (section 12) with a reason and an expiry; integrity holds expire after six months.

On the board

  • Every row on the Monthly tab of a prize level carries a small badge: “Eligible” or “Not eligible”. Tapping the latter shows the reason to the student themselves and a neutral “not eligible for the prize” to everyone else.
  • The pinned footer row for the logged-in student gains a checklist: account, parent phone, birthday, age band, tests counted so far (7 of 10).

11Requirement C: claim flow for students

A page on ranking.flyer.us reachable from the notification and from the board banner, for the logged-in provisional winner or reserve.

ranking.flyer.us/prize/claim/2026-09/movers
You finished 1st in Movers for SeptemberScore 98.5 · 14 tests · Formula v1. Claim by 7 October 23:59.
1 · Check your detailsName, date of birth (locked), school. Wrong? Contact support.
2 · Teacher letterDownload template (PDF, EN/VI). Upload signed photo or PDF. Teacher name, school, phone.
3 · DeliveryParent name, phone, address, preferred bicycle size (child / junior).
4 · ConsentPublish first name, school and delivery photo on the winner wall.
Submit claimSave draft
StatusSubmitted 3 Oct · Under review · FLYER may call your teacher at 09xx xxx xxx
  • States: invited, draft, submitted, under_review, verified, rejected, expired, awarded, delivered. Each state change notifies the parent.
  • Reserves see the same page with “You are 1st reserve. If the winner's claim is not verified you will have five days to submit.” They may upload early.
  • A rejected claimant sees the reason code and a support contact; they cannot resubmit in the same cycle except for “incomplete letter”, which allows one resubmission within the window.
  • The winner wall at /prize/winners lists past winners by month and level: first name, school, province, score, delivery photo where consented.
  • The public rules page at /prize/rules renders section 5 and section 6 of this document, bilingual, with the current formula version.

12Requirement D: admin and verification

12.1 Cycles

A list in admin-web of prize cycles, one row per month and level, with status (open, frozen, claiming, verifying, announced, closed), the frozen top ten, and the claim chain (winner, reserve 1, reserve 2 and who currently holds the prize).

12.2 Claims queue

A plain list, newest first, filterable by status and level. Each claim opens to a single screen: profile card, frozen standings row with the score breakdown (the ten counted tests), the uploaded letter, the teacher's phone, integrity signals (section 13), a call log, and Approve or Reject with the fixed reason list. Approving the winner sets the cycle to announced once all five levels are verified or on the 12th, whichever is first.

12.3 Exclusions and overrides

An exclusion list with add, reason, expiry and an audit log. A manual override to pass the prize to a reserve outside the automatic rules, with a mandatory note; overrides are listed on the cycle for later review.

12.4 In-product teacher confirmation (phase 2)

If the student belongs to a class on FLYER Teacher or Teado.ai, the claim can name that teacher. The teacher receives an in-product request: “Confirm that Minh Anh is in your class, is 9 years old, and completed the tests personally.” One tap confirms; the claim records the teacher's account, which is stronger than a phone call. Paper remains for students without a FLYER teacher.

12.5 Exports

CSV of winners per cycle for finance (prize value, delivery cost) and a monthly report of claims by outcome and reason for the product review.

13Anti-abuse and audit trail

A bicycle attracts effort. Rules alone are not enough; the platform must surface signals so that CS reviews with evidence, and every decision must be reconstructible.

SignalRuleEffect
Implausible speedA counted test with grade 10 completed in under 25% of the level's allotted timeFlag on the claim; CS asks the teacher specifically about it
Multi-profile userThe user account owns more than one student profile with attempts this monthOnly the primary profile is eligible; flag for review
Shared device or networkSeveral top-20 profiles at the same level share a device fingerprint or the same network across most attemptsFlag; does not auto-reject, because siblings and classrooms exist
Night-time burstsMore than five counted tests submitted between 00:00 and 05:00 in the monthFlag
Late birthdayDate of birth added within ten days of the freezeAutomatic NO_DOB for that month
Answer-pattern similarityTwo profiles with identical answer sequences on three or more testsFlag both; integrity hold if confirmed
  • Flags are advisory. The only automatic outcomes are the eligibility reasons in section 10; everything else goes to a human with the evidence attached.
  • Every cycle keeps: the frozen standings, the eligibility rows, every claim with its letter and call log, every state change with actor and time, and the formula version. Nothing in that set is ever updated in place.
  • A student placed on integrity hold is told so, with a support route. Silence is worse than a clear rule.

14Data and technical design

New tables (exam database, migrations in the exam repository)

TableKey columnsNotes
prize_cyclesid, month, test_level_id, status, formula_version, frozen_at, announced_at, closed_atOne row per level per month, created by the freeze job
prize_standingscycle_id, learner_profile_id, rank, score, distinct_tests, counted_time, final_score_at, eligible, reasons (jsonb), counted_tests (jsonb)Frozen snapshot of the top 50 per level plus every eligible student's own row; immutable
prize_eligibilitylearner_profile_id, test_level_id, month, eligible, reasons (jsonb), computed_atNightly; drives the badge on the board
prize_claimsid, cycle_id, learner_profile_id, position (winner / reserve1 / reserve2), status, letter_file, teacher_name, teacher_role, teacher_org, teacher_phone, parent_name, parent_phone, address, bike_size, consent_publish, reviewer_email, reject_reason, deadline_at, timestampsOne per invited student per cycle
prize_claim_eventsclaim_id, actor, action, detail, created_atAppend-only audit log including call outcomes
prize_exclusionslearner_profile_id or user_id, reason, expires_at, created_bySeeded from staff domains and the demo student list
prize_age_bandstest_level_id, min_age, max_age, effective_fromAcademic-owned; history kept

Profile changes

  • learner_profiles.dob_locked_at, learner_profiles.is_primary (one per user, default the oldest profile with attempts).
  • API validation on date_of_birth: range check, and reject updates once locked unless the caller has the support role.

Jobs (Temporal, Asia/Ho_Chi_Minh)

  • Hourly: refresh monthly_prize_ranks alongside the existing views.
  • Nightly 03:00: recompute prize_eligibility for the current month.
  • 1st 02:00: freeze the previous month into prize_cycles and prize_standings, create claims for the winner and two reserves per level, send notifications.
  • Daily 09:00: expire overdue claims, promote the next reserve, remind claimants two days before deadline.

API

  • GraphQL v2: monthlyPrizeRanks(testLevelId, month, first, after, filterBy) returning score, distinct tests, rank, eligible and reasons; viewerPrizeStatus(testLevelId) for the checklist; prizeCycle(month, testLevelId) for the banner and winner wall.
  • REST v4 student: GET /v4/student/prize/claims/current, PUT /v4/student/prize/claims/{id}, POST /v4/student/prize/claims/{id}/letter (multipart), POST /v4/student/prize/claims/{id}/submit.
  • REST v4 admin: cycles list and detail, claims list and detail, approve, reject, override, exclusions CRUD, age bands CRUD, CSV exports.
  • Notifications reuse the existing in-app notification rows and the Zalo and email channels used by recap announcements; the Discord announcement reuses the Bingo bot.

Frontend

  • webs-ranking: Monthly tab reads the new query for the five levels; eligibility badge and checklist; prize banner; pages /prize/claim/[month]/[level], /prize/winners, /prize/rules; fix the index-based medals.
  • Student app: date-of-birth prompt and parent phone prompt, deep link to the claim page from the notification.
  • admin-web: /center/prizes/cycles, /center/prizes/claims, /center/prizes/exclusions, /center/prizes/age-bands, following the plain-list pattern of the wisdom-stone screens.

15Scope, phasing and budget

PhaseContentsOutcome
Phase 1, first cycleRequirement A (versioned formula, championship test as a single-attempt check point with a window, medals fix), Requirement B (date of birth prompt and lock, eligibility job, exclusion seed, board badges), Requirement C (claim page, letter upload, rules page, winner wall), Requirement D (cycles, claims queue, exclusions, age bands), freeze and notification jobsThe first month of prizes runs on rules a parent can read and a CS agent can enforce
Phase 2In-product teacher confirmation via FLYER Teacher and Teado.ai, integrity signals in the claim screen, answer-pattern similarity, reserve pre-submission, finance exportVerification takes hours instead of days and stops relying on phone calls
Phase 3Apply the versioned formula to Weekly and Yearly, school and province sub-prizes, weekly mini-prizes in wisdom stonesOne formula family across every tab
Out of scopeCash prizes, prizes for IELTS or TOEFL levels, changes to the Daily tab, changes to the wisdom-stone store

Running cost per month

ItemAssumptionVND per month
Bicycles5 × 2,500,000 to 3,500,000 (child and junior sizes)12,500,000 to 17,500,000
Delivery5 × 150,000 to 400,000, nationwide750,000 to 2,000,000
CS time15 claims including reserves × 30 minutesabout 1 working day
Totalroughly 14,000,000 to 20,000,000

Bicycle sizes differ across the age bands: a six-year-old and a sixteen-year-old do not ride the same bicycle. The claim form asks for a size, and procurement should hold two models.

16Open questions

QuestionWhy it affects the designDecider
Confirm the age bands in section 6, in particular KET 11 to 14 and PET 13 to 16.Cambridge publishes no fixed age for the for-Schools papers; these are FLYER's choice and they decide who can win.Academic
Admit students younger than the band?A seven-year-old winning Flyers is good marketing and not unfair to older students. Excluding them is simpler to explain. The default in this draft is to exclude.Academic + Product
Ten tests, or a different cap?Sets the effort floor. Ten is about four to five hours at Starters; if Academic thinks that is too much for six-year-olds, Starters could use eight.Academic
Can Academic deliver five fresh, unpublished full tests every month?The championship test only works if the content has never been seen. If capacity is the constraint, the window could reuse a test drawn at random from a sealed pool of twenty per level.Academic + Content
Championship window: the whole last weekend, or Sunday only?A longer window is fairer to families with weekend classes; a shorter one makes answer sharing harder. The draft uses Saturday 08:00 to Sunday 22:00.Product + Academic
Should the versioned formula also replace Daily, Weekly and Yearly from day one?Cleaner, but changes numbers students already watch and widens phase 1. The draft keeps it to the Monthly tab of the five prize levels.Product
Is a parent phone number available on enough student profiles?If most profiles lack one, requiring it in month one will disqualify most of the top ten. Needs a replica query.Product + Data
Who signs for home-schooled students or students without an English teacher?The draft accepts centre teachers and homeroom teachers for grades 1 to 3, but has no answer for a child with neither.CS + Academic
Publish the full score breakdown of winners publicly?Transparency reduces disputes but exposes which tests a child took. The draft shows the breakdown to the claimant and to admin only.Product + Legal
Budget approval and procurement of two bicycle models.Roughly 14 to 20 million VND a month before marketing.Finance
SourcesSection 3 comes from flyer/webs-ranking (src/pages/index.tsx, src/components/TableRanking, src/api/common.ts, src/providers/auth), flyer/exam (graph/schema/type_extended.graphql, graph/user/type_extended.resolvers.go, ent/schema/views.go, module/mview, module/learnerprofile) and flyer/flyer-go (db/migrations/base/0092_user_score_views.up.sql, service/student/db/queries/score.sql). The exact production definition of learner_test_level_ranks must be read from pg_matviews; it is in no repository.