Inspiration
Every student has studied something in August and had nothing left by November. What no tool tells you is how much nothing — or the day it became too late to do anything about it.
Spaced-repetition schedulers answer "what should I review today?". They do not answer the question a student in a semester actually has: given everything I've studied, one fixed exam date, and the fact that I can only get through about forty facts a night — is my target still reachable, and when does it stop being reachable?
What it does
Paste your syllabus and your exam date. Cutoff:
- Forecasts, per concept, the probability you'll still recall it on exam morning — not today, on that specific calendar date.
- Computes your cutoff. For every possible start day, it simulates studying flat out from then until the exam — every night filled to capacity, always the weakest card. The last start day whose ceiling still clears your target is your deadline. Start a day later and no amount of effort closes the gap.
- Plans the smallest schedule that gets you there, choosing sessions one at a time by marginal gain per minute and stopping the moment you hit target.
- Handles two exams, because mid-sems and end-sems compete for the same evenings. It computes the Pareto frontier over one set of nights: every point is a real schedule, and no schedule wins both.
Every figure is shown as a range, and the range is the model's own measured error at that horizon — not a design flourish.
How I built it
The model is the moat, so it was built first. A difficulty–stability–retrievability model, fitted by us with L-BFGS in numpy on real Anki review logs. The shipped artefact is sixteen floats in a JSON file — production never imports a training stack.
cutoff/model/— the DSR model, plus Half-Life Regression (ACL 2016) as a baseline we implemented ourselvescutoff/core/— the forecast, the capacity-constrained planner, the two-exam frontier. Pure arithmetic.cutoff/eval/— held-out scoring against the community benchmark's own metricscutoff/api/— FastAPI, stateless; the client holds its own cardsweb/— two hand-written files. No bundler, no chart library; every chart is hand-drawn SVG sized to its container.
A language model appears in exactly one file — cutoff/ingest/llm.py — where it reads a pasted syllabus into subjects and topics. Delete it and the product still runs on a parser that needs no key, and every forecast is identical. The forecast, the ceiling and the plan are re-derivable from committed code and committed weights.
Does it work?
Fitted on 40 real Anki collections, scored on each learner's future reviews — 520,243 answers the model never saw.
| log loss | calibration | AUC | |
|---|---|---|---|
| Cutoff (fitted) | 0.4764 | 0.0527 | 0.6965 |
| Cutoff (unfitted defaults) | 0.6484 | 0.2687 | 0.5697 |
| HLR (ACL 2016, our implementation) | 0.9672 | 0.3426 | 0.4794 |
| Predict the mean | 0.5297 | 0.1244 | 0.5000 |
The published FSRS-6 figures (0.3460 / 0.0653 / 0.7034) are measured on ~350M reviews across 9,999 collections. Ours is a 40-collection sample. Read the ordering, not the decimals — we have not beaten FSRS and do not claim to.
Calibration matters more than accuracy here, because the product shows you a number and you plan around it. Expected calibration error is 0.0244, and 2.3 points three months out — which is exactly the horizon the whole premise depends on. So every number in the app is displayed as a range that wide, shifted down by the bias measured here. The honesty screen is not a footnote; it is the source of every figure on every other screen.
Challenges I ran into
The first dataset had almost no forgetting curve. We started on Duolingo's 12.85M traces. Recall barely moves with time — 90.6% under a day, 86.8% after a month. Measuring the confound directly explained it: the scheduler grants 58 days after a success and 10.4 after a lapse, so a long gap is a reward for strength and the two effects cancel. Conditioning on repetition number brings the curve back. We moved the production model to Anki logs, where people genuinely forget, and kept Duolingo as an ACL replication.
Half-Life Regression scored below chance. We implemented the 2016 paper and reproduced its published baselines within a few points — then watched it lose to predicting the average on Anki data, because it has no notion of stability and reads a long gap as weakness. That failure is the entire argument for a DSR model.
A bug that made November revision improve a September mark. recall_on clamped elapsed time at zero, so a card queried before its last review returned 1.0. It now raises instead of clamping, and two regression tests guard it.
The deployment was 5× slower than the laptop. Measured against production rather than an M4 Pro, the two-exam frontier took 46 seconds on a two-core serverless instance. Both heavy endpoints are pure functions of their request, so they are memoised; the frontier simulates an evenly-spaced 300-fact sample and says so on screen. 46s → 9.7s cold, 1.0s warm. The forecast also paints before the ceiling finishes, so nobody watches six seconds of shimmer to learn we're busy.
Accomplishments I'm proud of
We were wrong about our own thesis and said so. We set out to show spacing beats cramming. In our own model, at identical effort, cramming wins at every horizon — 90.0% against 75.4% spread evenly, and it still leads three months later. Rather than bury that, we rebuilt the product around what survives it: you cannot sit through six hundred facts the night before, and once each night has a limit there is a last day you can start. That constraint is the product.
The three things we got wrong are on a screen inside the app, not in a footnote.
What I learned
- Measure the confound before trusting the dataset. Two hours measuring gap-length-after-success would have saved a day of modelling.
- Calibration is the product feature. A forecast you plan around has to say how wrong it is, and that turned out to be the most persuasive thing in the whole app.
- Profile against the deployment, not the laptop. Every performance number that mattered was 5× worse on the box that judges would actually hit.
What's next
Import real Anki collections directly instead of self-assessment, so the model starts from your actual review history. Per-topic difficulty learned from your own lapses. And a version of the ceiling that accounts for the nights you will realistically miss, rather than assuming you never skip one.
Built With
- css
- fastapi
- google-gemini
- html
- javascript
- machine-learning
- numpy
- python
- scipy
- svg
- vercel
Log in or sign up for Devpost to join the conversation.