← Blog
Building Borker· 7 min read

Building an AI that writes in your voice: a technical deep-dive

By the Borker Team

Borker generates social posts that are supposed to be indistinguishable from what a specific founder would write. This post is the engineering story: the approaches we rejected, the architecture we landed on, and the handful of techniques that account for most of the quality. It's written for people who build with LLMs; expect config shapes and prompt mechanics rather than marketing.

The problem, stated precisely

"Write in my voice" decomposes into three requirements that pull against each other:

  1. Fidelity. Output must match a specific person's register, vocabulary, rhythm, and formatting habits, closely enough that their followers don't flinch.
  2. Data scarcity. A typical founder has 50 to 500 usable posts. Many have fewer. Some have a website and nothing else.
  3. Drift tolerance. The person's product, positioning, and mood change monthly. Whatever encodes the voice must be cheap to update.

Any architecture gets you two of the three. The interesting decisions come from refusing to give up the third.

Why not fine-tuning

Fine-tuning is everyone's first instinct, and it's the wrong tool for this job. We rejected it for four reasons:

  • Sample inefficiency at this scale. Fine-tuning wants orders of magnitude more data than a founder's few hundred short posts; against pretraining scale, an entire posting history is a rounding error. You can overfit a LoRA on it and get a model that parrots the training posts' topics rather than abstracting the style.
  • Drift means retraining. Requirement 3 kills it. Founder pivots the product, changes tone after a rebrand, or just stops using an emoji: with fine-tuning, that's a training run. With configuration, it's an edit.
  • No inspectability. When a fine-tuned model writes something off-brand, your debugging tool is vibes. When a context-driven system writes something off-brand, you can point at the exact config section that failed to constrain it.
  • Per-tenant weights are an operational tax. We're a multi-tenant SaaS. One set of weights per workspace means a model registry, per-tenant inference routing, and cold-start pain. Structured context rides along with every request instead.

So: no custom weights. Voice lives entirely in context, and the engineering problem becomes "what context, in what shape, assembled how?"

Voice as a typed schema, not a prompt

The core design decision: voice is structured data, not prose instructions. A workspace's brand configuration looks roughly like this:

{
  "identity": {
    "name": "Acme",
    "mission": "...",
    "whatWeAre": "AI content automation SaaS"
  },
  "voiceAttributes": {
    "formality": 3,        // 1-10, continuous dials, not adjectives
    "friendliness": 6,
    "clarity": 8,
    "brevity": 8,
    "technicalDepth": 7
  },
  "terminology": {
    "use": ["shipped", "founders", "pipeline"],
    "avoid": ["leverage", "unlock", "game-changer", "delve"]
  },
  "traits": [
    { "trait": "dry humor", "application": "one wry aside max, never forced" }
  ],
  "writingPrinciples": [
    {
      "title": "Lead with the change, not the emotion",
      "goodExample": "scheduled publishing now works for linkedin.",
      "badExample": "We're thrilled to announce our latest feature!"
    }
  ],
  "toneAdjustments": [
    { "context": "linkedin-announcement", "adjustment": "+2 formality, allow 3 paragraphs" }
  ]
}

Three properties of this shape carry most of the value:

Dials are numbers. "Formality: 3" survives the trip through a prompt far better than "casual but competent." Numbers give the model a stable coordinate system, and they give users a UI: sliders instead of a prompt-engineering textarea. When a user nudges formality from 3 to 5, the diff is meaningful and reversible.

Principles are example pairs. Every writing principle carries a good example AND a bad example. The pair defines a decision boundary rather than a direction. In our evals, paired examples outperformed positive-only examples by a wide margin on "would the founder actually post this" ratings, because most voice failure is the model doing something plausible-but-wrong, and only the negative example tells it where plausible ends.

Negative constraints do disproportionate work. The avoid list plus a set of global anti-patterns kills the majority of what people recognize as AI smell. Which deserves its own section.

The anti-AI-tell layer

There is a specific, enumerable set of patterns that mark text as machine-written: gerund openers, "excited to" constructions, exclamation inflation, the em-dash cadence, engagement-bait closers, hedged both-sidesing, the word "delve." We maintain that list as an explicit rule block that ships with every generation request, on top of the per-brand avoid list.

Two engineering notes on it:

  1. It's a living list, and it decays. Models update, the internet's median style shifts, and new tells emerge (the current generation loves structural reframes: "It's not X. It's Y."). We treat the tell-list like a dependency that needs upgrades, with periodic review against fresh model output.
  2. Enforcement beats instruction. Rules stated once in a system prompt get diluted as context grows. We found meaningful gains from re-asserting the constraint block close to the generation instruction (recency matters in long prompts) and from validating output after generation: length limits per platform are checked in code, not trusted to the model.

Assembly: the context stack

A generation request is assembled from layers, in a deliberate order:

1. Task frame        what to write: platform, content type, length budget
2. Brand identity    who is speaking: mission, product, positioning
3. Voice config      dials, terminology, traits, principles (with pairs)
4. Platform adapter  channel-specific structure and format rules
5. Grounding         the actual material: article, changelog entry, topic
6. Recent history    last N posts on this topic area, for diversity
7. Constraint block  anti-tells + hard rules, restated last

Two of these layers are less obvious than they look:

Platform adapters (4) encode that the same person writes differently per channel without becoming a different person. The delta is structural, not personal: X gets hard length discipline and line-break rhythm; LinkedIn gets paragraph structure and a formality bump; Farcaster reads closer to X but with different community norms. The voice config stays constant; the adapter reshapes it. Modeling this as "same voice, per-channel transform" rather than "per-channel voice" halved our config surface and fixed the uncanny effect of LinkedIn posts that read like stretched tweets.

Recent history (6) exists because voice includes not repeating yourself. Without it, an automated system happily writes the same insight three Tuesdays in a row, which no human does. We feed recent post topics back into the request so the model can diverge. Repetition is a voice violation too.

The review loop is part of the architecture

Every generated post lands in a review pipeline as a draft. Nothing publishes without either explicit approval or an opt-in automation rule, and even opted-in automation passes sensitivity checks (configurable keywords and principles that force a human hold on touchy topics).

We initially built review as a safety feature. It turned out to be the highest-leverage voice feature in the system, for a reason that's obvious in hindsight: edits are labeled training data. When a user rewrites a draft's opener for the fourth time, that's a failing rule announcing itself. The system's job is to make that signal cheap to act on: promote the recurring edit into a terminology rule, a principle pair, or a tone adjustment, so the fix compounds instead of evaporating.

That's the practical version of the fine-tuning loop, running on configuration instead of weights: human feedback, applied to an inspectable artifact, at edit-cost instead of training-cost.

The context stack: seven layers assembled into a generation request, flowing into the review pipeline, with an edits-become-rules feedback loop back into voice config

Cold start: extracting voice from a website

Most users don't arrive with 500 labeled tweets. They arrive with a URL. Onboarding runs an extraction pass: scrape the site, pull whatever public writing exists, and have a model propose the initial config: dials estimated from register, terminology harvested from actual usage, candidate principles from observable patterns.

Two hard-won lessons from that pipeline:

  • Extraction needs a sufficiency gate. Scraping a login wall or a JS-shell page yields nothing, and a model asked to extract voice from nothing will cheerfully hallucinate a median brand. We gate on actual signal: if the scrape plus user-provided context carries too little usable content, we stop and ask for a better source instead of saving a placeholder profile. Silent fallback to generic is the worst possible failure mode for a voice product, because it poisons everything downstream.
  • The proposal is a draft, not a verdict. Every extracted config field is presented for review during onboarding. Partly for accuracy, mostly for trust: a founder who has corrected their own formality dial believes the system in a way that no amount of magic-black-box accuracy would buy.

Writing principles in Borker: each rule carries a good example and a bad example, defining the boundary the model must stay inside

What moved the needle (honest ranking)

If I had to rebuild this with a week and rank techniques by quality-per-effort:

  1. Negative constraints (global tells + per-brand avoid list). Cheapest, biggest.
  2. Paired examples in principles. The boundary effect is real.
  3. Numeric dials. Mostly for stability and UX; the model effect is moderate but the maintainability effect is huge.
  4. Platform adapters. Fixes a failure class nothing else touches.
  5. Constraint restatement + post-hoc validation. Boring, mechanical, reliable.
  6. History-aware diversity. Invisible when present, glaring when absent.

And the one thing I'd tell past-me: resist the mega-prompt. Every time voice quality disappointed us, the temptation was to write more prose instructions, and the fix that actually worked was more structure: another typed field, another example pair, another validated constraint. Prose instructions are where voice rules go to get averaged away, which is fitting, because averaging is the whole disease.

We run this system on ourselves: every post on Borker's own X, LinkedIn, and Farcaster comes out of this exact pipeline, drafted from our feeds and changelog, reviewed in our own inbox. This blog gets redistributed through it too, which means an AI wrote a social post about this article, in our voice, and you probably couldn't tell. That's the bar.

If you want to see the extraction pass on your own writing, the free brand voice analyzer is the cold-start pipeline described above, pointed at your site. And if you're building your own version of this: steal the never-list idea first. It's the best quality-per-token trade in the whole system.

New here? Borker is the AI content engine for founders.

We learn your voice, watch your news feeds, and ship posts to X, LinkedIn, Farcaster and your blog while you build the actual product.