Wattfare / Docs
Dashboard
Guides

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.

.env.local
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
// 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
// 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
// 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
// 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
// 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
// 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>
  );
}

Keep going