> ## Documentation Index
> Fetch the complete documentation index at: https://docs.nika.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Spend caps

> Bound what a run may spend on paid models: refused before it starts, stopped when it crosses, never blocked for local or mock models.

The engine prices a workflow before it runs, from each model's catalog price
and the output limit each step declares (`max_tokens`). A spend cap turns that
price into a rule: a run whose cost floor is above the cap never starts.

```ts spend.mts theme={"system"}
import { Nika, NikaOperationError, isNikaRunSucceeded } from '@supernovae-st/nika'

const nika = new Nika({ cwd: process.cwd() })

// The engine prices the file before anything runs.
const report = await nika.check('summary.nika')
const cost = report.cost as { bounded_total_usd?: number; has_unbounded?: boolean }
console.log('worst case:', cost.bounded_total_usd, 'unbounded:', cost.has_unbounded)

try {
  await nika.run('summary.nika', { maxCostUsd: 0 })
} catch (error) {
  if (!(error instanceof NikaOperationError)) throw error
  // Both refusals come before any provider call: nothing was spent.
  // NIKA-1709 (status 2): a priced cost floor above the cap.
  // run_refused (status 3), "price unknown": a cloud model the catalog cannot price.
  const unpriced = error.code === 'run_refused' && error.message.startsWith('price unknown')
  if (error.code !== 'NIKA-1709' && !unpriced) throw error
  console.log('refused:', error.status, error.message.split(' — ')[0])
}

// The same file, rehearsed on the mock model: allowed under a zero cap.
const run = await nika.run('summary.nika', { model: 'mock/echo', maxCostUsd: 0 })
const result = await run.result()
console.log(isNikaRunSucceeded(result), result.settlement?.spend)
```

```yaml summary.nika theme={"system"}
nika: summary
model: anthropic/claude-sonnet-5-5
permits: {}
tasks:
  summarize:
    infer:
      prompt: "Summarize the week in three bullet points."
      max_tokens: 1000
outputs:
  summary: ${{ tasks.summarize.output }}
```

```text output theme={"system"}
worst case: 0.01 unbounded: false
refused: 2 NIKA-1709 · refusing to start: the workflow's unavoidable cost floor $0.010000 exceeds --max-cost-usd $0.000000 (cheapest static path · gates closed · first-try)
true { priced_calls: 0, qualifier: 'unpriced', unpriced_calls: 1 }
```

This runs offline, with no Anthropic key: the refusal comes before any access
check or provider call.

## The rules, on the local engine

`maxCostUsd` is the run's budget over **metered** spend, in USD, at the
catalog's list prices.

* **Before the run.** A cost floor above the cap makes `run()` throw
  `NikaOperationError` with code `NIKA-1709` (`status` 2).
* **A cloud model the catalog cannot price** is refused before any call
  too, with or without `maxCostUsd`: the engine cannot bound spend it cannot
  meter, and an unpriced model never counts as `$0`. The engine names no
  code for this refusal, so `code` is the SDK's `run_refused`, `status` is
  3 and the message starts `price unknown:`. The engine your app starts has
  no terminal to ask; `nika run` in an interactive terminal asks once for a
  cost review instead.
* **During the run.** The call that crosses the cap completes and counts,
  nothing new starts, unstarted tasks are cancelled, and `run.result()`
  resolves `failed` with `NIKA-1704` and no outputs.
* **Local and mock models are never blocked.** They are unpriced, not free:
  the settlement reports them as `unpriced`.
* **Without `maxCostUsd`**, the `ceiling:` in the project's
  [`nika.yaml`](https://docs.nika.sh/sdk/project/nika-yaml) applies; `maxCostUsd` wins over it.
* **The floor is an output-token estimate** (`max_tokens` × output price).
  Prompt tokens are not priced statically, and a wide wave of parallel tasks
  can overshoot by up to that wave's spend before the crossing is seen:
  `max_parallel:` narrows that window.

## `model` replaces only the default

The `model` option replaces the workflow's envelope `model:` only. A task that
pins its own `infer.model`, and a child workflow it invokes, keep their models:
`model: 'mock/echo'` does not make those calls offline. Keep `maxCostUsd: 0`
next to it, so a priced pin is refused (`NIKA-1709`) instead of spending. A
local-model pin still runs.

## On `nika serve`

There is no per-run `maxCostUsd` over HTTP: the SDK refuses it with
`NikaCompatibilityError` (`capability: 'runOptions'`) instead of dropping it.
The server bounds spend itself:

* `nika serve --run-cost-ceiling <USD>` caps every job your code submits:
  **1 USD by default**; `none` removes the cap explicitly.
* A job whose floor exceeds it is admitted and then settles `failed` with
  `error.code` `NIKA-1709`; `run()` does not throw.
* A schedule carries its own `maxCostUsd`, which must be positive
  ([Schedules](https://docs.nika.sh/sdk/remote/schedules.md)).

Catalog prices are estimates, not invoices: your provider meters and bills
the calls. [Cost honesty](https://docs.nika.sh/guides/cost-honesty.md) explains how `nika check`
reads them.

> **Which version is this?**
>
> > [!NOTE]
> >
> > These pages describe the published `@supernovae-st/nika@0.123.0`
> > [SDK release](https://github.com/supernovae-st/nika-client/tree/v0.123.0) and its matching
> > [bundled engine](https://github.com/supernovae-st/nika/releases/tag/v0.123.0). A newer engine
> > release or SDK main commit does not change an installed package. Use Node.js
> > 22 or newer, on macOS (arm64, x64) or Linux with glibc (x64, arm64); no
> > global Nika installation is required for local mode.

> **[Results](https://docs.nika.sh/sdk/runtime/results.md)**
>
> How a budget stop reads in the settlement.

> **[CI gate](https://docs.nika.sh/sdk/operations/ci.md)**
>
> Rehearse on the mock model before a protected job spends.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.