Cost guard (Claude Agent SDK)

Premium

The person's answer to the agent's permission request: { id, approved, reason? }.

Markdown

Preview

It runs with a server and an agent, so there’s no preview here: install it to run it in your app.

Install

With the Candor CLI

First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/cost-guard-claude

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: @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

<POST>

No props.

Also takes the props of Request.

<POST>

The agent, on the Claude Agent SDK. Set ANTHROPIC_API_KEY. Runs on one Node server, not spread over instances.

No props.

Also takes the props of Request.

<GET>

The latest run, the meters and the recorded spend by week.

No props.

<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.

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

Functions

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(db: CostDb)
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()

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

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(modelId: string, t: Tokens)

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

setCostRepo(repo: CostRepo)

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

costRepo()

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

viewerOf(request: Request)

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

listFlags()
estimateRun(flags: string[])

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

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

Start a run on the budget the person approved.

auditFlag(name: string)

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

finishRun(summary: string)

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

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(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()

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

spendBreakdown()

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

usages()

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

resetStore()

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

costToolTitle(tool: string, input: unknown)

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

Types

CostDb
PgDatabase<PgQueryResultHKT, any>
Basis
How a figure was measured. Both are estimates of a bill, from usage the SDK reported.
"tokens at list price" | "Claude Agent SDK estimate"
RunStatus
"running" | "finished" | "stopped"
Run
One audit run, as stored.
{ 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.
{ 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.
{ id: string; title: string; items: number; costUsd: number; seconds: number; at: string }
Flag
{ name: string; production: "on" | "off"; staging: "on" | "off"; lastChangedDaysAgo: number; references: { repo: string; path: string }[] }
NewRun
Pick<Run, "title" | "flags" | "budgetUsd" | "approvedBy" | "startedAt">
RunPatch
Partial<Pick<Run, "done" | "status" | "stopped">>
NewLedgerEntry
Omit<LedgerEntry, "id">
Tokens
Tokens as the SDK reported them for one model call. Undefined means the SDK didn't say.
{ input?: number | undefined; output?: number | undefined; cacheRead?: number | undefined; cacheWrite?: number | undefined }
SpendSnapshot
What GET /api/spend returns.
{ 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.
{ 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 }