Social Publisher Agent

Production-grade Telegram → Facebook + Instagram pipeline. Human approval gate. No instant publish, ever.

Built · 2026

Hermes Meta Graph API Supabase Telegram

The problem with most "AI social media" tools

Most AI-driven social media tools fail in one of two ways. Either they auto-post AI-generated slop with no human in the loop — fast, but dangerous, and the brand pays for it. Or they hide behind a SaaS wall: you upload, you click, and you have no idea what happened between the click and the post going live. Both lose the same thing — observability. When the post goes wrong, you can't trace why.

We built the Social Publisher Agent as the opposite: every step is inspectable, every state transition writes a database row, and nothing publishes without an explicit human approval. The architecture is deliberately boring. Boring is what survives the day after launch.

How it works

A single-purpose agent. One input channel, one approval gate, two output channels.

> telegram.receive(image, caption, hashtags)

Image + text arrive from a private bot.

> draft.park()

Image goes to a private Supabase bucket.
A row is written to social_posts with status=draft.

> human.approve("OBJAVI")

Hard gate. No timeout, no default, no auto-yes.
The human types the word.

> publish.facebook + publish.instagram

Signed URL (10-minute expiry) minted at publish time.
FB Page first, then IG Professional. Two-step IG flow with container polling.

> db.mark_published(fb_post_id, ig_media_id)

Final state written. Telegram replies with both IDs.

Layer I · Profile Layer II · Instruction Layer III · Execution Layer IV · Gateway

What it does. And what it deliberately doesn't.

Does

  • Accepts one JPG (4:5) + caption + hashtags from Telegram
  • Validates image before anything else
  • Stores drafts in a private bucket, never publicly URL-exposed
  • Mints short-lived signed URLs only at publish time
  • Publishes to a Facebook Page and a linked Instagram Professional account
  • Writes every state transition to a SQL table
  • Surfaces every failure to the human with the upstream payload attached

Deliberately doesn't

  • No instant publish. No "skip approval" flag.
  • No AI caption generation. The human writes the caption.
  • No silent retries. The human decides if/when to retry.
  • No multi-image, no reels, no stories — V1 stays narrow.
  • No public storage URLs in the database, ever.
  • No analytics ingestion in V1 (separate concern, separate agent).

Every "doesn't" is a contract, not a TODO.

Four layers, never mixed

The most common failure in production agent systems is mixing instruction and execution. People put business logic in prompts, or behavior contracts in code. Both fail in opposite ways — non-deterministic prompts break silently, while hard-coded contracts can't evolve without redeploys.

Layer I   · Profile      · Hermes profile, env, identity
Layer II  · Instruction  · System prompt, behavior contract
Layer III · Execution    · Skill + Python wrappers
Layer IV  · Gateway      · Telegram bot, approval flow

If you find yourself writing "never X" in code, that probably belongs in instructions. If you find yourself concatenating strings in a prompt, that belongs in execution.

Stack

Hermes Meta Graph API v25.0 Supabase (Storage + Postgres) python-telegram-bot Pillow

Boring. Stable. Replaceable. Everything in this stack has been in production for at least three years.

The code is public

We open-sourced the full implementation — Hermes profile, skills, Python wrappers, Telegram gateway, SQL migration, setup docs. Use it, fork it, run it for your own brand. The repo is sanitized: no credentials, no IDs, just the architecture.

The five failure modes that kill social publishing pipelines

Most teams discover these the hard way — usually on a Friday evening, usually with a post already live. We listed them before writing a single line of execution code, and the architecture answers each one explicitly.

Failure 01 · The duplicate publish

A naive retry on a transient Meta Graph timeout posts the same image twice. We solved this with idempotency keys derived from the social_posts.id and a status machine that refuses to transition from publishing back to approved. Retries are a human decision, not a wrapper around try/except.

Failure 02 · The leaked storage URL

Storing public Supabase URLs in the database is the standard tutorial pattern — and it leaks every draft to anyone who guesses the bucket path. We store only the object key. Signed URLs are minted at publish time with a 10-minute expiry, used once by Meta's CDN, and never written to disk. A draft sitting in the approval queue for a week has zero public surface area.

Failure 03 · The half-published post

Facebook accepts the post, Instagram rejects it (wrong aspect ratio, expired token, container timeout). The brand now has an inconsistent presence and nobody knows. Our publish step writes a row per platform with the upstream error payload attached, surfaces both outcomes to the operator in Telegram, and never marks the parent post published unless both children succeeded.

Failure 04 · The silent token expiry

Meta Page Access Tokens expire on a schedule that nobody remembers. The agent checks token freshness on every approval — not at publish time, when it's too late. An expiring token triggers a Telegram nudge 72 hours before, with the exact Graph API call to refresh it. We treat credentials as a first-class operational concern, not a deploy-time secret.

Failure 05 · The approval-by-thumb

A single emoji reaction is too easy to send by accident. The gate requires the literal word OBJAVI ("publish" in the operator's language), typed in full, in the same Telegram thread as the draft. There is no inline button, no default action, no "approve all". Friction at the approval step is the whole point.

What the system actually looks like in production

Numbers from the first ninety days of live operation across the initial pilot deployment. No A/B framing, no synthetic load — just what the database table actually contains.

1,847

Drafts processed end-to-end

100%

Required explicit human approval

0

Auto-published posts. Ever.

3

Half-publish events, all auto-detected

< 4s

Median draft → ready-for-review

11s

Median publish duration (FB + IG)

The number that matters most is the one in the middle: every single post that went live did so because a human typed a word. That is the contract. The other numbers exist to prove the system is fast enough that the contract doesn't slow the brand down.

Three things we'd tell any team building their first publishing agent

One — narrow the surface before you widen the stack. The temptation with a V1 is to support every aspect ratio, every platform, every content type. Don't. We shipped with one image format, two destinations, and one approval keyword. Every "no" in the scope list saved a week of edge-case work and a class of production incidents we never had to debug.

Two — the database is the source of truth, not the LLM. Every state lives in Postgres. The agent reads from it, writes to it, and the human can query it directly when something looks wrong. The model is allowed to draft, format, and route — never to remember. Memory in agents is where reliability goes to die.

Three — make the failure path nicer than the happy path. When Meta returns a 400, the operator gets the full upstream JSON, the request payload, the draft ID, and a one-line suggested fix — all in the same Telegram thread. When everything works, they get two post IDs. The asymmetry is intentional: success is boring, failure should be informative. Operators who can diagnose without opening a dashboard stay calm under load.

Want one for your business?

This is one agent. We build others — sales, support, scheduling, finance, research. Same engineering discipline: production-grade, human-in-the-loop, observable.

If we don't think an agent is right for you, we'll tell you. No obligation.