turnley.dev

PHP and MySQL code for faucet operators

Tools & Calculators

PRC-002

Take the median of several price feeds

One API returning a wrong number should not decide what you pay. Three sources and a median makes a single bad quote irrelevant.

Price APIs go wrong in ways that are worse than going down. A thin exchange pair, a decimal error, a stale cache on their side — the response is a valid number that is simply wrong, and nothing in your code can tell.

Three independent sources and a median fixes it structurally. A single outlier is discarded by construction, without any threshold to tune or alert to respond to.

Two sources are not enough: with two, an average is dragged halfway toward the bad one and a median is undefined. Three is the smallest number that works.

PHP
function median_price(array $sources, float $maxSpread = 0.10): array
{
    $prices = [];
    foreach ($sources as $name => $fetcher) {
        try {
            $p = (float) $fetcher();
            if ($p > 0) {
                $prices[$name] = $p;
            }
        } catch (Throwable $e) {
            // one source down is expected, not fatal
        }
    }

    if (count($prices) === 0) {
        return ['price' => 0.0, 'ok' => false, 'reason' => 'no source responded'];
    }
    if (count($prices) === 1) {
        return ['price' => reset($prices), 'ok' => true, 'reason' => 'single source — unverified', 'sources' => $prices];
    }

    $values = array_values($prices);
    sort($values);
    $n = count($values);
    $median = $n % 2 ? $values[intdiv($n, 2)] : ($values[$n / 2 - 1] + $values[$n / 2]) / 2;

    // If even the median cannot be trusted, say so rather than paying on it.
    $spread = ($values[$n - 1] - $values[0]) / $median;
    if ($spread > $maxSpread) {
        return ['price' => $median, 'ok' => false,
                'reason' => 'sources disagree by ' . round($spread * 100, 1) . '%', 'sources' => $prices];
    }

    return ['price' => $median, 'ok' => true, 'sources' => $prices];
}

Using it

Log every source's answer alongside the median. When a payout looks wrong later, that record shows which feed was lying.

A ten percent spread is generous for a major coin and tight for an illiquid one. Set it per coin.

Cache the median, not the individual sources, so the rate limits apply to your cron and not to your visitors.

What bites people

Do not average. One source reporting a price ten times too high moves an average enormously and a median not at all.

An unresponsive source is not a zero. Exclude it from the set instead of letting it drag the result down.

Failing the spread check should stop payouts, not silently continue with the median.

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