2026-03-18 21:01:13 -04:00
|
|
|
import { makeLogger } from "./logger.js";
|
|
|
|
|
|
|
|
|
|
const logger = makeLogger("btc-oracle");
|
|
|
|
|
|
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
2026-03-18 19:20:34 +00:00
|
|
|
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();
|
2026-03-18 21:01:13 -04:00
|
|
|
logger.warn("price fetch failed — using fallback", {
|
|
|
|
|
fallback_usd: fb,
|
|
|
|
|
error: err instanceof Error ? err.message : String(err),
|
|
|
|
|
});
|
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
2026-03-18 19:20:34 +00:00
|
|
|
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));
|
|
|
|
|
}
|