97 lines
2.6 KiB
TypeScript
97 lines
2.6 KiB
TypeScript
/**
|
||
* Per-skill bonding curve — demand/supply pricing for intent marketplace.
|
||
*
|
||
* Formula: price = BASE_SKILL_PRICE × (demandHours / supplyHours) ^ ELASTICITY
|
||
*
|
||
* High demand + low supply → higher price → incentivizes offers.
|
||
* All prices in tokens per hour.
|
||
*/
|
||
|
||
import type { Skill } from './schemas';
|
||
import type { SkillCurvesDoc, SkillCurveEntry } from './schemas-intent';
|
||
|
||
// ── Curve parameters ──
|
||
|
||
/** Base price in tokens per hour */
|
||
export const BASE_SKILL_PRICE = 100;
|
||
|
||
/** Elasticity exponent — 0.5 = square root (moderate responsiveness) */
|
||
export const ELASTICITY = 0.5;
|
||
|
||
/** Minimum supply to avoid division by zero / extreme prices */
|
||
const MIN_SUPPLY = 1;
|
||
|
||
/** Maximum price cap (10x base) to prevent runaway pricing */
|
||
const MAX_PRICE = BASE_SKILL_PRICE * 10;
|
||
|
||
// ── Price functions ──
|
||
|
||
/**
|
||
* Calculate the current price per hour for a skill.
|
||
* Returns tokens per hour.
|
||
*/
|
||
export function getSkillPrice(skill: Skill, curves: SkillCurvesDoc): number {
|
||
const curve = curves.curves[skill];
|
||
if (!curve || curve.supplyHours <= 0) return BASE_SKILL_PRICE;
|
||
|
||
const supply = Math.max(curve.supplyHours, MIN_SUPPLY);
|
||
const demand = Math.max(curve.demandHours, 0);
|
||
const ratio = demand / supply;
|
||
|
||
const price = Math.round(BASE_SKILL_PRICE * Math.pow(ratio, ELASTICITY));
|
||
return Math.min(price, MAX_PRICE);
|
||
}
|
||
|
||
/**
|
||
* Calculate the total cost for a need intent (hours × skill price).
|
||
*/
|
||
export function calculateIntentCost(skill: Skill, hours: number, curves: SkillCurvesDoc): number {
|
||
const price = getSkillPrice(skill, curves);
|
||
return price * hours;
|
||
}
|
||
|
||
/**
|
||
* Get all current skill prices.
|
||
*/
|
||
export function getAllSkillPrices(curves: SkillCurvesDoc): Record<Skill, number> {
|
||
const skills: Skill[] = ['facilitation', 'design', 'tech', 'outreach', 'logistics'];
|
||
const prices = {} as Record<Skill, number>;
|
||
for (const skill of skills) {
|
||
prices[skill] = getSkillPrice(skill, curves);
|
||
}
|
||
return prices;
|
||
}
|
||
|
||
/**
|
||
* Create or update a skill curve entry after an intent is created/withdrawn.
|
||
* Returns the updated curve entry.
|
||
*/
|
||
export function recalculateCurve(
|
||
skill: Skill,
|
||
supplyHours: number,
|
||
demandHours: number,
|
||
): Omit<SkillCurveEntry, 'history'> {
|
||
const supply = Math.max(supplyHours, MIN_SUPPLY);
|
||
const demand = Math.max(demandHours, 0);
|
||
const ratio = demand / supply;
|
||
const price = Math.min(Math.round(BASE_SKILL_PRICE * Math.pow(ratio, ELASTICITY)), MAX_PRICE);
|
||
|
||
return {
|
||
skill,
|
||
supplyHours,
|
||
demandHours,
|
||
currentPrice: price,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Get curve configuration for UI display.
|
||
*/
|
||
export function getSkillCurveConfig() {
|
||
return {
|
||
basePrice: BASE_SKILL_PRICE,
|
||
elasticity: ELASTICITY,
|
||
maxPrice: MAX_PRICE,
|
||
};
|
||
}
|