1. Bot Identity
| Layer | Risk Risk |
|---|
| Bot class | Guardrail |
|---|
| Authority | Veto |
|---|
| Status | PLANNED |
|---|
| Readiness | Planned |
|---|
| Runs before | Override action applied |
|---|
| Runs after | Override request submitted |
|---|
| Applies to | Every manual override request for any guardrail — requires justification, rate-limits overrides, and emits an immutable audit RiskVote for every request |
|---|
| Default mode | planned |
|---|
| User-visible | summary-only |
|---|
| Developer owner | Polytraders core — Risk pod |
|---|
Operational profile
| Modes supported | quarantine |
|---|
2. Purpose
ManualOverrideAuditor intercepts every request to bypass or adjust a guardrail, enforces a rate limit on overrides per time window, requires a non-empty justification string, and emits an immutable RiskVote audit record for every approved or rejected override attempt. It ensures that manual guardrail bypasses cannot occur silently and that every override is visible to the risk team.
3. Why This Bot Matters
Silent override of a guardrail
Without audit enforcement, a guardrail can be bypassed without trace, removing the protective layer without any record for post-hoc review.
Override rate limit bypassed
Repeated overrides in a short window can be used to trade in conditions that guardrails are designed to block, effectively disabling the risk controls.
No worked examples on this bot yet. Worked examples are optional but strongly recommended — they turn an abstract failure mode into something a developer can verify in a fixture.
6. Parameter Guide
| Parameter | Default | Warning | Hard | What it controls |
|---|
| max_overrides_per_window | 3 | 2 | 3 | Maximum number of manual override approvals allowed per requestor in the override_window_minutes period. |
| override_window_minutes | 60 | None | None | Time window in minutes over which the override count is tracked per requestor. |
| require_justification | True | None | True | When true, override requests with empty or missing justification strings are hard-rejected. |
7. Detailed Parameter Instructions
max_overrides_per_window
What it means
Maximum number of manual override approvals allowed per requestor in the override_window_minutes period.
Default
{ "max_overrides_per_window": 3 }
Why this default matters
3 overrides per window is enough for legitimate operational needs while preventing systematic abuse.
Threshold logic
| Condition | Action |
|---|
| override_count < 2 | APPROVE |
| override_count == 2 | WARN — OVERRIDE_AUDITOR_RATE_APPROACHING |
| override_count >= 3 | REJECT — OVERRIDE_AUDITOR_RATE_EXCEEDED |
Developer check
if (overrideCount >= params.max_overrides_per_window) return reject('OVERRIDE_AUDITOR_RATE_EXCEEDED');
User-facing English
You have reached the maximum number of overrides allowed in this time window.
override_window_minutes
What it means
Time window in minutes over which the override count is tracked per requestor.
Default
{ "override_window_minutes": 60 }
Why this default matters
A 60-minute window aligns with operational shifts and provides meaningful rate-limiting without being too restrictive for incident response.
Threshold logic
| Condition | Action |
|---|
| always | Count overrides within sliding 60-minute window per requestor |
Developer check
const windowStart = now_ms() - params.override_window_minutes * 60000;
User-facing English
— not yet authored —
require_justification
What it means
When true, override requests with empty or missing justification strings are hard-rejected.
Default
{ "require_justification": true }
Why this default matters
Requiring justification ensures every override has a documented rationale for post-hoc audit.
Threshold logic
| Condition | Action |
|---|
| require_justification=true AND justification is empty | REJECT — OVERRIDE_AUDITOR_NO_JUSTIFICATION |
| justification is non-empty OR require_justification=false | APPROVE (this check) |
Developer check
if (params.require_justification && !override.justification.trim()) return reject('OVERRIDE_AUDITOR_NO_JUSTIFICATION');
User-facing English
A justification is required for manual overrides.
8. Default Configuration
{
"bot_id": "risk.manual_override_auditor",
"version": "0.1.0",
"mode": "hard_guard",
"defaults": {
"max_overrides_per_window": 3,
"override_window_minutes": 60,
"require_justification": true
},
"locked": {
"require_justification": {
"immutable": true
},
"max_overrides_per_window": {
"min": 1
}
}
}
9. Implementation Flow
- Receive override request payload: target_guardrail, requestor_id, justification, timestamp_ms.
- Check KillSwitch; if active, REJECT all overrides immediately.
- If require_justification=true and justification is empty, HARD_REJECT(OVERRIDE_AUDITOR_NO_JUSTIFICATION).
- Load override count for requestor_id within the last override_window_minutes from Redis.
- If override_count >= max_overrides_per_window, HARD_REJECT(OVERRIDE_AUDITOR_RATE_EXCEEDED).
- If override_count == max_overrides_per_window - 1, attach WARN(OVERRIDE_AUDITOR_RATE_APPROACHING).
- Emit immutable audit RiskVote with requestor_id, target_guardrail, justification, and timestamp.
- Increment override counter for requestor_id in Redis (with window_minutes TTL).
- Return APPROVE to allow the override to proceed.
10. Reference Implementation
Pseudocode is language-agnostic. FETCH = read input. EMIT = produce output. IF/THEN/ELSE = decision. Translate directly to TypeScript, Python, Go, or Rust.
FUNCTION evaluateOverride(request):
ks = FETCH internal.killswitch.status
IF ks.active:
EMIT RiskVote(HARD_REJECT, KILL_SWITCH_ACTIVE); RETURN
IF params.require_justification AND NOT request.justification.strip():
EMIT RiskVote(HARD_REJECT, OVERRIDE_AUDITOR_NO_JUSTIFICATION); RETURN
windowStart = now_ms() - params.override_window_minutes * 60000
counter = FETCH redis.zcount(request.requestor_id, windowStart, now_ms())
IF counter IS NULL:
EMIT RiskVote(HARD_REJECT, OVERRIDE_AUDITOR_DATA_UNAVAILABLE); RETURN
IF counter >= params.max_overrides_per_window:
EMIT RiskVote(HARD_REJECT, OVERRIDE_AUDITOR_RATE_EXCEEDED); RETURN
IF counter == params.max_overrides_per_window - 1:
annotations.append(WARN(OVERRIDE_AUDITOR_RATE_APPROACHING))
// Write audit record atomically before approving
ok = WRITE redis.xadd('audit_overrides', {
requestor_id: request.requestor_id,
target_guardrail: request.target_guardrail,
justification: request.justification,
timestamp_ms: now_ms()
})
IF NOT ok:
EMIT RiskVote(HARD_REJECT, OVERRIDE_AUDITOR_DATA_UNAVAILABLE); RETURN
redis.zadd(request.requestor_id, now_ms(), now_ms())
EMIT RiskVote(APPROVE, audit_id=ok.entry_id)
SDK calls used
redis.zcount(requestor_id, windowStart, now)redis.xadd('audit_overrides', record)redis.zadd(requestor_id, score, member)internal.killswitch.status()
Complexity: O(1) — constant Redis ops
11. Wire Examples
Input — what arrives on the wire
Override request — rate limit exceeded — internal
{
"override_request_id": "ovr_b8c9d0e1f2a30008",
"requestor_id": "ops_user_001",
"target_guardrail": "risk.liquidity_guard",
"justification": "Incident response — book feed down",
"timestamp_ms": 1746800000000
}
Output — what the bot emits
RiskVote — HARD_REJECT rate exceeded
{
"guard_id": "risk.manual_override_auditor",
"decision": "HARD_REJECT",
"severity": "HARD",
"reason_code": "OVERRIDE_AUDITOR_RATE_EXCEEDED",
"message": "ops_user_001 has submitted 3 overrides in the last 60 minutes.",
"constraints": {},
"checked_at": "2026-05-10T15:00:00Z"
}
12. Decision Logic
APPROVE
Justification is non-empty, override count is within the rate limit, and KillSwitch is not active.
RESHAPE_REQUIRED
Not used; override requests are binary — either approved or rejected.
REJECT
Justification is missing, rate limit exceeded, or KillSwitch active.
WARNING_ONLY
— not yet authored —13. Standard Decision Output
This bot returns a RiskVote object. See RiskVote schema.
{
"guard_id": "risk.manual_override_auditor",
"decision": "HARD_REJECT",
"severity": "HARD",
"reason_code": "OVERRIDE_AUDITOR_RATE_EXCEEDED",
"message": "Requestor ops_user_001 has submitted 3 overrides in the last 60 minutes, exceeding the limit.",
"constraints": {},
"inputs_used": [
"internal.override_counter",
"internal.killswitch.status"
],
"checked_at": "2026-05-10T15:00:00Z"
}
14. Reason Codes
| Code | Severity | Meaning | Action | User-facing message |
|---|
KILL_SWITCH_ACTIVE | HARD_REJECT | Global kill switch active; no overrides allowed. | Immediate HARD_REJECT. | Override requests are blocked while trading is paused. |
OVERRIDE_AUDITOR_NO_JUSTIFICATION | HARD_REJECT | Override request missing required justification string. | HARD_REJECT; do not emit override counter increment. | A justification is required for all manual override requests. |
OVERRIDE_AUDITOR_RATE_EXCEEDED | HARD_REJECT | Requestor has exceeded the max_overrides_per_window limit. | HARD_REJECT; do not emit override counter increment. | You have exceeded the override limit for this time window. |
OVERRIDE_AUDITOR_RATE_APPROACHING | WARN | Override count is one below the hard limit. | Attach WARN annotation; APPROVE. | |
OVERRIDE_AUDITOR_DATA_UNAVAILABLE | HARD_REJECT | Redis override counter unavailable; cannot enforce rate limit or record audit. | HARD_REJECT (fail-closed). | Override system temporarily unavailable. Please try again. |
15. Metrics & Logs
Metrics emitted
| Metric | Type | Unit | Labels | Meaning |
|---|
polytraders_risk_manualoverrideauditor_decisions_total | counter | count | decision, reason_code, target_guardrail | Total override decisions by type, reason, and target guardrail. |
polytraders_risk_manualoverrideauditor_overrides_per_window | gauge | count | requestor_id | Current override count per requestor within the rolling window. |
polytraders_risk_manualoverrideauditor_eval_latency_ms | histogram | milliseconds | | Latency from request receipt to RiskVote emit. |
Alerts
| Alert | Condition | Severity | Runbook |
|---|
OverrideAuditorRateExceeded | rate(polytraders_risk_manualoverrideauditor_decisions_total{reason_code='OVERRIDE_AUDITOR_RATE_EXCEEDED'}[5m]) > 0 | P1 | #runbook-overrideauditor-rate |
OverrideAuditorDataUnavailable | rate(polytraders_risk_manualoverrideauditor_decisions_total{reason_code='OVERRIDE_AUDITOR_DATA_UNAVAILABLE'}[5m]) > 0 | P1 | #runbook-overrideauditor-data |
16. Developer Reporting
{
"bot_id": "risk.manual_override_auditor",
"decision": "HARD_REJECT",
"reason_code": "OVERRIDE_AUDITOR_RATE_EXCEEDED",
"inputs_used": [
"internal.override_counter"
],
"metrics": {
"requestor_id": "ops_user_001",
"target_guardrail": "risk.liquidity_guard",
"override_count_in_window": 3,
"max_overrides_per_window": 3,
"window_minutes": 60
},
"checked_at": "2026-05-10T15:00:00Z"
}
17. Plain-English Reporting
| Situation | User-facing explanation |
|---|
| Override blocked — rate limit | You have submitted too many overrides in the current time window. Please wait before submitting another override request. |
| Override blocked — no justification | A justification is required for all manual override requests. Please provide a reason before proceeding. |
| Override approved — audit recorded | Your override request has been approved and an audit record has been created. All overrides are logged for review by the risk team. |
18. Failure-Mode Block
| main_failure_mode | Failing to record the override audit entry due to a Redis write failure, allowing a silent override. |
|---|
| false_positive_risk | Rate counter includes expired entries due to a clock skew, causing a false rate-limit rejection. |
|---|
| false_negative_risk | Concurrent override requests from the same requestor being processed before the counter is incremented, allowing more overrides than the limit. |
|---|
| safe_fallback | If the Redis override counter is unavailable, HARD_REJECT with OVERRIDE_AUDITOR_DATA_UNAVAILABLE. Never approve when the counter cannot be read or written. |
|---|
| required_dependencies | Redis override counter store, KillSwitch active flag, Audit log write path |
|---|
19. Failure-Injection Recipes
| Scenario | How to inject | Expected behaviour | Recovery |
|---|
REDIS_UNAVAILABLE | Block TCP to Redis cluster | | Returns to normal within one cycle after Redis is restored; counter rebuilt from audit stream. |
RATE_LIMIT_EXCEEDED | Submit 3 overrides in 60 minutes for the same requestor_id | | Returns to APPROVE after the rolling window expires. |
EMPTY_JUSTIFICATION | Submit override request with justification='' | | Immediate on resubmission with non-empty justification. |
20. State & Persistence
Cold-start recovery
Counter rebuilt from audit stream on cold start. If Redis unavailable, HARD_REJECT until restored.
21. Concurrency & Idempotency
| Aspect | Specification |
|---|
| Execution model | single-threaded event loop |
| Max in-flight | 50 |
| Idempotency key | override_request_id |
| Per-call timeout (ms) | 50 |
| Backpressure strategy | drop newest |
| Locking / mutual exclusion | Redis INCR for counter ensures atomic increment; audit stream uses XADD for append-only safety |
22. Dependencies
Depends on (must run first)
| Bot | Why | Contract |
|---|
| risk.kill_switch | All overrides blocked when kill switch is active. | HARD_REJECT(KILL_SWITCH_ACTIVE) on every override request when active. |
Emits to (downstream consumers)
| Bot | Why | Contract |
|---|
internal.audit_log | Every override request (approved or rejected) emits an immutable audit RiskVote to the audit log. | Audit record written before APPROVE is returned; write failure triggers HARD_REJECT. |
External services
| Service | Endpoint | SLA assumed | On failure |
|---|
| Redis (override counter + audit stream) | internal Redis cluster | 99.99% (in-cluster) | HARD_REJECT(OVERRIDE_AUDITOR_DATA_UNAVAILABLE) if Redis unavailable. |
23. Security Surfaces
Abuse vectors considered
- Rotating requestor_id to bypass per-requestor rate limit
- Submitting overrides with boilerplate justification strings to pass the non-empty check without meaningful documentation
Mitigations
- Requestor ID is sourced from authenticated session token; cannot be spoofed by the requester
- Justification minimum length and keyword checks planned for v2 of this spec
24. Polymarket V2 Compatibility
| Aspect | Value |
|---|
| CLOB version | v2 |
| Collateral asset | pUSD |
| EIP-712 Exchange domain version | 2 |
| Aware of builderCode field | no |
| Aware of negative-risk markets | no |
| Multi-chain ready | no |
| SDK used | py-clob-client-v2 |
| Settlement contract | CTFExchangeV2 |
| Notes | ManualOverrideAuditor does not interact with CLOB or Polymarket directly; all inputs are internal override request payloads. |
25. Versioning & Migration
| Field | Value |
|---|
| spec | 2.0.0 |
| implementation | 0.1.0 |
| schema | 2 |
| released | None |
| planned_release | Q3-2026 |
Migration history
| Date | From | To | Reason | Action taken |
|---|
| 2026-04-28 | n/a | v2-spec | Spec drafted post-CLOB-V2 cutover; bot not yet implemented | Designed against V2 schema (pUSD, builder codes, V2 EIP-712 domain) |
26. Acceptance Tests
Unit Tests
| Test | Setup | Expected result |
|---|
| Approve valid override with justification and within rate limit | override_count=1, max=3, justification='Incident response' | APPROVE with audit record emitted |
| Reject when justification is empty | justification='' | HARD_REJECT(OVERRIDE_AUDITOR_NO_JUSTIFICATION) |
| Reject when rate limit exceeded | override_count=3, max=3 | HARD_REJECT(OVERRIDE_AUDITOR_RATE_EXCEEDED) |
| Warn when approaching rate limit | override_count=2, max=3 | APPROVE with WARN annotation |
Integration Tests
| Test | Expected result |
|---|
| Override counter increments atomically for concurrent requests | At most max_overrides_per_window overrides approved in any 60-minute window for the same requestor |
| Audit record immutably stored and retrievable after override | Audit record queryable from audit log with requestor_id, target_guardrail, and justification |
Property Tests
| Property | Required behaviour |
|---|
| Empty justification never results in APPROVE when require_justification=true | Always true |
| Audit record emitted for every APPROVE and HARD_REJECT | Always true — audit log is the primary compliance artefact |
27. Operational Runbook
ManualOverrideAuditor incidents typically involve a Redis failure causing fail-closed rejections, or a legitimate rate-limit breach requiring escalation to risk pod lead for review.
On-call actions
| Alert | First step | Diagnosis | Mitigation | Escalate to |
|---|
OverrideAuditorRateExceeded | Identify requestor_id from the reason_code log; review override audit stream to determine if requests are legitimate incident response or potential abuse. | | | Risk pod lead immediately; review override audit records. |
OverrideAuditorDataUnavailable | Check Redis cluster connectivity; confirm audit stream is writable. | | | Infra on-call if Redis unavailable > 1 minute. |
Manual overrides
polytraders risk reset-override-counter --requestor-id <id> — After a confirmed legitimate incident response where the rate limit was exceeded; requires risk pod lead written approval.
Healthcheck
GET /internal/health/manualoverrideauditor → green: Redis reachable, audit stream writable, override counter TTL active; red: Redis unreachable, audit stream write failure, or counter TTL expired
29. Developer Checklist
Ready-to-ship score: 27/27 sections complete · 100%
| Requirement | Status |
|---|
| Purpose defined | ✓ done |
| Required inputs listed | ✓ done |
| Parameters defined | ✓ done |
| Defaults defined | ✓ done |
| Warning thresholds defined | ✓ done |
| Hard thresholds defined | ✓ done |
| Safe fallback defined | ✓ done |
| Structured output defined | ✓ done |
| Developer log defined | ✓ done |
| Plain-English explanation | ✓ done |
| Unit tests defined | ✓ done |
| Integration tests defined | ✓ done |
| Property tests defined | ✓ done |
| Failure-mode block complete | ✓ done |
| Reference implementation pseudocode | ✓ done |
| Wire examples (input + output) | ✓ done |
| Reason codes listed | ✓ done |
| Metrics & logs defined | ✓ done |
| State & persistence defined | ✓ done |
| Concurrency & idempotency defined | ✓ done |
| Dependencies declared | ✓ done |
| Security surfaces declared | ✓ done |
| Polymarket V2 compatibility declared | ✓ done |
| Version & migration history declared | ✓ done |
| Operational runbook defined | ✓ done |
| Promotion gates defined | ✓ done |
| Failure-injection recipes defined | ✓ done |