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

# Budgets quickstart

> Put a hard monthly cap on one customer's model calls.

In this quickstart, we cap one customer, `customer:acme`, at \$50 a month. Then we wrap an AI SDK
call in a route handler, so the call runs only when the cap can cover it.

<Steps>
  <Step title="Create an API key">
    Create a **runtime** key in your project's API key settings in the
    [Unprice dashboard](https://app.unprice.dev). Store it as `UNPRICE_TOKEN` in your server's
    environment. Do not send it to a browser or commit it to a repository.
  </Step>

  <Step title="Install the package">
    <CodeGroup>
      ```bash npm theme={null}
      npm install @unprice/budgets ai @ai-sdk/openai
      ```

      ```bash pnpm theme={null}
      pnpm add @unprice/budgets ai @ai-sdk/openai
      ```

      ```bash yarn theme={null}
      yarn add @unprice/budgets ai @ai-sdk/openai
      ```

      ```bash bun theme={null}
      bun add @unprice/budgets ai @ai-sdk/openai
      ```
    </CodeGroup>

    The examples call OpenAI, so they also need `OPENAI_API_KEY` in your environment. If you already
    use `@unprice/api`, leave out `@unprice/budgets` and import from `@unprice/api` instead. It
    includes the same client.
  </Step>

  <Step title="Define the budget">
    Run this once, for example when the customer signs up or from a deploy script. Sending the same
    definition again changes nothing.

    ```ts theme={null}
    import { Unprice } from "@unprice/budgets";

    const unprice = new Unprice({ token: process.env.UNPRICE_TOKEN! });

    const { error } = await unprice.budgets.upsert({
      key: "customer:acme",
      unit: "USD",
      limits: [{ amount: "50", window: "month" }]
    });

    if (error) throw new Error(`${error.code}: ${error.message}`);
    ```

    The call succeeds, and `result.budget.balance.remaining` is `"50"`.

    A runtime key can create a budget and tighten it. Raising the cap later needs a config key. See
    [who can change a cap](/budgets/keys-and-limits#who-can-change-a-cap).
  </Step>

  <Step title="Wrap the model call">
    `spend` reserves the most the call can cost, runs it, and settles what it actually cost.

    ```ts theme={null}
    import { openai } from "@ai-sdk/openai";
    import { generateText } from "ai";

    const model = openai("gpt-4.1");

    // Example prices in USD per million tokens. Use your model's current prices.
    const PRICE = { input: 2, output: 8 };
    const costOf = (usage: { inputTokens?: number; outputTokens?: number }) =>
      ((usage.inputTokens ?? 0) * PRICE.input + (usage.outputTokens ?? 0) * PRICE.output) / 1_000_000;

    // At most one token per UTF-8 byte, plus room for the message format.
    const maxInputTokens = (prompt: string) => new TextEncoder().encode(prompt).length + 64;

    const acme = unprice.budget("customer:acme");
    const maxOutputTokens = 512;

    async function answer(prompt: string) {
      const estimate = costOf({ inputTokens: maxInputTokens(prompt), outputTokens: maxOutputTokens });

      return acme.spend({ estimate }, async () => {
        const r = await generateText({ model, prompt, maxOutputTokens });
        return { value: r.text, actual: costOf(r.usage) };
      });
    }
    ```

    The estimate must cover every billable unit, because `actual` may not exceed it.
    `maxInputTokens` returns a high bound, so most calls settle below the estimate. If your provider
    can count a prompt's tokens before the call, use that count instead.
  </Step>

  <Step title="Handle the denial">
    When the cap cannot cover the estimate, `spend` throws `BudgetExceededError` and the model is
    never called. `error.blocked` names the limit, when it resets, and the remedy.

    ```ts theme={null}
    import { BudgetExceededError } from "@unprice/budgets";

    export async function POST(request: Request) {
      const { prompt } = await request.json();

      try {
        const text = await answer(prompt);
        return Response.json({ text });
      } catch (error) {
        if (error instanceof BudgetExceededError) {
          return Response.json(
            { error: "budget_exceeded", resetsAt: error.blocked.resetsAt },
            { status: 402 }
          );
        }
        throw error;
      }
    }
    ```

    Send the route a prompt. It answers with `{ "text": ... }` while the cap has room, and with a
    402 once it does not.

    `spend` throws other errors when Unprice cannot be reached or the work fails. Read
    [errors and recovery](/budgets/errors) before you go to production.
  </Step>

  <Step title="Check the balance">
    ```ts theme={null}
    const { result } = await unprice.budget("customer:acme").get();

    console.log(result?.budget.balance.remaining); // "49.99412"
    ```

    `remaining` is now below `"50"` by what the call cost. `balance.limits` holds each limit's
    `consumed`, `held`, and `remaining` amounts, and `history` holds the last ended periods.
  </Step>
</Steps>

## Next steps

<CardGroup cols={2}>
  <Card title="Keys and limits" icon="sliders" href="/budgets/keys-and-limits">
    Cap runs and agents, combine windows, and close a budget.
  </Card>

  <Card title="Serverless functions" icon="cloud" href="/budgets/spend#settle-in-the-background">
    Settle after the response with `waitUntil`.
  </Card>
</CardGroup>


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