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:
@@ -7,6 +7,8 @@ export interface EvalResult {
|
||||
|
||||
export interface WorkResult {
|
||||
result: string;
|
||||
inputTokens: number;
|
||||
outputTokens: number;
|
||||
}
|
||||
|
||||
export interface AgentConfig {
|
||||
@@ -15,12 +17,12 @@ export interface AgentConfig {
|
||||
}
|
||||
|
||||
export class AgentService {
|
||||
private readonly evalModel: string;
|
||||
private readonly workModel: string;
|
||||
readonly evalModel: string;
|
||||
readonly workModel: string;
|
||||
|
||||
constructor(config?: AgentConfig) {
|
||||
this.evalModel = config?.evalModel ?? "claude-haiku-4-5";
|
||||
this.workModel = config?.workModel ?? "claude-sonnet-4-6";
|
||||
this.evalModel = config?.evalModel ?? process.env.EVAL_MODEL ?? "claude-haiku-4-5";
|
||||
this.workModel = config?.workModel ?? process.env.WORK_MODEL ?? "claude-sonnet-4-6";
|
||||
}
|
||||
|
||||
async evaluateRequest(requestText: string): Promise<EvalResult> {
|
||||
@@ -64,7 +66,11 @@ Fulfill it thoroughly and helpfully. Be concise yet complete.`,
|
||||
throw new Error("Unexpected non-text response from work model");
|
||||
}
|
||||
|
||||
return { result: block.text };
|
||||
return {
|
||||
result: block.text,
|
||||
inputTokens: message.usage.input_tokens,
|
||||
outputTokens: message.usage.output_tokens,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
57
artifacts/api-server/src/lib/btc-oracle.ts
Normal file
57
artifacts/api-server/src/lib/btc-oracle.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
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();
|
||||
console.warn(`[btc-oracle] Price fetch failed (using $${fb} fallback):`, err);
|
||||
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));
|
||||
}
|
||||
@@ -1,48 +1,162 @@
|
||||
export interface PricingConfig {
|
||||
evalFeeSats?: number;
|
||||
workFeeShortSats?: number;
|
||||
workFeeMediumSats?: number;
|
||||
workFeeLongSats?: number;
|
||||
shortMaxChars?: number;
|
||||
mediumMaxChars?: number;
|
||||
bootstrapFeeSats?: number;
|
||||
import { getBtcPriceUsd, usdToSats } from "./btc-oracle.js";
|
||||
|
||||
// ── Env-var helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
function envFloat(name: string, fallback: number): number {
|
||||
const raw = parseFloat(process.env[name] ?? "");
|
||||
return Number.isFinite(raw) && raw > 0 ? raw : fallback;
|
||||
}
|
||||
|
||||
export class PricingService {
|
||||
private readonly evalFee: number;
|
||||
private readonly workFeeShort: number;
|
||||
private readonly workFeeMedium: number;
|
||||
private readonly workFeeLong: number;
|
||||
private readonly shortMax: number;
|
||||
private readonly mediumMax: number;
|
||||
private readonly bootstrapFee: number;
|
||||
function envInt(name: string, fallback: number): number {
|
||||
const raw = parseInt(process.env[name] ?? "", 10);
|
||||
return Number.isFinite(raw) && raw > 0 ? raw : fallback;
|
||||
}
|
||||
|
||||
constructor(config?: PricingConfig) {
|
||||
this.evalFee = config?.evalFeeSats ?? 10;
|
||||
this.workFeeShort = config?.workFeeShortSats ?? 50;
|
||||
this.workFeeMedium = config?.workFeeMediumSats ?? 100;
|
||||
this.workFeeLong = config?.workFeeLongSats ?? 250;
|
||||
this.shortMax = config?.shortMaxChars ?? 100;
|
||||
this.mediumMax = config?.mediumMaxChars ?? 300;
|
||||
const rawFee = parseInt(process.env.BOOTSTRAP_FEE_SATS ?? "", 10);
|
||||
this.bootstrapFee =
|
||||
config?.bootstrapFeeSats ??
|
||||
(Number.isFinite(rawFee) && rawFee > 0 ? rawFee : 10_000);
|
||||
}
|
||||
// ── Model rate tables ──────────────────────────────────────────────────────────
|
||||
|
||||
export interface ModelRates {
|
||||
inputPer1kUsd: number;
|
||||
outputPer1kUsd: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Anthropic model rates (USD per 1 000 tokens).
|
||||
* Defaults approximate claude-haiku-4-x / claude-sonnet-4-x pricing.
|
||||
* Override via env vars.
|
||||
*/
|
||||
const HAIKU_RATES: ModelRates = {
|
||||
inputPer1kUsd: envFloat("HAIKU_INPUT_COST_PER_1K_TOKENS", 0.0008),
|
||||
outputPer1kUsd: envFloat("HAIKU_OUTPUT_COST_PER_1K_TOKENS", 0.004),
|
||||
};
|
||||
|
||||
const SONNET_RATES: ModelRates = {
|
||||
inputPer1kUsd: envFloat("SONNET_INPUT_COST_PER_1K_TOKENS", 0.003),
|
||||
outputPer1kUsd: envFloat("SONNET_OUTPUT_COST_PER_1K_TOKENS", 0.015),
|
||||
};
|
||||
|
||||
function ratesForModel(modelId: string): ModelRates {
|
||||
const id = modelId.toLowerCase();
|
||||
if (id.includes("haiku")) return HAIKU_RATES;
|
||||
if (id.includes("sonnet")) return SONNET_RATES;
|
||||
return SONNET_RATES; // conservative fallback
|
||||
}
|
||||
|
||||
// ── Output token estimates by request length tier ─────────────────────────────
|
||||
|
||||
const OUTPUT_EST_SHORT = envInt("OUTPUT_TOKENS_SHORT_EST", 200);
|
||||
const OUTPUT_EST_MEDIUM = envInt("OUTPUT_TOKENS_MEDIUM_EST", 400);
|
||||
const OUTPUT_EST_LONG = envInt("OUTPUT_TOKENS_LONG_EST", 800);
|
||||
const SHORT_MAX_CHARS = envInt("SHORT_MAX_CHARS", 100);
|
||||
const MEDIUM_MAX_CHARS = envInt("MEDIUM_MAX_CHARS", 300);
|
||||
|
||||
// Approximate tokens in the work system prompt (for input token estimation).
|
||||
const WORK_SYSTEM_PROMPT_TOKENS = envInt("WORK_SYSTEM_PROMPT_TOKENS_EST", 50);
|
||||
|
||||
// ── DO infra amortisation ─────────────────────────────────────────────────────
|
||||
|
||||
const DO_MONTHLY_COST_USD = envFloat("DO_MONTHLY_COST_USD", 100);
|
||||
const DO_MONTHLY_REQUESTS = envInt("DO_MONTHLY_REQUEST_VOLUME", 1000);
|
||||
const DO_INFRA_PER_REQUEST_USD = DO_MONTHLY_COST_USD / DO_MONTHLY_REQUESTS;
|
||||
|
||||
// ── Originator margin ──────────────────────────────────────────────────────────
|
||||
|
||||
const ORIGINATOR_MARGIN_PCT = envFloat("ORIGINATOR_MARGIN_PCT", 25);
|
||||
|
||||
// ── Fixed fees ────────────────────────────────────────────────────────────────
|
||||
|
||||
const EVAL_FEE_SATS = envInt("EVAL_FEE_SATS", 10);
|
||||
|
||||
const BOOTSTRAP_FEE_SATS = (() => {
|
||||
const raw = parseInt(process.env.BOOTSTRAP_FEE_SATS ?? "", 10);
|
||||
return Number.isFinite(raw) && raw > 0 ? raw : 10_000;
|
||||
})();
|
||||
|
||||
// ── Public types ──────────────────────────────────────────────────────────────
|
||||
|
||||
export interface WorkFeeBreakdown {
|
||||
amountSats: number;
|
||||
estimatedCostUsd: number;
|
||||
marginPct: number;
|
||||
btcPriceUsd: number;
|
||||
}
|
||||
|
||||
// ── PricingService ────────────────────────────────────────────────────────────
|
||||
|
||||
export class PricingService {
|
||||
readonly marginPct = ORIGINATOR_MARGIN_PCT;
|
||||
|
||||
// ── Fixed fees (unchanged from v1) ───────────────────────────────────────
|
||||
|
||||
calculateEvalFeeSats(): number {
|
||||
return this.evalFee;
|
||||
}
|
||||
|
||||
calculateWorkFeeSats(requestText: string): number {
|
||||
const len = requestText.trim().length;
|
||||
if (len <= this.shortMax) return this.workFeeShort;
|
||||
if (len <= this.mediumMax) return this.workFeeMedium;
|
||||
return this.workFeeLong;
|
||||
return EVAL_FEE_SATS;
|
||||
}
|
||||
|
||||
calculateBootstrapFeeSats(): number {
|
||||
return this.bootstrapFee;
|
||||
return BOOTSTRAP_FEE_SATS;
|
||||
}
|
||||
|
||||
// ── Token estimation ─────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Estimate input tokens for a work request.
|
||||
* Uses chars/4 rule for the user message plus a fixed system-prompt overhead.
|
||||
*/
|
||||
estimateInputTokens(requestText: string): number {
|
||||
return Math.ceil(requestText.length / 4) + WORK_SYSTEM_PROMPT_TOKENS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimate output tokens based on request length tier.
|
||||
*/
|
||||
estimateOutputTokens(requestText: string): number {
|
||||
const len = requestText.trim().length;
|
||||
if (len <= SHORT_MAX_CHARS) return OUTPUT_EST_SHORT;
|
||||
if (len <= MEDIUM_MAX_CHARS) return OUTPUT_EST_MEDIUM;
|
||||
return OUTPUT_EST_LONG;
|
||||
}
|
||||
|
||||
// ── Cost calculation (pure, no oracle) ──────────────────────────────────
|
||||
|
||||
/**
|
||||
* Calculate the total USD cost for a set of token counts + model.
|
||||
* Includes DO infra amortisation and originator margin.
|
||||
*/
|
||||
calculateWorkFeeUsd(inputTokens: number, outputTokens: number, modelId: string): number {
|
||||
const rates = ratesForModel(modelId);
|
||||
const tokenCostUsd =
|
||||
(inputTokens / 1000) * rates.inputPer1kUsd +
|
||||
(outputTokens / 1000) * rates.outputPer1kUsd;
|
||||
const rawCostUsd = tokenCostUsd + DO_INFRA_PER_REQUEST_USD;
|
||||
return rawCostUsd * (1 + this.marginPct / 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate actual token cost (no infra, no margin — raw Anthropic spend).
|
||||
* Used for the post-work cost ledger.
|
||||
*/
|
||||
calculateActualCostUsd(inputTokens: number, outputTokens: number, modelId: string): number {
|
||||
const rates = ratesForModel(modelId);
|
||||
return (
|
||||
(inputTokens / 1000) * rates.inputPer1kUsd +
|
||||
(outputTokens / 1000) * rates.outputPer1kUsd
|
||||
);
|
||||
}
|
||||
|
||||
// ── Invoice amount (calls oracle) ────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Fetch BTC price, convert USD cost to sats, and return the full breakdown.
|
||||
* This is the main entry point for generating a work invoice amount.
|
||||
*/
|
||||
async calculateWorkFeeSats(
|
||||
inputTokens: number,
|
||||
outputTokens: number,
|
||||
modelId: string,
|
||||
): Promise<WorkFeeBreakdown> {
|
||||
const estimatedCostUsd = this.calculateWorkFeeUsd(inputTokens, outputTokens, modelId);
|
||||
const btcPriceUsd = await getBtcPriceUsd();
|
||||
const amountSats = usdToSats(estimatedCostUsd, btcPriceUsd);
|
||||
return { amountSats, estimatedCostUsd, marginPct: this.marginPct, btcPriceUsd };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -181,6 +181,36 @@ components:
|
||||
- executing
|
||||
- complete
|
||||
- failed
|
||||
PricingBreakdown:
|
||||
type: object
|
||||
description: Cost breakdown shown with the work invoice (estimations at invoice-creation time)
|
||||
properties:
|
||||
estimatedCostUsd:
|
||||
type: number
|
||||
description: Total estimated cost in USD (token cost + DO infra + margin)
|
||||
marginPct:
|
||||
type: number
|
||||
description: Originator margin percentage applied
|
||||
btcPriceUsd:
|
||||
type: number
|
||||
description: BTC/USD spot price used to convert the invoice to sats
|
||||
CostLedger:
|
||||
type: object
|
||||
description: Actual cost record stored after the job completes
|
||||
properties:
|
||||
actualInputTokens:
|
||||
type: integer
|
||||
actualOutputTokens:
|
||||
type: integer
|
||||
actualCostUsd:
|
||||
type: number
|
||||
description: Raw Anthropic token cost (no infra, no margin)
|
||||
estimatedCostUsd:
|
||||
type: number
|
||||
marginPct:
|
||||
type: number
|
||||
btcPriceUsd:
|
||||
type: number
|
||||
JobStatusResponse:
|
||||
type: object
|
||||
required:
|
||||
@@ -195,10 +225,14 @@ components:
|
||||
$ref: "#/components/schemas/InvoiceInfo"
|
||||
workInvoice:
|
||||
$ref: "#/components/schemas/InvoiceInfo"
|
||||
pricingBreakdown:
|
||||
$ref: "#/components/schemas/PricingBreakdown"
|
||||
reason:
|
||||
type: string
|
||||
result:
|
||||
type: string
|
||||
costLedger:
|
||||
$ref: "#/components/schemas/CostLedger"
|
||||
errorMessage:
|
||||
type: string
|
||||
DemoResponse:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { pgTable, text, timestamp, integer } from "drizzle-orm/pg-core";
|
||||
import { pgTable, text, timestamp, integer, real } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
@@ -25,6 +25,17 @@ export const jobs = pgTable("jobs", {
|
||||
rejectionReason: text("rejection_reason"),
|
||||
result: text("result"),
|
||||
errorMessage: text("error_message"),
|
||||
|
||||
// ── Cost-based pricing (set when work invoice is created) ───────────────
|
||||
estimatedCostUsd: real("estimated_cost_usd"),
|
||||
marginPct: real("margin_pct"),
|
||||
btcPriceUsd: real("btc_price_usd"),
|
||||
|
||||
// ── Actual token usage (set after work executes) ────────────────────────
|
||||
actualInputTokens: integer("actual_input_tokens"),
|
||||
actualOutputTokens: integer("actual_output_tokens"),
|
||||
actualCostUsd: real("actual_cost_usd"),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
|
||||
});
|
||||
|
||||
25
replit.md
25
replit.md
@@ -68,6 +68,31 @@ Every package extends `tsconfig.base.json` which sets `composite: true`. The roo
|
||||
|
||||
> **Note:** If `LNBITS_URL` and `LNBITS_API_KEY` are absent, `LNbitsService` automatically runs in **stub mode** — invoices are simulated in-memory and can be marked paid via `svc.stubMarkPaid(hash)`. This is intentional for development without a Lightning node.
|
||||
|
||||
### Cost-based work fee pricing
|
||||
|
||||
| Secret | Description | Default |
|
||||
|---|---|---|
|
||||
| `HAIKU_INPUT_COST_PER_1K_TOKENS` | Haiku input cost per 1K tokens (USD) | `0.0008` |
|
||||
| `HAIKU_OUTPUT_COST_PER_1K_TOKENS` | Haiku output cost per 1K tokens (USD) | `0.004` |
|
||||
| `SONNET_INPUT_COST_PER_1K_TOKENS` | Sonnet input cost per 1K tokens (USD) | `0.003` |
|
||||
| `SONNET_OUTPUT_COST_PER_1K_TOKENS` | Sonnet output cost per 1K tokens (USD) | `0.015` |
|
||||
| `DO_MONTHLY_COST_USD` | Monthly DO infra cost amortised per request | `100` |
|
||||
| `DO_MONTHLY_REQUEST_VOLUME` | Expected monthly request volume (divisor) | `1000` |
|
||||
| `ORIGINATOR_MARGIN_PCT` | Margin percentage on top of cost | `25` |
|
||||
| `OUTPUT_TOKENS_SHORT_EST` | Estimated output tokens for short requests | `200` |
|
||||
| `OUTPUT_TOKENS_MEDIUM_EST` | Estimated output tokens for medium requests | `400` |
|
||||
| `OUTPUT_TOKENS_LONG_EST` | Estimated output tokens for long requests | `800` |
|
||||
| `WORK_SYSTEM_PROMPT_TOKENS_EST` | Work model system-prompt size in tokens | `50` |
|
||||
| `SHORT_MAX_CHARS` | Max chars for "short" request tier | `100` |
|
||||
| `MEDIUM_MAX_CHARS` | Max chars for "medium" request tier | `300` |
|
||||
| `EVAL_FEE_SATS` | Fixed eval invoice amount | `10` |
|
||||
| `BTC_PRICE_USD_FALLBACK` | BTC/USD price fallback if CoinGecko is unreachable | `100000` |
|
||||
| `EVAL_MODEL` | Anthropic model used for evaluation | `claude-haiku-4-5` |
|
||||
| `WORK_MODEL` | Anthropic model used for work execution | `claude-sonnet-4-6` |
|
||||
|
||||
Work fee flow: estimate tokens → fetch BTC price from CoinGecko (60s cache) → `(token_cost + DO_infra) × (1 + margin%)` → convert USD → sats.
|
||||
After work runs, actual token counts and raw Anthropic spend are stored in `jobs` as `actual_input_tokens`, `actual_output_tokens`, `actual_cost_usd`.
|
||||
|
||||
### Node bootstrap secrets (for `POST /api/bootstrap`)
|
||||
|
||||
| Secret | Description | Default |
|
||||
|
||||
Reference in New Issue
Block a user