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:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user