Package Exports
- @onstoa/sdk
Readme
Stoa SDK
Usage-based billing SDK for AI applications. Wrap your AI provider calls to automatically meter usage and bill your users.
Installation
npm install @onstoa/sdkQuick Start
import OpenAI from "openai";
import { Stoa, StoaChargeRequiredError, StoaProvider } from "@onstoa/sdk";
// Reads STOA_API_KEY from environment
const stoa = new Stoa();
// Build provider clients in your app, then wrap them for metering.
const client = stoa.wrap(new OpenAI(), {
provider: StoaProvider.OpenAI,
userId: "user_123",
});
try {
const response = await client.chat.completions.create({
model: "gpt-4",
messages: [{ role: "user", content: "Hello!" }],
});
} catch (error) {
if (error instanceof StoaChargeRequiredError) {
const payment = await stoa.createPaymentSession({
userId: "user_123",
email: "ada@example.com",
returnUrl: "https://app.example.com/billing/stoa-return",
});
if (!payment.checkoutUrl) throw new Error("missing checkoutUrl");
redirectUserTo(payment.checkoutUrl);
} else {
throw error;
}
}Typical flow:
- call
stoa.createPaymentSession(...)when you want to start a payment flow - keep using your own app
userIdwhen wrapping provider clients - if a metered call raises
StoaChargeRequiredError, callstoa.createPaymentSession(...)
For existing users who predate your Stoa integration, call
stoa.createPaymentSession(...) before their first billable AI request. The
operation is idempotent.
Onboard Users
Stoa can stay invisible to the customer while still owning the canonical user and membership records.
Explicitly bootstrap a member
registerMember(...) is still available if you want to bootstrap a user eagerly
during signup.
import { Stoa } from "@onstoa/sdk";
const stoa = new Stoa({
apiKey: process.env.STOA_API_KEY,
baseUrl: process.env.STOA_BASE_URL,
});
const result = await stoa.registerMember({
registrationSecret: process.env.STOA_REGISTRATION_SECRET!,
userId: user.id,
email: user.email,
name: user.name,
avatarUrl: user.avatarUrl,
});
// Optional: store the membership ID for debugging or richer account UX.
await persistMembershipBinding({
userId: user.id,
membershipId: result.membershipId,
});Stoa registers the user for billing in your app.
Your app can continue billing that user with its own userId.
Start a payment flow
If you want an explicit pay or add-funds action in your product:
const payment = await stoa.createPaymentSession({
userId: user.id,
email: user.email,
returnUrl: "https://app.example.com/billing/stoa-return",
});Send the user to payment.checkoutUrl. The response also carries the payment ID.
Use a returnUrl route that restores request context and resumes the action
that triggered billing after the payment flow completes.
Recover from StoaChargeRequiredError
If a metered call fails because the user is not yet chargeable in Stoa, start a
payment flow explicitly with createPaymentSession(...):
import OpenAI from "openai";
import { StoaChargeRequiredError, StoaProvider } from "@onstoa/sdk";
const client = stoa.wrap(new OpenAI(), {
provider: StoaProvider.OpenAI,
userId: user.id,
});
try {
const response = await client.chat.completions.create({
model: "gpt-4",
messages: [{ role: "user", content: "Hello!" }],
});
} catch (error) {
if (error instanceof StoaChargeRequiredError) {
const payment = await stoa.createPaymentSession({
userId: user.id,
email: user.email,
// Restore context and resume this request after payment.
returnUrl: "https://app.example.com/billing/stoa-return",
});
if (!payment.checkoutUrl) throw new Error("missing checkoutUrl");
redirectUserTo(payment.checkoutUrl);
} else {
throw error;
}
}Supported Providers
OpenAI
import OpenAI from "openai";
import { StoaProvider } from "@onstoa/sdk";
const client = stoa.wrap(new OpenAI(), {
provider: StoaProvider.OpenAI,
userId: "user_123",
});
const response = await client.chat.completions.create({
model: "gpt-4",
messages: [{ role: "user", content: "Explain quantum computing" }],
});
const responseApiResult = await client.responses.create({
model: "gpt-4.1-mini",
input: "Explain quantum computing",
});
const embeddings = await client.embeddings.create({
model: "text-embedding-3-small",
input: "Hello world",
});Anthropic
import Anthropic from "@anthropic-ai/sdk";
import { StoaProvider } from "@onstoa/sdk";
const client = stoa.wrap(new Anthropic(), {
provider: StoaProvider.Anthropic,
userId: "user_123",
});
const response = await client.messages.create({
model: "claude-3-5-sonnet-20241022",
max_tokens: 1024,
messages: [{ role: "user", content: "Write a haiku about TypeScript" }],
});OpenRouter
OpenRouter uses the OpenAI SDK, so pass provider: StoaProvider.OpenRouter.
import OpenAI from "openai";
import { StoaProvider } from "@onstoa/sdk";
const client = stoa.wrap(
new OpenAI({
apiKey: process.env.OPENROUTER_API_KEY,
baseURL: "https://openrouter.ai/api/v1",
}),
{
provider: StoaProvider.OpenRouter,
userId: "user_123",
}
);Explicit Providers
stoa.wrap(...) requires a provider because constructor names are not stable
after bundling or minification. Use the exported StoaProvider values:
const client = stoa.wrap(customClient, {
provider: StoaProvider.OpenAI,
userId: "user_123",
});Environment Variables
| Variable | Required | Default | Description |
|---|---|---|---|
STOA_API_KEY |
Yes | - | Your Stoa application API key |
STOA_BASE_URL |
No | https://www.onstoa.com/api |
Stoa API base URL (for self-hosted or testing) |
STOA_REGISTRATION_SECRET |
No | - | Registration signing secret for onboarding users from your app |
Example .env
# Required
STOA_API_KEY=stoa_app_xxx
STOA_REGISTRATION_SECRET=stoa_reg_xxx
# Optional - override API endpoint
STOA_BASE_URL=https://www.onstoa.com/apiDocumentation
See docs.onstoa.com for full documentation.