# User Points & Chat Data Protocol This protocol defines the canonical data contract for user profile, points account, points ledger, chat session, and chat messages. Protocol verification status: - Last audited migration: `backend/alembic/versions/20260403_0004_remove_points_reason_code.py` - Last audited models: `backend/src/models/profile.py`, `backend/src/models/user_points.py`, `backend/src/models/points_ledger.py`, `backend/src/models/agent_chat_session.py`, `backend/src/models/agent_chat_message.py` - Current status: aligned ## Scope - `profiles` - `user_points` - `points_ledger` - `sessions` - `messages` ## Compatibility strategy - Current strategy: additive evolution. - Breaking changes (drop/rename/type change on core fields) require explicit migration + rollback notes. - `points_ledger.metadata.schema_version` is mandatory and current value is `1`. ## Runtime charging policy (chat) - Charge unit: `20` points per successful run. - Charge timing: deduct after worker run succeeds (`RUN_FINISHED` path). - Failure behavior: failed/canceled runs do not deduct points. - Precheck: before accepting a run, backend must verify `available = balance - frozen_balance >= 20`. - Session follow-up cap: one session allows at most 4 user runs total (initial divination + 3 follow-ups). - Billing idempotency key for per-run consume: `chat.run.success:{session_id}:{run_id}`. ## Table contract ### profiles - PK: `id` (`auth.users.id`, `on delete cascade`) - Core fields: `username`, `avatar_url`, `bio`, `settings`, `created_at`, `updated_at`, `deleted_at` - Constraints: - `username` not empty - Indexes: - `ix_profiles_username` - `ix_profiles_settings_gin` ### user_points - PK: `user_id` (`auth.users.id`, `on delete cascade`) - Core fields: `balance`, `frozen_balance`, `lifetime_earned`, `lifetime_spent`, `version`, `created_at`, `updated_at` - Constraints: - all numeric totals must be non-negative - `frozen_balance <= balance` ### points_ledger - PK: `id` - FK: - `user_id -> auth.users.id` (`on delete cascade`) - `biz_id -> sessions.id` (`on delete restrict`, nullable) - `operator_id -> auth.users.id` (`on delete set null`) - Core fields: `direction`, `amount`, `balance_after`, `change_type`, `biz_type`, `biz_id`, `event_id`, `operator_id`, `metadata`, `created_at`, `updated_at` - Constraints: - `amount > 0` - `direction in (1, -1)` - `balance_after >= 0` - `change_type in ('register', 'consume', 'grant', 'adjust')` - `biz_type is null or biz_type='chat'` - biz binding: - `register => biz_type is null and biz_id is null` - `consume/grant/adjust => biz_type='chat' and biz_id not null` - direction and change_type coupling: - `register/grant => direction = 1` - `consume => direction = -1` - `adjust => direction in (1, -1)` - idempotency: `unique (user_id, event_id)` #### points_ledger.metadata (schema_version=1) Canonical shape: ```json { "schema_version": 1, "operator_type": "user|system|admin", "run_id": "string", "request_id": "string|null", "charge": { "message_id": "uuid", "message_seq": 1, "model_code": "string", "input_tokens": 0, "output_tokens": 0, "cost": "0.000000" }, "ext": {} } ``` JSON constraints: - Common: - must be object - `schema_version = 1` - `operator_type in (user, system, admin)` - `run_id` non-empty - if present, `ext` must be object - Per `change_type`: - `register`: no `charge`, and no chat binding (`biz_type/biz_id` both null) - `consume`: requires `charge` object with required fields - `grant`: no extra metadata shape requirement - `adjust`: requires `ext.ticket_id` non-empty ## Signup initialization contract - Trigger: `auth.users` after insert - Function: `public.initialize_profile_and_points_on_signup()` - Side effects: - create `profiles` row with default settings - username format: `user_xxxxxx` (`x` = 6 chars from `[a-z0-9]`) - create `user_points` row with initial `balance=100`, `lifetime_earned=100` - create `points_ledger` register row: - `change_type='register'` - `biz_type=null`, `biz_id=null` - `amount=100`, `direction=1`, `balance_after=100` ### sessions - PK: `id` - FK: `user_id -> auth.users.id` - Core fields: `session_type`, `job_id`, `title`, `status`, `last_activity_at`, `message_count`, `total_tokens`, `total_cost`, `state_snapshot`, `created_at`, `updated_at`, `deleted_at` - Constraints: - `session_type in ('chat', 'automation')` - `status in ('pending', 'running', 'completed', 'failed')` - `message_count/total_tokens/total_cost` non-negative ### messages - PK: `id` - FK: `session_id -> sessions.id` (`on delete cascade`) - Core fields: `seq`, `role`, `content`, `model_code`, `tool_name`, `input_tokens`, `output_tokens`, `cost`, `latency_ms`, `visibility_mask`, `metadata`, `created_at`, `updated_at`, `deleted_at` - Constraints: - `unique (session_id, seq)` - `seq > 0` - `role in ('user', 'assistant', 'system', 'tool')` - token/cost non-negative - `latency_ms` null or non-negative ## Security and ownership - Backend service must derive owner identity from verified auth context. - Client must not be trusted for `user_id`/`operator_id` ownership semantics. - `metadata` and `settings` must not include secrets.