Rates
The rates port supplying the fixed CREDIT↔USD buy, par, and payout rates.
Source src/ports.ts#L149-L167 · Ratessrc/ports.ts#L176 · Ratesrc/adapters/rates.ts#L60-L104 · configuredRates
The seam
Somewhere, the economy has to answer a single question: how many real dollars is a credit worth? Buying credits, backing them, and paying creators out all turn on that number. The Rates port is the seam where that answer comes in, so the core never hard-codes a price and never trusts a caller to supply one.
The port is small on purpose. It hands the rest of the system three rates, each tagged with the exact rateId it used, and nothing else. A top-up reads one rate, the backing check reads another, and a payout saga reads a third — and because each call returns the rate's id, a transaction can record which number it priced against.
The Rates interface lives in src/ports.ts. It exposes exactly three methods, one per rate.
The three rates
The whole money model is built on three fixed CREDIT-to-USD rates, and this port is where they enter. That page owns the meaning of buy, par, and the spread in full — here is just enough to read the port's surface.
Each method answers a different question:
| Method | Returns | The question it answers |
|---|---|---|
buy(currency) | The acquisition rate | What does a user pay per credit when buying? |
par(currency) | The redemption/backing rate | What does a credit cash out at, and how much real USD must back it? |
payout(from, to, at) | The creator settlement rate | What does an earned credit convert to USD at? (equals par) |
The rates hold the order buy ≥ par ≥ payout, and the buy-to-par gap is the platform spread. The full story of why that ordering matters lives on the money-model page.
Two of the methods are synchronous and one is not. buy and par return a Rate directly, because they're constants the core reads on the hot path. payout is async and takes an at timestamp — it carries the shape a live, time-varying settlement feed would need, even though the reference adapter ignores the timestamp.
A Rate is three integers, not a float: { rate, scale, rateId }, where the multiplier is rate / 10^scale. Storing the price as exact integers keeps conversion free of floating-point drift. See Rate in src/ports.ts.
The configured adapter
The default adapter is configuredRates in src/adapters/rates.ts. You hand it a RatesConfig — six integers, a rate and a scale for each of the three rates — and it returns a Rates instance that serves them back.
let rates = configuredRates({
buyRate: 8333n, buyScale: 6, // buy ~120 credits/USD ($0.00833)
parRate: 5n, parScale: 3, // par/payout ~200 credits/USD ($0.005)
payoutRate: 5n, payoutScale: 3, // a ~40% spread
});
Each returned Rate carries a descriptive rateId the adapter builds from the config, like buy:CREDIT->USD:8333/6. That id is what a transaction records, so an auditor can later see exactly which configured number priced a given move.
Only CREDIT and USD exist in the lab. A same-currency conversion returns a 1:1 identity rate, and USD is the base unit, so buy('USD') and par('USD') are both 1:1. Any other currency pair is a wiring bug rather than a missing feature.
Fixed in the lab — no live feed
The thing this port deliberately does not do is track a market. Credits are platform-priced, not market-priced, so the rates are business constants a deployment sets, not quotes from an exchange. There is no FX feed, no refresh, and no clock-driven movement — configuredRates returns the same numbers every time it's asked.
That at timestamp on payout is the one place the contract leaves room for a live source. A real settlement feed would price CREDIT-to-USD as of a moment in time; the reference adapter accepts the timestamp and ignores it. Wiring a live feed would mean writing a new adapter that honors at, with the rest of the core unchanged, because everything downstream already reads through this seam.
What the core assumes is narrow: that a rate read returns promptly, that it carries a stable rateId to record, and that the three rates keep their order. The configured adapter meets all three by construction. A live feed would have to as well.