| client | Lionheart Strategy International — critical minerals trade facilitation, South Africa to the United States |
| role | Sole developer |
| status | Live, running in production |
| stack | Firebase · Google Genkit · Gemini 2.0 · TypeScript · Flutter |
Trade facilitation runs on follow-up. Someone has to research a counterparty, prepare the document, send the message, notice that nobody replied, chase it, log what happened, and do that consistently across a pipeline for months. It’s structured, repetitive, high-volume work where the cost of dropping a thread is real — which makes it exactly the sort of thing worth automating and exactly the sort of thing that goes badly wrong when automated carelessly.
LSI facilitate trade in critical minerals between South Africa and the United States. I built them a system of seven specialised AI agents that runs that pipeline.
Each one owns a stage and hands off to the next:
| Agent | What it does |
|---|---|
| LEON | Finds and scores prospective buyers, and produces a ranked shortlist each week |
| SIERRA | Drafts the outreach — email sequences and connection messages |
| DELTA | Qualifies an incoming enquiry into a one-page brief with a recommendation |
| NOVA | Runs the compliance and due-diligence screen and issues a clearance memo |
| ATLAS | Structures the cleared deal — terms and the logistics route |
| IRIS | Keeps the CRM straight: records, follow-ups, meeting summaries |
| APEX | Reports — pipeline state, market data, policy changes worth knowing about |
Read down that list and it’s a sales team’s org chart, which is the point. The division isn’t technical, it’s the one the business already had.

Why seven and not one
The instinct with a large language model is to give it the whole job and a long prompt. It usually half-works, and it fails in a way that’s impossible to debug: the output is wrong, the prompt is three pages, and you have no idea which instruction lost.
Separate agents with narrow jobs behave differently. Each has one responsibility, a defined input and a defined output, and when something goes wrong you can see which step produced the bad result. It’s the same reason software is written in functions rather than as one long script, and it applies more strongly here, not less, because the components are non-deterministic.
It also means each agent can use the right model for its job. Gemini 2.0 comes in a fast, cheap variant and a slower, stronger one — and the difference between running everything on the expensive model and running it only where judgement is needed is, at pipeline volume, the difference between a system that pays for itself and one that doesn’t.
Every model output is validated before anything acts on it
This is the single most important design decision in the system, and it’s the one I’d argue hardest for on any AI project.
Every flow validates its input and its output against a schema. Not “the prompt asks for JSON” — an actual schema, checked in code, that fails loudly when the model returns something else.
Language models are confident by construction. They will return a field you didn’t ask for, omit one you did, put a date in a different format than yesterday, or produce beautifully-formatted nonsense. None of that is a bug in the model; it’s what a probabilistic text generator does. The bug is treating its output as trustworthy because it looks right.
So the model’s response is untrusted input, exactly like a form submission from a stranger. It gets validated at the boundary, and nothing downstream runs on unvalidated data. That single rule is most of the difference between an automation that works in a demo and one you can leave running.
Agents that touch real systems
The agents don’t live in a chat window. They act on the systems the business actually uses: the CRM, where the pipeline state lives; Gmail, for outbound correspondence; Drive and Docs, for the documents that get produced; and Firestore, holding each agent’s own state, configuration and templates. Scheduled triggers run them on a cadence rather than waiting for someone to click.
The compliance agent is the one I’d point a sceptical reader at. Trade in this sector carries real obligations — sanctions screening, export documentation, ESG verification — and the agent’s job is to run that screen and produce a clearance memo with a clear verdict, rather than a paragraph of prose someone has to interpret. It doesn’t decide anything. It assembles the evidence and states a position, and a human signs it off. That’s the correct division of labour for compliance work, and it happens to be the one that makes the output auditable.
That’s the part that makes it useful and the part that needs thinking about. The mail access is granted through a Workspace service account with domain-wide delegation — in plain terms, the system can act as the organisation rather than as one named user. That is a serious credential to hand something that generates its own text.
Which brings us to the decision I’d point at first if someone asked what makes this system safe.
The agents write. A person sends.
So the system does not send email. It drafts it.
Every message an agent produces lands in the Gmail account as a draft, where a human reads it and decides whether it goes. The agent has done the work — researched the counterparty, written the message, put it in the right thread — and the last action, the irreversible one, belongs to a person.
That’s one design decision and it changes the entire risk profile. Everything the system does becomes reversible: a bad draft is deleted in two seconds and costs nothing but the reading. Compare that with the same system given send permission, where being wrong means a counterparty in an international minerals negotiation has already received something the company didn’t intend, and there is no undo.
It’s worth being precise about what this is, because “human in the loop” gets used to mean almost anything. It isn’t a review queue nobody looks at, and it isn’t an approval step bolted on afterwards. The reviewing happens in the tool the team already lives in, on the artifact itself, as part of their normal working day. The lowest-friction place to put a human check is somewhere they already are.
I’d defend this even where the model is good enough to send unsupervised, and I’d expect to keep defending it. The cost is one person skimming a draft. The saving is that no failure of the system is final. Given autonomy is easy to add later and impossible to take back after an incident, that trade is not close.
Testing something that doesn't give the same answer twice
Conventional testing assumes determinism: given this input, expect that output. A language model breaks that assumption on the first line.
The approach here splits the problem. The deterministic parts — schema validation, state transitions, the integrations, what happens when an external API fails — are tested normally, offline and fast, because they’re ordinary code and they’re where most real bugs live. The integrated behaviour is tested against the Firebase emulator, so the wiring is exercised for real without touching a live system or a client’s mailbox.
What that gives you is confidence in everything except the model’s judgement, which is the correct place to draw the line. You cannot unit-test whether an answer was wise. You can absolutely test that a malformed answer is caught, that a failed send is retried rather than lost, and that a crashed run doesn’t leave the pipeline in an ambiguous state.
Separate development, staging and production environments do the rest. Agents that send email need somewhere to be wrong that isn’t production — and for autonomous systems that’s not hygiene, it’s the whole safety model.
What I'd take from it
So what often happens with agent projects is that all the attention goes to the agent, and the agent is really the easy part now. Any competent developer can get a model to produce a good looking draft. The engineering is in everything around it: what it is allowed to touch, what happens when it is wrong, how you find out that it was wrong, and how you stop one bad output turning into something you cannot take back.
Autonomy is a dial and not a switch, and I think the honest default sits lower than it is fashionable to admit. Drafting instead of sending cost this system almost nothing and it made every failure recoverable. You can always turn autonomy up later once the evidence justifies it, and you can never un-send an email.
That is the same argument as the permission model in my own agent tooling and the same instinct as the compliance work on the government platform, which is to decide the constraints before you build on top of the assumption that nothing will go wrong.
The cheap decision here was the model split. Working out where judgement is really required, instead of routing everything through the strongest model available, is what makes the economics work at volume. It is a boring thing to get right and I think it is usually the difference between a pilot and a system that stays switched on.
If this is the kind of problem you have, here is how I work on it. The tooling that keeps a write reversible is described in HakunaMCP.