# Cost guard (Claude Agent SDK)

Before a run the agent estimates its cost from recorded runs, or says it can't; the person approves a budget; spend is recorded from the usage the SDK reports, and the run stops on the server when it reaches the budget and says why.

- Registry name: `cost-guard-claude` (`@candor/cost-guard-claude`)
- Tier: Premium. Installing it needs a Premium or Team licence key: the person runs `npx candor-ui key <key>` themselves; never ask for the key in chat or write it to a file.
- Docs: https://candor.themethodic.com/docs/cost-guard-claude

## Install

```bash
npx candor-ui add cost-guard-claude
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/cost-guard-claude`.
It brings in: button, card-registry, chat, claude-agent-sdk, claude-agent-server, composer, entity-chip, guardrail-notice, message, ops, prompt-suggestions, run-estimate, spend-breakdown, thread, usage-meters, use-claude-agent.
npm packages: @anthropic-ai/claude-agent-sdk@^0.3.318, drizzle-orm@^0.45.4, lucide-react@^1.67.0, postgres@^4.0.0, zod@^4.6.7.

It installs under your app's `app/` folder and runs with a server and an agent: set `ANTHROPIC_API_KEY`, and read the comment at the top of its `store.ts` for storage and auth. Its files:

- `app/cost-guard/api/approve/route.ts`
- `app/cost-guard/api/route.ts`
- `app/cost-guard/api/spend/route.ts`
- `app/cost-guard/db/connect.ts`
- `app/cost-guard/db/drizzle-repo.ts`
- `app/cost-guard/db/schema.ts`
- `app/cost-guard/db/seed.ts`
- `app/cost-guard/page.tsx`
- `app/cost-guard/repo.ts`
- `app/cost-guard/store.ts`
- `app/cost-guard/ui.tsx`

## <Page>

No props.

## <CostGuard>

A chat with an agent that audits feature flags on a budget: it estimates first (or says it can't), the person approves a budget, and the run stops on the server when recorded spend reaches it. Spend is shown beside it.

Props:

- `chat: CostChat`: The chat state from useCandorChat (AI SDK) or useClaudeAgent (Claude Agent SDK); the block works with either.
- `api: string`: The spend API route the page reads usage and the budget from, e.g. "/cost-guard/api/spend".

## Functions

### connectCostDb

```ts
connectCostDb(url: string)
```

Open the block's database from a Postgres URL with the postgres-js driver. Swap the driver here (node-postgres, Neon's websocket Pool, …) if your app already uses another; it must support interactive transactions.

### drizzleCostRepo

```ts
drizzleCostRepo(db: CostDb)
```

### seedCostData

```ts
seedCostData(db: CostDb)
```

Load the demo's finished audits and five weeks of spend, leaving existing data alone: history rows that exist are kept, and the spend is loaded only into an empty ledger. Run it once after migrating, e.g. from a script: `await seedCostData(connectCostDb(process.env.DATABASE_URL!))`.

### demoLedger

```ts
demoLedger()
```

Five weeks of recorded spend, oldest first, for the breakdown. Your app records these from real runs.

### memoryCostRepo

```ts
memoryCostRepo()
```

In this server process's memory: gone on restart and not shared between instances. Transactions run one at a time, and a transaction that throws is rolled back to the state it started from.

### costOf

```ts
costOf(modelId: string, t: Tokens)
```

What one call cost at list price, or null when the model or the token counts are unknown.

### setCostRepo

```ts
setCostRepo(repo: CostRepo)
```

Use your own storage (or a database you opened yourself). Call it once, before the routes run.

### costRepo

```ts
costRepo()
```

The storage in use: whatever setCostRepo() was given, else Postgres when DATABASE_URL is set, else memory.

### viewerOf

```ts
viewerOf(request: Request)
```

Who is asking. Replace with your auth; the demo reads a cookie.

### listFlags

```ts
listFlags()
```

### estimateRun

```ts
estimateRun(flags: string[])
```

A cost and time range from finished flag audits, per flag; "unknown" with fewer than three of them.

### startRun

```ts
startRun(input: { flags: string[]; budgetUsd: number }, viewer: string)
```

Start a run on the budget the person approved.

### auditFlag

```ts
auditFlag(name: string)
```

One unit of the run's work. Refused when no run is going or it has stopped.

### finishRun

```ts
finishRun(summary: string)
```

Finish the run; it joins the recorded runs that later estimates come from.

### recordSpend

```ts
recordSpend(input: { model: string; tokens: Tokens; basis: Basis; queryId?: string | undefined; agent?: string | undefined })
```

Record one model call's spend, against the running run if there is one. Returns the guardrail notice when this call made the run stop: spend reached the approved budget, or couldn't be measured.

### settleClaudeQuery

```ts
settleClaudeQuery(queryId: string, sessionId: string, total: number, resumed: boolean)
```

The Claude Agent SDK reports each reply's cost (its own estimate) only when the reply ends. Replace what was recorded from tokens during that reply with the SDK's figure, keeping each entry's share. `total` is the SDK's cumulative `total_cost_usd` for the session.

### stopForSdkBudget

```ts
stopForSdkBudget()
```

The SDK stopped the reply at its own budget (`maxBudgetUsd`).

### spendBreakdown

```ts
spendBreakdown()
```

Recorded spend per week for the last five weeks, by model, agent and feature.

### usages

```ts
usages()
```

The meters: the run against its approved budget, its tokens, and the month's spend.

### resetStore

```ts
resetStore()
```

For tests: start from the seed data again. On a database this deletes the block's tables' rows.

### costToolTitle

```ts
costToolTitle(tool: string, input: unknown)
```

Plain-words titles for the block's tools, the same for both agent SDKs.

## Types

### CostDb

```ts
PgDatabase<PgQueryResultHKT, any>
```

### Basis

How a figure was measured. Both are estimates of a bill, from usage the SDK reported.

```ts
"tokens at list price" | "Claude Agent SDK estimate"
```

### RunStatus

```ts
"running" | "finished" | "stopped"
```

### Run

One audit run, as stored.

```ts
{ id: string title: string flags: string[] done: string[] budgetUsd: number spentUsd: number tokens: number basis?: Basis | undefined approvedBy: string startedAt: string status: RunStatus /** Why it stopped, in words, when it did. */ stopped?: { stopped: string; rule: string } | undefined }
```

### LedgerEntry

One model call's recorded spend. `id` is assigned by the repo.

```ts
{ id: number; at: string; runId?: string | undefined; queryId?: string | undefined; model: string; agent: string; feature: string; costUsd: number; basis: Basis }
```

### PastRun

A finished audit, with what it really cost. Estimates come only from these.

```ts
{ id: string; title: string; items: number; costUsd: number; seconds: number; at: string }
```

### Flag

```ts
{ name: string; production: "on" | "off"; staging: "on" | "off"; lastChangedDaysAgo: number; references: { repo: string; path: string }[] }
```

### NewRun

```ts
Pick<Run, "title" | "flags" | "budgetUsd" | "approvedBy" | "startedAt">
```

### RunPatch

```ts
Partial<Pick<Run, "done" | "status" | "stopped">>
```

### NewLedgerEntry

```ts
Omit<LedgerEntry, "id">
```

### Tokens

Tokens as the SDK reported them for one model call. Undefined means the SDK didn't say.

```ts
{ input?: number | undefined; output?: number | undefined; cacheRead?: number | undefined; cacheWrite?: number | undefined }
```

### SpendSnapshot

What GET /api/spend returns.

```ts
{ run: { id: string; status: "running" | "finished" | "stopped"; stopped?: { stopped: string; rule: string } | undefined } | null usages: Usage[] periods: string[] by: Record<"model" | "agent" | "feature", SpendRow[]> }
```

### CostChat

What either adapter hook returns (useCandorChat or useClaudeAgent): the block works with both.

```ts
{ messages: MessageData[] composer: React.ComponentProps<typeof Composer> approvals: { id: string; tool: string; input?: unknown; title?: string | undefined; reason?: string | undefined }[] respond: (id: string, approved: boolean, reason?: string) => unknown error?: string | undefined }
```

## Rules for using Candor UI

- Components take plain typed data. Never pass model output as HTML or render it as a link; messages are typed blocks.
- A control that can't be used says why: wrap it in `<Locked reason="…">`, don't just disable it.
- Don't show a value you don't have: leave it out (it renders as —) rather than guess. An estimate is labelled as one.
- Nothing is pre-selected in a decision a person makes, and the agent is named from your app's data, never from model output.
- Your app's words go in `LocaleProvider` (fixed strings) and `TermsProvider` (nouns); entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
