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.
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 |
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:
- Verifies your secret key and resolves the user's connection + funding.
- Checks the cap — if spend ≥ cap, it throws
budget_exceededbefore the request leaves. - Decrypts the user's funding key per request (so revocation is always live) and forwards to OpenRouter.
- Tees the response: bytes stream to you untouched while a parallel reader extracts the final
usageand records cost. The response is never buffered.
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.
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 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.