← Blog

AI agents

Budgetlimieten voor AI-agents: het DN42-incident van €4.820

Om 06:14 op zondag startte een customer-success agent in Eindhoven een DN42-verkenningsloop. Elf uur later had hij €4.820 verbrand aan één ticket van €189.

Jacob Molkenboer· Oprichter · A Brand New Company· 12 jun 2026· 9 min
Messing buispostkoker op botpapier, limoengroen label, papieren ticket, messing relais, zakhorloge, leren onderlegger.

Het alert kwam binnen om 06:14 op zondag. De egress op de customer-success agent had €1.000 gepasseerd binnen zijn eerste zes uur. Om 09:30 stond de teller op €1.840. Toen de engineering lead bij de Eindhovense SaaS hem om 17:32 killde, las hij €4.820.

De agent draaide vijf dagen in productie. Bedrijf van negenentwintig man, mid-tier B2B SaaS, een standaard customer-success copilot gekoppeld aan Intercom, Stripe, de productdatabase en (hier wordt het interessant) een outbound shell tool met het label "diagnostics". Het ticket dat de loop startte was het meest gewone op de queue: een betalende klant meldde dat zijn webhook deliveries te laat aankwamen.

Het verhaal staat deze week op de HN-frontpage. Het is ook precies de vorm van incident waar we de afgelopen achttien maanden omheen ontwerpen voor onze eigen deployed agents. Dus hier is de walkthrough, plus het orchestrator-patroon dat het bij de eerste retry had gevangen.

Wat de agent dacht dat hij deed

Het webhook-endpoint van de klant zat achter een self-hosted reverse proxy op een kleine VPS in Frankfurt. De agent kreeg de vraag bereikbaarheid te bevestigen en draaide curl. De curl faalde met een TLS-handshake error. De agent redeneerde, terecht, dat de volgende stap was om het onderliggende netwerkpad te checken. Hij draaide traceroute. De traceroute liet asymmetrische routing zien via een autonoom systeem dat de agent niet herkende.

Op dit punt zou een menselijke engineer gestopt zijn, Slack geopend hebben en de klant om zijn netwerksetup gevraagd hebben. De agent had geen Slack-tool. Hij had een shell. Dus hij ging door.

Het pad dat hij koos was DN42, een gedecentraliseerd netwerkexperiment dat overlay VPN-tunnels en BGP draait tussen autonome systemen van hobbyisten. De VPS-provider van de klant had toevallig een DN42-peer in hetzelfde datacenter staan. Het model besloot, na het lezen van de routing table dump uit traceroute, dat het goedkoopste pad om de netwerksetup van de klant te bevestigen was om de DN42 AS-lijst te enumereren, de peer te lokaliseren en een test daardoor te routeren.

Die beslissing was intern coherent. Hij was ook fout op elke andere meetlat. DN42 draagt geen productieverkeer. De peer was een sideproject van een sysadmin in Bremen. De daadwerkelijke webhook-vertraging was een verkeerd geconfigureerd retry-interval, zichtbaar in platte tekst in het Stripe-dashboard van de klant, twaalf clicks van waar de agent al read access had.

De kostenvorm van een elfurige loop

De €4.820 viel als volgt uiteen:

  • €2.940 aan outbound bandwidth vanaf de AWS-instance die de agent runtime hostte. Elf uur bijna constante scans, met retries en back-off retries erbovenop gestapeld.
  • €1.180 aan model inference. Elke scan retourneerde een muur van tekst. De agent voerde alles bij elke tool call opnieuw in de context, zonder samenvattingsstap ertussen.
  • €520 aan third-party API-fees. De agent had een "lookup AS owner" tool die een betaalde WHOIS-service aanriep. Hij riep hem 4.712 keer aan.
  • €180 aan downstream alerting noise (pager events, second-tier on-call). Echt geld zodra je mensen op weekendtarief meetelt.

Kijk naar de inference-regel. €1.180 aan tokens op één ticket waarvan de lifetime value €189 per maand is. Het model dat de loop draaide was geen frontier tier en niet de premium long-context variant. Het was een mid-tier productiemodel met een context window dat bleef groeien omdat niemand het had verteld te comprimeren.

Een groeiende context plus een open shell plus geen per-tool budget is geen edge case. Het is de standaardvorm van elk kant-en-klaar agent framework dat we dit jaar geaudit hebben.

De root cause was niet het model

De post-mortem in de interne Notion van het bedrijf framet dit als "het model ging rogue". Die framing klopt niet, en het is de framing die het volgende team in hetzelfde gat duwt.

Het model deed precies wat zijn tools en zijn halt condition toestonden. De orchestrator die het team bouwde was de standaardvorm die je krijgt door elke populaire agent tutorial te volgen: een planner, een tool list, een working memory en een halt condition die afhing van "task complete". De halt condition was de enige budget gate. Er stond een global daily cap van €500, met alerting op "warn at 80%", gerouteerd naar een Slack-kanaal waar niemand op zondag naar keek.

Het team had OWASP LLM10: Unbounded Consumption gelezen. Ze hadden er niets tegen geïmplementeerd. Dat is, vanuit wat wij zien in onze migratie-audits, de mediane staat van agent deployments medio 2026.

Drie guardrails die de orchestrator miste

Per-tool budget

Elke tool in de toolbox van je agent heeft een kostenvorm. Een read uit je productdatabase kost een paar cent aan inference en nul aan third-party fees. Een WHOIS lookup kost €0,11 per call. Een shell-commando kost ergens tussen nul en oneindig, afhankelijk van wat het draait. De orchestrator moet alle drie weten, en moet een call weigeren voordat hij hem maakt.

TOOL_BUDGETS = {
    "read_db":      {"per_call_eur": 0.02, "per_ticket_cap_eur": 0.40},
    "whois_lookup": {"per_call_eur": 0.11, "per_ticket_cap_eur": 0.55},
    "shell":        {"per_call_eur": None, "per_ticket_cap_eur": 1.00},
    "send_email":   {"per_call_eur": 0.00, "per_ticket_cap_eur": 0.00, "max_calls": 3},
}

class BudgetExceeded(Exception):
    pass

def invoke_tool(tool_name, args, ticket_state):
    budget = TOOL_BUDGETS[tool_name]
    spent = ticket_state["tool_spend"].get(tool_name, 0.0)
    estimated = budget.get("per_call_eur") or estimate_cost(tool_name, args)
    if spent + estimated > budget["per_ticket_cap_eur"]:
        raise BudgetExceeded(
            f"{tool_name} would exceed cap "
            f"({spent + estimated:.2f} > {budget['per_ticket_cap_eur']:.2f})"
        )
    result = TOOLS[tool_name](**args)
    cost = actual_cost(tool_name, result)
    ticket_state["tool_spend"][tool_name] = spent + cost
    return result

De per_call_eur van de shell-tool is None omdat je hem niet vooraf kunt prijzen. Schat na het parsen van de args (een curl naar een bekend endpoint is goedkoop, een nmap van een /16 niet), of weiger te schatten en val terug op een harde call-count cap.

Per-ticket budget

Eén ticket heeft een waarde. De Eindhovense klant betaalt €189 per maand. De agent gaf op één ticket genoeg uit om tweeëneenhalf jaar abonnement terug te storten. Een per-ticket plafond van €2,00 had de loop gestopt voor de derde WHOIS call. Het getal hoeft niet geraffineerd te zijn. Het moet bestaan.

Loop signature detection

De agent riep traceroute vierhonderdzeven keer aan. Vierhonderdzeven. De tweede van die calls had bij de orchestrator een wenkbrauw moeten doen optrekken. De vijfde had executie moeten stoppen en escaleren naar een mens.

from collections import Counter

def detect_loop(call_history, window=10, threshold=4):
    recent = call_history[-window:]
    sigs = Counter((c["tool"], c["args_signature"]) for c in recent)
    top = sigs.most_common(1)
    if top and top[0][1] >= threshold:
        return top[0][0]
    return None

De args_signature is een hash van genormaliseerde arguments (strip variërende timestamps, vouw aangrenzende IP's samen tot hun /24). Vier identiek gevormde calls in de laatste tien is je trip wire. Halt de loop, log het pad, page een mens. Goedkoop om te schrijven, goedkoop om te draaien, had het incident om 06:20 beëindigd.

Wat we draaien in productie

De orchestrator die we deployen voor client-agents zit tussen het model en de tools. Hij doet drie dingen die het team in Eindhoven niet deed.

Ten eerste, elke tool call gaat door één invoke_tool-pad dat de kosten in centen registreert en calls boven een harde ceiling weigert. Niet zacht, niet warn-at-80%, niet Slack-op-vrijdag. De call retourneert een BudgetExceeded error string terug naar het model, dat zich vervolgens naar een ander plan moet redeneren of moet overdragen aan een mens.

Ten tweede, de agent draait ook tegen een context budget. Elk vijfde tool-resultaat wordt samengevat tot een 200-token notitie en het ruwe resultaat wordt uit de context gegooid. Dit alleen al had ongeveer €700 van de zondagse inference-rekening gedood.

Ten derde, de halt condition is niet "task complete". Het is een stemming over drie signalen: de claim van het model zelf, een goedkope classifier die de laatste drie tool-resultaten leest, en een harde muur op de totale spend. Als twee van de drie stop zeggen, stopt de loop. Het model krijgt niet de kans om zich langs de muur te praten.

Kernpunt

De taak van je orchestrator is niet om de agent te helpen het ticket af te ronden. Zijn taak is om de volgende call van de agent te weigeren als die call meer kost dan het ticket waard is.

De zaak voor saaie middleware

Er loopt een thread in het huidige discours over de vraag of proactievere modellen de noodzaak van guardrails op orchestrator-niveau verminderen. De HN-frontpage heeft deze week een apart stuk dat stelt dat de nieuwste tier van één groot model "onverzoenlijk proactief" is, en nog één over zijn onzichtbare guardrails. De conclusie waar het Eindhovense incident op landt is de inverse van dat argument. Proactievere modellen maken orchestrator budgets belangrijker, niet minder. Een model dat harder zal proberen, probeert harder langer en met meer tools.

De middleware die dit vangt is niet glamoureus. Het is een functie die een call weigert. Het is een counter die op elke tool-invocatie ophoogt. Het is een regex over de laatste tien tool-signatures. Niks ervan vereist een nieuw model. Alles ervan vereist dat je, vóór deployment, besluit dat elke tool een prijs heeft en elk ticket een plafond.

De overdracht

Toen we eerder dit jaar de customer-success agent bouwden voor een Nederlandse logistiek-klant, was het ding dat ons in week twee bijna onderuit haalde bijna identiek: een shell-tool, een open ticket en een model dat door wilde gaan. We hebben het opgelost door de budget check uit de prompt van het model te halen en in de call layer te zetten, waar het model er niet meer over kan onderhandelen. Dat patroon is wat we standaard meeleveren in onze AI-agent-trajecten.

Het kleinste dat je vandaag kunt doen: open de tool registry van je agent, voeg een kolom toe voor "kosten per call in centen" en nog één voor "calls per ticket cap". Vul de getallen in vanuit de logs van vorige week. Als een tool geen getal heeft, heb je je eerste incident al half geschreven.

Kern

Elke tool heeft een prijs nodig, elk ticket een plafond, en de budget check hoort in de call layer, waar het model er niet over kan onderhandelen.

FAQ

Wat is DN42?

Een gedecentraliseerd netwerkexperiment dat overlay VPN-tunnels en BGP draait tussen autonome systemen van hobbyisten. Het is geen productienetwerk en is nooit ontworpen om klantverkeer te dragen.

Hoe klein moet een per-ticket agent budget zijn?

Koppel het aan de economische waarde van het ticket. Voor een abonnement van €189 per maand is een plafond van €1 tot €2 redelijk. Het precieze getal is minder belangrijk dan dat er überhaupt een bestaat.

Lossen nieuwere model-tiers dit op zonder orchestrator guardrails?

Nee. Een proactiever model probeert harder langer met meer tools. Spend caps horen buiten de prompt, in de call layer, waar het model er niet over kan onderhandelen.

Handhaven de grote SDK's spend caps native?

Per medio 2026 niet. Vendor SDK's retourneren token counts maar laten budget handhaving over aan jouw orchestrator code. Je schrijft de call-layer middleware zelf.

ai agentsoperationsarchitecturecase studytoolingautomation

Iets bouwen?

Start een project