WspWrapped: chat metrics without storing a single message
Upload a WhatsApp export and get 30+ metrics about the conversation — without storing a single message.
- Type
- Consumer App
- Year
- 2026
- Role
- Founder / Product Engineer
- Stack
- Next.js · TypeScript · Supabase · Vercel
- Status
- Live

By Jorge CortésPublished
Summary
I built WspWrapped so anyone can upload a WhatsApp export and get 30+ conversation metrics without storing message text. The parser runs in memory. Only aggregates persist. Privacy is the architecture. Surfaces labeled "AI" must run on those numbers, not on the chat — or they are copy over features.
Problem
WhatsApp is how people actually talk. It is also a log nobody reads as data. Every 1:1 and every group hides the same questions: who writes more, who starts, who replies at 3am, who goes silent, who deletes. People argue from memory. They screenshot a streak. They cannot see a year.
WhatsApp already has Export chat: Options, More, Export chat. Without media, the file is lighter. You get a .txt, or a .zip that still wraps a .txt. The file is timestamps, names, and lines. It is unreadable as a conversation metric. That is the hole.
I did not start from "add AI to chats." I started from a file the user already owns. The landing lists the questions because they are the job: who replies slowest, who ghosts, who starts, who deletes, who stays up. The data is in the export. The job is to turn it into numbers without turning it into a database of other people's messages.
A consumer product has a second job the log does not: a card you can post. People do not open a CSV on Friday night. They open Stories. If the report cannot leave the dashboard as a 9:16 frame, the product dies in a screenshot folder.
Constraints
Personal chat is not analytics telemetry. If I persist message text, I become a store of intimate data. That is a product I refuse to operate. The landing says 100% private. The FAQ says we do not store the content of messages or files. The architecture has to make that true. A privacy policy is not a data model.
Export format is not a spec I control. Android and iPhone write different lines. Locales write dates as dd/mm/yyyy or m/d/yy. Some lines wrap timestamps in brackets with seconds. Some inject Unicode left-to-right marks. "Without media" still zips a text file. System messages are localized: "This message was deleted", "Este mensaje fue eliminado", "Mensagem apagada". If the parser assumes one locale, it lies.
I ship on Vercel. Unzipping and loading tens of megabytes of text into a serverless function hits time and memory. A 50MB chat is a real export, not an edge case.
Usage is episodic. One chat, once, to post a story. A monthly subscription fights that shape. Credits have to exist without turning the account into the chat.
The product UI ships in Spanish, English, and Portuguese. Root with ES selected is Spanish. EN lives at /en. PT lives at /pt. The parser has to survive all three locales inside the file, not only in the header.
Group and 1:1 share a pipeline. The analysis adapts. I cannot assume two participants. I cannot assume a couple.
Cards are HD, 9:16, made for Stories, Reels, and TikTok. Pro copy on the Portuguese page says HD export without a watermark. A unique share link is a second path. I do not have impression counts. "Viral" on the landing is marketing, not a metric I can stand behind.
I work as a product engineer from Santiago. The canonical page for that is Jorge Cortés.
Architecture I designed
The user exports a chat, drops a .txt or .zip, and gets a report. The last node is a share card, not a CRM. The landing says metrics in seconds — a product claim, not a benchmark I published.
The file is never written to a messages table. Parse happens in request memory, or in a short-lived worker, or on the device. I persist aggregates in Supabase so a share link and a credit debit can exist: counts, histograms, scores, participant labels the user already sees. Raw text is dropped. The session is not the chat.
The FAQ disagrees with itself. One answer says processed locally without storing messages. Another says processed in memory, only statistical aggregates stored — numbers, not text. Locally can mean the browser. In memory can mean a function that dies at the end of the request. I have not published the runtime here.
Credits live on the account in Supabase: remaining credits, not remaining messages. Auth exists. The header has Log In. You sign up free. The session is the wallet behind a share URL, not a replica of the export.
Packs are public. One credit equals one full report. Three credits are $2.99. Ten credits are $4.99. Credits never expire. No recurring fees. Pro is the 30+ unlock. The free tier is basic metrics. Checkout is not on the landing as a named vendor.
I did not observe a payment processor on the public pages. I will not invent one.
Stack and why
Next.js is the product shell: three locales, the landing, the upload path, the report, the share page. Locales in the header are routes, not a plugin.
TypeScript is not a fashion choice on a parser. The export is messy text. Types keep line kinds honest: user message, system tombstone, media omission, date header. Without that, the parser becomes regex folklore.
Supabase is auth, credits, and aggregate rows. It is not a message lake. If a table looks like a chat, I designed the wrong store. Access control belongs on the account, not on other people's sentences.
Vercel is where the site runs. Serverless time and memory are part of the product. A worker, an Edge handler, or the client are different answers to the same envelope. Stack: Next.js, TypeScript, Supabase, Vercel. Year 2026. Role: Founder / Product Engineer.
The difference from other products I shipped on the same four names is the data contract. WspWrapped may remember numbers. It may not remember sentences.
Technical decisions
Drop the text. Keep the histogram. Most of the 30+ surfaces are descriptive statistics. Message counts. Activity heatmaps. Response-time distributions. Emoji frequency. Night-owl hours. Double-texting. Deleted-message counts, when the export still has a tombstone. Initiative: who starts. Ghosting, as shipped on the landing, is a silence pattern — a threshold on gaps, not a classifier I have evidenced. If that is a model, it is not documented here.
Name the marketing, then name the math. The landing sells Couple Dynamics, Interest Level, Compatibility on five dimensions, AI Predictions, The Roast, Ghosting Zone, Deleted Messages, Hall of Fame, Heatmap, Top Emojis, Romance Score, Night Owls, Double Texting, Psychological Insights. Interest Level is described as response times, initiative, and engagement. Those are features I can compute from timestamps and senders. Compatibility is a score. A score can be a weighted sum. It can also be a model. I will not upgrade a label into a neural net.
AI as a capability, not a demo. The product claims an AI layer. "AI Predictions" is "the future of your chat based on patterns detected by artificial intelligence." A demo would POST the whole transcript to a hosted LLM and paste the completion into The Roast. That architecture transmits text. It fights the FAQ. If those surfaces exist in production, they must run on aggregates and features in the same request, or they are templates over those features.
I do not name an LLM vendor. I have not evidenced one. That gap is the essay. The work I take under AI systems is the same rule: the model is a capability inside a product with a data contract. It is not the homepage.
Credits, not a subscription. Consumer, episodic. Pay when you want the full 30+. Free basic metrics are the funnel. Credits that never expire lower regret. The trade-off is no monthly revenue smoothness. I accepted that. A subscription on a one-off curiosity product trains people to cancel, not to analyze.
One pipeline, two chat shapes. Individual and group. The engine does not fork into two apps. Participant cardinality changes the report. Couple labels are a view, not a schema that assumes two rows.
Cards for organic distribution. HD, 9:16. No watermark on Pro, per the Portuguese page. Designed to be posted. The share link does not need a download. I am not reporting reach.
Auth is a wallet. You log in to hold credits and to own a share URL — not so I can reread your chat next week.
What went wrong
The first failure mode is not a gradient. It is a date.
A line that parses in English on Android is a different line on iPhone, in Spanish, with a Unicode mark before the bracket. If I split on commas, I eat the wrong field. If I assume m/d/yy, I swap day and month for most of Latin America. If I ignore system lines, deleted messages vanish. If I treat system lines as user text, I invent a participant named "This message was deleted."
Deleted messages are a lower bound. They are only countable when the export still has a tombstone line. If someone deletes for everyone and the export omits it, the metric is silent. Shipping "who deletes the most" as a hard number without that caveat is a lie. I would rather publish a lower bound than a fiction.
Large chats are the second failure mode. Unzip plus parse in memory is simple. Simple dies on a year-long group. A serverless function that loads 50MB of text will time out or exceed memory. Streaming is the obvious fix and a worse parser. Client-side parse is the other fix and a privacy bonus: the raw file never leaves the device. A chunked worker is the third. I will not claim which one shipped.
The third failure mode is copy versus contract. The landing says advanced algorithms and patterns detected by artificial intelligence. The FAQ says statistical aggregates. Both can live on one site. Only one of them is a data-flow. If The Roast is a template, calling it AI trains the wrong expectation. If it is an LLM call on full text, the privacy architecture is a poster. That tension is not resolved in public.
The fourth is the paywall after curiosity. Free basic metrics are the funnel. Too thin and they bounce. Too thick and they never buy. I do not have conversion numbers.
How I fixed it
I treated the export as a family of formats, not as one regex.
- 01/
Parse timestamps and sender separately from body. Locale and OS change the left side of the line. The body is not where the date lives.
- 02/
Catalog system messages in ES, EN, and PT. Deleted, omitted media, and other tombstones are counts, not quotes. If the line is missing, the count does not go up.
- 03/
Bound memory. Fail on a file that will not fit the runtime, or move parse to the client, or chunk it. Silence is worse than an error. Which path is in production is still a TODO.
- 04/
Persist only aggregates. Share URLs and credit debits read numbers. They never read a transcript, because there is no transcript to read.
- 05/
Sell a pack, not a month. One credit, one full report. Free basic metrics stay free. Credits do not expire, so a second chat next year is still paid for.
- 06/
Keep the AI labels in the product if they ship, and refuse to send the chat to a model vendor I have not confirmed. Features in, text out.
The fix for a bad parse is a better parse, not a model. The fix for privacy is not storing the file.
Result
- Conversation metrics
- 30+
- Languages (ES, EN, PT)
- 3
- Three-credit pack
- $2.99
- Messages stored
- 0
WspWrapped is live in 2026, in three languages, with a free tier and credit packs instead of a subscription. Pro is 30+ metrics for one credit. Flow: export, drop .txt or .zip, metrics in seconds, 100% private — as the product states it.
I am not publishing user counts, TikTok views, or revenue. The confirmed numbers are the product surface: metric count, locales, pack price, and zero message text stored.
Evidence



The live product is wspwrapped.online. Footer: Privacy, Terms, Cookies. I am not inventing a DPA.
What I'd do differently
I would label statistics and model output as different things in the UI. Heatmaps are counts. The Roast, if it is a template, should not wear an AI badge. If it is a model, it should say it runs on features, not on the transcript.
I would publish the parse runtime. Client, route handler, worker. "In memory" is true of many bad designs. "The file never leaves the device" is a stronger sentence, if it is true.
If parse is still on the server, I would try client-side parse as the default. The privacy bonus is real. The serverless limit goes away. The cost is a heavier browser.
I would keep the no-recompute rule. New metrics need a new upload. I would not start storing text "just for a week." That week becomes the product.
I would not chase viral counts I do not have. Cards designed to be posted are a design decision I can defend. Views are not.
Until the TODOs are replaced, the architecture I will stand behind is the one I can see: parse, drop text, store numbers, charge a credit, render a card.