Companion Guide: This guide belongs to the Laravel kits. Every Laravel kit ships these checks built in. Explore Kit #1 (Filament), Kit #2 (Livewire), and Kit #3 (MongoDB).
Track 05 Lesson 14

LLM API Key and Cost Security in Laravel

Curated by Umar Farooq • 5 min read
Quick Answer & Core Takeaway

LLM cost security protects applications from financial denial-of-wallet attacks and credential leaks. In Laravel, API keys must remain strictly server-side (stored in encrypted environment configurations, never bundled into frontend assets). Every AI endpoint requires granular per-tenant token budgets, strict sliding-window rate limiting via Redis, and automatic circuit breakers that halt traffic when unexpected token spikes occur.

Last reviewed: 2026-10. Applies to: PHP 8.4+, Laravel 11.x – 13.x.

Quick Answer

LLM cost security protects applications from financial denial-of-wallet attacks and credential leaks. In Laravel, API keys must remain strictly server-side (stored in encrypted environment configurations, never bundled into frontend assets). Every AI endpoint requires granular per-tenant token budgets, strict sliding-window rate limiting via Redis, and automatic circuit breakers that halt traffic when unexpected token spikes occur.

Threat Model

Vulnerable Example

[INSECURE] Vulnerable Pattern
<?php

declare(strict_types=1);

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;

class VulnerableAiChatController extends Controller
{
    /**
     * [INSECURE]: No rate limiting, unbounded token limits, and keys loaded directly from raw env.
     */
    public function chat(Request $request)
    {
        // Vulnerable: no user quota check, no maximum token constraint
        $prompt = (string) $request->input('message');

        $response = Http::withToken((string) env('OPENAI_API_KEY'))
            ->post('https://api.openai.com/v1/chat/completions', [
                'model' => 'gpt-4o',
                'messages' => [['role' => 'user', 'content' => $prompt]],
            ]);

        return response()->json($response->json());
    }
}

Secure Implementation

[SECURE] Hardened Pattern
<?php

declare(strict_types=1);

namespace App\Services\Ai;

use App\Models\User;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use RuntimeException;

final class SecureLlmBillingGuardService
{
    private const int DAILY_USER_TOKEN_LIMIT = 50_000;
    private const int MAX_OUTPUT_TOKENS = 1_000;

    /**
     * [SECURE]: Budget verification, enforced output limits, and usage tracking.
     */
    public function executeBudgetedPrompt(User $user, string $systemPrompt, string $userMessage): string
    {
        $todayKey = 'ai_usage:' . $user->id . ':' . now()->format('Y-m-d');
        $usedTokens = (int) Cache::get($todayKey, 0);

        if ($usedTokens >= self::DAILY_USER_TOKEN_LIMIT) {
            Log::warning('User exceeded daily AI token quota', [
                'user_id' => $user->id,
                'used_tokens' => $usedTokens,
            ]);
            throw new RuntimeException('Daily AI token quota reached. Please upgrade or try again tomorrow.');
        }

        $apiKey = config('services.openai.key');
        if (!is_string($apiKey) || empty($apiKey)) {
            throw new RuntimeException('OpenAI API key is missing or not configured.');
        }

        $response = Http::withToken($apiKey)
            ->timeout(20)
            ->post('https://api.openai.com/v1/chat/completions', [
                'model' => 'gpt-4o-mini',
                'max_tokens' => self::MAX_OUTPUT_TOKENS, // Enforce strict upper bound
                'messages' => [
                    ['role' => 'system', 'content' => $systemPrompt],
                    ['role' => 'user', 'content' => $userMessage],
                ],
            ]);

        if (!$response->successful()) {
            Log::error('AI Provider Error', ['status' => $response->status(), 'body' => $response->body()]);
            throw new RuntimeException('Upstream AI service error.');
        }

        $totalUsedInCall = (int) $response->json('usage.total_tokens', 500);

        // Atomically update user daily token usage
        Cache::increment($todayKey, $totalUsedInCall);
        Cache::put($todayKey, Cache::get($todayKey), now()->endOfDay());

        return (string) $response->json('choices.0.message.content');
    }
}

Laravel-Specific Notes

Checklist

Further Reading

AI Assistant Shortcuts
Ask AI about this topic
← Prev: Prompt Injection Defense
Next: RAG Multi-Tenant Isolation →
UF

Curated by Umar Farooq

Senior Full-Stack & Security Engineer with 5+ years of production experience in Laravel, PHP, and modern web application security. Author of the Laravel AI Security Masterclass.