A production-ready Laravel package for AI usage metering, quotas, and billing integration. Track token usage, enforce limits, and integrate with Stripe for billing.
Repository: GitHub | Packagist
- 🎯 Simple Developer Experience - Fluent API for metering AI usage
- 📊 Usage Tracking - Automatic token and cost tracking
- 🚦 Quota Management - Configurable limits (tokens, cost, per-plan, per-tenant)
- 💳 Billing Integration - Stripe/Cashier support with credit-based and subscription modes
- 🏢 Multi-Tenancy - Works with or without tenancy packages
- 📈 Dashboard - Built-in dashboard for usage monitoring
- 🔌 Provider Agnostic - Support for OpenAI, Anthropic, and custom providers
- ⚡ Performance - Caching, queue support, and optimized queries
- PHP >= 8.1
- Laravel 10.x, 11.x, or 12.x
- Database (MySQL, PostgreSQL, SQLite, or SQL Server) - configured in your Laravel application
For AI provider integration, you'll need the respective SDK packages:
- OpenAI:
openai-php/laravel(for OpenAI provider) - Anthropic:
anthropic-php/sdk(for Anthropic provider) - Stripe/Cashier:
laravel/cashier(for Stripe billing integration)
# For OpenAI
composer require openai-php/laravel
# For Anthropic
composer require anthropic-php/sdk
# For Stripe billing
composer require laravel/cashierInstall the package via Composer:
composer require ajooda/laravel-ai-meteringPublish the configuration file:
php artisan vendor:publish --tag=ai-metering-configPublish and run migrations:
php artisan vendor:publish --tag=ai-metering-migrations
php artisan migrateNote: Make sure your database connection is configured in
.envbefore running migrations. The package will use your default database connection unless specified otherwise viaAI_METERING_DB_CONNECTION.
The package supports various environment variables for configuration. Add these to your .env file:
# Default Provider
AI_METERING_DEFAULT_PROVIDER=openai
# Billing Configuration
AI_METERING_BILLING_DRIVER=Metrix\AiMetering\Services\Billing\NullBillingDriver
AI_METERING_OVERAGE_BEHAVIOR=block
AI_METERING_OVERAGE_SYNC_STRATEGY=batch
AI_METERING_CREDIT_OVERDRAFT=false
AI_METERING_CURRENCY=usd
AI_METERING_PAYMENT_FAILURE_GRACE_PERIOD=7
# Tenant Resolver (optional)
AI_METERING_TENANT_RESOLVER=Metrix\AiMetering\Resolvers\NullTenantResolver
# Period Configuration
AI_METERING_PERIOD_TYPE=monthly
AI_METERING_PERIOD_ALIGNMENT=calendar
AI_METERING_TIMEZONE=UTC
# Storage Configuration
AI_METERING_PRUNE_AFTER_DAYS=365
AI_METERING_DB_CONNECTION=
# Performance Configuration
AI_METERING_CACHE_LIMIT_CHECKS=true
AI_METERING_CACHE_TTL=300
AI_METERING_QUEUE_RECORDING=false
AI_METERING_BATCH_SIZE=100
# Dashboard Configuration
AI_METERING_DASHBOARD_ENABLED=true
AI_METERING_DASHBOARD_PREFIX=ai-metering
AI_METERING_DASHBOARD_GATE=
# Security Configuration
AI_METERING_VALIDATE_FEATURES=true
AI_METERING_SANITIZE_METADATA=true
AI_METERING_RATE_LIMIT=false
AI_METERING_PREVENT_RACE_CONDITIONS=true
# Logging Configuration
AI_METERING_LOGGING_ENABLED=true
AI_METERING_LOG_LEVEL=info
AI_METERING_LOG_FAILURES=true
# Feature Flags
AI_METERING_SOFT_DELETES=false
AI_METERING_AGGREGATION_TABLES=falseNote: All environment variables are optional and have sensible defaults. You only need to set them if you want to override the default values.
After installation, you can start using the package immediately:
- Create a plan (optional, for quota management):
use Metrix\AiMetering\Models\AiPlan;
$plan = AiPlan::create([
'name' => 'Basic Plan',
'slug' => 'basic',
'monthly_token_limit' => 100000,
'monthly_cost_limit' => 10.00,
'is_active' => true,
]);- Create a subscription (optional, if using plan-based billing):
use Metrix\AiMetering\Models\AiSubscription;
$subscription = AiSubscription::create([
'billable_type' => User::class,
'billable_id' => $user->id,
'ai_plan_id' => $plan->id,
'billing_mode' => 'plan',
'started_at' => now(),
'renews_at' => now()->addMonth(),
]);- Start metering AI usage:
use Metrix\AiMetering\Facades\AiMeter;
$response = AiMeter::forUser($user)
->billable($user)
->usingProvider('openai', 'gpt-4o-mini')
->feature('chat')
->call(function () {
// Your AI provider call here
return OpenAI::chat()->create([...]);
});Tip: If you're not using plans or subscriptions, you can still track usage. The package will work with just the
billableentity.
The configuration file is located at config/ai-metering.php. Key settings:
Configure AI providers and their pricing:
'providers' => [
'openai' => [
'class' => \Metrix\AiMetering\Services\Providers\OpenAiProvider::class,
'models' => [
'gpt-4o-mini' => [
'input_price_per_1k' => 0.00015,
'output_price_per_1k' => 0.00060,
],
],
],
],Configure billing driver and behavior:
'billing' => [
'driver' => \Metrix\AiMetering\Services\Billing\CashierBillingDriver::class,
'overage_behavior' => 'block', // 'block', 'charge', 'allow'
'overage_sync_strategy' => 'batch', // 'immediate', 'batch'
'credit_overdraft_allowed' => false,
],Configure usage period calculation:
'period' => [
'type' => 'monthly', // 'monthly', 'weekly', 'daily', 'yearly', 'rolling'
'alignment' => 'calendar', // 'calendar' or 'rolling'
'timezone' => 'UTC',
],Use the AiMeter facade to wrap your AI provider calls. The package works with any AI provider SDK - you just need to wrap your existing calls:
use Metrix\AiMetering\Facades\AiMeter;
use OpenAI\Laravel\Facades\OpenAI; // Requires: composer require openai-php/laravel
$response = AiMeter::forUser(auth()->user())
->billable(auth()->user())
->usingProvider('openai', 'gpt-4o-mini')
->feature('email_reply')
->call(function () {
return OpenAI::chat()->create([
'model' => 'gpt-4o-mini',
'messages' => [
['role' => 'user', 'content' => 'Write a polite reply...'],
],
]);
});
// Access the response
$aiResponse = $response->getResponse();
// Access usage information
$usage = $response->getUsage();
echo "Tokens used: " . $usage->totalTokens;
echo "Cost: $" . $response->getUsage()->totalCost;
// Check limits
if ($response->isApproachingLimit()) {
// Handle approaching limit
}For providers that don't return usage automatically:
$response = AiMeter::forUser($user)
->billable($user)
->usingProvider('manual', 'custom-model')
->withManualUsage([
'input_tokens' => 450,
'output_tokens' => 900,
'total_tokens' => 1350,
])
->call(function () use ($client) {
return $client->doStuff();
});Add metadata for better tracking:
$response = AiMeter::forUser($user)
->billable($user)
->usingProvider('openai', 'gpt-4o-mini')
->feature('support_reply')
->withMeta([
'ticket_id' => $ticket->id,
'customer_id' => $customer->id,
])
->call(fn () => OpenAI::chat()->create([...]));Prevent duplicate usage records:
$response = AiMeter::forUser($user)
->withIdempotencyKey('unique-request-id-123')
->call(fn () => OpenAI::chat()->create([...]));use Metrix\AiMetering\Models\AiPlan;
$plan = AiPlan::create([
'name' => 'Pro Plan',
'slug' => 'pro',
'monthly_token_limit' => 1000000,
'monthly_cost_limit' => 100.00,
'overage_price_per_1k_tokens' => 0.01,
'is_active' => true,
]);use Metrix\AiMetering\Models\AiSubscription;
$subscription = AiSubscription::create([
'billable_type' => User::class,
'billable_id' => $user->id,
'ai_plan_id' => $plan->id,
'billing_mode' => 'plan', // or 'credits'
'started_at' => now(),
'renews_at' => now()->addMonth(),
]);Override limits for specific periods:
use Metrix\AiMetering\Models\AiUsageLimitOverride;
AiUsageLimitOverride::create([
'billable_type' => User::class,
'billable_id' => $user->id,
'period_start' => now()->startOfMonth(),
'period_end' => now()->endOfMonth(),
'token_limit' => 2000000, // Double the plan limit
]);Set up credit wallets:
use Metrix\AiMetering\Models\AiCreditWallet;
$wallet = AiCreditWallet::firstOrCreate(
[
'billable_type' => User::class,
'billable_id' => $user->id,
],
[
'balance' => 0,
'currency' => 'usd',
]
);
// Add credits
$wallet->addCredits(100.00, 'top-up', ['payment_id' => 'pay_123']);
// Check balance
if ($wallet->hasSufficientBalance(50.00)) {
// Proceed with usage
}$response = AiMeter::forUser($user)
->billable($user)
->billingMode('credits')
->usingProvider('openai', 'gpt-4o-mini')
->call(fn () => OpenAI::chat()->create([...]));The package supports multi-tenancy without coupling to a specific package.
Create a tenant resolver:
namespace App\Resolvers;
use Metrix\AiMetering\Contracts\TenantResolver;
class CustomTenantResolver implements TenantResolver
{
public function resolve(mixed $context = null): mixed
{
// Return the current tenant
return tenant(); // or your tenancy package's method
}
}Register it in config/ai-metering.php:
'tenant_resolver' => \App\Resolvers\CustomTenantResolver::class,$response = AiMeter::forUser($user)
->forTenant($tenant)
->billable($tenant) // Bill the tenant, not the user
->usingProvider('openai', 'gpt-4o-mini')
->call(fn () => OpenAI::chat()->create([...]));The package integrates with Laravel Cashier for Stripe billing.
- Install Cashier:
composer require laravel/cashier- Configure your billable model:
use Laravel\Cashier\Billable;
class User extends Authenticatable
{
use Billable;
}- Set billing driver in config:
'billing' => [
'driver' => \Metrix\AiMetering\Services\Billing\CashierBillingDriver::class,
],Overage charges can be synced to Stripe:
Immediate sync:
'overage_sync_strategy' => 'immediate',Batch sync (recommended for high volume):
'overage_sync_strategy' => 'batch',Then run the sync command:
php artisan ai-metering:sync-stripe-overagesProtect routes with quota enforcement:
use Illuminate\Support\Facades\Route;
Route::middleware(['auth', 'ai.quota'])->group(function () {
Route::post('/ai/chat', [AiController::class, 'chat']);
});The middleware:
- Checks usage limits before allowing the request
- Returns 429 (Too Many Requests) if limit exceeded
- Adds response headers:
X-Remaining-Tokens,X-Remaining-Cost,X-Usage-Percentage
Access the dashboard at /ai-metering (configurable):
- View usage statistics
- Monitor quotas and limits
- See breakdown by provider/model
- Filter and search usage history
Configure in config/ai-metering.php:
'dashboard' => [
'enabled' => true,
'prefix' => 'ai-metering',
'middleware' => ['web', 'auth'],
'gate' => 'viewAiMetering', // Optional gate for authorization
],php artisan ai-metering:report
php artisan ai-metering:report --month=2024-01
php artisan ai-metering:report --billable-type="App\Models\User" --billable=1php artisan ai-metering:cleanup
php artisan ai-metering:cleanup --days=90
php artisan ai-metering:cleanup --dry-runphp artisan ai-metering:sync-stripe-overages
php artisan ai-metering:sync-stripe-overages --limit=50php artisan ai-metering:validatephp artisan ai-metering:migrate-plan "App\Models\User" 1 "pro-plan"
php artisan ai-metering:migrate-plan "App\Models\User" 1 "pro-plan" --from-plan="basic-plan"php artisan ai-metering:sync-plansList all active plans in the system.
php artisan ai-metering:aggregate
php artisan ai-metering:aggregate --period=month
php artisan ai-metering:aggregate --period=week --billable-type="App\Models\User"Pre-computes usage aggregates for dashboard performance (requires aggregation_tables feature enabled in config).
Use Eloquent scopes to query usage records:
use Metrix\AiMetering\Models\AiUsage;
use Carbon\Carbon;
// Query by billable entity
$usage = AiUsage::forBillable($user)->get();
// Query by provider
$usage = AiUsage::byProvider('openai')->get();
// Query by model
$usage = AiUsage::byModel('gpt-4o-mini')->get();
// Query by feature
$usage = AiUsage::byFeature('support_reply')->get();
// Query within a period
$start = Carbon::now()->startOfMonth();
$end = Carbon::now()->endOfMonth();
$usage = AiUsage::inPeriod($start, $end)->get();
// Combine scopes
$usage = AiUsage::forBillable($user)
->byProvider('openai')
->byFeature('email_reply')
->inPeriod($start, $end)
->get();When a user changes plans mid-period:
use Metrix\AiMetering\Models\AiSubscription;
$subscription = AiSubscription::where('billable_id', $user->id)->first();
// Update to new plan
$subscription->update([
'ai_plan_id' => $newPlan->id,
'previous_plan_id' => $oldPlan->id, // Track previous plan
]);
// Usage before plan change counts against old plan
// Usage after plan change counts against new planCheck subscription status:
$subscription = AiSubscription::where('billable_id', $user->id)->first();
if ($subscription->isActive()) {
// Subscription is active
}
if ($subscription->isExpired()) {
// Subscription has expired
}
if ($subscription->isInGracePeriod()) {
// Subscription is in grace period (still active)
}
if ($subscription->isInTrial()) {
// Subscription is in trial period
}Configure grace periods in subscriptions:
$subscription = AiSubscription::create([
'billable_type' => User::class,
'billable_id' => $user->id,
'ai_plan_id' => $plan->id,
'ends_at' => now()->subDay(), // Expired yesterday
'grace_period_ends_at' => now()->addDays(7), // Grace period for 7 days
]);During grace period, the subscription is still considered active and limits apply.
The package supports various period types and alignments:
monthly: Calendar month (1st to last day) or rolling 30 daysweekly: Calendar week (Monday-Sunday) or rolling 7 daysdaily: Calendar day (00:00-23:59) or rolling 24 hoursyearly: Calendar year or rolling 365 daysrolling: Rolling period from subscription start
calendar: Period aligns to calendar boundaries (e.g., 1st of month)rolling: Period starts from a specific date and rolls forward
use Metrix\AiMetering\Support\Period;
// Create period from config
$period = Period::fromConfig(config('ai-metering.period'));
// Get period start/end
$start = $period->getStart(); // Current period start
$end = $period->getEnd(); // Current period end (exclusive)
// Check if date is in period
if ($period->contains(Carbon::now())) {
// Date is within current period
}
// Get next/previous periods
$nextPeriod = $period->getNext();
$previousPeriod = $period->getPrevious();Listen to usage events:
use Metrix\AiMetering\Events\AiUsageRecorded;
use Metrix\AiMetering\Events\AiLimitApproaching;
use Metrix\AiMetering\Events\AiLimitReached;
Event::listen(AiUsageRecorded::class, function ($event) {
// Handle usage recorded
});
Event::listen(AiLimitApproaching::class, function ($event) {
// Send notification when approaching limit
});
Event::listen(AiLimitReached::class, function ($event) {
// Handle limit reached
});Available events:
AiUsageRecorded- When usage is recordedAiLimitApproaching- When usage exceeds 80% of limitAiLimitReached- When hard limit is reachedAiProviderCallFailed- When provider call failsAiOverageCharged- When overage is chargedAiPlanChanged- When plan changesAiCreditsAdded- When credits are addedAiCreditsDeducted- When credits are deductedAiSubscriptionExpired- When subscription expires
Create custom provider implementations:
namespace App\Providers;
use Metrix\AiMetering\Contracts\ProviderClient;
use Metrix\AiMetering\Support\ProviderUsage;
class CustomProvider implements ProviderClient
{
public function call(callable $callback): array
{
$response = $callback();
// Extract usage from response
$usage = new ProviderUsage(
inputTokens: $response->input_tokens,
outputTokens: $response->output_tokens,
totalTokens: $response->total_tokens,
totalCost: $response->cost,
);
return [
'response' => $response,
'usage' => $usage,
];
}
}Register in config:
'providers' => [
'custom' => [
'class' => \App\Providers\CustomProvider::class,
],
],Limit checks are cached by default:
'performance' => [
'cache_limit_checks' => true,
'cache_ttl' => 300, // seconds
],Record usage asynchronously for better performance:
'performance' => [
'queue_usage_recording' => 'default', // Queue name or false
],Important: If you enable queue recording, make sure your queue worker is running:
php artisan queue:workOr use a process manager like Supervisor for production environments. See Laravel Queue Documentation for more details.
Record multiple usages efficiently:
use Metrix\AiMetering\Services\UsageRecorder;
$recorder = app(UsageRecorder::class);
$usages = [
[
'billable_type' => User::class,
'billable_id' => $user->id,
'provider' => 'openai',
'model' => 'gpt-4o-mini',
'total_tokens' => 100,
'total_cost' => 0.1,
'occurred_at' => now(),
],
// ... more usage records
];
$recorded = $recorder->recordBatch($usages);Protect dashboard access with Gates:
// In AuthServiceProvider
Gate::define('viewAiMetering', function ($user) {
return $user->isAdmin(); // Your authorization logic
});Configure in config/ai-metering.php:
'dashboard' => [
'gate' => 'viewAiMetering', // Gate name for authorization
],The package validates:
- Feature names (alphanumeric + underscore)
- Token/cost values (non-negative)
- Metadata sanitization
Always validate user input before passing to AiMeter:
$validated = $request->validate([
'feature' => 'required|string|max:100|regex:/^[a-zA-Z0-9_]+$/',
]);Delete all usage for a user:
use Metrix\AiMetering\Models\AiUsage;
// Delete all usage records
AiUsage::where('billable_type', User::class)
->where('billable_id', $user->id)
->delete();
// Or delete by user_id
AiUsage::where('user_id', $user->id)->delete();Anonymize old usage data:
AiUsage::where('occurred_at', '<', now()->subYears(2))
->update([
'billable_type' => null,
'billable_id' => null,
'user_id' => null,
'meta' => null,
]);The package throws specific exceptions:
use Metrix\AiMetering\Exceptions\AiLimitExceededException;
use Metrix\AiMetering\Exceptions\AiCreditsInsufficientException;
try {
$response = AiMeter::forUser($user)->call(fn () => OpenAI::chat()->create([...]));
} catch (AiLimitExceededException $e) {
// Handle limit exceeded
} catch (AiCreditsInsufficientException $e) {
// Handle insufficient credits
}The package integrates with Laravel Cashier's webhook handling. Listen to Cashier events:
// In EventServiceProvider
protected $listen = [
\Laravel\Cashier\Events\WebhookReceived::class => [
\App\Listeners\HandleStripeWebhook::class,
],
];Handle subscription updates:
use Laravel\Cashier\Events\WebhookReceived;
use Metrix\AiMetering\Models\AiSubscription;
public function handle(WebhookReceived $event)
{
if ($event->payload['type'] === 'customer.subscription.updated') {
// Update AiSubscription based on Stripe subscription
$stripeSubscription = $event->payload['data']['object'];
AiSubscription::where('stripe_id', $stripeSubscription['id'])
->update([
'ends_at' => Carbon::createFromTimestamp($stripeSubscription['current_period_end']),
]);
}
}AiMeter::forUser($user) // Set user for usage tracking
->forTenant($tenant) // Set tenant (optional)
->billable($billable) // Set billable entity
->usingProvider($provider, $model) // Set AI provider and model
->feature($feature) // Set feature name
->billingMode($mode) // Set billing mode ('plan' or 'credits')
->withMeta($meta) // Add metadata (array)
->withIdempotencyKey($key) // Set idempotency key
->withManualUsage($usage) // Set manual usage data
->call($callback) // Execute AI call and record usage$response->getResponse() // Original provider response
$response->getUsage() // ProviderUsage object
$response->getLimitCheck() // LimitCheckResult object
$response->getRemainingTokens() // ?int - Remaining tokens in period
$response->getRemainingCost() // ?float - Remaining cost in period
$response->isApproachingLimit() // bool - Usage > 80% of limit
$response->isLimitReached() // bool - Hard limit reacheduse Metrix\AiMetering\Support\ProviderUsage;
$usage = new ProviderUsage(
inputTokens: 100,
outputTokens: 200,
totalTokens: 300,
inputCost: 0.00015,
outputCost: 0.00030,
totalCost: 0.00045,
currency: 'usd'
);
// Access properties
$usage->inputTokens; // ?int
$usage->outputTokens; // ?int
$usage->totalTokens; // ?int
$usage->inputCost; // ?float
$usage->outputCost; // ?float
$usage->totalCost; // ?float
$usage->currency; // ?string
// Convert to array
$array = $usage->toArray();
// Create from array
$usage = ProviderUsage::fromArray($array);use Metrix\AiMetering\Support\LimitCheckResult;
// Properties
$result->allowed; // bool - Is usage allowed?
$result->approaching; // bool - Is usage approaching limit (>80%)?
$result->hardLimitReached; // bool - Has hard limit been reached?
$result->remainingTokens; // ?int - Remaining tokens
$result->remainingCost; // ?float - Remaining cost
$result->usagePercentage; // float - Usage percentage (0-100)
// Factory methods
$result = LimitCheckResult::allowed(
remainingTokens: 1000,
remainingCost: 50.0,
usagePercentage: 50.0
);
$result = LimitCheckResult::limitReached();
$result = LimitCheckResult::unlimited();// AiSubscription
$subscription->plan; // BelongsTo AiPlan
$subscription->previousPlan; // BelongsTo AiPlan (previous plan)
$subscription->billable; // MorphTo (User, Tenant, etc.)
// AiPlan
$plan->subscriptions; // HasMany AiSubscription
// AiUsage
$usage->billable; // MorphTo (User, Tenant, etc.)
// AiCreditWallet
$wallet->billable; // MorphTo
$wallet->transactions; // HasMany AiCreditTransaction
// AiCreditTransaction
$transaction->wallet; // BelongsTo AiCreditWallet
// AiOverage
$overage->billable; // MorphTo
$overage->usage; // BelongsTo AiUsage (if ai_usage_id set)// AiPlan
$plan->hasUnlimitedTokens(); // bool
$plan->hasUnlimitedCost(); // bool
$plan->allowsOverage(); // bool
// AiSubscription
$subscription->isActive(); // bool
$subscription->isExpired(); // bool
$subscription->isInTrial(); // bool
$subscription->isInGracePeriod(); // bool
// AiCreditWallet
$wallet->addCredits($amount, $reason, $meta); // AiCreditTransaction
$wallet->deductCredits($amount, $reason, $meta); // AiCreditTransaction
$wallet->hasSufficientBalance($amount); // bool
// AiOverage
$overage->isSynced(); // bool
$overage->markAsSynced($stripeId); // voidMake sure the provider is configured in config/ai-metering.php:
'providers' => [
'xxx' => [
'class' => \Your\Provider\Class::class,
],
],- Check if you're using the correct period (calendar vs rolling)
- Verify the subscription is active:
$subscription->isActive() - Clear cache:
php artisan cache:clear - Check for period-specific overrides
- Ensure billing mode is set to 'credits':
->billingMode('credits') - Verify wallet exists:
AiCreditWallet::firstOrCreate([...]) - Check if overdraft is allowed in config
- Look for exceptions in logs
- Check if queue is enabled and jobs are processing
- Verify database connection in config
- Check logs for recording errors
- Ensure idempotency keys are unique
- Verify dashboard is enabled:
'dashboard.enabled' => true - Check middleware configuration
- Verify Gate authorization if configured
- Check route prefix matches config
Enable detailed logging:
'logging' => [
'enabled' => true,
'level' => 'debug', // Use 'debug' for detailed logs
'log_failures' => true,
],Check logs in storage/logs/laravel.log for detailed information.
Check package health:
php artisan ai-metering:validateThis validates:
- Configuration
- Database connectivity
- Data integrity
- Orphaned records
composer testRun with coverage:
composer test-coverageThis is the initial release. No migrations needed.
For future versions, migration guides will be documented here. See CHANGELOG.md for detailed version history.
Contributions are welcome! Please feel free to submit a Pull Request.
Before contributing:
- Read the codebase structure
- Follow PSR-12 coding standards
- Write tests for new features
- Update documentation
The MIT License (MIT). Please see License File for more information.
For issues and questions, please open an issue on GitHub.