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

@@ -50,9 +50,17 @@ async function advanceJob(job: Job): Promise<Job | null> {
const evalResult = await agentService.evaluateRequest(job.request);
if (evalResult.accepted) {
const workFee = pricingService.calculateWorkFeeSats(job.request);
// Cost-based work fee: estimate tokens → fetch BTC price → invoice sats
const inputEst = pricingService.estimateInputTokens(job.request);
const outputEst = pricingService.estimateOutputTokens(job.request);
const breakdown = await pricingService.calculateWorkFeeSats(
inputEst,
outputEst,
agentService.workModel,
);
const workInvoiceData = await lnbitsService.createInvoice(
workFee,
breakdown.amountSats,
`Work fee for job ${job.id}`,
);
const workInvoiceId = randomUUID();
@@ -63,7 +71,7 @@ async function advanceJob(job: Job): Promise<Job | null> {
jobId: job.id,
paymentHash: workInvoiceData.paymentHash,
paymentRequest: workInvoiceData.paymentRequest,
amountSats: workFee,
amountSats: breakdown.amountSats,
type: "work",
paid: false,
});
@@ -72,7 +80,10 @@ async function advanceJob(job: Job): Promise<Job | null> {
.set({
state: "awaiting_work_payment",
workInvoiceId,
workAmountSats: workFee,
workAmountSats: breakdown.amountSats,
estimatedCostUsd: breakdown.estimatedCostUsd,
marginPct: breakdown.marginPct,
btcPriceUsd: breakdown.btcPriceUsd,
updatedAt: new Date(),
})
.where(eq(jobs.id, job.id));
@@ -118,9 +129,22 @@ async function advanceJob(job: Job): Promise<Job | null> {
try {
const workResult = await agentService.executeWork(job.request);
const actualCostUsd = pricingService.calculateActualCostUsd(
workResult.inputTokens,
workResult.outputTokens,
agentService.workModel,
);
await db
.update(jobs)
.set({ state: "complete", result: workResult.result, updatedAt: new Date() })
.set({
state: "complete",
result: workResult.result,
actualInputTokens: workResult.inputTokens,
actualOutputTokens: workResult.outputTokens,
actualCostUsd,
updatedAt: new Date(),
})
.where(eq(jobs.id, job.id));
} catch (err) {
const message = err instanceof Error ? err.message : "Execution error";
@@ -228,6 +252,7 @@ router.get("/jobs/:id", async (req: Request, res: Response) => {
});
break;
}
case "awaiting_work_payment": {
const inv = job.workInvoiceId ? await getInvoiceById(job.workInvoiceId) : null;
res.json({
@@ -239,18 +264,44 @@ router.get("/jobs/:id", async (req: Request, res: Response) => {
...(lnbitsService.stubMode ? { paymentHash: inv.paymentHash } : {}),
},
} : {}),
// Cost breakdown so callers understand the invoice amount
...(job.estimatedCostUsd != null ? {
pricingBreakdown: {
estimatedCostUsd: job.estimatedCostUsd,
marginPct: job.marginPct,
btcPriceUsd: job.btcPriceUsd,
},
} : {}),
});
break;
}
case "rejected":
res.json({ ...base, reason: job.rejectionReason ?? undefined });
break;
case "complete":
res.json({ ...base, result: job.result ?? undefined });
res.json({
...base,
result: job.result ?? undefined,
// Actual token usage + cost ledger
...(job.actualCostUsd != null ? {
costLedger: {
actualInputTokens: job.actualInputTokens,
actualOutputTokens: job.actualOutputTokens,
actualCostUsd: job.actualCostUsd,
estimatedCostUsd: job.estimatedCostUsd,
marginPct: job.marginPct,
btcPriceUsd: job.btcPriceUsd,
},
} : {}),
});
break;
case "failed":
res.json({ ...base, errorMessage: job.errorMessage ?? undefined });
break;
default:
res.json(base);
}