# ASD Says Treat AI Tokens Like a Budget. Here's How

URL: https://nanosolutions.io/ai-token-budget
Date: 1792886400
Description: ASD's new guidance treats AI tokens as a cost, a capacity limit and an attack surface. What Perth businesses should budget, cap and alert on.

---


<div class="post-tldr">
  <p class="post-tldr__label">The short version</p>
  <ul>
    <li>On [date:2026-10-23|https://www.cyber.gov.au/publication/managing-ai-token-consumption-for-operational-resilience] ASD published guidance on managing AI token consumption, written with its Canadian, US, UK and New Zealand counterparts. It treats tokens as a cost, a capacity limit and a security risk.</li>
    <li>Two failure modes it names: <em>token exhaustion</em>, where you hit your own quota and lose the service, and <em>denial-of-wallet</em>, where someone else spends your money on purpose.</li>
    <li>Its concrete asks are small: set alerts as you approach your limit (its example is 70%, 80% and 90%), split usage into three priority tiers, cap outputs, bound agent loops, and put a proxy or gateway in front so you can see usage per team.</li>
    <li>If you run a customer-facing chatbot or an agent with a public endpoint, this is a this-week job. If AI is internal-only, it is a this-month job.</li>
  </ul>
</div>

If your business pays for AI by the token, the bill moves around. Most businesses lack a number they can defend to finance, and a stop button for the day someone points a script at their chatbot.

On 23 October 2026 the Australian Signals Directorate published [Managing AI token consumption for operational resilience](https://www.cyber.gov.au/publication/managing-ai-token-consumption-for-operational-resilience). The [Canadian Centre for Cyber Security](https://www.cyber.gc.ca/en/guidance/managing-ai-token-consumption-operational-resilience-itsm00125) led it, with ASD's ACSC, the US CISA and the UK and New Zealand cyber centres as co-authors. There is no vulnerability here, just an ordinary problem: organisations depend on AI they pay for by use, and most cannot say what a bad month looks like until it arrives.

## What ASD says

Tokens are the units a model reads and writes. You pay for input (what you send) and output (what comes back). The guidance asks you to understand the pricing *"prior to engaging with vendors and signing service level agreements or contracts."* Ask about quota limits and overage pricing before you sign.

Usage grows quietly: chat history resent with every request, connectors to other platforms, and agents where *"each step reprocess[es] prior content and tool outputs."* One chat message is cheap. A five-step agent that re-reads a 40-page document at every step is not.

## Two ways the bill hurts you

**Token exhaustion.** You hit a quota, rate limit or budget threshold, and the service slows or stops. *"Users and systems may experience delays, degraded performance, failed requests, or loss of access to AI capabilities."* ASD's fix is alerts that escalate as you approach the limit. Its example is 70% (information), 80% (warning) and 90% (critical).

**Denial-of-wallet.** Someone uses your endpoint so that you pay. ASD describes attackers exploiting *"consumption-based, usage-based, and pay-per-use pricing models to increase costs,"* with prompts that force long answers or loops that feed output back in. Its cousin, denial-of-tokens, burns your quota so your own users lose access.

Neither needs a bug in your code. Your endpoint only has to accept work from people you did not mean to serve.

<figure>
  <img src="/images/blog/ai-token-budget-meters.webp" alt="A row of electronic electricity meters with digital displays" width="1400" height="974" loading="lazy" />
  <figcaption>Metered services are easy to budget for until the meter is on someone else's wall. Photo by <a href="https://commons.wikimedia.org/w/index.php?curid=19892367" rel="nofollow noopener">Mamlik on Wikimedia Commons</a>.</figcaption>
</figure>

## Decide now what gets throttled first

ASD recommends three priority tiers, so routine work cannot eat the capacity that critical work needs:

- **Tier 1, mission critical:** cyber defence and incident response. Protected capacity.
- **Tier 2, operational:** essential processes such as compliance work. Monitored and managed.
- **Tier 3, productivity:** emails, meeting summaries, research. Limited first when demand is high.

In a 30-person business, Tier 1 might be the after-hours support bot and Tier 3 the staff ChatGPT seats. Decide the order now, not at 4:55 pm on a Friday. ASD also asks for a governance group and fallback plans with *"human oversight at key decision points."* For a small business that is one page: four names, a priority order and a fallback.

## What would a bad week cost you?

Pick what you run, adjust the sliders, then simulate a bad week. Check the result against your own invoice.

<div x-data="tokenBudget()" class="not-prose my-10 rounded-sm border border-gray-200 bg-[#f7f7f7] p-4 sm:p-8">
  <p class="text-xs font-semibold uppercase tracking-wider text-brand-accent">Try it</p>
  <h3 class="mt-1 font-heading text-xl font-bold leading-snug text-[#303133] sm:text-2xl">AI token cost and bad-week estimator</h3>
  <fieldset class="mt-5">
    <legend class="text-sm font-semibold text-[#303133]">What are you running?</legend>
    <div class="mt-2 grid grid-cols-3 gap-2">
      <template x-for="p in presets" :key="p.id">
        <button type="button" x-on:click="applyPreset(p.id)" :aria-pressed="(preset === p.id).toString()" class="rounded-sm border bg-white px-2 py-3 text-left transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-accent sm:px-4" :class="preset === p.id ? 'border-brand-accent shadow-[inset_0_0_0_1px_#FF0099]' : 'border-gray-300 hover:border-gray-400'">
          <span class="block font-heading text-sm font-semibold leading-tight text-[#303133]" x-text="p.label"></span>
          <span class="mt-1 block text-xs leading-snug text-gray-500" x-text="p.hint"></span>
        </button>
      </template>
    </div>
  </fieldset>
  <div class="mt-6 grid gap-5 rounded-sm bg-[#141618] p-5 text-white transition-shadow sm:grid-cols-[minmax(0,1fr)_15rem] sm:items-center sm:p-6" :class="bad ? verdictRing : ''">
    <div class="min-w-0">
      <p class="text-xs font-semibold uppercase tracking-wider text-white/60" x-text="bad ? 'This month, with one bad week' : 'A normal month'"></p>
      <p class="mt-1 origin-left break-words font-heading text-4xl font-extrabold tabular-nums transition duration-200 motion-reduce:transition-none sm:text-5xl" :class="[bad ? verdictText : 'text-white', bump ? 'motion-safe:scale-110' : '']" aria-hidden="true" x-text="fmt(shown)"></p>
      <p class="sr-only" aria-live="polite" x-text="announce"></p>
      <p class="mt-2 text-sm text-white/70" x-show="!bad"><span x-text="fmt(perReq)"></span> a request · <span x-text="fmt(total * 12)"></span> a year</p>
      <p class="mt-2 text-sm text-white/70" x-show="bad" x-cloak><span class="font-semibold text-white" x-text="timesNormal"></span> a normal month. The bad week alone: <span class="font-semibold text-white" x-text="fmt(badWeek)"></span>.</p>
    </div>
    <label for="tb-bad" class="flex cursor-pointer items-center gap-3 rounded-sm border border-white/15 bg-white/5 p-3 transition-colors hover:bg-white/10">
      <input id="tb-bad" type="checkbox" x-model="bad" class="peer sr-only">
      <span aria-hidden="true" class="relative h-7 w-12 shrink-0 rounded-full transition-colors peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-brand-accent" :class="bad ? 'bg-brand-accent' : 'bg-white/25'"><span class="absolute left-1 top-1 h-5 w-5 rounded-full bg-white shadow transition-transform motion-reduce:transition-none" :class="bad ? 'translate-x-5' : ''"></span></span>
      <span class="min-w-0"><span class="block text-sm font-semibold">Simulate a bad week</span><span class="block text-xs leading-snug text-white/60" x-text="badCaption"></span></span>
    </label>
    <div x-show="bad" x-cloak x-transition.opacity.duration.200ms class="rounded-sm border p-3 text-sm leading-snug sm:col-span-2" :class="verdictBox">
      <strong x-text="bandLabel"></strong> <span x-text="bandNote"></span>
    </div>
  </div>
  <div class="mt-6 grid gap-x-8 gap-y-5 sm:grid-cols-2">
    <fieldset class="sm:col-span-2">
      <legend class="text-sm font-semibold text-[#303133]">Model</legend>
      <div class="mt-2 grid grid-cols-4 gap-1 rounded-sm border border-gray-300 bg-white p-1">
        <template x-for="m in models" :key="m.id">
          <label class="cursor-pointer">
            <input type="radio" name="tb-model" class="peer sr-only" :value="m.id" x-model="model">
            <span class="block rounded-sm px-1 py-2 text-center text-xs font-semibold transition-colors peer-focus-visible:outline-2 peer-focus-visible:outline-brand-accent sm:text-sm" :class="model === m.id ? 'bg-brand-accent text-white' : 'text-[#303133] hover:bg-gray-100'" x-text="m.label"></span>
          </label>
        </template>
      </div>
      <p class="mt-1 text-xs text-gray-500" x-text="rateNote"></p>
    </fieldset>
    <div>
      <div class="flex items-baseline justify-between gap-3">
        <label for="tb-req" class="text-sm font-semibold text-[#303133]">Requests a day</label>
        <output for="tb-req" class="font-heading text-sm font-bold tabular-nums text-brand-accent" x-text="num(requests)"></output>
      </div>
      <input id="tb-req" type="range" min="0" :max="scales.req.length - 1" step="1" x-model.number="reqIdx" :aria-valuetext="num(requests) + ' requests a day'" class="mt-1 h-6 w-full cursor-pointer accent-brand-accent">
    </div>
    <div>
      <div class="flex items-baseline justify-between gap-3">
        <label for="tb-steps" class="text-sm font-semibold text-[#303133]">Model calls per request</label>
        <output for="tb-steps" class="font-heading text-sm font-bold tabular-nums text-brand-accent" x-text="steps"></output>
      </div>
      <input id="tb-steps" type="range" min="1" max="10" step="1" x-model.number="steps" :aria-valuetext="steps + ' model calls per request'" aria-describedby="tb-steps-help" class="mt-1 h-6 w-full cursor-pointer accent-brand-accent">
      <p id="tb-steps-help" class="text-xs text-gray-500">1 for a simple chatbot. Agents often make 5 to 10.</p>
    </div>
    <div>
      <div class="flex items-baseline justify-between gap-3">
        <label for="tb-in" class="text-sm font-semibold text-[#303133]">Tokens read per call</label>
        <output for="tb-in" class="font-heading text-sm font-bold tabular-nums text-brand-accent" x-text="num(inTokens)"></output>
      </div>
      <input id="tb-in" type="range" min="0" :max="scales.inTok.length - 1" step="1" x-model.number="inIdx" :aria-valuetext="num(inTokens) + ' input tokens'" aria-describedby="tb-in-help" class="mt-1 h-6 w-full cursor-pointer accent-brand-accent">
      <p id="tb-in-help" class="text-xs text-gray-500">Instructions, documents and chat history. 1,000 tokens is about 750 words.</p>
    </div>
    <div>
      <div class="flex items-baseline justify-between gap-3">
        <label for="tb-out" class="text-sm font-semibold text-[#303133]">Tokens written per call</label>
        <output for="tb-out" class="font-heading text-sm font-bold tabular-nums text-brand-accent" x-text="num(outTokens)"></output>
      </div>
      <input id="tb-out" type="range" min="0" :max="scales.outTok.length - 1" step="1" x-model.number="outIdx" :aria-valuetext="num(outTokens) + ' output tokens'" aria-describedby="tb-out-help" class="mt-1 h-6 w-full cursor-pointer accent-brand-accent">
      <p id="tb-out-help" class="text-xs text-gray-500">The answer. Output costs about five times as much as input.</p>
    </div>
    <div class="grid gap-x-6 gap-y-2 border-t border-gray-200 pt-4 sm:col-span-2 sm:grid-cols-3">
      <template x-for="t in switches" :key="t.key">
        <label :for="'tb-' + t.key" class="flex cursor-pointer items-center justify-between gap-3 py-1 sm:flex-row-reverse sm:justify-end">
          <span class="text-sm leading-snug text-[#303133]" x-text="t.label"></span>
          <input :id="'tb-' + t.key" type="checkbox" class="peer sr-only" x-model="$data[t.key]">
          <span aria-hidden="true" class="relative h-6 w-11 shrink-0 rounded-full transition-colors peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-brand-accent" :class="$data[t.key] ? 'bg-brand-accent' : 'bg-gray-300'"><span class="absolute left-1 top-1 h-4 w-4 rounded-full bg-white shadow transition-transform motion-reduce:transition-none" :class="$data[t.key] ? 'translate-x-5' : ''"></span></span>
        </label>
      </template>
    </div>
  </div>
  <details class="mt-6 text-xs leading-relaxed text-gray-600">
    <summary class="cursor-pointer font-semibold text-[#303133]">Assumptions behind the numbers</summary>
    <p class="mt-2">All figures in US dollars, because that is how the providers bill. Rates are examples for planning, not quotes, based on October 2026 list prices from Anthropic, OpenAI and Google: frontier US$10 per million input tokens and US$50 per million output, mid-tier US$2 and US$10, small US$0.10 and US$0.50, local US$0. Prompt caching assumes about half the input is cached (most of it for the agent preset), costed at 10% of the input rate; cache-write charges are ignored. A month is 30 days. A bad week is three days at a traffic multiplier plus four normal days. The multipliers are our assumptions, not ASD figures: 3x for staff-only use, 20x for a public endpoint with a hard cap, 100x for a public endpoint without one. Nothing you enter leaves this page.</p>
  </details>
</div>

<figure>
  <img src="/images/blog/ai-token-budget-analytics.webp" alt="A person typing on a laptop that shows analytics charts, with a cup of coffee beside it" width="1400" height="934" loading="lazy" />
  <figcaption>A token baseline turns an unpredictable bill into a line item you can forecast. Photo by <a href="https://stocksnap.io/photo/analytics-charts-RCFX768X06" rel="nofollow noopener">Negative Space on StockSnap</a>.</figcaption>
</figure>

## What to do this week

1. **Work out your cost per request.** Divide the last three invoices by request volume.
2. **Set the three alerts.** 70%, 80% and 90%, on both quota and budget. If your provider cannot do this, raise it at renewal.
3. **Sort every AI use into the three tiers.** Write down what gets throttled first.
4. **Lock down what the public can reach.** Sign-in, per-user rate limits, a maximum output length and a cap on agent loops. Do this first if you run a customer-facing chatbot.
5. **Write the fallback.** If the model is down, what does the team do instead? If the answer is "nothing", that is the finding.
6. **Plan a proxy or gateway** for next quarter. ASD calls AI proxies *"an increasingly practical way for organizations to monitor and measure consumption and use, enforce quotas, apply guardrails and support chargeback."*

## For the technical team

- **Right-size the model.** ASD: *"Using the same high-cost model across all workflows is not necessary."* A local model swaps token bills for hardware and staff time, as [our 2015 GPU test](/local-llm-testing-old-gpu) showed.
- **Cap output and effort.** Reasoning tokens are billed as output, the expensive half.
- **Cache deliberately.** Put static instructions and documents at the start of the prompt so the cache can hit.
- **Bound the loops.** Maximum iterations, tool calls and a timeout. An unbounded agent loop is a denial-of-wallet attack you wrote yourself.
- **Treat spikes as a security signal.** ASD suggests watching unusual consumption *"as a potential indicator of data exfiltration,"* and sending token telemetry to your SIEM. Use one API key per application so an abused integration cannot hide in the total.
- **Keep a second provider** for outages and quota exhaustion.

## Frequently asked questions

### Is this guidance binding on Australian businesses?

No. It is guidance, not regulation, but it is what a government client, auditor or insurer will point at when they ask how you manage AI use.

### We only use AI through staff subscriptions. Does this apply?

Partly. The seat price caps your spend, but shared seats still hit usage limits. The tiers decide who keeps access when they do.

### What is the single cheapest control?

A maximum output length per call. It is one setting, and it limits what each abusive request can cost you.

---

Our [cyber security team](/services/cloud-security) can review where your AI usage goes, and our [software development team](/services/software-development) can add quotas, logging and fallbacks to an existing integration. [Get in touch](/contact) and we will start with your last three invoices.

**Related reading:** [AI agents in business: what agentic AI actually means](/ai-agents-in-business-what-agentic-ai-actually-means) · [AI agents built a message board and broke out](/openai-agents-hugging-face-incident) · [Fintech cloud costs: cutting the bill without breaking CPS 230](/fintech-cloud-cost-review-cps-230)

**Photo credits.** All images CC0 / public domain, free for commercial use:

- Network rack and switch — [Mohammed Kateregga on WordPress Photo Directory](https://wordpress.org/photos/photo/90569ce09f/)
- Electronic electricity meters — [Mamlik on Wikimedia Commons](https://commons.wikimedia.org/w/index.php?curid=19892367)
- Analytics charts on a laptop — [Negative Space on StockSnap](https://stocksnap.io/photo/analytics-charts-RCFX768X06)

## Sources

- [Managing AI token consumption for operational resilience](https://www.cyber.gov.au/publication/managing-ai-token-consumption-for-operational-resilience) — Australian Signals Directorate / ACSC, published 23 October 2026
- [Managing AI token consumption for operational resilience (ITSM.00.125)](https://www.cyber.gc.ca/en/guidance/managing-ai-token-consumption-operational-resilience-itsm00125) — Canadian Centre for Cyber Security, lead author, with ASD's ACSC, CISA, NCSC-UK and NCSC-NZ
- Model list prices, October 2026: [Anthropic](https://platform.claude.com/docs/en/about-claude/pricing), [OpenAI](https://developers.openai.com/api/docs/pricing), [Google Gemini](https://ai.google.dev/gemini-api/docs/pricing)
- [Artificial intelligence guidance index](https://www.cyber.gov.au/artificial-intelligence) — ACSC
- [Careful adoption of agentic AI services](https://www.cyber.gov.au/publication/careful-adoption-of-agentic-ai-services) — joint guidance, ACSC

<script>
function tokenBudget() {
  // Slider stops. Non-linear, so one slider covers a hobby bot and a busy one.
  const scales = {
    req: [10, 25, 50, 100, 200, 400, 800, 1500, 3000, 5000, 10000],
    inTok: [500, 1000, 1500, 2500, 4000, 6000, 8000, 12000, 20000, 40000],
    outTok: [100, 200, 300, 600, 1000, 1500, 2500, 4000]
  };
  const presetValues = {
    staff: { model: 'mid', requests: 200, inTokens: 2500, outTokens: 600, steps: 1, cachePct: 50, publicEndpoint: false, hardCap: true },
    customer: { model: 'mid', requests: 400, inTokens: 2500, outTokens: 600, steps: 2, cachePct: 50, publicEndpoint: true, hardCap: false },
    agent: { model: 'frontier', requests: 100, inTokens: 8000, outTokens: 1000, steps: 6, cachePct: 80, publicEndpoint: false, hardCap: false }
  };
  return {
    scales,
    presets: [
      { id: 'staff', label: 'Staff chatbot', hint: 'Internal helper' },
      { id: 'customer', label: 'Customer chatbot', hint: 'On your website' },
      { id: 'agent', label: 'AI agent', hint: 'Multi-step work' }
    ],
    models: [
      { id: 'frontier', label: 'Frontier' },
      { id: 'mid', label: 'Mid-tier' },
      { id: 'small', label: 'Small' },
      { id: 'local', label: 'Local' }
    ],
    switches: [
      { key: 'publicEndpoint', label: 'Open to the public' },
      { key: 'hardCap', label: 'Hard monthly spend cap' },
      { key: 'cacheOn', label: 'Prompt caching on' }
    ],
    rates: {
      frontier: { in: 10, out: 50 },
      mid: { in: 2, out: 10 },
      small: { in: 0.1, out: 0.5 },
      local: { in: 0, out: 0 }
    },
    preset: 'customer',
    model: 'mid',
    reqIdx: 5,
    inIdx: 3,
    outIdx: 3,
    steps: 2,
    presetCache: 50,
    cacheOn: true,
    publicEndpoint: true,
    hardCap: false,
    bad: false,
    shown: 0,
    bump: false,
    raf: null,
    init() {
      this.applyPreset('customer');
      this.shown = this.headline;
      this.$watch('headline', (v) => this.animateTo(v));
      this.$watch('bad', (on) => {
        if (!on) return;
        this.bump = true;
        setTimeout(() => { this.bump = false; }, 220);
      });
    },
    applyPreset(id) {
      const p = presetValues[id];
      if (!p) return;
      this.preset = id;
      this.model = p.model;
      this.reqIdx = scales.req.indexOf(p.requests);
      this.inIdx = scales.inTok.indexOf(p.inTokens);
      this.outIdx = scales.outTok.indexOf(p.outTokens);
      this.steps = p.steps;
      this.presetCache = p.cachePct;
      this.cacheOn = p.cachePct > 0;
      this.publicEndpoint = p.publicEndpoint;
      this.hardCap = p.hardCap;
    },
    animateTo(target) {
      cancelAnimationFrame(this.raf);
      const reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
      if (reduce) { this.shown = target; return; }
      const from = this.shown;
      const start = performance.now();
      const duration = 650;
      const tick = (now) => {
        const t = Math.min(1, (now - start) / duration);
        const eased = 1 - Math.pow(1 - t, 3);
        this.shown = from + (target - from) * eased;
        if (t < 1) this.raf = requestAnimationFrame(tick);
      };
      this.raf = requestAnimationFrame(tick);
    },
    fmt(n) {
      if (!isFinite(n)) return 'US$0';
      // Sub-dollar amounts (cost per request) need more decimals, or a
      // small model shows as US$0.
      const opts = n >= 100 ? { maximumFractionDigits: 0 }
        : n >= 1 ? { minimumFractionDigits: 2, maximumFractionDigits: 2 }
        : n > 0 ? { minimumFractionDigits: 2, maximumFractionDigits: 4 }
        : { maximumFractionDigits: 0 };
      return 'US$' + n.toLocaleString('en-AU', opts);
    },
    num(n) { return Number(n).toLocaleString('en-AU'); },
    get requests() { return scales.req[this.reqIdx] ?? 0; },
    get inTokens() { return scales.inTok[this.inIdx] ?? 0; },
    get outTokens() { return scales.outTok[this.outIdx] ?? 0; },
    get cachePct() { return this.cacheOn ? (this.presetCache || 50) : 0; },
    get rate() { return this.rates[this.model] || this.rates.mid; },
    get rateNote() {
      if (this.model === 'local') return 'No token bill. You pay for hardware, power and staff time instead.';
      return 'US$' + this.rate.in + ' per million tokens in, US$' + this.rate.out + ' per million out.';
    },
    get stepCount() { return Math.max(1, Number(this.steps) || 1); },
    get cacheShare() { return Math.min(90, Math.max(0, Number(this.cachePct) || 0)) / 100; },
    get effectiveInputPerRequest() {
      const total = this.inTokens * this.stepCount;
      return total * (1 - this.cacheShare) + total * this.cacheShare * 0.1;
    },
    get outputPerRequest() { return this.outTokens * this.stepCount; },
    get monthlyRequests() { return this.requests * 30; },
    get total() {
      const inCost = this.monthlyRequests * this.effectiveInputPerRequest / 1000000 * this.rate.in;
      const outCost = this.monthlyRequests * this.outputPerRequest / 1000000 * this.rate.out;
      return inCost + outCost;
    },
    get perReq() { return this.monthlyRequests > 0 ? this.total / this.monthlyRequests : 0; },
    get multiplier() {
      if (this.model === 'local') return 1;
      if (this.publicEndpoint && !this.hardCap) return 100;
      if (this.publicEndpoint) return 20;
      return 3;
    },
    // Three days at the multiplier plus four normal days.
    get badWeek() { return this.total / 30 * (3 * this.multiplier + 4); },
    // The month with the bad week swapped in for a normal week.
    get badMonth() { return this.total - this.total / 30 * 7 + this.badWeek; },
    get headline() { return this.bad ? this.badMonth : this.total; },
    get timesNormal() {
      if (this.total <= 0) return '1x';
      const r = this.badMonth / this.total;
      return (r < 10 ? r.toFixed(1) : Math.round(r)) + 'x';
    },
    get announce() {
      return (this.bad ? 'This month with one bad week: ' : 'A normal month: ') + this.fmt(this.headline);
    },
    get badCaption() {
      if (this.model === 'local') return 'Local models have no token bill to spike.';
      if (this.publicEndpoint && !this.hardCap) return 'Someone finds your endpoint and scripts it: 100x traffic for three days.';
      if (this.publicEndpoint) return 'Someone finds your endpoint, but the cap holds it to about 20x for three days.';
      return 'A busy period or a looping agent: 3x traffic for three days.';
    },
    get band() {
      if (this.model === 'local') return 'local';
      if (this.publicEndpoint && !this.hardCap) return 'high';
      if (this.publicEndpoint || !this.hardCap) return 'medium';
      return 'low';
    },
    get verdictText() {
      return { high: 'text-red-400', medium: 'text-amber-300', low: 'text-emerald-400', local: 'text-emerald-400' }[this.band];
    },
    get verdictRing() {
      return { high: 'shadow-[0_0_0_3px_#ef4444]', medium: 'shadow-[0_0_0_3px_#f59e0b]', low: 'shadow-[0_0_0_3px_#10b981]', local: 'shadow-[0_0_0_3px_#10b981]' }[this.band];
    },
    get verdictBox() {
      return {
        high: 'border-red-500/50 bg-red-500/15 text-red-100',
        medium: 'border-amber-400/50 bg-amber-400/10 text-amber-100',
        low: 'border-emerald-400/50 bg-emerald-400/10 text-emerald-100',
        local: 'border-emerald-400/50 bg-emerald-400/10 text-emerald-100'
      }[this.band];
    },
    get bandLabel() {
      return {
        local: 'Local or self-hosted.',
        high: 'Exposed.',
        medium: 'Partly exposed.',
        low: 'Contained.'
      }[this.band];
    },
    get bandNote() {
      if (this.band === 'local') return 'Your cost is hardware, electricity and staff time. Budget those instead, and keep a hosted fallback for peak.';
      if (this.band === 'high') return 'A public endpoint with no hard cap is the exact shape denial-of-wallet attacks look for. Add sign-in, per-user rate limits and a monthly cap before anything else.';
      if (this.band === 'medium') return 'One of the two controls is missing. A hard monthly cap costs nothing to set and limits the damage.';
      return 'Staff-only, capped usage. Keep the alerts on and review the baseline monthly.';
    }
  };
}
</script>
