CogniPrep sells access one assessment provider at a time. Unlock a provider and you get every test that provider has, forever.
The trouble with that as a product is that providers are wildly different sizes. One has 14 games. Another has 3. A single flat price either overcharges the reader facing the short suite or undercharges the one facing the long one, and both of those are visible to anyone who clicks two pages.
So there are two tiers. The interesting part is not the tiers. It is that no file in the repository says which tier a provider is on.
The price is computed
export const PROVIDER_TOKEN_PRICE = 1500; // standard
export const SMALL_PROVIDER_TOKEN_PRICE = 1000; // short suite
export const SMALL_PROVIDER_MAX_GAMES = 5;
function playableGameCount(provider: AssessmentProvider): number {
return getGamesByProvider(provider).filter((g) => g.implemented !== false).length;
}
function isSmallProvider(provider?: AssessmentProvider | string | null): boolean {
if (!provider) return false;
const count = playableGameCount(provider);
return count > 0 && count <= SMALL_PROVIDER_MAX_GAMES;
}
export function getProviderPrice(provider?: AssessmentProvider | string | null): number {
return isSmallProvider(provider) ? SMALL_PROVIDER_TOKEN_PRICE : PROVIDER_TOKEN_PRICE;
}
That is the whole pricing model. Four details in it are deliberate.
implemented !== false excludes coming soon entries. The unlock buys what is playable today, so a test that is listed but not built cannot raise the price. Without that filter, adding a placeholder row to the library is a price rise, which is a thing you would do by accident within a month.
The boundary is inclusive. Exactly five tests bills at the lower tier, not the higher one. That is a real product decision hiding in a <=, and it is the kind of decision that gets silently flipped by someone tidying a comparison operator, so it has a dedicated test that exists purely to fail when it changes.
An unrecognised provider is never small. A bad id falls through to the standard price. That is the safe direction: a typo can never buy a large suite at the short suite price. It is also the right answer for a marketing surface that has no specific provider in hand.
The bundle rides on the same tier. We also sell "this provider plus the feedback upgrade" in one checkout, and it is tiered by the same predicate rather than being a flat price. If it were flat, the bundle would be worth nothing on a short suite, where the unlock and the upgrade already come to the bundle price on their own. Both prices read isSmallProvider, so the two can never end up disagreeing about which tier a provider is on.
What deriving it actually removes
The thing this replaces is a hand kept table mapping provider to price. That table is not hard to write. It is hard to keep true, and the failure is asymmetric: the code keeps working, the tests stay green, and the only symptom is that a reader is charged the wrong amount.
Derived, the whole class goes away. Ship a new test for a short suite provider and its price moves on its own, in the same commit, everywhere. There is no second list to update and nothing to forget.
"Everywhere" is doing real work in that sentence. The same library feeds the marketing copy:
export const MIN_PROVIDER_PRICE: number = Math.min(...ALL_PROVIDERS.map(getProviderPrice));
Every "from £X" on the site reads that. The pricing page, the comparison page, the help page FAQ. If every short suite provider grew past five tests tomorrow, the lowest available price would genuinely be the standard one, and every page would say so in the same deploy, without anyone remembering that the comparison page exists.
A "from £X" that is computed from the actual minimum is a different kind of claim from one that is typed into a heading. The first cannot become false.
The incentive you have just created
Here is the part I did not anticipate, and it is the reason this post exists.
We recently added eleven new providers, built in parallel, each by a different worker from a research brief. Each brief named a target number of tests. And each worker could see the pricing rule.
Derived pricing means content decisions are price decisions. A worker that builds a sixth test moves its provider from £10 to £15. Nothing dishonest has to happen for that to go wrong: you are researching a vendor, the sources are thin, there is one more product in the marketing material that may or may not be a separate test, and the brief says six. The pull is entirely in one direction.
So the rule went into the shared plan document, in the pricing section, in bold:
Never invent a test that the vendor does not sell in order to reach a price tier. A provider on the lower tier built on real tests is correct; one on the higher tier padded with a fabricated test is a lie to a paying candidate. If your research shows the real suite is smaller than the brief assumed, build the smaller suite and say so in your report.
Two of the eleven came back smaller than their brief assumed and landed on the lower tier. That is the rule working, and it only worked because it was written down before anyone started, next to the thing that creates the pressure.
The general version: when you derive a number from your own content, look at which direction the derivation rewards, and write the counter-incentive into the same document as the formula. A derived value removes a drift bug and hands you an incentive problem in exchange. It is a good trade, but it is a trade.
Check our arithmetic
The two pages are public. The pricing page quotes the standard price and the "from" price. The comparison page quotes the same "from" figure, and it is the same constant, not a second copy someone kept in sync.
Then go and count. Korn Ferry has 3 tests and Watson Glaser has exactly 5, which is the inclusive boundary, so both are on the lower tier. Arctic Shores has 14 and HireVue has 13, so both are standard. Every one of those pages lists its tests, so the count behind the price is the count you can see.
If a provider page ever shows six tests at the lower price, the bug is real and it is ours.
Top comments (0)