Scaffold PIG and model the compute-GTM ontology
PIG is an agent-native CRM for two-sided AI-compute companies: businesses that buy GPU capacity from providers and resell it. Their business is the spread between two pipelines, which is precisely what a generic CRM cannot represent. The load-bearing decision is the `allocations` table, joining a capacity_commitment (what we bought, at a known cost) to a demand_deal (what we sold, at a known price). Margin, utilisation and idle capacity all fall out of that one join. Cost is charged against the full commitment rather than only the hours that sold, because unsold hours are already paid for and any other treatment flatters a block that is losing money. Domain decisions worth noting, each grounded in how this market operates: - Demand stages put `legal` second, not last. Customers do not hand workloads to an infrastructure provider before paper is executed. - Supply qualification splits technical from financial diligence, recorded attributably. Accepting capacity is a two-key decision. - Capacity carries a time SHAPE (intervals + quantities), not a window. Commitments ramp and step down; a rectangle reports availability that does not exist in the month someone wants it. - SLAs model three distinct shapes: none, a reliability tier plus credits policy, and a negotiated agreement. Aggregators generally cannot promise uptime on resold capacity, but negotiate heavyweight paper upstream. Remedies include fee abatement, which is materially better than a capped credit and is not expressible as one. - Export control is a predicate on the allocation edge, evaluated against the ULTIMATE parent's jurisdiction. Country of incorporation is not a valid key, so this cannot live as a flag on an account. - Agent-derived claims land in `facts` with a confidence band and evidence. Only verified claims self-apply; weaker ones await review. - The API never calls the agent. It writes to a leased queue, guarded by a partial unique index on unfinished work. Verified: typechecks clean, migration generates and applies to Postgres 16 (31 tables, 24 enums, 117 indexes). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* Margin arithmetic — the reason PIG exists.
|
||||
*
|
||||
* A two-sided compute business buys capacity in blocks (a commitment) and sells
|
||||
* it in slices (allocations against deals). The spread between what a
|
||||
* GPU-hour cost and what it sold for, net of the hours nobody bought, is the
|
||||
* business. These functions are deliberately pure and unit-tested: every
|
||||
* dashboard number and every agent answer resolves through them, so an error
|
||||
* here is an error everywhere.
|
||||
*
|
||||
* All money is handled in minor units (cents) as integers. Floating-point
|
||||
* currency in a system that reports margin is a defect waiting to be found by
|
||||
* an accountant.
|
||||
*/
|
||||
|
||||
/** A block of capacity purchased from a supplier. */
|
||||
export interface CommitmentInput {
|
||||
/** Total GPU-hours contracted over the term. */
|
||||
gpuHours: number;
|
||||
/** What we pay per GPU-hour, in cents. */
|
||||
costPerGpuHourCents: number;
|
||||
}
|
||||
|
||||
/** A slice of that block sold to a customer. */
|
||||
export interface AllocationInput {
|
||||
/** GPU-hours allocated to a demand deal. */
|
||||
gpuHours: number;
|
||||
/** What the customer pays per GPU-hour, in cents. */
|
||||
pricePerGpuHourCents: number;
|
||||
}
|
||||
|
||||
export interface MarginResult {
|
||||
/** Hours bought. */
|
||||
committedGpuHours: number;
|
||||
/** Hours sold. */
|
||||
allocatedGpuHours: number;
|
||||
/** Hours bought and not sold. This is the number that hurts. */
|
||||
idleGpuHours: number;
|
||||
/** Share of committed capacity that is sold, 0–1. */
|
||||
utilisation: number;
|
||||
/** Total paid to the supplier, cents. */
|
||||
costCents: number;
|
||||
/** Total billed to customers, cents. */
|
||||
revenueCents: number;
|
||||
/**
|
||||
* Revenue minus the FULL cost of the commitment — not merely the cost of the
|
||||
* hours that sold. Unsold hours on a commitment are already paid for, so
|
||||
* charging only allocated cost would flatter the number and hide the very
|
||||
* problem this system exists to surface.
|
||||
*/
|
||||
grossMarginCents: number;
|
||||
/** Gross margin as a share of revenue, 0–1. Null when there is no revenue. */
|
||||
grossMarginPct: number | null;
|
||||
/** Effective blended margin per GPU-hour sold, in cents. Null if nothing sold. */
|
||||
marginPerAllocatedGpuHourCents: number | null;
|
||||
}
|
||||
|
||||
export function computeMargin(
|
||||
commitment: CommitmentInput,
|
||||
allocations: readonly AllocationInput[],
|
||||
): MarginResult {
|
||||
const committedGpuHours = commitment.gpuHours;
|
||||
const allocatedGpuHours = allocations.reduce((sum, a) => sum + a.gpuHours, 0);
|
||||
|
||||
// Over-allocation is possible and legitimate: capacity can be oversubscribed
|
||||
// deliberately, on the assumption not every buyer uses their full reservation.
|
||||
// Clamping idle at zero keeps the figure meaningful when that happens.
|
||||
const idleGpuHours = Math.max(0, committedGpuHours - allocatedGpuHours);
|
||||
|
||||
const costCents = Math.round(committedGpuHours * commitment.costPerGpuHourCents);
|
||||
const revenueCents = allocations.reduce(
|
||||
(sum, a) => sum + Math.round(a.gpuHours * a.pricePerGpuHourCents),
|
||||
0,
|
||||
);
|
||||
|
||||
const grossMarginCents = revenueCents - costCents;
|
||||
|
||||
return {
|
||||
committedGpuHours,
|
||||
allocatedGpuHours,
|
||||
idleGpuHours,
|
||||
utilisation: committedGpuHours > 0 ? allocatedGpuHours / committedGpuHours : 0,
|
||||
costCents,
|
||||
revenueCents,
|
||||
grossMarginCents,
|
||||
grossMarginPct: revenueCents > 0 ? grossMarginCents / revenueCents : null,
|
||||
marginPerAllocatedGpuHourCents:
|
||||
allocatedGpuHours > 0 ? grossMarginCents / allocatedGpuHours : null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The break-even sell price for the remaining unsold hours on a commitment.
|
||||
*
|
||||
* This is the number a seller actually wants mid-quarter: "the block is half
|
||||
* sold and already paid for — what must I get for the rest to come out even?"
|
||||
* It falls as more of the block sells, which is why it is computed against
|
||||
* remaining hours rather than the whole commitment.
|
||||
*
|
||||
* Returns null when the block is fully allocated: there is nothing left to
|
||||
* price, and dividing by zero hours would produce a confident-looking
|
||||
* Infinity.
|
||||
*/
|
||||
export function breakEvenPricePerGpuHourCents(
|
||||
commitment: CommitmentInput,
|
||||
allocations: readonly AllocationInput[],
|
||||
): number | null {
|
||||
const m = computeMargin(commitment, allocations);
|
||||
if (m.idleGpuHours <= 0) return null;
|
||||
const uncoveredCents = m.costCents - m.revenueCents;
|
||||
// Already in profit: any further sale is upside, so the floor is zero rather
|
||||
// than a negative price, which would be nonsense to display.
|
||||
if (uncoveredCents <= 0) return 0;
|
||||
return uncoveredCents / m.idleGpuHours;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregate margin across many commitments — a book-level view.
|
||||
*
|
||||
* Deliberately sums the underlying cents rather than averaging the per-block
|
||||
* percentages: an average of ratios weights a tiny block equally with a huge
|
||||
* one and produces a number that is wrong in the direction of whichever blocks
|
||||
* happen to be small.
|
||||
*/
|
||||
export function aggregateMargin(
|
||||
books: readonly { commitment: CommitmentInput; allocations: readonly AllocationInput[] }[],
|
||||
): MarginResult {
|
||||
const results = books.map((b) => computeMargin(b.commitment, b.allocations));
|
||||
|
||||
const committedGpuHours = results.reduce((s, r) => s + r.committedGpuHours, 0);
|
||||
const allocatedGpuHours = results.reduce((s, r) => s + r.allocatedGpuHours, 0);
|
||||
const idleGpuHours = results.reduce((s, r) => s + r.idleGpuHours, 0);
|
||||
const costCents = results.reduce((s, r) => s + r.costCents, 0);
|
||||
const revenueCents = results.reduce((s, r) => s + r.revenueCents, 0);
|
||||
const grossMarginCents = revenueCents - costCents;
|
||||
|
||||
return {
|
||||
committedGpuHours,
|
||||
allocatedGpuHours,
|
||||
idleGpuHours,
|
||||
utilisation: committedGpuHours > 0 ? allocatedGpuHours / committedGpuHours : 0,
|
||||
costCents,
|
||||
revenueCents,
|
||||
grossMarginCents,
|
||||
grossMarginPct: revenueCents > 0 ? grossMarginCents / revenueCents : null,
|
||||
marginPerAllocatedGpuHourCents:
|
||||
allocatedGpuHours > 0 ? grossMarginCents / allocatedGpuHours : null,
|
||||
};
|
||||
}
|
||||
|
||||
/** Format cents as a currency string for display. */
|
||||
export function formatCents(cents: number, currency = 'USD'): string {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency,
|
||||
maximumFractionDigits: 2,
|
||||
}).format(cents / 100);
|
||||
}
|
||||
Reference in New Issue
Block a user