Files
eryao/docs/protocols/common/user-points-chat-data-protocol.md
T

6.6 KiB

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/20260410_0005_add_points_audit_and_register_bonus_claims.py
  • Last audited models: backend/src/models/profile.py, backend/src/models/user_points.py, backend/src/models/points_ledger.py, backend/src/models/points_audit_ledger.py, backend/src/models/register_bonus_claims.py, backend/src/models/agent_chat_session.py, backend/src/models/agent_chat_message.py
  • Current status: partially aligned (register bonus still runs in DB trigger, audit ledger tables are additive and ready)

Scope

  • profiles
  • user_points
  • points_ledger
  • points_audit_ledger
  • register_bonus_claims
  • 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 2 user runs total (initial divination + 1 follow-up).
  • Billing idempotency key for per-run consume: chat.run.success:{sha1(session_id:run_id)}.
  • Failed/canceled runs do not deduct user points. If real provider cost is observed, audit record is written with billed_to='platform'.

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_audit_ledger

  • PK: id
  • No FK to auth.users for user_id_snapshot to avoid cascade delete and preserve audit retention
  • Core fields: event_id, user_id_snapshot, user_email_snapshot, change_type, biz_type, biz_id, direction, amount, balance_after, billed_to, run_id, request_id, input_tokens, output_tokens, cost, metadata, created_at, updated_at
  • Constraints:
    • amount >= 0
    • direction in (1, 0, -1)
    • balance_after >= 0
    • change_type in ('register', 'consume', 'grant', 'adjust')
    • biz_type is null or biz_type='chat'
    • billed_to in ('user', 'platform')
    • metadata must be object
    • idempotency: unique (event_id)

register_bonus_claims

  • PK: id
  • Core fields: email_hash, user_email_snapshot, first_user_id, grant_event_id, created_at, updated_at
  • Constraints:
    • email_hash unique
    • grant_event_id unique
  • Notes:
    • email_hash must be HMAC-SHA256 over normalized email (trim + lower)
    • key source: backend config points_policy.register_bonus_hmac_key

points_ledger.metadata (schema_version=1)

Canonical shape:

{
  "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 (current + target)

  • Current trigger:
    • Trigger: auth.users after insert
    • Function: public.initialize_profile_and_invite_code_on_signup()
    • Side effects include profile init + invite code init + register points (currently fixed to 60)
  • Target migration:
    • remove register points grant from DB trigger
    • grant register bonus in application service with eligibility ledger register_bonus_claims
    • keep trigger focused on profile/invite initialization only

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.