← Tutorials
BUILD · PIPELINE

AI picks the story. I keep the pen.

I do not let AI write my posts. I let it do the part I am slow at: reading a messy week of commits and surfacing the one thing worth saying. Then I write it, and I publish it.

For
Builders using AI for content who want to stay the author, not hand it the keyboard
Needs
A queue you own, a bearer token, and somewhere to publish
Time
An afternoon

A scheduled agent told me this page was worth writing. It read the last four days of my git history, found the one idea worth explaining, and handed me a rough first pass. I wrote and edited what you are reading, and then I pressed publish. The agent never gets to press that button. That split is the whole point of the system, and it is the part most people skip.

I run several projects that all want to talk: a directory, an art platform, this site, client work. Each ships real material every week, and I cannot sit and mine all of it for what is actually worth saying. So I handed the boring half to an agent: read everything, tell me the strongest angle, rough it out. The writing stays mine. The publishing stays mine. The agent’s job ends at my review queue.

Here is how the pieces fit.

The shape

Three moving parts:

  1. A scheduled agent that surveys real work and surfaces the strongest angle as a rough draft.
  2. One ingest endpoint that accepts those drafts from any project.
  3. A review hub where I read, rewrite, and publish in one place.

The agent never touches a social API or my site. It only knows how to POST into my own hub. Everything it sends arrives as status = 'pending' — raw material waiting for me, never a live post.

Step 1: the agent curates from real commits

The agent runs git log --since="4 days ago" across my repos, clusters the commits, and picks the single most teachable thing. This is the part I am bad at: noticing, across a scattered week, which one thing is worth a post. It hands that over as a draft I can react to, not a finished article. A starting point.

// scripts/ingest-draft.mjs — one POST per channel, same group
const channels = ["article", "facebook", "instagram"];
for (const ch of channels) {
  const part = bundle[ch];
  if (!part) continue;
  const payload = { ...part, project, channel: ch, group };
  const { ok, json } = await ingestOne(payload);
  // ...
}

The group id keeps one piece of work together. The article and its social posts all carry the same string, so the hub shows them as one card with one action instead of loose drafts I have to reassemble by eye.

Step 2: the ingest endpoint trusts a token, not me

This is the line that matters. The ingest route is authed by a shared bearer token, and it is deliberately exempt from the browser login:

// app/api/ingest/route.js
const auth = (request.headers.get("authorization") || "")
  .replace(/^Bearer\s+/i, "").trim();
if (auth !== process.env.INGEST_TOKEN)
  return Response.json({ error: "Unauthorized" }, { status: 401 });

A machine with the token can add to my queue. That is all it can do. The endpoint upserts on (project, slug), so a re-run updates a pending draft in place instead of piling up duplicates. Published drafts are never touched.

The publish route is the other half. It sits behind the session cookie that only my login sets:

// app/api/publish/route.js
// Publish/send a pending draft to its channel. Gated by the session
// cookie (middleware). Per channel: instagram, facebook, threads,
// x, email, article...

So the boundary is not a convention or a flag I promise to respect. It is two different auth boundaries. The token can fill my queue and nothing else. My login is the only thing that can put words in front of people, and only after I have written and checked them. A confused agent, or a leaked token, can at worst clutter my review queue. Neither can publish a sentence.

What publishing actually does

For the social channels, publish calls the platform API. For the article channel, “publish” means something I like more: it commits a markdown file to this site’s repo through the GitHub Contents API, and the existing deploy pipeline does the rest.

// lib/github.js
// "Publish" = the .md file lands on main -> Vercel rebuilds
// edragrey.com -> the article is live. Path matches the Astro
// collection: src/content/articles/<lang>/<slug>.md

No separate CMS, no second source of truth. A draft I have finished becomes a commit, the commit becomes a deploy, the deploy becomes a live page.

The road I did not take

My first instinct was to route everything through Buffer and let the agent schedule posts directly. I dropped it. That is an agent that writes and ships on its own, which is exactly the thing I did not want. Folding the queue into a hub I own meant the agent’s reach ends at my database, and the words stay mine.

What I used

Takeaway

The tempting idea is that AI can write your posts. It can, the output is fine, and that is the trap. The better split is to let AI do the noticing, reading everything and surfacing the angle, and to keep the writing and the publishing for yourself. The words people read should be yours. This page started as an agent pointing at four days of commits and saying “that one.” The rest is me.

FOUND THIS USEFUL? GET THE NEXT ONE

Drop your email and I'll ping you when the next one goes up, with the tools and workflows I actually use. No spam.

More tutorials ↗