Inspiration
Every screen-time app is a punishment. It blocks Instagram, you feel bad, and within a week you've uninstalled the blocker. It asks for willpower and gives nothing back.
We flipped the transaction. What if the apps engineered to steal your attention became something you bought — and the currency was vocabulary?
Scrollary is a TikTok-style endless feed of word cards. Every card is one word, its part of speech, a real example sentence, native audio, and a quiz you must answer to swipe past. 30 correct answers banks an hour of screen time on your gated apps, enforced through Apple's Family Controls framework. The first reward lands after 5 correct, so the loop proves itself before you can talk yourself out of it.
Same scroll reflex. Different payload.
What it does
Your distracting apps are shielded by default. To unlock them you scroll Scrollary — and scrolling is studying, because no card passes unanswered. Words you miss land in an Unknown Words folder, and clearing that folder earns time at double rate: the fastest path to your own freedom runs through the words you're worst at.
Time expires at midnight. Every day starts at zero. No streak to shame you, no debt to carry.
3,639 curated words across 12 categories, each with a hand-checked example sentence rather than a bare flashcard. 7-day free trial, then $6.99/month or $39.99/year.
Monetization — and why we tore ours out
We built subscriptions the hard way first: StoreKit 2 on the client, and a backend endpoint consuming Apple's App Store Server Notifications V2 — verifying the signed JWS payload, walking the certificate chain, decoding transaction and renewal info by hand.
It worked. It also taught us what we'd signed up for. Entitlement state now lived in our database, derived from a webhook stream we had to keep correct through renewals, upgrades, downgrades, billing retries, grace periods, refunds, and family sharing — each with its own notification subtype and its own way of being silently wrong. And every line of it was single-platform. Android would have meant writing the whole thing again against a completely different API.
We were maintaining infrastructure, not building a product. Moving to the RevenueCat SDK collapsed that surface to an entitlement check, gave us paywalls we can change without shipping a build, and made cross-platform a configuration problem instead of a rewrite.
The lesson wasn't that the hand-rolled version was bad. It's that we'd built a worse version of a solved problem — and every hour spent on JWS verification was an hour not spent on the thing only we could build.
The challenges
Our anti-farming math was backwards — and it was written next to a claim that it wasn't
Our spec asserted that same-tier distractors meant guessing yields ~25%, so the daily cap was "realistically unreachable." We finally multiplied it out. With guess probability $p = 0.25$, card pace $t$ seconds, and reward rate $r$ minutes per correct word:
$$\text{return} = p \times \frac{60}{t} \times r$$
At our own documented 2–4 second pace with $r = 1.2$:
$$0.25 \times \frac{60}{4} \times 1.2 = 4.5 \qquad 0.25 \times \frac{60}{2} \times 1.2 = 9$$
Reflex-tapping paid 4.5× to 9× real time. The 8-hour cap wasn't unreachable — it was 80 minutes of thumb. The claim and its own refutation sat two sentences apart in the same document.
This is a monetization bug, not just a design bug: if the product's core value can be farmed, nobody renews. Three fixes, none of which slow honest use — a minimum-latency gate (answers under 1.2s don't count), per-session diminishing returns, and hardened distractors matched on frequency band and length profile, because grammatical fit alone let you eliminate three of four cloze options without knowing the word.
One row couldn't hold two languages
Going multilingual looked like translation. It was normalization.
A Spanish speaker learning English reads the word and sentence fine — that's the point. What they can't do is choose between four English definitions. So answer options must be selectable independently of the study language.
Our schema made that impossible for a precise reason: word, sentence, pos, and level depend on the learning language, while definition and the distractors depend on the answer language. Two determinants flattened into one row — a partial dependency. "Let the user pick the answer language" had nowhere to go.
Wikidata's Lexeme model formalizes exactly this: a Lexeme carries one language; its Senses carry glosses in many. Finding our problem already solved in a mature ontology was when the design locked.
Forty-four passing tests, one bug they structurally could not see
We validated the language-keyed options map with class-validator's @ValidateNested({ each: true }). Every test passed. It also rejected every real HTTP request — each: true iterates arrays, and ours was a keyed object. Every test mocked the payload past the validation layer, so the suite was green and the endpoint was broken.
MySQL DDL is not transactional, and a backup can succeed while being useless
A migration that fails partway leaves completed statements permanently applied. We learned this twice. Then our restore point failed with error 3546, GTID_PURGED overlap — the mysqldump had succeeded and was unrecoverable. Without --set-gtid-purged=OFF you find out mid-incident.
What we learned
Verify the claim, not the summary. Our most dangerous line was a confident assertion sitting beside the numbers that disproved it.
Test at the layer where the bug lives. Green tests over a mocked boundary say nothing about the boundary.
Mutation-probe your tests. Break the fix on purpose and confirm the test fails. Several of ours asserted learn: 'en' — exactly what a hardcoded value produces. They couldn't tell correct from coincidental.
Don't rebuild solved infrastructure. We shipped faster the day we stopped owning our billing stack.
Log in or sign up for Devpost to join the conversation.