Skip to content
Back to case studies

Case study

GoReach

LinkedIn writing for founders, live as a product: it learns your voice from posts you have already written, drafts in it, grades each draft against a fixed reach checklist and publishes through LinkedIn's official API. A bounded LangGraph agent on one FastAPI service, with every action gated and audited.

Headline metric

2.3x

lift with voice setup

Stack

  • Next.js
  • FastAPI
  • LangGraph
  • pgvector
  • Supabase
  • Modal
  • Cloudflare Workers

LinkedIn writing for founders: posts that sound like the person posting, not like an AI.

The problem

Every AI writing tool for LinkedIn produces the same output. Over-structured, suspiciously enthusiastic, unmistakably artificial. You can spot an AI-written post in the first sentence. The irony is that LinkedIn is one of the most personal professional spaces online. It is where people build reputations, not just content calendars.

The category was already crowded. The gap was not "AI for LinkedIn content". It was AI that actually sounds like the person posting it. Not a generic professional voice. Not a template dressed up with industry keywords. The way you actually write when you are thinking clearly and not staring at a blank box.

I started building GoReach in early 2025. The first version was embarrassingly simple: a prompt, a text box, and a LinkedIn card preview. Even that version pointed at something real. When a post matched how someone actually wrote, the reaction I got was different. Not "this is useful". More like "wait, how did it know that".

The approach

My first instinct was to treat voice matching as a fine-tuning problem. Train a model on the user's posts, get output that sounds like them. That collapsed quickly. Most people have written fifteen to thirty LinkedIn posts in their life, which is not enough to fine-tune anything meaningfully. And fine-tuning per user is economically absurd at any scale.

The shift came when I reframed voice matching as a context injection problem, not a training problem. The model already knows how to write in thousands of different styles. What it does not know is which style is yours. Give it your actual posts as context, tell it to write like that person, and the problem changes shape.

The beta used two retrieval layers in the same prompt: three to five of the user's own posts as in-context examples, plus high-performing LinkedIn posts pulled by semantic similarity to the topic. One anchored style, the other anchored structure. Users who completed voice setup rated their posts positively at roughly 2.3x the rate of users who skipped it. Same model, same pipeline. The difference was whether the system had something true to work with.

From feature to product

The beta proved the thesis. The product is what I built on top of it. GoReach today is a LinkedIn writing tool for founders who know posting would bring in business and have never had a spare hour to do it. It is live at trygoreach.com with a 30-day trial, a Pro plan and a Done-for-you service.

The weekly loop takes about ten minutes. Train your voice once from posts you have already written. Answer one weekly question about your own work, which becomes the raw material. The agent drafts in your voice, grades the draft against a reach checklist, offers one-click fixes and a first comment for the hour after you post, then publishes or schedules through LinkedIn's official API. Nothing goes out without your approval, and it never auto-likes, auto-connects or auto-comments on your behalf.

Two product decisions came straight out of the beta:

  • Your voice samples are the authority. When a voice profile exists, its samples bind the output and the generic reference posts drop out entirely. Samples shape form, never facts: the generator is not allowed to borrow a claim from an old post to make a new one sound specific.
  • The reach grade is a fixed checklist, not a model's opinion. Hook above the mobile fold, outbound links, hashtags, scannability, signs of AI writing. The same post scores the same every time, and every problem comes with the fix.

Architecture today

GoReach architecture: a Next.js app on Cloudflare Workers streams to a FastAPI service on Modal in eu-west. Inside it, a LangGraph agent graph routes each turn to compose, grade, fix, first comment or clarify, every action passes an action runner that checks entitlement, confirmation, idempotency and quota, and an LLM router sends each task to its model with a fallback. Supabase Postgres with pgvector sits beside it, scheduled workers publish through the LinkedIn API, and billing and email go to Dodo Payments and Resend.
One Python service behind a thin edge app, placed next to its database.

The beta ran as a Next.js app, a Node backend and a separate Python service for the AI. As the product grew, the AI stopped being a side service and became the product, so I folded the backend into one FastAPI service and moved it to Modal in eu-west, beside the Supabase Postgres it reads, which saves a network hop on every query. The web app runs on Cloudflare Workers and streams each agent turn over server-sent events.

  • A bounded agent, not an open-ended planner. Each turn is classified (a fast rule first, then a small model) and routed through a fixed LangGraph graph: compose, grade, auto-fix, first comment, clarify or disambiguate. Fix loops are capped. When the agent needs an answer from the user, the graph pauses, checkpoints to Postgres and resumes exactly where it stopped.
  • Every action goes through one runner. Entitlement, input validation, a confirmation token, idempotency and quota are checked in that order, then the action is logged with its token and dollar cost. Publishing needs a single-use confirmation token, so no message text can ever trigger a post.
  • Model routing per task. Writing and rewriting run on GPT-4.1, small jobs like intent, hooks and persona run on GPT-4o mini, and every route has a Llama 3.3 70B fallback on Together AI. The service refuses to start if a route has no credentials.
  • Scheduled workers instead of a queue. Publishing, first-comment reminders, billing reconciliation and the idea sweep run as scheduled Modal functions that claim rows straight from Postgres. LinkedIn tokens are encrypted at rest.
  • Retrieval on pgvector. Voice samples and a reference corpus of human-written posts are embedded in Postgres; generated posts are never fed back into the corpus.

Quality, measured

I treat generation quality as an engineering problem with tests, not a vibe.

  • Copying, cut by 98%. Voice samples were leaking whole phrases into new posts. Dropping the single sample closest to the topic before prompting cut copied five-word phrases from 73 per post to 1.3.
  • An anti-slop pass that has to earn its place. A repair pass rewrites a draft only if the result has strictly fewer AI tells than the original.
  • A founder simulator. An LLM plays a founder through whole sessions, with guards against a simulated user who agrees too easily, and logs every number the generator invents so fabrication is counted, not guessed at.
  • Killing a metric that lied. I built a voice-similarity score, then tested it leave-one-out: for three of five authors it rated a stranger closer than their own other posts. It no longer counts. Knowing what not to trust is part of the work.

What I learned

My internal benchmark for the product is not "the user liked it" or "they published it". One beta user pasted eight of their past posts. Four were about failure, written in a specific register: short sentences, no softening language, no lessons-learned coda. They asked GoReach to write about consistency. The output opened with "I missed thirty days of posting last year. Not because I was busy. Because I was embarrassed." They had not written that. They had not prompted for it. The message back was just "okay this is actually unsettling".

A GoReach composer card showing the generated line "I missed thirty days of posting last year. Not because I was busy. Because I was embarrassed." with a small handwritten note next to it that reads "okay this is actually unsettling".
The user moment that became the internal quality bar.

That reaction, unsettling in a good way, became the bar. Once you have a benchmark that specific, prompt and retrieval work stops being vibes-driven and starts being a search problem with a target.

The second lesson is about restraint. The most valuable things I built into GoReach are the things it refuses to do: post without approval, automate engagement, invent a testimonial, or borrow a fact it was not given. Trust is the product. Founders put their name on every post.

A diagram showing two streams flowing into a central Generation node. The left stream, labeled Voice Context, is five cards representing the user past posts in purple. The right stream, labeled Topic Context, is five cards representing high-performing reference posts. Both streams converge into a glowing hexagonal Generation node, which outputs to a single LinkedIn post card labeled "LinkedIn post in user voice".
The beta's retrieval design: voice context anchored style, topic context anchored structure.

Get in touch

Pick the option that fits and I'll reply soon.

I'm reaching out about

Usually a reply within a few days. For urgent, email me.

Or contact directly