Files
hanzo.ai/e2e/gates/api.ts
zeekay df1033b7f9 Business draws a team's seat types on its one card: Standard and Premium, each priced once on the term picked
The plans commerce serves name the team a per-seat plan is a seat type
of (group). The Team card lists every seat type of its group with its
own price a member a month, and what each beyond the standard seat
adds, in place of one headline price; a seat type draws no card of its
own. Until the catalog sells a second seat type the card is as it was.
2026-10-17 04:13:00 -07:00

185 lines
12 KiB
TypeScript

import type { Page, Route } from '@playwright/test'
/**
* THE BACKEND, AS A GATE SERVES IT.
*
* A page reads its plans, rates and counts from api.hanzo.ai when it opens
* (lib/api) and keeps no copy, so a gate proves two things: that a page draws
* exactly what it was served, and what it says when nothing answers. The
* answers below are invented — a Pro at $23, an Enso at $7 → $31 — numbers that
* exist nowhere in the source, so a page showing one can only have read it here.
* (scripts/audit-price-literals.mjs reads e2e/ as fixtures for that reason.)
*/
/** A plan row as GET /v1/billing/plans serves it: money in cents. */
export type Row = {
slug: string
name: string
description: string
category: string
price: number
priceAnnual?: number | null
annualTotal?: number
perSeat?: boolean
/** The team a per-seat plan is a seat type of: plans of one group are sold on one subscription. */
group?: string
contactSales?: boolean
popular?: boolean
limits?: Record<string, number>
features: string[]
}
/** The plan authority's answer: the three families the pages sell, and one they never draw. */
export const PLANS: Row[] = [
{ slug: 'free', name: 'Free', description: 'Fixture: the free rung.', category: 'personal', price: 0, priceAnnual: 0, features: ['Fixture free line'], limits: { agents: 1 } },
{ slug: 'dev', name: 'Pro', description: 'Fixture: one person, every model.', category: 'personal', price: 2300, priceAnnual: 1900, annualTotal: 22800, popular: true, features: ['Fixture pro line'], limits: { agents: 10 } },
{ slug: 'max-5x', name: 'Max 5x', description: 'Fixture all-day: 5x the usage of Pro.', category: 'personal', price: 11100, priceAnnual: 9250, annualTotal: 111000, features: ['Fixture max line', '5x more usage than Pro', '5x fixture line only this tier has'], limits: { agents: 10 } },
{ slug: 'max-20x', name: 'Max 20x', description: 'Fixture heaviest: 20x the usage of Pro.', category: 'personal', price: 22200, priceAnnual: 18500, annualTotal: 222000, features: ['Fixture max line', '20x more usage than Pro'], limits: { agents: 10 } },
{ slug: 'team', name: 'Team', description: 'Fixture: by the seat.', category: 'team', price: 2700, priceAnnual: 2250, annualTotal: 27000, perSeat: true, features: ['Fixture team line'], limits: { minSeats: 3, maxMembers: 100 } },
{ slug: 'enterprise', name: 'Enterprise', description: 'Fixture: by conversation.', category: 'enterprise', price: 0, priceAnnual: null, contactSales: true, features: ['Fixture enterprise line'], limits: { agents: -1 } },
{ slug: 'dns-pro', name: 'DNS Pro', description: 'Fixture: another catalog.', category: 'dns', price: 500, priceAnnual: 400, annualTotal: 4800, features: ['Fixture dns line'], limits: { zones: 25 } },
]
/** Dollars as the cards write them: whole dollars bare, anything else to the cent. */
export const money = (cents: number) => {
const n = cents / 100
return Number.isInteger(n) ? `$${n.toLocaleString('en-US')}` : `$${n.toFixed(2)}`
}
/** One rate-card line: micro-USD per unit. */
const line = (name: string, title: string, component: string, rate_micro_usd: number, per: string) => ({ name, title, component, rate_micro_usd, per, note: `Fixture: ${title}.` })
/** The price list's answer (GET /v1/pricing): the rate card, our models, two labs, the tools and the counts. */
export const PRICING = {
updated: '2026-10-01T00:00:00Z',
tariff: {
unit: 'micro_usd',
components: [
{ name: 'model', title: 'Model inference', basis: 'per token, five tiers', quoted: true, tiers: ['input', 'cache_read', 'cache_write', 'output', 'reasoning'], note: 'Fixture: tokens.' },
{ name: 'computer', title: 'Computer', basis: 'per second, while running', quoted: true, note: 'Fixture: seconds.' },
{ name: 'web', title: 'Web tools', basis: 'per call', quoted: false, note: 'Fixture: calls.' },
{ name: 'media', title: 'Media generation', basis: 'per finished job', quoted: false, note: 'Fixture: jobs.' },
],
windows: [
{ name: 'immediate', rank: 0, note: 'Fixture: now.' },
{ name: 'soon', rank: 1, note: 'Fixture: soon.' },
{ name: 'eventually', rank: 2, note: 'Fixture: later.' },
],
rates: [
line('vcpu', 'vCPU', 'computer', 41_000, 'vcpu-hour'),
line('memory', 'Memory', 'computer', 5_300, 'gib-hour'),
line('web_search', 'Web search', 'web', 3_100, 'call'),
line('web_fetch', 'Web fetch', 'web', 700, 'call'),
],
},
hanzoModels: [
{ name: 'enso', fullName: 'Enso', description: 'Fixture: the default.', features: ['Fixture enso'], tier: 'ultra max', context: 1_000_000, owned_by: 'hanzo', family: 'enso', pricing: { input: 7, output: 31, cacheRead: null, cacheWrite: null } },
{ name: 'enso-flash', fullName: 'Enso Flash', description: 'Fixture: the fast one.', features: ['Fixture flash'], tier: 'pro', context: 262_144, owned_by: 'hanzo', family: 'enso', pricing: { input: 3, output: 9, cacheRead: null, cacheWrite: null } },
{ name: 'enso-ultra', fullName: 'Enso Ultra', description: 'Fixture: the top.', features: ['Fixture ultra'], tier: 'ultra max', context: 1_000_000, owned_by: 'hanzo', family: 'enso', pricing: { input: 11, output: 47, cacheRead: null, cacheWrite: null } },
{ name: 'kai', fullName: 'Kai', description: 'Fixture: decisions.', features: ['Decisions'], tier: 'starter', owned_by: 'hanzo', family: 'kai', endpoint: '/v1/decisions', pricingUnit: 'token', specs: { arch: 'decision' }, pricing: { input: 0.013, output: 0, cacheRead: null, cacheWrite: null } },
{ name: 'hanzo/kai', fullName: 'Kai', description: 'Fixture: decisions.', features: ['Decisions'], tier: 'starter', owned_by: 'hanzo', family: 'kai', endpoint: '/v1/decisions', pricingUnit: 'token', specs: { arch: 'decision' }, pricing: { input: 0.013, output: 0, cacheRead: null, cacheWrite: null } },
{ name: 'typesafe/jev-1.13', fullName: 'Jev 1.13', description: 'Fixture: the rival.', features: ['Decisions'], tier: 'starter', owned_by: 'typesafe', provider: 'TypeSafe', endpoint: '/v1/decisions', pricingUnit: 'token', specs: { arch: 'decision' }, pricing: { input: 0.052, output: 0, cacheRead: null, cacheWrite: null } },
{ name: 'zen5', fullName: 'Zen5', description: 'Fixture: open weights.', features: ['Fixture zen'], tier: 'pro', context: 1_000_000, owned_by: 'zenlm', family: 'zen', specs: { params: '35B', arch: 'MoE' }, pricing: { input: 1.7, output: 5.9, cacheRead: null, cacheWrite: null } },
],
thirdPartyModels: [
{ id: 'anthropic/claude-opus-4.6', name: 'Claude Opus 4.6', provider: 'Anthropic', features: ['Fixture lab'], contextWindow: 1_000_000, isFree: false, featured: true, pricing: { input: 6.1, output: 29, cacheRead: null, cacheWrite: null } },
{ id: 'openai/gpt-5', name: 'GPT-5', provider: 'OpenAI', features: ['Fixture lab'], contextWindow: 400_000, isFree: false, pricing: { input: 1.9, output: 13, cacheRead: null, cacheWrite: null } },
],
tools: [
{ name: 'Web Search', price: 0.009, unit: 'per call' },
{ name: 'Code Interpreter', price: 0.07, unit: 'per session minute' },
],
summary: { totalModels: 777, thirdPartyModels: 700, zenModels: 70, freeModels: 77, featuredModels: 7, providers: 66 },
providers: { Anthropic: { total: 1, free: 0, paid: 1 }, OpenAI: { total: 1, free: 0, paid: 1 } },
infrastructure: { gpu: [{ gpu: '1x H100', name: 'Fixture GPU', vram: '80 GB', price: 4.44 }] },
}
/** The price list's counts alone (GET /v1/pricing/summary): `providers` there is a map of lab to its counts, as the live one answers. */
export const SUMMARY = {
...PRICING.summary,
providers: Object.fromEntries(Array.from({ length: PRICING.summary.providers }, (_, i) => [`Fixture lab ${i}`, { total: 1, free: 0, paid: 1 }])),
}
/** The routing table's answer (GET /v1/models): what answers, a lab's rate as the gateway bills it. */
export const MODELS = {
object: 'list',
data: [
{ id: 'enso', owned_by: 'hanzo', family: 'enso', context_window: 1_000_000, outputs: ['text'] },
{ id: 'enso-flash', owned_by: 'hanzo', family: 'enso', context_window: 262_144, outputs: ['text'] },
{ id: 'kai', owned_by: 'hanzo', family: 'kai', outputs: ['decision'] },
{ id: 'hanzo/kai', owned_by: 'hanzo', family: 'kai', outputs: ['decision'] },
{ id: 'gpt-5', owned_by: 'hanzo', context_window: 400_000, outputs: ['text'] },
{ id: 'zen5', owned_by: 'zenlm', family: 'zen', context_window: 1_000_000, outputs: ['text'] },
{ id: 'anthropic/claude-opus-4.6', name: 'Claude Opus 4.6', owned_by: 'anthropic', context_window: 1_000_000, outputs: ['text'], pricing: { input_per_million: 6.4, output_per_million: 30.6 } },
{ id: 'anthropic/claude-sonnet-4.6', name: 'Claude Sonnet 4.6', owned_by: 'anthropic', context_window: 1_000_000, outputs: ['text'], pricing: { input_per_million: 3.3, output_per_million: 16.1 } },
{ id: 'openai/gpt-5', name: 'GPT-5', owned_by: 'openai', context_window: 400_000, outputs: ['text'], pricing: { input_per_million: 2.1, output_per_million: 13.7 } },
{ id: 'meta-llama/llama-4-scout:free', name: 'Llama 4 Scout (free)', owned_by: 'meta-llama', context_window: 131_072, outputs: ['text'], pricing: { input_per_million: 0, output_per_million: 0 } },
],
}
/** The template catalog's answer (GET /v1/templates): twelve to draw, and one the shelf hides. */
export const TEMPLATES = {
data: [
...['studio', 'oasis', 'vault', 'drive', 'savor', 'prism', 'kinetic', 'portfolio', 'pixel', 'hygge', 'solo', 'loop'].map((slug, i) => ({
slug,
title: `Fixture ${slug}`,
category: i % 2 ? 'AI/SaaS' : 'Portfolio',
description: `Fixture: the ${slug} template.`,
})),
{ slug: 'construct', title: 'Fixture construct', category: 'AI/SaaS', description: 'Fixture: hidden from the shelf.' },
],
}
/** The blueprint plane's answer (GET /v1/blueprint/sbom): two stacks priced, one not. */
export const BLUEPRINTS = {
data: [
{ templateId: 'gitea', sbom: [{ image: 'gitea/gitea:1.22' }, { image: 'postgres:16-alpine' }], vcpuHr: 1.25, gbHr: 2.5, estCentsPerMonth: 1717, rateCard: { basis: 'Fixture basis' } },
{ templateId: 'ghost', sbom: [{ image: 'ghost:5-alpine' }, { image: 'mysql:8' }], vcpuHr: 1.5, gbHr: 2.5, estCentsPerMonth: 2929, rateCard: { basis: 'Fixture basis' } },
{ templateId: 'unpriced', sbom: [{ image: 'busybox' }], vcpuHr: 0, gbHr: 0, estCentsPerMonth: 0 },
],
}
/** The reads a page makes, by path, and what a gate answers each with. */
const ANSWERS: Record<string, unknown> = {
'/v1/billing/plans': PLANS,
'/v1/pricing': PRICING,
'/v1/pricing/summary': SUMMARY,
'/v1/models': MODELS,
'/v1/templates': TEMPLATES,
'/v1/blueprint/sbom': BLUEPRINTS,
}
export type Read = keyof typeof ANSWERS
/** Which of the reads a request is, or null for any other. */
const which = (url: string): string | null => {
const path = new URL(url).pathname
return path in ANSWERS ? path : null
}
/**
* Answer the page's reads: the fixtures above, `with` in place of any of them.
* Returns the paths the page asked for, in order, so a gate can say the page
* read the backend rather than something it carried.
*/
export async function serve(page: Page, edits: Partial<Record<Read, unknown>> = {}): Promise<string[]> {
const asked: string[] = []
await page.route(
(u) => which(u.toString()) !== null,
(route: Route) => {
const path = which(route.request().url())!
asked.push(path)
return route.fulfill({ json: path in edits ? edits[path as Read] : ANSWERS[path] })
},
)
return asked
}
/** No answer at all, as when api.hanzo.ai is down: every read fails at the network. */
export async function block(page: Page): Promise<void> {
await page.route(
(u) => which(u.toString()) !== null,
(route) => route.abort('connectionrefused'),
)
}