---
title: Guard the Spend
description: Part 7 of the Build an Agent tutorial. Gate expensive queries with cost-based approval. The agent pauses, asks, and resumes.
url: "https://eve.dev/docs/tutorial/guard-the-spend"
docs_index: /llms.txt
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Make the analytics assistant check with you before a query exceeds a cost threshold. The agent pauses, asks you, and resumes with your answer. That's human-in-the-loop, and you wire it up with one field on the tool. The example assigns each query a simulated scan cost, so it incurs no warehouse charges.

`approval` runs before `execute` and receives the tool input, so the decision can be cost-based. Return `"user-approval"` and the turn pauses on an approval request. When you answer, the turn continues from that exact step.

## Estimate, then gate

Add a cheap estimator and gate `run_sql` on it. If you later [connect a warehouse](./connect-a-warehouse), use the provider's dry-run estimate instead:

```ts title="agent/lib/cost.ts"
// Illustrative: a real warehouse exposes a dry-run byte estimate.
export function estimateScanGb(sql: string): number {
  return /\bwhere\b/i.test(sql) ? 1 : 200; // unfiltered scans are the expensive ones
}
```

```ts title="agent/tools/run_sql.ts"
import { defineTool } from "eve/tools";
import { z } from "zod";
import { runReadOnlySql } from "../lib/sample-db";
import { estimateScanGb } from "../lib/cost";

const THRESHOLD_GB = 50;

export default defineTool({
  description: "Run a read-only SQL query against the analytics tables.",
  inputSchema: z.object({ sql: z.string().max(10_000) }),
  // Cost-based gate: only the expensive queries need a human yes.
  approval: ({ toolInput }) =>
    estimateScanGb(toolInput?.sql ?? "") > THRESHOLD_GB ? "user-approval" : "not-applicable",
  async execute({ sql }) {
    const { columns, rows } = await runReadOnlySql(sql);
    return { columns, rows: rows.slice(0, 500), truncated: rows.length > 500 };
  },
});
```

Cheap queries run straight through. A query estimated above the threshold trips the gate.

## Pause, ask, resume

Ask for something that forces a large unfiltered scan:

```text
Total revenue across all customers, all time, broken out by day.
```

The model proposes the query, `approval` returns `"user-approval"`, and the turn pauses. The stream emits `interaction.opened`, then `turn.paused`, and the turn stays open until someone answers. The channel renders the prompt: buttons in the TUI, Block Kit in Slack, or a UI control on the web. Approve it and the turn continues from that step and runs the query. Deny it and the tool is skipped, with the model told why.

An answer approves the call only while its request is waiting. An answer to an already-answered or cancelled request never approves the earlier call, so a paused turn can't resume twice. See [Send a follow-up message](../concepts/sessions-runs-and-streaming#send-a-follow-up-message).

The same pause and resume also powers the opt-in `ask_question` tool, where the model asks you mid-turn, and per-connection approval via `approval: once()`. See [Tools and human-in-the-loop](../tools).

→ Next: [Ship it](./ship-it)

Learn more: [Tools and human-in-the-loop](../tools)

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)