Multi-agent crews and runaway spend anyone solved delegation without a shared budget ceiling?

Running a crew where one agent delegates sub-tasks to others, and a couple of those sub-agents have tools that call paid services. The delegation pattern is great for capability, but it also means I’ve got N agents each independently deciding to spend money, with no shared awareness of a budget across the crew.

Concretely: agent A delegates a research task to agent B, B decides it needs three paid API calls to do it well, and there’s nothing stopping B from deciding that three more times if the first pass isn’t satisfying. Multiply that across a crew of five and the blast radius if something misfires is a lot bigger than a single-agent chain.

Has anyone built (or found) something like a shared spend ceiling that the whole crew respects, independent of which agent is making the call? Interested in whether people are handling this at the crew config level or bolting on something external.

The reason this bites specifically in a delegating crew is that the budget lives in the wrong place. Each agent’s LLM only reasons about its own task — there’s no natural spot in the delegation flow where “how much has the whole crew spent so far” is visible. So no individual agent can respect a ceiling it can’t see, no matter how you prompt it.

The pattern that’s actually held up is to move the budget out of the agents entirely and into a single shared object the tools consult, not the LLMs:

  • A crew-scoped ledger (one counter, created per run) that every paid tool wraps. Before B’s API tool fires, it does a check-and-reserve against that shared counter — estimate the call’s cost, atomically reserve it, and refuse if the reservation would breach the crew ceiling. Reconcile to the actual charge after the call returns.
  • Because it’s the tool enforcing it, not the agent, it doesn’t matter which agent (A, B, or B’s third retry) is calling — they all debit the same pool. That’s what makes it a genuine crew-wide ceiling instead of five independent per-agent limits that sum to 5× your intended cap.
  • The retry-storm you describe (B deciding it needs three more calls) is exactly what this catches: each retry hits the same shrinking pool, so the third pass gets denied by the ledger even though B’s reasoning still “wants” it.

CrewAI doesn’t hand you this at the config level today as far as I can tell — you bolt it on by wrapping your paid tools with the reserve-check. A dependency-injected budget object passed into the crew and closed over by each tool wrapper is the least-invasive way I’ve found to do it.

One thing worth separating out: a chunk of a crew’s spend is predictable before the run even starts — the system prompts + full tool schemas get re-sent and re-billed on every turn of every agent, and that part is countable statically. Bounding it up front (I use npx @wartzar-bee/tokenscope for the static estimate, Apache-2.0) means your runtime ceiling only has to police the genuinely variable spend, which makes the ceiling much easier to set at a sane number.

Genuine question back: when a reservation is denied mid-crew, are you aiming to hard-fail the whole crew, or degrade the delegating agent onto a cheaper path? The “deny gracefully instead of crashing the run” behavior is the part I’ve found hardest to get right.

I think moving the ceiling into the tool layer is the right direction. If the LLM is responsible for deciding whether it is “okay” to spend more, the budget control is always going to be somewhat advisory.

The part I’d be careful with is the reservation itself when several agents are running concurrently. A simple shared counter can still race if two agents both see enough remaining budget and reserve it at nearly the same time. I’d make the reserve operation atomic and treat the reservation as a temporary hold rather than immediately treating it as final spend.

I’d probably structure each paid tool call roughly like:

estimate → reserve → execute → reconcile

If the reservation succeeds, the tool gets a bounded amount it is allowed to spend. When the call completes, the actual charge is reconciled against the reservation. If it fails before making the external request, the reservation can be released.

I’d also put a per-call maximum alongside the crew-wide ceiling. Otherwise, one unusually expensive tool call could consume most of the remaining crew budget even though the total ceiling itself is working correctly.

The other issue is what happens when the reservation is denied. I would avoid simply throwing an exception and letting the delegating agent retry the same action, because that can turn a budget-control mechanism into another retry loop.

Instead, the tool could return a structured result such as:

budget_exceeded

remaining_budget

estimated_cost

retry_allowed: false

Then the delegating agent can make an explicit decision: use a cheaper tool, reduce the scope of the task, return a partial result, or stop that branch of the crew.

That also makes observability much better. At the end of a run you can see not just “the crew spent $X,” but which agent requested the spend, which tool reserved it, how much was estimated, how much was actually charged, and how much was rejected.

I’d probably treat the budget as a runtime policy enforced outside the agent reasoning, with the agent only receiving enough information to choose a fallback. That keeps the safety boundary deterministic while still allowing the crew to degrade gracefully when it hits the ceiling.

The static token estimate you mentioned could be useful as an initial reservation too. Then the runtime ledger mainly has to deal with the unpredictable part rather than trying to control everything after the fact.

The interesting question for me would be whether a denied reservation should propagate as a normal tool result that the agent can reason about, or whether certain budget thresholds should terminate the branch immediately. I suspect having both behaviors configurable per tool would be useful.

The “budget lives in the wrong place” framing is the key insight in this whole thread — once the ceiling is something the tools consult instead of something the LLM is supposed to remember, most of these failure modes stop being possible instead of just being handled well.One thing I’d add: does this ledger persist only for the duration of a single crew run, or across runs too? Concurrent crews sharing one budget pool is where I’ve seen this get genuinely hard — the atomic-reserve pattern works cleanly within a process, but once you’ve got multiple crew instances (or scheduled runs) drawing against the same account-level ceiling, you need the ledger to live outside any single run’s memory entirely.

That’s basically the generalized version of what you’ve built here — instead of a per-crew object, each agent gets an actual wallet with a hard limit and a kill switch, so the reservation logic is infrastructure the crew calls into rather than something you re-wire per project.

Persistence is exactly the seam where this stops being a code pattern and becomes an infrastructure decision, so it’s worth splitting into two ledgers that people tend to conflate:

  • The reservation ledger (the atomic hold @Matthewjon is describing) only needs to live as long as the inflight calls it’s guarding. Its whole job is the estimate→reserve→execute→reconcile window, so it can be per-run and in-process — as long as the reserve is genuinely atomic.
  • The spend ledger — “how much of the ceiling is gone” — is the one that has to outlive any single run the moment you have concurrent or scheduled crews. Once two crew instances draw against the same account-level cap, an in-process counter can’t see the other’s spend, so the hold has to be taken against a shared durable store, not a Python object.

The mistake I made first was trying to make one object do both jobs. Separating them means the durable part can be dumb: a single row you decrement under a real lock (SELECT … FOR UPDATE, or a Redis INCRBY/Lua check-and-reserve) so the atomicity @Matthewjon flagged holds across processes, not just within one. The reconcile step then writes the true charge back to that same durable row. The “each agent gets an actual wallet with a hard limit and a kill switch” generalization you’re pointing at is basically this: the wallet is the durable ledger, and the per-run reservation object is just a short-lived lease against it.

One thing that makes the durable ceiling much easier to set at a sane number: a big chunk of a crew’s spend is the fixed overhead I mentioned above — system prompts + full tool schemas re-sent every turn of every agent — which you can bound statically before the run even starts. If the predictable part is already accounted for, the durable ledger only has to police the genuinely variable spend, which is a far smaller and less jumpy number to threshold on.

Question back at you: for the account-level wallet, are you reconciling against the provider’s actual billed cost (post-hoc, from usage records) or holding against your own pre-call estimate and never truing it up? The drift between estimate-at-reserve and actual-at-bill is the thing I’ve found decides whether a hard kill-switch fires too early or too late.

Yes, I think the reserve should stay an estimate rather than being treated as the final charge. The important part is that the reservation gives us a hard upper bound before the call, while the actual provider usage becomes the source of truth afterward.

My preferred flow is still estimate → atomic reserve → execute → reconcile. The reserve protects the shared ceiling while the call is in flight, and once the provider returns the actual usage/cost, the difference gets settled back into the durable spend ledger.

That also makes the distinction between the two ledgers useful. The reservation is temporary protection against concurrent calls, while the durable account wallet represents the actual accumulated spend. If the estimate was higher than the final charge, the unused amount is released; if the actual charge is higher, the reconciliation accounts for that difference before another reservation is allowed.

I agree that this is where the kill-switch behavior gets interesting. I wouldn’t want an estimate to permanently consume budget, because conservative estimates could otherwise stop a crew unnecessarily. At the same time, I wouldn’t want reconciliation to happen only at the end of an entire crew run, because concurrent calls could overshoot the account ceiling in the meantime.

So for me the key invariant is that the reservation is authoritative while work is in flight, but actual provider usage is authoritative once the call completes. The durable wallet then remains the shared source of truth across crew instances and scheduled runs.

On the persistence question: the split @wartzarbee2 drew is the one that matters,
and the reservation half can stay in-process while the spend half cannot. Once two
crew instances or a scheduled run draw against the same ceiling, an in-process
counter cannot see the other’s holds, so the hold has to be taken in a shared
durable store.

Cheapest way to see it is to reproduce it rather than reason about it. I ran the
same loop twice. A local counter across two processes spent 1,000 units against a
budget of 500, because neither process could see the other’s holds. The same loop
with both processes reserving against one row keyed to a shared job id stopped at
exactly 500.

Two things that bit me building it, in case they save someone time:

A reservation needs an expiry and something that reclaims it. A process that dies
between reserve and execute leaks budget permanently otherwise, and you find out
days later when the ceiling is mysteriously full.

The reconcile write has to be idempotent on a key. A retried call that records
twice double-counts silently, and it looks exactly like a bad estimate.

Disclosure so nobody has to guess: I build a tool in this space. Not linking it.
DMs do not seem to work from my account here yet, so if the schema is useful to
anyone just reply and I will write it out.

The ledger in the tool layer settles the ceiling question, and I would put a second read beside it, because a ceiling tells you when the crew has spent enough and says nothing about which calls it needed. In the delegation case you describe, B making three more paid calls when the first pass looks unsatisfying, the extra calls tend to re-establish something the crew already settled, a value B fetched two steps earlier, a decision A committed before delegating, or a search B already ran under slightly different wording. In my studies of long runs, repeat calls of that kind cluster late in a run and account for a meaningful share of the spend a ceiling ends up cutting off. I refer to that share as reflexive burden, the work an agent spends re-deriving its own committed state.

The ceiling and the repeats respond to different fixes. A ceiling stops the run. B still has no way to learn that the answer already exists in the crew’s own event stream. Checking each paid call against what the crew has already committed, before the reserve fires, turns a repeat into a no-op, and that check runs off the same events the ledger already wraps, so it lives in the tool layer with the reserve and stays out of the prompt. Has anyone here split repeat calls from novel calls in the reconcile step? The split changes which lever you reach for.