Integrate Autumn billing—define features/plans in autumn.config.ts, use autumn-js SDK for credit checks/tracking, manage the atmn CLI for push/pull. Use when working on billing, pricing, credits, plan gating, or metered usage.
autumn-js SDK and atmn CLIUse this when you need to:
autumn.config.tsautumn-js SDK.atmn CLI.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 | — | 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.
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 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.
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.
atmnbunx atmn login # OAuth login, saves keys to .env
bunx 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';
bunx atmn preview # Dry run — shows what would change
bunx atmn push # Push to sandbox (interactive confirmation)
bunx atmn push --prod # Push to production
bunx atmn push --yes # Auto-confirm (for CI/CD)
bunx atmn pull # Pull remote config, generate SDK types
bunx atmn customers # Browse customers
bunx atmn plans # Browse plans
bunx atmn features # Browse features
bunx 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 --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 model-costs.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/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) |