James Carl Sitsit

AI · Case study

Live

AlgoLens: AI pre-post reviewer that scores and rewrites posts for X

AlgoLens is an AI pre-post reviewer for X: paste a draft and get a content score with a hook / clarity / shareability breakdown and specific suggestions, plus AI rewrites in three styles. It analyses any X post, repurposes long-form into X, and ships as a Chrome extension. Free in open beta, live at algolens.app.

AlgoLens app: the pre-post reviewer scoring an X draft 87 out of 100 with a hook, clarity, and shareability breakdown

At a glance

Role
Solo: design, build, deploy
Scope
Content scoring with hook / clarity / shareability breakdown, specific suggestions, AI rewrites in three styles, repurpose-to-X, and a Chrome extension
Constraints
Scores and suggestions had to be specific and consistent enough to trust on a real draft, not generic AI feedback a writer would ignore

Stack

  • React
  • Vite
  • TypeScript
  • Tailwind CSS
  • Express
  • SQLite
  • OpenRouter
  • Gemini 2.5 Flash

Deep dive

I had the product built. Users could paste a draft tweet, get a score, see which signals were weak, and get rewrites. Then LemonSqueezy looked at what I'd made, an "X algorithm" tool that promised to make your posts go viral, and declined to process payments for it. Too close to a growth-hack, engagement-farming category they didn't want to touch. The code worked fine. The framing around it was the problem.

That rejection made me rethink what AlgoLens actually was. It isn't a trick for beating a ranking system; it's a second opinion on whether your writing is clear and worth reading, which is the thing it was genuinely good at all along. So I repositioned it around that. The "Viral Decoder" tab became the "Post Analyzer," the output became a content score, and every surface started describing the real job, making your own posts better, instead of a shortcut it was never really selling. That reframing is the real origin of AlgoLens, and it colors everything about how it's built.

What it is

AlgoLens is a small SaaS web app for people who post on X and want a second opinion before they hit send. You paste a draft, and it returns a content score out of 100, a breakdown of engagement signals, and a few rewrites in different formats. There's also a Chrome extension that does the same thing inside the X compose box, and a couple of adjacent tools: one that reverse-engineers the "writing DNA" of a post you admire, one that repurposes a link or a blob of text into native posts.

The users are solo posters and small creators. Not agencies, not teams. One person with a draft and a nagging feeling it could be better.

The constraints

That repositioning wasn't a one-time rename I did and forgot. It's a principle that lives in the codebase. Every piece of user-facing language describes optimizing your own content, never gaming a platform, because that's honestly what the tool is for. The AI prompts talk about clarity and hooks. The content score itself is a designed heuristic, not a machine-learning model, and I keep a note in the code saying exactly that so I never oversell what it is.

The other constraint is that I'm one person running this on a hobby budget, so the backend stays deliberately small, only what a beta actually needs and nothing more. That decision shows up in small places all over the app.

Architecture on a napkin

React (Vite) ──httpOnly JWT cookie──> Express
                                        │
                            authenticate ─> checkLimits ─> reserve quota
                                        │
                              OpenRouter (gemini-2.5-flash, json_object)
                                        │
                                  SQLite (better-sqlite3)

Nothing exotic. The frontend never holds a token in JavaScript; auth rides in an httpOnly cookie, so a request is a plain fetch with credentials: 'include' and no Authorization header. The server authenticates, checks limits, calls the model, writes usage and history to SQLite, and returns JSON. The Chrome extension is a fourth client hanging off the same API with its own token type.

The stack, and why

Express 5 and better-sqlite3 on the backend, because a synchronous SQLite driver is the least infrastructure that can hold a beta. React 19, Vite 7, and Tailwind on the frontend, because that's the fastest edit-refresh loop I know. OpenRouter as the model gateway, because it lets me swap the underlying model with an env var instead of a rewrite. That last one turned out to matter more than I expected.

The default model is google/gemini-2.5-flash, and it wasn't my first pick. I'd built against anthropic/claude-3.5-sonnet, and one day it started returning 404s in production. OpenRouter had retired the slug. I ran a real-prompt bake-off across a few candidates, checking which ones returned schema-valid JSON reliably, and gemini-flash won on two counts: it held the JSON contract, and it came in a lot cheaper per call. For a tool that makes an LLM call on every single analyze, that cost gap is the difference between a viable free beta and a bill I can't pay.

How an analysis actually happens

A user pastes a draft and hits analyze. The request lands in routes/analyze.js, and the first things that touch it aren't the AI, they're the guards.

The text gets sanitized, then length-checked. Then, before spending a cent on a model call, the endpoint reserves a quota slot:

const sanitizedText = sanitizeInput(text);
if (rejectIfTooLong(req, res, sanitizedText)) return;

const reservation = reserveFeatureUsage(req, 'analyze');
if (!reservation.allowed)
  return res.status(429).json({ error: reservation.error, reason: reservation.reason });

That reserve-first order is deliberate. An earlier version counted usage after the AI call, which meant two requests firing at once could both check the limit, both pass, and both spend. Reserving a slot before the call, and refunding it if the call throws, closes that race. It's the same pattern I use in the optimize route, and getting analyze to match it was a specific fix in the history.

If nothing's cached, the endpoint builds the prompt and wraps the user's text in XML tags before handing it to the model:

const userContent = `<user_post>${sanitizedText}</user_post>\n\nMedia flags: hasImage=${hasImage}, hasVideo=${hasVideo}, hasLink=${hasLink}`;

The tags are one prompt-injection mitigation. sanitizeInput removes user-supplied tags before interpolation, and the system prompt tells the model to treat <user_post> as data. A later repository audit found that other request fields cross the prompt boundary without the same validation. I am keeping the exact path in a draft security note until the route is fixed and tested.

Then callOpenRouter sends the system and user messages to gemini with a json_object response format. It strips optional code fences and runs JSON.parse, but it does not validate the returned fields or ranges against a schema. The result gets cached with the user's id baked into the key, history gets recorded, and the parsed JSON goes back to the browser.

The hard part

The hardest security issue sits in the URL tools. Remote main validates every address returned before a fetch and revalidates each redirect, but a later audit found another DNS lookup at connection time.

The connect-time guard exists in a tested local commit. After refreshing the remote refs, that commit is still on no remote branch. I previously wrote about the issue as closed, which was wrong. The detailed code stays in a draft security note until the fix is pushed, reviewed, and confirmed in the deployed build.

What I'd do differently

I built too much for a beta with no users yet.

By launch, AlgoLens had five distinct tools: analyze, decode, optimize, replicate, repurpose. Plus a Chrome extension with its own token system, its own structured error contract, its own dev-versus-prod environment flipping, and end-to-end tests. All of that is real, working code I'm not embarrassed by. But none of it was validated against a single paying user, because there weren't any yet. I was polishing the fifth feature and the browser extension before I knew whether anyone wanted the first one.

The honest version is that building felt like progress and talking to users felt like risk, so I kept building. If I did it again I'd ship analyze alone, get it in front of ten real posters, and let their reaction decide whether feature two ever gets written. The scope I shipped is a bet that I already knew the answer. I didn't.

Numbers

A couple of things are worth stating. The backend runs 22 Jest suites and 173 tests against an in-memory database with the AI mocked. I re-ran all of them with email verification disabled for the test process, which matches the older analyze fixtures, and they pass. There is still no targeted test for prompt-injection payloads or non-boolean media flags. The model swap to gemini-flash also cut the per-call cost by a wide margin on a tool that calls the model on every analysis.

I don't have user numbers to share yet, which is the whole point of the section above.

Where it is now

It's in open beta, every feature free, live behind the positioning that finally describes what it actually does. Before I call the security work finished, I need to validate the media flags, add prompt-boundary tests, and get the connect-time DNS guard onto a reviewed remote branch. User feedback still matters, but these are release blockers I can prove from the repository today.

Status

Open beta · live on algolens.app

Have a project like this? Let’s talk