LISAOS // DOCS
GATEWAY // COMPUTE METERING

Gateway Module — `compute-metering/`

1. Purpose

The compute-metering/ module captures per-job compute spend (token counts + model only, never payload) from both edges — the CC-shell transcript parser and the Hermes/VPS model-call site — and costs it two ways: a billing figure (all tokens × the single "pricing model of record" rate, the cost-plus cap) and an internal figure (each captured model's actual rate on the two-tier Fable/Opus fleet). The headline security control is the .strict() capture envelope (AK-421 §04.1, the payload-projection invariant): only the allow-listed dimensions { counts, model, edge, job ref, timestamp } are accepted; any prompt/messages/content field is rejected at the trust boundary so payload can never leak into the billing table. All money is computed in exact-decimal integer micro-USD (BigInt), never a JS float. AK-434 Increment 2 added per-dispatch and LISA-central (main-loop) cost read-models.

2. File Inventory

FileLinesResponsibility
config.ts224DEFAULT_PRICE_TABLE (multi-model, id-keyed), resolvePriceTable() env-override resolver
index.ts33Layer factory createComputeMeteringLayer(db){ repo, router, priceTable }
repository.ts203compute_capture table DDL + capture/cost queries
routes.ts1261 POST capture + 3 GET cost endpoints
service.ts322Pure costing fns (micro-USD arithmetic, per-model + dispatch + central)
types.ts305.strict() capture Zod + cost query Zod + all cost read-model interfaces + price-table shape
Total1213

3. Public API Surface

REST Endpoints

Mounted at /api/compute-capture (index.ts line 766). No auth exemption for this prefix, so:

MethodPathAuthBody / QueryResponseSide effects
POST/api/compute-captureBearer (write only)ComputeCaptureInputSchema (.strict())201 { data: ComputeCaptureRow }INSERT into compute_capture
GET/api/compute-capture/cost?job_ref=…BearerComputeCostQuerySchema{ data: ComputeCostResult }none
GET/api/compute-capture/cost/by-dispatch?dispatch_id=NBearerComputeCostByDispatchQuerySchema (coerced int){ data: ComputeDispatchCostResult } — billing + internal split + per-model linesnone
GET/api/compute-capture/cost/centralBearernone{ data: ComputeCentralCostResult } — main-loop (dispatch_id IS NULL) complement + unattributed_fail_closed_countnone

Security note (gray-fox F-1 CRITICAL, documented in code): these cost routes MUST NOT mount under /api/dashboard/*, because the dashboard read gate serves every GET /api/dashboard/* without a caller Bearer — if the gateway is exposed publicly that would make raw cost figures world-readable. The dashboard's compute tab fetches them with the injected read-only token it already carries. Responses carry raw two-rate costs only — no ×1.50 markup; margin materialises exclusively in the out-of-scope invoice calculator (gray-fox F-6).

MCP Tools

None. Capture is machine-to-machine edge→gateway REST traffic (AK-421 §06 Q6) — the .strict() schema is the sole validation surface, with no MCP layer.

4. Internal API

  • createComputeMeteringLayer(db): { repo, router, priceTable } — resolves the price table once at construction.
  • createComputeMeteringRepository(db, priceTable): ComputeMeteringRepositorycapture, costForJob, costForDispatch, costForCentral; exports COMPUTE_CAPTURE_DDL.
  • service.ts pure fns: parseUsdToMicro, renderMicroToUsd, selectRecordEntry (BY model id, never positional — raiden ADV), computeJobCost, computeDispatchCost, computeCentralCost.
  • config.ts: DEFAULT_RECORD_MODEL_ID, DEFAULT_PRICE_TABLE, resolvePriceTable(rawEnv?).

5. Background Services

None. Capture is push-driven from the edges; cost is request-driven.

6. Data Contracts

  • ComputeCaptureInputSchema.strict(). Fields: job_ref (required), job_timing_id (nullish FK seam to the not-yet-built TEE), dispatch_id (nullish FK → agent_dispatch(id); NULL for main-loop, populated for dispatched work), attribution_fail_closed (nullish bool — TRUE when a subagent capture's structural join couldn't resolve a dispatch; distinct from a genuine main-loop NULL), edge (cc_transcript | hermes), model, input_tokens, output_tokens, cache_read_tokens/cache_write_tokens (nullish), captured_at (optional; server-stamped if omitted). Do not relax .strict() without re-opening the §04 security review.
  • ComputeCostQuerySchema / ComputeCostByDispatchQuerySchema — job_ref / coerced positive-int dispatch_id.
  • Cost read-model interfaces: ComputeCostResult, InternalModelCostLine, ComputeDispatchCostResult, ComputeCentralCostResult (extends with unattributed_fail_closed_count). All money as exact-decimal USD strings.
  • Price table: multi-model, model_id-keyed (PriceTableEntry with input/output/cache_read/cache_write_cost_per_million strings, optional cache_write_1h_cost_per_million, deployable flag). Unknown captured model id → costed at the record rate (conservative cap) + flagged unknown_model_rate (gray-fox F-5, never $0/skipped).

7. Dependencies

  • Gateway modules consumed: shared/ (formatZodError); FK reference (nullable) to dispatch's agent_dispatch table.
  • External libraries: better-sqlite3, express, zod.
  • Environment variables: COMPUTE_PRICE_TABLE_JSON (optional override of the deployable price table — the §09 configurability path).

8. Test Coverage

LayerFileCases
Service + routes + repositorytest/compute-metering.test.ts (838 L)42 it blocks
Legacy boot guardtest/zz-compute-metering-legacy-boot.test.ts (36 L)boot-wiring regression guard

Covers exact-decimal arithmetic, .strict() rejection of payload fields, two-rate split, unknown-model fallback, per-dispatch attribution, and central-lane fail-closed counting.

9. Known Limitations

  • types.ts docstring drift (flagged, not a bug): the PriceTableConfig.record_model_id docstring says "Increment 1 default: Opus 4.8", but config.ts DEFAULT_RECORD_MODEL_ID is now 'claude-fable-5' (the Increment-2 flip). The docstring is stale relative to the deployed default — a MEDIUM documentation gap for the campaign tail.
  • 1-hour cache-write tier is carried in the shape but not applied: the capture envelope has a single cache_write_tokens count and does not distinguish 5m from 1h at source, so all cache-write tokens are costed at the 5-minute rate (deliberate, conservative — under-states only for rare 1h-TTL writes).
  • job_timing_id is a nullable seam to a TEE (Temporal Estimation Engine) that does not yet exist; always NULL in Increment 1.

10. Change History

DateDispatchSummary
2026-07-042297Initial module spec (smoke-clone-3, LisaOS audit campaign Phase 4)
(AK-434)Increment 2 — two-rate cost read-model, per-dispatch + central views, Fable-5 record flip
(AK-421)Increment 1 — module created; .strict() capture envelope

On this page