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