autumn
$
npx mdskill add EpicenterHQ/epicenter/autumnManages billing, credits, and plan access using Autumn in Epicenter
- Solves tasks related to billing configuration, credit checks, and plan access control
- Uses `autumn-js` SDK, `atmn` CLI, and Autumn billing platform APIs
- Validates billing config and enforces plan rules via `autumn.config.ts` definitions
- Applies usage tracking, refunds, and entitlements directly to user accounts
SKILL.md
.github/skills/autumnView on GitHub ↗
---
name: autumn
description: 'Autumn billing in Epicenter: `autumn.config.ts`, `autumn-js` credit checks, `atmn` CLI, plan gates, and metered AI usage. Use when changing billing, pricing, credits, plan access, refunds, or usage events.'
metadata:
author: epicenter
version: '1.2'
---
# Autumn Billing Integration Guide
## Reference Repositories
- [Autumn](https://github.com/useautumn/autumn) — Usage-based billing platform
- [Autumn TypeScript SDK + CLI](https://github.com/useautumn/typescript) — `autumn-js` SDK and `atmn` CLI
- [Autumn Docs](https://docs.useautumn.com)
---
## When to Apply This Skill
Use this when you need to:
- Define or modify features, credit systems, or plans in `autumn.config.ts`.
- Add credit checks or usage tracking via the `autumn-js` SDK.
- Gate API endpoints behind billing (free tier limits, paid plan access).
- Push/pull billing config with the `atmn` CLI.
- Debug billing issues (insufficient credits, customer sync, refunds).
## Domain Model Checks
- Use Autumn's current nouns precisely: Feature, Entitlement, Product, ProductItem, Price, Customer, and CustomerProduct.
- Validate ProductItem shapes before pushing config. Most failures come from invalid interval combinations, missing linked features, or price/reset variants that do not match the feature type.
- Decide fail-open versus fail-closed behavior for `check()` errors at each endpoint. AI credit charging should fail closed before expensive provider calls.
- If Stripe webhooks or CustomerProduct state transitions are touched, make the handler idempotent around retries.
---
## Naming Conventions (CRITICAL)
**All IDs use `snake_case`.** This is Autumn's explicit convention.
Feature IDs should be **descriptive** (not abstract tier numbers) and **ecosystem-scoped** (not tied to a single app feature like "chat"). The metered features represent model cost tiers that any AI feature can consume.
```typescript
// CORRECT — descriptive, ecosystem-scoped
feature({ id: 'ai_fast', ... })
feature({ id: 'ai_standard', ... })
feature({ id: 'ai_premium', ... })
plan({ id: 'pro', ... })
plan({ id: 'credit_top_up', ... })
// WRONG — tied to a single feature ("chat")
feature({ id: 'ai_chat_fast', ... })
// WRONG — abstract tier numbers (Autumn convention prefers descriptive)
feature({ id: 'ai_tier_1', ... })
// WRONG — kebab-case
feature({ id: 'ai-fast', ... })
```
---
## Feature Types
| Type | `consumable` | Use Case | Example |
|------|-------------|----------|---------|
| `metered` | `true` | Usage that resets periodically (messages, API calls) | AI model invocations |
| `metered` | `false` | Persistent allocation (seats, storage) | Team seats |
| `credit_system` | — | Pool that maps to metered features via `creditSchema` | AI credits |
| `boolean` | — | Feature flag on/off | Advanced analytics |
**Credit systems** require linked `metered` features with `consumable: true`. Each linked feature has a `creditCost` defining how many credits one unit consumes.
```typescript
export const aiUsage = feature({
id: 'ai_usage',
name: 'AI Usage',
type: 'metered',
consumable: true,
});
export const aiCredits = feature({
id: 'ai_credits',
name: 'AI Credits',
type: 'credit_system',
creditSchema: [
{ meteredFeatureId: 'ai_usage', creditCost: 1 },
],
});
```
### Proportional Billing
Instead of multiple metered features with fixed `creditCost` per tier, use a **single metered feature** with `creditCost: 1` and vary the `requiredBalance` at runtime.
This gives per-model cost precision without cluttering the Autumn dashboard with dozens of features.
**How it works**: Autumn's `check()` with `sendEvent: true` uses `requiredBalance` as the deduction amount. With `creditCost: 1`, passing `requiredBalance: 5` deducts exactly 5 credits from the pool.
```typescript
// Runtime cost table (in model-costs.ts, not autumn.config.ts)
const MODEL_CREDITS: Record<string, number> = {
'gpt-4o-mini': 1, // cheap model = 1 credit
'claude-sonnet-4': 5, // mid-range = 5 credits
'claude-opus-4': 30, // expensive = 30 credits
};
// Dynamic deduction
const credits = MODEL_CREDITS[model];
await autumn.check({
customerId,
featureId: 'ai_usage', // single feature for all models
requiredBalance: credits, // varies per model
sendEvent: true,
});
```
**Refund on error**: Use `track({ featureId: 'ai_usage', value: -credits })` to refund the exact amount.
**Blocking expensive models**: Omit them from `MODEL_CREDITS`. Unknown models → `getModelCredits()` returns `undefined` → 400.
---
## Plan Structure
### Groups
Plans in the same `group` are **mutually exclusive**. Subscribing to a new plan in the same group replaces the old one. Autumn handles the Stripe subscription swap automatically.
- **Upgrade** (free → pro): Immediate swap with proration.
- **Downgrade** (pro → free): Scheduled for end of billing cycle.
### Add-ons
Plans with `addOn: true` **stack** on top of any plan. No group conflict.
### `autoEnable`
Plans with `autoEnable: true` are auto-assigned when a customer is created via `customers.getOrCreate()`. Use for free tiers. Only allowed on plans with no `price`.
### Plan items: `reset.interval` vs `price.interval`
The **intervals** are mutually exclusive, not `reset` and `price` themselves. A `PlanItem` is one of three variants:
**`PlanItemWithReset`** — Has `reset.interval`. If `price` is also present, it CANNOT have `price.interval`. Use for free allocations that reset periodically, optionally with one-time overage pricing.
**`PlanItemWithPriceInterval`** — Has `price.interval`. CANNOT have `reset`. The `price.interval` determines BOTH the billing cycle AND when the `included` balance resets for consumable features. Use for paid plans with usage-based overage.
**`PlanItemNoReset`** — No `reset`. Use for continuous-use features like seats, or boolean features.
```typescript
// Free plan — reset only, no price
// `reset.interval` controls when the 50 included credits refresh
item({ featureId: aiCredits.id, included: 50, reset: { interval: 'month' } })
// Paid plan — price.interval handles both billing AND reset
// The 2000 included credits reset monthly via `price.interval: 'month'`
// Overage beyond 2000 billed at $1/100 credits
item({
featureId: aiCredits.id,
included: 2000,
price: { amount: 1, billingUnits: 100, billingMethod: 'usage_based', interval: 'month' },
})
```
**Key insight**: For paid plans, `included` + `price.interval` implies monthly reset. The `included` field's Zod description: "Balance resets to this each interval for consumable features." You do NOT need a separate `reset` field on paid plan items.
---
## SDK: `autumn-js`
### Initialization
```typescript
import { Autumn } from 'autumn-js';
const autumn = new Autumn({ secretKey: env.AUTUMN_SECRET_KEY });
```
Stateless—safe to create per-request. No connection pooling needed.
### Customer Sync (MUST be blocking)
```typescript
await autumn.customers.getOrCreate({
customerId: userId,
name: userName ?? undefined,
email: userEmail ?? undefined,
});
```
**This call MUST be awaited (blocking).** Autumn's `/check` endpoint does not auto-create customers. The customer must exist before any `check()` call.
### Credit Check
```typescript
const credits = getModelCredits(data.model);
const { allowed, balance } = await autumn.check({
customerId: userId,
featureId: 'ai_usage',
requiredBalance: credits,
sendEvent: true,
properties: { model, provider },
});
if (!allowed) {
// Return 402 with balance info
}
```
**featureId** is always 'ai_usage'. The credit cost varies per model via the dynamic requiredBalance.
### Refund on Error
```typescript
await autumn.track({
customerId: userId,
featureId: 'ai_usage',
value: -credits, // Negative value = refund
});
```
Use when the operation fails after credits were already deducted (e.g., AI stream errors). Typically pushed to an `afterResponse` queue to avoid blocking the error response.
---
## CLI: `atmn`
### Setup
```bash
bun x atmn login # OAuth login, saves keys to .env
bun x atmn env # Verify org and environment
```
### Config File
`autumn.config.ts` at the project root. Defines features and plans using `atmn` builders:
```typescript
import { feature, item, plan } from 'atmn';
```
### Push/Pull
```bash
bun x atmn preview # Dry run, shows what would change
bun x atmn push # Push to sandbox (interactive confirmation)
bun x atmn push --prod # Push to production
bun x atmn push --yes # Auto-confirm (for CI/CD)
bun x atmn pull # Pull remote config, generate SDK types
```
### Data Inspection
```bash
bun x atmn customers # Browse customers
bun x atmn plans # Browse plans
bun x atmn features # Browse features
bun x atmn events # Browse usage events
```
---
## Environment & Secrets
| Key | Environment | Prefix |
|-----|-------------|--------|
| `AUTUMN_SECRET_KEY` | Sandbox (test) | `am_sk_test_...` |
| `AUTUMN_SECRET_KEY` | Production | `am_sk_prod_...` |
Use the **same key name** in both environments. Let your secrets manager (Infisical, etc.) swap the value per environment. Don't create separate key names for sandbox vs prod.
For Cloudflare Workers: `wrangler secret put AUTUMN_SECRET_KEY`
For local dev with Infisical: secrets are auto-injected via `infisical run --path=/api -- wrangler dev`
---
## Middleware Pattern (Cloudflare Workers + Hono)
### Ensure Customer Exists
Run after `authGuard`, before any billing-gated routes:
```typescript
app.use('/ai/*', async (c, next) => {
const autumn = createAutumn(c.env);
await autumn.customers.getOrCreate({
customerId: c.var.user.id,
name: c.var.user.name ?? undefined,
email: c.var.user.email ?? undefined,
});
await next();
});
```
**Why inline?** Cloudflare Workers don't expose `env` at module scope. The Autumn client must be created inside the request handler.
### Credit Gate in Handler
```typescript
const credits = getModelCredits(data.model);
if (!credits) return c.json(error, 400);
const { allowed, balance } = await autumn.check({
customerId: c.var.user.id,
featureId: 'ai_usage',
requiredBalance: credits,
sendEvent: true,
});
if (!allowed) return c.json(error, 402);
```
---
## Stripe Integration
- **Sandbox**: Built-in Stripe test account. No setup needed.
- **Production**: Connect via Dashboard → Integrations → Stripe (OAuth recommended).
- Autumn creates Stripe products/prices automatically when you `atmn push`.
- Autumn is the source of truth for customer state; Stripe handles payments.
---
## Common Gotchas
1. **`getOrCreate` must be awaited** — Fire-and-forget will cause `check()` to fail with "customer not found."
2. **`featureId` in `check()` is always 'ai_usage'** — The credit cost varies per model via dynamic `requiredBalance`, not featureId.
3. **`reset.interval` and `price.interval` are mutually exclusive** — not `reset` and `price` themselves. A `PlanItemWithReset` CAN have a `price`, but that price cannot have an `interval`. For paid plans, `price.interval` handles both billing and balance reset.
4. **`sendEvent: true` deducts atomically** — Don't call `track()` separately for the happy path. Only use `track({ value: -1 })` for refunds.
5. **All IDs are snake_case** — Autumn's pricing agent convention. Don't use kebab-case.
6. **`autoEnable` triggers on customer creation** — Not on first `check()`. Ensure the middleware calls `getOrCreate` before checking.
7. **Multiple keys per environment** — Autumn supports multiple active secret keys for rotation. Generate new key → update secrets → revoke old key.
8. **Use proportional billing** — One metered feature (`ai_usage`) with `creditCost: 1` and dynamic `requiredBalance` per model. Per-model costs live in model-costs.ts, not autumn.config.ts. This avoids cluttering the dashboard with dozens of features.
---
## Project Files
| File | Purpose |
|------|---------|
| `apps/api/autumn.config.ts` | Feature, credit system, and plan definitions |
| `apps/api/src/autumn.ts` | `createAutumn(env)` factory for per-request SDK client |
| `apps/api/src/model-costs.ts` | Model string → proportional credit cost mapping |
| `apps/api/src/ai-chat.ts` | Credit check + refund logic for AI chat handler |
| `apps/api/src/app.ts` | Middleware wiring (ensureAutumnCustomer) |
---
## Resources
- [Autumn Docs](https://docs.useautumn.com)
- [Autumn Dashboard](https://app.useautumn.com)
- [GitHub: Autumn](https://github.com/useautumn/autumn)
- [GitHub: TypeScript SDK + CLI](https://github.com/useautumn/typescript)
- [API Keys](https://app.useautumn.com/dev?tab=api_keys)
More from EpicenterHQ/epicenter
- agent-goalWrite `/goal` prompts for long-running agent work in Codex or Claude Code. Use for slash goal, agent goal, durable objective, autonomous coding run.
- approachability-auditReview code as a new TypeScript developer. Use when code feels indirect, clever, hard to follow, or needs a pass on abstractions, names, first-read clarity.
- arktypeArktype: runtime validation, discriminated unions with .merge()/.or(), spread keys. Use when mentioning arktype, type(), union types, command/event schemas.
- attach-primitiveContract and invariants for `attach*` composition primitives in `packages/workspace` (side-effectful building blocks like attachIndexedDb, attachSqlite, attachBroadcastChannel, attachEncryption, attachTable, openCollaboration), and when to use `create*` (pure construction) instead. Use when writing or reviewing an `attach*` or `create*` function, naming a new workspace primitive, composing inside a workspace builder, or deciding whether a primitive registers listeners at call time.
- authEpicenter auth packages: `@epicenter/auth`, `@epicenter/auth-svelte`, OAuth sessions, identity state, auth-owned fetch/WebSocket, and workspace lifecycle binding. Use when editing Epicenter auth clients, session state, hosted sign-in, or auth/workspace integration.
- better-auth-best-practicesBetter Auth server/client setup: `auth.ts`, generated schema, DB adapters, sessions, cookies, env vars, and plugins. Use when mentioning Better Auth, betterauth, auth handlers, OAuth, email/password, or session configuration.
- better-auth-security-best-practicesBetter Auth security hardening: rate limits, secrets, CSRF, trusted origins, cookies, sessions, OAuth tokens, and audit logging. Use when reviewing auth security, brute-force protection, token handling, or deployment safety.
- change-proposalPresent proposed code changes visually before implementing. Use when: "show me options", "compare approaches", "what should we do", or when changes need before/after comparison.
- claude-code-consultUse this skill when the user asks to consult Claude, ask Claude Code, get another model's take, run a taste check, find cleaner options, or prepare a Claude prompt. Create a bounded second-opinion prompt or run a read-only Claude Code consult, then verify Claude's claims against local files.
- cloudflare-workersCloudflare Workers patterns for Worker runtime APIs, Durable Objects, KV, R2, D1, Queues, WebSockets, streaming responses, bindings, wrangler configuration, and deployment limits. Use when users mention Cloudflare Workers, Durable Objects, KV, R2, D1, Queues, wrangler, or edge runtime behavior.