Inspiration
Every morning, someone at a multi-entity company spends one to two hours deciding which subsidiary bails out which. Getting it wrong is an overdraft, or a covenant breach that ends up in a board pack.
We automated the judgement, not just the arithmetic — and it knows when to stop and ask.
Most companies of any size are not one company. They are six or ten legal entities, in different countries, holding cash in different currencies, each with its own bank and its own minimum balance it is contractually forbidden to drop below.
Someone has to sit down every morning, pull the balances, work out which subsidiary is about to run dry, and decide who should send money to whom. It takes one to two hours. It is done in a spreadsheet. Getting it wrong means an overdraft or a covenant breach, and covenant breaches are the kind of mistake that ends up in a board pack.
What makes it genuinely hard is not the arithmetic — it is that the obvious answer is frequently illegal. The parent company has plenty of cash, but an Irish subsidiary lending upstream to a US parent is a deemed dividend under IRC §956. Singapore could cover Germany, except intercompany lending between them is suspended pending a transfer pricing review. The constraints are written in loan agreements, in prose, and they change.
We wanted to automate the whole judgement, not just the arithmetic.
What it does
Sluice takes over the daily multi-entity cash positioning run end to end.
It projects every entity's cash position across a 14-day horizon, identifies who will breach their floor and when, and then solves for the cheapest set of transfers that keeps every entity above its minimum — respecting FX spreads, wire fees, intercompany lending limits, settlement lag, and the pairs that are prohibited from lending to each other at all. It writes the treasurer a one-page memo explaining what is moving, what it costs, and what was rejected and why.
The part we care most about is what happens when there is no answer.
When no legal set of transfers covers every shortfall, Sluice does not produce a plausible-looking plan anyway. It identifies the binding constraint, works out which remedies are actually available — draw on the revolver, breach a soft internal policy with sign-off, delay a payable — ranks them by business cost, and escalates to a human with a recommendation. Contractual covenants are never in the option set. When a human rejects a remedy, that rejection is written back as a constraint, and it never resurfaces in a later run.
How we built it
The core insight is that this is not a language model problem wrapped in a finance story. Underneath is a real optimisation problem, and we built it as one.
The solver. A mixed-integer program over an (entity-pair × day) grid, in PuLP/CBC — one binary leg-activation variable per pair per day (~300 binaries at 6 entities over a 14-day horizon), solved in well under a second to a few seconds depending on scenario. It does not optimise one blended number: phase 1 minimises real group cost (FX spread + wire fees, on both the draw and any repayment); phase 2, holding that real cost at its optimum, minimises intercompany interest as a tie-break only. Interest is real to the paying entity but nets to zero on group consolidation, so a single blended objective let the solver trade away real cash cost to buy an interest saving that never actually left the group — the two-phase split closes that. Constraints enforce the covenant floor per entity per day, cumulative exposure limits per lender/borrower pair, prohibited pairs, and settlement lag — money sent on day d lands on day d + n, where n is pushed forward past weekends and public holidays in either the sending or receiving country. Every intercompany loan also carries a real maturity; if that maturity falls inside the horizon, the plan includes a mandatory repayment leg, priced the same way the draw is. The whole thing returns a provably optimal plan or it returns INFEASIBLE. There is no middle ground and nothing to hallucinate.
Where the model earns its place. The solver cannot tell you that breaching an internal policy minimum is recoverable while breaching a term loan covenant is not. It cannot read an infeasibility certificate and explain it to a CFO. It cannot weigh a damaged vendor relationship against a drawn revolver. Those are the three jobs we gave the model: diagnose infeasibility, rank remedies, write the memo.
Data. A fictional six-entity group, Meridian Systems, across USD, EUR and GBP with ten bank accounts. Three seeded scenarios drive the demo: a normal day, a covenant shock, and an unsolvable day. Prohibited lending pairs carry their legal justification in prose, because that justification is what the memo has to quote.
Inference. TensorMux, using GLM-4.7-Flash. Temperature is pinned at zero throughout — treasury output that changes between identical runs is not auditable.
Money is integer minor units everywhere below the presentation layer. Float drift in a constraint solver produces plans that are wrong by a cent and rejected by a bank.
How we used AO
We built through AO (Agent Orchestrator) from the start, in sessions numbered
2 through 17 on the project. 10 PRs opened, all 10 merged, across 27
commits on main.
The build was deliberately shaped as a serial spine, then a parallel fan-out.
The schema, solver and naive baseline are sequential: everything downstream
consumes a Plan object, so fanning out before it existed would have meant four
workers guessing at one interface. We froze that interface in a contract
document before any parallel work started. Once the solver merged, four workers
ran simultaneously in isolated git worktrees — infeasibility diagnosis, memo
generation, the Streamlit review UI, and tracing plus the metrics panel. Their
owned files were disjoint by design; the only file two workers touched was
requirements.txt.
AO's review loop then did real work. On PR #8 the reviewer returned changes requested — "one real reporting bug, plus a correctness issue" — AO routed that back to the owning worker session, the worker pushed fixes, and the follow-up review came back approved: "all review threads resolved." That round trip happened without us mediating it, and it caught a bug we had not.
The kanban view mattered more than we expected. With four workers running, the thing that actually goes wrong is not agent quality, it is losing track of what is in flight.
Challenges we ran into
Designing a dataset that fails correctly. Making a scenario infeasible is easy; making it infeasible for an interesting reason is not. Our first attempt had the parent company holding so much cash that it trivially solved every shortfall, which made the constraint model pointless. We had to tune balances, flows and covenant floors until the cheap route was blocked for a legal reason rather than an arithmetic one. Under our covenant shock scenario the group's largest lender flips to being a borrower, which is the moment the whole idea becomes legible.
Context budget. GLM-4.7-Flash has a 32k window. The raw inputs — 84 forecast rows, 30 intercompany agreements, covenants, FX — crowd it and degrade the reasoning. We pass summarised positions and only the constraints that actually bind. This turned out to improve the memos as well: the model stops narrating the data and starts explaining the decision.
A reasoning model that fails silently. GLM-4.7-Flash fills a reasoning
field before it writes any content, burning 450–500 tokens first. Our client
returned content or "", so a truncated response was indistinguishable from a
genuine empty answer — and that empty string would have been rendered straight
into a treasurer's approval memo. It now raises on finish_reason == "length".
We also found that pinning temperature to 0 is not enough for reproducibility:
two calls differing only in max_tokens returned different answers, so
auditability needs both pinned.
Optimising the wrong thing. Our first objective blended FX spread, wire fees and intercompany interest into one number, which let the solver buy its way to "optimal" by trading real cash cost against accrued interest. We split it into a lexicographic objective: minimise real group cost first, then — holding that at its optimum — minimise interest. The headline savings number dropped when we fixed this. We kept the honest number.
A calendar that only knew about weekends. Settlement lag originally pushed a wire past Saturday and Sunday but not past a public holiday, so seed data had UK payroll and two other cash events scheduled to land on a bank holiday — a data bug that a real bank would simply reject. Two independent reviewers (a technical and a finance persona) both caught it independently, which told us it wasn't a one-off. We now resolve settlement against a real per-country holiday calendar, not just weekends, on both the sending and receiving side of a wire.
A covenant exemption that could be gamed. Balances before an entity's earliest-actionable day are exempt from the floor check, because no plan built today can physically move cash there in time. That exemption originally meant those days were unconstrained, so a plan could technically show "zero violations" while leaving an entity worse off than doing nothing at all on a day it couldn't fix anyway. We now hold every plan to its own zero-transfer baseline on those days — the exemption covers what a plan can't influence, not what it chooses not to.
Accomplishments that we're proud of
- Zero constraint violations, on every run, checked independently.
verify()re-derives every closing balance, intercompany exposure and settlement date from the database — not from the solver's own state — and returns a list of violations. It returns empty on every feasible scenario. This is asserted programmatically, not eyeballed. - 30.4% cheaper than the naive baseline of funding every shortfall from the parent company on the base scenario, and 12.7% on the covenant shock — though that covenant-shock number is inflated by an interest saving that nets to zero on group consolidation. Stripped to the FX-and-fee cost that's actually real cash, the solved plan there spends USD 26.26 more than the naive baseline, buying a much larger interest saving invisible to the consolidated group. We report both numbers rather than only the flattering one.
- Fast planning. 0.4s to produce an optimal plan on the base scenario, ~2.5s under covenant shock (two MILP solves now, not one, for the lexicographic split above) — against a one-to-two hour manual process. The full infeasible-scenario chain, including LLM diagnosis and the escalation memo, is ~75s.
- An agent that escalates rather than guessing. On the infeasible scenario it identifies the binding constraint — "MER-IE short 475,000 EUR of its 6,500,000 EUR floor on day 7 even after routing all available, permitted intercompany capacity" — ranks three remedies by business cost, and refuses to put a contractual covenant breach in the option set at any rank.
What we learned
The most useful design question we asked was: if we removed the language model, would there still be a system? For this project the answer is yes — a ledger, a constraint model, and a solver. That test kept us honest, and it is why the output is verifiable rather than merely fluent.
The second thing: the interesting engineering in an agent is almost never the happy path. Building the infeasible branch — diagnosing the binding constraint, ranking remedies, knowing which lines can never be crossed — took longer than building the solver, and it is the only part that would matter to a real treasury team.
What's next for Sluice
- Real bank connectivity. The
BankConnectorinterface is mocked but isolated, so swapping in a live API is a config change rather than a rewrite. - A naive baseline that prices its own repayment risk. Every loan now carries a real maturity and a mandatory repayment leg, but the naive comparison baseline still prices loans as if they stay open through the whole horizon — pricing its repayment honestly pushed at least one entity below its floor in testing, which is a real finding (naive treasury practice can create a repayment crisis it never saw coming) that needs a loan-rollover mechanism to represent properly.
- Covenant extraction from loan agreement PDFs, so the constraint set maintains itself as documents are amended.
- Intraday positioning, same-day cutoffs by bank and currency, and leverage/ DSCR-style covenants tested at period end rather than only daily minimum-cash floors.
- Learning across runs, so the remedy ranker reflects a specific treasurer's revealed preferences and not a generic policy.
Built with
python
sqlite
pulp
mixed-integer-programming
streamlit
tensormux
glm-4.7-flash
ao
neatlogs
openai-api