@crediball/sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,49 @@
1
+ # @crediball/sdk
2
+
3
+ TypeScript SDK for [Crediball](https://github.com/filipporezzadore/Crediball) —
4
+ usage-based credit billing for AI apps. Charge users per action; let prices live
5
+ in the dashboard, not your code.
6
+
7
+ ```bash
8
+ npm install @crediball/sdk
9
+ ```
10
+
11
+ ## Quick start (server-side)
12
+
13
+ ```ts
14
+ import { Crediball } from "@crediball/sdk";
15
+
16
+ const meter = new Crediball({
17
+ apiKey: process.env.CREDIBALL_API_KEY!, // cb_live_…
18
+ baseUrl: process.env.CREDIBALL_API_URL!, // https://crediball.app/api
19
+ });
20
+
21
+ // Seed prices once (create-if-absent — never overwrites dashboard edits):
22
+ await meter.defineActions([{ action: "generate_poem", credits: 5 }]);
23
+
24
+ // Wrap an AI call: deduct → run → auto-refund if it throws.
25
+ const poem = await meter.wrapAction("u123", "generate_poem", () => callAI());
26
+ ```
27
+
28
+ ## API
29
+
30
+ | Method | Description |
31
+ |---|---|
32
+ | `new Crediball({ apiKey, baseUrl?, fetch? })` | Create a client. `baseUrl` is required (or set `CREDIBALL_API_URL`). |
33
+ | `defineActions([{ action, credits, label?, force? }])` | Seed action prices. Create-if-absent unless `force`. |
34
+ | `charge({ userId, action, amount?, metadata? })` | Deduct credits. Price from the catalog unless `amount` is given. |
35
+ | `wrapAction(userId, action, fn)` | Charge by action, run `fn`, refund on failure. **Recommended.** |
36
+ | `wrap(userId, amount, fn, action?)` | Same with an explicit hardcoded amount (advanced). |
37
+ | `topup({ userId, amount, action?, metadata? })` | Add credits to a user's balance. |
38
+ | `getBalance(userId)` | Return the user's current balance. |
39
+ | `listActions()` | List the app's action catalog. |
40
+
41
+ ### Errors
42
+ - `InsufficientCreditsError` — balance too low (catch it to show your paywall).
43
+ - `CrediballApiError` — any other non-2xx response (`.status`, `.body`).
44
+
45
+ ## Principles
46
+ - **Don't hardcode prices.** Reference actions by key; set credits in the dashboard.
47
+ - **Server-side only.** Never ship your `cb_live_` key to the browser.
48
+ - Pair with [`@crediball/react`](https://www.npmjs.com/package/@crediball/react)
49
+ for drop-in, unbranded credit UI.
@@ -0,0 +1,151 @@
1
+ /**
2
+ * @crediball/sdk
3
+ *
4
+ * Usage-based credit billing for AI apps. Initialize once with your app API key,
5
+ * then charge users per action or wrap an AI call so credits are deducted
6
+ * automatically.
7
+ *
8
+ * import { Crediball } from "@crediball/sdk";
9
+ *
10
+ * const meter = new Crediball({ apiKey: "cb_live_xxx" });
11
+ *
12
+ * await meter.charge({ userId: "u123", amount: 5, action: "ai_request" });
13
+ *
14
+ * const result = await meter.wrap("u123", 3, async () => callAI());
15
+ */
16
+ export interface CrediballOptions {
17
+ /** Your app API key, e.g. "cb_live_xxx". Find it in the Crediball dashboard. */
18
+ apiKey: string;
19
+ /**
20
+ * Base URL of your Crediball API, e.g. "https://crediball.app/api".
21
+ * Required — falls back to the CREDIBALL_API_URL env var if omitted.
22
+ */
23
+ baseUrl?: string;
24
+ /** Optional custom fetch implementation (e.g. for Node < 18 or testing). */
25
+ fetch?: typeof fetch;
26
+ /**
27
+ * Escape hatch to allow constructing the client in a browser. DON'T use this in
28
+ * production: your `cb_live_` key is a secret and must never reach the browser.
29
+ * By default the constructor throws if it detects a browser environment.
30
+ */
31
+ allowBrowser?: boolean;
32
+ }
33
+ export interface ChargeParams {
34
+ userId: string;
35
+ /** The action key — priced in the dashboard. Recommended over a hardcoded amount. */
36
+ action: string;
37
+ /**
38
+ * Optional. When omitted, the price is taken from the dashboard action catalog
39
+ * (recommended — lets the price change without a redeploy). When provided, it
40
+ * overrides the catalog (advanced / dynamic pricing).
41
+ */
42
+ amount?: number;
43
+ metadata?: Record<string, unknown>;
44
+ }
45
+ export interface ActionDef {
46
+ /** Stable action key referenced from code (e.g. "generate_poem"). */
47
+ action: string;
48
+ /** Default price in credits. */
49
+ credits: number;
50
+ /** Human-friendly name shown in the dashboard. */
51
+ label?: string;
52
+ /**
53
+ * By default this is create-if-absent: an existing action's dashboard-owned
54
+ * price is left untouched. Set force=true to overwrite from code (advanced).
55
+ */
56
+ force?: boolean;
57
+ }
58
+ export interface ActionRecord {
59
+ action: string;
60
+ label: string;
61
+ credits: number;
62
+ needsPricing: boolean;
63
+ active: boolean;
64
+ }
65
+ export interface TopupParams {
66
+ userId: string;
67
+ amount: number;
68
+ action?: string;
69
+ metadata?: Record<string, unknown>;
70
+ }
71
+ export interface BalanceResult {
72
+ userId: string;
73
+ balance: number;
74
+ }
75
+ export interface ChargeResult {
76
+ userId: string;
77
+ balance: number;
78
+ charged: number;
79
+ ledgerId: string;
80
+ }
81
+ export interface TopupResult {
82
+ userId: string;
83
+ balance: number;
84
+ added: number;
85
+ ledgerId: string;
86
+ }
87
+ /** Thrown when a charge or wrap fails because the user lacks sufficient credits. */
88
+ export declare class InsufficientCreditsError extends Error {
89
+ readonly code = "insufficient_credits";
90
+ readonly balance: number;
91
+ readonly required: number;
92
+ constructor(balance: number, required: number);
93
+ }
94
+ /** Thrown for any non-2xx API response that isn't an insufficient-credits 402. */
95
+ export declare class CrediballApiError extends Error {
96
+ readonly status: number;
97
+ readonly body: unknown;
98
+ constructor(status: number, message: string, body: unknown);
99
+ }
100
+ export declare class Crediball {
101
+ private readonly apiKey;
102
+ private readonly baseUrl;
103
+ private readonly fetchImpl;
104
+ constructor(options: CrediballOptions);
105
+ /**
106
+ * Deduct credits from a user for an action. With no `amount`, the price comes
107
+ * from the dashboard action catalog. Throws InsufficientCreditsError if too low.
108
+ */
109
+ charge(params: ChargeParams): Promise<ChargeResult>;
110
+ /**
111
+ * Define (seed) prices for actions. Create-if-absent by default: this never
112
+ * overwrites a price a human set in the dashboard unless `force` is true.
113
+ * Call this once at setup; change prices later in the dashboard.
114
+ *
115
+ * await meter.defineActions([
116
+ * { action: "generate_poem", credits: 5 },
117
+ * { action: "refine_poem", credits: 3 },
118
+ * ]);
119
+ */
120
+ defineActions(defs: ActionDef[]): Promise<ActionRecord[]>;
121
+ /** Define a single action price (see defineActions). */
122
+ defineAction(def: ActionDef): Promise<ActionRecord>;
123
+ /** List the app's action catalog (keys, prices, labels). */
124
+ listActions(): Promise<ActionRecord[]>;
125
+ /** Add credits to a user's balance (e.g. after a purchase or grant). */
126
+ topup(params: TopupParams): Promise<TopupResult>;
127
+ /** Get a user's current credit balance. */
128
+ getBalance(userId: string): Promise<BalanceResult>;
129
+ /**
130
+ * Wrap an AI function so credits are charged before it runs.
131
+ *
132
+ * await meter.wrap("u123", 3, async () => callAI());
133
+ *
134
+ * Behavior:
135
+ * - checks balance and deducts `amount` first (throws InsufficientCreditsError if too low),
136
+ * - executes `fn` only if the charge succeeded,
137
+ * - if `fn` throws, refunds the charge so the user isn't billed for a failed call,
138
+ * - logs every transaction in the ledger.
139
+ */
140
+ wrap<T>(userId: string, amount: number, fn: () => Promise<T> | T, action?: string): Promise<T>;
141
+ /**
142
+ * Like `wrap`, but the price comes from the dashboard action catalog instead of
143
+ * a hardcoded amount — the recommended pattern.
144
+ *
145
+ * await meter.wrapAction("u123", "generate_poem", () => callAI());
146
+ */
147
+ wrapAction<T>(userId: string, action: string, fn: () => Promise<T> | T): Promise<T>;
148
+ private runWithRefund;
149
+ private request;
150
+ }
151
+ export default Crediball;
package/dist/index.js ADDED
@@ -0,0 +1,186 @@
1
+ /**
2
+ * @crediball/sdk
3
+ *
4
+ * Usage-based credit billing for AI apps. Initialize once with your app API key,
5
+ * then charge users per action or wrap an AI call so credits are deducted
6
+ * automatically.
7
+ *
8
+ * import { Crediball } from "@crediball/sdk";
9
+ *
10
+ * const meter = new Crediball({ apiKey: "cb_live_xxx" });
11
+ *
12
+ * await meter.charge({ userId: "u123", amount: 5, action: "ai_request" });
13
+ *
14
+ * const result = await meter.wrap("u123", 3, async () => callAI());
15
+ */
16
+ /** Read the API base URL from the environment when available (Node/edge), else undefined. */
17
+ function envBaseUrl() {
18
+ const env = globalThis
19
+ .process?.env;
20
+ return env?.CREDIBALL_API_URL;
21
+ }
22
+ /** Thrown when a charge or wrap fails because the user lacks sufficient credits. */
23
+ export class InsufficientCreditsError extends Error {
24
+ constructor(balance, required) {
25
+ super(`Insufficient credits: balance ${balance}, required ${required}.`);
26
+ this.code = "insufficient_credits";
27
+ this.name = "InsufficientCreditsError";
28
+ this.balance = balance;
29
+ this.required = required;
30
+ }
31
+ }
32
+ /** Thrown for any non-2xx API response that isn't an insufficient-credits 402. */
33
+ export class CrediballApiError extends Error {
34
+ constructor(status, message, body) {
35
+ super(message);
36
+ this.name = "CrediballApiError";
37
+ this.status = status;
38
+ this.body = body;
39
+ }
40
+ }
41
+ export class Crediball {
42
+ constructor(options) {
43
+ if (!options || !options.apiKey) {
44
+ throw new Error("Crediball requires an apiKey.");
45
+ }
46
+ // Guard against the catastrophic mistake of shipping the secret key to the
47
+ // browser. The key authorizes charges — it must only live on your server.
48
+ if (!options.allowBrowser &&
49
+ typeof window !== "undefined" &&
50
+ typeof document !== "undefined") {
51
+ throw new Error("Crediball must run server-side: your cb_live_ key is a secret and must " +
52
+ "never reach the browser. Initialize it in a server route/action with " +
53
+ "process.env.CREDIBALL_API_KEY. (Set allowBrowser:true only if you " +
54
+ "really know what you're doing.)");
55
+ }
56
+ this.apiKey = options.apiKey;
57
+ const base = options.baseUrl ?? envBaseUrl();
58
+ if (!base) {
59
+ throw new Error("Crediball requires a baseUrl. Pass `baseUrl` (e.g. " +
60
+ '"https://crediball.app/api") or set CREDIBALL_API_URL.');
61
+ }
62
+ this.baseUrl = base.replace(/\/$/, "");
63
+ const f = options.fetch ?? globalThis.fetch;
64
+ if (!f) {
65
+ throw new Error("No fetch implementation available. Pass `fetch` in CrediballOptions.");
66
+ }
67
+ this.fetchImpl = f.bind(globalThis);
68
+ }
69
+ /**
70
+ * Deduct credits from a user for an action. With no `amount`, the price comes
71
+ * from the dashboard action catalog. Throws InsufficientCreditsError if too low.
72
+ */
73
+ async charge(params) {
74
+ return this.request("POST", "/credits/charge", params);
75
+ }
76
+ /**
77
+ * Define (seed) prices for actions. Create-if-absent by default: this never
78
+ * overwrites a price a human set in the dashboard unless `force` is true.
79
+ * Call this once at setup; change prices later in the dashboard.
80
+ *
81
+ * await meter.defineActions([
82
+ * { action: "generate_poem", credits: 5 },
83
+ * { action: "refine_poem", credits: 3 },
84
+ * ]);
85
+ */
86
+ async defineActions(defs) {
87
+ const res = await this.request("POST", "/actions", { actions: defs });
88
+ return res.actions;
89
+ }
90
+ /** Define a single action price (see defineActions). */
91
+ async defineAction(def) {
92
+ const [record] = await this.defineActions([def]);
93
+ return record;
94
+ }
95
+ /** List the app's action catalog (keys, prices, labels). */
96
+ async listActions() {
97
+ const res = await this.request("GET", "/actions");
98
+ return res.actions;
99
+ }
100
+ /** Add credits to a user's balance (e.g. after a purchase or grant). */
101
+ async topup(params) {
102
+ return this.request("POST", "/credits/topup", params);
103
+ }
104
+ /** Get a user's current credit balance. */
105
+ async getBalance(userId) {
106
+ return this.request("GET", `/user/${encodeURIComponent(userId)}/balance`);
107
+ }
108
+ /**
109
+ * Wrap an AI function so credits are charged before it runs.
110
+ *
111
+ * await meter.wrap("u123", 3, async () => callAI());
112
+ *
113
+ * Behavior:
114
+ * - checks balance and deducts `amount` first (throws InsufficientCreditsError if too low),
115
+ * - executes `fn` only if the charge succeeded,
116
+ * - if `fn` throws, refunds the charge so the user isn't billed for a failed call,
117
+ * - logs every transaction in the ledger.
118
+ */
119
+ async wrap(userId, amount, fn, action = "ai_request") {
120
+ const { charged } = await this.charge({ userId, amount, action });
121
+ return this.runWithRefund(userId, charged, action, fn);
122
+ }
123
+ /**
124
+ * Like `wrap`, but the price comes from the dashboard action catalog instead of
125
+ * a hardcoded amount — the recommended pattern.
126
+ *
127
+ * await meter.wrapAction("u123", "generate_poem", () => callAI());
128
+ */
129
+ async wrapAction(userId, action, fn) {
130
+ const { charged } = await this.charge({ userId, action });
131
+ return this.runWithRefund(userId, charged, action, fn);
132
+ }
133
+ async runWithRefund(userId, charged, action, fn) {
134
+ try {
135
+ return await fn();
136
+ }
137
+ catch (err) {
138
+ // Compensating refund: the AI call failed, so give the credits back.
139
+ if (charged > 0) {
140
+ try {
141
+ await this.topup({
142
+ userId,
143
+ amount: charged,
144
+ action: `${action}:refund`,
145
+ metadata: { refund: true, reason: "wrapped_fn_threw" },
146
+ });
147
+ }
148
+ catch {
149
+ // Swallow refund errors so the original failure surfaces to the caller.
150
+ }
151
+ }
152
+ throw err;
153
+ }
154
+ }
155
+ async request(method, path, body) {
156
+ const res = await this.fetchImpl(`${this.baseUrl}${path}`, {
157
+ method,
158
+ headers: {
159
+ Authorization: `Bearer ${this.apiKey}`,
160
+ ...(body ? { "Content-Type": "application/json" } : {}),
161
+ },
162
+ body: body ? JSON.stringify(body) : undefined,
163
+ });
164
+ const text = await res.text();
165
+ const data = text ? safeJson(text) : undefined;
166
+ if (res.status === 402) {
167
+ const d = (data ?? {});
168
+ throw new InsufficientCreditsError(d.balance ?? 0, d.required ?? 0);
169
+ }
170
+ if (!res.ok) {
171
+ const message = data?.error ??
172
+ `Crediball API error (${res.status})`;
173
+ throw new CrediballApiError(res.status, message, data);
174
+ }
175
+ return data;
176
+ }
177
+ }
178
+ function safeJson(text) {
179
+ try {
180
+ return JSON.parse(text);
181
+ }
182
+ catch {
183
+ return text;
184
+ }
185
+ }
186
+ export default Crediball;
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@crediball/sdk",
3
+ "version": "0.1.0",
4
+ "description": "TypeScript SDK for Crediball — usage-based credit billing for AI apps.",
5
+ "license": "MIT",
6
+ "author": "Filippo Rezzadore <filipporezzadore@gmail.com>",
7
+ "homepage": "https://crediball.app",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/filipporezzadore/Crediball.git",
11
+ "directory": "packages/sdk"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/filipporezzadore/Crediball/issues"
15
+ },
16
+ "keywords": [
17
+ "billing",
18
+ "credits",
19
+ "usage-based",
20
+ "metering",
21
+ "pricing",
22
+ "ai",
23
+ "saas"
24
+ ],
25
+ "type": "module",
26
+ "sideEffects": false,
27
+ "main": "./dist/index.js",
28
+ "module": "./dist/index.js",
29
+ "types": "./dist/index.d.ts",
30
+ "exports": {
31
+ ".": {
32
+ "types": "./dist/index.d.ts",
33
+ "import": "./dist/index.js"
34
+ }
35
+ },
36
+ "files": [
37
+ "dist"
38
+ ],
39
+ "engines": {
40
+ "node": ">=18"
41
+ },
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "scripts": {
46
+ "build": "tsc -p tsconfig.json",
47
+ "dev": "tsc -p tsconfig.json --watch",
48
+ "prepublishOnly": "npm run build"
49
+ },
50
+ "devDependencies": {
51
+ "typescript": "^5.6.3"
52
+ }
53
+ }