turnley.dev

PHP and MySQL code for faucet operators

Tools & Calculators

FCT-003

Idle mining accrual with a capacity cap

Earn while away, but only up to a ceiling — computed on read from a timestamp, so there is no cron and no per-user tick.

Idle accumulation gives people a reason to come back without giving them a reason to sit on the page. The temptation is to run a cron that adds to every balance every minute, which does not scale and drifts when the cron misses a run.

Compute it on read instead. Store the rate, the capacity and the timestamp of the last collection; when the user loads the page, work out how much has accrued since then and cap it. Nothing runs in the background and the arithmetic is always exact.

The cap is what brings them back. Uncapped accrual rewards leaving; a cap that fills in eight hours rewards returning.

PHP
function pending_accrual(array $miner, ?int $now = null): int
{
    $now = $now ?? time();
    $elapsed = max(0, $now - (int) $miner['collected_at']);
    $earned  = (int) floor($elapsed * (int) $miner['rate_per_hour'] / 3600);

    return min($earned, (int) $miner['capacity']);      // the ceiling does the work
}

function collect(PDO $pdo, int $userId): int
{
    $pdo->beginTransaction();
    try {
        $st = $pdo->prepare('SELECT * FROM miners WHERE user_id = ? FOR UPDATE');
        $st->execute([$userId]);
        $miner = $st->fetch(PDO::FETCH_ASSOC);
        if (!$miner) {
            $pdo->rollBack();
            return 0;
        }

        $units = pending_accrual($miner);
        if ($units <= 0) {
            $pdo->rollBack();
            return 0;
        }

        // Reset the clock to now, not forward by what we paid: anything above
        // the cap is deliberately forfeited, which is the point of the cap.
        $pdo->prepare('UPDATE miners SET collected_at = UNIX_TIMESTAMP() WHERE user_id = ?')->execute([$userId]);
        $pdo->prepare('UPDATE users SET balance = balance + ? WHERE id = ?')->execute([$units, $userId]);
        $pdo->prepare('INSERT INTO ledger (user_id, kind, units, created_at) VALUES (?, "mining", ?, NOW())')
            ->execute([$userId, $units]);

        $pdo->commit();
        return $units;
    } catch (Throwable $e) {
        $pdo->rollBack();
        throw $e;
    }
}

Using it

Show the fill level as a percentage of capacity and the time until full. "Your miner is full" is the notification that brings people back.

Express capacity in hours of production rather than in units, so raising the rate does not silently shorten the cycle.

What bites people

Store the timestamp as an integer, not a DATETIME string parsed in PHP. Timezone handling between MySQL and PHP is where accrual bugs hide.

Compute on read, but always collect inside a transaction with the row locked. Two tabs collecting at once is the same double-claim problem as a faucet.

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