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.
1 · Environment variables
The secret stays server-side; only the publishable key gets the NEXT_PUBLIC_ prefix.
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.
// 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.
// 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.
// 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.
// app/wattfare-provider.tsx
"use client";
import { WattfareProvider } from "wattfare/react";
export function Providers({ children }: { children: React.ReactNode }) {
return (
<WattfareProvider
publishableKey={process.env.NEXT_PUBLIC_WATTFARE_KEY!}
session="/api/wattfare-token"
>
{children}
</WattfareProvider>
);
} // app/layout.tsx
import { Providers } from "./wattfare-provider";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
} 6 · The chat UI
Read connection state from useWattfare(), gate the UI on connected, and
refresh budget after each reply.
// 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 <p>Loading…</p>;
if (!connected) return <button onClick={connect}>⚡ Connect AI budget</button>;
return (
<div>
<header>Budget: {remainingUsd === null ? "∞" : `$${remainingUsd.toFixed(2)}`}</header>
{messages.map((m) => <p key={m.id}><b>{m.role}:</b> {m.content}</p>)}
<form onSubmit={handleSubmit}>
<input value={input} onChange={handleInputChange} placeholder="Ask anything…" />
</form>
</div>
);
}