Wattfare / Docs
Dashboard
Getting started

Quickstart

From zero to a working “Connect AI budget” button in about five minutes. Five steps: install, get keys, mint sessions, connect, infer.

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.

terminal
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. You'll get two keys with very different jobs:

KeyFormatWhere it livesSecret?
Publishablepk_live_{appId}Browser — passed to the client / providerNo
Secretsk_live_{appId}_{secret}Backend only — mints sessions, runs inferenceYes

Store them as environment variables. The secret key must never reach the browser.

.env
# .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

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…).

app/api/wattfare-token/route.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:

App.tsx
import { WattfareProvider, useWattfare } from "wattfare/react";

function App() {
  return (
    <WattfareProvider
      publishableKey={import.meta.env.PUBLIC_WATTFARE_KEY}
      session="/api/wattfare-token"
    >
      <Chat />
    </WattfareProvider>
  );
}

function Chat() {
  const { connected, connect, remainingUsd, loading } = useWattfare();

  if (loading) return <p>Loading…</p>;
  if (!connected) return <button onClick={connect}>⚡ Connect AI budget</button>;

  return (
    <div>
      <p>Budget left: {remainingUsd === null ? "unlimited" : `$${remainingUsd}`}</p>
      {/* your chat UI */}
    </div>
  );
}

Not using React? The vanilla client is the same idea without the context:

connect.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();

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.

app/api/chat/route.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: