Someone shared a token counter called TokenLens on V2EX, and it stuck with me because it's honest about the one thing most of these tools quietly fudge. It's a single-file, pure-frontend app. It does exact tokenization for OpenAI's tokenizers — o200k and cl100k, loaded via js-tiktoken from a CDN — and for everything else (Claude, Gemini, DeepSeek, Qwen, GLM, Kimi, Grok, 22+ models in total) it applies empirical coefficients and estimates. The clever part: the UI slaps a "precise / estimate" badge on each result so you know which is which. Model list and prices sync from LiteLLM's open JSON, with a manual refresh and an offline fallback. Text never leaves the browser; the only outbound request is the pricing data.
That's a genuinely thoughtful design. But if you've shipped tools people rely on for money decisions, you know where the crack is: the honesty lives in a badge. A badge is a promise the frontend makes to itself. Nothing structural stops an estimate from being read, copied, or screenshotted as if it were exact. When the number feeds a cost projection or a context-window sanity check, "trust the label" isn't the same as "the system can't hand you a precise number it didn't actually compute."
What I'd move, and why
The rebuild I'd pitch isn't "make it a big app." It's narrower: move the part that must be true onto a backend that can enforce it, and leave the rest exactly where it already works well.
Exact tokenization is deterministic and reproducible — same text, same vocab, same count every time. That belongs somewhere you can actually run the tokenizer end to end. If you run Python online as the counting service, you compute real token counts for the tokenizers you can genuinely load, and for any model you cannot tokenize exactly, the endpoint returns a value that is *typed* as an estimate at the source. The client never gets to decide which bucket a number falls into. The API answers with `{count, mode: "exact" | "estimate", tokenizer}` and the boundary is a property of the response, not a styling choice.
That reframes the badge entirely. In the client-only version, the badge is decoration over a guess. In the hosted version, the badge is just a faithful render of a field the backend already committed to. If the backend can't run a given tokenizer, it says `estimate` and it physically cannot say otherwise. That's the difference between labeling a boundary and enforcing one.
The pricing table is the other quiet liability
TokenLens fetches LiteLLM's JSON live and falls back to a bundled copy offline. Fine for a personal tool. But pricing is the thing that goes stale in the worst way — a vendor cuts a per-million rate on Tuesday, upstream updates on Friday, and for three days your "cost comparison" is confidently wrong. There's no seam to patch it by hand.
So I'd cache the pricing table in a real database instead of treating it as a JSON blob. Pull the LiteLLM-style data into a SQLite table — model, input price per million, output price, context window, tokenizer family, last-updated — and let the counting service read from that. The payoff is the manual override you actually need: when a price changes before the upstream source catches up, you open the SQLite editor and correct the one row, timestamped, without redeploying anything. The refresh job overwrites from upstream on a schedule; your manual edits are just rows with a newer `updated_at`, and you can decide which wins. That's a maintenance story a single HTML file can't offer, because it has nowhere durable to keep an edit.
A table also makes the estimate coefficients auditable. TokenLens invited people to argue about its coefficients — that's healthy. Put them in a column next to each model and the argument becomes "change this number and re-check," not "read the source and rebuild."
Where the client-only design actually wins — and where I'd keep it
Here's the part I don't want to lose in the rewrite: TokenLens's biggest feature is that your text never leaves the machine. For anyone pasting proprietary prompts, contracts, or unreleased copy just to check length, that privacy guarantee is the whole reason they'd use it over a hosted counter. The moment you move tokenization to a backend, you've broken that promise for the exact path — the text has to travel to get counted.
So I wouldn't force everything server-side. I'd keep a client-side fast path for the tokenizers that genuinely run in the browser and for rough local estimates, and reach the backend only when you want an authoritative count or current pricing. You run HTML online for the frontend — the bar charts showing where your text sits in each model's context window, the red overflow markers, the dark mode, the bilingual UI, all of that is presentation and stays exactly as light as it was. The frontend just gains a clear choice: "count locally, private" versus "count on the server, authoritative." Two modes, both honest about their trade-off, instead of one mode pretending estimates are facts.
The boundary I won't cross
A fair question: can't the backend just tokenize *every* model exactly?
No — and pretending otherwise would repeat the original sin at a different layer. Exact tokenization requires the actual tokenizer for that model. Where you can load one in the Python runtime, you get a real count. Where you can't, it stays an estimate, full stop. Wiring a counter to model APIs that happen to be available on the platform is a different thing from having each vendor's tokenizer — availability of a model to *call* does not hand you its exact token boundaries. I'd wire the tool to what's genuinely reachable and mark everything else as estimate, rather than manufacture precision I can't defend. That's the same discipline TokenLens showed with its badge, just moved somewhere it can't be bypassed.
Whether these empirical coefficients are close enough for real budgeting is unverified — that depends on the vendor and the text, and it's exactly the kind of claim that should carry a visible "estimate" flag rather than a confident number.
What you actually ship
Stripped down, it's a small, sharp tool: a hosted Python counting endpoint that returns a count plus an honest mode flag, a SQLite pricing-and-coefficient table you can hand-edit when reality moves faster than upstream, and a light HTML frontend that keeps the private local path for people who need it. Nothing here needs a framework or a fleet of services. It's the kind of thing one developer can stand up, publish, and — because the pricing data and the honesty are now maintainable assets rather than a frozen file — keep trustworthy long after launch. The original was a good idea trapped in a single file. The rebuild is the same idea with the load-bearing promise moved somewhere it can hold weight.