@usequota/core
v3.0.0
Published
Framework-agnostic SDK for Quota — typed HTTP client, OAuth, webhooks, and SSE streaming
Readme
@usequota/core
Framework-agnostic SDK for Quota — the AI credit wallet and multi-provider inference API.
Works anywhere JavaScript runs: Node.js, Deno, Bun, Cloudflare Workers, React Native. Typed HTTP client, OAuth token management, webhook verification (via Web Crypto), and SSE streaming with zero framework dependencies.
Using Next.js? Use
@usequota/nextjsinstead — it adds route handlers, middleware, React hooks, and cookie-based auth on top of this package.
Full docs: usequota.ai/docs
The three integration paths
Pick which one fits your app, then come back here for the SDK pieces.
- Developer API key — you cover the AI cost. Use an
sk-quota-…key. Simplest path; best for prototypes and internal tools. - Sign in with Quota — Quota provides identity and a wallet per user. End users pay from their own balance.
- Connect Quota Wallet — your app already has auth; you attach a Quota wallet to each existing user. End users pay from their own balance.
This SDK supports all three. For the Sign-in / Connect-Wallet flows specifically, the OAuth round-trip is yours to wire up — see the docs recipes — but token storage, refresh, and signed requests are all here.
Install
npm install @usequota/coreCreate a client
API key auth (developer-pays)
import { QuotaClient } from "@usequota/core";
const client = new QuotaClient({
apiKey: process.env.QUOTA_API_KEY,
});
const user = await client.getMe();
console.log(user.email);OAuth mode (user-pays — Sign in with Quota or Connect Wallet)
import { QuotaClient } from "@usequota/core";
const client = new QuotaClient({
accessToken: storedAccessToken,
refreshToken: storedRefreshToken,
clientId: process.env.QUOTA_CLIENT_ID,
clientSecret: process.env.QUOTA_CLIENT_SECRET,
onTokenRefresh: async (tokens) => {
// Persist refreshed tokens to your database
await db.updateTokens(userId, tokens);
},
});When the access token expires, the client automatically refreshes it using the refresh token and calls onTokenRefresh so you can persist the new tokens.
Check balance
const { balance } = await client.getBalance();
if (balance < 100) {
console.log("Low credits -- purchase more at usequota.ai");
}Stream AI completions with parseSSEStream
import { QuotaClient, parseSSEStream } from "@usequota/core";
const client = new QuotaClient({ apiKey: process.env.QUOTA_API_KEY });
const response = await fetch(`${client.baseUrl}/v1/chat/completions`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.QUOTA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "gpt-4o",
messages: [{ role: "user", content: "Explain quantum computing" }],
stream: true,
}),
});
for await (const chunk of parseSSEStream<{
choices: Array<{ delta: { content?: string } }>;
}>(response.body!)) {
process.stdout.write(chunk.choices[0].delta.content ?? "");
}Verify webhooks
import { verifyWebhookSignature, parseWebhook } from "@usequota/core";
// Low-level: verify a signature yourself
const isValid = await verifyWebhookSignature({
payload: rawBody,
signature: request.headers.get("x-quota-signature")!,
secret: process.env.QUOTA_WEBHOOK_SECRET!,
});
// High-level: parse and verify in one step
const event = await parseWebhook(request, process.env.QUOTA_WEBHOOK_SECRET!);
console.log(event.type, event.data);createWebhookHandler
For a complete request-in, response-out handler:
import { createWebhookHandler } from "@usequota/core";
const handler = createWebhookHandler(process.env.QUOTA_WEBHOOK_SECRET!, {
"balance.low": async (event) => {
await sendLowBalanceEmail(event.data.user_id);
},
"user.connected": async (event) => {
await syncUser(event.data);
},
});
// Use with any framework that gives you a Request object:
// Deno: Deno.serve(handler)
// Bun: Bun.serve({ fetch: handler })
// Cloudflare Workers: export default { fetch: handler }Error handling
All Quota errors extend QuotaError and can be narrowed with instanceof:
import {
QuotaClient,
QuotaInsufficientCreditsError,
QuotaNotConnectedError,
QuotaTokenExpiredError,
QuotaRateLimitError,
} from "@usequota/core";
const client = new QuotaClient({ apiKey: process.env.QUOTA_API_KEY });
try {
await client.getBalance();
} catch (error) {
if (error instanceof QuotaInsufficientCreditsError) {
// 402 -- user needs more credits
console.log(`Balance: ${error.balance}, Required: ${error.required}`);
} else if (error instanceof QuotaNotConnectedError) {
// 401 -- user hasn't connected their Quota account
console.log(error.hint); // "Connect your Quota account to use this feature"
} else if (error instanceof QuotaTokenExpiredError) {
// 401 -- token expired and could not be refreshed
// Re-authenticate the user
} else if (error instanceof QuotaRateLimitError) {
// 429 -- wait and retry
console.log(`Retry after ${error.retryAfter} seconds`);
}
}OAuth token exchange
For apps that implement the OAuth flow manually:
import { exchangeCodeForToken, refreshAccessToken } from "@usequota/core";
// After receiving the authorization code from the OAuth redirect:
const tokens = await exchangeCodeForToken({
code: authorizationCode,
redirectUri: "https://yourapp.com/callback",
clientId: process.env.QUOTA_CLIENT_ID!,
clientSecret: process.env.QUOTA_CLIENT_SECRET!,
});
// Later, refresh the token:
const newTokens = await refreshAccessToken({
refreshToken: tokens.refresh_token,
clientId: process.env.QUOTA_CLIENT_ID!,
clientSecret: process.env.QUOTA_CLIENT_SECRET!,
});Token storage adapters
Implement QuotaTokenStorage to plug in your own persistence layer:
import { QuotaTokenStorage } from "@usequota/core";
const redisTokenStorage: QuotaTokenStorage = {
async getTokens(request) {
const userId = getUserIdFromRequest(request);
const data = await redis.get(`quota:${userId}`);
return data ? JSON.parse(data) : null;
},
async setTokens(tokens, request) {
const userId = getUserIdFromRequest(request);
await redis.set(`quota:${userId}`, JSON.stringify(tokens));
},
async deleteTokens(request) {
const userId = getUserIdFromRequest(request);
await redis.del(`quota:${userId}`);
},
};InMemoryTokenStorage is included for testing and prototyping.
Contributing
Any change under src/ (except tests) requires a version bump in
package.json. See agent/WORKFLOWS.md → SDK Versioning
for semver guidance and the bump+publish sequence. A CI check fails any
PR that touches src without bumping. @usequota/nextjs depends on this
package — major bumps here require a matching dep-range update in
packages/quota-nextjs/package.json.
License
MIT
