This is a submission for the Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady
What I Built
SlowPost is a slow post office for heated messages.
We've all written that message: the 1 a.m. resignation letter, the reply-all
with a little too much honesty, the text to your roommate about the dishes that
somehow escalates into questioning their entire upbringing. You don't regret
writing it. You regret sending it.
SlowPost changes the workflow. Instead of sending the message, you mail it to
tomorrow:
- Paste the draft exactly as angrily as you wrote it, plus a line of context ("to my roommate, about the dishes").
- An open-weight model rewrites it into a calm version — same facts, same request, none of the character assassination.
- You also get a neutral third-party summary of the dispute and a breakdown of what each side probably needs.
- The letter is then held in the post office. It doesn't go anywhere. In real mode it unlocks at 09:00 the next morning, when you can take out the calm version and decide — with a rested brain — whether to send anything at all.
SlowPost is a sibling of my earlier project WeVi, a private AI mediation
space where two people talk and only share summaries both sides approve. WeVi
mediates conversations that are already happening; SlowPost catches the message
before it becomes a conversation you'll regret.
Demo
- Live demo: https://slowpost.wevi.top — the public demo runs in demo mode: the overnight hold is shortened to 3 minutes so you can watch a letter unlock. (Production mode holds letters until 09:00 the next morning.)
slowpost.wevi.top
SlowPost demo: a roommate rant about takeout boxes, cooled from heat 54 to 0 — calm rewrite, neutral summary, both sides' needs, and the technique cards used
Code
Full source: https://github.com/awer123456/slowpost — TypeScript + Fastify +
zod, no database (letters live in a small JSON store). The technique-card
library (data/cards.json), the prompts, and the eval harness are all in the
repo.
bash
npm install
cp .env.example .env # point it at any OpenAI-compatible endpoint serving open-weight models
npm start # the post office
npm run eval # reproduce every number in this post
Every model is swappable by environment variable — run the eval and pick
your own default.
How I Built It
The open-source stack (and why it's the whole point)
A tool that stores your angriest, most private words has no business being
locked into one vendor's closed API. Every model in SlowPost is open-weight:
Role Model
Rewrite + analysis DeepSeek-V3.2 (open weights)
Embeddings Qwen3-Embedding-8B
Rerank Qwen3-Reranker-8B
Here they're served through SiliconFlow's OpenAI-compatible API — but open
weights mean the same code also runs fully locally (Ollama, vLLM) with one
environment variable changed. Hosted today, self-hosted whenever: the choice
is the user's.
Retrieval, not just vibes. SlowPost keeps a library of 17 de-escalation
technique cards — Nonviolent Communication, Gottman's "soft startup", repair
attempts, boundaries without threats, the overnight rule. For each draft it
runs hybrid retrieval (BM25 + embedding recall, merged with Reciprocal Rank
Fusion) and a cross-encoder rerank to pick the three techniques that fit
this specific conflict. Those cards are injected into the prompt, so the
rewrite is grounded in an explicit technique, not generic "be nicer" energy.
Strict structured output. The model must return JSON — calmRewrite,
neutralSummary, theirNeeds, myNeeds, techniquesUsed, riskFlags —
validated against a zod schema. Invalid output is retried once with the
validation error fed back.
The hold. Letters are stored with a release time (next morning, 09:00;
demo mode shortens it). The UI won't show the calm version until the hold
expires. Slowness is the feature.
I measured it instead of guessing
I built a small eval harness: 12 heated Chinese drafts (roommates, partners,
clients, landlords, a 1 a.m. breakup letter…), each labeled with the technique
cards a human would pick.
Retrieval Recall@3: 75.0% (first run: 54.2%) — strict scoring: each case
labels the two cards a human would pick, and near-miss cards (equally valid
techniques) count as misses. The first run's misses turned out to be mostly
my pipeline's fault, not the reranker's: I cut the candidate pool to 8
before reranking (the right card was often sitting at #9–14), and the
cards' written vocabulary didn't match how people actually type when angry.
Widening the pool, adding colloquial keywords to the cards, and phrasing the
rerank query as a task ("pick the techniques that fit this message") took
recall from 54.2% to 75.0%.
Generation, per model (schema success / avg latency / avg "heat reduction"
from a transparent lexicon metric that scores absolutes, insults, threats and
exclamation density before vs. after; latency measured from a remote dev box,
the in-region production server is faster):
Model Schema success Latency Heat reduction
DeepSeek-V3.2 12/12 (100%) 12.9s 83%
DeepSeek-V4.1-Flash 12/12 (100%) 14.8s 72%
Qwen3.5-35B-A3B 9/12 (75%)* 97.2s 78%
DeepSeek-V3.2 won on every axis — fastest and coolest-headed — so it's the
default. The newest model (Qwen3.5-35B-A3B) was 7× slower because of its long
reasoning traces: a neat reminder that "latest flagship" ≠ "best fit" for a
small, well-scoped job.
- Qwen3.5 was measured from the production server; 3 of its 12 calls failed on provider errors (503s, dropped connections) across repeated attempts from two networks, so its figures cover the 9 completed cases.
The eval ships in the repo (npm run eval) — run it, get your own numbers,
pick your own default model. That's the open-source way.
What I learned
Retrieval quality is the product. My first instinct was to spend all the
effort on the rewrite prompt. The eval disagreed: when the wrong technique
cards were retrieved, even a perfect prompt produced generic advice. Adding the
rerank stage changed which cards got picked for most drafts, and the rewrites
immediately became more specific — a concrete rota for the roommate, a boundary
script for the parents. Grounding beats eloquence.
Newest ≠ best fit. I benchmarked three open-weight models expecting the
newest flagship to win. DeepSeek-V3.2 — not the newest — was fastest and
produced the coolest rewrites, while Qwen3.5-35B-A3B's long reasoning traces
made it 8× slower for no quality gain. For a small, well-scoped job, a boring
model with a strict schema beats a brilliant one thinking out loud.
Define the metric before you argue about quality. "Is this rewrite good?"
is a vibes question. Forcing myself to define heat — absolutes, insults,
threats, exclamation density — as a dumb, transparent lexicon score made
quality measurable and comparable across models. The metric is crude and I say
so in the repo; a crude metric you can run beats a sophisticated opinion you
can't.
…and check your ruler before you blame the model. Midway through, a demo
run showed a calm rewrite scoring hotter than the original draft (54 → 64).
The rewrite was fine; the metric wasn't. It contained the word 垃圾 — which
means both "trash" (as in "shall we take turns clearing the trash?") and
"you're trash" — and one literal, innocent use maxed out the score. I fixed
the lexicon (masking literal uses, replacing ambiguous single characters with
actual insult phrases) and added a guard to the pipeline itself: if a rewrite
doesn't measure cooler than the draft, it regenerates once and keeps the
cooler version. The most dangerous bugs in an AI pipeline are the ones in
your own measuring stick.
Why Does Open Innovation Matter?
SlowPost stores the angriest, most private words a person types all week.
"A closed API is your only option" is the wrong default for that job — open
weights are what let the user choose where those words are processed,
inspect what processes them, and self-host the whole thing when they want to.
Openness is also what made this build possible at all. In one weekend, one
person benchmarked three open-weight models head-to-head, picked a default by
measurement instead of marketing, and shipped — because every model in the
stack can be swapped with an environment variable and re-evaluated with the
harness in the repo. No contracts, no vendor lineup deciding for you.
And it compounds after the hackathon: anyone can fork this, run the eval,
beat my 75.0% recall, and publish their numbers. A closed tool gets users;
an open one gets successors.
Slow down. Send tomorrow.
Top comments (0)