Live repricing
Change the CREDIT-to-USD rates on a running economy by rebuilding over the same store.
Source src/adapters/rates.ts#L84configuredRatessrc/economy.ts#L92createEconomy
Rates and config freeze into an economy at construction — on purpose, so a half-applied change
can never price two legs of one posting differently. The sanctioned way to change them is a
rebuild: createEconomy(ports) is cheap because the store
carries all the state, and a rebuild over the same store is the same economy with new policy.
Two pieces make the reprice safe:
- A fresh table, re-asserted. The new
configuredRatesbuild rides a new bag intocreateEconomy, which re-checksbuy ≥ par— a bad table refuses to build. Each build hands out frozenRateobjects whoserateIdembeds the value, so no reader sees a torn rate and every posting records exactly which rate priced it. - Quiesce before the rebuild. A payout in flight was reserved at the old rate and would
settle at the new one.
read.payouts({ states: ['REQUESTED', 'RESERVED', 'SUBMITTED'] })streams exactly the sagas that must reach zero first.
import {
DEV_RATES,
createEconomy,
credits,
memoryPorts,
systemActor,
topUp,
} from '@pwngh/economy-lab';
import { configuredRates } from '@pwngh/economy-lab/adapters';
import type { Ports, RatesConfig } from '@pwngh/economy-lab';
import type { SnippetReport } from './context.ts';
// Rates freeze into an economy at construction, and construction asserts buy >= par. So a
// reprice is a rebuild over the SAME store with a fresh rate table: cheap (the store carries all
// the state), atomic (no reader ever sees a torn rate — each table hands out frozen Rate objects
// whose rateId embeds the value), and re-asserted (a bad table refuses to build).
export async function run(): Promise<SnippetReport> {
let ports: Ports = memoryPorts({ signingKey: 'docs-signing-key' });
let economy = createEconomy(ports);
await economy.submit(
topUp({
idempotencyKey: 'idem_before_reprice',
actor: systemActor('payments'),
userId: 'usr_reprice',
amount: credits(100),
source: 'card',
}),
);
const before = ports.rates.buy('CREDIT');
// Quiesce first: a payout in flight was reserved at the old rate and would settle at the new.
let inFlight = 0;
for await (const saga of economy.read.payouts({
states: ['REQUESTED', 'RESERVED', 'SUBMITTED'],
})) {
void saga;
inFlight += 1;
}
// The reprice: a new table in a new bag, rebuilt over the same store.
const next: RatesConfig = { ...DEV_RATES, buyRate: 9000n };
ports = { ...ports, rates: configuredRates(next) };
economy = createEconomy(ports);
const after = ports.rates.buy('CREDIT');
return {
lines: [
`payouts in flight before repricing: ${inFlight} — safe to proceed`,
`buy rate before: ${before.rateId}`,
`buy rate after: ${after.rateId}`,
],
consolePath: '/controls',
};
}If you run the background worker, rebuild it from the
same new bag: the worker settles payouts at the rates it was built with, so an economy rebuilt
without it would settle payouts at the stale table. The console rebuilds both together —
createEconomy(ports) then createWorker(ports, economy).
The buy rate may move freely between quiesced payouts; changing par also revalues the
backing requirement, so treat it as a governed operation with the solvency check re-run after.