The checkmark that lies — building Instafeeder

Builder's log  ·  Instafeeder  ·  09 Aug 2026

The checkmark
that lies

Every social scheduler shows you a green tick when your post goes out. A surprising number of them are guessing. I spent ten weeks building the one that isn't — and then built an entire publishing operation on top of that single refusal.

710commits
10weeks
54workstreams
13channels
61agent tools
95migrations
77.5klines

01  ·  The failure that started it

A post that went out twice, and a dashboard that said once

Here is the failure mode that made me build this. A scheduler fires. It calls the platform API. The post is created. The response never comes back — a timeout, a dropped connection, a function that died mid-flight. The job is marked failed. A retry runs. The post is created again.

Now you have two identical posts on your feed and a dashboard that says the publish failed. Every component behaved reasonably. The result is still a lie.

The tools I was paying for did some version of this. More often it was the quieter one, where a post silently failed on three of five channels and the row went green anyway — because the row's status was a single column and the truth had five values. I'd find out days later, from a client.

A status that can be wrong is worse than no status. It teaches you to stop reading it. The principle the whole product is built on

So I started from the other end. Not "what features should a social scheduler have" — that list is well known and every competitor has it. Instead: what would a system look like if it were structurally incapable of telling you something false about your own content? That question had consequences all the way down, and answering it honestly is what took ten weeks.

What came out is Instafeeder. Everything below is the real thing running against a live workspace — my own company's account, real posts, real creative, real publish state, nothing staged.

02  ·  The surface

One workspace, every stage of the work, no tab-hopping

Content work has stages — draft, review, ready, scheduled, posted — and most tools give you one view and make you filter. Instafeeder gives the same set of posts six lenses and lets the work choose: a grid when you're thinking visually, a board when you're moving things through review, a list when you're auditing, a gallery when you're checking how the feed will actually look, a calendar and a scheduler when you're thinking in time.

app.instafeeder.com/w/interstaff/posts

Workspace

Posts

Home Grid Gallery Posted Archived
30 posts
Amber carousel slide: You hired help. So why are you busier?

IG — The delegation mistake (jab carousel)

07 Aug 07 Aug · 16:00
Posted
Compliance card: Calling back someone who already called you is not cold-calling. 4

B5 · Social — Outbound & TCPA

06 Aug 06 Aug · 17:00
Posted
Card: How much does it cost? is the wrong question.

B6 · Social — Answering Service Cost

10 Aug 10 Aug · 17:00
Scheduled
Carousel cover: 12 hours a week. Gone. The Intake Autopsy.

[W1 Wed] Manny LI — The Intake Autopsy

03 Aug
Review
Dark violet card: Cost line, or front door? 10

B1 · Social — Legal Answering Service

29 Jul 29 Jul · 17:00
Posted
Carousel cover: Five upgrades that cost nothing — a warmly lit law office reception at golden hour. 6

IG — 5 upgrades that cost $0 (jab carousel)

03 Aug 03 Aug · 16:00
Posted
Video poster: Someone needs a lawyer tonight.

P1 — The Shift (attorney launch reel)

26 Aug
Draft

Being a lawyer: 40% practicing law, 60% explaining why the thing they saw on TikTok doesn't apply to their case.

420

Threads — Lawyer 40/60 (jab)

29 Jul 29 Jul · 20:00
Posted

Posts · Grid lens, real workspace, every card showing the creative its post actually owns. The 3px top edge is the status colour, and where a post has live jobs a second band bevels in underneath it — so publish state is readable before you've read a word. Two cards carry only a bare date with no time beside it: Manny's carousel and the 26 August draft have a planner slot and no queue, and the missing timestamp is the signal. Manny's date was 3 August; it has been and gone, and nothing went out, because a date is not a job. The heart is total engagement across channels, and the month's best post is a text-only Threads jab, which renders as a miniature Threads card rather than an empty grey box.

03  ·  The hardest idea in the product

Two schedules, deliberately

A post carries two different dates, and keeping them apart is the design decision I'd defend hardest.

The first is the planner date — where the post sits in your calendar. It's a plan. Dragging it changes nothing in the world. The second is the publish queue — actual jobs, per channel, that a cron executes. Only the queue makes anything go live.

Every tool I've used collapses these into one field, and it's why calendars lie. You drag a post to Thursday, the calendar shows Thursday, and it goes out Tuesday because the queue was never touched. Or worse: it shows as scheduled because it has a date, and it will never publish at all.

Separating them costs a concept to learn. It buys a calendar that is never wrong. Everywhere the two could be confused — the API, the agent tools, the post header — they're named apart, because a caller who can write the queue but can't see it will queue the same post twice.

app.instafeeder.com/w/interstaff/scheduler
Scheduler

The queue

4 queued · 6 channels connected · Europe/Madrid

21h
next out
B6 · Social — Answering Service Cost
Facebook · 17:00 X · 17:00 Threads · 17:00
3 jobs pending
Sun 03 Augpassed
[W1 Wed] Manny LI — The Intake Autopsy
planner date only · 0 publish jobs · did not go out
Never queued
Fri 08 Augnothing queued
Sat 09 Aug
20:00 now
Sun 10 Aug
B6 · Social — Answering Service Cost
17:00 Queued
11 – 18 Augeight empty days
Wed 19 Aug
IG — Answered: Missed to Booked (HOOK)
rescheduled from 16:00 · that job kept as canceled
17:00 Queued
Wed 26 Aug
P1 — The Shift (attorney launch reel)
planned for this date · not queued
Planned only

Scheduler, real queue. Every day gets a tick whether or not anything publishes on it, so the eight-day gap between 11 and 18 August is literally empty space rather than days that quietly aren't drawn. Two entries carry the distinction: the 26 August draft is Planned only — it has a date and will not publish — and the 3 August carousel is that same condition after the fact, still saying so instead of pretending.

04  ·  The rule that keeps it honest

A blocker is exactly what the server refuses. Nothing more.

Thirteen platforms have thirteen sets of rules, and they contradict each other. YouTube needs a video and a title. X cuts at 280 characters, Threads at 500. Instagram feed captions aren't clickable, so a link in one is simply unreachable — which matters enormously when the whole post exists to drive a click.

Most tools discover this at publish time, the worst possible moment: you've stopped thinking about the post and the failure arrives as an email at 6am. So the knowledge moved to where the work happens — a readiness panel in the editor that answers "will this go out" while it's still cheap to fix.

The panel has one rule, and it's why it's worth trusting: it does not re-implement the platform rules. It calls the same function the schedule button calls. It cannot say "blocked" for something that would in fact schedule, and it cannot stay quiet about something the server will refuse. Everything else is a warning — and a warning says what will actually happen, not a vague "check this".

Try it. These are the six channels this workspace really has connected, and the rules are the real ones.

app.instafeeder.com/w/interstaff/post/8b956593 — readiness
Publish to
Channels 3 on
Creative
Attached creative
TCPA compliance card
Image · 1080×1350 · 4:5
All checks passed.
Readiness

Blockers come from channelScheduleError() — the same
call the Schedule action makes. Warnings never block.

Post editor · Readiness panel — live, not a picture. Switch YouTube on with an image attached and it blocks, because the server would refuse it; swap to video and the block clears on its own. The Instagram warning never blocks anything, but this post's whole job is a link to the blog, and on Instagram that link is unreachable — worth knowing before you publish, not after.

05  ·  Composition

One post, thirteen channels, and none of them the same

"Cross-posting" usually means broadcasting one caption everywhere and accepting that it reads badly on four platforms. Instafeeder treats a post as a single idea with per-channel expression: its own caption, hashtags, first comment, title, crop — and its own creative.

The screenshot below is a real post. The master caption is 428 characters and carries a UTM-tagged link. X's limit is 280, so X has an override: a tighter 248-character rewrite with its own UTM source, so the traffic it drives is attributable separately. YouTube carries a title, because it's the only channel that has one. None of that is possible if a post is a single text field.

Posts also carry variants — a real A/B set, each with its own media, aspect ratio and video cover — and different variants can go to different channels at different times from the same post. Only the active variant publishes, and you can push a new thumbnail onto an already-live YouTube video, which is the actual A/B move nobody lets you make.

app.instafeeder.com/w/interstaff/post/8b956593
← Back to Grid

B5 · Social — Outbound & TCPA

Posted · 3 of 3
Master Facebook X override Threads YouTube title Instagram · off
X caption — overrides the master
Warm follow-up can win back dead leads — call the wrong way and it's TCPA trouble. A plain-English guide for firms → interstaff.ai/blog/automated-outbound-calling-legal-tcpa?utm_source=x&utm_medium=social
248 / 280
master would be 428 — over by 148
Master caption · used by Facebook and Threads
Warm follow-up calls can win back leads you thought were dead — but call the wrong way and you're in TCPA trouble. The rules are specific, and they're written down. … utm_source=social
Instagram is switched off for this post. Its captions aren't clickable, and the entire point of this one is the link. The first comment is clickable on Facebook, LinkedIn and YouTube — that path is plumbed.
Creative · 4:5
TCPA compliance card, type-only on ruled paper stock
Placements · all live
Facebook ✓ permalink
X ✓ permalink
Threads ✓ permalink
A placement is only "succeeded" once it can prove it —
every success here carries a live link.

Post editor, real post. One idea, four channels, one caption override written specifically because X's 280-character limit would have truncated the link out of the master. Instagram is deliberately off — the post is a link, and Instagram captions don't carry one.

06  ·  Measurement

Numbers that refuse to invent zeros

Same principle, pointed at reporting. When a platform doesn't return a metric, most dashboards render 0. That's a fabrication, and it compounds: zeros drag averages down, make a good week look flat, and quietly teach you the wrong lesson about your own content.

Instafeeder renders . A dash means "not reported", a zero means zero, and the two are never merged. The panel below is this workspace's real 30-day shape — same posts, same channels, same reported-versus-not pattern — with the engagement volumes scaled up for legibility at screenshot size. Every rate is recomputed from the two numbers sitting beside it, which is why they all still agree.

The coverage grid is the part I'd point at first. Five platforms, nine metrics, and they agree on almost nothing — Threads reports shares but not reach, Instagram reports reach and saves but not shares, X reports only impressions and clicks, YouTube reports neither impressions nor reach. Any dashboard showing you one tidy comparable table across those five is making something up.

app.instafeeder.com/w/interstaff/analytics
Analytics

Last 30 days

26 posts measured · snapshot 09 Aug 04:17

Impressions
15,560
summed only where reported
Interactions
530
3 of 5 channels report these
Engagement rate
3.41%
recomputed, never averaged
Reach
per channel only · never summed
LinkedIn followers
12,786
▲ 325 in 23 days
Posts ledger Overview Channels Report
Post Channel Published Impr. Reach React. Rate Captured
Threads — Lawyer 40/60 (jab) Threads 29 Jul 20:00 7,160 7,160 420 5.87% 17 captures
Threads — Calendar doesn't lie (jab) Threads 01 Aug 16:00 1,470 1,470 62 4.22% 2 captures
IG — Calendar doesn't lie (jab) Instagram 05 Aug 16:00 1,210 1,050 48 3.97% not yet captured
B5 · Outbound & TCPA Threads 06 Aug 17:00 100 100 4 4.00% not yet captured
B5 · Outbound & TCPA X 06 Aug 17:00 0 not yet captured
B5 · Outbound & TCPA Facebook 06 Aug 17:00 0 not yet captured
[YT] V1 — The 4 Places Law Firms Lose Clients YouTube 28 Jul 19:34 3 19 captures
Coverage — which metrics each channel actually reports
Channel Impr.ReachViewsLikes Comm.SharesSavesClicksFollows
Threads
Instagram
Facebook
X
YouTube

Reach is deliberately absent from every cross-channel total: it counts unique people, so adding it across channels double-counts anyone who saw more than one. It appears per channel only.

Analytics · Posts ledger and coverage grid. The structure, channels, capture ages and coverage map are this workspace's real 30-day snapshot; engagement volumes are illustrative. One row per placement, not per post — the same post on Threads and on X is two different results. Facebook and X return no impression figure for the TCPA post, so the ledger shows a dash and the engagement rate stays blank rather than being computed from a number nobody supplied.

07  ·  The part I think changes things

Your social team is a chat window

Instafeeder ships a remote MCP server — 61 tools that connect Claude directly to your workspace so you can run the operation by talking to it. Not a chatbot bolted on the side. The agent is a first-class client of the same product. Every figure in the screenshots above was pulled through it.

Three things make it more than a demo. First, the tools call the same core functions the UI calls — nothing is re-implemented for the agent, so the app and the agent can't drift apart or enforce different rules. Second, authorization is identical: an OAuth token is exchanged for a short-lived user JWT, so every read and write goes through the same row-level security as the browser. Claude can only touch the workspaces you belong to, limited by your role. There's no admin backdoor for the agent, because there's no separate path.

Third — the part I thought hardest about — some tools are marked destructive, and the set isn't what you'd guess. It isn't just the deletes. It's the ad budget controls, because the risk of changing a budget isn't a lost row, it's spend that keeps charging. And some things are deliberately not exposed at all: connecting channels (OAuth belongs in a browser), deleting a workspace, anything that sends a direct message.

claude.ai — connector: instafeeder
CT
Put the TCPA guide out to Facebook, X and Threads at 5pm. Same caption, but X needs its own — the master won't fit.
C
Checking the workspace first.
list_workspaces → Interstaff list_channels → 6 connected list_media → TCPA card, 1080×1350
C
Created it with a per-channel override on X.
create_post → 8b956593 attach_media → 4:5 set_post_platform ×4 · X caption + YT title check_readiness → 0 blockers, 1 warning
One note before I queue: the warning is Instagram — its captions aren't clickable, and this post exists to drive a click. I've left Instagram switched off. Facebook, X and Threads are clear.
queue_post → FB · X · Threads, all 17:00
Queued — three placements. Open the post ↗
CT
Also double the budget on AD-107.
C
That one moves real money, so it needs your confirmation:
set_ad_budget destructive · AD-107 · awaiting confirmation

MCP connector — a representative session against the real workspace, with the real tool names. The agent surfaced the Instagram link problem before queueing rather than after, and the budget change stops for confirmation because destructiveHint covers spend, not just deletion.

What I keep coming back to: the readiness rules, the two-schedule distinction, the refusal to invent zeros — none of it was built for the agent. But an agent is the harshest possible consumer of a dishonest status, because it acts on it immediately and at speed. Building for truthfulness first is what made the product agent-safe. I didn't plan that. It's the most useful accident in the whole build.

08  ·  The build

Ten weeks, one person, and a register that stopped the agents colliding

710 commits between 2 June and 9 August. 89 pull requests squashed into main, the most recent #250. 558 TypeScript files, 77,565 lines, 95 database migrations. Next.js 16, React 19, Supabase with row-level security throughout, Backblaze B2 for media, Vercel for everything else.

I built it solo, running several Claude Code agents in parallel. That works right up until two of them edit the same file, and then it stops working badly. The fix was boring and effective: a git-tracked program register. Every workstream claims the file globs it intends to touch and commits that claim before writing code. Two sessions claiming the same path collide as a merge conflict instead of silently overwriting each other. Each agent works in its own worktree on its own branch.

The register has a companion I'd recommend to anyone: a roadmap of what is deliberately not built, with reasons. Nothing in it is half-finished — if it's listed, no code for it exists. Applying the product's own honesty principle to its own documentation is what kept a ten-week solo build legible.

WS-013Publish idempotency — a lost response is no longer a false failureshipped
WS-014Unpublish per channel, and honest statusshipped
WS-031Publish sweep — a lost response recovers instead of lyingshipped
WS-042Logging, audit and status platform — with a verifiable seal chainshipped
WS-044Platform guards — worker pre-flight and editor readinessshipped
WS-049Link at publish time, false-success repairshipped
WS-054A partly-published post is shown honestly everywhereshipped
WS-052Analytics V3 — web and social, honestly joinedplanned

Eight of fifty-four rows from docs/PROGRAM.md. Fifty-two have shipped. I named each workstream after the lie it removed, which is what made the register readable a month later.

09  ·  What it's worth

What changes when the status is trustworthy

The benefits aren't the feature list — every scheduler has a calendar. They come from the one property a feature list can't show you. One number from the live workspace makes it concrete: 38 placements succeeded, and all 38 carry a permalink. Success and provable success are the same set. That equality is the product.

Outcome

You stop checking the feeds

People manually verify their own posts because they've been burned by a green tick. When partial failure is a first-class state and a lost response recovers instead of lying, the dashboard becomes the source of truth again and the daily audit ritual disappears.

Outcome

Failures surface in minutes, not from a client

Expired connections, refused channels and stalled jobs are caught by a pre-flight check, a sweep and an alert — before the content goes out, or within the hour after. Nobody finds out from a customer.

Outcome

Thirteen channels stop costing thirteen workflows

Per-channel captions, hashtags, first comments, titles, crops and creative variants live on one post. The work scales with ideas, not with the number of platforms you're on.

Benefit

Reporting you can hand over unedited

A share link with no login, one snapshot, dashes where the platform said nothing. Nothing in it has been smoothed, so there's no gap between the report and the truth to explain. That's a much shorter meeting.

Benefit

The calendar is a plan, and it stays one

Because planning and publishing are different objects, you can move things around freely without wondering whether you just changed what goes live — and a post that will never publish can't hide behind a date.

Disruption

An operator who doesn't need the UI

61 tools under exactly the same permissions as the browser. Brief it in a sentence and it composes, checks readiness, queues per channel — and stops for confirmation on anything that moves money.

Disruption

Category economics, rewritten

Publishing, scheduling, media, approvals, tasks, analytics, ads, audit and an agent API. Ten weeks, one person. That used to be a funded team and two years, and that gap is the actual story.

Disruption

Honesty as a moat

Any competitor can copy a calendar in a sprint. Retrofitting truthful state means changing what a status is — a schema change, a job model, and every surface that reads it. That is not a sprint.

10  ·  What isn't built

The part most launch posts leave out

Instafeeder is in beta and there are real gaps, all written down. Web analytics aren't joined to social yet — that's next, and the spec already admits the hard part: at this posting cadence almost any traffic spike has several candidate posts, so ambiguity is the normal case and the design has to lead with it rather than discover it later.

Seventy-nine error paths still log only to ephemeral platform logs instead of the event system. Audit-chain verification is a script rather than a screen. Three historical placements will never get a permalink — one post the provider insists never published, and two from a workspace whose key returns 403. They're listed by name in the roadmap, because a known unfixable is information and a quiet gap is a lie of a different kind.

That's the whole thesis, applied to the thing itself. A product that tells you the truth about your posts should be able to tell you the truth about its own edges. If it can't do the second, I wouldn't believe it about the first.

Build the thing that refuses to guess. Everything else is a feature list.

Instafeeder · a social publishing command center · beta
Next.js 16 · React 19 · Supabase · Backblaze B2 · Vercel · MCP
Written by the person who built it, August 2026.
Screenshots are the live Interstaff workspace — real posts, real creative, real publish state, captured 09 August 2026. Engagement volumes are illustrative; every derived rate is recomputed from the two numbers beside it.