The model reads the brand's website and never picks the number, so the same creator always gets the same fee Nakodo, a tool that generates brand outreach emails to creators, deliberately keeps fee calculation out of its LLM, using the model only to classify a brand's website into three enums (buyer type, price level, company size) that collapse into a single clamped multiplier. All downstream pricing is a pure, deterministic function so the same brand and creator always receive the same fee and every figure can be explained, with amounts snapped to human-friendly steps on a log scale. Nakodo https://nakodo.app/how-it-works writes the outreach email a brand sends a creator, and the newest part of that is the proposal: what the creator is asked to make, what they get to keep, and what they are paid. The fee goes in an email with the brand's name on it. The temptation with an LLM in the stack is obvious. It has read the brand's site, it knows the creator's follower count, so ask it for a fee. We do not, and the reason is in a comment at the top of the file: // Fees are worked out here rather than by the AI, so the same brand and // creator always get the same fee and every number can be explained: the AI // reads the brand's website how much its customers are worth, its value , // and a rate card turns that and each creator's audience into an amount. // Pure, so the proposal editor can use it too. Three requirements hide in that sentence, and none of them is about accuracy. The same inputs must give the same output. A creator asks what you pay. The brand opens the proposal editor, sees £180, and sends it. Two weeks later the same creator is re-contacted for a second campaign and the model, with a different temperature draw, says £240. Now the brand has a conversation it cannot explain, and the creator has evidence that the number is made up. A model call is a sample from a distribution. Money has to be a function. Every number must be explainable. When a brand asks "why 180?", the answer has to be a chain: this creator's typical views, times the rate for this format on this platform, times what your customers are worth, times the level you chose, floored and rounded. Each link is inspectable. "The model thought so" is not an answer you can give somebody who is about to spend their own money. It has to run in the browser. The proposal editor shows fees updating as you drag a tier boundary or switch from market rate to a fixed amount. That is a pure function or it is a network round trip per keystroke. The model does the thing it is good at, which is reading prose and returning a judgement from a closed set: js export const BUYERS = "consumers", "both", "businesses" as const; export const PRICE LEVELS = "low", "mid", "high", "enterprise" as const; export const COMPANY SIZES = "solo", "small", "medium", "large" as const; export type BrandRead = { buyer: typeof BUYERS number ; priceLevel: typeof PRICE LEVELS number ; companySize: typeof COMPANY SIZES number ; }; Three enums, three to four options each: 48 possible readings of a brand. It is answering "who buys this, is it cheap or expensive, how big is this company", which is exactly what a person gets from thirty seconds on a homepage. Those collapse into one number: js export function brandValue b: BrandRead : number { const v = BUYER FACTOR b.buyer PRICE FACTOR b.priceLevel SIZE FACTOR b.companySize ; return Math.round clamp v, 0.5, 3 100 / 100; } A single multiplier, clamped to between half and triple, rounded to two decimals. The clamp is there because three independent factors multiplied together have a product range wider than anybody's actual willingness to pay, and the rounding is so the value can be stored, shown, and edited by a human without ever rendering as 1.3799999999999999 . That multiplier is what crosses the boundary between the probabilistic part of the system and the arithmetic part. Everything downstream of it is a pure function of numbers, and the whole of it can be re-run against a hand-edited value when a brand disagrees with the model's read. Multiply a follower count by a rate and you get 211.68 . Nobody quotes 211.68 . People quote 200, 250, 175, 1,200. // Amounts people actually charge: 50, 75, 120, 150, 175, 200, 250, 300 and so // on, the nearest one on a log scale. Works the same in yen or forints. const NICE STEPS = 1, 1.2, 1.5, 1.75, 2, 2.5, 3, 3.5, 4, 4.5, 5, 6, 7, 7.5, 8, 9, 10 ; export function niceAmount n: number : number { if Number.isFinite n || n <= 0 return 0; const magnitude = 10 Math.floor Math.log10 n ; const x = n / magnitude; let best = NICE STEPS 0 ; for const s of NICE STEPS if Math.abs Math.log x / s < Math.abs Math.log x / best best = s; return Math.max 1, Math.round best magnitude ; } Paste that into Node and try it. It is self-contained: php 37 - 35 62 - 60 118 - 120 164 - 175 183 - 175 227 - 250 460 - 450 1234 - 1200 2750 - 3000 18400 - 17500 164000 - 175000 211.68 - 200 Two details make it work in every currency rather than just in pounds. The mantissa is extracted with 10 Math.floor Math.log10 n , so the step list is scale-free. The same seventeen steps give you 35 and 175 and 175,000. A fixed list of nice numbers would have needed a different list per currency, and 20,000 forints is as round a number as 50 pounds. The comparison is Math.abs Math.log x / s , which is distance in proportion rather than in units. That matters because the step list is unevenly spaced on purpose: 0.2 apart at the bottom of a decade and 1.0 apart at the top. Measuring in units would make the wide gaps near the top win more often than they should, and would resolve a lot of near-ties by accident. In proportion, 183 sits 4.6% above 175 and 8.5% below 200, so it rounds down to 175. A 5% difference in a fee reads the same whether the fee is 20 or 20,000, which is exactly the property you want from a rounder that has to work at both ends. There is also a floor, and it is a product decision rather than an arithmetic one: // No fee from an audience goes below this pounds : less isn't worth a // creator's afternoon, so it would read as an insult rather than an offer. const FLOOR GBP = 50; const floor = niceAmount FLOOR GBP CURRENCIES r.currency .rate ; return Math.max floor, niceAmount units RATES a.platform r.format r.value LEVEL FACTOR r.level rate ; A nano creator with 1,200 followers computes out to single digits. An email offering somebody eleven pounds to make a video is worse for the brand than an email offering nothing, so the arithmetic is allowed to produce a number the rate card did not. Note that the floor is itself passed through niceAmount after conversion. 50 pounds in Hungarian forints is 21,708, and the floor of a price range should look like a decision, so it becomes 20,000. Creators are grouped by size nano, micro, mid, macro, mega , each tier gets its own ask, offer and fee mode, and a tier's fee can either be worked out from each creator's own audience or be a flat amount for everyone in the tier. When it is from their own audience, the result is clamped into the tier's own range: js const range = tierFees tier, terms, a.platform ; // priced at the tier's endpoints const market = marketFee { ...a, platform: creatorPlatform a.platform }, { ...terms, format: tier.format } ; return { tier, amount: market === null ? range.low : clamp market, range.low, range.high }; So what a brand sees in the editor "micro creators: £120 to £400" is a promise, not a summary. No individual creator can be quoted outside the band the brand approved, even if their view count is freakish. And when the brand switches a tier to a flat fee, the starting suggestion is the fee for a creator in the middle of the tier, where "middle" is the geometric mean: export function middleFee t: Pick