Self-initiated, in two parts. First I designed a landing page for Sorta, a fictional AI tool that sorts overnight support tickets. Then I built the product for real on Replit, so every promise on the page had to work. Sorta isn't a real company, and the app never sends anything to anyone.
The problem
Support leads don't trust AI tools that reply confidently and wrongly. So Sorta never hides a decision. Every ticket shows what routed it (a rule, Claude or a person), the reason and the confidence.
Part 1: the landing page
Positioning and every line of copy start from the objection, "I can't see why it did that", not from the feature list.
A visual identity borrowed from mail-sorting rooms. Pigeonholes, routing labels, postal blue, signal red and label yellow.
The hero runs a live sorting demo instead of a screenshot. Every ticket gets a yellow label with its reason before it drops into a queue.
Trust before pricing. "You stay in control" sits above the plans, so an unconvinced buyer never reaches a price.
Part 2: the live app
Your own rules run first. Claude Haiku routes the rest, and anything under a 70% confidence threshold goes to a "Needs a person" queue with Claude's best guess. When three or more tickets share the same underlying problem, Sorta flags it as a pattern. Claude Sonnet drafts replies from your help-centre text, and nothing is final until a person approves.
Built and deployed on Replit
Deployments. Autoscale, held to one instance because sorting progress lives in memory. Express serves the production build.
Database. PostgreSQL with Drizzle. Every routing decision is an appended row, so moving a ticket by hand keeps the audit trail. A daily model-call cap per workspace is enforced with one atomic upsert.
Auth. Replit Auth over OpenID Connect, with sessions in Postgres. Every query is scoped to the signed-in user's workspace.
Object Storage. CSV imports and ticket attachments, one prefix per workspace.
Security Scanner. The first scan flagged uuid (High) and esbuild (Medium) inside the storage and database tooling. I pinned patched versions with npm overrides rather than dismissing them, and the rescan came back clean.
Secrets for the API key and session secret, and checkpoints at each working step.
What testing against the real model taught me
The first run sent 33 of 40 tickets to a person. The current Haiku rejects the temperature setting, and my code was swallowing the error. Failed calls are now logged.
Claude sometimes thinks before answering. A tight token limit left some tickets with no answer and cut drafts off mid-sentence. Both now have room, and a cut-off draft is never saved.
Four Safari checkout failures each looked like a small bug and got three different issue keys. Sorting in arrival order, and asking for keys that name the problem rather than the customer, turned them into one flagged incident.
Result
On the 40-ticket sample night, runs land 32 to 35 tickets where a support lead would put them. Most misses are tickets Claude was unsure about and handed to a person instead of guessing. The public demo shows one recorded run with its exact numbers.
React 19, TypeScript, Express 5, Drizzle, Zod, Vitest (30 tests). The landing page was built in Framer with one React code component.