Funua has a new job

Get in touch

In 2012 I had two hours to kill in the Nairobi CBD. I'm not known to love shopping, but I found myself wandering the streets looking at shop windows anyway, and I noticed that almost every second one had some sort of offer or discount in it.

This mattered because I was looking for a business idea. A small one. I wanted something on the side that would take less than an hour of my time every day, and where I could outsource the kazi ya mkono (the manual work) to somebody else. (I know, very naive.)

Walking past all those shop windows gave me the idea: a website where people could check Nairobi's sales and discounts before heading out to shop. Once I saw that, I put together a directory using WordPress, Genesis Themes, and a bunch of plugins. For the legwork, I hired a young lady fresh out of school to go from shop to shop around Nairobi collecting deals you couldn't refuse.

I called the site Funua. In Swahili, funua means to open or uncover, and when I introduced the site on this blog, I invited readers to open up Funua.com before going out shopping.

I tried to keep my expectations reasonable. In that same announcement post, I told readers I had no delusions of grandeur and was very aware the whole thing could be dead in a month or two.

Well, it ran for years rather than months, though it never made any real money. It once made KES 10,000 from Jumia referral links, and that was about it. The young lady collecting the deals eventually got a job and left, my own financial needs took me elsewhere, and years after that announcement the business just died naturally.

The domain name, however, stayed with me. Every version of funua.com over the years has been mine: a Magento online shop in 2010, a Christian blog in 2011 with the masthead "Uncover to Discover", the 2012 discounts site (still showing Jumia offers in 2016), and by 2024 a page advertising an email journal. Most of them were about uncovering hidden data in some way - except the journal, which was just a failed anomaly.

Archived funua.com pages: a 2010 shop with account and cart links, the 2011 Funua blog headed Uncover to Discover, and the 2013 discounts directory with shopping categories.

Three earlier lives of funua.com, from the Wayback Machine: a shop in 2010, a Christian blog in 2011, and the discounts directory in 2013.

Anyway, this year the old address got a new job, and it started with the sort of reading I actually do for fun.

I was reading Dan Abnett's Ravenor, ended up clicking through Warhammer 40k wikis, and remembered all the hours I used to lose on StarCraft wikis back in the day. You click on one character or battle, and an hour later you have ten tabs open. At some point I thought that maybe a Kenya wiki would be cool.

Once that thought was in my head, the Kenya Gazette felt like a natural next step. If you haven't looked at one, the Kenya Gazette is a publication of official notices, and its issues are full of individual announcements: companies, people, land parcels, public offices doing things. If you could organise all those notices properly, you would basically get the wiki for free. Who did what, to what parcel or company, and when: every one of those is a rabbit hole. You could start on a company page, click a parcel of land, jump to the notice about it, and open the printed source page to see it for yourself.

That was the idea, anyway: take the 2025 Gazette records and make them browsable as an actual wiki! Standing between me and my shiny new wiki were 254 Gazette PDFs, coming to 8,525 pages of dense official print.

My first attempt to get AI agents to build this thing was back in June.

I set up a team of three agents with specific jobs. There was a coordinator (called main) that saved finished work into the project history - what programmers call a commit - a worker that read notices and wrote the wiki pages (ingest), and a reviewer (review) to check everything before it went in. I figured I could stay out of the weeds and let them handle it.

Instead, I kept checking on them. At one point in June, the whole team had a single reviewed source saved, and a land notice sat stuck in review.

The design itself made everything slow. Because the agents were writing the wiki directly, finishing one notice also meant updating the subject pages, a shared ledger, navigation, the index, and a log. I was frustrated with the pace, yet I still didn't want to get my own hands dirty in the code. Working with AI on this in June needed too much of my attention, and it didn't feel like it was going in the right direction. So I put the project down for a few months.

On 30 September, I reopened it with Claude Code, Anthropic's coding assistant, ready to inspect what was already there and then delete and redo as much of it as we needed to.

What pulled me back was that the models felt much more capable than they had in June. Claude Opus 5.5 coordinated the restart, OpenAI's Codex coding agents - GPT-6.1 Sol among them later on - handled the bulk reading, and a stronger Codex model stepped in for hard cases. My rule for myself stayed the same: I would direct, and the agents would write and maintain all the code. Still outsourcing the hand work, basically.

We threw out the June workflow where agents edited the wiki pages directly. Now the reading agents had one job: read each notice and save labelled facts, events, people, companies, parcels, and source references into structured records. Ordinary Python code then loaded those saved records, connected everything, and generated static web pages. Someone browsing the finished site wouldn't trigger any fresh AI model calls at all.

Of course, we immediately hit PDF trouble. On our first test of a two-column Gazette page, a standard PDF reader dropped whole notices. So the agents wrote a replacement reader that used word positions and column rules to read down the left column and then down the right.

That evening, I set the first real run going and stepped away, possibly until morning, joking that we were about to see whether we would run out of credits. About twenty-five minutes later, Claude hit a session limit and stopped. Fortunately, the reads already finished were saved on disk for reuse.

Then I made a bad call of my own. Trying to be frugal, I insisted we build around a cheap model for the routine reading, figuring checks and balances would catch anything it missed. They didn't. When we ran proper comparisons, the cheap reader was leaving out details, especially on unusual notice types. We had to scrap that plan and switch to a better routine Codex reader, with a stronger model rereading hard notices, using the Claude and Codex subscriptions I already paid for.

Because the full pass was going to take a long time, on 14 October I moved the project onto a laptop that could stay on without sleeping. Claude set the job up, checked that it was healthy, and then mostly sat idle while Codex worked through the pages, starting from the reads we already had saved.

That first full pass finished with 2 failed batches out of about 2,900, so we kicked off a retry that reused our saved work. While that retry was running, I saw my usage meter climbing and panicked that Codex was eating my credits. Claude checked what was actually running on the laptop: nothing on that machine was calling a model at all, just a local Python script checking records on one processor core. The credits were going to a completely different project of mine running on Codex at the same time, Story Studio. I had blamed the wrong machine.

The retry finished with no failed batches, which meant the agents had read all 8,525 Gazette pages. Now we had to see if what they pulled out actually made sense.

Gazette notices are numbered in order, so we ran a sequence checker to look for gaps where a notice might have been missed. It reported nearly thirty-four thousand missing notices.

When the agents dug into it, the mistake was in the Gazette itself. In an April special issue, the printer had put Notice No. 53045 instead of 5304. One stray digit on the page made our checker count tens of thousands of phantom gaps. We fixed the number to 5304 in the record and kept a note that 53045 was what appeared in print.

Going through thousands of notices, you also run into plenty of spelling mistakes from human typists, and those break search and stop pages from linking up. So I made a rule: fix obvious typos in headings so pages connect, and keep the printed wording beside them as evidence.

Source typos are easy enough to deal with, but tables are much worse, as we found out with Notice 16521.

Notice 16521 is a National Land Commission notice full of tables listing land cases and the commission's recommendations. In one row - let us call it Row A - there was a dismissal of a claim to register community land. Right below it sat the next case, Row B.

When our routine text reader went through that notice, it attached Row A's dismissal to Row B.

Now, we had checks for this sort of thing, including a stronger text reader for harder notices. So the stronger text reader checked Notice 16521. And it agreed with the first one! Both of them put that dismissal on the wrong case.

Both models were reading flattened text pulled from the PDF, and the printed border separating Row A from Row B did not survive the flattening.

Two simplified tables: the text readers put the dismissal on Row B, while the printed border on the page keeps it in Row A.

A simplified version of the mistake, with the real case details left out. Both text readers gave the dismissal to Row B; the printed line keeps it in Row A.

When I had the agents go back over these hard notices, they compared what they had extracted against pictures of the actual pages. On the picture of the page, the mistake is obvious: the printed line right under the dismissal keeps it inside Row A. Switching to a stronger reread that worked directly from the page images recovered the cell boundaries, and the dismissal went back where it belonged.

Did that solve every table in the Gazette? I wish.

Even in our final review of hard notices, the agents were still finding table-cell slips, or Treasury rows grouped under the wrong spending section. Tables in PDFs are just nasty. Right now, 36 notices on the site carry public review warnings, stating the reasons - such as unsupported printed values or missing case and parcel numbers - alongside links back to the official source PDFs so you can check the print yourself.

Still, once the records were connected properly, a very different problem showed up: real private people were much easier to find.

The Kenya Gazette is already public, and plenty of its notices are about private people: probate, name changes, land dealings, insolvency. Scattered across hundreds of separate PDFs, they take some digging to find. Put them on a site whose Swahili name means to uncover and link them together, and a person's affairs can end up a click apart.

Our first privacy rules focused on person pages, until a review caught an indirect leak: a private person's name was still showing up openly in an office page title that named a law firm's client. So we had to extend the privacy rules beyond person pages to mixed notices and non-person pages whose titles contained private people's names.

That still left the names themselves. I decided not to shorten private people's names, so their full names stay readable on their pages, and you can still browse those pages on the site. Around the names, we hide sensitive details. On the public site, we mask identifiers like national IDs, passports, phone numbers, bank accounts, and KRA PINs, and we strip personal postal and street addresses out of the fields and table columns where they appear. Official submission or registry addresses stay, along with public-body government emails and insolvency practitioners' registration numbers. Private-person pages are also left out of site directories and sitemaps, and carry an instruction asking search engines not to list them. I do not know if where I landed is the perfect answer, but it is the trade-off I chose.

I still wanted the wiki I had pictured from the start, the kind where every school, well, and market gets its own page, so that went into the plan. In the same breath, confident as ever, I set deployment for Monday.

Monday became Friday, 23 October.

Cloudflare permissions felt like a bad puzzle. Once we got through it, the agents rehearsed the deployment on a subdomain, checked it over, and switched funua.com to the Gazette wiki, about three weeks after I reopened the project.

Later on launch day, an email came in warning about my hosting request quota. Claude checked the traffic: the site was still up, but heavy crawling by ClaudeBot, Anthropic's web crawler, was eating into our request allowance. Most of our early visitors, it turned out, were robots.

I kept normal access for Google and Bing, limited other verified crawlers to one request every ten seconds per address, and put a challenge in front of unverified clients that called themselves bots. Five days later, on 28 October, I blocked the AI training crawlers outright - ClaudeBot included, along with GPTBot, Meta's and ByteDance's crawlers, and Common Crawl's.

Then on 8 November, a scraper pushed funua.com past its daily request allowance and knocked the site offline. Visitors got an error page until midnight UTC. My hosting plan allows 100,000 requests a day, and on top of the other crawlers, this one scraper made about 87,000 of them.

It was hard to catch because it came from thousands of addresses, about ten requests each, using old Chrome and Firefox browser names, so neither the rate limit nor our bot rules held it. What gave it away were two habits. First, it barely fetched stylesheets, which a real browser loads to make a page look right. Second, nearly all of its requests (97%) came over HTTP/1.1, an older version of the web's protocol, whereas modern browsers normally reach Cloudflare over HTTP/2 or HTTP/3. Rather than pay for extra hosting capacity before we knew how to fight it, we put a targeted challenge on unverified HTTP/1 requests on 9 November, and the traffic came back down.

Two days before the outage, on 6 November, a separate due-diligence tool had fetched the same notice thirty times in a single day. I really wished I could track them down and ask them to pay for the data.

Anyway, the site is up today, holding 19,416 notice records from 254 issues of the 2025 Kenya Gazette.

If I open the company page for Mayfair Holdings Limited today, it gathers 29 land notices and links 29 land parcels from one issue. It also notes that those mentions are matched by name alone, since no company registration number was printed in the notices.

Clicking one of those parcels takes me to Nakuru Municipality Block 2/1027. The heading spells Municipality properly so links and lookups work, while right underneath, beside "Printed as", sits what the Gazette actually printed: "Nakuru Muncipality Block 2/1027".

From that parcel I click through to Gazette Notice No. 18240 of 2025. The notice records that the Land Registrar, Nakuru, intends to reconstruct a lost or destroyed land register for the parcel, names Mayfair Holdings Limited as proprietor as printed, and gives sixty days for objections. Funua is an independent guide, so the page also has an Official PDF link that opens the full 11 December 2025 issue hosted by Kenya Law, where Notice 18240 sits in two-column print on page 9007.

Three steps on funua.com: Mayfair Holdings Limited, parcel Nakuru Municipality Block 2/1027 with its printed spelling, and Gazette Notice No. 18240 with its Official PDF link.

The start of Gazette Notice No. 18240 on printed page 9007, naming Mayfair Holdings Limited as proprietor of Nakuru Muncipality Block 2/1027.

Company, parcel, notice, and the printed page it all came from.

That's the rabbit hole I wanted when I closed my Warhammer wiki tabs.

That 2012 discounts business did die, just not quite in a month or two. Fourteen years later, the address is still here. Why don't you open up funua.com?

Working with me

If you have messy documents, public records, or other challenging AI and programming problems you want turned into something people can actually use, I am available for hire. You can read my resume, look at my code on GitHub, or get in touch through the contact page.