← Larameter 3 / 12

Quick start

The shortest path from an empty config to a call that refuses to run.

Price the things you sell

// config/larameter.php
'prices' => [
    'create_form'      => 1,
    'generate_report'  => 25,
    'send_email'       => 1,
],

An action you never priced is free, not guessed at. A package should not invent a price for something you did not write down.

Ask, then charge

if (! $org->credits()->allows('generate_report')) {
    return back()->with('error', 'Not enough credits.');
}

$report = $this->build($case);

$org->credits()->charge('generate_report', actor: $user, subject: $report);

Two calls rather than one, and deliberately so. Charging does not refuse: an account with nothing left still gets its usage row, recorded as an overdraft, because a half finished turn is worse than a small negative number. Asking first has to be the easy thing to write, or nobody asks and the ceiling goes unenforced.

allows() takes either a number of credits or the name of an action, so you never have to look the price up yourself:

$org->credits()->allows();                   // is there one credit left?
$org->credits()->allows(250);                // are there 250?
$org->credits()->allows('generate_report');  // enough for what that costs?

Charge consumption, not just actions

For anything priced per unit rather than per press, meter() takes the quantity in and out:

$org->credits()->meter('gpt-4o', 'token', $inputTokens, $outputTokens);

with the rate table alongside the prices:

'rates' => [
    'gpt-4o' => ['input' => 25_000, 'output' => 100_000],
],

25,000 credits per million input tokens. Everything here is expressed in credits, including the rates. What a credit is worth in money is your business and the package never asks. See Prices and rates.

Sell a top-up

$org->credits()->deposit(5_000, reason: 'purchase', source: $payment);

One call, two tables: the deposit row and the balance move together and cannot be written apart. Purchased credits sit outside every window, so they survive a reset, and what they pay for is not counted against the windows either. Run out of session, buy more, carry on, and your week has not moved meanwhile.

Cap what may exist

Credits are spent and come back. Seats and projects are different: a standing count of what exists, which the plan caps. Those are meters.

php artisan make:meter MemberMeter Organization
namespace App\Meters\Organization;

class MemberMeter extends Meter
{
    public function count(): int
    {
        return $this->meterable->members()->count();
    }
}
class Organization extends Model
{
    use HasCredits, HasPlans, HasMeters;

    protected array $meters = [MemberMeter::class];
}

Then no caller has to remember how to count:

if (! $org->quota()->allows('members', count($emails))) {
    return back()->with('error', 'Your plan has no room for that many.');
}

See Quotas and meters.

The usage screen

$org->credits()->windows();    // one WindowUsage per window: allowance, used, remaining, endsAt
$org->quota()->summary();      // one row per meter: handle, label, count, limit
$org->credits()->remaining();  // headroom in the tightest window, plus what was bought
$org->credits()->resetsAt();   // when spending is possible again, or null

None of those write anything. See Reading usage.

What you have not had to do

No webhook when a subscription renews, no scheduled command to reset anything, no column on your own table, no interface to implement, and no service provider to edit. A window's grid is fixed by when it started and every reset lands one length later, for ever; the plan is worked out on every request. See Windows for why those two clocks are deliberately not tied together.