Usage-Based Billing
Track consumption and charge based on actual usage
Usage-based billing lets customers pay for what they consume. UseAutumn tracks usage through Convex actions and automatically enforces limits based on subscription tier.
Pro Tip
Use ctx.track()
to record consumption and ctx.check()
to verify remaining quota before performing expensive operations.
Feature Configuration
Define usage-tracked features in autumn.config.ts
with type: "single_use"
:
import { feature, product, featureItem, priceItem } from "atmn";
// Define usage-tracked features
export const apiCalls = feature({
id: "api_calls",
name: "API Calls",
type: "single_use", // Enables usage tracking
});
export const aiTokens = feature({
id: "ai_tokens",
name: "AI Tokens",
type: "single_use",
});
// Product with usage limits
export const paidPlan = product({
id: "starterapp-demo-paid",
name: "Paid Plan",
items: [
priceItem({
price: 10,
interval: "month",
}),
// 10,000 API calls per month
featureItem({
feature_id: apiCalls.id,
included_usage: 10_000,
interval: "month", // Resets monthly
}),
// 50,000 AI tokens per month
featureItem({
feature_id: aiTokens.id,
included_usage: 50_000,
interval: "month",
}),
],
});
Push configuration to UseAutumn:
npx atmn push
Heads up
Usage tracking only works for features defined in autumn.config.ts
. Push configuration before testing.
Track Usage in Convex
Use ctx.track()
in Convex actions to record consumption:
import { userAction } from "./_helpers/builders";
import { AppError } from "./_helpers/errors";
export const callExternalApi = userAction({
args: { endpoint: v.string() },
handler: async (ctx, { endpoint }) => {
// Check if user has remaining API calls
const result = await ctx.check({ featureId: "api_calls" });
if (!result.data?.allowed) {
throw new AppError("USAGE_EXCEEDED", "API call limit reached");
}
// Track usage (decrement available calls)
await ctx.track({ featureId: "api_calls", value: 1 });
// Perform the actual API call
const data = await fetch(endpoint);
return { success: true, data };
},
});
Track Variable Consumption
For features with variable costs (e.g., AI tokens), track actual usage:
import { userAction } from "./_helpers/builders";
import { AppError } from "./_helpers/errors";
export const generateText = userAction({
args: { prompt: v.string() },
handler: async (ctx, { prompt }) => {
// Check if user has token quota
const result = await ctx.check({ featureId: "ai_tokens" });
if (!result.data?.allowed) {
throw new AppError("USAGE_EXCEEDED", "Token limit reached. Upgrade for more.");
}
// Call AI service
const response = await callOpenAI(prompt);
const tokensUsed = response.usage.total_tokens;
// Track actual tokens consumed
await ctx.track({ featureId: "ai_tokens", value: tokensUsed });
return { text: response.choices[0].text };
},
});
Display Usage to Users
Show remaining quota using billing.useCustomer()
:
'use client';
import { useServices } from '~/lib/providers/services-context';
export function UsageMeter({ featureId }: { featureId: string }) {
const { billing } = useServices();
const { customer, loading } = billing.useCustomer();
if (loading) {
return <div className="h-4 w-full animate-pulse bg-gray-200 rounded" />;
}
// Find the feature in customer's products
const product = customer?.products?.[0];
if (!product) {
return <div className="text-sm text-muted-foreground">No active plan</div>;
}
// UseAutumn doesn't expose per-feature usage in customer object
// You need to implement this via a Convex query that calls ctx.check()
return (
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span>API Calls</span>
<span>Check usage via Convex query</span>
</div>
</div>
);
}
Get Usage from Convex
Create a query to fetch current usage:
import { userQuery } from "./_helpers/builders";
export const getUsage = userQuery({
args: { featureId: v.string() },
handler: async (ctx, { featureId }) => {
const result = await ctx.check({ featureId });
return {
allowed: result.data?.allowed || false,
// Note: UseAutumn's check response structure may vary
// Consult their docs for exact response shape
};
},
});
Then use in client:
'use client';
import { api } from "@convex/_generated/api";
import { useQuery } from "convex/react";
export function UsageMeterWithData({ featureId }: { featureId: string }) {
const usage = useQuery(api.usage.getUsage, { featureId });
if (!usage) {
return <div>Loading...</div>;
}
return (
<div className="rounded-lg border p-4">
<p className="text-sm font-medium">
{usage.allowed ? "✓ Quota Available" : "⚠️ Limit Reached"}
</p>
</div>
);
}
Overage Handling
Soft Limits (Recommended)
Block actions when limit is reached:
export const sendEmail = userAction({
args: { to: v.string(), body: v.string() },
handler: async (ctx, { to, body }) => {
const result = await ctx.check({ featureId: "emails" });
if (!result.data?.allowed) {
throw new AppError(
"USAGE_EXCEEDED",
"Email limit reached. Upgrade to send more emails."
);
}
await ctx.track({ featureId: "emails", value: 1 });
// Send email
},
});
Overage Billing (Pay-as-you-go)
Define overage pricing in autumn.config.ts
:
export const paidPlanWithOverage = product({
id: "starterapp-paid-overage",
name: "Paid Plan with Overage",
items: [
priceItem({ price: 10, interval: "month" }),
featureItem({
feature_id: apiCalls.id,
included_usage: 10_000,
interval: "month",
}),
// Note: UseAutumn overage syntax may vary - check their docs
// This is a conceptual example
],
});
Heads up
Check UseAutumn documentation for exact overage configuration syntax. Implementation details vary by pricing model.
Credit Top-Ups
Let users purchase additional usage without changing plans:
export const apiCallPack = product({
id: "api-call-pack-1000",
name: "1,000 API Calls",
items: [
priceItem({ price: 5 }), // One-time $5
featureItem({
feature_id: apiCalls.id,
included_usage: 1_000,
// No interval = permanent credits
}),
],
});
Purchase from client:
'use client';
import { useState } from 'react';
import { useServices } from '~/lib/providers/services-context';
import { Button } from '@workspace/ui/components/button';
export function BuyCreditsButton() {
const { billing } = useServices();
const [loading, setLoading] = useState(false);
async function handlePurchase() {
setLoading(true);
try {
await billing.checkout({
productId: 'api-call-pack-1000',
successUrl: window.location.origin + '/dashboard/billing',
});
} catch (error) {
console.error('Purchase failed:', error);
} finally {
setLoading(false);
}
}
return (
<Button onClick={handlePurchase} disabled={loading}>
{loading ? 'Loading...' : 'Buy 1,000 Credits - $5'}
</Button>
);
}
Usage Patterns
Common Errors
Related Documentation
- Feature Gating - Boolean access control
- Subscriptions - Recurring billing
- One-Time Payments - Credit packs and add-ons