Build log
Nobody Clicks Link in Bio
I built a pipeline where Claude writes, renders and posts FreelanceCFO’s Instagram reels while I approve them with a merge button. The first ones reached almost nobody. Here is what changed.
FreelanceCFO has a landing page, a free tier, a pile of free calculators and, until October, no audience. I can build the app. I cannot make myself post on Instagram twice a day. So I did the obvious, slightly unhinged thing: I wrote software that does it for me, and then spent two weeks learning that posting was never the hard part.
the machine
It lives in a folder called social/ and it merged on 13 October. Twice a week a scheduled Claude Code session opens the Remotion studio in the repo, writes brand new marketing films in React, renders them, watches its own renders, writes captions and opens a pull request. The pull request is the approval queue. I watch the videos on the linked release, delete anything I don’t like from the batch file and hit merge. Merging is me saying yes to every post in it.
After that it’s GitHub Actions. A publish job posts the next approved reel in each slot, a daily job pulls every reel’s views, reach, saves and shares into a performance.json, and the next generator run reads that file before it writes anything. Write, approve, post, measure, write again. Nobody has to open Instagram.
None of it runs on my laptop. The films get made on my Claude subscription instead of API billing, the state lives on its own git branch because main is protected and a workflow can’t push to it, and the Instagram token never goes anywhere near a chat window. I typed it into GitHub’s secrets form myself.
first contact with instagram
The first version talked to Meta’s Graph API directly, with a resumable upload so the videos never needed public hosting. I tested it against a fake Meta server and it was lovely. Then I looked at what getting a real, never-expiring token involves (a Business app, a Facebook Page, a system user, six permissions) and switched the default backend to Composio the next day, keeping Graph as a fallback.
The first live post, a calculator promo, took three attempts. The pipeline was looking for a connected account that didn’t exist under the id I’d given it, and then it needed a user id on every call that the docs hadn’t made obvious. The error landed in published.json exactly where the README said it would, which is the only part of that afternoon I’m proud of.
the cron that was always late
Then nothing posted on its own. Every slot, I’d end up clicking “Run workflow” with force ticked like a guy manually flushing a smart toilet.
The publisher only accepted a run within 100 minutes of its slot, which felt generous. GitHub starts scheduled workflows late. Not a little late: eight runs out of eight between 15 and 19 October began somewhere between 112 and 229 minutes after the time I’d scheduled them. Every single one woke up, saw it had missed its window and politely did nothing.
Two fixes. A slot now stays claimable for five hours, with a rule that a slot can be served once however many runs show up for it, so a late cron, a manual run and a retry can’t double-post. And a tiny Supabase function now pings GitHub on the minute to start the workflow directly, which doesn’t queue. It checks a shared secret before it does anything and it sends no inputs, so a ping can start a run but can never force a post.
five reels, nineteen people
So now it posted on time, and I could finally see the real problem. The early reels were fine. Clean type, warm paper background, one green accent, a tidy brand card at the end saying Free to start. Link in bio. The four I have numbers for reached between 19 and 46 accounts each. Between them: one share, zero comments, zero saves.
I went back and read the brief the generator was following, and it was my fault in a very specific way. It told the model that the newest films were “the format to match”. It mandated the warm paper and the one green, a 120 BPM grid, the brand end card, and “Link in bio” as the closing line. So every batch dutifully matched the last batch, and the account turned into the same reel posted over and over with different numbers in it.
films that share nothing
On 19 October the brief got rewritten. The existing films became the list of what not to repeat. Within a run, no two films may share a mechanism, a visual world, a score, a length or a call to action, and the model has to write a table proving it and throw out its own plan if two rows look alike. A test fails the build if one film imports another or a shared template. Every film still has to get its numbers from the real calculators, with a test that recomputes each figure through the calculator’s own code, because the other rules didn’t change: no invented proof, no em dashes, and Lance drafts and never sends.
And the ending changed. One action, the cheapest one there is: comment a word and I’ll DM you the free tool.
The first three under the new rules really didn’t share anything. Beat the Clock was a keystroke-by-keystroke speedrun of the live tax calculator. The Jar was a physics simulation of coins, brass on warm black, no music: $10,000 lands, $3,049 of it was never yours. Left On Read was an iMessage thread. Three polite nudges to a client called Greg, three “Read” receipts, then one message with a late fee in it ($59.97 on a $3,200 invoice, 38 days late), and Greg pays.
Everyone has a Greg.
the end card of the reel that worked
Left On Read reached 1,612 accounts and got 1,865 views. People watched almost eleven seconds of a 26-second video on average, about double the early reels. The Jar did 1,299 views. A week later, Dominoes, an actual 3D scene of domino blocks labelled things like Skipped Q3 tax and Any client at all falling toward a green one marked 3 months of runway, got the most shares and likes of anything so far.
Beat the Clock got 66 views. Different isn’t automatically good. It’s just the minimum for finding out what is.
the boring parts that keep it alive
Fifteen reels have gone out since 14 October, about 4,400 views in total, which is tiny by any standard except “what the account had before”. Most of what I’ve built since the second week is plumbing:
- A video that was two bytes. The Net60 render got renamed in GitHub’s web UI and became a 2-byte file. Instagram rejected it three times, and my probe read ffprobe failing as “ffprobe isn’t installed” and waved it through. It now refuses tiny or unreadable files before Instagram ever sees them.
- A schedule editor. Moving a reel used to mean editing YAML in a pull request. Now there’s a small local page for reordering, parking and retrying posts, backed by a function that wants both an allowed address and a passphrase.
- The DM. The reels were promising “I’ll DM you the link” before any code sent one. Now Meta pushes every comment to a webhook that checks the signature, matches the keyword, claims the comment in the database first and only then sends one private reply, so a redelivered webhook can’t DM someone twice. It’s on a branch, finished today.
the marketing i wrote and never posted
Here’s the embarrassing bit. In September, before any of this, I sat down and wrote an entire marketing kit: a Product Hunt launch plan, thirty days of X and LinkedIn posts, forty Reddit answers to common freelancer tax questions, DM templates for podcasts and accountants, three onboarding emails. Every figure in it matches the tax guides on the site, which cite the IRS. In October I added a list of 19 subreddits and 82 launch directories with a status column.
Every row in that status column still says Not posted.
Writing is comfortable. Posting is the part that involves strangers. That’s exactly why the Instagram pipeline is the only marketing that’s actually happening: it removed me from the step I kept skipping.
Reddit is next, and it’s deliberately the opposite. An agent reads replies to my account and drafts answers in my voice, written from an interview where I described how I actually type (lowercase, one CAPS word for emphasis, agree before disagreeing, never pretend to be an expert). Each draft becomes a GitHub issue. Nothing posts until I comment send on it. The drafting session never even holds the Reddit key, so the model physically can’t post, and a guard refuses any reply with a link, a dash, an assistant-sounding phrase or a product mention that isn’t disclosed. Reels are me approving a batch. Reddit is me approving every word, because a comment section can tell.
what i can’t tell you yet
Whether any of this turned into signups. Every link from Instagram now carries its source into the app, the app remembers the first tagged visit instead of reading the URL after sign-in, and the landing page runs a small A/B test on its main button, with a test that fails the build if anyone ever adds session replay. The plumbing is there. The honest answer is: give it a month.
What I do know: the AI part of this was the easy part. Claude writes a decent motion graphics film faster than I can write the brief for one. What took two weeks was the late cron, the two-byte video, the template I accidentally told it to copy, and the “link in bio” that I’d have sworn was fine. The machine does exactly what you tell it, every Monday and Thursday, for as long as you let it. Be very sure what you’re telling it.
- FreelanceCFO
- Marketing
- Automation
- Build log