Kindling
A voice engine for personal branding - it provokes you into your own voice, not "GPT writes your posts."
Design + Build - strategy, interface, engineering · Next.js 15 · Supabase / pgvector · BullMQ worker · multi-model LLM
A voice engine for personal branding - not "GPT writes your posts."
Most tools hand you a finished post and call it help. Kindling does the opposite: it provokes you into saying what you actually think, then dresses that thought in your own voice. It opens with two deliberately polarized takes - a Socratic nudge, not an answer - you react in your native language, and the system shapes that reaction into a post, carrying your intonation into English. You publish. It learns from every edit and stays half a step ahead.
One line carries the whole architecture:
Math owns behavior. AI owns craft. Not a wrapper around a model - a symbiosis of two engines, each doing what the other can't.
A living organism, not a one-shot generator
Most "AI writing tools" start from zero on every request - they have no memory of who you are. Kindling carries state that lives over time: it remembers the author and grows with them. That state has two organs, and they pull in different directions on purpose.
The Heart - readiness that moves with you
How bold a topic an author is ready for isn't a setting - it's a value that climbs and cools with real feedback. The clever part is what it rewards: courage is earned by range and consistency, not by one runaway obsession. The maths is tuned so a well-rounded veteran outranks a single-issue zealot - a deliberate asymmetry, not a side effect.
Per-facet momentum is an exponentially weighted moving average with decay on read:
momentum ← momentum · 0.5^(Δt / halflife) + sign (halflife = 21d, +1 on ok, −1 on flop)
Negative Δt can't amplify (clock-skew safe).
A separate monotonic floor (+1 only on a fresh transition into ok, never decreases)
keeps the author from sliding back when momentum goes stale.
Both fold into one scalar:
risk = clamp[0.05…0.9]( base + w_act·activity + Σ w_facet · decayed_momentum )
base = 0.2, w_act = 0.01, w_profession = 0.04, w_topic = 0.02, w_interest = 0
The weights ARE the philosophy: w_profession is deliberately small against the 0.9 ceiling,
so high readiness is bought with breadth + persistence, not one hot tag. Four facets by
distance to the core - profession (core), adjacent knowledge (growth vector), hobby
(w = 0, decoration not a path), general background.The Wave - catching a trend as it's born
In parallel, Kindling watches the author's niche for topics that are accelerating - the moment a conversation starts to climb, not yesterday's headline everyone has already picked over. Being first to frame a rising topic reads as prescient; arriving late reads as everyone else. So the Wave measures the rate of change, not the size of the crowd.
An always-on worker measures velocity anomaly, not volume. For communities with history,
a shrinkage z-score blends the community's own baseline with a global prior through a
pseudo-count k (baseline matures at ~30 samples), so small samples lean on the prior and
large ones trust the community - no cliff at the threshold:
z = (v − μ_eff) / σ_eff, μ_eff = (n·μ_sub + k·μ_prior)/(n + k)
A spike qualifies at z ≥ 2.5 and becomes "heat" via a logistic centered on the threshold
(heat(2.5) = 0.5). Cold / low-history sources fall back to a gravity floor mapped into
[0 … 0.45] - strictly below 0.5, so a qualified spike always outranks merely "popular."
Freshness decays with a 12h half-life.The feed is planned, not listed
A calendar that says "post on Tuesdays" dies the first time Tuesday is empty. Kindling builds a slot plan shaped by the author's current readiness instead - the profession always leads, trends are seasoning, and a guaranteed floor means there's always something worth opening. The plan decides the shape of the week; the content fills in underneath it.
Slot-first. computeMediaPlan(risk) sets composition by band - low (<0.34): safe range
(hobby-evergreen, general/industry hot-takes); mid (<0.67): industry leads; high: two
industry slots + general. Regional slots are pinned to a minority (2/7 low, 1/7 above).
Sources (thematic search ∥ Wave) gather into one pool in parallel; a pure function assigns
candidates to slots by position in the plan, not by content. waveCap = 2 (seasoning, not
spine); cold-start fills empty slots; the floor keeps it alive. Hard rule against "salad":
profession and hobby never merge into one vector.Repeats are filtered by cosine similarity on vector embeddings (pgvector / halfvec(512)),
duplicate at cosine ≥ 0.86, bounded window (90d / last 50). No embedding key → lexical
fallback (trigram Jaccard ≥ 0.5). O(1) in history size, and old topics may deliberately
resurface. Fresh niche news dedups by source_url, not semantically - otherwise an on-topic
fresh item would be killed against the author's own history.The moat - it learns your hand
Generation isn't the moat; any model generates. The moat is that every edit teaches. Kindling watches how you change a draft, finds the corrections you keep making, and quietly closes the gap to your style. And it does this in the open - a learned change surfaces as a visible, optional observation. The system never rewrites you in silence.
Word-level edit-distance on every final; diffs under ~20 words dropped as noise. Each edit
is classified by an LLM; every 5 posts, patterns aggregate - if the same style patch-operator
recurs in 3 of 5, one pending voice observation surfaces. Applied per session via optimistic
concurrency (vacation-safe), not on a timer.Phase moves reflection → semi_reflection → mature → graduated → alumni over a 5-final window
on: choice balance (|0.5 − plus_share| ≤ 0.05), reaction-to-chosen-extreme semantic closeness
(cosine ≥ 0.75, best-effort), and edit ratio (avg < 0.15). Forward motion is smooth and
unannounced (the author just notices the extremes "tightened"); regression (edit-ratio > 0.3)
is notified, not silent. Asymmetric brake: ≥2 self-flagged flops block promotion but never
accelerate it - fatigue can only make the system more cautious.Honesty is an invariant
What makes it a product, not a toy, is what it refuses to do. There is no numeric scoring anywhere in the interface - no "Voice Match 88%." The system holds internal confidence but never leaks it as a number, because a score would invite the author to perform for the meter instead of saying what they mean.
- It never invents an opinion for the author.
- It listens to public niche trends (Reddit) - and never touches the author's own metrics.
- No autopublishing. The human presses publish.
- Profile and phase changes never happen silently.
- The only feedback it asks for is one honest button: worked / didn't.
The human is always the final decision.
Result
The first post built this way: 1,708 impressions - 4.5× the author's usual reach. Not virality, not an algorithm hack - a real voice finding its people once it finally sounds like a person.
(Framed honestly: the author's result, not a promise of yours.)
Next.js 15 + TypeScript · Supabase (Postgres 17 + pgvector/halfvec, Auth) · background BullMQ
worker on a dedicated node + Redis · multi-model LLM access (fast models classify, strong
models create; embeddings on a separate provider) · full RU/EN localization - think in the
native language, publish in English with voice preserved · niche-signal collection (Reddit)
via a workflow orchestrator · production on Vercel.