turnley.dev

PHP and MySQL code for faucet operators

Tools & Calculators

LDG-006

Rescale a live economy in one migration

Moving the unit rate means moving every stored figure with it, in one operation, guarded so a resumed run cannot do it twice.

Sooner or later the internal unit turns out to be the wrong size — too small to pay anyone meaningfully, or so large that the smallest win rounds to nothing. Fixing it means changing the rate and every figure denominated in it.

It has to be one operation. Any order of two separate steps has a window where the rate and the balances disagree, and whatever happens in that window is wrong by the ratio.

Guard it with a marker. A migration that is resumed or run by hand must not rescale twice — that is the failure that has bitten real sites, and the second run is far harder to unpick than the first.

SQL
-- 034_rescale_credit.sql
-- Rate moves from 1e-8 to 1e-7 per unit, so every stored figure divides by 10.
-- Real value is unchanged: 1,538,420 at 1e-8 becomes 153,842 at 1e-7.
-- @detect: SELECT 1 FROM settings WHERE k = &#039;credit_rescaled_at&#039; AND v <> &#039;&#039;

-- Guard: do nothing if a rescale has already been recorded.
SET @already := (SELECT COALESCE(MAX(v), &#039;&#039;) FROM settings WHERE k = &#039;credit_rescaled_at&#039;);
SET @factor  := IF(@already = &#039;&#039;, 10, 1);           -- 1 = no-op on a second run

-- Everything denominated in the unit moves together, in one file.
UPDATE users              SET balance     = FLOOR(balance / @factor);
UPDATE users              SET entitlement = FLOOR(entitlement / @factor);
UPDATE entitlement_ledger SET units       = FLOOR(units / @factor);
UPDATE jackpot            SET pot_units   = FLOOR(pot_units / @factor),
                              seed_units  = FLOOR(seed_units / @factor);

-- The rate itself, last.
UPDATE settings SET v = &#039;0.0000001&#039; WHERE k = &#039;unit_to_coin_rate&#039; AND @factor > 1;

-- Record that it happened, which is also the guard for next time.
INSERT INTO settings (k, v, grp, kind) VALUES (&#039;credit_rescaled_at&#039;, NOW(), &#039;economy&#039;, &#039;hidden&#039;)
  ON DUPLICATE KEY UPDATE v = IF(v = &#039;&#039;, NOW(), v);

-- Deliberately NOT touched:
--   withdrawals in flight  — each froze its own rate and knows what it is worth
--   ledger history         — it is history; rewriting it destroys the audit trail
--   operator thresholds    — minimum withdrawal is a decision, not a derived figure

Using it

Work an example with real numbers before running it, and check the value is identical before and after. That is the only test that matters.

Tell users. A balance that changes by a factor of ten with no explanation looks like theft even when it is not — a site notice costs nothing.

Leave in-flight withdrawals alone, and say why in the migration comments so the next person does not "fix" it.

What bites people

Rescaling twice is the failure mode. The marker is not optional.

FLOOR, not ROUND, and accept the dust. Rounding up across every row mints money.

Operator thresholds are expressed in the old scale too. They are a judgement call, so flag them in the release notes rather than moving them silently.

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 Money and Ledger