Skip to content

Touli Points Architecture (DLD)

Purpose

Define the deterministic points engine contract, config snapshoting model, cap/decay ordering, and ledger invariants.

Core Principle

Points attribution is deterministic and reproducible:

  • no model calls
  • no hidden constants in code
  • all thresholds and multipliers sourced from versioned config snapshots

Engine Contract

Function signature (logical)

  • calculatePoints(input, context, configSnapshot) -> pointsDecision

Inputs

  • submission_id
  • user_id
  • hours_claimed
  • verify_level
  • group_size_bucket
  • beneficiary_flag
  • pillar
  • activity_type
  • beneficiary_id (nullable)
  • activity_at

Context

  • trust_tier
  • streak_state
  • challenge_completion_state
  • pillar_diversity_state
  • daily_hours_used
  • weekly_hours_used
  • repeat_activity_counter
  • market_mode

Output

  • points_awarded
  • points_ledger_entry_type
  • computation_breakdown (json)
  • applied_caps[]
  • applied_decays[]
  • config_snapshot_id
  • decision_reason_codes[]

Formula and Ordering

Base formula:

  • final = round(55 * hours_capped * verify * community * trust * (1 + streak + completion + diversity))

Deterministic ordering:

  1. Validate eligibility and launch-mode constraints.
  2. Apply hour bounds:
    • max per submission
    • max per day
    • max per week
  3. Resolve multipliers from lookup tables (verify, community, trust).
  4. Compute additive bonuses (streak, completion, diversity).
  5. Apply structural multiplier cap.
  6. Apply repeat-decay rule.
  7. Apply beneficiary policy rules (self vs others|community).
  8. Round and return final points.

The ordering above is immutable per config_snapshot_id.

Config Snapshot Model

Config domains:

  • anchor_points_per_hour
  • multiplier tables
  • cap thresholds
  • decay policies
  • market mode flags (launch|steady)
  • generosity bonus policy
  • redemption eligibility policies

Snapshot rules:

  • Each points decision references one immutable config_snapshot_id.
  • Historical entries are never recalculated in place.
  • Recompute jobs must use the historical snapshot attached to each ledger row.

Ledger Model

Source of truth:

  • append-only points_ledger event rows

Projection:

  • points_wallet.balance as derived current-state cache

Required ledger fields:

  • ledger_entry_id
  • user_id
  • submission_id (nullable for non-deed events)
  • entry_type (deed_reward, gift_debit, gift_credit, adjustment, expiry, escrow_hold, escrow_release)
  • points_delta
  • balance_after (optional but recommended for fast audit)
  • config_snapshot_id
  • reason_codes[]
  • created_at

Ledger Invariants

  1. Ledger is append-only; no update/delete on financial semantics.
  2. points_delta must be non-zero for economic events.
  3. Wallet projection must equal sum of ledger deltas for the user.
  4. One deed can produce at most one final reward entry per decision version.
  5. Reversal entries must reference original ledger entry id.
  6. Idempotent processing must prevent duplicate credits.

Idempotency Strategy

Idempotency key:

  • submission_id + decision_version + ledger_entry_type

Enforcement:

  • unique constraint on idempotency key
  • workers retry safely using the same key
  • duplicate message processing resolves to no-op

Repeat-Decay and Beneficiary Rules

Repeat decay:

  • keyed by (pillar, activity_type, beneficiary_id, week_window)
  • third and subsequent identical actions apply configured decay

Beneficiary policy:

  • beneficiary_flag=self can trigger restricted/non-redeemable pathways in launch mode
  • beneficiary_flag=others|community required for full redeemable points, based on policy config

Generosity Bonus Path

  • Applied on send events only.
  • Percent sourced from policy tables by gift route type.
  • Monthly cap and count cap enforced before credit.
  • Bonus posts as separate ledger entry for audit clarity.

Dispute and Reversal Flow

mermaid
flowchart LR
  disputeRaised[DisputeRaised] --> adjudication[Adjudication]
  adjudication --> noChange[NoChange]
  adjudication --> reverseEntry[ReverseOriginalEntry]
  reverseEntry --> correctedEntry[PostCorrectedEntry]
  correctedEntry --> walletReproject[WalletReprojection]

Rules:

  • never mutate original event
  • always post compensating entries
  • preserve linkage between original and correction entries

Observability Metrics

  • points_calc_latency_ms
  • points_credit_success_rate
  • duplicate_credit_prevented_count
  • ledger_projection_drift_count
  • reversal_rate
  • config_snapshot_usage_distribution