Lesson 35 · The Cost-Allocation Model · New Subsystem

Billing a shared platform

Attributing cost to customers when there is no per-customer cost to measure. ~13 min.

Builds on: L23 · L15 · L2 Anchor: shared infra, focus-token configs New: allocation, not measurement New: blended-share billing

Every subsystem so far built or guarded the risk graph. This one is different in kind: it answers a business question — "what should each customer pay?" — and the interesting part is that the obvious approach is impossible. There is no per-customer cost to measure. The whole subsystem is a careful answer to that constraint, and it's a clean study in allocation vs measurement.

Your anchor: one shared pipeline, many customers
Risk-graph runs on shared infrastructure — one Memgraph cluster, one indexer fleet, one self-hosted Erigon node. It scales by total focus-token volume, not per-customer. Closing customer X doesn't drop the GCP bill linearly, so there's no "unit cost" per customer to read off a meter. You can't measure what customer X costs. What you can compute is their defensible share of platform load — and that's the whole game.

1 · The core insight: the pipeline is controller-agnostic

Here's the fact the model is built on: grep -rn 'controller_id' pkg/indexer pkg/enrichment pkg/risk returns zero hits. The pipeline has no idea who consumes its output. Customer config drives exactly one thing into the system: the focus-token set — which tokens that customer wants watched.

Billing is derived, never measured — and that's a feature
Because the hot path never learns "who," per-customer cost is reconstructed downstream from the focus-token lists, not stamped during processing. That keeps billing concerns entirely out of the indexer, enrichment, and risk code — they stay clean and customer-blind. The cost model is a separate reader that joins "which customer asked for which tokens" with "what did those tokens cost the platform." Separation of concerns, enforced by the architecture.

2 · The allocation: blend measurable signals into a per-token share

If you can't measure per-customer cost, attribute per-token share of measurable load, then split each token across the customers who asked for it. Each token T gets a fraction of platform load from several signals, every one a proxy for a real cost driver:

SignalSourceProxies the cost of…
HOLDS edgesMemgraphbalance lookups + per-event balanceOf + HOLDS storage
PROTO edgesMemgraph (10 structural edge types)refresh-worker multicall RPC keeping structure fresh
compute_secsPrometheus risk_token_duration (L23)risk-engine per-token compute (at-risk / exposure / admin)
alerts_30dOpenSearch risk-alerts (L15)notification + alert-storage marginal cost per fired alert

Each signal becomes a normalized share — the token's count over the platform total — and the shares blend:

holds_share(T)   = HOLDS(T)        / Σ HOLDS
proto_share(T)   = PROTO(T)        / Σ PROTO
compute_share(T) = compute_secs(T) / Σ compute_secs
                          ⋮
blended_share(T) = Σ  Weightᵢ × shareᵢ(T)          // weights split the pool across signals
per_token_$(T)   = blended_share(T) × PoolUSDPerMonth
Share-based, so stage and prod agree
Notice every term is a fraction of total, never a rate × absolute count. So a token gets the same allocation on stage and prod — there's no "rate × count" drift between environments. The footprint is scale-invariant: double the whole platform and every share is unchanged. That's a deliberate property, not a coincidence.

3 · Splitting a token across its customers (co_users)

A token's cost is platform-wide, but several customers may all watch it. So a token's dollars are split equally across the controllers that list it:

per_controller_$ = BaseAllocationUSD
                 + Σ_token ( per_token_$(T) / co_users(T) )   // T over the controller's config

If three customers all monitor USDC, each pays a third of USDC's allocation. Popular tokens are cheap per customer; a niche token a single customer watches is borne by that one customer. Fair by construction — you pay for what you ask for, shared with everyone else who asked for the same thing.

4 · The pool is a sales input, not an infra number

The single most important boundary: PoolUSDPerMonth — the total dollars being distributed — is chosen by sales-ops, not derived from infra spend. The model splits whatever pool it's given; it never sets it.

Engineering ownsSales-ops owns
the shares — defensible fractions that track measurable on-chain signalsthe pool — cost-recovery (1.0× infra), margin (1.5×), or a flat negotiated $X/mo
Why this split matters
Keeping the shares (math) separate from the pool (pricing strategy) means engineering ships a number that's defensible and auditable — "your share tracks these measured signals" — without ever taking a position on what the customer should be charged. The measured infra spend is shown elsewhere (the Cost Explorer's infra tab) as one input sales looks at; it does not flow into the allocation. Honest billing: the model attributes share, humans set price.

5 · What's excluded — sunk cost vs ongoing cost

One sharp modelling choice: admin edges (ADMIN_CTRL, OWNS, DEPLOYED_BY) are not billed. They're written once at classification time (L25) — a sunk enrichment cost, not an ongoing per-token load. They're shown on the Tokens page for transparency but excluded from the pool. The billable signals are all recurring costs: balances refresh, structure refresh, risk recompute, alerts fire. You bill the ongoing burn, not the one-time discovery.

A capstone that reads the whole system
Look at the signal sources: HOLDS & PROTO from Memgraph (L2), compute_secs from the risk scheduler's Prometheus metric (L23), alerts_30d from the alert store (L15). The cost model is a meta-layer built entirely from the observable outputs of every subsystem you've studied. It's a fitting last stop — billing the platform turns out to be a join over everything else's telemetry.

Check yourself

1. Why can't the system simply measure what each customer costs?
2. What does it mean that the pipeline is "controller-agnostic"?
3. Since cost isn't measured per customer, how is per-customer cost obtained?
4. Each signal contributes a token's count divided by the platform total. Why use shares rather than absolute counts × a rate?
5. Three customers all list USDC in their configs. How is USDC's allocation handled?
6. PoolUSDPerMonth — the total dollars distributed — comes from where?
7. Why is keeping the shares (engineering) separate from the pool (sales) important?
8. Admin edges (ADMIN_CTRL, OWNS, DEPLOYED_BY) are excluded from the billed pool. Why?
↳ Ask your teacher
Try: "What are the two WeightingModes (measured vs flat) in the Report?" · "Show me ComputeByToken and how TokenCost.Footprint blends the shares." · "Which 10 edge types count as PROTO, and why those?" · "How does the Cost Explorer admin page render this?" · "What's BaseAllocationUSD for — a per-controller floor?"

What you can now do

The system, end to end and then some
Ingest → graph → enrich → risk → rules → alerts, the streaming/single-writer/recovery/observability scaffolding, the discovery flywheel, the self-checking harnesses — and now the model that turns all of that measurable activity into a customer's bill. You've traced the platform from a raw log all the way to an invoice line.

Grounded in: docs/pricing-model.md (allocation formula, controller-agnostic insight, signals table, pool-is-sales-input, admin-edge exclusion) + pkg/pricing/allocate.go (package cost, ComputeByToken, TokenCost{HoldsShare/ProtoShare/ComputeShare, footprint=mean of normalized shares}, TokenLine{TokenCostUSD, CoUsers, YourShareUSD=TokenCostUSD/CoUsers}, ControllerSeed, Attribution, Report.Pool + WeightingMode). Verify against source — the code is the truth.