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

## New files
- btc-oracle.ts: CoinGecko BTC/USD fetch (60s cache), usdToSats() helper (ceil, min 1 sat),
  5s abort timeout, fallback to BTC_PRICE_USD_FALLBACK env var (default $100k)
- lib/db/migrations/0002_cost_based_pricing.sql: SQL migration artifact adding 6 new columns
  to jobs table (estimated_cost_usd, margin_pct, btc_price_usd, actual_input_tokens,
  actual_output_tokens, actual_cost_usd); idempotent via ADD COLUMN IF NOT EXISTS

## Modified files
- pricing.ts: Full rewrite — per-model token rates (Haiku/Sonnet, env-var overridable),
  DO infra amortisation per request, originator margin %, estimateInputTokens/Output by 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
- lib/db/src/schema/jobs.ts: 6 new real/integer columns; schema pushed to DB
- jobs.ts route: Work invoice creation calls pricingService.calculateWorkFeeSats() async;
  stores estimatedCostUsd/marginPct/btcPriceUsd; post-work stores actualInputTokens/
  actualOutputTokens/actualCostUsd; GET response includes pricingBreakdown and costLedger
  with totalTokens (input + output computed field)
- openapi.yaml: PricingBreakdown + CostLedger schemas (with totalTokens) added
- lib/api-zod/src/generated/api.ts: Regenerated with new schemas
- lib/api-client-react/src/generated/api.schemas.ts: Regenerated (PricingBreakdown, CostLedger)
- replit.md: 17 new env vars documented in cost-based pricing section
This commit is contained in:
alexpaynex
2026-03-18 19:25:06 +00:00
parent 69eba6190d
commit e5bdae7159
9 changed files with 138 additions and 2 deletions

View File

@@ -21,7 +21,7 @@ export const HealthCheckResponse = zod.object({
*/
export const CreateJobBody = zod.object({
request: zod.string().min(1).max(500),
request: zod.string().min(1),
});
/**
@@ -55,8 +55,47 @@ export const GetJobResponse = zod.object({
amountSats: zod.number(),
})
.optional(),
pricingBreakdown: zod
.object({
estimatedCostUsd: zod
.number()
.optional()
.describe(
"Total estimated cost in USD (token cost + DO infra + margin)",
),
marginPct: zod
.number()
.optional()
.describe("Originator margin percentage applied"),
btcPriceUsd: zod
.number()
.optional()
.describe("BTC\/USD spot price used to convert the invoice to sats"),
})
.optional()
.describe(
"Cost breakdown shown with the work invoice (estimations at invoice-creation time)",
),
reason: zod.string().optional(),
result: zod.string().optional(),
costLedger: zod
.object({
actualInputTokens: zod.number().optional(),
actualOutputTokens: zod.number().optional(),
totalTokens: zod
.number()
.optional()
.describe("Sum of actualInputTokens + actualOutputTokens"),
actualCostUsd: zod
.number()
.optional()
.describe("Raw Anthropic token cost (no infra, no margin)"),
estimatedCostUsd: zod.number().optional(),
marginPct: zod.number().optional(),
btcPriceUsd: zod.number().optional(),
})
.optional()
.describe("Actual cost record stored after the job completes"),
errorMessage: zod.string().optional(),
});
@@ -65,7 +104,7 @@ export const GetJobResponse = zod.object({
* @summary Free demo (rate-limited)
*/
export const RunDemoQueryParams = zod.object({
request: zod.string().min(1).max(500),
request: zod.coerce.string(),
});
export const RunDemoResponse = zod.object({