1. Bot Identity
| Layer | Security Security |
|---|
| Bot class | Guardrail |
|---|
| Authority | RejectPause |
|---|
| Status | PLANNED |
|---|
| Readiness | Spec started |
|---|
| Runs before | Any signing call using a key past its rotation schedule |
|---|
| Runs after | Session key validation |
|---|
| Applies to | All active signing keys per user across environments |
|---|
| Default mode | shadow_only |
|---|
| User-visible | Advanced details only |
|---|
| Developer owner | Polytraders core |
|---|
Operational profile
| Modes supported | quarantine |
|---|
2. Purpose
Nag the user to rotate signing keys on a schedule; prevent key reuse across environments.
3. Why This Bot Matters
Signing key never rotated
A long-lived key that is compromised silently provides unlimited signing authority over time.
Same key used across prod and staging
A staging environment compromise exposes production signing capability.
No block on overdue rotation
Users ignore rotation reminders indefinitely, leaving stale keys in production.
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 |
|---|
| rotate_every_days | 30 | Key age > 0.9 * rotate_every_days | Key age > rotate_every_days + (block_on_overdue_h / 24) | Number of days before a signing key should be rotated. |
| require_unique_per_env | True | Same key fingerprint detected in multiple environments | Same key fingerprint used in prod and non-prod simultaneously | Reject signing if the key fingerprint is shared across environments. |
7. Detailed Parameter Instructions
rotate_every_days
What it means
Number of days before a signing key should be rotated.
Default
{ "rotate_every_days": 30 }
Why this default matters
30-day default aligns with industry key management best practices.
Threshold logic
| Condition | Action |
|---|
| key_age_d < rotate_every_days | APPROVE |
| rotate_every_days <= key_age_d <= rotate_every_days + block_on_overdue_h/24 | WARN — rotation overdue |
| key_age_d > rotate_every_days + block_on_overdue_h/24 | REJECT — KEY_ROTATION_OVERDUE |
Developer check
if (key_age_d > p.rotate_every_days + p.block_on_overdue_h/24) return reject('KEY_ROTATION_OVERDUE');
User-facing English
Your signing key is overdue for rotation. Trading is blocked until you rotate it.
require_unique_per_env
What it means
Reject signing if the key fingerprint is shared across environments.
Default
{ "require_unique_per_env": true }
Why this default matters
Unique keys per environment limit blast radius of a staging compromise.
Threshold logic
| Condition | Action |
|---|
| key fingerprint unique per env | APPROVE |
| key fingerprint shared across envs AND require_unique_per_env=true | REJECT — KEY_REUSE_ACROSS_ENV |
Developer check
if (p.require_unique_per_env && isKeySharedAcrossEnvs(key)) return reject('KEY_REUSE_ACROSS_ENV');
User-facing English
Your signing key is shared across environments. Please rotate to a unique key.
8. Default Configuration
{
"bot_id": "sec.key_rotation_reminder",
"version": "0.1.0",
"mode": "hard_guard",
"defaults": {
"rotate_every_days": 30,
"block_on_overdue_h": 24,
"require_unique_per_env": true,
"publish_to_user": true
}
}
9. Implementation Flow
- Receive signing call.
- Check KillSwitch; if active, REJECT(KILL_SWITCH_ACTIVE).
- FETCH key registration timestamp from ClobAuth.
- Compute key_age_d = (now - registered_at) / 86400.
- If key_age_d in warning range: emit WARN and continue.
- If key_age_d > rotate_every_days + block_on_overdue_h/24: REJECT(KEY_ROTATION_OVERDUE).
- If require_unique_per_env: check fingerprint across env registry; if shared, REJECT(KEY_REUSE_ACROSS_ENV).
- Emit RiskVote(APPROVE) if all checks pass.
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.
// KeyRotationReminder
FUNCTION checkKeyRotation(signingKey, envRegistry):
IF FETCH(internal.killswitch).active:
EMIT RiskVote(DENY, KILL_SWITCH_ACTIVE); RETURN
keyInfo = FETCH(clob_auth.key_info(signingKey.fingerprint))
IF keyInfo == null:
EMIT RiskVote(DENY, STALE_DATA); RETURN
key_age_d = (NOW() - keyInfo.registered_at) / 86400
grace_d = params.block_on_overdue_h / 24
// Overdue block
IF key_age_d > params.rotate_every_days + grace_d:
EMIT RiskVote(DENY, KEY_ROTATION_OVERDUE); RETURN
// Warning band
IF key_age_d > params.rotate_every_days * 0.9:
EMIT warn(KEY_ROTATION_DUE_SOON)
// Cross-env check
IF params.require_unique_per_env:
IF envRegistry.count(signingKey.fingerprint) > 1:
EMIT RiskVote(DENY, KEY_REUSE_ACROSS_ENV); RETURN
EMIT RiskVote(APPROVE)
LOG(governance.audit, {fingerprint, key_age_d})
SDK calls used
clob_auth.get_key_info(fingerprint)internal.killswitch.status()
Complexity: O(e) where e = number of environments (small constant)
11. Wire Examples
Input — what arrives on the wire
Signing call with healthy key — internal
{
"intent_id": "int_5e6f7a8b9c0d1e2f",
"key_fingerprint": "ab12cd34",
"env": "prod",
"timestamp_ms": 1746768672000
}
Output — what the bot emits
RiskVote — APPROVE
{
"vote_id": "sec.key_rotation_reminder.20260509T160000Z",
"decision": "APPROVE",
"reason_code": null,
"evidence": {
"key_age_d": 12,
"days_until_block": 19
},
"checked_at": "2026-05-09T16:00:00Z"
}
12. Decision Logic
APPROVE
Key is within rotation schedule, unique per environment, and KillSwitch inactive.
RESHAPE_REQUIRED
Not applicable.
REJECT
Key overdue for rotation or shared across environments.
WARNING_ONLY
Warn when key age is within 10% of rotate_every_days.
13. Standard Decision Output
This bot returns a RiskVote object. See RiskVote schema.
{
"vote_id": "sec.key_rotation_reminder.20260509T160000Z",
"decision": "APPROVE",
"reason_code": null,
"evidence": {
"key_fingerprint": "ab12cd34",
"key_age_d": 12,
"rotate_every_days": 30,
"days_until_required_rotation": 18
},
"checked_at": "2026-05-09T16:00:00Z"
}
14. Reason Codes
| Code | Severity | Meaning | Action | User-facing message |
|---|
KILL_SWITCH_ACTIVE | HARD_REJECT | Global kill switch is active. | Immediately return DENY. | Trading is currently paused. |
KEY_ROTATION_OVERDUE | HARD_REJECT | Signing key age exceeds rotate_every_days plus grace period. | Return DENY; prompt key rotation. | Your signing key is overdue for rotation. Please rotate it to resume trading. |
KEY_REUSE_ACROSS_ENV | HARD_REJECT | Key fingerprint detected in multiple environments. | Return DENY; require unique key per environment. | Your signing key is shared across environments. Please use a unique key. |
KEY_ROTATION_DUE_SOON | WARN | Key age exceeds 90% of rotate_every_days. | Warn user; allow signing to continue. | Your signing key will require rotation soon. |
STALE_DATA | INFO | ClobAuth API unavailable; key age could not be verified. | Log; continue with cached data if available. | Could not verify key rotation status. |
15. Metrics & Logs
Metrics emitted
| Metric | Type | Unit | Labels | Meaning |
|---|
polytraders_sec_keyrotationreminder_decisions_total | counter | count | decision | Total rotation check decisions. |
polytraders_sec_keyrotationreminder_overdue_total | counter | count | | Times signing was blocked for overdue rotation. |
polytraders_sec_keyrotationreminder_key_age_d | gauge | days | env | Current age of active signing key per environment. |
Alerts
| Alert | Condition | Severity | Runbook |
|---|
KeyRotationOverdue | polytraders_sec_keyrotationreminder_key_age_d > rotate_every_days | P1 | #runbook-keyrotation-overdue |
KeyRotationBlock | rate(polytraders_sec_keyrotationreminder_overdue_total[5m]) > 0 | P0 | #runbook-keyrotation-block |
16. Developer Reporting
{
"bot_id": "sec.key_rotation_reminder",
"decision": "APPROVE",
"inputs_used": [
"clob_auth.key_registered_at",
"config.rotate_every_days"
],
"checked_at": "2026-05-09T16:00:00Z"
}
17. Plain-English Reporting
| Situation | User-facing explanation |
|---|
| Rotation reminder | Your signing key is approaching its rotation deadline. Please rotate it before it expires. |
| Signing blocked — rotation overdue | Your signing key is overdue for rotation. Trading is blocked until you complete the rotation. |
| Signing blocked — key shared across environments | Your signing key is shared across environments. Please rotate to a unique key for each environment. |
18. Failure-Mode Block
| main_failure_mode | ClobAuth API unreachable so key registration timestamp cannot be retrieved, causing all rotation checks to fail. |
|---|
| false_positive_risk | Clock skew between ClobAuth and the bot causes a key to appear older than it is, triggering premature block. |
|---|
| false_negative_risk | Key registration timestamp cached stale, allowing an overdue key to continue signing. |
|---|
| safe_fallback | If ClobAuth is unreachable, fail-closed after block_on_overdue_h: block all signing until connectivity restored. |
|---|
| required_dependencies | ClobAuth API, Admin UI config, KillSwitch |
|---|
19. Failure-Injection Recipes
| Scenario | How to inject | Expected behaviour | Recovery |
|---|
KEY_OVERDUE | Set key registered_at to now - (rotate_every_days + 2) days | | User rotates key. |
KEY_REUSE | Register same fingerprint in both prod and staging env registry | | User generates separate key for each environment. |
CLOB_AUTH_DOWN | Block ClobAuth API endpoint | | Automatic when ClobAuth recovers. |
20. State & Persistence
Cold-start recovery
Re-read from ClobAuth on restart; fail-closed if unavailable after grace period.
21. Concurrency & Idempotency
| Aspect | Specification |
|---|
| Execution model | single-threaded event loop |
| Max in-flight | 200 |
| Idempotency key | intent_id |
| Per-call timeout (ms) | 200 |
| Backpressure strategy | drop newest |
| Locking / mutual exclusion | none |
22. Dependencies
Depends on (must run first)
Emits to (downstream consumers)
Sibling bots (same OrderIntent)
External services
| Service | Endpoint | SLA assumed | On failure |
|---|
| ClobAuth API | https://clob.polymarket.com | 99.9% | Fail-closed after block_on_overdue_h. |
23. Security Surfaces
Abuse vectors considered
- Backdating key registration timestamp to appear younger than actual age
- Using same key fingerprint across environments to avoid key generation overhead
Mitigations
- Registration timestamp read from authoritative ClobAuth API, not from client
- Cross-environment fingerprint check is server-side
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 | Tracks V2 ClobAuth API key registration timestamps; rotation policy applies to keys used for V2 order signing. |
API surfaces declared
clob_authinternal
Networks supported
polygon
25. Versioning & Migration
| Field | Value |
|---|
| spec | 2.0.0 |
| implementation | 0.1.0 |
| schema | 2 |
| released | None |
| planned_release | Q4-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 key within rotation window | key_age_d=12, rotate_every_days=30 | APPROVE |
| Warn when key in 90% of rotation window | key_age_d=28, rotate_every_days=30 | APPROVE with WARN |
| Reject when key overdue | key_age_d=32, rotate_every_days=30, block_on_overdue_h=24 | DENY(KEY_ROTATION_OVERDUE) |
Integration Tests
| Test | Expected result |
|---|
| Key shared across envs triggers reject | DENY(KEY_REUSE_ACROSS_ENV) when same fingerprint in prod and staging |
| ClobAuth unreachable fails closed after grace period | DENY after block_on_overdue_h elapses without connectivity |
Property Tests
| Property | Required behaviour |
|---|
| key_age_d > rotate_every_days + block_on_overdue_h/24 always DENY | Always true |
| Shared key fingerprint across envs always DENY when require_unique_per_env=true | Always true |
27. Operational Runbook
KeyRotationOverdue is a P1 security event; trading is blocked until rotation completes.
On-call actions
| Alert | First step | Diagnosis | Mitigation | Escalate to |
|---|
KeyRotationBlock | | | | |
KeyRotationOverdue | | | | |
Manual overrides
Healthcheck
GET /internal/health/keyrotationreminder → green if All active keys within rotate_every_days; no overdue alerts.; red if Any key older than rotate_every_days + block_on_overdue_h/24.
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 |