API Reference
Complete reference for Scrawn SDK and gRPC API
SDK API
scrawn()
import { scrawn } from "@scrawn/core";
const biller = scrawn(config: ScrawnInitConfig): ScrawnCreates a billing instance.
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
apiKey | string | — | Your Scrawn API key (starts with scrn_) |
baseURL | string | — | Backend server URL |
secure | boolean | true | Whether to use TLS for gRPC. Set to false for local dev |
tags | readonly string[] | — | Known tag names for compile-time type checking on biller.tag() |
expressions | readonly string[] | — | Known expression names for compile-time type checking on biller.expr() |
retryCount | number | 2 | Auto-retry attempts on transient network errors |
Example:
const biller = scrawn({
apiKey: process.env.SCRAWN_KEY as `scrn_${string}`,
baseURL: process.env.SCRAWN_BASE_URL || "http://localhost:8069",
tags: ["PREMIUM_CALL", "EXTRA_FEE"] as const,
});basicUsageEventConsumer
biller.basicUsageEventConsumer(payload: EventPayload, options?: { onError? }): Promise<void>Track a billable usage event.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
userId | string | Yes | Unique user identifier |
debit | number | PriceExpr | Yes | Cents or pricing expression (tag(), mul(), etc.) |
metadata | object | No | Arbitrary metadata |
Returns: Promise<void>
Example:
// Direct amount — 500 cents = $5.00
await biller.basicUsageEventConsumer({
userId: "user-123",
debit: 500,
});
// Price tag
await biller.basicUsageEventConsumer({
userId: "user-123",
debit: tag("PREMIUM_CALL"),
});
// Pricing expression
await biller.basicUsageEventConsumer({
userId: "user-123",
debit: mul(tag("PER_CALL"), 3),
});Error callback:
await biller.basicUsageEventConsumer(
{ userId: "user-123", debit: 500 },
{
onError: (error, context) => {
if (error.retryable && context) await context.retry();
},
}
);middlewareEventConsumer
biller.middlewareEventConsumer(config: MiddlewareEventConfig):
(req, res, next) => Promise<void>Creates Express-compatible middleware that automatically tracks usage for API requests.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
extractor | (req) => { userId, debit } | null | Yes | Extracts billing info from each request |
whitelist | string[] | No | Endpoint patterns to track (* single, ** multi segment) |
blacklist | string[] | No | Endpoint patterns to exclude |
Example:
app.use(biller.middlewareEventConsumer({
extractor: (req) => ({
userId: req.headers["x-user-id"] as string,
debit: req.body?.cost || 1,
}),
blacklist: ["/health", "/api/collect-payment"],
}));aiTokenStreamConsumer
biller.aiTokenStreamConsumer(
stream: AsyncIterable<AITokenUsagePayload>
): Promise<StreamEventResponse | undefined>Stream AI token usage events for billing. Supports a fork mode for simultaneous user streaming.
Parameters:
| Parameter | Type | Description |
|---|---|---|
stream | AsyncIterable<AITokenUsagePayload> | Token usage events to bill |
config.return | boolean | If true, returns a forked stream alongside the response |
Example:
async function* generateUsage() {
yield {
userId: "user-123",
model: "gpt-4",
inputTokens: 100,
outputTokens: 50,
inputDebit: 3,
outputDebit: 6,
};
}
const response = await biller.aiTokenStreamConsumer(generateUsage());biller.ai()
Wraps the Vercel AI SDK so every LLM call auto-bills:
import * as ai from "ai";
const aii = biller.ai(ai, {
inputDebit: mul(biller.tag("GPT_INPUT"), inputTokens()),
outputDebit: mul(biller.tag("GPT_OUTPUT"), outputTokens()),
});
const result = await aii.streamText({
userId: "user-123",
model: openai("gpt-4o-mini"),
prompt: "Hello",
});biller.trackAI()
Manually track AI token usage from an onFinish callback:
await biller.trackAI(userId, model, usage, overrides, defaults);collectPayment
biller.collectPayment(userId: string): Promise<string>Generate a checkout link for a user to pay their outstanding balance.
Example:
const checkoutLink = await biller.collectPayment("user-123");
res.redirect(checkoutLink);gRPC API
Scrawn backend exposes three gRPC services for direct integration.
EventService
registerEvent
Registers a billable SDK call event.
Request:
message RegisterEventRequest {
EventType type = 1;
string userId = 2;
oneof data {
SDKCall sdkCall = 3;
}
}
message SDKCall {
SDKCallType sdkCallType = 1;
oneof debit {
float amount = 2;
string tag = 3;
}
}
enum EventType {
EVENT_TYPE_UNSPECIFIED = 0;
SDK_CALL = 1;
}
enum SDKCallType {
SDKCallType_UNSPECIFIED = 0;
RAW = 1;
MIDDLEWARE_CALL = 2;
}Response:
message RegisterEventResponse {
string random = 1;
}Example (TypeScript):
import { createPromiseClient } from '@connectrpc/connect';
import { EventService } from './gen/event/v1/event_connect';
import { EventType, SDKCallType } from './gen/event/v1/event_pb';
const client = createPromiseClient(EventService, transport);
const response = await client.registerEvent({
type: EventType.SDK_CALL,
userId: 'user_123',
data: {
case: 'sdkCall',
value: {
sdkCallType: SDKCallType.RAW,
debit: {
case: 'amount',
value: 100,
},
}
}
});
console.log('Status:', response.random);Authentication:
- Requires valid API key in
Authorizationheader as Bearer token - API key must start with
scrn_
AuthService
createAPIKey
Creates a new API key for authenticated users.
Request:
message CreateAPIKeyRequest {
string name = 1;
int64 expiresIn = 2; // expiration time in seconds from now
}Response:
message CreateAPIKeyResponse {
string apiKeyId = 1;
string apiKey = 2;
string name = 3;
string createdAt = 4;
string expiresAt = 5;
}Example:
import { createPromiseClient } from '@connectrpc/connect';
import { AuthService } from './gen/auth/v1/auth_connect';
const client = createPromiseClient(AuthService, transport);
const response = await client.createAPIKey({
name: 'Production Key',
expiresIn: BigInt(365 * 24 * 60 * 60) // 1 year in seconds
});
console.log('API Key:', response.apiKey);
console.log('API Key ID:', response.apiKeyId);
console.log('Expires At:', response.expiresAt);Authentication:
- Requires valid API key in
Authorizationheader as Bearer token - New API key will be generated with
scrn_prefix
Notes:
- The API key is returned only once during creation
- Store the API key securely - it cannot be retrieved later
- The backend stores a hashed version of the API key
PaymentService
createCheckoutLink
Generates a Lemon Squeezy checkout link for a user to pay their outstanding balance.
Request:
message CreateCheckoutLinkRequest {
string userId = 1;
}Response:
message CreateCheckoutLinkResponse {
string checkoutLink = 1;
}Example:
import { createPromiseClient } from '@connectrpc/connect';
import { PaymentService } from './gen/payment/v1/payment_connect';
const client = createPromiseClient(PaymentService, transport);
const response = await client.createCheckoutLink({
userId: 'user_123'
});
console.log('Checkout Link:', response.checkoutLink);
// Redirect user to this URL to complete paymentHow it works:
- The backend calculates the user's outstanding balance automatically
- A custom-priced checkout session is created via Lemon Squeezy
- The checkout link is valid for immediate use
- User ID is passed to the payment provider for tracking
Notes:
- Payment amount is calculated from stored debit events
- Uses Lemon Squeezy as the payment provider
- No need to specify amount - it's calculated server-side
Error Handling
SDK Errors
The SDK throws standard JavaScript Error objects with descriptive messages.
Validation Errors:
try {
await biller.basicUsageEventConsumer({
userId: "", // Invalid: empty string
debit: -5, // Invalid: negative number
});
} catch (error) {
console.error(error.message);
// "Payload validation failed: userId: must be a non-empty string, debit: must be a positive number"
}gRPC Connection Errors:
try {
await biller.basicUsageEventConsumer({
userId: "user-123",
debit: 100,
});
} catch (error) {
console.error("Failed to register event:", error.message);
}Backend Error Types
AuthError
Authentication and authorization errors.
Error Types:
| Type | Code | Description | Solution |
|---|---|---|---|
MISSING_HEADER | UNAUTHENTICATED | No Authorization header provided | Include Authorization: Bearer <api_key> header |
INVALID_HEADER_FORMAT | UNAUTHENTICATED | Authorization header format is wrong | Use format: Bearer scrn_... |
INVALID_API_KEY | UNAUTHENTICATED | API key is invalid or not found | Check API key is correct and exists |
EXPIRED_API_KEY | UNAUTHENTICATED | API key has expired | Create a new API key |
REVOKED_API_KEY | UNAUTHENTICATED | API key has been revoked | Create a new API key |
DATABASE_ERROR | INTERNAL | Failed to verify API key | Contact support |
UNKNOWN | INTERNAL | Unknown authentication error | Contact support |
EventError
Event registration and processing errors.
Error Types:
| Type | Code | Description | Solution |
|---|---|---|---|
INVALID_PAYLOAD | INVALID_ARGUMENT | Event payload is invalid | Check payload structure matches schema |
UNSUPPORTED_EVENT_TYPE | INVALID_ARGUMENT | Event type is not supported | Use SDK_CALL event type |
VALIDATION_FAILED | INVALID_ARGUMENT | Zod schema validation failed | Review validation error details |
INVALID_USER_ID | INVALID_ARGUMENT | User ID format is invalid | Provide non-empty string for userId |
MISSING_DATA | INVALID_ARGUMENT | Required field is missing | Include all required fields |
INVALID_DATA_FORMAT | INVALID_ARGUMENT | Data format doesn't match expected | Check field types and formats |
SERIALIZATION_ERROR | INTERNAL | Failed to store event | Contact support |
UNKNOWN | INTERNAL | Unknown event processing error | Contact support |
PaymentError
Payment and checkout link creation errors.
Error Types:
| Type | Code | Description | Solution |
|---|---|---|---|
INVALID_USER_ID | INVALID_ARGUMENT | User ID is invalid | Provide valid non-empty userId |
VALIDATION_FAILED | INVALID_ARGUMENT | Request validation failed | Check request parameters |
MISSING_API_KEY | FAILED_PRECONDITION | Lemon Squeezy API key not configured | Configure LEMON_SQUEEZY_API_KEY env var |
MISSING_STORE_ID | FAILED_PRECONDITION | Lemon Squeezy store ID not configured | Configure LEMON_SQUEEZY_STORE_ID env var |
MISSING_VARIANT_ID | FAILED_PRECONDITION | Lemon Squeezy variant ID not configured | Configure LEMON_SQUEEZY_VARIANT_ID env var |
PRICE_CALCULATION_FAILED | INTERNAL | Failed to calculate user's balance | Ensure user has registered events |
STORAGE_ADAPTER_FAILED | INTERNAL | Failed to retrieve data from storage | Check database connectivity |
LEMON_SQUEEZY_API_ERROR | INTERNAL | Lemon Squeezy API call failed | Check Lemon Squeezy service status |
INVALID_CHECKOUT_RESPONSE | INTERNAL | Invalid response from payment provider | Contact support |
CHECKOUT_CREATION_FAILED | INTERNAL | Failed to create checkout link | Review error details |
CONFIGURATION_ERROR | FAILED_PRECONDITION | Payment system misconfigured | Check environment configuration |
UNKNOWN | INTERNAL | Unknown payment error | Contact support |
Error Response Format
All backend errors follow the gRPC Connect error format:
{
"code": "invalid_argument",
"message": "Event validation failed: userId: must be a non-empty string",
"details": []
}Common gRPC Codes:
INVALID_ARGUMENT- Invalid request parametersUNAUTHENTICATED- Authentication failedINTERNAL- Internal server errorFAILED_PRECONDITION- Server not properly configured
Types
EventPayload
type Debit<TTag extends string = string> = number | PriceExpr<TTag>;
interface EventPayload<TTag extends string = string> {
userId: string;
debit: Debit<TTag>;
metadata?: Record<string, unknown>;
}The debit field accepts:
- A number (cents) — e.g.
500for $5.00 - A
tag()reference — e.g.tag("PREMIUM_CALL") - A pricing expression — e.g.
mul(tag("PER_CALL"), 3)
AITokenUsagePayload
interface AITokenUsagePayload<TTag extends string = string> {
userId: string;
model: string;
inputTokens: number;
outputTokens: number;
inputDebit: Debit<TTag>;
outputDebit: Debit<TTag>;
provider?: string;
inputCacheTokens?: number;
inputCacheDebit?: Debit<TTag>;
outputCacheTokens?: number;
outputCacheDebit?: Debit<TTag>;
metadata?: Record<string, unknown>;
}Middleware Types
interface MiddlewareRequest {
method?: string;
url?: string;
path?: string;
body?: any;
headers?: Record<string, string | string[] | undefined>;
query?: Record<string, any>;
params?: Record<string, any>;
[key: string]: any;
}
interface MiddlewareResponse {
status?: (code: number) => MiddlewareResponse;
json?: (data: any) => void;
send?: (data: any) => void;
[key: string]: any;
}
type MiddlewareNext = (error?: any) => void;
type PayloadExtractor = (req: MiddlewareRequest) =>
EventPayload | Promise<EventPayload> | null | Promise<null>;
interface MiddlewareEventConfig {
extractor: PayloadExtractor;
whitelist?: string[];
blacklist?: string[];
}Utilities
matchPath
matchPath(path: string, pattern: string): booleanMatches a path against a pattern with wildcard support.
Parameters:
| Parameter | Type | Description |
|---|---|---|
path | string | The path to match |
pattern | string | The pattern with optional wildcards |
Wildcard Patterns:
*matches single path segment**matches multiple segments
Example:
import { matchPath } from '@scrawn/core';
matchPath('/api/users', '/api/*'); // true
matchPath('/api/users/123', '/api/*'); // false
matchPath('/api/users/123', '/api/**'); // true
matchPath('/api/v1/users', '/api/v1/*'); // trueAuthentication
All API requests require authentication via Bearer token in the Authorization header.
API Key Format:
- Must start with
scrn_prefix - Example:
scrn_1234567890abcdef
Using the SDK:
The SDK handles authentication automatically using the apiKey provided to the scrawn() factory:
const biller = scrawn({
apiKey: process.env.SCRAWN_KEY as `scrn_${string}`,
baseURL: "http://localhost:8069",
});Direct gRPC Calls:
When making direct gRPC calls without the SDK, include the API key in the Authorization header:
Authorization: Bearer scrn_your_api_keyAuthentication Flow:
- Backend receives request with Bearer token
- Auth interceptor validates the API key format
- API key hash is verified against database
- API key expiration is checked
- API key ID is added to request context for downstream handlers
Security Notes:
- API keys are hashed using SHA-256 before storage
- Original API keys are never stored in the database
- API keys are cached in memory for performance (with TTL)
- Expired API keys are automatically rejected