NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Credits for Laravel

Granting & deducting

Every change appends one ledger row. Grant with add(), deduct with deduct() — both take a positive amount — or pass a signed amount to modify(); the optional description and meta array are stored on the row:

use RoundlyConsulting\Credits\Facades\Credits;

Credits::for($user)->add(100, 'signup bonus');                   // a grant — returns the Credit row
Credits::for($user)->deduct(30, 'purchase', ['order_id' => 42]); // a deduction, passed as a positive amount

Credits::for($user)->balance(); // 70

Credits::for($user)->modify(-5, 'correction');                   // a signed change

On a model with the HasCredits trait, modifyCredits() is the shorthand — a positive amount grants, a negative one deducts:

$user->modifyCredits(100, 'signup bonus');
$user->modifyCredits(-30, 'purchase', ['order_id' => 42]);

$user->creditsBalance(); // 70

It returns the recorded Credit row:

$credit = $user->modifyCredits(25, 'referral reward', ['referrer_id' => 7]);

$credit->amount;       // 25
$credit->description;  // 'referral reward'
$credit->meta;         // ['referrer_id' => 7]
$credit->bucket;       // 'default'
$credit->created_at;   // when the change was recorded

Named arguments keep longer calls readable — the full signature is modifyCredits(int $amount, ?string $description = null, ?array $meta = null, bool $allowOverdraft = false, ?string $bucket = null):

$user->modifyCredits(
    amount: -30,
    description: 'order #42',
    meta: ['order_id' => 42],
    allowOverdraft: false,
    bucket: 'purchased',
);

Setting an exact balance

setTo() computes the delta from the current balance and records a single adjusting row. When the balance already matches, it records nothing, returns null and fires no event. It takes the owner lock and reads the balance under a row lock, in one transaction with the write, so two racing calls serialise — the second computes its delta from the balance the first left behind:

Credits::for($user)->setTo(500, 'manual adjustment');     // one delta row; null when already there
Credits::for($user)->bucket('promotional')->setTo(200);   // per bucket, like every other call

The trait form is setCreditsTo():

$user->creditsBalance();                         // 70

$user->setCreditsTo(500, 'manual adjustment');   // records +430, returns the Credit row
$user->setCreditsTo(500);                        // already 500: records nothing, returns null

$user->setCreditsTo(200, bucket: 'promotional'); // per bucket, like every other method

A downward adjustment is recorded as a deduction, so the overdraft guard applies to it — setting a balance below the floor needs allowOverdraft: true.

An append-only history

Nothing is ever overwritten: amounts are signed integers, each change is its own time-stamped row, and the balance is their sum. To reverse a change, record a compensating row — the history then shows both the change and its correction.

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.