A small company has the same problem as a large one: it must be visible every day, in two languages, on eight channels, forever. It does not have the same staff. So we built the staff.
What follows is not a product pitch. It is the actual architecture of the system that publishes for LUNO, including the parts that failed. If you are considering something similar — for your own business or with us — the failures are the useful half.
Two engines, not one
The first design decision was the one that mattered most: we built two separate engines, not one clever one.
LUNO speaks to two audiences who share almost nothing. Bodaboda and bajaji riders read Swahili, on phones, in a register closer to a conversation at a stand than to a press release. Investors, grant judges and fleet owners read English, on LinkedIn, and are deciding whether the person behind the company is credible.
One engine trying to serve both would have produced content that suited neither. So there are two codebases, two voices, two schedules, two sets of channels — and, as it turned out, two entirely different technical problems.
The stack, and why each piece is there
What one cycle actually does
Every run is the same eight steps:
1. advance the content pillar (11 subjects, rotated, position stored on disk)
2. read the last 20 ideas (so it cannot repeat itself)
3. generate a fresh idea (LLM, given the pillar + a "do not repeat" block)
4. draft the post (LLM, given real example posts in our own voice)
5. render the branded card (Pillow, fitted to the text it actually received)
6. render the vertical video (ffmpeg, only if a video channel needs one)
7. upload both, publish to Buffer (per-channel caption, per-channel format)
8. commit the new state back (pillar position + idea memory, into git)
Step 8 is the one people forget. A scheduled job runs on a fresh machine every time and remembers nothing. If the engine's memory is not written back somewhere permanent, it starts from zero on every run — same pillar, same idea, forever.
Four things that broke
Anyone can publish an architecture diagram. These are the parts that cost real days.
1. The language problem nobody warns you about
We started on open-weight models. They are fast, cheap, and they follow instructions well. They also invented Swahili words.
Not clumsy Swahili — confident Swahili. Correct rhythm, native-sounding cadence, and inside it, words that do not exist and grammar that quietly breaks. One model produced a phrase that translates roughly as “the tins of your heart” while reaching for “the debts of your heart”. Another used the wrong object marker, addressing the wrong person entirely. A native speaker catches it in a second. A benchmark never will.
Benchmarks measure reasoning, coding and maths. None of them measure whether a model can write the street register of a low-resource language. The only test that counts is a native speaker reading the output — and being willing to reject it.
We tested four models and moved to a frontier model in the end. The register improved sharply. The lesson generalises: if your audience does not read English, model choice is not a cost decision, it is a quality decision, and you cannot delegate the judgement to a leaderboard.
2. The laptop problem
For weeks the engine ran on a laptop, on a schedule. Then we counted: over eleven days it fired eight times out of twenty-two. Roughly two thirds of the schedule never happened, because a closed laptop runs nothing.
Moving to GitHub Actions fixed it permanently. It also introduced the state problem above, and one more: a scheduled machine has a stripped-down environment. Our video step failed silently for days with ffmpeg not found — on a machine where ffmpeg was installed, but not on the minimal path a scheduler uses.
“It works on my machine” is not a joke about developers. It is a description of every automation that has never run anywhere else.
3. The cards that were quietly broken
Our image cards render Swahili headlines, and Swahili sentence length varies enormously. The layout used fixed font sizes. Short lines looked good. Long ones ran off the bottom of the canvas and printed through the logo.
Worse, and harder to see: a large figure rendered at a fixed size overflowed the canvas by 610 pixels — the number 2,920,000 was published cut in half. These went out for days before anyone noticed, because each card looked plausible in a thumbnail.
The fix was to stop assuming and start measuring: text now shrinks until it fits both the width and a height budget, and a reserved zone at the bottom is one that no text may enter.
4. The bug you cannot see by looking
The most interesting failure was invisible on screen. Our cards have a coloured glow behind them — a brand element. On some layouts, that glow sat directly behind the English subtitle.
We measured the contrast rather than judging it by eye. The result: 1.10:1, against a minimum accessible ratio of 4.5:1. Even pure white text scored only 3.06:1 in that spot. The text was effectively invisible on a bright phone screen, and no amount of choosing a different colour would have fixed it, because the background was the problem.
So the renderer now measures its own output and darkens the area behind the text until the contrast target is met — a guarantee rather than a guess:
before worst background (148,147,79) contrast 1.10:1 FAIL
after worst background (30, 30, 15) contrast 5.77:1 PASS
Design opinions are cheap and contradictory. Measurements settle arguments. If a rule can be checked in code, check it in code — then it holds on every future post, not just the one you looked at.
The guardrails matter more than the generator
It is easy to make a model write. The engineering is in stopping it writing the wrong thing.
- A verified fact sheet. The founder engine may state only facts from one document. Anything else must come back as a placeholder for a human to fill. Inventing a metric in front of an investor is the worst failure this system could produce, so it is made structurally impossible.
- A refusal to run. If the fact sheet still contains unfilled placeholders, the engine exits rather than publishing around them.
- Anti-repetition memory. Every idea is logged. The last twenty are fed back in as a “do not repeat” instruction, and near-identical drafts are rejected and regenerated.
- Retry, not crash. A truncated or malformed reply is retried up to three times. An unattended job that dies on the first hiccup is not automation.
- A human gate. Nothing publishes without passing through a queue a person can edit. This is deliberate and permanent.
What it costs
About two dollars a month in model usage. The scheduler is free. The rendering is free. The publishing tier is free.
The real cost was the four failures above — and the honest answer is that most of that cost is not repeatable, because it has now been paid. That is what a case study is for.
The honest limit
This system produces presence. It cannot produce conviction.
It will post reliably, in the right language, in a consistent voice, forever. It will not reply to a comment with real warmth, argue a point in someone else's thread, or notice that a customer sounded worried on the phone. Those things move a business, and no engine does them.
We say this to every client who asks for automation: automate the tireless part, and spend the time you save on the part that only a person can do. A company that automates its voice entirely ends up with a very consistent way of saying nothing.
Want to see it working?
Open @lunomwanajando on any platform. Every post there came out of this engine. That is the only proof worth offering — the same standard we hold for everything on our shipped ledger.