turnley.dev

PHP and MySQL code for faucet operators

Tools & Calculators

FCT-005

An internal credit economy with an admin-set rate

Earn in integer credits, convert to coin only at withdrawal, and keep the rate in settings. Repricing the whole site becomes one field.

Paying every reward directly in coin ties every number on the site to a market price. Change coins, change price, change payout minimums, and every reward figure in the database is suddenly wrong.

An internal credit does the earning and converts once, at the withdrawal boundary. Rewards are integers with no decimal places, the exchange rate lives in settings, and repricing the entire economy is editing one number.

It also makes the site legible. "500 credits" is something a user can reason about; 0.00000420 LTC is not.

PHP
// Rate is stored as: how many coin units one credit is worth.
// 1 credit = 100 satoshi  ->  credit_units = 100
function credits_to_units(int $credits): int
{
    $rate = max(1, setting_int('credit_units', 100));
    return $credits * $rate;
}

function units_to_credits(int $units): int
{
    $rate = max(1, setting_int('credit_units', 100));
    return (int) floor($units / $rate);          // never round up in the user's favour
}

function withdraw_credits(PDO $pdo, int $userId, int $credits, string $address): int
{
    $minCredits = setting_int('min_withdraw_credits', 1000);
    if ($credits < $minCredits) {
        throw new RuntimeException('minimum is ' . $minCredits . ' credits');
    }

    $units = credits_to_units($credits);

    $pdo->beginTransaction();
    try {
        $st = $pdo->prepare('SELECT credits FROM users WHERE id = ? FOR UPDATE');
        $st->execute([$userId]);
        if ((int) $st->fetchColumn() < $credits) {
            $pdo->rollBack();
            throw new RuntimeException('insufficient credits');
        }

        $pdo->prepare('UPDATE users SET credits = credits - ? WHERE id = ?')->execute([$credits, $userId]);
        $pdo->prepare(
            'INSERT INTO withdrawals (user_id, credits, units, rate_used, address, status, created_at)
             VALUES (?, ?, ?, ?, ?, "held", NOW())'
        )->execute([$userId, $credits, $units, setting_int('credit_units', 100), $address]);

        $id = (int) $pdo->lastInsertId();
        $pdo->commit();
        return $id;
    } catch (Throwable $e) {
        $pdo->rollBack();
        throw $e;
    }
}

Using it

Store the rate used on every withdrawal row. Without it, a rate change makes your historical payouts impossible to reconcile.

Show the conversion live next to the withdrawal form so nobody is surprised by what lands in their wallet.

What bites people

Lowering the rate devalues every credit already earned. Announce it before you do it, or expect to be accused of stealing — and technically they will be right.

Keep credits as integers. The moment a fraction of a credit exists, the whole benefit of this design is gone.

This one touches real money. Point it at a throwaway wallet and watch a full cycle before you trust it with a live balance.

Also in Faucet Mechanics