FP-003
Convert USD balances to coin units for a payout
If your ledger is in dollars and your payout API is in satoshi, this is the conversion, with the guard that stops a stale price from emptying your wallet.
Sites that sell advertising or run offerwalls usually keep balances in USD. Payout APIs almost always want the coin's smallest unit. The conversion itself is two multiplications; the danger is doing it with a price you fetched an hour ago, or with no price at all because the feed timed out and your code fell back to zero.
The guard below refuses to convert on a stale or absent price. Refusing to pay is recoverable. Paying a hundred times the right amount is not.
Pegging to a stablecoin sidesteps the whole problem: set usd_per_coin to 1 for USDT and you never need a live feed.
function usd_to_units(float $usd, float $usdPerCoin, int $unitsPerCoin, int $priceAgeSeconds, int $maxAge = 900): int
{
if ($usd <= 0) {
throw new InvalidArgumentException('amount must be positive');
}
if ($usdPerCoin <= 0) {
throw new RuntimeException('no usable price for this coin');
}
if ($priceAgeSeconds > $maxAge) {
throw new RuntimeException('price is stale (' . $priceAgeSeconds . 's) — refusing to pay');
}
$coins = $usd / $usdPerCoin;
$units = (int) floor($coins * $unitsPerCoin); // always floor, never round up
if ($units < 1) {
throw new RuntimeException('amount rounds to zero units');
}
return $units;
}
Using it
For a stablecoin, usd_per_coin is 1 and units_per_coin is 100000000. A one dollar payout is 100,000,000 units.
Store the rate you used on the payout row alongside the amount. When someone disputes a payout six weeks later, that column is the whole answer.
Floor rather than round. Rounding up by one unit per payout across ten thousand payouts is a slow leak with no entry in your ledger.
What bites people
A price feed that fails should raise, not return zero. Zero passes every naive check and produces a payout of nothing, or worse, a division that hands out everything.
Keep the maximum age tight. Fifteen minutes is generous for a volatile coin.
This one touches real money. Point it at a throwaway wallet and watch a full cycle before you trust it with a live balance.