with one click
autumn
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.
Menu
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.
| 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-js SDK and atmn CLIWhen Autumn Product, ProductItem, Feature, Entitlement, Customer, CustomerProduct, pricing, credit checks, SDK calls, CLI behavior, or usage-event semantics affect correctness, use source-backed grounding before relying on memory. If DeepWiki MCP is available, ask a narrow question against useautumn/autumn; if it is unavailable or the repo is not indexed, use upstream source or official docs directly. For autumn-js SDK or atmn CLI behavior, verify against the installed package, useautumn/typescript, or official docs. Treat DeepWiki as orientation, then verify decisive details against local billing code, installed types, source, or official docs before changing code.
Skip DeepWiki for hosted-only Epicenter billing boundaries already documented in AGENTS.md and below.
Use this when you need to:
autumn.config.ts.autumn-js SDK.atmn CLI.check() errors at each endpoint. AI credit charging should fail closed before expensive provider calls.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.
// 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', ... })
| 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 | n/a | Pool that maps to metered features via creditSchema | AI credits |
boolean | n/a | 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.
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 },
],
});
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.
// Runtime cost table (in worker/billing/ai-model-pricing.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.
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.
Plans with addOn: true stack on top of any plan. No group conflict.
autoEnablePlans 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.
reset.interval vs price.intervalThe 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.
// 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.
autumn-jsimport { Autumn } from 'autumn-js';
const autumn = new Autumn({ secretKey: env.AUTUMN_SECRET_KEY });
Stateless: safe to create per-request. No connection pooling needed.
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.
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.
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.
atmnbun x atmn login # OAuth login, saves keys to .env
bun x atmn env # Verify org and environment
autumn.config.ts at the project root. Defines features and plans using atmn builders:
import { feature, item, plan } from 'atmn';
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
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
| 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 --env=dev --path=/api -- wrangler dev
Run after authGuard, before any billing-gated routes:
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.
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);
atmn push.getOrCreate must be awaited: Fire-and-forget will cause check() to fail with "customer not found."featureId in check() is always 'ai_usage': The credit cost varies per model via dynamic requiredBalance, not featureId.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.sendEvent: true deducts atomically: Don't call track() separately for the happy path. Only use track({ value: -1 }) for refunds.autoEnable triggers on customer creation: Not on first check(). Ensure the middleware calls getOrCreate before checking.ai_usage) with creditCost: 1 and dynamic requiredBalance per model. Per-model costs live in worker/billing/ai-model-pricing.ts, not autumn.config.ts. This avoids cluttering the dashboard with dozens of features.| File | Purpose |
|---|---|
apps/api/autumn.config.ts | Feature, credit system, and plan definitions |
apps/api/worker/billing/autumn.ts | createAutumnClient(env) SDK adapter and provider error mapping |
apps/api/worker/billing/ai-model-pricing.ts | Model string to proportional credit cost mapping |
apps/api/worker/billing/service.ts | Billing domain operations, reservations, dashboard DTOs, and storage sync |
apps/api/worker/billing/policies.ts | AI credit charging and asset storage billing policies |
apps/api/worker/billing/routes.ts | /api/billing/* routes and billing auth mount |
apps/api/worker/index.ts | Cloud Worker composition and billing policy wiring |
Epicenter UI component selection and composition patterns for Svelte apps using packages/ui. Use when choosing or reviewing @epicenter/ui components, loading states, empty states, skeletons, spinners, command empty states, action pending UI, table/list no-row states, button tooltips, wrapper minimization, or replacing ad hoc UI such as Loading... text, custom loading dots, raw animate-pulse placeholders, or one-off centered status markup.
Visual design direction for new or redesigned frontend surfaces: layout, typography, color, motion, and anti-generic aesthetics. Use when designing pages, dashboards, posters, marketing surfaces, or exploratory UI variations.
CSS and Tailwind, cn(), flex layouts. Use for "style this", "fix the CSS", "add classes", "not scrolling", "overflow", Tailwind utilities.
Review UI code for Web Interface Guidelines compliance. Use when asked to "review my UI", "check accessibility", "audit design", "review UX", or "check my site against best practices".
Drizzle ORM patterns: schema definitions, Drizzle Kit migrations, query builders, type branding, custom types, SQLite, Postgres, D1, and Turso/libSQL boundaries. Use when mentioning Drizzle, drizzle-orm, DB schemas, migrations, branded column types, or typed SQL queries.
Encryption: HKDF, XChaCha20-Poly1305, blob formats, key hierarchy/rotation. Use for "encrypt this", "key management", crypto primitives, EncryptedBlob.