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:
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) │ │ -
Your frontend calls
requestGrant({ requests, maxUsd? })— the consent popup opens and the user approves a fixed number of requests (plus an optional dollar cap). -
On approval, the popup returns a
grantToken. Your frontend forwards it to your backend. -
Your backend calls
wf.grant(grantToken)to get aWattfareGranthandle, then usesai.model(...)exactly like a connected user — but each call decrements the grant's remaining count. -
Once the request count (or dollar cap) is exhausted, further calls throw
WattfareGrantError(grant_invalid).
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.
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. |
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.
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.
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).
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
} 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.
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.
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. |