ran in production · 35 posts, no human in the loop

Daily LinkedIn auto-poster

Wire · Python · local Ollama · self-reviewing · browser-driven publish

Built for:
One person who wanted to stay visibly current on AI without spending the first hour of every day reading and writing — and who trusted a machine to do it only if it checked its own work first.
Not built for:
A content farm. Wire writes two careful posts a day from real sources, not a firehose; the scoring exists to throw most of the day away.

Every morning Wire pulled the day’s AI news from seventeen feeds, scored every item across eight signals, drafted two posts on a local model, ran a second model over them to fact-check and trim, and published — no approval screen, no human in the loop. It ran this loop in production for three weeks and shipped thirty-five posts before I parked it. This is how it worked.

§ I

The problem

Staying current on AI as a solo engineer is a daily tax: read the feeds, find the one thing worth saying, write it without sounding like everyone else, post it before the day eats the time. Done by hand it’s an hour every morning. Skipped for a week and you’ve gone quiet.

The interesting part isn’t the writing — a local model can draft. The interesting part is trust. An agent that posts to a real account under your name with no one watching has to be wrong about facts roughly never. So Wire spends most of its effort not on drafting but on choosing what’s worth saying and checking that the draft is true before it goes out.

§ II

Decisions

  1. local

    Every model runs on the machine — Ollama with a 26B Gemma for drafting and review, a small embedding model for semantic novelty. No tokens leave the box, no per-post API bill, and the whole pipeline keeps working when a provider has a bad day. The cost of a post is electricity.

  2. second pass

    Drafting and reviewing are two separate model calls with two different jobs. The first writes; the second is handed the draft and the source it came from and told to fact-check it against that source, enforce the style rules, and rewrite anything that drifts. A draft never reaches the publish step without a second model having tried to break it. This is the only reason an unattended loop is defensible.

  3. no gate

    No approval screen. The first design had a phone notification asking me to bless each post; I deleted it. A loop that needs a human at 6 a.m. isn’t autonomous, it’s a chore with extra steps. The safety has to live inside the loop — in the scoring and the review pass — not in a person tapping approve. It’s currently parked, not because it broke, but because I’d proven the point.

§ III

System

Six stages, one direction. Findings are scored and deduped against a seven-day topic memory so the same story never posts twice; the draft and review passes are separate models; publishing is the last thing that happens, after the work is already trustworthy.

Sources17 RSS + webScore8 signalsDraftgemma4:26bReviewfact-checkPublishno gatePersistSQLite7-day topic memory · no repeats
FIGURE 1. One morning run. The two accented stages — review and publish — are where trust is won and spent: nothing reaches publish that a second model hasn’t checked, and the loop reads back its own seven-day history so a story never posts twice.
Stack — current pins.
LayerImplementationPurpose
ScheduleWindows Task · 6 a.m.Unattended daily trigger
Sources17 RSS feeds + Firecrawl web searchRaw findings, deduped
Scoring8-signal composite + nomic-embedThrow most of the day away
Draft + ReviewOllama · gemma4:26b (×2 roles)Write, then fact-check vs source
PublishPlaywright (headless Chromium)Posts to the feed · no human gate
StatePeewee + SQLiteBriefs · topic cooldown · audit
research/scorer.pypython · composite scorer
# Eight independent signals, each a float in [0, 1], combined
# by config weights. The point of the composite isn't to rank
# the winners — it's to discard the ~95% not worth a post.
def compute_composite(scores: dict[str, float]) -> float:
    weights = {
        "recency":       cfg.scoring.recency,
        "novelty":       cfg.scoring.novelty,
        "relevance":     cfg.scoring.relevance,
        "semantic":      cfg.scoring.semantic,   # embedding sim
        "velocity":      cfg.scoring.velocity,
        "actionability": cfg.scoring.actionability,
        "authority":     cfg.scoring.authority,
        "signal":        cfg.scoring.signal,     # source count
    }
    total = 0.0
    for dimension, weight in weights.items():
        total += scores.get(dimension, 0.0) * weight
    return round(total, 4)
finding.scored.jsonone finding · top of the day
{
  "title":   "...",
  "scores": {
    "recency":       0.95,
    "novelty":       0.88,
    "relevance":     0.91,
    "semantic":      0.73,
    "velocity":      0.40,
    "actionability": 0.66,
    "authority":     1.00,
    "signal":        0.80
  },
  "composite":  0.842,
  "decision":   "draft",
  "cooldown":   "topic unseen in 7d"
}
FIGURE. One finding, scored. Recency, embedding-based novelty, and source authority do most of the lifting; the composite is a single number the draft step trusts. Items below the threshold — most of them — are logged and dropped, never drafted.
§ IV

What it did

Over about three weeks it ran fifty-five times and published thirty-five posts to a real account, twice a day, with no one approving them — the database still has every run logged with its timestamps. Then I disabled the schedule. The point I’d wanted to prove — that an unattended local agent could research, write, check itself, and act in the world without embarrassing me — was proven, and I’d rather show the working pipeline than leave a bot posting forever. It’s parked, not broken; the loop is intact and the history is real.

Acknowledgments

Wire stands on Ollama and the Gemma models that run on one GPU, nomic-embed-text for semantic novelty, Playwright for the publish step, Firecrawl for search, and the rule that an agent allowed to act unattended must be made to check its own work first.

← Index