Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 42 additions & 36 deletions data/calculator-presets.json
Original file line number Diff line number Diff line change
@@ -1,46 +1,52 @@
[
{
"id": "solo-developer",
"name": "Solo Developer",
"description": "One builder automating repetitive coding and support tasks.",
"inputs": {
"agents": 1,
"hoursSavedPerAgentPerDay": 1.5,
"hourlyRate": 90,
"monthlyToolCosts": 149
}
"id": "solo-builder",
"name": "Solo Builder",
"description": "One founder using a single agent stack for coding and support triage.",
"agents": 1,
"tokensPerDay": 450000,
"costPerToken": 0.0000012,
"hoursPerDay": 1.5,
"humanHourlyCost": 95
},
{
"id": "small-team",
"name": "Small Team",
"description": "A startup squad using agents across product and operations.",
"inputs": {
"agents": 5,
"hoursSavedPerAgentPerDay": 2,
"hourlyRate": 75,
"monthlyToolCosts": 699
}
"id": "lean-startup",
"name": "Lean Startup Team",
"description": "Five agents helping product, support, and growth in a fast-moving startup.",
"agents": 5,
"tokensPerDay": 700000,
"costPerToken": 0.0000011,
"hoursPerDay": 2.2,
"humanHourlyCost": 78
},
{
"id": "enterprise",
"name": "Enterprise",
"description": "Cross-functional enterprise deployment with strict SLAs.",
"inputs": {
"agents": 40,
"hoursSavedPerAgentPerDay": 1.75,
"hourlyRate": 95,
"monthlyToolCosts": 9500
}
"id": "agency-ops",
"name": "Agency Operations",
"description": "Client delivery team using AI agents to speed production and QA cycles.",
"agents": 12,
"tokensPerDay": 900000,
"costPerToken": 0.000001,
"hoursPerDay": 2.8,
"humanHourlyCost": 85
},
{
"id": "agency",
"name": "Agency",
"description": "Client services team using agents to speed delivery.",
"inputs": {
"agents": 12,
"hoursSavedPerAgentPerDay": 2.5,
"hourlyRate": 85,
"monthlyToolCosts": 2200
}
"id": "enterprise-coe",
"name": "Enterprise COE",
"description": "Central AI center of excellence supporting multiple internal departments.",
"agents": 30,
"tokensPerDay": 1200000,
"costPerToken": 0.00000095,
"hoursPerDay": 1.9,
"humanHourlyCost": 105
},
{
"id": "support-heavy",
"name": "Support-Heavy Org",
"description": "Customer operations team running always-on assistant workflows.",
"agents": 20,
"tokensPerDay": 1600000,
"costPerToken": 0.0000009,
"hoursPerDay": 2.4,
"humanHourlyCost": 62
}
]
175 changes: 72 additions & 103 deletions src/app/api/calculator/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,97 +2,68 @@ import { promises as fs } from "fs";
import path from "path";
import { NextRequest, NextResponse } from "next/server";

type CalculatorInputs = {
agents: number;
hoursSavedPerAgentPerDay: number;
hourlyRate: number;
monthlyToolCosts: number;
};

type CalculatorResults = {
monthlySavings: number;
annualRoi: number;
paybackPeriodMonths: number | null;
efficiencyGainPercent: number;
};

type SavedCalculation = {
type CalculatorPreset = {
id: string;
name: string;
inputs: CalculatorInputs;
results: CalculatorResults;
createdAt: string;
description: string;
agents: number;
tokensPerDay: number;
costPerToken: number;
hoursPerDay: number;
humanHourlyCost: number;
};

type SaveCalculationBody = {
name?: unknown;
inputs?: unknown;
results?: unknown;
type ComputeRequestBody = {
agentCount?: unknown;
tokensPerDay?: unknown;
costPerToken?: unknown;
humanEquivalentHours?: unknown;
hourlyRate?: unknown;
};

const PRESETS_PATH = path.join(process.cwd(), "data", "calculator-presets.json");
const RESULTS_PATH = path.join(process.cwd(), "data", "calculator-results.json");
const DAYS_PER_MONTH = 30;
const WORK_DAYS_PER_MONTH = 22;

function isFiniteNumber(value: unknown): value is number {
return typeof value === "number" && Number.isFinite(value);
}

function isInputs(value: unknown): value is CalculatorInputs {
if (!value || typeof value !== "object") {
return false;
}

const inputs = value as Partial<CalculatorInputs>;

return (
isFiniteNumber(inputs.agents) &&
isFiniteNumber(inputs.hoursSavedPerAgentPerDay) &&
isFiniteNumber(inputs.hourlyRate) &&
isFiniteNumber(inputs.monthlyToolCosts)
);
function clamp(value: number, min: number): number {
return value < min ? min : value;
}

function isResults(value: unknown): value is CalculatorResults {
if (!value || typeof value !== "object") {
return false;
}

const results = value as Partial<CalculatorResults>;

const paybackValid =
results.paybackPeriodMonths === null || isFiniteNumber(results.paybackPeriodMonths);
async function readPresets(): Promise<CalculatorPreset[]> {
const raw = await fs.readFile(PRESETS_PATH, "utf8");
const parsed = JSON.parse(raw) as unknown;

return (
isFiniteNumber(results.monthlySavings) &&
isFiniteNumber(results.annualRoi) &&
paybackValid &&
isFiniteNumber(results.efficiencyGainPercent)
);
}

async function readJsonFile<T>(filePath: string, fallback: T): Promise<T> {
try {
const raw = await fs.readFile(filePath, "utf8");
return JSON.parse(raw) as T;
} catch {
return fallback;
if (!Array.isArray(parsed)) {
return [];
}
}

async function writeResults(results: SavedCalculation[]): Promise<void> {
const dir = path.dirname(RESULTS_PATH);
await fs.mkdir(dir, { recursive: true });
await fs.writeFile(RESULTS_PATH, JSON.stringify(results, null, 2), "utf8");
return parsed.filter((item): item is CalculatorPreset => {
if (!item || typeof item !== "object") {
return false;
}

const preset = item as Partial<CalculatorPreset>;

return (
typeof preset.id === "string" &&
typeof preset.name === "string" &&
typeof preset.description === "string" &&
isFiniteNumber(preset.agents) &&
isFiniteNumber(preset.tokensPerDay) &&
isFiniteNumber(preset.costPerToken) &&
isFiniteNumber(preset.hoursPerDay) &&
isFiniteNumber(preset.humanHourlyCost)
);
});
}

export async function GET() {
try {
const presets = await readJsonFile(PRESETS_PATH, []);

if (!Array.isArray(presets)) {
return NextResponse.json({ error: "Invalid presets format" }, { status: 500 });
}

const presets = await readPresets();
return NextResponse.json({ presets });
} catch (error) {
console.error("Failed to load calculator presets", error);
Expand All @@ -102,50 +73,48 @@ export async function GET() {

export async function POST(request: NextRequest) {
try {
const body = (await request.json()) as SaveCalculationBody;

const name = typeof body.name === "string" ? body.name.trim() : "";
const inputs = body.inputs;
const results = body.results;

if (!name) {
return NextResponse.json({ error: "name is required" }, { status: 400 });
}

if (!isInputs(inputs)) {
return NextResponse.json(
{ error: "inputs must include agents, hoursSavedPerAgentPerDay, hourlyRate, and monthlyToolCosts" },
{ status: 400 }
);
}

if (!isResults(results)) {
const body = (await request.json()) as ComputeRequestBody;

if (
!isFiniteNumber(body.agentCount) ||
!isFiniteNumber(body.tokensPerDay) ||
!isFiniteNumber(body.costPerToken) ||
!isFiniteNumber(body.humanEquivalentHours) ||
!isFiniteNumber(body.hourlyRate)
) {
return NextResponse.json(
{
error:
"results must include monthlySavings, annualRoi, paybackPeriodMonths, and efficiencyGainPercent",
"agentCount, tokensPerDay, costPerToken, humanEquivalentHours, and hourlyRate must be valid numbers",
},
{ status: 400 }
);
}

const existingResults = await readJsonFile<SavedCalculation[]>(RESULTS_PATH, []);
const safeResults = Array.isArray(existingResults) ? existingResults : [];
const agentCount = clamp(body.agentCount, 0);
const tokensPerDay = clamp(body.tokensPerDay, 0);
const costPerToken = clamp(body.costPerToken, 0);
const humanEquivalentHours = clamp(body.humanEquivalentHours, 0);
const hourlyRate = clamp(body.hourlyRate, 0);

const monthlyCost = agentCount * tokensPerDay * costPerToken * DAYS_PER_MONTH;
const monthlySavings = agentCount * humanEquivalentHours * hourlyRate * WORK_DAYS_PER_MONTH;

const record: SavedCalculation = {
id: `calc_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
name,
inputs,
results,
createdAt: new Date().toISOString(),
};
const netMonthly = monthlySavings - monthlyCost;
const roiPercentage =
monthlyCost > 0 ? (netMonthly / monthlyCost) * 100 : monthlySavings > 0 ? 9999 : 0;

safeResults.unshift(record);
await writeResults(safeResults);
const netDaily = netMonthly / DAYS_PER_MONTH;
const breakevenDays = netDaily > 0 && monthlyCost > 0 ? monthlyCost / netDaily : null;

return NextResponse.json({ calculation: record }, { status: 201 });
return NextResponse.json({
monthlyCost: Number(monthlyCost.toFixed(2)),
monthlySavings: Number(monthlySavings.toFixed(2)),
roiPercentage: Number(roiPercentage.toFixed(2)),
breakevenDays: breakevenDays === null ? null : Number(breakevenDays.toFixed(2)),
});
} catch (error) {
console.error("Failed to save calculator result", error);
return NextResponse.json({ error: "Failed to save calculator result" }, { status: 500 });
console.error("Failed to compute calculator ROI", error);
return NextResponse.json({ error: "Failed to compute calculator ROI" }, { status: 500 });
}
}
Loading
Loading