Story-2543 — Private Vendor-Cost Ledger & Staff Unit-Economics API
pramaan-functions · RFD-BILL-002 §5.2/§5.3/§7.2/§8/§9 · pramaan-docs ADR-173 · Epic-2469 Phase 3
1. Why two books
Customer page usage and PRAMAAN's own machine spend are different facts. A failed
200-page INTAKE bills the customer nothing but still cost PRAMAAN a Textract job and
several model calls. Story-2539 owns the customer book; this story adds the
platform-private cost book — billing.vendor_cost_event —
and the staff-only view that joins the two into per-firm/month unit economics.
2. The ledger — billing.vendor_cost_event
| Field | Meaning |
|---|---|
idempotency_key (unique) | Producer receipt: textract:<job> / openrouter:<generation> / storage:<firm>:<date>:<class> — grammar CHECK-enforced per category |
firm_id · matter_id · document_id | Nullable attribution — identifiers only, never content |
category · vendor · source_id | textract|inference|storage · aws_textract|openrouter|aws_s3 · provider receipt id |
quantity · unit | Pages / generation / byte-hours |
native_cost_micros · native_currency · reporting_cost_usd_micros | Integer micros (D-8) — a $0.000033 model call survives aggregation exactly (CL-11) |
fx_rate_version · rate_card_version | Frozen conversion and vendor rate references |
event_kind · reconciles_event_id | estimate|actual|adjustment; an adjustment must link the event it reconciles |
event_metadata | Allowlisted JSONB: region, feature set, served model, provider, cache class, storage class — nothing else |
adjustment event linked via
reconciles_event_id (CL-14) — the original estimate is never edited, and an
adjustment can never reconcile another adjustment.extra="forbid" validator at the single write seam, and a Postgres
CHECK (event_metadata - ARRAY[allowed keys] = '{}'). Prompts, completions,
filenames, quotes, and legal text cannot enter the ledger through either door.3. The view — billing.firm_monthly_unit_economics
A security_invoker SQL view (projection, rebuildable from the immutable
ledgers) joining vendor cost against the Story-2539 commercial customer ledger per
firm/month: native revenue split by currency (revenue_inr_micros /
revenue_usd_micros — never FX-mixed at read time, D-8), pages, cost by
category, total direct machine cost, and gross contribution.
4. The staff API
GET /admin/unit-economics?period=2026-07 → all firms, one month (§7.2 table)
GET /admin/firms/{firm_id}/unit-economics?period=2026-07 → summary + drill-down receipts
Drill-down identifiers per event: matter → document/artifact → provider receipt
(source_id), with adjustment linkage. Both routes run under the staff RLS
session (any active platform staff).
5. Denied means audited (CL-12)
A firm member calling either route receives 403 — and the
refusal itself becomes evidence: one committed unit_economics.access_denied
row in ops.audit_log, stamped with the caller's real firm-user identity
(never as staff), carrying the route and method. Integration-proven: two denied requests
→ two audit rows on the member's firm trail.
6. What was proven
| Invariant | Proof |
|---|---|
| UPDATE/DELETE blocked after insert | integration — trigger raises on both |
| Duplicate delivery writes once (D-4) | integration — same key twice → one row |
| Sub-cent micros survive aggregation (CL-11) | integration — 3 × 33µ$ → exactly 99µ$ in the view |
| Estimate + linked adjustment (CL-14) | integration — linkage kept, estimate untouched, adjust-an-adjustment refused |
| Metadata allowlist | unit (validator) + integration (DB CHECK) — forbidden keys rejected at both layers |
| Firm session reads zero vendor rows | integration — RLS |
| Staff totals reconcile from seeded events | integration — 300,000 − 50,000 + 99 + 26 = 250,125 µ$ |
| Firm member denied + audited | integration — 403 + audit rows |
Postgres only (D-2): no DynamoDB, no OpenSearch. Producers (Textract completion, inference receipt capture, the daily storage allocation job) wire into the single write seam in later Epic-2469 stories.