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:
@@ -289,6 +289,7 @@ router.get("/jobs/:id", async (req: Request, res: Response) => {
|
||||
costLedger: {
|
||||
actualInputTokens: job.actualInputTokens,
|
||||
actualOutputTokens: job.actualOutputTokens,
|
||||
totalTokens: (job.actualInputTokens ?? 0) + (job.actualOutputTokens ?? 0),
|
||||
actualCostUsd: job.actualCostUsd,
|
||||
estimatedCostUsd: job.estimatedCostUsd,
|
||||
marginPct: job.marginPct,
|
||||
|
||||
@@ -40,13 +40,42 @@ export const JobState = {
|
||||
failed: "failed",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Cost breakdown shown with the work invoice (estimations at invoice-creation time)
|
||||
*/
|
||||
export interface PricingBreakdown {
|
||||
/** Total estimated cost in USD (token cost + DO infra + margin) */
|
||||
estimatedCostUsd?: number;
|
||||
/** Originator margin percentage applied */
|
||||
marginPct?: number;
|
||||
/** BTC/USD spot price used to convert the invoice to sats */
|
||||
btcPriceUsd?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Actual cost record stored after the job completes
|
||||
*/
|
||||
export interface CostLedger {
|
||||
actualInputTokens?: number;
|
||||
actualOutputTokens?: number;
|
||||
/** Sum of actualInputTokens + actualOutputTokens */
|
||||
totalTokens?: number;
|
||||
/** Raw Anthropic token cost (no infra, no margin) */
|
||||
actualCostUsd?: number;
|
||||
estimatedCostUsd?: number;
|
||||
marginPct?: number;
|
||||
btcPriceUsd?: number;
|
||||
}
|
||||
|
||||
export interface JobStatusResponse {
|
||||
jobId: string;
|
||||
state: JobState;
|
||||
evalInvoice?: InvoiceInfo;
|
||||
workInvoice?: InvoiceInfo;
|
||||
pricingBreakdown?: PricingBreakdown;
|
||||
reason?: string;
|
||||
result?: string;
|
||||
costLedger?: CostLedger;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -202,6 +202,9 @@ components:
|
||||
type: integer
|
||||
actualOutputTokens:
|
||||
type: integer
|
||||
totalTokens:
|
||||
type: integer
|
||||
description: Sum of actualInputTokens + actualOutputTokens
|
||||
actualCostUsd:
|
||||
type: number
|
||||
description: Raw Anthropic token cost (no infra, no margin)
|
||||
|
||||
@@ -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({
|
||||
|
||||
22
lib/api-zod/src/generated/types/costLedger.ts
Normal file
22
lib/api-zod/src/generated/types/costLedger.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Generated by orval v8.5.3 🍺
|
||||
* Do not edit manually.
|
||||
* Api
|
||||
* API specification
|
||||
* OpenAPI spec version: 0.1.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Actual cost record stored after the job completes
|
||||
*/
|
||||
export interface CostLedger {
|
||||
actualInputTokens?: number;
|
||||
actualOutputTokens?: number;
|
||||
/** Sum of actualInputTokens + actualOutputTokens */
|
||||
totalTokens?: number;
|
||||
/** Raw Anthropic token cost (no infra, no margin) */
|
||||
actualCostUsd?: number;
|
||||
estimatedCostUsd?: number;
|
||||
marginPct?: number;
|
||||
btcPriceUsd?: number;
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
* OpenAPI spec version: 0.1.0
|
||||
*/
|
||||
|
||||
export * from "./costLedger";
|
||||
export * from "./createJobRequest";
|
||||
export * from "./createJobResponse";
|
||||
export * from "./demoResponse";
|
||||
@@ -14,4 +15,5 @@ export * from "./healthStatus";
|
||||
export * from "./invoiceInfo";
|
||||
export * from "./jobState";
|
||||
export * from "./jobStatusResponse";
|
||||
export * from "./pricingBreakdown";
|
||||
export * from "./runDemoParams";
|
||||
|
||||
@@ -5,15 +5,19 @@
|
||||
* API specification
|
||||
* OpenAPI spec version: 0.1.0
|
||||
*/
|
||||
import type { CostLedger } from "./costLedger";
|
||||
import type { InvoiceInfo } from "./invoiceInfo";
|
||||
import type { JobState } from "./jobState";
|
||||
import type { PricingBreakdown } from "./pricingBreakdown";
|
||||
|
||||
export interface JobStatusResponse {
|
||||
jobId: string;
|
||||
state: JobState;
|
||||
evalInvoice?: InvoiceInfo;
|
||||
workInvoice?: InvoiceInfo;
|
||||
pricingBreakdown?: PricingBreakdown;
|
||||
reason?: string;
|
||||
result?: string;
|
||||
costLedger?: CostLedger;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
19
lib/api-zod/src/generated/types/pricingBreakdown.ts
Normal file
19
lib/api-zod/src/generated/types/pricingBreakdown.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Generated by orval v8.5.3 🍺
|
||||
* Do not edit manually.
|
||||
* Api
|
||||
* API specification
|
||||
* OpenAPI spec version: 0.1.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Cost breakdown shown with the work invoice (estimations at invoice-creation time)
|
||||
*/
|
||||
export interface PricingBreakdown {
|
||||
/** Total estimated cost in USD (token cost + DO infra + margin) */
|
||||
estimatedCostUsd?: number;
|
||||
/** Originator margin percentage applied */
|
||||
marginPct?: number;
|
||||
/** BTC/USD spot price used to convert the invoice to sats */
|
||||
btcPriceUsd?: number;
|
||||
}
|
||||
17
lib/db/migrations/0002_cost_based_pricing.sql
Normal file
17
lib/db/migrations/0002_cost_based_pricing.sql
Normal file
@@ -0,0 +1,17 @@
|
||||
-- Migration: Add cost-based pricing columns to jobs table
|
||||
-- Task #6: Cost-based work fee pricing with BTC oracle
|
||||
|
||||
ALTER TABLE jobs
|
||||
ADD COLUMN IF NOT EXISTS estimated_cost_usd REAL,
|
||||
ADD COLUMN IF NOT EXISTS margin_pct REAL,
|
||||
ADD COLUMN IF NOT EXISTS btc_price_usd REAL,
|
||||
ADD COLUMN IF NOT EXISTS actual_input_tokens INTEGER,
|
||||
ADD COLUMN IF NOT EXISTS actual_output_tokens INTEGER,
|
||||
ADD COLUMN IF NOT EXISTS actual_cost_usd REAL;
|
||||
|
||||
COMMENT ON COLUMN jobs.estimated_cost_usd IS 'Total estimated cost in USD at work invoice creation time (token + DO infra + margin)';
|
||||
COMMENT ON COLUMN jobs.margin_pct IS 'Originator margin percentage applied to this job';
|
||||
COMMENT ON COLUMN jobs.btc_price_usd IS 'BTC/USD price used to convert work fee to sats (from CoinGecko at invoice time)';
|
||||
COMMENT ON COLUMN jobs.actual_input_tokens IS 'Actual input token count reported by Anthropic after work execution';
|
||||
COMMENT ON COLUMN jobs.actual_output_tokens IS 'Actual output token count reported by Anthropic after work execution';
|
||||
COMMENT ON COLUMN jobs.actual_cost_usd IS 'Actual raw Anthropic token cost (no infra amortisation, no margin) after work execution';
|
||||
Reference in New Issue
Block a user