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

5.2 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/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:

{
  "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.