Production-grade Telegram → Facebook + Instagram pipeline. Human approval gate. No instant publish, ever.
Built · 2026
Hermes Meta Graph API Supabase Telegram
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.
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
Every "doesn't" is a contract, not a TODO.
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.
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.
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.
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.
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.
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.
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.