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:

  1. PageMetadataFetcher gets 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.
  2. TaggingService asks 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 single TagClassificationModel and nothing else.
  3. 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.

Built With

Share this project:

Updates

posted an update —

Assorta is live on both Android and iOS!

The launch is still very fresh, but Assorta has already reached 117 active users and 222 engaged sessions.

I’ve started promoting it through Threads and X, talking directly with early users, collecting feedback, fixing issues, and learning a lot about what actually resonates outside my own assumptions.

Next experiment: TikTok I’m going to start creating short videos around the problems Assorta solves and see if video can become another acquisition channel.

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

Submission history