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
74 changes: 74 additions & 0 deletions data/economics.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
[
{
"id": "pricing-usage-based-models",
"title": "When usage-based pricing beats seat-based pricing",
"content": "Usage-based pricing usually outperforms seat pricing when value scales with automation volume rather than headcount. Agent products tied to runs, tasks, or tokens can map revenue directly to customer outcomes and reduce friction for small teams adopting early.",
"category": "pricing",
"tags": ["pricing", "usage-based", "packaging"],
"updatedAt": "2026-02-01T09:00:00.000Z"
},
{
"id": "pricing-tier-design",
"title": "Designing pricing tiers for AI agent products",
"content": "A strong tier model combines one clear value metric with operational guardrails. Keep starter tiers generous enough to activate adoption, then gate premium features like private data connectors, advanced workflows, and team controls at higher plans.",
"category": "pricing",
"tags": ["pricing", "tiers", "saas"],
"updatedAt": "2026-02-02T10:30:00.000Z"
},
{
"id": "costs-token-governance",
"title": "Token cost governance for production agents",
"content": "Token governance starts with observability per workflow and model. Teams that enforce prompt budgets, caching strategies, and fallback routing can often reduce monthly inference spend by double-digit percentages without harming quality.",
"category": "costs",
"tags": ["costs", "tokens", "governance"],
"updatedAt": "2026-02-03T08:15:00.000Z"
},
{
"id": "costs-infra-overhead",
"title": "Estimating infrastructure overhead beyond model spend",
"content": "Model inference is only one part of unit economics. Storage, vector indexing, retries, queue workers, monitoring, and support overhead should be included in cost per task calculations before pricing decisions are finalized.",
"category": "costs",
"tags": ["costs", "infrastructure", "unit-economics"],
"updatedAt": "2026-02-03T17:45:00.000Z"
},
{
"id": "revenue-expansion-playbooks",
"title": "Revenue expansion with agent capability ladders",
"content": "Expansion revenue improves when customers can graduate from simple automations to higher-leverage orchestrations. Packaging advanced reliability, integrations, and analytics as clear upgrade paths increases net revenue retention.",
"category": "revenue",
"tags": ["revenue", "expansion", "nrr"],
"updatedAt": "2026-02-04T11:10:00.000Z"
},
{
"id": "revenue-onboarding-activation",
"title": "Onboarding activation as a revenue multiplier",
"content": "Fast activation shortens time-to-value and raises paid conversion. Product onboarding should guide teams to one measurable win in week one, then surface monetizable next steps through templates and integrations.",
"category": "revenue",
"tags": ["revenue", "onboarding", "activation"],
"updatedAt": "2026-02-05T12:00:00.000Z"
},
{
"id": "roi-baseline-method",
"title": "How to set an ROI baseline before deploying agents",
"content": "Reliable ROI measurement starts with a pre-launch baseline for cycle time, manual hours, defect rate, and throughput. Without baseline metrics, post-launch wins can be hard to attribute and defend with finance teams.",
"category": "roi",
"tags": ["roi", "measurement", "baseline"],
"updatedAt": "2026-02-06T14:20:00.000Z"
},
{
"id": "roi-payback-window",
"title": "Calculating payback windows for agent initiatives",
"content": "Payback window analysis should include build time, implementation support, and ongoing model/infrastructure costs. Teams with disciplined rollout sequencing can often recover costs in one to two quarters.",
"category": "roi",
"tags": ["roi", "payback", "finance"],
"updatedAt": "2026-02-07T16:35:00.000Z"
},
{
"id": "roi-risk-adjusted-forecasting",
"title": "Risk-adjusted forecasting for AI agent rollouts",
"content": "Forecast ranges should account for adoption risk, quality regressions, and integration delays. Scenario planning with conservative, expected, and upside cases improves stakeholder confidence and reduces planning surprises.",
"category": "roi",
"tags": ["roi", "forecasting", "risk"],
"updatedAt": "2026-02-08T09:40:00.000Z"
}
]
95 changes: 87 additions & 8 deletions src/app/api/economics/route.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,88 @@
import { modelEconomics } from "@/lib/economics";

export function GET() {
return Response.json({
generatedAt: new Date().toISOString(),
count: modelEconomics.length,
models: modelEconomics,
});
import { NextRequest, NextResponse } from "next/server";
import {
createEconomicsEntryId,
filterEconomicsEntries,
normalizeEconomicsCategory,
readEconomicsEntries,
type EconomicsCategory,
type EconomicsEntry,
writeEconomicsEntries,
} from "@/lib/economics";

export const runtime = "nodejs";
export const dynamic = "force-dynamic";

export async function GET(request: NextRequest) {
try {
const category = request.nextUrl.searchParams.get("category");
const search = request.nextUrl.searchParams.get("search");

const entries = await readEconomicsEntries();
const filtered = filterEconomicsEntries(entries, { category, search });

const categories = Array.from(new Set(entries.map((entry) => entry.category)));

return NextResponse.json(
{
entries: filtered,
total: filtered.length,
categories,
},
{ headers: { "Cache-Control": "no-store" } }
);
} catch (error) {
console.error("Failed to load economics entries", error);
return NextResponse.json({ error: "Failed to load economics entries" }, { status: 500 });
}
}

export async function POST(request: NextRequest) {
try {
const body = (await request.json()) as Record<string, unknown>;

const title = typeof body.title === "string" ? body.title.trim() : "";
const content = typeof body.content === "string" ? body.content.trim() : "";
const categoryInput = typeof body.category === "string" ? body.category : "";
const category = normalizeEconomicsCategory(categoryInput);

const tags = Array.isArray(body.tags)
? body.tags
.filter((tag): tag is string => typeof tag === "string")
.map((tag) => tag.trim())
.filter(Boolean)
: typeof body.tags === "string"
? body.tags
.split(",")
.map((tag) => tag.trim())
.filter(Boolean)
: [];

if (!title || !content || !category || tags.length === 0) {
return NextResponse.json(
{
error: "title, content, category (pricing|costs|revenue|roi), and at least one tag are required",
},
{ status: 400 }
);
}

const entries = await readEconomicsEntries();

const entry: EconomicsEntry = {
id: createEconomicsEntryId(title),
title,
content,
category: category as EconomicsCategory,
tags,
updatedAt: new Date().toISOString(),
};

const updated = [entry, ...entries];
await writeEconomicsEntries(updated);

return NextResponse.json({ entry }, { status: 201 });
} catch (error) {
console.error("Failed to create economics entry", error);
return NextResponse.json({ error: "Failed to create economics entry" }, { status: 500 });
}
}
197 changes: 91 additions & 106 deletions src/app/economics/compare/page.tsx
Original file line number Diff line number Diff line change
@@ -1,130 +1,115 @@
/* eslint-disable react/no-unescaped-entities */
import Link from "next/link";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import { costEfficiencyScore, modelEconomics, type ModelEconomics } from "@/lib/economics";
import { readEconomicsEntries, type EconomicsCategory } from "@/lib/economics";

const formatCurrency = (value: number) =>
new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: value < 1 ? 3 : 2,
maximumFractionDigits: value < 1 ? 3 : 2,
}).format(value);
const categoryOrder: EconomicsCategory[] = ["pricing", "costs", "revenue", "roi"];

const scoreByUseCase = (model: ModelEconomics, useCase: string) => {
const qualityWeight = useCase === "feature-build" ? 0.6 : 0.45;
const speedWeight = useCase === "testing" ? 0.35 : 0.2;
const efficiencyWeight = 0.2;
const affinityWeight = 0.15;

const affinity = model.recommendedFor.includes(useCase) ? 10 : 5;

return (
model.qualityRating * qualityWeight +
model.speedRating * speedWeight +
Math.min(costEfficiencyScore(model), 10) * efficiencyWeight +
affinity * affinityWeight
);
const categoryNarrative: Record<EconomicsCategory, string> = {
pricing: "How to package, tier, and position value metrics.",
costs: "How to control spend and improve operational efficiency.",
revenue: "How to increase conversion, retention, and expansion.",
roi: "How to prove measurable business outcomes from deployment.",
};

const useCases = [
{ id: "feature-build", label: "Feature builds" },
{ id: "bug-fix", label: "Bug fixing" },
{ id: "code-review", label: "Code review" },
{ id: "testing", label: "Testing pipelines" },
{ id: "docs", label: "Documentation" },
];
export default async function EconomicsComparePage() {
const entries = await readEconomicsEntries();

const grouped = categoryOrder.map((category) => {
const items = entries.filter((entry) => entry.category === category);
const latest = items[0];
const tags = Array.from(new Set(items.flatMap((entry) => entry.tags))).slice(0, 6);

export default function EconomicsComparePage() {
const ranked = [...modelEconomics].sort((a, b) => costEfficiencyScore(b) - costEfficiencyScore(a));
return {
category,
count: items.length,
latest,
tags,
};
});

return (
<div className="min-h-screen bg-[#0a0a0a]">
<section className="max-w-6xl mx-auto px-4 py-16">
<Badge className="mb-4 bg-[#06D6A0]/15 text-[#06D6A0] border border-[#06D6A0]/30">
Model Cost Comparison
<div className="min-h-screen bg-[#0a0a0a] text-white">
<section className="mx-auto max-w-6xl px-4 py-16">
<Badge className="mb-4 border border-[#06D6A0]/30 bg-[#06D6A0]/15 text-[#06D6A0]">
Economics Category Comparison
</Badge>
<h1 className="text-4xl font-bold text-white mb-3">Compare model economics side-by-side</h1>
<p className="text-foreground/70 max-w-3xl">
Compare price, context window, speed, and quality. Cost-efficiency score highlights how much quality you get per dollar.
<h1 className="mb-3 text-4xl font-bold">Compare economics categories side-by-side</h1>
<p className="max-w-3xl text-foreground/70">
This page reads persistent entries from data/economics.json and summarizes depth, latest updates, and common themes by category.
</p>
<div className="mt-6">
<Link href="/economics" className="text-sm text-white/70 hover:text-white">
← Back to economics hub
</Link>
</div>
</section>

<Separator className="opacity-10" />

<section className="max-w-6xl mx-auto px-4 py-12">
<div className="grid md:grid-cols-2 xl:grid-cols-3 gap-4">
{ranked.map((model, index) => {
const score = costEfficiencyScore(model);

return (
<Card key={model.id} className="bg-card/40 border-white/10">
<CardHeader>
<div className="flex items-start justify-between gap-3">
<div>
<CardTitle className="text-white text-xl">{model.name}</CardTitle>
<p className="text-sm text-foreground/70 mt-1">{model.provider}</p>
</div>
{index === 0 && <Badge className="bg-[#06D6A0]/20 text-[#06D6A0]">Best value</Badge>}
</div>
</CardHeader>
<CardContent className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-foreground/70">Input / 1M</span>
<span className="text-white">{formatCurrency(model.inputPricePer1M)}</span>
</div>
<div className="flex justify-between">
<span className="text-foreground/70">Output / 1M</span>
<span className="text-white">{formatCurrency(model.outputPricePer1M)}</span>
</div>
<div className="flex justify-between">
<span className="text-foreground/70">Context window</span>
<span className="text-white">{model.contextWindow.toLocaleString()}</span>
</div>
<div className="flex justify-between">
<span className="text-foreground/70">Speed rating</span>
<span className="text-white">{model.speedRating.toFixed(1)} / 10</span>
</div>
<div className="flex justify-between">
<span className="text-foreground/70">Quality rating</span>
<span className="text-white">{model.qualityRating.toFixed(1)} / 10</span>
</div>
<div className="mt-3 rounded-md border border-[#06D6A0]/30 bg-[#06D6A0]/10 px-3 py-2 flex items-center justify-between">
<span className="text-foreground/80">Cost-efficiency score</span>
<span className="text-[#06D6A0] font-semibold">{score.toFixed(2)}</span>
</div>
</CardContent>
</Card>
);
})}
<section className="mx-auto max-w-6xl px-4 py-12">
<div className="mb-6 flex items-center justify-between">
<p className="text-sm text-foreground/70">Total entries: {entries.length}</p>
<a href="/api/economics" className="text-sm text-foreground/70 hover:text-white">
API endpoint →
</a>
</div>
</section>

<Separator className="opacity-10" />
<div className="grid gap-4 md:grid-cols-2">
{grouped.map((group) => (
<Card key={group.category} className="border-white/10 bg-card/40">
<CardHeader>
<div className="mb-2 flex items-center justify-between">
<Badge className="border border-white/20 bg-white/10 text-white/80 capitalize">
{group.category}
</Badge>
<span className="text-xs text-foreground/60">{group.count} entries</span>
</div>
<CardTitle className="text-xl text-white capitalize">{group.category}</CardTitle>
<p className="text-sm text-foreground/65">{categoryNarrative[group.category]}</p>
</CardHeader>
<CardContent className="space-y-4 text-sm">
{group.latest ? (
<div className="rounded-lg border border-white/10 bg-black/20 p-3">
<p className="mb-1 text-xs uppercase text-foreground/50">Latest update</p>
<p className="font-medium text-white">{group.latest.title}</p>
<p className="mt-1 text-foreground/70">
{new Date(group.latest.updatedAt).toLocaleDateString()}
</p>
</div>
) : (
<p className="text-foreground/60">No entries in this category yet.</p>
)}

<section className="max-w-6xl mx-auto px-4 py-12">
<h2 className="text-2xl font-semibold text-white mb-5">Best model recommendations by use case</h2>
<div className="grid md:grid-cols-2 gap-4">
{useCases.map((useCase) => {
const best = [...modelEconomics].sort(
(a, b) => scoreByUseCase(b, useCase.id) - scoreByUseCase(a, useCase.id)
)[0];
<div>
<p className="mb-2 text-xs uppercase text-foreground/50">Top tags</p>
<div className="flex flex-wrap gap-2">
{group.tags.length > 0 ? (
group.tags.map((tag) => (
<span
key={`${group.category}-${tag}`}
className="rounded-md bg-black/30 px-2 py-1 text-xs text-white/70"
>
{tag}
</span>
))
) : (
<span className="text-foreground/60">No tags yet.</span>
)}
</div>
</div>

return (
<Card key={useCase.id} className="bg-card/40 border-white/10">
<CardHeader>
<CardTitle className="text-white text-lg">{useCase.label}</CardTitle>
</CardHeader>
<CardContent>
<p className="text-[#06D6A0] font-semibold">{best.name}</p>
<p className="text-sm text-foreground/70 mt-1">
Quality {best.qualityRating.toFixed(1)} · Speed {best.speedRating.toFixed(1)} · Efficiency {costEfficiencyScore(best).toFixed(2)}
</p>
</CardContent>
</Card>
);
})}
<Link
href={`/economics?category=${group.category}`}
className="inline-block text-[#06D6A0] hover:underline"
>
View {group.category} entries →
</Link>
</CardContent>
</Card>
))}
</div>
</section>
</div>
Expand Down
Loading
Loading