Task #6: Cost-based work fee pricing with BTC oracle

- btc-oracle.ts: CoinGecko BTC/USD fetch (60s cache), usdToSats() helper,
  fallback to BTC_PRICE_USD_FALLBACK env var (default $100k), 5s abort timeout
- pricing.ts: Full rewrite — per-model token rates (Haiku/Sonnet, env-var
  overridable), DO infra amortisation, originator margin %, estimateInputTokens(),
  estimateOutputTokens() by request tier, calculateActualCostUsd() for post-work ledger,
  async calculateWorkFeeSats() → WorkFeeBreakdown
- agent.ts: WorkResult now includes inputTokens + outputTokens from Anthropic usage;
  workModel/evalModel exposed as readonly public; EVAL_MODEL/WORK_MODEL env var support
- jobs.ts: Work invoice creation calls pricingService.calculateWorkFeeSats() async;
  stores estimatedCostUsd/marginPct/btcPriceUsd on job; after executeWork stores
  actualInputTokens/actualOutputTokens/actualCostUsd; GET response includes
  pricingBreakdown (awaiting_work_payment) and costLedger (complete)
- lib/db/src/schema/jobs.ts: 6 new real/integer columns for cost tracking; schema pushed
- openapi.yaml: PricingBreakdown + CostLedger schemas added to JobStatusResponse
- replit.md: 17 new env vars documented in Cost-based work fee pricing section
This commit is contained in:
alexpaynex
2026-03-18 19:20:34 +00:00
parent bc78bfa452
commit 69eba6190d
7 changed files with 347 additions and 49 deletions

View File

@@ -0,0 +1,57 @@
const COINGECKO_URL =
"https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd";
interface CachedPrice {
price: number;
at: number;
}
let cache: CachedPrice | null = null;
const CACHE_MS = 60_000;
function fallbackPrice(): number {
const raw = parseFloat(process.env.BTC_PRICE_USD_FALLBACK ?? "");
return Number.isFinite(raw) && raw > 0 ? raw : 100_000;
}
/**
* Returns the current BTC/USD price.
* Caches for 60 s; falls back to BTC_PRICE_USD_FALLBACK env var (default $100,000)
* on network failure so the service keeps running without internet access.
*/
export async function getBtcPriceUsd(): Promise<number> {
if (cache && Date.now() - cache.at < CACHE_MS) {
return cache.price;
}
try {
const res = await fetch(COINGECKO_URL, {
headers: { Accept: "application/json" },
signal: AbortSignal.timeout(5_000),
});
if (!res.ok) throw new Error(`CoinGecko HTTP ${res.status}`);
const data = (await res.json()) as { bitcoin?: { usd?: number } };
const price = data?.bitcoin?.usd;
if (!price || !Number.isFinite(price) || price <= 0) {
throw new Error(`Unexpected CoinGecko response: ${JSON.stringify(data)}`);
}
cache = { price, at: Date.now() };
return price;
} catch (err) {
const fb = fallbackPrice();
console.warn(`[btc-oracle] Price fetch failed (using $${fb} fallback):`, err);
return fb;
}
}
/**
* Convert a USD amount to satoshis at the given BTC/USD price.
* Always rounds up (ceil) so costs are fully covered; minimum 1 sat.
*/
export function usdToSats(usd: number, btcPriceUsd: number): number {
if (usd <= 0) return 1;
return Math.max(1, Math.ceil((usd / btcPriceUsd) * 1e8));
}