Founder · Johannesburg · building for South Africa & Uganda
Language, networks, and the machines in between.
I run an early-stage AI company building applied ML for markets where the interface is still a USSD menu, the training data is code-switched, and the customer is a mobile operator. Sentiment and speech models tuned for African languages, natural-language systems sitting on live subscriber data, and a three-node Raspberry Pi cluster on my desk where all of it gets proven before it goes anywhere near production.
Most of what I ship is shaped by a constraint someone else would call a blocker — no GPU budget, no approved LLM vendor, no permission for the data to leave the building. Those are the interesting problems.
NLP · ASR · retrieval
Telco data & access
k3s · edge · MLOps
Mac mini + 3 × Pi 5
Zindi competitor
*165#
Live sessionMTN UG
Track 01 — the models
Language & speech
African text and audio break most off-the-shelf models. Three projects on what it actually takes to make them work.
Live
Proof-of-capability asset
South African sentiment pipelineSentiment classification for code-switched SA social text
A sentiment model tuned for how South Africans actually write online — English braided with isiZulu, Afrikaans and Sesotho inside a single post, plus local slang that generic English classifiers read as noise. Built on Cardiff NLP's Twitter-RoBERTa-base over a corpus of roughly 29,000 South African social posts.
The interesting problem was never the architecture. It was labelling: a large share of posts are genuinely ambiguous, and annotator disagreement on those cases silently caps how good any model can get. I used a hybrid strategy — clear cases labelled directly, ambiguous ones routed through LLaMA-assisted annotation before human adjudication.
What I learned
Off-the-shelf English sentiment models degrade sharply on code-switched text — the failure is quiet, not loud, and only shows up in per-class recall.
Cohen's kappa is the honest metric here. Accuracy flatters a model on an imbalanced corpus; kappa measures agreement beyond chance, which is the thing you actually care about with contested labels.
The annotation strategy moved the numbers more than any model swap did.
Twitter-RoBERTa-base
HuggingFace Transformers
PyTorch
LLaMA (annotation)
accuracy · F1 · macro-F1 · κ
Annotation is the load-bearing part of the pipeline, not the model.
Shipped
Checkpoint public on HuggingFace
waxal-ext — multilingual African ASRWhisper large-v3 fine-tuned for Lingala and Swahili
A speech recognition fine-tuning pipeline targeting two languages Whisper handles unevenly out of the box. The best checkpoint is published as tobopat/whisper-large-v3-waxal-ext.
Most of the real effort went somewhere unglamorous: a long chain of environment and API compatibility failures across Colab and Jupyter, where library versions, the Transformers trainer API and the audio stack kept disagreeing with each other.
What I learned
On a large-v3 fine-tune, the environment is the experiment. Pinning versions early would have bought back most of the time I lost.
Low-resource ASR rewards data curation and audio preprocessing far more than extra training steps.
Whisper large-v3
HuggingFace Transformers
PEFT
Colab / Jupyter
HF Hub
Competition
Zindi hackathon ~0.604 best score
Multilingual health Q&ALuganda · Kiswahili · Akan/Twi · Amharic · English variants
A question-answering system for health queries across five low-resource African language subsets, built over four experimental phases: BM25+E5 hybrid retrieval, pure dense retrieval, QLoRA fine-tuning of Aya-Expanse-8B, and finally RAG-trained fine-tuning.
The best result came from refusing to pick one approach. Per-subset hybrid routing — choosing between the fine-tuned model and pure retrieval based on which language subset the question came from — outscored every uniform strategy at roughly 0.604 on the leaderboard.
What I learned
Pure E5 retrieval beat the BM25+E5 hybrid. Lexical matching helps less than expected when the query and corpus are in a morphologically rich language.
ROUGE's default tokenizer breaks on Ge'ez script, quietly deflating every Amharic score. A metric bug that looks exactly like a model problem.
RAG-trained fine-tuning — training the model on retrieved context, not just on answers — outperformed bare fine-tuning under ROUGE-heavy scoring.
Routing by subset is a legitimate architecture, not a hack, when the subsets have genuinely different data profiles.
Aya-Expanse-8B
QLoRA / PEFT
E5 embeddings
BM25
RAG
ROUGE
Track 02 — the products
Networks & access
Systems built on live operator data, and one built for the handset at the other end of it.
In build
Core loop working on a live MoMo SIM
Voice-to-USSD for mobile moneySpeaking your way through MTN MoMo and Airtel Money in Uganda
Mobile money in Uganda runs on USSD — a menu tree you navigate by reading a screen and typing digits under a timeout. If you can't see the screen, you can't send money independently. This is a voice front end for that flow, built for visually impaired users.
It runs entirely on-device as an Android app using TelephonyManager.sendUssdRequest(), with a session manager and state machine driving the callback loop. The regulatory position matters as much as the code: framed as an accessibility layer rather than a payment intermediary, it stays clear of PSP licensing requirements.
What the field testing turned up
Menu shortcuts jump the tree — *165*8*1# lands straight on the PIN prompt for balance, *165*1*1# straight on "enter number you are sending to".
Fully pre-composed strings are rejected. You can compress the menu path, but MoMo insists on interactive entry for recipient, amount and reason — which is exactly why a session state machine is unavoidable.
The reason field is mandatory and needs at least four characters. Small detail, breaks the whole flow if you miss it.
MoMo normalises locally-entered numbers to full international format on the confirmation screen — so the confirmation you read back to the user won't match what they said.
The PIN is handed to MTN's own system dialog. Never voiced, never captured, never stored. The one place where the right answer was to build less.
Android / Kotlin
TelephonyManager USSD API
ADB · fastboot
Speech-to-text + TTS
State machine session manager
WhatsApp receipts (next)
The PIN handoff is the design decision the rest of the architecture bends around.
In design
Live operator data, not synthetic
VVIP experience query systemPlain-language questions over subscriber experience data
An internal tool letting operator staff ask things like "when did this subscriber last have a dropped call, and why?" against a tracked group of high-value customers — dropped calls, site performance, and the root cause behind both.
The constraints are the design. The secondary store is Oracle, but not every root-cause signal is fed into it, so answering "why" means tool calls out to systems that hold the rest. And the target operator has no GPU infrastructure and no approved LLM vendor yet, which rules out anything that assumes a local model and a permissive data path.
Where it sits
Natural-language layer over a schema that was never designed to be asked questions in English.
Tool calling as the bridge between the warehouse and the signals that never reached it.
Vendor and governance approval is a real dependency, planned for rather than assumed away.
Runs on Sisyphus — the local agent runtime below, which is what makes "no approved LLM vendor" a solvable constraint instead of a blocker.
Oracle
Tool calling / function calling
Text-to-query
Telco experience KPIs
Sisyphus runtime
Pivoted
MTN · Airtel SA & Uganda
Outage social listeningDetecting network outages before the ticket does
The goal: a near-real-time outage signal for the operators in my markets. The first attempt pulled from Downdetector and hit persistent rate limiting. Rotating through residential IPs on a bank of Raspberry Pis hit exactly the same wall — which was the useful result.
So the project turned around: rather than scraping an aggregator, extract the outage signal directly from the social posts the aggregator is reading anyway. That also feeds straight back into the sentiment work.
What I learned
When the aggregator is the bottleneck, go to its source. Two failed evasion attempts were cheaper than one wrong assumption about the data supply.
Rate limiting on a commercial data source is a product decision, not a technical obstacle to route around.
Python scrapers
Playwright
Raspberry Pi fleet
Social signal extraction
In progress
Enterprise AI business case
AI value realizationProving return, not just capability
A framework layer over the rest of the portfolio: value measurement that carries both quantitative and qualitative evidence, TCO with break-even and sensitivity analysis, per-use-case KPIs surfaced on live dashboards, investment proposals with projected ROI, portfolio-level resource allocation, and detection of underperforming use cases early enough to remediate them.
It exists because the hardest part of enterprise AI is rarely the model. It's demonstrating, in the finance team's language, that the thing paid for itself.
TCO · break-even · sensitivity
KPI design
Real-time dashboards
Portfolio allocation
Stakeholder reporting
Track 03 — the substrate
Infrastructure
A three-node Raspberry Pi cluster standing in for a telco edge site, and the agents that run on it — self-healing, offline, and self-directed.
Running
3 × Pi 5 · k3s Grafana live
AI Grid — telco edge compute simulationProtect critical workloads at peak, reclaim the capacity for AI off-peak
A three-node k3s cluster on Raspberry Pi 5s, roled as control plane, edge-critical and edge-ai, simulating how an operator might protect network functions during busy hours and hand the spare capacity to AI inference when the traffic drops.
It's built entirely from native Kubernetes primitives rather than a bespoke scheduler — node labels and affinity, three PriorityClasses (telco-critical, telco-standard, ai-burst), ResourceQuotas and LimitRanges per namespace, taints on the control plane, KEDA scaling AI jobs on a SAST cron schedule, and a CronJob orchestrator that flips the cluster between modes. A small Flask control UI on a MetalLB address does it manually when I want to force a switch.
What I learned
PriorityClasses are the whole design. The simulation is only meaningful because eviction order under real memory pressure decides who survives — everything else is bookkeeping.
Grafana annotation overlays on mode switches turned an abstract policy into something you can see happen.
On 8GB nodes, the un-containerised processes are what bite you. Two systemd services were quietly holding over a gigabyte on the control plane.
k3s v1.34
containerd
KEDA
Prometheus · Grafana · Loki
MetalLB · Traefik · Longhorn
Ollama
Flask
Descheduler
Native primitives only — the scheduler already knows how to do this if you configure it honestly.
Next
Builds on the same cluster
Kubernetes AI Ops agentGraph-backed observability and self-healing
An agent platform layered on the Grid cluster: an ingestion pipeline pulling from the k8s API, Prometheus, runbooks and git events, embedding into a vector store and writing relationships into a graph, with an orchestrator routing questions to sub-agents — a self-healing ReAct loop watching for CrashLoopBackOff and OOMKilled, a RAG assistant doing hybrid retrieval, a graph analyst running Cypher over cluster topology, and an action agent with kubectl access.
Decisions worth defending
FalkorDB over Neo4j. Redis-compatible, ARM-friendly and materially lighter on memory — which is the binding constraint on an 8GB Pi.
Flat vector search can't answer "which services have been failing together?" Graph traversal can. That's the reason the graph store exists at all.
Qdrant for vectors — good ARM build, proven on Pi hardware.
FalkorDB
Qdrant
Ollama (phi3 · tinyllama)
nomic-embed-text
ReAct / LangGraph
kubernetes Python client
Cypher
Runtime
The engine under the VVIP system
Sisyphus — offline agent runtimeA provider-agnostic tool-calling harness running fully local on Qwen3
Sisyphus is what the VVIP experience system actually runs on. A tool registry with JSON Schema validation and sandboxed execution, a swappable provider interface, and an agentic loop with a hard iteration cap — the unglamorous plumbing that decides whether an agent is dependable or merely impressive in a demo.
It started against a hosted API and then got pointed at a locally-served model: plain HTTP against any OpenAI-compatible endpoint, no vendor SDK, so vLLM, Ollama, llama.cpp and SGLang are one config line apart. That was a requirement, not a preference. The target operator has no approved LLM vendor and the data is live subscriber traffic — a hosted endpoint isn't a deployment path, it's a procurement cycle.
The name is the joke and the design brief. The loop pushes, the loop fails, the loop pushes again — so the interesting engineering is all in what happens when the model gets it wrong.
What I learned
Recovery beats benchmarks. A model that emits a plausible-but-wrong query and can't correct itself after seeing the error is useless in a loop, whatever its leaderboard position. That property is what I select on now.
Narrow the tools. A general "run SQL" tool is both unreliable and a wide attack surface against live operator data. Parameterised, purpose-built tools fixed reliability and security in the same move.
Three format differences will bite when you swap providers: the nested tool schema shape, the per-message tool-result role, and arguments arriving as strings that need defensive JSON parsing.
vLLM needs --enable-auto-tool-choice and a model-family-specific tool-call parser, or tool_calls comes back empty while the model is emitting perfectly good tokens.
When native tool calling is shaky, drop the tools parameter entirely and prompt for JSON with grammar-constrained decoding. Same provider interface, third implementation.
Build the eval set first — around thirty real questions with known-correct tool sequences. It's the only honest way to know whether a local model is good enough before it touches production data.
Qwen3
vLLM · Ollama · llama.cpp · SGLang
OpenAI-compatible HTTP
JSON Schema
Grammar-constrained decoding
Offline-first
Nothing about the loop assumes a vendor. That's the only reason it can run where the data lives.
Research only
One agent per Pi
PiLab — research scout & plannerWhat's new in telco, fintech and data science — and what will actually run on a Pi
A three-agent system, one per node, that reads the field so I don't have to. The Scout monitors arXiv, Hacker News, X, Reddit, GitHub trending and releases, RSS tech blogs and a set of YouTube channels. The Evaluator scores what it finds against the thing that actually matters here — will this run on 8GB ARM nodes, and is it worth the effort. The Planner turns whatever survives into a build plan and tracks it.
Deliberately research-only. It scouts and evaluates; I decide what gets built. An autonomous agent that starts deploying things to my own cluster is a much harder problem than the one I set out to solve, and a worse one.
What I learned
The feasibility filter is the product. Finding interesting papers is trivial and produces noise; judging what fits constrained ARM hardware is the part with no substitute.
YouTube is an underrated signal source — practical implementation detail lands there weeks before it reaches a paper or a blog post.
The benchmark harness earns its keep: comparing llama.cpp RPC against exo for distributed inference across the three Pis, at several context sizes, turns "which is faster" into a table instead of an opinion.
FastAPI
SQLite
Ollama
asyncssh
llama.cpp RPC · exo
Tailscale
Playwright
Read the field, filter for what fits the hardware, hand a plan to a human.
Track 04 — also in flight
fploverdrive.com
FPL Overdrive
Fantasy Premier League analytics — a planner screen with player pool stats by position, projected xP over a gameweek range, price and elite ownership. Advanced analytics and autonomous team management, chasing green arrows.
feeds FPL Overdrive
Match odds model
Deriving clean-sheet and expected-goals probabilities from bookmaker lines, cross-referenced against independent per-fixture projections. Becomes a data feed rather than a standalone product.
agenticfinance.co.za
Agentic finance
An agent that writes about advances in agentic finance — currently digging into x402 and machine-to-machine payment rails.
early
Forex trading agent
Systematic rather than discretionary from the start: the goal is an algorithm on a MetaTrader-style platform, not a trading habit.