Debarred together by the African Development Bank in 2021 for fraud in tenders on a Kenyan education project. On a tender table, nothing connects the names. The affiliates were found by tracing common officers.
AegisVeritas
Two bidders. One owner.
AegisVeritas joins Kenya's published contract awards to the ownership and contact details firms disclose, and flags supplier pairs that are likely under common control but that a name check cannot see. At bid opening it checks one tender's bidders in under a second. Every flag shows its evidence, and a person makes every decision.


What the committee can't see
A debarred company rarely bids under its own name. It bids through a clean-looking company that shares a director, owner, phone number or address with it.
A name check passes that company, because there is no name to match. Two "competing" bidders can also belong to the same person, so the committee compares them, picks the cheaper one and signs, and the competition was never real. Public procurement is about 60 per cent of Kenya's government budget. Since 2022 the owners of firms awarded tenders have been public, but nobody checks them against the bidders, and an evaluation committee sees one tender at a time.
of 2024/25 contracts went to firms that declared no owner: 14,819 of 25,994. When ownership is declared, it sits in records no committee can search across the whole market in the time it has.
What I built
A full system, not a notebook. It joins 144,097 published contract records with 12,441 declared ownership links, scores supplier pairs, and serves an investigator-facing web app. It runs on one laptop with Docker Compose, and invited evaluators reach it through Cloudflare Tunnel and Access.
Bidder screening
Checks a supplier against sanctions and debarment lists, then follows owners and directors two steps out to find exposure a name check misses.
Review queue and evidence cards
A short list of supplier pairs for an investigator, each with its evidence, how strong that kind of evidence has proven, and what to check next.
Pre-award tender check
Takes the bidders on one tender and reports any links between them: median 0.07 seconds, the way Kenya's e-GP system would call it at bid opening.
Ownership path tracing
Draws the path from a supplier to a listed party on an investigation canvas. The one practitioner tester rated it the most helpful part.
Tamper-evident audit trail
Every screening, verdict and tender check is appended to a SHA-256 hash chain. The app's own database account can add entries but cannot change or delete them.
Role-based access
Only investigator, auditor and admin roles see named pairs. Each institution sees only its own data, enforced by PostgreSQL row-level security.
My first approach failed
The proposal asked "is this one firm bad?" After the data was cleaned, only 18 confirmed Kenyan cases remained, and 11 of them had fewer than three recorded links.
A graph attention network had reported 91 per cent recall on a synthetic benchmark; once label leakage was removed, it fell to between 24 and 48 per cent, and on real Kenyan data no graph model beat a baseline that only looked at company size. So I changed the question to "are these two firms under one control?" For that question, the ownership details Kenyan awards must disclose give 12,441 labelled links. Kenya has no open ownership register to test hidden links against, so I also validated the method in Estonia, where both procurement data and beneficial ownership are public.
Three kinds of evidence, one probability
Candidate pairs are firms that win from the same buyer within 30 days, or that share a disclosed record. Each pair gets evidence from three channels, each a log-likelihood ratio, combined Fellegi-Sunter style so every part of the score stays explainable. A contact shared by many firms counts for less, and the behaviour model is capped so it can never outweigh an ID match.
How much each clue multiplies the odds that two firms share an owner
Starting weights in the evidence engine, log scale. They are replaced by weights learned from investigators' verdicts.
Show as table
| Evidence | Odds multiplier | Log-likelihood ratio |
|---|---|---|
| Same owner, matched by ID number | ×1,000 | 6.9 |
| Same director, matched by ID number | ×200 | 5.3 |
| Behaves like one company (behaviour model, cap) | up to ×55 | ±4.0 |
| Same phone or email | ×50 | 3.9 |
| Same address or P.O. Box | ×10 | 2.3 |
| Owner with the same name only | ×5 | 1.6 |
LightGBM on 20 award-pattern features, trained on Kenyan and Estonian pairs, with isotonic calibration. Calibration error 0.010. The strongest features: tenders won together, and the shortest gap in days between two firms' awards.
Silent on pairs where a firm has fewer than 3 awards, and on pairs unlike its training data: 44 per cent of held-back pairs. Refusing to judge is a result, not a failure.
A retrained model goes live only if it beats both the baseline and the current live model on a fixed holdout. The rule is in the code, and it caught a weaker model running live.
Results
Tests marked pre-registered had their protocol and pass mark committed to GitHub before the run, so the rules could not move after the results were seen. The baseline throughout is Adamic-Adar, the standard link-prediction score.
Estonia: firms the model never saw
AUC, with true owners hidden from the model and answers checked against Estonia's open register. 0.5 is a coin flip.
Show as table
| Method | AUC |
|---|---|
| AegisVeritas (LightGBM) | 0.85 |
| Adamic-Adar | 0.57 |
Estonia: the whole system
Share of all register-linked pairs found in the top 1,000, before and after adding register candidates to behaviour.
Show as table
| Top 1,000 | Earlier | Full system |
|---|---|---|
| Recall | 4% | 32% |
| Precision | 6% | 51% |
Kenya: held-back supplier pairs
Average precision on 7,884 pairs the model never trained on, only 178 truly linked. Reproduced on a full retrain.
Show as table
| Method | Average precision |
|---|---|
| AegisVeritas, Estonia + Kenya pooled | 0.146 |
| Kenya only | 0.144 |
| Adamic-Adar | 0.067 |
| Estonia only (failed) | 0.065 |
| Random (base rate) | 0.023 |
What a reviewer gets
Each square is one supplier pair. Filled squares are real links.
AegisVeritas top 100
25 linked
100 random pairs
about 2 linked
Linked pairs found as the review queue grows
The model that runs in the app, on 7,783 held-back pairs it never saw (178 linked), against Adamic-Adar. Queue size on a log scale.
Show as table
| Top k pairs | Linked found, model | Precision, model | Linked found, Adamic-Adar |
|---|---|---|---|
| 10 | 4 | 0.40 | 2 |
| 50 | 15 | 0.30 | 7 |
| 100 | 25 | 0.25 | 10 |
| 200 | 38 | 0.19 | 20 |
| 500 | 68 | 0.14 | 37 |
| 1,000 | 96 | 0.10 | 72 |
A register the model never saw
KeRRA, the rural roads authority, publishes contractor registers that list each firm's P.O. Box. The model flags pairs from ownership records only; the registers were never part of its data. This check was run after the detector was built.
209.9 times more often than same-buyer controls, and at least 63.8 times at the cautious end of the 95% interval (Fisher's exact test, p = 1.0e-42). Against random pairs from the same region: 118.6 times, at least 23.2 times (p = 2.7e-27). A shared box shows a relationship, not wrongdoing, and the p-values assume independent pairs, so they are indicative.
The Kenyan labels come from disclosed ownership, so the Kenyan result shows that award behaviour recovers links that were disclosed. Estonia's official register is what supports the claim for hidden links. There, 68 per cent of linked pairs had won the same lot together; without those features the model still reached 0.83, but on pairs that never won together it fell to 0.66 against 0.56.
Screening the Kenyan records against the final watchlists found one company under World Bank cross-debarment since July 2013 that won two published Kenyan contracts, in 2019 and 2020. A development bank debarment does not by itself bar a firm from Kenyan tenders, so this is a lead for due diligence, not a finding that any rule was broken.
Every objective, against its target
The proposal set a target for each objective before the work began. This is how each one ended.
Review existing mechanisms
Existing checks rely on names and on firms declaring their own owners, so they miss nominees, relatives and affiliates.
Build the graph
144,097 contract records and 12,441 declared ownership links joined. Entity resolution precision on a hand-checked sample was not measured.
Graph representation learning
No graph model beat the size-only baseline on real data. The replacement pair model reached 0.141 against 0.066, over five seeds.
Web application
Every feature built and verified by tests: 687 of 701 tests passed, none failed, and no system check failed.
Practitioner evaluation
The target was 12 to 15 practitioner interviews; one practitioner took part. Ease of use was then tested with ten general users.
Conditions for success
It works where firms leave traces in public records, and it abstains on 44 per cent of pairs, mostly firms with fewer than three awards.
Reported in full
Failed and withdrawn results sit in the results ledger next to the ones that passed. They are the reason the passing results are worth believing.
A benchmark that leaked its labels
A graph attention network's recall on a synthetic benchmark fell from 91 per cent to between 24 and 48 per cent once the leakage was removed. On real Kenyan data, no graph model beat company size alone.
A model trained on Estonia alone failed in Kenya
On the Kenyan holdout it scored 0.065, no better than the 0.067 baseline.
Two outside tests missed their pass marks
The pre-registered KeRRA tests on firms with no disclosed link found no signal at their primary endpoint, and a back-test on later ownership disclosures was underpowered at every cut-off.
My own safety gate caught a weaker model running live
The model then running live used an importance weighting that was never tested, and scored 0.0745 on the held-back test.
A defect found in the last week
Testing showed the tender check matching two invented bidder names to the same real supplier, then reporting no links.
A result I withdrew
An Estonia-to-Kenya AUC of 0.87 turned out to have read the synthetic sample market, not real data.
Every number rerun after submission
The submitted figures came from a run on an older database that still held duplicate company records. After a clean rebuild, concealed pairs went from 3,976 to 3,916 and the P.O. Box ratio from 242 to 210 times. The submission is tagged in git, and every run is kept.
Built to be trusted
Because it names firms, the system has to be correct, explainable and hard to misuse. Every test had its pass mark fixed before it ran, on the final code between 7 and 10 November 2026.
Review queue under rising load
95th-percentile response time with 100 to 1,500 clients calling at once for 30 seconds each, on one laptop that also ran the load generator. Throughput levels off at about 350 requests a second.
One laptop serves between 300 and 600 users at once within the pass mark, with no failed requests at any level. A separate server, or more workers, would raise the ceiling.
Show as table
| Clients at once | Requests per second | Median / 95th percentile | Failed | Result |
|---|---|---|---|---|
| 100 | 390 | 0.23 s / 0.44 s | 0 of 11,857 | Pass |
| 300 | 351 | 0.84 s / 1.03 s | 0 of 10,785 | Pass |
| 600 | 339 | 1.77 s / 2.06 s | 0 of 10,627 | Fail (1.66 s on 8 Nov) |
| 1,000 | 378 | 2.58 s / 2.82 s | 0 of 12,282 | Fail |
| 1,500 | 318 | 2.98 s / 4.04 s | 0 of 10,436 | Fail |
| Connection | Tender check | Page ready |
|---|---|---|
| No throttling | 0.49 s | 3.87 s |
| Fast 4G | 0.49 s | 1.63 s |
| Slow 4G | 0.59 s | 4.56 s |
| 3G fail | 2.04 s | 16.09 s |
Through the live link on 7 November. A dropped connection now shows "Could not reach the server" instead of a raw error.
| Algorithm | Why |
|---|---|
| Breadth-first search | Shortest path to a listed party, skipping buyers |
| Union-find | Firm groups, so no group crosses the holdout |
| Sliding time window | Candidate pairs: same buyer within 30 days |
| Fellegi-Sunter fusion | Combine evidence, keep each part explainable |
| SHA-256 hash chain | Tamper-evident audit log |
Seven data connectors implement one abstract connector; two risk models share one interface, so the graph model can be swapped for rules. A full retrain and scoring of 1.44 million candidate pairs takes 3 minutes 45 seconds and 2.6 GB of memory. Start-up checks stop the app with a clear message if a database, table or model file is missing.
A person always decides, as Kenya's Data Protection Act requires. The API serves links by kind and date, never people's names, and firm names can be masked as stable codes. The system is not on the open internet, because its findings name firms that have not been found to do anything wrong: invited evaluators reach it through Cloudflare Access.
Tested with people
Ten general users opened bids, read evidence and screened suppliers. They finished 39 of 40 tasks, and rated the app 74 out of 100 for ease of use.
Median 72.5, range 45 to 90. Seven of the ten scored at or above the published average of 68.
Open a tender's bids and explain the warning
Read a pair's evidence and record a verdict
Screen a supplier against the lists
Show an auditor no decision was changed
A supply chain researcher finished all six guided tasks and gave a usability score of 67.5. The ownership path was rated the most helpful part, 5 of 5, and easier to trust than the score; explaining a result to an auditor was rated 3 of 5.
Clearer labels for warnings and risk levels, plain explanations of terms such as "common control", links to the original evidence, and an easier audit history. One asked whether a warning meant the supplier should be rejected automatically. It does not.
The target was 12 to 15 practitioner interviews, under ALU ethics approval SEPT26-BEL-070. One practitioner took part, and six of the ten general users described work in audit, banking, risk or procurement, so these results are early feedback, not proof of usefulness in practice.
The product
Real records with firm names masked as stable codes. The screening example uses an invented demo company.
At bid opening, a simulated e-GP screen sends one tender's bidders to AegisVeritas. Only the e-GP screen is simulated: the check and its answer are real.


Tested on Pixel 7 and iPhone 14 sizes
The system is invite-only. Evaluators and recruiters can ask for a guided session.
Walkthrough
A recorded tour of the system: screening, the review queue, an evidence card and the pre-award tender check.
A reason to look, not an accusation
The system sees only what is disclosed: about 48 per cent of linked pairs left no behavioural or contact trace, and 75 of the top 100 Kenyan pairs are not confirmed links. Suppliers are matched by normalised name, and entity resolution precision has not been measured on a hand-checked sample. The bid-opening check was tested on a simulated tender, the page takes about 16 seconds on 3G, and the model's reach to firms that declare no owner rests on the Estonian result.
The PPRA should add a pre-award check to Kenya's e-GP system: when bids close, send the bidders to the check and give the committee a brief on any links, so it can ask bidders to declare common control before signing. It should also enforce ownership disclosure and publish it in machine-readable form. Banks and development finance institutions could use the same engine in onboarding, trade finance and bid-bond lending.
A pilot with an organisation that works with procurement authorities. Batch scoring to lift the memory limit, an early-stopping split kept apart from the held-back pairs, a feature that discounts batch awards on the same day, and data from Uganda's and Rwanda's procurement portals.
The final report was submitted in November 2026. The system has run invite-only since 28 October 2026, under ALU ethics approval SEPT26-BEL-070. The repository is private; access is given on request.
When the public pays for competition, it should get competition.




