{
  "schema_version": "1.0.0",
  "bot_id": "6.13",
  "bot_name": "UserActivityLedger",
  "slug": "useractivityledger",
  "layer": "Governance",
  "layer_key": "gov",
  "bot_class": "Governance Service",
  "authority": [
    "Explain"
  ],
  "status": "planned",
  "readiness": "Spec started",
  "flagship": false,
  "is_reference": false,
  "public_export": false,
  "identity": {
    "layer": "Governance",
    "bot_class": "Governance Service",
    "authority": "Explain",
    "runs_before": "Nothing \u2014 runs on every user action event",
    "runs_after": "Any user-initiated action: strategy start, parameter change, halt, override",
    "applies_to": "All user wallet sessions across the Polytraders platform",
    "default_mode": "shadow_only",
    "user_visible": "summary-only",
    "developer_owner": "Polytraders core"
  },
  "purpose": "UserActivityLedger records every user-initiated action \u2014 strategy starts, parameter changes, halts, and overrides \u2014 as a SettlementReport, providing a per-user, per-session compliance ledger retained for 7 years.",
  "why_it_matters": [
    {
      "failure": "No user activity ledger",
      "consequence": "Regulatory requests for user action history cannot be fulfilled; compliance audit fails."
    },
    {
      "failure": "User activity not linked to fills",
      "consequence": "It is impossible to reconstruct which user action led to which trade; audit trail is broken."
    }
  ],
  "polymarket_inputs": [
    {
      "input": "None \u2014 UserActivityLedger consumes internal event stream only",
      "source": "internal",
      "required": false,
      "use": "N/A"
    }
  ],
  "internal_inputs": [
    {
      "input": "User action events from the platform event stream",
      "source": "internal.event_stream",
      "required": true,
      "use": "Record every user-initiated action with wallet address, session ID, and action type."
    },
    {
      "input": "ExecutionReport for fills linked to user sessions",
      "source": "internal.report_bus",
      "required": true,
      "use": "Link fills to the user session that initiated the strategy."
    }
  ],
  "raw_params": [
    "retain_days \u00b7 int",
    "export_format \u00b7 enum",
    "scrub_on_account_close \u00b7 bool",
    "publish_to_user \u00b7 bool"
  ],
  "parameters": [
    {
      "name": "retain_days",
      "default": 2555,
      "warning": 1825,
      "hard": 2555,
      "controls": "Retention period for ledger records in days (default 7 years).",
      "why_default_matters": "7-year retention is required by financial regulations.",
      "threshold_logic": [
        {
          "condition": "retain_days < 2555",
          "action": "WARN \u2014 below regulatory minimum"
        }
      ],
      "dev_check": "if p.retain_days < 2555: emit('RETENTION_BELOW_REGULATORY_MINIMUM')",
      "user_facing": "Your activity history is kept for 7 years as required by regulation."
    },
    {
      "name": "scrub_on_account_close",
      "default": false,
      "warning": null,
      "hard": null,
      "controls": "Whether to scrub PII on account close (retain anonymised records).",
      "why_default_matters": "Default false preserves full records; scrubbing is opt-in per jurisdiction.",
      "threshold_logic": [
        {
          "condition": "scrub_on_account_close=true",
          "action": "Replace PII fields with hashed identifiers on account close"
        }
      ],
      "dev_check": "if p.scrub_on_account_close: record.wallet = hash(record.wallet)",
      "user_facing": ""
    }
  ],
  "default_config": {
    "bot_id": "gov.useractivityledger",
    "version": "0.1.0",
    "mode": "shadow_only",
    "defaults": {
      "retain_days": 2555,
      "export_format": "jsonl",
      "scrub_on_account_close": false,
      "publish_to_user": true
    }
  },
  "implementation_flow": [
    "Subscribe to user action events on the internal platform event stream.",
    "For each event, record: wallet_address, session_id, action_type, parameters, timestamp.",
    "Link fill events to user sessions via trace_id from ExecutionReport.",
    "Emit SettlementReport(event_type=USER_ACTION_RECORDED) on every action.",
    "Enforce retain_days; purge records older than retention window (or scrub PII if scrub_on_account_close=true).",
    "Provide a read API for compliance export in jsonl format."
  ],
  "decision_logic": {
    "approve": "Not applicable \u2014 UserActivityLedger is a pure audit recorder.",
    "reshape_required": "Not applicable.",
    "reject": "Not applicable.",
    "warning_only": "Emits RETENTION_BELOW_REGULATORY_MINIMUM if retain_days < 2555."
  },
  "decision_output_schema": "SettlementReport",
  "decision_output_example": {
    "report_id": "stl_useractivityledger_01HX9Z",
    "bot_id": "gov.useractivityledger",
    "event_type": "USER_ACTION_RECORDED",
    "wallet_address": "0xdeadbeef00000000000000000000000000000001",
    "session_id": "sess_01HX9Z",
    "action_type": "STRATEGY_START",
    "action_params": {
      "strategy": "sports-model",
      "max_size_pusd": 500.0
    },
    "trace_id": "trc_01HX9Z",
    "recorded_at": "2026-05-09T10:00:00Z",
    "report_kind": "SettlementReport",
    "topic": "polytraders.reports.settlement",
    "retained_until": "2033-05-09"
  },
  "developer_log": {
    "bot_id": "gov.useractivityledger",
    "event_type": "ACTION_LINKED_TO_FILL",
    "session_id": "sess_01HX9Z",
    "fill_id": "fill_00a1b2c3d4e5f6a7",
    "linked_at_ms": 1746792060000
  },
  "user_explanations": [
    {
      "situation": "User requests activity history",
      "message": "Your complete activity history is available for the past 7 years."
    },
    {
      "situation": "Account close with PII scrub",
      "message": "Your personally identifiable information has been removed from records, but anonymised activity history is retained for compliance."
    }
  ],
  "failure_modes": {
    "main_failure_mode": "Platform event stream is unavailable; user actions are not recorded in real time.",
    "false_positive_risk": "A replayed event is recorded twice due to missing deduplication.",
    "false_negative_risk": "A user action fires before the ledger subscribes; the action is missing from the ledger.",
    "safe_fallback": "Buffer missed events in memory; flush on reconnect. Use idempotency key to deduplicate replays.",
    "required_dependencies": [
      "internal.event_stream",
      "internal.report_bus (ExecutionReport)",
      "Postgres ledger store"
    ]
  },
  "acceptance_tests": {
    "unit": [
      {
        "test": "STRATEGY_START action recorded with all required fields",
        "setup": "User action event with action_type=STRATEGY_START",
        "expected": "SettlementReport with wallet_address, session_id, and action_params"
      },
      {
        "test": "PII scrubbed on account close",
        "setup": "scrub_on_account_close=true; account close event",
        "expected": "wallet_address replaced with hash; all other fields retained"
      }
    ],
    "integration": [
      {
        "test": "Fill linked to user session via trace_id",
        "setup": "ExecutionReport with trace_id matching user session",
        "expected": "SettlementReport updated with fill_id cross-reference"
      }
    ],
    "property": [
      {
        "property": "All records retained for >= 2555 days",
        "required": "Always true \u2014 retention enforcement is mandatory"
      }
    ]
  },
  "checklist_overrides": {},
  "legacy_goal": "Per-user, per-session record of strategy starts, parameter changes, halts, and overrides.",
  "legacy_pm_signals": [
    "Strategy lifecycle events scoped to user",
    "Manual overrides with ManualOverrideAuditor (1.16) cross-reference",
    "Wallet permission state at each action"
  ],
  "legacy_external_feeds": [],
  "reporting_groups": [
    "governance_audit"
  ],
  "network": [
    "polygon"
  ],
  "api_surface": [
    "internal"
  ],
  "version": {
    "spec": "2.0.0",
    "implementation": "0.1.0",
    "schema": "2",
    "released": null,
    "planned_release": "Q4-2026"
  },
  "migration_history": [
    {
      "date": "2026-04-28",
      "from": "n/a",
      "to": "v2-spec",
      "reason": "Spec drafted post-CLOB-V2 cutover; bot not yet implemented",
      "action_taken": "Designed against V2 schema (pUSD, builder codes, V2 EIP-712 domain)"
    }
  ],
  "polymarket_v2_compat": {
    "clob_version": "v2",
    "collateral": "pUSD",
    "eip712_domain_version": "2",
    "builder_code_aware": false,
    "negrisk_aware": false,
    "multichain_ready": false,
    "sdk_used": "py-clob-client-v2",
    "settlement_contract": "CTFExchangeV2",
    "notes": "UserActivityLedger records user actions in pUSD denomination; no direct CLOB calls."
  },
  "reference_implementation": {
    "pseudocode": "// ---- SUBSCRIBE TO USER EVENTS ----\nFUNCTION onUserAction(event):\n  record = {\n    wallet_address: event.wallet,\n    session_id:     event.session_id,\n    action_type:    event.action_type,\n    action_params:  event.params,\n    trace_id:       event.trace_id,\n    recorded_at:    now()\n  }\n  IF postgres.exists('user_activity_ledger', idempotency_key=event.event_id):\n    RETURN  // deduplicate replay\n  postgres.insert('user_activity_ledger', record)\n  EMIT SettlementReport(event_type='USER_ACTION_RECORDED', ...record,\n    retained_until=now() + days(config.retain_days))\n\n// ---- LINK FILL TO SESSION ----\nFUNCTION onExecutionReport(execReport):\n  ledgerRows = postgres.select('user_activity_ledger',\n    WHERE trace_id = execReport.trace_id)\n  FOR row IN ledgerRows:\n    row.fill_ids.append(execReport.fill_id)\n    postgres.upsert('user_activity_ledger', row)\n    EMIT SettlementReport(event_type='ACTION_LINKED_TO_FILL', ...)",
    "sdk_calls": [
      "postgres.insert('user_activity_ledger', record)",
      "postgres.select('user_activity_ledger', WHERE trace_id=...)",
      "alerting.emit('RETENTION_BELOW_REGULATORY_MINIMUM')"
    ],
    "complexity": "O(1) per user action; O(R) for fill linking where R = ledger rows per trace_id"
  },
  "wire_examples": {
    "input": {
      "label": "User action event",
      "source": "internal.event_stream",
      "payload": {
        "event_id": "evt_01HX9Z",
        "wallet": "0xdeadbeef00000000000000000000000000000001",
        "session_id": "sess_01HX9Z",
        "action_type": "STRATEGY_START",
        "params": {
          "strategy": "sports-model"
        },
        "trace_id": "trc_01HX9Z"
      }
    },
    "output": {
      "label": "SettlementReport \u2014 USER_ACTION_RECORDED",
      "payload": {
        "report_id": "stl_activity_01HX9Z",
        "event_type": "USER_ACTION_RECORDED",
        "wallet_address": "0xdeadbeef00000000000000000000000000000001",
        "action_type": "STRATEGY_START",
        "report_kind": "SettlementReport",
        "topic": "polytraders.reports.settlement",
        "retained_until": "2033-05-09"
      }
    }
  },
  "reason_codes": [
    {
      "code": "USER_ACTION_RECORDED",
      "severity": "INFO",
      "meaning": "A user action was recorded in the ledger.",
      "action": "Log and emit SettlementReport.",
      "user_message": ""
    },
    {
      "code": "ACTION_LINKED_TO_FILL",
      "severity": "INFO",
      "meaning": "A user action was linked to a fill via trace_id.",
      "action": "Update ledger record.",
      "user_message": ""
    },
    {
      "code": "RETENTION_BELOW_REGULATORY_MINIMUM",
      "severity": "WARN",
      "meaning": "retain_days < 2555.",
      "action": "Emit WARN; refuse config change.",
      "user_message": ""
    },
    {
      "code": "LEDGER_WRITE_FAILED",
      "severity": "WARN",
      "meaning": "Postgres write failed; event buffered in memory.",
      "action": "Buffer; emit WARN; flush on reconnect.",
      "user_message": ""
    },
    {
      "code": "KILL_SWITCH_ACTIVE",
      "severity": "WARN",
      "meaning": "KillSwitch active; KillSwitch event recorded in ledger.",
      "action": "Record kill-switch event as USER_ACTION.",
      "user_message": ""
    }
  ],
  "metrics": {
    "emitted": [
      {
        "name": "polytraders_gov_useractivityledger_actions_total",
        "type": "counter",
        "unit": "count",
        "labels": [
          "action_type"
        ],
        "meaning": "Total user actions recorded by type."
      },
      {
        "name": "polytraders_gov_useractivityledger_fill_links_total",
        "type": "counter",
        "unit": "count",
        "labels": [],
        "meaning": "Total fill-to-session links created."
      },
      {
        "name": "polytraders_gov_useractivityledger_buffer_size",
        "type": "gauge",
        "unit": "count",
        "labels": [],
        "meaning": "Events buffered in memory awaiting Postgres write."
      },
      {
        "name": "polytraders_gov_useractivityledger_retention_days",
        "type": "gauge",
        "unit": "days",
        "labels": [],
        "meaning": "Configured retention; must be >= 2555."
      }
    ],
    "alerts": [
      {
        "name": "UserActivityLedgerBufferGrowing",
        "condition": "polytraders_gov_useractivityledger_buffer_size > 500",
        "severity": "P2",
        "runbook": "#runbook-useractivityledger-buffer"
      },
      {
        "name": "UserActivityLedgerRetentionBreach",
        "condition": "polytraders_gov_useractivityledger_retention_days < 2555",
        "severity": "P1",
        "runbook": "#runbook-useractivityledger-retention"
      }
    ]
  },
  "state": {
    "store": "postgres",
    "shape": "user_activity_ledger table: {event_id, wallet_address, session_id, action_type, action_params, trace_id, fill_ids[], recorded_at, retained_until}",
    "ttl": "2555 days (7 years)",
    "recovery": "On restart, flush memory buffer; resume event stream consumption from last committed offset.",
    "size_estimate": "~1 KB per action; ~4 MB per year at 10 actions/day per 1000 users"
  },
  "concurrency": {
    "execution_model": "event-driven; single consumer goroutine on user event stream",
    "max_in_flight": 200,
    "idempotency_key": "event_id",
    "timeout_ms": 1000,
    "backpressure": "buffer-in-memory",
    "locking": "Postgres unique constraint on event_id"
  },
  "dependencies": {
    "depends_on": [
      {
        "bot_id": "internal.event_stream",
        "why": "All user action events are sourced from the platform event stream.",
        "contract": "Events include wallet_address, session_id, action_type, and trace_id."
      }
    ],
    "emits_to": [
      {
        "bot_id": "internal.post_trade_archive",
        "what": "SettlementReport with 7-year retention"
      }
    ],
    "sibling": [
      {
        "bot_id": "gov.posttradeexplainer",
        "why": "PostTradeExplainer produces trade explanations; UserActivityLedger links them to user sessions.",
        "contract": "trace_id is shared between both systems."
      }
    ],
    "external": [
      {
        "service": "Internal Postgres (ledger)",
        "endpoint": "postgres://internal",
        "sla": "99.9%",
        "failure_mode": "Buffer events in memory; flush on reconnect; idempotency prevents duplicates."
      }
    ]
  },
  "security_surfaces": {
    "signs_orders": false,
    "private_key_access": "none",
    "abuse_vectors": [
      "Querying another user's activity ledger without authorisation"
    ],
    "mitigations": [
      "All ledger reads require authenticated user context; wallet_address is verified against session token"
    ]
  },
  "failure_injection": [
    {
      "scenario": "POSTGRES_UNAVAILABLE",
      "how_to_inject": "Kill Postgres connection during high-volume user actions",
      "expected_behaviour": "Events buffered; LEDGER_WRITE_FAILED emitted; flush on reconnect",
      "recovery": "Automatic flush when Postgres recovers."
    },
    {
      "scenario": "DUPLICATE_EVENT_REPLAY",
      "how_to_inject": "Replay an already-recorded event_id",
      "expected_behaviour": "Duplicate detected via idempotency key; second write skipped",
      "recovery": "Idempotent \u2014 no action needed."
    },
    {
      "scenario": "RETENTION_BREACH",
      "how_to_inject": "Set retain_days=30 in config",
      "expected_behaviour": "RETENTION_BELOW_REGULATORY_MINIMUM emitted; config rejected",
      "recovery": "Restore retain_days to >= 2555."
    }
  ],
  "runbook": {
    "summary": "UserActivityLedger incidents are usually Postgres unavailability or retention config breaches.",
    "oncall_actions": [
      {
        "alert": "UserActivityLedgerBufferGrowing",
        "first_action": "Check Postgres health; verify buffer is draining.",
        "escalate_to": "SRE on-call"
      },
      {
        "alert": "UserActivityLedgerRetentionBreach",
        "first_action": "Identify config change that reduced retention; revert immediately.",
        "escalate_to": "Governance pod lead and compliance team"
      }
    ],
    "manual_overrides": [
      {
        "name": "export-user-activity",
        "how": "polytraders gov ledger export --wallet <address> --format jsonl",
        "when": "Regulatory or compliance export request for a specific user."
      }
    ],
    "healthcheck": "/internal/health/useractivityledger \u2192 green if Postgres reachable; buffer_size == 0; retention_days >= 2555; red if retention_days < 2555 or buffer_size > 500"
  },
  "promotion_gates": {
    "to_shadow": [
      {
        "gate": "Idempotency deduplication unit test passes",
        "how_measured": "CI",
        "threshold": "Pass"
      }
    ],
    "to_limited_live": [
      {
        "gate": "PII scrub integration test passes",
        "how_measured": "Integration test",
        "threshold": "Pass"
      }
    ],
    "to_general_live": [
      {
        "gate": "Compliance team sign-off on 7-year retention schema and PII scrub policy",
        "how_measured": "Compliance review",
        "threshold": "Pass"
      }
    ]
  },
  "reporting": {
    "emits_kinds": [
      "SettlementReport"
    ],
    "topics": [
      "polytraders.reports.settlement"
    ],
    "cadence": "every-event",
    "retention_class": "7y",
    "sampling_rule": "emit-every",
    "bus_failure_action": "wal-then-retry",
    "user_visible": "summary-only",
    "consumes_kinds": [
      "ExecutionReport"
    ]
  },
  "capital_impact": "Indirect",
  "v3_status": {
    "phase": 3,
    "phase_name": "Reporting & event store",
    "docs": {
      "done": 27,
      "total": 27,
      "state": "done"
    },
    "impl": {
      "done": 0,
      "total": 15,
      "state": "pending"
    },
    "runtime": {
      "done": 0,
      "total": 8,
      "state": "pending"
    },
    "overall": "pending"
  }
}