URL: https://wattfare.com/docs/grants
# One-time grants

> One-time grants let users approve a fixed number of AI requests without signing in or creating a persistent connection — ideal for single-purpose tools like file converters, text rewriters, or any feature where users interact once and move on.

## When to use grants

The standard Wattfare flow — `connect()` → session → metered inference — assumes your app has a user system and wants an ongoing budget relationship. Grants skip all of that.

| Use grants when… | Use connections when… |
| --- | --- |
| Your app has no auth / user accounts. | You have user ids and want ongoing metered access. |
| The user will interact once or twice (e.g. convert a file, rewrite a paragraph). | The user will make many requests over days or weeks. |
| You want the simplest possible integration. | You need status tracking, budget bars, and revocation. |
| You want per-action consent: 'allow 3 requests for this task'. | You want a monthly spending cap the user manages. |

## How it works

Two steps instead of four. No session token, no user id, no persistent connection:

```text
Your App (browser)               Wattfare popup              Your Server
      │                                  │                          │
  1   ├─ requestGrant({ requests: 3 }) ─►│ user sees request count  │
      │                                  │ + optional $ cap         │
      │◄─ { grantToken, expiresAt } ─────┤ user approves            │
      │                                  │                          │
  2   ├─ POST /api/chat  { grantToken } ────────────────────────────►│
      │                                  │         wf.grant(token)  │
      │                                  │         ai.model(…)      │
      │◄─ response ─────────────────────────────────────────────────┤
      │   (grant: 2 requests remaining)  │                          │
```

1. Your frontend calls `requestGrant({ requests, maxUsd? })` — the consent popup opens and the user approves a fixed number of requests (plus an optional dollar cap).
2. On approval, the popup returns a `grantToken`. Your frontend forwards it to your backend.
3. Your backend calls `wf.grant(grantToken)` to get a `WattfareGrant` handle, then uses `ai.model(...)` exactly like a connected user — but each call decrements the grant's remaining count.
4. Once the request count (or dollar cap) is exhausted, further calls throw `WattfareGrantError` (`grant_invalid`).

> **Grants are disposable**
>
> A grant token is short-lived, request-counted, and optionally dollar-capped. Once spent, it cannot be reused. There is no lingering access — the user approves exactly what they're comfortable with.

## Browser: requestGrant()

Available on both the vanilla client (`wattfare/client`) and the React hook (`wattfare/react`). Opens the consent popup and resolves with a `GrantResult`, or `null` if the user denies.

```ts
import { createWattfare } from "wattfare/client";

const wf = createWattfare({
  publishableKey: "pk_live_…",
  session: "/api/wattfare-token",
});

// Must be called from a user gesture (click/tap).
convertBtn.onclick = async () => {
  const result = await wf.requestGrant({ requests: 1, maxUsd: 0.50 });
  if (!result) return; // user denied or closed the popup

  // Forward the grant token to your backend
  const res = await fetch("/api/convert", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ grantToken: result.grantToken, file: selectedFile }),
  });
};
```

| Option | Type | Description |
| --- | --- | --- |
| requests | number | How many inference calls to authorize (hard limit). |
| maxUsd | number \| null | Optional dollar backstop. Omit for request-count-only grants. |

| Result field | Type | Description |
| --- | --- | --- |
| grantToken | string | One-time token. Forward to your backend to redeem. |
| expiresAt | number | Token expiry (unix seconds). Default: 10 minutes. |
| requests | number | Number of authorized requests. |

> **Same popup rule as connect()**
>
> `requestGrant()` opens a popup synchronously inside the click handler. Don't `await` other work before calling it, or popup blockers will kill it.

## React: useWattfare().requestGrant()

The `requestGrant` function from the `useWattfare()` hook works identically. It does **not** affect connection state — `connected`, `remainingUsd`, and other hook values stay unchanged.

```tsx
import { useWattfare } from "wattfare/react";

function ConvertButton() {
  const { requestGrant } = useWattfare();

  async function handleClick() {
    const result = await requestGrant({ requests: 1, maxUsd: 0.50 });
    if (!result) return; // user denied

    await fetch("/api/convert", {
      method: "POST",
      body: JSON.stringify({ grantToken: result.grantToken, file }),
    });
  }

  return <button onClick={handleClick}>✨ Convert with AI</button>;
}
```

## Server: wf.grant()

Redeems the grant token. Returns a `WattfareGrant` handle whose `model()` method works exactly like `WattfareUser.model()` — it's a standard AI SDK provider. No user id required.

```ts
import { Wattfare } from "wattfare/server";
import { generateText } from "ai";

const wf = new Wattfare({ secretKey: process.env.WATTFARE_SECRET_KEY! });

export async function POST(req: Request) {
  const { grantToken, file } = await req.json();

  // Redeem the grant — no user id needed.
  const ai = wf.grant(grantToken);

  const { text } = await generateText({
    model: ai.model("anthropic/claude-sonnet-4"),
    prompt: `Convert this document to markdown: ${file}`,
  });

  return Response.json({ result: text });
  // The grant's request count was decremented by 1.
}
```

| Method | Returns | Description |
| --- | --- | --- |
| wf.grant(grantToken) | WattfareGrant | Redeem a grant. Throws WattfareAuthError if the token is empty. |
| ai.model(modelId) | LanguageModelV3 | AI-SDK-compatible model. Each call decrements the grant's remaining request count. |

## HTTP API

The SDK handles this for you, but here's the raw API for debugging or non-JS stacks.

### POST /grants

Creates a grant. Authed with a **session token** (the consent popup calls this on approval).

```http
POST /api/v1/grants
Authorization: Bearer {session-jwt}
Content-Type: application/json

{
  "requests": 3,       // how many inference calls to authorize
  "maxUsd": 1.50       // optional dollar backstop
}
```

```json
200 OK
{
  "grantToken": "wfg_abc123…",   // one-time token for the backend
  "expiresAt": 1718400000,       // unix seconds
  "requests": 3                  // authorized request count
}
```

### Redeeming via /chat/completions

Send the `x-wattfare-grant` header instead of `x-wattfare-user`. Each successful call decrements the grant's remaining count. When exhausted (or the dollar cap is hit), the proxy responds `402 grant_invalid`.

```http
POST /api/v1/chat/completions
Authorization: Bearer sk_live_{appId}_{secret}
x-wattfare-grant: wfg_abc123…
Content-Type: application/json

{
  "model": "openai/gpt-4o-mini",
  "messages": [{ "role": "user", "content": "Hello" }]
}
```

## Error handling

Grant errors use the same typed error model as the rest of Wattfare. The specific class is `WattfareGrantError` with code `grant_invalid`.

```ts
import { toWattfareError } from "wattfare/server";

try {
  await generateText({ model: ai.model("openai/gpt-4o-mini"), prompt });
} catch (err) {
  const wf = toWattfareError(err);
  if (wf?.code === "grant_invalid") {
    // Grant is expired, fully used, or over its dollar cap.
    // Ask the user to approve a new grant.
    return Response.json({ error: "grant_used" }, { status: 402 });
  }
  throw err;
}
```

| Condition | Meaning | What to do |
| --- | --- | --- |
| `grant_invalid` | The token is unknown, expired, fully used, or over its dollar cap. | Show a "grant used up" message and offer to request a new one. |

> **toWattfareError sees through the AI SDK**
>
> Like all Wattfare errors, `grant_invalid` can be wrapped by the AI SDK's `APICallError`. Use `toWattfareError(err)` instead of `instanceof` — see [Error handling](https://wattfare.com/docs/errors).
