← Back to Platform Architecture
Credit Ledger — Event-Sourced Credit System
Overview
The credit ledger is an immutable, append-only event log that tracks all credit transactions: mints (earning credits), burns (spending credits), allocations (monthly baseline), and bonuses. Never update or delete ledger entries. To get a tenant's balance, sum all ledger entries.
Core Concept
Why Event-Sourced?
- Audit trail: every credit transaction is permanent and timestamped
- No double-spend: entries are immutable
- Reconciliation: can recompute balance at any point in time
- Compliance: 7-year retention for regulatory review
Not a balance table — No tenant_balance column. Instead:
balance(tenant_id) = SUM(amount) FROM credit_ledger WHERE tenant_id = ? AND status = 'posted'
Schema Definition
CREATE TABLE credit_ledger (
id UUID PRIMARY KEY,
-- Event Classification
event_type TEXT NOT NULL,
-- contribution_mint: tenant added a precedent
-- query_burn: tenant queried a marketplace cluster
-- membership_allocation: monthly baseline credits
-- bonus_mint: high-quality contribution reward
-- adjustment: manual correction (admin only)
-- Credit Amount (signed: positive = credit, negative = debit)
amount DECIMAL(10, 2) NOT NULL,
-- Event Reference
tenant_id UUID NOT NULL REFERENCES tenants(id),
reference_type TEXT, -- 'precedent', 'marketplace_cluster', 'appeal', etc.
reference_id TEXT, -- ID of referenced object (e.g., precedent UUID)
-- Event Metadata (JSONB for flexibility)
metadata JSONB,
-- Shape varies by event_type:
-- contribution_mint: { "precedent_id": "uuid", "evidence_score": "Strong", "outcome": "Won" }
-- query_burn: { "cluster_id": "Cigna | Frequency Limit |
IOP", "ipo_count": 42 }
-- membership_allocation: { "allocation_period": "2024-04", "monthly_baseline": 100 }
-- bonus_mint: { "reason": "high_quality", "base_precedent_id": "uuid" }
-- Cryptographic Integrity
receipt_hash TEXT, -- SHA-256(event_type + amount + reference_id + timestamp)
-- Status (for auditing)
status TEXT DEFAULT 'posted', -- 'pending', 'posted', 'reversed'
-- Note: reversals create NEW ledger entries (credit_type='reversal')
-- Never delete or update entries
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
posted_at TIMESTAMPTZ, -- When event cleared (NULL if pending)
-- Indexes
INDEX idx_ledger_tenant (tenant_id, posted_at DESC),
INDEX idx_ledger_event (event_type, posted_at DESC),
INDEX idx_ledger_reference (reference_type, reference_id),
INDEX idx_ledger_receipt (receipt_hash),
INDEX idx_ledger_status (status, posted_at DESC)
);
Event Types & Amounts
contribution_mint
Tenant creates a precedent → credits earned.
Amount: base_amount quality_multiplier
Base: 10 credits per precedent
Multiplier:
×1.0 if evidence_score = "Minimal"
×1.5 if evidence_score = "Moderate"
×2.0 if evidence_score = "Strong"
Example:
Tenant A creates precedent with evidence_score="Strong" and outcome="Won"
Amount: 10 × 2.0 = 20 credits
Log entry: { "event_type": "contribution_mint", "amount": 20, "reference_id": "precedent-uuid", "metadata": { "evidence_score": "Strong", "outcome": "Won" } }
INSERT INTO credit_ledger (event_type, amount, tenant_id, reference_type, reference_id, metadata)
VALUES (
'contribution_mint',
20.00,
'tenant-a-uuid',
'precedent',
'precedent-uuid',
'{"evidence_score": "Strong", "outcome": "Won"}'
);
query_burn
Tenant queries marketplace cluster → credits spent.
Amount: -(1 + ipo_count / 10 0.5)
1 credit base + 0.5 per 10 precedents
Example: 42-precedent cluster = -(1 + 42/10*0.5) = -3.1 credits
INSERT INTO credit_ledger (event_type, amount, tenant_id, reference_type, reference_id, metadata)
VALUES (
'query_burn',
-3.10,
'tenant-b-uuid',
'marketplace_cluster',
'Cigna | Frequency Limit |
IOP',
'{"cluster_id": "Cigna | Frequency Limit |
IOP", "ipo_count": 42}'
);
membership_allocation
Monthly baseline credits (first of month).
Amount: 100 credits per tenant per month
INSERT INTO credit_ledger (event_type, amount, tenant_id, reference_type, metadata)
VALUES (
'membership_allocation',
100.00,
'tenant-a-uuid',
NULL,
'{"allocation_period": "2024-04", "monthly_baseline": 100}'
);
bonus_mint
High-quality contribution bonus (manual or automatic).
Criteria:
Precedent outcome = "Won"
Evidence score = "Strong"
Cluster maturity_tier >= "Mature"
Amount: +5 credits per qualifying precedent
INSERT INTO credit_ledger (event_type, amount, tenant_id, reference_type, reference_id, metadata)
VALUES (
'bonus_mint',
5.00,
'tenant-a-uuid',
'precedent',
'precedent-uuid',
'{"reason": "high_quality", "cluster_id": "Cigna | Frequency Limit |
IOP"}'
);
adjustment (Admin Only)
Manual correction (e.g., refund disputed query charge).
Amount: Any (positive or negative)
INSERT INTO credit_ledger (event_type, amount, tenant_id, reference_type, metadata)
VALUES (
'adjustment',
3.10,
'tenant-b-uuid',
'marketplace_cluster',
'{"reason": "disputed_query_charge_reversed", "original_event_id": "uuid"}'
);
Query Patterns
Get Tenant's Current Balance
SELECT COALESCE(SUM(amount), 0) AS balance
FROM credit_ledger
WHERE tenant_id = $1 AND status = 'posted';
Get Balance at Specific Date
SELECT COALESCE(SUM(amount), 0) AS balance_as_of
FROM credit_ledger
WHERE tenant_id = $1
AND status = 'posted'
AND posted_at <= $2; -- Example: '2024-04-01'
Audit Trail (All Transactions)
SELECT event_type, amount, reference_type, posted_at, metadata
FROM credit_ledger
WHERE tenant_id = $1 AND status = 'posted'
ORDER BY posted_at DESC
LIMIT 100;
Monthly Burn Summary
SELECT
DATE_TRUNC('month', posted_at)::DATE AS month,
SUM(CASE WHEN amount < 0 THEN -amount ELSE 0 END) AS credits_burned,
SUM(CASE WHEN amount > 0 THEN amount ELSE 0 END) AS credits_earned,
SUM(amount) AS net_change
FROM credit_ledger
WHERE tenant_id = $1 AND status = 'posted'
GROUP BY DATE_TRUNC('month', posted_at)
ORDER BY month DESC
LIMIT 12;
Find Pending Transactions
SELECT event_type, amount, created_at
FROM credit_ledger
WHERE tenant_id = $1 AND status = 'pending'
ORDER BY created_at DESC;
Reversal Pattern (Corrections)
Never delete or update. To reverse a transaction:
Find original entry:
SELECT id, amount FROM credit_ledger WHERE id = $1;
Create reversal entry (negates the original):
INSERT INTO credit_ledger (event_type, amount, tenant_id, reference_type, metadata, status)
VALUES (
'adjustment',
-20.00, -- Negate original +20
'tenant-a-uuid',
'precedent',
'{"reason": "reversal", "original_ledger_entry_id": "uuid", "original_amount": 20}'
);
Result: balance is unchanged (20 + -20 = 0)
Integrity & Audit
Receipt Hash
Every entry includes receipt_hash = SHA-256(event_type + amount + reference_id + posted_at).
Purpose: Detect tampering. Recompute all hashes:
SELECT
id,
receipt_hash,
SHA256(CONCAT(event_type, amount, reference_id, posted_at::TEXT)) AS computed_hash,
CASE WHEN receipt_hash = SHA256(...) THEN 'valid' ELSE 'TAMPERED' END AS integrity
FROM credit_ledger
WHERE tenant_id = $1
ORDER BY posted_at DESC;
Reconciliation
Monthly reconciliation with marketplace queries:
-- Verify query_burn entries match marketplace_queries
SELECT
cl.id AS ledger_entry,
cl.amount,
mq.id AS query_log
FROM credit_ledger cl
LEFT JOIN marketplace_queries mq ON mq.receipt_hash = cl.receipt_hash
WHERE cl.event_type = 'query_burn'
AND cl.posted_at > NOW() - INTERVAL '30 days'
AND mq.id IS NULL -- ORPHANED: ledger entry with no matching query log
LIMIT 100;
Compliance Notes
HIPAA Audit Trail:
Every credit transaction is logged with tenant_id, timestamp, and reference
7-year retention required
No deletion allowed (enforced at app layer, not DB constraint)
Prevent Double-Spend:
Check balance BEFORE burning credits
If balance insufficient, return 402 Payment Required
Ledger prevents overdraft (entries are immutable; can't subtract more than exists in sum)
Multi-Tenancy:
Each entry is scoped to tenant_id
No cross-tenant queries without explicit WHERE tenant_id = $1
See also
Credit Economy — Business logic and mint/burn rules
Marketplace Tables — Cluster querying and credit costs
Audit Log — General compliance logging pattern
Data Models Overview — All tables and relationships
Code reference
src/models/credit-ledger.ts — Query helpers (balance, audit trail, reconciliation)
src/services/credit-economy.ts — Mint/burn logic, balance checking
src/routes/marketplace.ts — Query endpoints that trigger burns
src/routes/precedents.ts — Create endpoints that trigger mints
src/db/marketplace-schema.sql — Table definition