Inspiration
Everyone has the same graveyard: a chat with yourself, a browser's bookmark bar, a notes app with 400 untitled entries. Saving is instant, finding is impossible — because filing something properly costs more attention than the thing was worth at the moment you saved it.
Assorta starts from the opposite end: you never file anything. You drop a link, a note, or a chunk of text into one input, and the app decides where it belongs — reading the linked page, picking one category, and giving the item a title you'll actually recognise later.
What it does
- One inbox. Paste anything, or share into Assorta from any app (Android share target, iOS share extension, web).
- AI files it. The backend fetches the linked page, classifies it into exactly one group —
SPORT,SHOPPING,TECH,HEALTH,REAL_ESTATE… — and returns a readable title plus a preview image and favicon. - One tap to correct. The classifier also returns ranked runners-up, so fixing a wrong tag is a single tap, not a trip through a folder tree.
- It never stops working. Past the free AI quota, notes are still filed — by a keyword heuristic instead of the LLM. The paid feature is the quality of the sorting, not the ability to save.
- Everywhere. One Kotlin codebase ships Android, iOS, and the web (both JS and Wasm targets).
How we built it
Client — Kotlin Multiplatform + Compose Multiplatform. One shared module holds the UI, the MVI
state machines, navigation and the object graph; Android, iOS and two web targets are thin shells.
Room + SQLite on mobile, Firebase Auth/Analytics/Crashlytics through GitLive's multiplatform wrappers,
RevenueCat KMP for subscriptions, Coil 3 for images, Ktor client for networking.
Backend — Ktor on the JVM, Docker-built, deployed on Fly.io (assortabackend.fly.dev) after a
tour through Cloud Run and Railway; the host is a compile-time enum in the app, so moving hosting is
one line.
The classification pipeline is the interesting half:
PageMetadataFetchergets the page's title and description as cheaply as it can — oEmbed for heavy-JS hosts, otherwise a partial streaming read of the first data parsed with Jsoup, walking an Open Graph → JSON-LD →<title>ladder, with bot-wall detection so a "Just a moment…" interstitial never reaches the model as if it were the page.TaggingServiceasks the configured provider, then every other provider, then falls back to a keyword heuristic. Providers are Gemini → OpenAI → Anthropic, all behind one shared system prompt and one parser — a new provider is a singleTagClassificationModeland nothing else.- Every classification logs the tag the model wanted to invent, which is how the vocabulary grows from evidence instead of opinion.
Challenges we ran into
The first version cost 2–4¢ per saved link. I attached Gemini's URL Context tool to every request, so the model ingested the whole page — a YouTube page is ~1.3 MB against an ~800-token task. Fetching just the title and description ourselves brought it to roughly 0.01¢, a ~200–400x drop, with no measurable loss in tag quality. We later retired the "deep read" Pro mode entirely after measuring it: it produced no tag the cheap path missed, and it collapsed on exactly the login-walled sites people save most.
The sites people save most are the hardest to read. YouTube buries its og:title 690 KB into the
page; Pinterest 1054 KB. Raising the fetch limit was measured and rejected — every host that ships OG
tags at all puts them under 86 KB, and the two that don't both speak oEmbed. Pinterest then taught us
that a rejection can arrive as HTTP 200 with an error body, so the fetcher treats a missing title,
not the status code, as the signal to fall through.
We spent a week comparing models and found the bug was ours. A full provider bake-off — latency, cost, confidence, agreement per note — showed that both the Gemini thinking config and the OpenAI reasoning config were misconfigured. Fixing them changed every number in the comparison. All three providers agreed on most notes anyway; the real lever was never the model, it was whether the page read at all.
Multiplatform gaps are real. Room, Crashlytics and the Firebase wrappers each cover a different subset of Android/iOS/JS/Wasm, so the shared code is written against interfaces with per-target actuals — and the web builds run without a local database at all.
Accomplishments we're proud of
Four platforms from one codebase, a classification pipeline that costs a hundredth of a cent per
note, and a tag vocabulary with written boundaries — every candidate tag has to be decidable from
the note alone, which is why HOBBIES, NOTES and PROJECTS were rejected with reasons and
REAL_ESTATE earned its place from production logs.
What we learned
Measure before optimising, and then measure whether the optimisation did anything — half the things we were sure about (deep read, higher reasoning effort, a bigger model) bought nothing and were reverted. And a fast wrong answer the user can fix in one tap beats a slow right one.
What's next for Assorta
Search across groups, multi-tag suggestions, link summaries, shipping the web app publicly, and production releases on both stores.


Log in or sign up for Devpost to join the conversation.