JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 30
  • Score
    100M100P100Q89214F
  • License MIT

TypeScript SDK for Stoa usage-based billing

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/sdk

Quick Start

import OpenAI from "openai";
import { Stoa, StoaChargeRequiredError } 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(), {
  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.createPayment({
      userId: "user_123",
      email: "ada@example.com",
      returnUrl: "https://app.example.com/billing/stoa-return",
    });
    redirectUserTo(payment.hostedUrl);
  } else {
    throw error;
  }
}

Typical flow:

  1. call stoa.createPayment(...) when you want to start a hosted payment flow
  2. keep using your own app userId when wrapping provider clients
  3. if a metered call raises StoaChargeRequiredError, call stoa.createPayment(...)

If you already have users who predate your Stoa integration, you can also add a one-time lazy fallback 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 hosted payment flow

If you want an explicit pay or add-funds action in your product:

const payment = await stoa.createPayment({
  userId: user.id,
  email: user.email,
  returnUrl: "https://app.example.com/billing/stoa-return",
});

Send the user to payment.hostedUrl. The response also carries the payment ID.

Recover from StoaChargeRequiredError

If a metered call fails because the user is not yet chargeable in Stoa, start a hosted payment flow explicitly with createPayment(...):

import OpenAI from "openai";
import { StoaChargeRequiredError } from "@onstoa/sdk";

const client = stoa.wrap(new 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.createPayment({
      userId: user.id,
      email: user.email,
      returnUrl: "https://app.example.com/billing/stoa-return",
    });

    redirectUserTo(payment.hostedUrl);
  } else {
    throw error;
  }
}

Supported Providers

OpenAI

import OpenAI from "openai";

const client = stoa.wrap(new OpenAI(), {
  userId: "user_123",
});

const response = await client.chat.completions.create({
  model: "gpt-4",
  messages: [{ role: "user", content: "Explain quantum computing" }],
});

const embeddings = await client.embeddings.create({
  model: "text-embedding-3-small",
  input: "Hello world",
});

Anthropic

import Anthropic from "@anthropic-ai/sdk";

const client = stoa.wrap(new 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: "openrouter" explicitly.

import OpenAI from "openai";

const client = stoa.wrap(
  new OpenAI({
    apiKey: process.env.OPENROUTER_API_KEY,
    baseURL: "https://openrouter.ai/api/v1",
  }),
  {
    provider: "openrouter",
    userId: "user_123",
  }
);

Provider Inference

stoa.wrap(...) infers "openai" and "anthropic" when the provider client is unambiguous. For OpenAI-compatible gateways, custom clients, or anything ambiguous, pass provider explicitly:

const client = stoa.wrap(customClient, {
  provider: "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/api

Documentation

See docs.onstoa.com for full documentation.