Inspiration

In 2024 and 2025 I was preparing for NET, the entry test for NUST.

Preparing for it meant reading entire chapters just to pull out the important information. Then doing it again. And again. It was tough and mentally draining, and almost none of that effort was the learning. It was the digging.

Then university, and the semester courses were overwhelming in a different way. I searched for study apps, installed them, and deleted them. A lot of them. They were all the same idea: upload your notes, get flashcards. That isn't how I study. I wanted something that would question me.

So I built the one I wanted.

You paste in your notes. Vesprit pulls out the key concepts and questions you on them, starting with the ones you're closest to forgetting, based on how you've actually done on them.

It's for anyone who wants to remember what they study or read: a university student working through a semester of lectures, someone preparing for an exam or a certification, a reader who wants to keep what they learned from an article. It's the same gap for all of them: recognising a page when you reread it is not the same as producing the answer when a question is in front of you.

What it does

Four steps, all working end to end on a real Android phone:

1. Paste your notes. A chapter at a time. The source is saved to the database before anything else happens, so if extraction fails you haven't lost what you pasted.

2. Concepts are extracted for you. Nobody writes a flashcard. There's no card editor anywhere in the app, and that's deliberate: writing the cards is the work that makes people give up.

3. Recall, ten at a time, most at risk first. You type what you remember. Then you reveal the answer and grade yourself: Recalled it, Partly, or Missed it. Typing matters. Recognising the right answer in a list is not the same as producing it, and the exam will ask you to produce it.

4. See what's fading. The timeline shows everything you've added and every session you've run, and flags the concepts that are slipping. Tap a fading concept and you're in a session for it.

The difference is the question it answers

Most study tools are built around a schedule: what should you look at today? That's a fair question, and plenty of people are well served by it. Vesprit asks a different one.

Vesprit doesn't ask what to study today. It answers what you're about to forget.

Vesprit keeps no calendar and no backlog of cards waiting for you. Every session is built fresh from one thing: your own record on your own concepts, ranked by how likely you are to have lost each one right now.

How the fade score works

This is the part the whole app rests on, so here it is in full. Every concept carries a history of your attempts, and the model walks it in order:

STABILITY       starts at 1 day: how long until recall is a coin flip
  Recalled it   → stability × 2.0
  Partly        → stability × 1.3
  Missed it     → stability × 0.4
                  clamped between half a day and a year

RECALL ODDS     0.5 ^ (days since your last attempt ÷ stability)
FADE SCORE      1 − recall odds

A miss cuts deeper than a hit adds, on purpose. With symmetric numbers, a streak of correct answers from weeks ago would hide a concept you're failing today. The ceiling exists because ten hits in a row would otherwise push a concept out for almost three years. The floor exists so that every missed concept doesn't collapse to the same score, because the ranking between the things you're struggling with is where the ranking matters most.

Now let me name the limit honestly. Those constants are judgement calls, not fitted parameters. I have no population data to fit them to, and I'm not going to pretend otherwise. What's personal is the input. Two people who paste the same chapter get different sessions the moment they answer differently.

I could have flagged concepts by how long it's been since you last saw them. On screen it would have looked the same, and nobody would have checked. It's written into the project's rules that fade detection has to come from real retrieval history and never from a timer or a random number, and the model is a pure function with its own tests to hold it there.

The app never shows the raw score either. A number like "73% faded" suggests a precision the model doesn't have, so each concept shows one of three plain labels: Never tested, Holding or Fading.

How I built it

  • Expo and React Native, in strict TypeScript. No any anywhere. Business logic lives in plain modules outside the screens, and the fade model is pure: no database, no network, and the current time is passed in so tests can pin it.
  • Clerk for sign-in, bridged to Firebase Firestore through a custom token, so the database's security rules scope every read and write to the signed-in user.
  • A Cloudflare Worker is the only place the Anthropic API key exists. It checks your sign-in before it spends anything, then runs extraction with Claude Haiku 4.5. The key has never been in the app.
  • RevenueCat on top of Google Play Billing. More on that below.
  • Sentry for handled errors, with source maps, so a failure on a tester's phone arrives as a readable stack trace.
  • No product analytics. The app has no analytics key, so nothing is collected, and it's declared that way on Google Play.
  • 590 tests across 32 suites. The most important thing they taught me is under Challenges.

How RevenueCat is used

Free covers three sources. Recall sessions, fade tracking and the timeline are free for everyone, and they'll stay that way. A subscription lifts the source limit, with a 7-day free trial through Google Play.

The paywall sits on sources and not on the fade model. The fade model is the reason to use Vesprit, and charging for it would ask someone to pay before they've felt why they'd want to. Sources are also where the cost is, because every source is one paid extraction call. The limit sits on cost, not on value.

How it's wired:

  • The dashboard decides what's for sale. The paywall renders whatever packages the current offering holds (Monthly and Annual today), so a plan or price can change without an app release.
  • One entitlement, premium, decides everything. The app reads it from CustomerInfo and listens for updates, so the source limit lifts the moment a purchase lands, and a renewal, expiry or refund later is picked up without a restart.
  • "Not known yet" is not "free". Before RevenueCat answers on a cold launch, a paying user would otherwise look exactly like a free one and hit the limit. That state lets them through instead.
  • Identity is the Clerk user id, not an anonymous one. I bought Monthly as a license tester, uninstalled, reinstalled, signed in, and Premium was still there.
  • Restore is always on the paywall, and finding nothing to restore is shown as a normal outcome, not an error.
  • Two silent failures are checked for explicitly. An offering with nothing marked current renders an empty paywall that looks like a working screen. A purchase can succeed while the entitlement never activates. Both are reported to me, and the user is told plainly what happened and that they won't be charged twice.
  • Cancelling isn't an error. The app classifies a user cancellation and shows nothing.
  • In the redesign, the paywall only promises what the store will honour. A trial is offered on screen only when the plan carries one and RevenueCat shows this account has never bought anything. Each plan's billing period is checked against what the plan claims to be, and a plan that doesn't match is withheld and reported instead of sold.

Challenges

The feature that was "done" for ten days and had never worked once

Google Play requires that a user can delete their account from inside the app. I built that early, on purpose: data first, then the identity, so a failure halfway through never leaves someone with no account and all their data still stored.

The first real run failed with "Could not reach the token service." Three times, identically. Then I found a genuine bug on that path, a mismatch between the Clerk instance the app signed in with and the one the Worker trusted. I fixed it and marked deletion done.

It wasn't done. That bug rejected the request before the deletion code ever ran, so it couldn't have produced that error message. It was a second bug standing in front of the first one, and fixing it only moved the wall.

Ten days later I found the real one. It was a single line:

// before: the global fetch, handed over without its receiver
fetchImpl: fetch,

// after
fetchImpl: (input, init) => fetch(input, init),

Passing fetch bare detaches it, so the call threw instead of making a request, and the error handler reported that throw as a network failure. Google was never contacted. Every test for that code passed, because every test gave it a mock. The handling was tested. The call itself had never run.

This time I didn't mark anything done until I'd watched it work: a throwaway account deleted on the signed store build, all three of its database collections empty, the identity gone from Clerk, and a sign-in with the old password rejected.

The Annual plan that billed every month

My redesigned paywall showed the Annual plan as "per month". It wasn't a display bug. In Play Console, the Annual base plan had been set up to bill monthly at the yearly price, and nothing in Play Console or RevenueCat flagged it. The check that compares each plan's billing period with what it claims to be is what caught it. No real user was ever charged, because only license testers had bought. I replaced it with a proper yearly base plan and a new 7-day trial offer, and pointed RevenueCat's Annual package at it. The check stays.

And the rest

  • Sign-in was a dead end for every account. Clerk asked for a second step that the dashboard said was switched off, and the screen had no way to enter it. I found it when I finally signed in as a user instead of as a developer with a session already open.
  • Not one environment variable had reached the build server. They were all set on my laptop, and the app handled their absence so gracefully that nothing looked wrong. Now I check the shipped bundle itself before calling a build good.
  • Extraction caps a source at 250 concepts, and it didn't say so. The obvious check, "did the list get cut?", never fires, because the model obeys the cap and returns exactly 250. The check that works is >=, not >. I'll take a hedged sentence on a rare exact hit over silently losing four fifths of someone's material.
  • My testers' phones were less kind than mine. A sign-in that failed silently on a dropped connection. An extraction that timed out when a phone went into power-save. Buttons hidden behind the navigation bar on phones with 3-button navigation, which on one screen made account deletion unreachable. All fixed, and re-checked on the phone that found them.

What I learned

Configured is not verified, and verified is not shipped. A feature is done when the thing it exists to do has happened, on the build people will install. It cost me a lot of wrong "done"s to learn that.

Real testers find what a solo developer can't. A tester got locked out of their account and emailed me, and that's how I found out I'd never built forgot-password, because as the only user I'd never needed it.

And stating a limit before anyone asks is easier than defending a claim you can't back.

What's next

Where it stands: Vesprit is launching on Google Play. It ran a 14-day closed test with testers in Pakistan, production access was granted on 30 September, and the 1.0.0 production release is with Google for review.

  • File upload, so a PDF or a slide deck can be a source without copying and pasting.
  • Longer sources, extracted in pieces instead of stopping at one ceiling.
  • Forgot-password. The locked-out tester showed me exactly why it matters.
  • Shipping the redesign from the video to Google Play.
  • A short onboarding, so the first minute explains the loop instead of leaving you to find it.

Limitations, plainly

  • Pasted text only, for now. No PDF or file upload yet.
  • A chapter at a time, not a whole book. Extraction stops at 250 concepts per source, and the app tells you when a source hit that ceiling.
  • The model's numbers are chosen, not learned. The ranking is computed from your history, but the rules that turn history into a score are mine.
  • Android only. And the design in the video isn't on Google Play yet. The store build has the plain first version until the redesign ships as an update.
  • It can't promise you'll pass. It tells you what you're most likely to have lost. What you do with that is still studying.

One person. Three sources free. Ten concepts a session, the ones you're most likely to have lost.

Paste a chapter you studied last month and see what's left.

Built With

Share this project:

Updates

Submission history