turnley.dev

PHP and MySQL code for faucet operators

Tools & Calculators

PRC-003

Refuse to pay on a stale price

The guard that turns a dead price feed into a delayed payout instead of a wrong one. Fail closed, log loudly, retry later.

When a price feed dies during a volatile hour, the last cached value can be badly wrong within minutes. Paying USD-denominated rewards from it means overpaying every single claim until someone notices.

The guard is one comparison and a thrown exception. Anything converting a dollar figure to coin asks how old the price is, and refuses above a threshold.

A refused payout is recoverable — it sits in the queue and pays when the feed returns. A payout made at a wrong price is gone.

PHP
class StalePriceException extends RuntimeException {}

function usd_to_units_guarded(float $usd, string $coinId, int $unitsPerCoin, int $maxAgeSeconds = 900): int
{
    $feed = coin_price($coinId);

    if ($feed['price'] <= 0) {
        throw new StalePriceException('no usable price for ' . $coinId);
    }
    if ($feed['age'] > $maxAgeSeconds) {
        throw new StalePriceException(
            sprintf('price for %s is %d seconds old (limit %d) — refusing to pay', $coinId, $feed['age'], $maxAgeSeconds)
        );
    }

    $units = (int) floor(($usd / $feed['price']) * $unitsPerCoin);
    if ($units < 1) {
        throw new RuntimeException('amount rounds to zero units');
    }
    return $units;
}

// In the payout worker:
//
// try {
//     $units = usd_to_units_guarded($w['usd'], 'litecoin', 100000000);
// } catch (StalePriceException $e) {
//     hold_withdrawal($pdo, $w['id'], $e->getMessage());   // stays queued, pays later
//     continue;
// }

Using it

Use a distinct exception class. The payout worker needs to tell "price problem, retry later" apart from "this withdrawal is invalid, reject it".

Alert on the first stale refusal rather than the hundredth. A dead feed is an outage, and the queue quietly filling up is not how you want to find out.

Peg to a stablecoin where you can. With USDT the price is one and the whole problem disappears.

What bites people

Fifteen minutes is generous. In a fast market, five is more honest.

Do not fall back to a hardcoded price. A number in the source is stale by definition and will be wrong in the direction nobody checked.

The guard belongs in the conversion function, not in each caller. One place to get right beats twelve places to forget.

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 Price Feeds