84 lines
2.8 KiB
TypeScript
84 lines
2.8 KiB
TypeScript
|
|
import { Router, type Request, type Response } from "express";
|
||
|
|
import { pricingService } from "../lib/pricing.js";
|
||
|
|
import { agentService } from "../lib/agent.js";
|
||
|
|
import { getBtcPriceUsd, usdToSats } from "../lib/btc-oracle.js";
|
||
|
|
import { freeTierService } from "../lib/free-tier.js";
|
||
|
|
import { trustService } from "../lib/trust.js";
|
||
|
|
|
||
|
|
const router = Router();
|
||
|
|
|
||
|
|
/**
|
||
|
|
* GET /api/estimate?request=<text>[&nostr_token=<token>]
|
||
|
|
*
|
||
|
|
* Returns a pre-flight cost estimate for a request, including free-tier
|
||
|
|
* status for the authenticated identity (if a valid nostr_token is supplied).
|
||
|
|
*
|
||
|
|
* No payment required. Does not create a job.
|
||
|
|
*/
|
||
|
|
router.get("/estimate", async (req: Request, res: Response) => {
|
||
|
|
const requestText =
|
||
|
|
typeof req.query.request === "string" ? req.query.request.trim() : "";
|
||
|
|
|
||
|
|
if (!requestText) {
|
||
|
|
res.status(400).json({ error: "Query param 'request' is required" });
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
const inputTokens = pricingService.estimateInputTokens(requestText);
|
||
|
|
const outputTokens = pricingService.estimateOutputTokens(requestText);
|
||
|
|
const btcPriceUsd = await getBtcPriceUsd();
|
||
|
|
const costUsd = pricingService.calculateWorkFeeUsd(inputTokens, outputTokens, agentService.workModel);
|
||
|
|
const estimatedSats = usdToSats(costUsd, btcPriceUsd);
|
||
|
|
|
||
|
|
// Optionally resolve Nostr identity from query param or header for free-tier preview
|
||
|
|
const rawToken =
|
||
|
|
(req.headers["x-nostr-token"] as string | undefined) ??
|
||
|
|
(typeof req.query.nostr_token === "string" ? req.query.nostr_token : undefined);
|
||
|
|
|
||
|
|
let pubkey: string | null = null;
|
||
|
|
let trustTier = "anonymous";
|
||
|
|
let freeTierDecision: { serve: string; absorbSats: number; chargeSats: number } | null = null;
|
||
|
|
|
||
|
|
if (rawToken) {
|
||
|
|
const parsed = trustService.verifyToken(rawToken.trim());
|
||
|
|
if (parsed) {
|
||
|
|
pubkey = parsed.pubkey;
|
||
|
|
trustTier = await trustService.getTier(pubkey);
|
||
|
|
const decision = await freeTierService.decide(pubkey, estimatedSats);
|
||
|
|
freeTierDecision = {
|
||
|
|
serve: decision.serve,
|
||
|
|
absorbSats: decision.absorbSats,
|
||
|
|
chargeSats: decision.chargeSats,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const poolStatus = await freeTierService.poolStatus();
|
||
|
|
|
||
|
|
res.json({
|
||
|
|
estimatedSats,
|
||
|
|
estimatedCostUsd: costUsd,
|
||
|
|
btcPriceUsd,
|
||
|
|
tokenEstimate: {
|
||
|
|
inputTokens,
|
||
|
|
outputTokens,
|
||
|
|
model: agentService.workModel,
|
||
|
|
},
|
||
|
|
identity: {
|
||
|
|
trust_tier: trustTier,
|
||
|
|
...(pubkey ? { pubkey } : {}),
|
||
|
|
...(freeTierDecision ? { free_tier: freeTierDecision } : {}),
|
||
|
|
},
|
||
|
|
pool: {
|
||
|
|
balanceSats: poolStatus.balanceSats,
|
||
|
|
dailyBudgets: poolStatus.budgets,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
} catch (err) {
|
||
|
|
res.status(500).json({ error: err instanceof Error ? err.message : "Estimate failed" });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
export default router;
|