A ledger of every AI request your app makes: estimated, used, spent.
Headroom Ledger records Headroom's events in your database, one row per provider call: what was estimated before it was sent, what was actually used, what it cost, how close it came to the context limit, and whether it was warned or blocked. Then it answers questions about them through a typed query API and an artisan report.
composer require phoenix1331/laravel-ai-headroom-ledger php artisan vendor:publish --tag=headroom-ledger-migrations php artisan migrate
What gets recorded: one request, one row
- TokensCounted
- HeadroomLow
- RequestBlocked
- RequestFailed
- TokensUsed
Queued listeners can run in any order. Every event upserts the same row, so it settles the same either way.
- Agent
App\Ai\Agents\SupportAgent- Model
- Input tokens
-
estimatedactual
- Cost
-
estimatedactual
- Context used
-
window
- Reason
The pre-flight side of AI spend
AI cost dashboards already exist for Laravel. They know what a request cost after it ran. Headroom Ledger also knows what was predicted before it ran, because Headroom counted it first.
That answers questions a cost log cannot: how far off are the estimates for each model, which agents creep towards their context limit, how many requests were warned or blocked, and what the requests that never completed would have cost. Headroom stays lean with no migrations; the ledger is the separate package for teams who want the history.
A report, before any dashboard
The data is useful the moment it is recorded. headroom-ledger:report prints spend, tokens, context usage, limits and estimate accuracy, grouped by model, provider, agent, day, run or a tag. Add --json to send it somewhere else.
Text version
$ php artisan headroom-ledger:report --since=7d --by=agent
INFO Headroom Ledger, 2026-10-11 to 2026-10-17.
Spend and tokens
+---------------+----------+-----------------+-----------+---------+--------+-----------------+
| Agent | Requests | Spend | Input | Output | Cached | Estimated input |
+---------------+----------+-----------------+-----------+---------+--------+-----------------+
| ResearchAgent | 35 | USD 2.640355 * | 1,364,542 | 30,688 | 0 | 1,922,658 |
| SummaryAgent | 60 | USD 2.621439 * | 1,900,614 | 55,793 | 0 | 2,252,423 |
| SupportAgent | 140 | USD 19.213263 * | 4,397,366 | 139,303 | 0 | 5,693,587 |
| Total | 235 | USD 24.475057 * | 7,662,522 | 225,784 | 0 | 9,868,668 |
+---------------+----------+-----------------+-----------+---------+--------+-----------------+
Context and limits
+---------------+----------+-------------+-------------+--------+---------+--------+
| Agent | Requests | Avg context | Max context | Warned | Blocked | Failed |
+---------------+----------+-------------+-------------+--------+---------+--------+
| ResearchAgent | 35 | 13.7% | 123.5% | 2 | 1 | 0 |
| SummaryAgent | 60 | 18.8% | 128.1% | 5 | 1 | 2 |
| SupportAgent | 140 | 20.3% | 125.6% | 10 | 5 | 5 |
| Total | 235 | 19.0% | 128.1% | 17 | 7 | 7 |
+---------------+----------+-------------+-------------+--------+---------+--------+
Estimate accuracy (requests that reported usage from the model counted)
+-------------------+------------+---------+-------+-------+
| Model | Method | Samples | Error | Bias |
+-------------------+------------+---------+-------+-------+
| claude-haiku-5-5 | calibrated | 53 | 6.4% | +2.9% |
| claude-sonnet-5-5 | native | 129 | 5.8% | -1.0% |
| gpt-5 | estimated | 34 | 5.9% | -4.7% |
| All | | 216 | 5.9% | -0.7% |
+-------------------+------------+---------+-------+-------+
* includes pre-flight estimates for requests that never reported usage
Real output from a week of demo data (the workbench DemoLedgerSeeder, built on the package's LedgerEntryFactory); the figures are illustrative.
A query API that returns values, not models
The query API is the only supported way to read the ledger, so the schema can change without breaking anything built on it. Results are typed, readonly value objects; no Eloquent model leaves the package.
use Phoenix1331\AiHeadroomLedger\Facades\Ledger; Ledger::query() ->between(now()->subDays(30), now()) ->forAgent(SupportAgent::class) ->forTag('tenant_id', 42) ->spend(); // actual where known, else estimated, flagged Ledger::query()->lastDays(7)->byModel()->tokens(); // input, output, cached per model Ledger::query()->lastDays(7)->byDay()->spend(); // every day, ready to chart Ledger::query()->lastDays(30)->accuracy(); // estimate error per model and count method Ledger::query()->lastDays(7)->nearLimit(80)->entries(); // requests at or above 80% of context Ledger::query()->lastDays(7)->blocked()->entries(); // blocked or warned Ledger::query()->lastDays(7)->byRun()->spend(); // per agent run, not per call Ledger::find($requestId); // one LedgerEntry Ledger::run($invocationId); // every step of an agent run, with totals
Honest by design. A request that never reported usage has an estimate but no actual figures; it is never counted as zero. spend() keeps the reported and estimate-only parts separate, every amount carries its currency and whether it is an estimate, and different currencies are never added together. Money is held as integer millionths, so a request that cost a fraction of a cent still shows up.
Fast as it grows: daily rollups
Aggregates read closed days from daily rollups and today from the raw entries, and give exactly the same answer as reading every entry. Filters a rollup cannot answer, such as nearLimit() or a run, simply read the entries.
- RecordEvery request is a raw entry, written by queued listeners
- Roll upEach night, closed days are summed by model, agent and your primary tag
- QueryRollups for closed days, entries for today, merged into one answer
- PruneOld entries go after their retention; their totals stay in the rollups
Scheduling
Both commands are scheduled for you on a cron expression you can change. Each rollup run catches up every closed day since the last one, so any frequency is safe.
// config/headroom-ledger.php 'rollup' => ['enabled' => true, 'schedule' => '15 0 * * *'], 'prune' => ['schedule' => '30 0 * * *'],
Prefer to schedule them yourself? Set either schedule to false and add your own entry, for example in routes/console.php:
Schedule::command('headroom-ledger:rollup')->hourly()->onOneServer(); Schedule::command('headroom-ledger:prune')->daily()->onOneServer();
Run them on one server: onOneServer() needs a cache store that supports locks (Redis, Memcached, database or DynamoDB). Rollups are a snapshot of each closed day; if entries for an older day arrive late, rebuild it with php artisan headroom-ledger:rollup --from=2026-10-01.
Numbers, not conversations
The ledger never stores prompt, instruction, tool or response text. An architecture test fails the build if any column could hold it.
| Stored | Never stored |
|---|---|
| Token counts and the per-part breakdown | Prompts, instructions and conversation history |
| Estimated and actual cost, with currency | Tool definitions, arguments and results |
| Agent class, provider and model names | Responses |
| Context window and percentage used | Exception messages (only the class and HTTP status) |
| Outcome: counted, completed, warned, blocked or failed | Any tag whose key is not on your allowlist |
Tags are opt in
Headroom can tag requests with your own context, such as a tenant or user. The ledger stores a tag only if its key is on the tags.allowed list, which is empty by default. A user ID is personal data, so record it only if you need it.
'tags' => ['allowed' => ['tenant_id'], 'primary_tag' => 'tenant_id'],
Retention and erasure
Raw entries and rollups have their own retention (90 days and two years by default). For a data subject request, Ledger::forgetTag('user_id', 123) removes the tag everywhere, gives that person's agent runs new random IDs so they no longer match your logs, and keeps the totals correct. Read the security policy for the full picture.
Part of the Headroom family
Three packages, each useful on its own.
-
Headroom
phoenix1331/laravel-ai-headroomCounts a request before it is sent, guards context and cost limits, and fires the events the ledger records.
-
Headroom Ledger
phoenix1331/laravel-ai-headroom-ledgerThis package. Stores those events per request and answers questions about them.
-
Filament Headroom
phoenix1331/filament-headroomA Filament dashboard built on the ledger's query API. Coming next.