Inspiration

The Greater Horn of Africa does not have a forecasting problem.

While building this there was an active GDACS Orange drought alert over Ethiopia, Kenya and Somalia — event 1027450, running since April 2026 — and ICPAC was publishing weekly rainfall anomaly and heat-stress forecasts valid days ahead. The science was already good, already public, already free.

Two numbers set the direction. GSMA's 2026 Mobile Gender Gap Report: 68% of Ethiopian women own a mobile phone; 9% use mobile internet daily. The population most exposed to climate shocks is the population least reachable by any dashboard or app ever built. They are reachable by SMS, USSD and voice, and by essentially nothing else.

And ICPAC already has a dissemination platform — HUSIKA, "be concerned". We did not want to compete with it. Hatua is Swahili for action, or step. HATUA is the step after.

What it does

HATUA takes live multi-hazard data for all eight IGAD member states, reasons about compound risk at district level, and emits verified, actionable, localised advisories down every channel that reaches a feature phone.

Three things make it different from a dashboard:

HATUA dashboard

Compound risk, not single hazard. Drought is normally monitored separately from conflict, displacement and food insecurity. In this region those are the same crisis. In our live run, Kampala had the maximum flood signal at IPC Phase 1 and scored 0.339. Lower Juba had a far weaker drought signal but sat at IPC Phase 4 with 87 conflict events and 64,000 displaced — and ranked higher. A single-hazard system would have sent Kampala the louder warning.

It refuses to send what it cannot prove. Every advisory passes six checks before dispatch. In the run shown in the demo, 28 of 54 advisories were blocked — 52%. The blocked ones are visible on the dashboard, with the failing check named.

A blocked advisory, with the failing check named

A blocked advisory. The failing check is named, and the message was never sent.

It reaches the last mile. SMS, USSD, Telegram, voice and a CAP 1.2 feed. Seven languages. Audio in Afaan Oromo and Tigrinya, which no commercial text-to-speech service supports.

How we built it

Python 3.11, FastAPI, Pydantic v2, async httpx, MapLibre GL, Docker, Render.

The architectural decision that shaped everything: the numbers are deterministic; only the words are generated.

hatua/fusion/engine.py contains no model. It is arithmetic:

CRS = hazard_composite
      × exposure_multiplier       (max 1.5)
      × vulnerability_multiplier  (IPC, conflict, displacement — max 2.6)
      ÷ (1.5 × 2.6)

That is deliberate. If a district is told to move livestock, an ICPAC analyst must be able to open one file, read the weights, and disagree with a specific number. You cannot do that with an embedding.

The language model never computes risk — it explains risk computed by that file, and it never authors advice. It selects action_ids from a library built from published IGAD, FAO and WFP anticipatory action guidance.

The Verifier runs six checks. Five are deterministic and run first, because a check that depends on a model cannot police a model. The model only adjudicates semantic faithfulness, and it can block but never rescue. It fails closed.

Confidence is deliberately not a term in the risk score. It gates whether a trigger fires and how severe an advisory may be. Folding uncertainty into the risk number would quietly downgrade a real threat we happen to have thin evidence for, when the correct response is to warn more cautiously.

Challenges we ran into

Our safety check was a tautology. geographic_sanity searched the advisory body plus the district name for the district name — so it could never fail. We found it by writing a test designed to break it: "Nairobi County: flooding tonight" passed against a Somali Region trigger. A verification layer nobody tries to break is just a claim. Now the body alone must name the district.

Ethiopic numerals are invisible to Python. ፻፳፭ is Unicode category No, not Nd, so str.isdigit() misses it entirely — a model could have written any figure it liked in Ge'ez numerals and walked straight past the numeric check. We wrote a folder for it.

Amharic can never be GSM-7. 3GPP TS 23.038 defines national language shift tables for Turkish, Spanish, Portuguese — and nothing for Ethiopic or Arabic. Those scripts are permanently UCS-2: 70 characters per segment against Swahili's 160, so the same warning costs 2.5× more to send in Amharic. We write Amharic first and back-translate, rather than translating into it and discovering the budget after.

Free-tier LLM quota is metered per model, and there are two different 429s. ...PerDay means that model is finished; ...PerMinute is transient and should be waited out on the same model. We initially treated them alike and burned through every model in the chain in one second.

We leaked our own API key. httpx logs request URLs at INFO, and the key was in the query string. Moved to a header, pinned httpx logging to WARNING, rotated the key.

The model wrote incoherent Amharic naming the wrong region. We measured it rather than assuming. Amharic, Tigrinya, Afaan Oromo and Arabic now come from reviewed templates, not free generation.

Africa's Talking was not the cheap option. We researched before committing and found it near the top of the Kenyan market at KES 0.80/SMS against HostPinnacle's 0.20. We switched to Zettatel.

Accomplishments that we're proud of

The Verifier catches real hallucinations, and we have the log. It caught the model inventing "Avoid conflict areas and protect your livestock" — plausible, approved-sounding, in no action library. It caught a 112,000 displacement figure being reused as if the displacement were flood-caused. Neither was sent.

HATUA Telegram bot

Audio in languages nobody else serves. Afaan Oromo and Tigrinya have no commercial TTS provider anywhere. Meta's open-weight MMS-TTS models do, so roughly half the adult women in Ethiopia and Somalia who cannot read the advisory can hear it.

We render gaps as gaps. Eritrea has no IPC data, no CAP feed and a 2001 census baseline. The dashboard shows "no data" — not a fabricated score.

We record our own false positive. The Verifier initially blocked a correct message because we had not shown it the approved action deadlines. Over-blocking is the right failure direction for a life-safety system, but it is a real tuning cost and pretending otherwise would be dishonest.

It costs what we say it costs. Reaching 100,000 people is KES 20,000 (~US$155) via Kenyan aggregation against KES 4,040,000 via Twilio. That 200× gap is the difference between a pilot and national scale.

What we learned

A check that depends on a model cannot police a model. This one principle reordered the whole Verifier: deterministic checks first, model last, model can block but never rescue.

Uncertainty should gate, not discount. A hazard is no less dangerous for our being unsure about it.

Language support is a delivery claim, not a feature list. Offering a language in a menu and then failing to deliver it wastes the one USSD session a user has. The menus now only offer languages actually issued for that district.

Encoding is an equity issue. A system that ignores GSM-7 versus UCS-2 quietly charges Amharic and Tigrinya speakers 2.5× more to be warned.

Try before declaring something impossible. Voice was going to be a "limitation" section. Measuring instead of assuming turned it into working audio in seven languages.

What's next for HATUA

Validation by the people who would use it. The action library is built from published IGAD, FAO and WFP guidance, but it needs national met agency and NDMA review before a single real SMS goes out. Nothing here should reach a subscriber until that happens.

Real vulnerability data on a live feed. IPC phase, conflict counts and IDP figures in the demonstration district set are pinned representative values in the range HDX HAPI returns. The connectors are live; the pinning is for reproducibility during judging, and it is documented as such.

A HUSIKA adapter, agreed with ICPAC. We emit CAP 1.2 with status=Exercise and built the adapter as a pluggable interface. Making it real is a conversation, not a code change.

Cell Broadcast. SMS is an advisory channel, not a flash-flood alarm — 100,000 subscribers takes about 2.8 hours at alphanumeric throughput. Cell Broadcast is the right technology for sudden onset, Kenya's CA is rolling it out, and because we emit CAP we plug into it rather than needing a rewrite.

Closing the ground-truth loop. The Telegram and voice channels already accept community reports. Feeding those back into confidence scoring is the piece that would make HATUA improve with use.

Built With

  • africas-talking
  • cap-1.2
  • climateserv
  • docker
  • fastapi
  • fews-net
  • gdacs
  • gemini
  • geospatial
  • glofas
  • groq
  • hdx-hapi
  • icpac
  • maplibre
  • open-meteo
  • pydantic
  • python
  • render
  • smsapi
  • telegram-bot-api
  • ussd
  • zettatel
Share this project:

Updates

Submission history