# Wattfare — Full Documentation > Wattfare is an OAuth-like consent layer for AI spend. Your users connect their own LLM inference budget, set a spending cap, and you call any model through one SDK — billed to them, not you. Source: https://wattfare.com/docs · Generated from the live docs. Index: https://wattfare.com/llms.txt ======================================================================== URL: https://wattfare.com/docs/ai # Build with AI > Wattfare is built to be wired in by an AI coding agent. Copy the prompt below into Cursor, Claude Code, Lovable, Bolt, or v0 — it carries the whole integration, so the agent can ship it in one pass. > **The fastest path** > > Hit **Copy prompt** on the prompt below and paste it into your agent. It already contains the full integration — keys, the session route, the connect button, and metered inference — so the agent can wire Wattfare into your stack without guessing. No chat required. ## The integration prompt One self-contained prompt. It's short enough to drop into any agent and complete enough to execute — and it links the [full plain-text docs](https://wattfare.com/docs/ai/llms-txt) so agents that can browse will pull exact signatures when they need them. **Integrate Wattfare** ```text Integrate Wattfare into this app. Wattfare is an OAuth-like layer that lets my users connect their own LLM budget (with a spending cap), so model calls are billed to them — not me. Full docs: https://wattfare.com/llms-full.txt Setup: run `npm i wattfare` (add `ai` for the inference helpers). Two keys — a publishable key (browser, env PUBLIC_WATTFARE_KEY) and a secret key (server only, env WATTFARE_SECRET_KEY). Wire up these three pieces, matching my framework and my existing auth: 1) Session route — the browser asks my server for a short-lived token: import { Wattfare } from "wattfare/server"; const wf = new Wattfare({ secretKey: process.env.WATTFARE_SECRET_KEY! }); export const POST = wf.sessionHandler( async (req) => (await getUser(req))?.id ?? null, // null => 401 { requestLimit: { monthlyUsd: 10 } }, // suggested cap ); 2) Connect button (React) — call connect() inside the click handler: import { WattfareProvider, useWattfare } from "wattfare/react"; // wrap the app once: // const { connected, connect, remainingUsd, loading } = useWattfare(); // !connected -> show a "Connect AI budget" button (onClick={connect}); // connected -> show remainingUsd. // No React? Use createWattfare({ publishableKey, session }) from "wattfare/client". 3) Inference (server) — billed to the connected user (any OpenRouter model id): const ai = wf.user(userId); if (!(await ai.status()).connected) return Response.json({ error: "not_connected" }, { status: 402 }); const result = streamText({ model: ai.model("openai/gpt-4o-mini"), prompt }); // catch isBudgetExceeded(err) from "wattfare/server" -> return HTTP 402. When you're done, list the env vars I need to set. Only ask me if you can't find my auth or my existing AI call. ``` Works with **Cursor**, **Claude Code**, **Windsurf**, **GitHub Copilot**, **Cline**, **Lovable**, **Bolt**, **v0** — any agent that edits your code. ## Just the UI Using a UI-first tool like **v0**, **Lovable**, or **Bolt** and only need the front end? This one builds the connect button and budget UI against the React hook, and leaves your backend alone. **Build the connect UI** ```text Build a polished "Connect AI budget" UI with Wattfare (React). Wattfare is an OAuth-like popup where a user connects their own AI budget; after they connect, I read their remaining balance from a hook. Docs: https://wattfare.com/llms-full.txt Use wattfare/react: - Wrap the app once: - const { connected, connect, disconnect, remainingUsd, loading } = useWattfare(); Design a component that matches my existing UI: - a loading skeleton while `loading`; - not connected: a primary "Connect AI budget" button that calls connect() DIRECTLY on click (deferring it gets the popup blocked); - connected: a budget bar showing remainingUsd (or "Unlimited" when it's null), plus a subtle "Disconnect" action; - a friendly state when remainingUsd reaches 0 that prompts raising the cap. Output the component and tell me where to mount the provider. ``` ## Point your agent at the docs Skip the copy-paste entirely: give your tooling the docs once and let it read what it needs. Every page is published as plain text following the [ llms.txt standard](https://wattfare.com/docs/ai/llms-txt) — clean, structured, no HTML noise. ### Cursor, Windsurf & Copilot Add the index as an indexed documentation source, then `@`-mention it as you build. ```bash # Cursor / Windsurf — add the docs as an indexed source # Settings → Indexing & Docs → Add doc, paste: https://wattfare.com/llms.txt # then @-mention it while you build: @Docs Wattfare add a session-token route to this project ``` ### Claude Code & coding agents Agents that can fetch URLs read the docs directly — point them at the full dump, or the index so they pull only the pages they need. Every page also has a predictable `.md` twin. ```bash # Claude Code / Codex / Cline & other agents — # point them at the docs, then let them work: Read https://wattfare.com/llms-full.txt, then integrate Wattfare into this project. # or have the agent pull only the pages it needs from the index: https://wattfare.com/llms.txt ``` ## Per-page AI actions Every docs page (this one included) has a **Copy page** button in the top-right. Open the caret beside it for more: - **Copy page** — the page as clean Markdown, ready to paste into an agent. - **Copy as prompt** — that Markdown wrapped in context, framed as a task. - **Open in ChatGPT / Claude** — for when you do want to talk it through. > **Always current** > > The prompts and plain-text files are generated from these docs on every build, so they never drift from the real SDK. Change a signature here and it changes everywhere. ## Keep going - [Docs for LLMs](https://wattfare.com/docs/ai/llms-txt) — The llms.txt files, per-page Markdown URLs, and autodiscovery — everything an agent needs. - [Quickstart](https://wattfare.com/docs/quickstart) — Prefer to wire it by hand? Zero to a working connect button in five steps. - [Server SDK](https://wattfare.com/docs/server-sdk) — The full reference for the methods the prompt leans on. - [Error handling](https://wattfare.com/docs/errors) — Turn “not connected”, “budget exceeded”, and rate limits into graceful UI. ======================================================================== URL: https://wattfare.com/docs/ai/llms-txt # Docs for LLMs > Every page of these docs is published as plain text following the llms.txt standard — optimised for AI coding assistants and autonomous agents that want structured content without the HTML. HTML is for humans. When an AI assistant reads documentation it wants clean, predictable text — so alongside this site we publish the same content as Markdown, indexed the way agents expect. The files below are generated from these exact pages on every build, so they're always in sync. ## Available files | File | URL | Best for | | --- | --- | --- | | **Index** `llms.txt` | [`wattfare.com/llms.txt`](https://wattfare.com/llms.txt) | Agents that discover and fetch only the pages they need | | **Full docs** `llms-full.txt` | [`wattfare.com/llms-full.txt`](https://wattfare.com/llms-full.txt) | One-shot context loading — every page in a single file | ### Per-page Markdown Every documentation page also has its own Markdown file at a predictable URL — just append `.md` to the page's path: ```bash https://wattfare.com/docs/.md # examples https://wattfare.com/docs.md # Introduction https://wattfare.com/docs/quickstart.md # Quickstart https://wattfare.com/docs/server-sdk.md # Server SDK https://wattfare.com/docs/guides/nextjs.md # Next.js guide ``` Each file starts with a `URL:` line naming its canonical source, so an agent always knows where the content came from even when it's cached or passed out of context. The complete list lives in [`llms.txt`](https://wattfare.com/llms.txt). ## How to use them ### Cursor, Windsurf & Copilot Add the index URL as a documentation source in your editor's AI settings: ```bash https://wattfare.com/llms.txt ``` Your editor fetches the index and pulls the relevant pages into context as you work — no manual copy-pasting. ### Claude & ChatGPT Paste the full documentation straight into the conversation or a system prompt: ```bash # Full context in one paste: https://wattfare.com/llms-full.txt # …or let an agent fetch only what it needs from the index: https://wattfare.com/llms.txt ``` ### Autonomous agents Agents can discover everything by fetching `llms.txt` first, then requesting only the individual `.md` files they need. The leading `URL:` line keeps the source canonical: ```text URL: https://wattfare.com/docs/quickstart.md # Quickstart > From zero to a working "Connect AI budget" button in about five minutes. ... ``` ## Autodiscovery Every page on this site advertises the index and its own Markdown twin in the HTML ``, so crawlers and browser-level agents can find them without knowing the URLs in advance: ```html ``` > **Always in sync** > > These files are regenerated from the live docs on every build. Update a page and its `.md`, the index, and the full dump all change with it — there's no separate content to maintain. ## Related - [Prompts & AI editors](https://wattfare.com/docs/ai) — Copy-paste prompts that turn this content into a finished integration. - [Quickstart](https://wattfare.com/docs/quickstart) — The five-step manual walkthrough, if you'd rather drive yourself. - [HTTP API](https://wattfare.com/docs/api-reference) — The raw endpoints, for agents wiring Wattfare into a custom stack. ======================================================================== URL: https://wattfare.com/docs # Wattfare documentation > Wattfare is an OAuth-like consent layer for AI spend. Your users connect their own inference budget, set a cap, and you call any model through one SDK — charged to them, not you. Shipping an AI feature usually means **you** pay for every token your users burn. Costs are unpredictable, a single power user can wreck your margins, and you carry the risk. Wattfare flips that around: your users bring their own AI budget and approve a spending cap once, in a hosted consent popup. You make model calls on their behalf — metered against that budget, billed to them. Think **“Sign in with Google”, but for AI spend**. One button connects a user's inference budget to your app: metered, capped, and revocable at any time. Your secret key never leaves your backend; the browser only ever holds a short-lived, scoped session token. ## Why Wattfare - **Predictable costs** — Your users' spend is their own. No surprise inference bills, no per-seat AI pricing you have to model and absorb. - **One SDK, every model** — An OpenAI-compatible proxy backs hundreds of models. Swap openai/gpt-4o-mini for anthropic/claude-sonnet-4 with a string. - **Users stay in control** — Caps are enforced upstream. Users can see usage and revoke any app from their dashboard — which is exactly what builds trust. - **No secrets in the browser** — The frontend forwards a 10-minute JWT minted by your backend. The secret key and the user's funding source never touch the client. ## Three SDK surfaces The `wattfare` package has no root export — you import only the surface you need. Each is a thin, focused layer over the same HTTP API. - [Server SDK](https://wattfare.com/docs/server-sdk) — Holds your secret key. Mints session tokens and runs inference on a user's behalf. - [Client SDK](https://wattfare.com/docs/client-sdk) — Runs in the browser. Opens the consent popup and reads connection status. - [React SDK](https://wattfare.com/docs/react-sdk) — A provider and a useWattfare() hook that wire the whole flow into React. ## At a glance Installing is one command. On your backend, inference is a drop-in AI SDK provider — if you've used the Vercel AI SDK, this will look familiar: ```bash npm install wattfare ``` ```ts import { Wattfare } from "wattfare/server"; import { generateText } from "ai"; const wf = new Wattfare({ secretKey: process.env.WATTFARE_SECRET_KEY! }); // Scoped to one of *your* user ids — no Wattfare account for end users. const ai = wf.user("user_123"); const { text } = await generateText({ model: ai.model("openai/gpt-4o-mini"), // billed to the user's budget prompt: "Say hello.", }); ``` The only Wattfare-specific lines are constructing the client and calling `wf.user(id)`. Everything downstream is standard AI SDK. Usage is metered automatically against the connected user's budget. > **Developer preview** > > Wattfare is in developer preview. The SDK surface documented here is stable; spending limits are soft-enforced for now. Have feedback or hit a rough edge? [hello@wattfare.com](mailto:hello@wattfare.com). ## Start here - [Quickstart](https://wattfare.com/docs/quickstart) — Wire up a working “Connect AI budget” button end-to-end in about five minutes. - [How it works](https://wattfare.com/docs/concepts) — The consent flow, keys, sessions, and metering — the model behind the SDK. - [Next.js guide](https://wattfare.com/docs/guides/nextjs) — A complete App Router integration with streaming chat, copy-paste ready. - [HTTP API](https://wattfare.com/docs/api-reference) — The raw REST endpoints, for debugging or building on a stack the SDK doesn't cover. ======================================================================== URL: https://wattfare.com/docs/quickstart # Quickstart > From zero to a working “Connect AI budget” button in about five minutes. Five steps: install, get keys, mint sessions, connect, infer. > **Before you start** > > You'll need a Wattfare app (free — create one in the [dashboard](https://wattfare.com/dashboard)) and an app of your own with some notion of a signed-in user. Wattfare keys everything by your own user ids, so there's no separate account for your end users to make. ## 1 · Install the SDK The package ships server, client, and React entry points from one install. `ai` and `react` are optional peer dependencies — add them only if you use the inference helpers or the React binding. ```bash npm install wattfare # peer deps — only if you use them: npm install ai # inference helpers (Vercel AI SDK) npm install react # the React binding ``` ## 2 · Get your keys Create an app in the [developer dashboard](https://wattfare.com/dashboard). You'll get two keys with very different jobs: | Key | Format | Where it lives | Secret? | | --- | --- | --- | --- | | Publishable | pk_live_{appId} | Browser — passed to the client / provider | No | | Secret | sk_live_{appId}_{secret} | Backend only — mints sessions, runs inference | Yes | Store them as environment variables. The secret key must never reach the browser. ```bash # .env (server — never ship to the browser) WATTFARE_SECRET_KEY=sk_live_myapp_abc123... # public — safe in the browser PUBLIC_WATTFARE_KEY=pk_live_myapp ``` > **The secret key is shown once** > > Copy it the moment you create the app. If you lose it, rotate it from the dashboard — the old one stops working immediately. ## 3 · Mint session tokens on your backend The browser can't hold your secret key, so it asks your backend for a short-lived token first. `sessionHandler()` turns that into a one-line route in any framework that speaks `Request → Response` (Next.js, Astro, Hono, Remix, Workers…). ```ts import { Wattfare } from "wattfare/server"; const wf = new Wattfare({ secretKey: process.env.WATTFARE_SECRET_KEY! }); // sessionHandler() returns a standard Request -> Response handler. // resolveUser returns your app's user id, or null to answer 401. export const POST = wf.sessionHandler( async (req) => { const session = await getSession(req); // <- your existing auth return session?.userId ?? null; }, { requestLimit: { monthlyUsd: 10 } }, // optional default cap to suggest ); ``` `resolveUser` receives the raw request — plug in your existing auth and return the user's id, or `null` to answer `401`. The optional second argument suggests a budget cap the user will see in the consent popup. ## 4 · Add the connect button On the frontend, point the SDK at the route you just made. In React, wrap your app once and read everything from the `useWattfare()` hook: ```tsx import { WattfareProvider, useWattfare } from "wattfare/react"; function App() { return ( ); } function Chat() { const { connected, connect, remainingUsd, loading } = useWattfare(); if (loading) return

Loading…

; if (!connected) return ; return (

Budget left: {remainingUsd === null ? "unlimited" : `$${remainingUsd}`}

{/* your chat UI */}
); } ``` Not using React? The vanilla client is the same idea without the context: ```ts import { createWattfare } from "wattfare/client"; const wf = createWattfare({ publishableKey: "pk_live_…", session: "/api/wattfare-token", }); // IMPORTANT: call connect() straight from the click handler, or popup // blockers will kill it — the popup must open inside the user gesture. connectBtn.onclick = () => wf.connect(); ``` > **Call connect() from a click** > > Opening the consent popup must happen inside a user gesture. The SDK opens a blank popup synchronously before any `await`, but if you wrap `connect()` in your own async work first, the browser will block it. ## 5 · Make an inference call Back on your server, `ai.model(...)` returns a standard AI SDK model. Pass it to `streamText` / `generateText` like any other provider — usage is metered against the user's budget automatically. ```ts import { Wattfare, isBudgetExceeded } from "wattfare/server"; import { streamText } from "ai"; const wf = new Wattfare({ secretKey: process.env.WATTFARE_SECRET_KEY! }); export async function POST(req: Request) { const { prompt, userId } = await req.json(); const ai = wf.user(userId); // "Not connected" is the expected first-run state — handle it as a branch. const status = await ai.status(); if (!status.connected) { return Response.json({ error: "not_connected" }, { status: 402 }); } try { const result = streamText({ model: ai.model("openai/gpt-4o-mini"), // any OpenRouter model id prompt, }); return result.toTextStreamResponse(); } catch (err) { if (isBudgetExceeded(err)) { return Response.json({ error: "budget_exceeded" }, { status: 402 }); } throw err; } } ``` ## That's it You now have the full loop: a user connects their budget once, and every model call you make on their behalf is metered and capped. From here: - [How it works](https://wattfare.com/docs/concepts) — Understand the consent flow, sessions, and metering so the pieces above click into place. - [Error handling](https://wattfare.com/docs/errors) — Turn “not connected”, “budget exceeded”, and rate limits into graceful UI. - [Next.js guide](https://wattfare.com/docs/guides/nextjs) — The same flow as a complete App Router project you can copy wholesale. - [Budget UI patterns](https://wattfare.com/docs/guides/budget-ui) — Connect buttons, budget bars, and depletion warnings that earn user trust. ======================================================================== URL: https://wattfare.com/docs/concepts # How it works > The consent flow, the moving parts, and the guarantees behind them. Read this once and the SDK reference will feel obvious. Wattfare sits between your app and the model provider. Your app never holds a user's funding source, and your users never hold a Wattfare account — connections are keyed by **your** user ids. This page walks the whole loop end to end. ## The four phases | Phase | Who acts | What happens | | --- | --- | --- | | **Register** | You, once | Create an app, get a publishable key (browser) and a secret key (backend). | | **Connect** | Your user | They open the consent popup, set a monthly cap, and approve. | | **Inference** | Your backend | `ai.model(...)` proxies through Wattfare, metered against the cap. | | **Monitor** | Both | You read `remainingUsd`; users can revoke from their dashboard. | ## End-to-end sequence Three round-trips that matter: minting a session, the consent popup, and the metered inference proxy. Everything else is detail. ```text Your App (browser) Your Server Wattfare API OpenRouter │ │ │ │ 1 ├─ POST /wattfare-token ─►│ │ │ │ ├─ POST /v1/sessions ─►│ │ │ │◄─ { token, exp } ────┤ │ │◄─ { token } ───────────┤ │ │ │ │ │ │ 2 ├─ connect() ── popup ───────────────────────► consent screen │ │◄─ approved (postMessage) ───────────────────── user sets a cap │ │ │ │ │ 3 ├─ POST /api/chat ──────►│ │ │ │ ├─ POST /v1/chat/* ───►├─ proxy (user's key)─►│ │ │ │◄─ completion ────────┤ │ │◄─ stream ────────────┤ (usage metered) │ │◄─ stream ──────────────┤ │ │ ``` ## Keys and what they unlock Two keys, two trust levels. Keeping them straight is the whole security model: | Key | Holder | Can do | Leak impact | | --- | --- | --- | --- | | pk_live_… | The browser | Identify your app to the consent popup | Low — it only names your app | | sk_live_… | Your backend | Mint sessions, run inference, read status | High — rotate immediately | > **Why a session token at all?** > > The browser needs to open the consent popup and read status, but it can't be trusted with the secret key or to assert “I am user 123”. So your backend mints a short-lived JWT (10-minute TTL) that encodes the app id, your user id, and the requested cap. The browser only ever forwards that token. ## Sessions A session token is a signed JWT your backend creates with `createSession()` (or the `sessionHandler()` wrapper). It carries: - `appId` — derived from your secret key, so the popup knows who's asking. - `appUserId` — your own user id, the join key for the connection. - `limits` — the monthly cap you want the user to approve. - `exp` — expiry, 10 minutes out by default. The client SDK caches the token until ~30 seconds before expiry, so reading status and opening the popup don't each hit your backend. The token rides in the popup's URL *fragment*, never the query string — fragments aren't sent to servers, logged, or kept in referrers. ## The consent popup `connect()` opens a popup on Wattfare's domain. The user sees which app is asking, sets a monthly spending cap, and connects their funding source. On approval, the popup sends the result back via `postMessage` with strict origin verification: - The popup asks “who opened me?”; the SDK replies, stamping your page's origin on the answer. - The popup compares that browser-set origin against your app's registered domain before allowing approval — an unforgeable proof, so a random site can't impersonate your app. - If the user closes the popup, the SDK treats it as a denial and returns the not-connected status. ## Inference and metering `ai.model(modelId)` returns an OpenAI-compatible AI SDK model. Each request carries an `x-wattfare-user` header so the proxy resolves the right connection. On every call, Wattfare: 1. Verifies your secret key and resolves the user's connection + funding. 2. Checks the cap — if spend ≥ cap, it throws `budget_exceeded` before the request leaves. 3. Decrypts the user's funding key per request (so revocation is always live) and forwards to OpenRouter. 4. Tees the response: bytes stream to you untouched while a parallel reader extracts the final `usage` and records cost. The response is never buffered. > **Streaming is first-class** > > Because metering happens on a teed branch of the stream, `streamText().toTextStreamResponse()` works exactly as you'd expect — no added latency, no buffering, full cost accounting. ## Connection status Both `ai.status()` (server) and `wf.status()` (client) return the same shape. It never throws for “not connected” — that's the expected first-run state, modeled as a value, not an exception. ```ts interface ConnectionStatus { connected: boolean; limits: { monthlyUsd: number | null } | null; usage: { monthlyUsd: number }; remainingUsd: number | null; // null = unlimited (dev) } ``` | Field | Meaning | | --- | --- | | connected | Whether the user has an active, funded connection to your app. | | limits.monthlyUsd | The approved cap, or `null` for unlimited (dev only). | | usage.monthlyUsd | Spend so far in the current rolling month. | | remainingUsd | `limits − usage`, or `null` when unlimited. | ## One-time grants Not every app has a user system. **One-time grants** let users approve a fixed number of AI requests without creating a persistent connection — perfect for single-purpose tools like file converters or text rewriters. See the [One-time grants](https://wattfare.com/docs/grants) page for the full flow. ## The guarantees - **Secret stays server-side.** The browser only holds a scoped, short-lived JWT. - **Your ids, not ours.** No Wattfare account for your users; state is keyed by `appId:appUserId`. - **Caps enforced upstream.** Budget is checked inside Wattfare before any request reaches the model provider. - **Revocable and live.** A user can disconnect anytime; the funding key is re-checked on every request. - **We never see prompts… for billing.** Cost is read from the provider's usage object; the proxy doesn't store prompt content. - **Grants are disposable.** One-time tokens are request-counted and optionally dollar-capped. No lingering access after the grant is spent. ======================================================================== 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 ; } ``` ## 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). ======================================================================== URL: https://wattfare.com/docs/server-sdk # Server SDK > Import from wattfare/server. This runs on your backend, holds the secret key, and brokers everything that must never touch the browser — minting sessions and running inference. ## new Wattfare(config) The server entry point. Construct it once with your secret key and reuse it. ```ts import { Wattfare } from "wattfare/server"; const wf = new Wattfare({ secretKey: process.env.WATTFARE_SECRET_KEY!, // required, starts with "sk_" baseURL: "https://wattfare.com", // optional override fetch: customFetch, // optional, for tests/runtimes }); ``` | Option | Type | Default | Notes | | --- | --- | --- | --- | | secretKey | string | — | Required. Must start with `sk_` or the constructor throws `WattfareAuthError`. | | baseURL | string | https://wattfare.com | Point at a different Wattfare service (self-host / staging). | | fetch | typeof fetch | global fetch | Inject a custom fetch for tests or non-standard runtimes. | ## Instance methods | Method | Returns | Description | | --- | --- | --- | | user(appUserId) | WattfareUser | Scope all operations to one of your user ids. | | createSession(appUserId, request?) | Promise | Mint a short-lived JWT for the frontend. | | sessionHandler(resolveUser, request?) | (req) => Promise | A drop-in route handler that mints sessions. | | grant(grantToken) | WattfareGrant | Redeem a one-time access grant. Returns a handle whose `model()` spends against the grant. | ## wf.user(appUserId) Returns a `WattfareUser` bound to one of your users. Pass **your own** user id — Wattfare keys all state by `appId:appUserId`, so there's nothing to map. ```ts const ai = wf.user("user_123"); // your own user id // 1) Inference — a standard AI SDK model, billed to this user. import { generateText } from "ai"; const { text } = await generateText({ model: ai.model("anthropic/claude-sonnet-4"), prompt: "Summarise this in one line.", }); // 2) Status — connection + budget snapshot (never throws on first run). const status = await ai.status(); if (!status.connected) { // prompt the user to connect their budget } ``` | Method | Returns | Description | | --- | --- | --- | | model(modelId) | LanguageModelV3 | An AI-SDK-compatible model. Requests proxy through Wattfare with the user header set. Accepts any OpenRouter model id. | | status() | Promise | The user's connection + budget snapshot. Returns `{ connected: false }` rather than throwing when they haven't connected. | > **model() is a real AI SDK provider** > > The return value works with `generateText`, `streamText`, `generateObject`, tools — anything in the Vercel AI SDK. There's nothing custom to learn; Wattfare just sits in the transport. ## wf.grant(grantToken) Redeems a one-time access grant token from the browser. Returns a `WattfareGrant` handle whose `model()` works like `WattfareUser.model()` — no user id needed. See [One-time grants](https://wattfare.com/docs/grants) for the full guide. ## wf.sessionHandler(resolveUser, request?) Builds a `Request → Response` handler that mints session tokens. This is the recommended way to expose your token route — it handles auth failures and error shaping for you. ```ts // Next.js — app/api/wattfare-token/route.ts export const POST = wf.sessionHandler( async (req) => getUserId(req), // -> your user id, or null -> 401 { requestLimit: { monthlyUsd: 20 } }, // optional cap the user approves ); // The second arg can also be a function of the request, e.g. per-plan caps: export const POST = wf.sessionHandler( (req) => getUserId(req), async (req) => ({ requestLimit: { monthlyUsd: await capFor(req) } }), ); ``` | Parameter | Type | Description | | --- | --- | --- | | resolveUser | (req) => string \| null \| Promise<…> | Resolve the signed-in user from the request. Return your user id, or `null` to answer `401`. | | request | SessionRequest \| (req) => SessionRequest | Optional. The cap to request, or a function of the request for dynamic caps. | On success it responds `{ token, expiresAt }`. On a thrown `WattfareError` it responds with that error's JSON and status; on anything else, a generic `500`. ## wf.createSession(appUserId, request?) The lower-level primitive behind `sessionHandler`. Reach for it when you need to shape the response yourself or aren't in a `Request → Response` runtime. ```ts const { token, expiresAt } = await wf.createSession("user_123", { requestLimit: { monthlyUsd: 20 }, // optional }); // Hand 'token' to the frontend however you like (it's just a string). return Response.json({ token, expiresAt }); ``` | Field | Type | Description | | --- | --- | --- | | token | string | The short-lived JWT. Pass it to the frontend client. | | expiresAt | number | Unix epoch (seconds). Default TTL is 10 minutes. | ## Types Re-exported from `wattfare/server` for convenience. ```ts interface ConnectionStatus { connected: boolean; limits: { monthlyUsd: number | null } | null; usage: { monthlyUsd: number }; remainingUsd: number | null; // null = unlimited } ``` | Type | Shape | | --- | --- | | Limits | `{ monthlyUsd: number \| null }` | | Usage | `{ monthlyUsd: number }` | | SessionRequest | `{ requestLimit?: { monthlyUsd?: number } }` | | CreatedSession | `{ token: string; expiresAt: number }` | | GrantResult | `{ grantToken: string; expiresAt: number; requests: number }` | > **Note** > > Error classes and the `isBudgetExceeded` / `isNotConnected` / `toWattfareError` helpers are also exported here. See [Error handling](https://wattfare.com/docs/errors). ======================================================================== URL: https://wattfare.com/docs/client-sdk # Client SDK > Import from wattfare/client. Framework-agnostic, runs in the browser, never touches the secret key — it works entirely with session tokens minted by your backend. ## createWattfare(config) Creates a browser client. It throws immediately if the publishable key is missing or malformed. ```ts import { createWattfare } from "wattfare/client"; const wf = createWattfare({ publishableKey: "pk_live_…", // required, starts with "pk_" session: "/api/wattfare-token", // backend route OR a token function baseURL: "https://wattfare.com", // optional consentURL: "https://wattfare.com", // optional popup origin }); ``` | Option | Type | Default | Notes | | --- | --- | --- | --- | | publishableKey | string | — | Required. Must start with `pk_`. | | session | string \| () => string \| Promise | — | Required. Your backend route path, or a function returning a token. | | baseURL | string | https://wattfare.com | Override the Wattfare service URL. | | consentURL | string | https://wattfare.com | Override the consent popup origin. | ## The session source `session` is how the client gets a token without ever seeing your secret key. Pass a string for the easy path, or a function when you need custom headers or token plumbing: ```ts // Shorthand: a backend route path. The SDK POSTs to it and reads // { token } (or a raw token string) from the response. const wf = createWattfare({ publishableKey, session: "/api/wattfare-token" }); // Full control: a function that returns (or resolves) a token string. const wf = createWattfare({ publishableKey, session: async () => { const res = await fetch("/api/wattfare-token", { method: "POST" }); const { token } = await res.json(); return token; }, }); ``` > **Tokens are cached** > > The client caches the token until ~30 seconds before it expires, so `status()`, `connect()`, and `disconnect()` don't each round-trip to your backend. ## connect() Opens the consent popup and resolves once the user approves or denies. On approval it returns the fresh `ConnectionStatus`; on denial (or if they close the popup) it returns the not-connected status. ```ts // Must be called from a user gesture (click/tap). connectBtn.onclick = async () => { const status = await wf.connect(); if (status.connected) renderBudget(status.remainingUsd); }; ``` > **Call it inside the click** > > The SDK opens a blank popup *synchronously* in the handler, then navigates it once the token resolves — that's what beats popup blockers. If you do your own `await` before calling `connect()`, the popup opens outside the gesture and gets blocked. It throws `WattfareNetworkError` in that case. ## status() Fetches the current connection and budget snapshot for the session's user. Returns `{ connected: false, … }` when they haven't connected — it never throws for that case, so you can call it on every page load safely. ```ts const status = await wf.status(); // { connected: true, limits: { monthlyUsd: 10 }, // usage: { monthlyUsd: 3.2 }, remainingUsd: 6.8 } if (!status.connected) showConnectButton(); ``` ## disconnect() Revokes the connection for the session's user. Their budget is immediately cut off from your app. ```ts await wf.disconnect(); // Connection revoked. In-flight requests may finish; new ones won't be authorised. ``` ## requestGrant(options) Opens the consent popup for a one-time access grant — no persistent connection needed. See [One-time grants](https://wattfare.com/docs/grants) for the full API, options, and examples. ## WattfareClient | Method | Returns | Description | | --- | --- | --- | | connect() | Promise | Open the consent popup; resolve on approve/deny. | | status() | Promise | Current connection + budget snapshot. | | disconnect() | Promise | Revoke the connection. | | requestGrant(options) | Promise | Request a one-time access grant; resolve on approve/deny. | > **Using React?** > > [wattfare/react](https://wattfare.com/docs/react-sdk) wraps this client in a provider + hook so you get reactive `connected` / `remainingUsd` state for free. ======================================================================== URL: https://wattfare.com/docs/react-sdk # React SDK > Import from wattfare/react. A provider that owns one client, and a useWattfare() hook that exposes reactive connection state and the connect / disconnect / refresh actions. ## Wrap your app (or the subtree that needs budget state) once. It creates a single `WattfareClient` via `createWattfare()` and memoizes it — stable across re-renders, only re-created when its config changes. ```tsx import { WattfareProvider } from "wattfare/react"; {children} ``` | Prop | Type | Default | Notes | | --- | --- | --- | --- | | publishableKey | string | — | Required. Starts with `pk_`. | | session | string \| () => string \| Promise | — | Required. Backend route path or a token-returning function. | | baseURL | string | https://wattfare.com | Override the Wattfare service URL. | | consentURL | string | https://wattfare.com | Override the consent popup origin. | | loadOnMount | boolean | true | Fetch status once on mount. First-run “not connected” is swallowed, not surfaced as an error. | ## useWattfare() Must be called inside a `` (it throws otherwise). Returns the live connection snapshot plus the three actions. ```tsx import { useWattfare } from "wattfare/react"; function BudgetBar() { const { connected, connect, disconnect, remainingUsd, loading } = useWattfare(); if (loading) return ; if (!connected) return ; return (
{remainingUsd !== null ? `$${remainingUsd.toFixed(2)} left` : "Unlimited"}
); } ``` | Field | Type | Description | | --- | --- | --- | | connected | boolean | Whether the user has an active budget connection. | | remainingUsd | number \| null | Remaining spend for the period. `null` = unlimited. | | status | ConnectionStatus | The full snapshot (`limits`, `usage`, `remainingUsd`). | | loading | boolean | True while any connect / disconnect / refresh is in flight. | | error | Error \| null | The last error, if any. | | connect() | () => Promise | Open the consent popup, then refresh status. Call from a click handler. | | disconnect() | () => Promise | Revoke the connection and reset to not-connected. | | refresh() | () => Promise | Re-fetch the connection + budget snapshot. | | requestGrant(options) | (options: RequestGrantOptions) => Promise | Open the consent popup for a one-time access grant. Returns the grant result or `null` if denied. Does not affect connection state. | > **Refresh after each call** > > The hook doesn't know when you've spent budget on your server. Call `refresh()` after an inference request to keep `remainingUsd` in sync with reality. ```tsx function Chat() { const { connect, refresh, remainingUsd } = useWattfare(); async function send(prompt: string) { await fetch("/api/chat", { method: "POST", body: JSON.stringify({ prompt }) }); await refresh(); // pull the new remaining budget after a call } // … } ``` > **connect() still needs a gesture** > > Even through the hook, `connect()` must run inside a user gesture so the consent popup opens. Wire it directly to `onClick` — don't await other work first. ## One-time grants `requestGrant()` opens the consent popup for a disposable access grant — it doesn't affect `connected` or any other hook state. See [One-time grants](https://wattfare.com/docs/grants) for the full guide and examples. ## SSR & “use client” The provider and hook are client components — they use React context and effects. In Next.js App Router, render `` in a file marked `"use client"` (or a client boundary) and keep your secret-key code in server route handlers. See the [Next.js guide](https://wattfare.com/docs/guides/nextjs) for a full layout. ======================================================================== URL: https://wattfare.com/docs/errors # Error handling > Every SDK error extends WattfareError. The “not connected” case is the expected first-run path, so it's modeled as a branch — not a crash. Helpers recover typed errors even when the AI SDK wraps them. ## Error classes Typed subclasses are exported from every entry point (`server`, `client`, `react`). | Class | code | HTTP | When it happens | | --- | --- | --- | --- | | WattfareNotConnectedError | not_connected | 402 | The user hasn't connected a budget to your app yet. | | WattfareBudgetExceededError | budget_exceeded | 402 | Their monthly cap for this period is reached. | | WattfareFundingError | funding_invalid | 402 | Their funding key was rejected upstream (revoked / out of credit). They must reconnect. | | WattfareRateLimitError | rate_limited | 429 | Too many requests. Back off and retry. | | WattfareGrantError | grant_invalid | 402 | The one-time access grant is unknown, expired, fully used, or over its dollar cap. | | WattfareAuthError | auth | 401 | Invalid or missing credentials (secret key, session token…). | | WattfareNetworkError | network | 0 | Couldn't reach the Wattfare service at all. | A generic `WattfareError` also covers `invalid_request`, `upstream` (the model provider failed), and `unknown`. ```ts // Every WattfareError serialises to this shape over the wire: { "error": { "code": "budget_exceeded", "message": "Spending cap reached for this period." } } // And carries: err.code // WattfareErrorCode err.status // HTTP status it maps to err.toJSON() ``` ## Helper functions Reach for these instead of `instanceof`. They see through the AI SDK's `APICallError` wrapper, which would otherwise hide the real code. | Function | Returns | Use it to… | | --- | --- | --- | | toWattfareError(err) | WattfareError \| null | Recover a typed error from anything. `null` when unrelated to Wattfare. | | isBudgetExceeded(err) | boolean | Detect a reached cap (even AI-SDK-wrapped). | | isNotConnected(err) | boolean | Detect “user must (re)connect” — matches both `not_connected` and `funding_invalid`. | > **Why plain instanceof fails** > > When you call a model through `ai.model(...)`, the AI SDK catches Wattfare's HTTP error and re-throws its own `APICallError` wrapping the response body. So `err instanceof WattfareBudgetExceededError` is `false`. Always use `toWattfareError` / the `is*` guards around inference calls. ## The canonical pattern Handle the two actionable cases first, then fall back to the typed error's own status: ```ts import { isNotConnected, isBudgetExceeded, toWattfareError } from "wattfare/server"; try { const result = await generateText({ model: ai.model("anthropic/claude-sonnet-4"), prompt, }); return Response.json({ text: result.text }); } catch (err) { if (isNotConnected(err)) return Response.json({ error: "connect_required" }, { status: 402 }); if (isBudgetExceeded(err)) return Response.json({ error: "budget_reached" }, { status: 402 }); const wf = toWattfareError(err); if (wf) return Response.json(wf.toJSON(), { status: wf.status }); throw err; // genuinely unrelated — let it bubble } ``` ## Branching on code When you need finer control, read `code` off the recovered error: ```ts import { toWattfareError } from "wattfare/server"; try { await generateText({ model: ai.model("openai/gpt-4o-mini"), prompt }); } catch (err) { // err is the AI SDK's APICallError — a plain instanceof check would miss it. const wf = toWattfareError(err); if (wf?.code === "budget_exceeded") return askUserToRaiseCap(); if (wf?.code === "funding_invalid") return askUserToReconnect(); throw err; } ``` ## What to show users | Condition | User-facing action | | --- | --- | | `not_connected` | Show the “Connect AI budget” button. | | `funding_invalid` | Same button, with a “reconnect — your funding needs attention” note. | | `budget_exceeded` | Show usage vs. cap; link them to raise the cap in their dashboard. | | `grant_invalid` | Show a "Your access grant has been used up" message and offer to request a new one. | | `rate_limited` | Back off (exponential) or show a brief “slow down” message. | | `network` / `upstream` | Retry with backoff; surface a transient-error toast. | | `auth` | A bug on your side — your secret key or session is wrong. Log and alert. | > **Tip** > > `isNotConnected` intentionally returns `true` for `funding_invalid` too, so a single check covers “the user needs to take action before this can work.” ======================================================================== URL: https://wattfare.com/docs/guides/nextjs # Next.js integration > A complete Next.js App Router integration: a shared server client, a session route, a streaming chat route, the provider, and a chat UI that tracks budget. Copy it wholesale. > **Prerequisites** > > A Next.js 14+ app with App Router and some auth that yields a stable user id (NextAuth, Clerk, Better Auth, your own — anything). And a Wattfare app from the [dashboard](https://wattfare.com/dashboard). ## 1 · Environment variables The secret stays server-side; only the publishable key gets the `NEXT_PUBLIC_` prefix. ```bash WATTFARE_SECRET_KEY=sk_live_myapp_abc123... NEXT_PUBLIC_WATTFARE_KEY=pk_live_myapp ``` ## 2 · A shared server client Construct `Wattfare` once and import it into your route handlers. ```ts // lib/wattfare.ts import { Wattfare } from "wattfare/server"; // One instance, reused across route handlers. export const wf = new Wattfare({ secretKey: process.env.WATTFARE_SECRET_KEY! }); ``` ## 3 · The session route One line with `sessionHandler`. It resolves the signed-in user and mints a token, or answers `401` when there's no session. ```ts // app/api/wattfare-token/route.ts import { wf } from "@/lib/wattfare"; import { auth } from "@/lib/auth"; // your auth export const POST = wf.sessionHandler(async () => { const session = await auth(); return session?.user?.id ?? null; // null -> 401 }); ``` ## 4 · The streaming chat route Standard AI SDK `streamText` — the only Wattfare-specific lines are `wf.user(id)` and the error guards. ```ts // app/api/chat/route.ts import { streamText } from "ai"; import { isNotConnected, isBudgetExceeded } from "wattfare/server"; import { wf } from "@/lib/wattfare"; import { auth } from "@/lib/auth"; export async function POST(req: Request) { const session = await auth(); if (!session?.user?.id) return new Response("Unauthorized", { status: 401 }); const { messages } = await req.json(); const ai = wf.user(session.user.id); try { const result = streamText({ model: ai.model("anthropic/claude-sonnet-4"), messages, }); return result.toTextStreamResponse(); } catch (err) { if (isNotConnected(err)) return Response.json({ error: "not_connected" }, { status: 402 }); if (isBudgetExceeded(err)) return Response.json({ error: "budget_exceeded" }, { status: 402 }); throw err; } } ``` ## 5 · The provider The provider is a client component, so put it behind a `"use client"` boundary and render it in your root layout. ```tsx // app/wattfare-provider.tsx "use client"; import { WattfareProvider } from "wattfare/react"; export function Providers({ children }: { children: React.ReactNode }) { return ( {children} ); } ``` ```tsx // app/layout.tsx import { Providers } from "./wattfare-provider"; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {children} ); } ``` ## 6 · The chat UI Read connection state from `useWattfare()`, gate the UI on `connected`, and refresh budget after each reply. ```tsx // app/chat.tsx "use client"; import { useWattfare } from "wattfare/react"; import { useChat } from "ai/react"; // optional — any chat UI works export function Chat() { const { connected, connect, remainingUsd, loading, refresh } = useWattfare(); const { messages, input, handleInputChange, handleSubmit } = useChat({ api: "/api/chat", onFinish: () => refresh(), // keep budget in sync after each reply }); if (loading) return

Loading…

; if (!connected) return ; return (
Budget: {remainingUsd === null ? "∞" : `$${remainingUsd.toFixed(2)}`}
{messages.map((m) =>

{m.role}: {m.content}

)}
); } ``` > **That's the whole integration** > > Two route handlers, one provider, one component. Swap the model id to change models; swap `auth()` for your provider. Everything else is plain Next.js + AI SDK. ## Keep going - [Error recovery](https://wattfare.com/docs/guides/error-recovery) — Make the 402 / 429 branches above into real, friendly UI states. - [Budget UI patterns](https://wattfare.com/docs/guides/budget-ui) — A polished budget bar and depletion warnings to drop into the header. ======================================================================== URL: https://wattfare.com/docs/guides/error-recovery # Error recovery > Every failure mode maps to a specific, recoverable piece of UI. This guide turns Wattfare's typed errors into states your users actually understand — connect, top up, slow down, or retry. ## The principle Don't surface raw errors. Each Wattfare error code corresponds to exactly one user action. Your job is to translate code → action, on the server first (so the browser gets a stable code) and in the UI second (so the user gets a button). | code | Meaning | The one action | | --- | --- | --- | | not_connected | No budget connected yet | Show “Connect AI budget” | | funding_invalid | Their funding key broke | Show “Reconnect” (same button) | | budget_exceeded | Monthly cap reached | Show usage; link to raise the cap | | rate_limited | Too many requests | Back off / “slow down” message | | network / upstream | Transient failure | Retry with backoff | | auth | Your credentials are wrong | Log & alert — it's your bug, not theirs | ## Step 1 — normalise on the server Recover the typed error and re-emit its JSON, so the browser always receives `{ error: { code, message } }` with a meaningful status instead of a generic 500. ```ts // app/api/chat/route.ts — translate every failure into a stable client code. import { toWattfareError } from "wattfare/server"; try { const result = streamText({ model: ai.model(MODEL), messages }); return result.toTextStreamResponse(); } catch (err) { const wf = toWattfareError(err); if (!wf) throw err; // not ours — let the platform 500 it // Re-emit the typed error so the browser gets a code it can switch on. return Response.json(wf.toJSON(), { status: wf.status || 502 }); } ``` ## Step 2 — switch in the UI Read the code and move to a named state. No string-matching on messages. ```ts async function send(prompt: string) { const res = await fetch("/api/chat", { method: "POST", body: JSON.stringify({ prompt }), }); if (!res.ok) { const { error } = await res.json(); switch (error?.code) { case "not_connected": case "funding_invalid": return setState({ kind: "needs_connect" }); case "budget_exceeded": return setState({ kind: "out_of_budget" }); case "rate_limited": return setState({ kind: "slow_down" }); default: return setState({ kind: "error", message: error?.message }); } } // …stream the successful response } ``` ## Connect vs. reconnect `not_connected` and `funding_invalid` both mean “the user must act before this works,” which is why `isNotConnected()` matches both. The same connect button fixes both — only the copy changes. ```tsx // funding_invalid means the *user's* funding broke (revoked key, no credit). // It's caught by isNotConnected, so the same "Connect" button fixes it — // just lead with a clearer message. const { connect } = useWattfare(); if (state.kind === "needs_connect") { return (

Reconnect your AI budget to continue.

); } ``` ## Out of budget On `budget_exceeded`, show the user where they stand and where to fix it. Pull the numbers from `status.usage.monthlyUsd` and `status.limits.monthlyUsd`, and link them to their [budget page](https://wattfare.com/account) to raise the cap. > **Warn before you block** > > Don't wait for the hard 402. When `remainingUsd` drops below a threshold (say $1), surface a gentle “running low” hint so the wall never comes as a surprise. See [Budget UI patterns](https://wattfare.com/docs/guides/budget-ui). ## Retrying transient failures Only `rate_limited`, `network`, and `upstream` are worth retrying — the rest need a human. Back off exponentially with a little jitter: ```ts // Exponential backoff for transient failures (network / upstream / rate limit). async function withRetry(fn: () => Promise, tries = 3): Promise { let lastErr: unknown; for (let i = 0; i < tries; i++) { try { return await fn(); } catch (err) { lastErr = err; const wf = toWattfareError(err); const retryable = wf?.code === "rate_limited" || wf?.code === "network" || wf?.code === "upstream"; if (!retryable) throw err; await new Promise((r) => setTimeout(r, 2 ** i * 400 + Math.random() * 200)); } } throw lastErr; } ``` > **Never retry a 402** > > `not_connected`, `funding_invalid`, and `budget_exceeded` won't resolve by retrying — they need the user. Retrying just burns rate-limit budget and delays the fix. ======================================================================== URL: https://wattfare.com/docs/guides/budget-ui # Budget UI patterns > The budget is the part of your product users feel. These patterns — a clear connect button, a live budget bar, depletion warnings, and an obvious disconnect — turn “AI billing” into something users trust. ## The connect button The first impression. Make it unmistakable and reassuring: a prominent CTA, the voltage bolt for recognition, and a disabled/loading state while the popup is open. ```tsx function ConnectButton() { const { connect, loading } = useWattfare(); return ( ); } ``` > **Set expectations in the label** > > “Connect AI budget” reads better than “Sign in” or “Authorize.” Users are about to set a spending cap — name the thing they control. ## The budget bar Once connected, show remaining budget inline — ideally somewhere persistent like a header. Drive the fill from `usage` vs. `limits`, and flag a low state early. ```tsx function BudgetBar() { const { connected, status, remainingUsd } = useWattfare(); if (!connected) return null; const cap = status.limits?.monthlyUsd ?? null; const used = status.usage.monthlyUsd; const pct = cap ? Math.min((used / cap) * 100, 100) : 0; const low = remainingUsd !== null && remainingUsd < 1; return (
{remainingUsd !== null ? `$${remainingUsd.toFixed(2)} left` : "Unlimited"} {low && · running low}
); } ``` > **Keep it fresh** > > `remainingUsd` only updates when you call `refresh()`. Refresh after each inference request (e.g. AI SDK's `onFinish`) so the bar tracks reality. ## Depletion: warn, then gate Two thresholds, two behaviours: - **Low (e.g. < $1 left)** — a soft inline warning. Don't block anything yet. - **Empty (≤ $0)** — disable the input and explain why, with a link to raise the cap. A disabled field with a reason beats a request that fails after the user hits enter. ```tsx function ChatInput() { const { connected, remainingUsd } = useWattfare(); const depleted = remainingUsd !== null && remainingUsd <= 0; if (!connected) return ; return (
{depleted && (

You've hit your monthly cap. Raise it to keep going.

)}
); } ``` ## Always offer disconnect Control is the whole pitch. Give users an obvious way to disconnect and to see what they've spent. It costs you nothing and it's the difference between “this app can spend my money” and “I let this app spend my money, and I can stop it.” ```tsx function BudgetMenu() { const { connected, disconnect, status } = useWattfare(); if (!connected) return null; return (
Budget · {status.usage.monthlyUsd.toFixed(2){"}"} used Manage budget
); } ``` ## A trust checklist - **Show the number.** Always display remaining budget once connected — never hide it. - **Warn before the wall.** A low-budget hint prevents the surprise of a hard stop. - **Explain the block.** When gated, say why and link the fix — don't just disable silently. - **Make leaving easy.** A visible disconnect builds more trust than any copy could. - **Mirror the cap.** The number you request in `sessionHandler` is what users approve — keep your UI honest about it. > **Tip** > > Pair this with [Error recovery](https://wattfare.com/docs/guides/error-recovery): the budget bar handles the happy path, and the error states handle the moment a call is refused. ======================================================================== URL: https://wattfare.com/docs/api-reference # HTTP API > The raw REST API the SDK wraps. You rarely call this directly — the SDK handles auth, token caching, and error typing — but it's here for debugging, non-JS stacks, and custom integrations. > **Base URL** > > All endpoints live under `https://wattfare.com/api/v1/`. The proxy is OpenAI-compatible, so existing OpenAI client libraries can talk to `/chat/completions` with the right headers. ## Authentication Two bearer schemes, for two trust levels: | Scheme | Header | Used by | | --- | --- | --- | | **Secret key** | `Authorization: Bearer sk_live_{appId}_{secret}` | Server-to-server: sessions, status, chat completions | | **Session token** | `Authorization: Bearer {jwt}` | Browser: the `/connections` endpoints (via the SDK) | Session tokens are JWTs with a 10-minute TTL carrying the app id, your user id, and the requested limits. Mint them with `POST /sessions`; the SDK caches and refreshes them for you. ## POST /sessions Mints a frontend session token. Requires secret-key auth. ```http POST /api/v1/sessions Authorization: Bearer sk_live_{appId}_{secret} Content-Type: application/json { "appUserId": "user_123", "requestLimit": { "monthlyUsd": 20 } // optional } ``` ```json 200 OK { "token": "eyJhbGciOi…", // short-lived JWT for the frontend "expiresAt": 1718400000 // unix seconds (default TTL: 10 min) } ``` Rate limited per app via `RL_SESSIONS` — 120 requests/minute. ## /connections The consent surface, authed with a **session token** (not the secret key). The SDK's `connect()`, `status()`, and `disconnect()` map onto these. ```http # Approve / update — body { monthlyUsd? }, returns ConnectionStatus POST /api/v1/connections Authorization: Bearer {session-jwt} # Read current status GET /api/v1/connections Authorization: Bearer {session-jwt} # Revoke DELETE /api/v1/connections Authorization: Bearer {session-jwt} ``` | Method | Body | Returns | | --- | --- | --- | | POST | `{ monthlyUsd? }` | ConnectionStatus — approve or update the cap | | GET | — | ConnectionStatus | | DELETE | — | 204 — revoke the connection | ## GET /status Server-side connection check, keyed by your user id. Requires secret-key auth. ```http GET /api/v1/status?appUserId=user_123 Authorization: Bearer sk_live_{appId}_{secret} ``` ```json 200 OK { "connected": true, "limits": { "monthlyUsd": 10 }, "usage": { "monthlyUsd": 3.20 }, "remainingUsd": 6.80 } ``` ## POST /chat/completions The OpenAI-compatible inference proxy. Requires secret-key auth **and** the `x-wattfare-user` header so Wattfare resolves the right connection and budget. ```http POST /api/v1/chat/completions Authorization: Bearer sk_live_{appId}_{secret} x-wattfare-user: user_123 Content-Type: application/json { "model": "openai/gpt-4o-mini", "messages": [{ "role": "user", "content": "Hello" }], "stream": true } ``` Responds with the standard OpenAI chat-completion format — JSON, or an SSE stream when `"stream": true`. Usage is metered automatically: cost is read from the provider's usage object and stored per-user, per-period. The stream is teed, never buffered. ```bash curl https://wattfare.com/api/v1/chat/completions \ -H "Authorization: Bearer $WATTFARE_SECRET_KEY" \ -H "x-wattfare-user: user_123" \ -H "Content-Type: application/json" \ -d '{ "model": "openai/gpt-4o-mini", "messages": [{ "role": "user", "content": "Say hi" }] }' ``` > **Note** > > `model` accepts any [OpenRouter model id](https://openrouter.ai/models) — hundreds across Anthropic, OpenAI, Google, Meta, and open-weight providers. ## One-time grants For apps without a user system, grants provide a simpler flow: the browser requests a `grantToken` via the consent popup, and the backend redeems it with the `x-wattfare-grant` header. See [One-time grants](https://wattfare.com/docs/grants) for endpoints and examples. ## Errors Every error responds with the same body shape and a meaningful status: ```json { "error": { "code": "budget_exceeded", "message": "Spending cap reached for this period." } } ``` | Status | code | Meaning | | --- | --- | --- | | 400 | invalid_request | Malformed body or parameters. | | 401 | auth | Missing/invalid secret key or session token. | | 402 | not_connected | User hasn't connected a budget. | | 402 | budget_exceeded | Monthly cap reached. | | 402 | grant_invalid | The grant token is unknown, expired, fully used, or over its dollar cap. | | 402 | funding_invalid | User's funding source rejected upstream (reconnect needed). | | 429 | rate_limited | Per-user inference limit — 30 requests/min per app-user. | | 502 | upstream | The model provider returned an error. | ## Rate limits | Limiter | Scope | Limit | | --- | --- | --- | | RL_SESSIONS | per app | 120 / minute | | RL_CHAT | per app + user | 30 / minute | > **Prefer the SDK** > > The SDK already implements token caching, the popup origin handshake, streaming, and typed error recovery over these endpoints. Hand-rolling against the raw API means re-implementing all of that — only do it when the SDK genuinely can't run in your environment.