1. Bot Identity
| Layer | Risk Risk |
|---|
| Bot class | Guardrail |
|---|
| Authority | VetoReshape |
|---|
| Status | PLANNED |
|---|
| Readiness | Planned |
|---|
| Runs before | ExecutionPlan emit |
|---|
| Runs after | Strategy OrderIntent |
|---|
| Applies to | Every OrderIntent — enforces per-strategy and portfolio-level capital allocation budgets |
|---|
| Default mode | planned |
|---|
| User-visible | summary-only |
|---|
| Developer owner | Polytraders core — Risk pod |
|---|
Operational profile
| Modes supported | quarantine |
|---|
2. Purpose
CapitalAllocator carves the user's total risk budget across active strategies and refuses new orders that would cause any strategy slice or the aggregate portfolio to exceed its configured allocation. It emits a RESHAPE constraint when a downsize is possible, and HARD_REJECT when the budget is exhausted.
3. Why This Bot Matters
Over-allocation to a single strategy
Unconstrained capital flow into one strategy can consume the user's full budget, preventing diversification and concentrating tail risk.
Aggregate portfolio budget exceeded
Without a total-portfolio cap, concurrent strategies can collectively exceed the user's acceptable exposure, especially during correlated market events.
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 |
|---|
| per_strategy_max_usd | 2000 | 1600 | 2000 | Maximum pUSD notional a single strategy may hold (open + pending). |
| portfolio_total_max_usd | 10000 | 8000 | 10000 | Maximum total pUSD notional across all active strategies combined. |
| min_remaining_buffer_pct | 0.05 | 0.1 | 0.05 | Minimum fraction of the portfolio budget that must remain unallocated as a buffer. Orders bringing the budget below this fraction are reshaped to maintain the buffer. |
7. Detailed Parameter Instructions
per_strategy_max_usd
What it means
Maximum pUSD notional a single strategy may hold (open + pending).
Default
{ "per_strategy_max_usd": 2000 }
Why this default matters
Caps each strategy at 2000 pUSD by default, forcing diversification across multiple strategies.
Threshold logic
| Condition | Action |
|---|
| strategy_exposure + intent.size_usd <= 2000 | APPROVE |
| 80-100% of cap | RESHAPE to remaining budget |
| > 100% of cap | HARD_REJECT — CAPITAL_ALLOCATOR_STRATEGY_BUDGET_EXCEEDED |
Developer check
if (strategyExposure + intent.size_usd > params.per_strategy_max_usd) return reshape or reject;
User-facing English
Your order exceeds the budget allocated to this strategy.
portfolio_total_max_usd
What it means
Maximum total pUSD notional across all active strategies combined.
Default
{ "portfolio_total_max_usd": 10000 }
Why this default matters
Ensures the sum of all strategy exposures stays within the user's declared total risk budget.
Threshold logic
| Condition | Action |
|---|
| total_exposure + intent.size_usd <= 10000 | APPROVE |
| > 10000 | HARD_REJECT — CAPITAL_ALLOCATOR_PORTFOLIO_BUDGET_EXCEEDED |
Developer check
if (totalExposure + intent.size_usd > params.portfolio_total_max_usd) return reject('CAPITAL_ALLOCATOR_PORTFOLIO_BUDGET_EXCEEDED');
User-facing English
Your total portfolio exposure has reached its limit.
min_remaining_buffer_pct
What it means
Minimum fraction of the portfolio budget that must remain unallocated as a buffer. Orders bringing the budget below this fraction are reshaped to maintain the buffer.
Default
{ "min_remaining_buffer_pct": 0.05 }
Why this default matters
Keeps 5% of budget free to handle mark-to-market fluctuations without triggering cascade rejects.
Threshold logic
| Condition | Action |
|---|
| remaining_pct >= 0.10 | APPROVE |
| 0.05 <= remaining_pct < 0.10 | WARN |
| remaining_pct < 0.05 | RESHAPE — cap order to preserve buffer |
Developer check
if (remainingAfterOrder / portfolioCap < params.min_remaining_buffer_pct) return reshape({ max_size_usd: remainingBudget - bufferAmount });
User-facing English
Your order was reduced to maintain a safety buffer in your portfolio budget.
8. Default Configuration
{
"bot_id": "risk.capital_allocator",
"version": "0.1.0",
"mode": "hard_guard",
"defaults": {
"per_strategy_max_usd": 2000,
"portfolio_total_max_usd": 10000,
"min_remaining_buffer_pct": 0.05
},
"locked": {
"per_strategy_max_usd": {
"min": 100
},
"portfolio_total_max_usd": {
"min": 500
}
}
}
9. Implementation Flow
- Receive OrderIntent with strategy_id, size_usd, and user context.
- Check KillSwitch; if active, HARD_REJECT(KILL_SWITCH_ACTIVE).
- Load capital allocation config (per_strategy_max_usd, portfolio_total_max_usd, buffer_pct) from internal store.
- Fetch current strategy exposure (open + pending) for strategy_id from CLOB and position ledger.
- Fetch total portfolio exposure across all strategies.
- If strategy_exposure + intent.size_usd > per_strategy_max_usd: compute safe_size = max(0, per_strategy_max_usd - strategy_exposure). If safe_size > 0, RESHAPE; else HARD_REJECT.
- If total_exposure + intent.size_usd > portfolio_total_max_usd * (1 - min_remaining_buffer_pct): RESHAPE or HARD_REJECT.
- All checks passed — APPROVE with budget metrics attached.
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 evaluateCapital(intent):
ks = FETCH internal.killswitch.status
IF ks.active: EMIT RiskVote(HARD_REJECT, KILL_SWITCH_ACTIVE); RETURN
config = FETCH internal.capital_config(intent.user_id)
IF config IS NULL: EMIT RiskVote(HARD_REJECT, CAPITAL_ALLOCATOR_DATA_UNAVAILABLE); RETURN
positions = FETCH clob.get_positions(intent.user_id, intent.strategy_id)
IF positions IS NULL: EMIT RiskVote(HARD_REJECT, CAPITAL_ALLOCATOR_DATA_UNAVAILABLE); RETURN
strategyExposure = positions.open_usd + positions.pending_usd
totalExposure = positions.portfolio_total_usd
// per-strategy check
IF strategyExposure + intent.size_usd > config.per_strategy_max_usd:
safeSize = config.per_strategy_max_usd - strategyExposure
IF safeSize <= 0: EMIT RiskVote(HARD_REJECT, CAPITAL_ALLOCATOR_STRATEGY_BUDGET_EXCEEDED); RETURN
EMIT RiskVote(RESHAPE_REQUIRED, constraints={max_size_usd: safeSize}); RETURN
// portfolio buffer check
buffer = config.portfolio_total_max_usd * config.min_remaining_buffer_pct
IF totalExposure + intent.size_usd > config.portfolio_total_max_usd - buffer:
EMIT RiskVote(HARD_REJECT, CAPITAL_ALLOCATOR_PORTFOLIO_BUDGET_EXCEEDED); RETURN
EMIT RiskVote(APPROVE, checked_at=now_ms())
SDK calls used
clob.get_positions(user_id, strategy_id)internal.capital_config(user_id)internal.killswitch.status()
Complexity: O(1) per strategy lookup
11. Wire Examples
Input — what arrives on the wire
OrderIntent — strategy budget overflow — internal
{
"intent_id": "int_b2c3d4e5f6a70002",
"strategy_id": "strat_001",
"size_usd": 400,
"generated_at_ms": 1746800000000
}
Output — what the bot emits
RiskVote — RESHAPE_REQUIRED
{
"guard_id": "risk.capital_allocator",
"decision": "RESHAPE_REQUIRED",
"severity": "WARN",
"reason_code": "CAPITAL_ALLOCATOR_STRATEGY_BUDGET_EXCEEDED",
"message": "Strategy exposure 1800 pUSD + 400 pUSD exceeds cap 2000. Resized to 200 pUSD.",
"constraints": {
"max_size_usd": 200
},
"checked_at": "2026-05-10T09:00:00Z"
}
12. Decision Logic
APPROVE
Strategy slice and portfolio total are both within budget after including the intent size.
RESHAPE_REQUIRED
Intent size would exceed the strategy or portfolio budget but a reduced size is possible within the remaining allocation.
REJECT
Budget is fully exhausted (no room even for a reduced order), or KillSwitch active.
WARNING_ONLY
— not yet authored —13. Standard Decision Output
This bot returns a RiskVote object. See RiskVote schema.
{
"guard_id": "risk.capital_allocator",
"decision": "RESHAPE_REQUIRED",
"severity": "WARN",
"reason_code": "CAPITAL_ALLOCATOR_STRATEGY_BUDGET_EXCEEDED",
"message": "Strategy exposure 1800 pUSD + intent 400 pUSD exceeds cap 2000 pUSD. Resized to 200 pUSD.",
"constraints": {
"max_size_usd": 200
},
"inputs_used": [
"internal.capital_config",
"clob_public.positions"
],
"checked_at": "2026-05-10T09:00:00Z"
}
14. Reason Codes
| Code | Severity | Meaning | Action | User-facing message |
|---|
KILL_SWITCH_ACTIVE | HARD_REJECT | Global kill switch active. | Immediate HARD_REJECT. | Trading is paused. Please try again later. |
CAPITAL_ALLOCATOR_STRATEGY_BUDGET_EXCEEDED | RESHAPE | Strategy slice is at or above per_strategy_max_usd. | RESHAPE if room remains; else HARD_REJECT. | Your order exceeds the budget for this strategy. |
CAPITAL_ALLOCATOR_PORTFOLIO_BUDGET_EXCEEDED | HARD_REJECT | Portfolio total is at or above the cap including buffer. | HARD_REJECT; no reshape possible at portfolio level. | Your total portfolio exposure has reached its limit. |
CAPITAL_ALLOCATOR_BUFFER_WARN | WARN | Remaining portfolio buffer is below the warning threshold. | Attach WARN annotation; allow order if still above hard floor. | |
CAPITAL_ALLOCATOR_DATA_UNAVAILABLE | HARD_REJECT | Capital config or position data unavailable. | HARD_REJECT (fail-closed). | We could not verify your position data. Please try again. |
15. Metrics & Logs
Metrics emitted
| Metric | Type | Unit | Labels | Meaning |
|---|
polytraders_risk_capitalallocator_decisions_total | counter | count | decision, reason_code | Total RiskVote decisions by type and reason. |
polytraders_risk_capitalallocator_strategy_exposure_usd | gauge | usd | strategy_id | Current strategy exposure in pUSD at evaluation time. |
polytraders_risk_capitalallocator_portfolio_utilisation_pct | gauge | ratio | | Portfolio budget utilisation fraction (0–1) at last evaluation. |
polytraders_risk_capitalallocator_eval_latency_ms | histogram | milliseconds | | Latency from intent receipt to RiskVote emit. |
Alerts
| Alert | Condition | Severity | Runbook |
|---|
CapitalAllocatorPortfolioNearCap | polytraders_risk_capitalallocator_portfolio_utilisation_pct > 0.9 | P2 | #runbook-capitalallocator-near-cap |
CapitalAllocatorDataUnavailable | rate(polytraders_risk_capitalallocator_decisions_total{reason_code='CAPITAL_ALLOCATOR_DATA_UNAVAILABLE'}[5m]) > 0 | P1 | #runbook-capitalallocator-data |
16. Developer Reporting
{
"bot_id": "risk.capital_allocator",
"decision": "RESHAPE_REQUIRED",
"reason_code": "CAPITAL_ALLOCATOR_STRATEGY_BUDGET_EXCEEDED",
"inputs_used": [
"internal.capital_config",
"clob_public.positions"
],
"metrics": {
"strategy_exposure_usd": 1800,
"portfolio_exposure_usd": 5400,
"intent_size_usd": 400,
"per_strategy_cap": 2000,
"portfolio_cap": 10000
},
"checked_at": "2026-05-10T09:00:00Z"
}
17. Plain-English Reporting
| Situation | User-facing explanation |
|---|
| Order downsized — strategy budget | Your order was reduced because this strategy has reached its capital allocation limit. The order was resized to fit within the remaining budget. |
| Order blocked — portfolio budget exhausted | Your total portfolio exposure has reached the configured limit. Please close some positions before placing new orders. |
18. Failure-Mode Block
| main_failure_mode | Approving an order that exceeds the strategy or portfolio budget due to a stale position snapshot, causing over-allocation. |
|---|
| false_positive_risk | Rejecting an order because the position cache includes a recently cancelled pending order that has not yet been removed. |
|---|
| false_negative_risk | Approving concurrent orders from the same strategy if idempotency deduplication is bypassed, leading to budget double-counting. |
|---|
| safe_fallback | If capital allocation config or position data is unavailable, HARD_REJECT with CAPITAL_ALLOCATOR_DATA_UNAVAILABLE. Never approve on missing data. |
|---|
| required_dependencies | Capital allocation config store, CLOB position and pending order data, KillSwitch active flag |
|---|
19. Failure-Injection Recipes
| Scenario | How to inject | Expected behaviour | Recovery |
|---|
POSITION_DATA_UNAVAILABLE | Return 503 from CLOB positions endpoint | | Returns to normal within one position-cache refresh cycle. |
STRATEGY_BUDGET_EXHAUSTED | Set strategy exposure = per_strategy_max_usd in position mock, submit any intent | | Returns to APPROVE after strategy closes positions. |
PORTFOLIO_BUFFER_BREACH | Set portfolio total > portfolio_max * (1 - buffer) in position mock | | Returns to APPROVE after portfolio exposure decreases. |
20. State & Persistence
Cold-start recovery
On cold start, position snapshot loaded from CLOB before first evaluation. If unavailable, HARD_REJECT until restored.
21. Concurrency & Idempotency
| Aspect | Specification |
|---|
| Execution model | single-threaded event loop |
| Max in-flight | 200 |
| Idempotency key | intent_id |
| Per-call timeout (ms) | 100 |
| Backpressure strategy | drop newest |
| Locking / mutual exclusion | per-strategy_id optimistic lock to prevent concurrent over-allocation |
22. Dependencies
Depends on (must run first)
| Bot | Why | Contract |
|---|
| risk.kill_switch | Global brake checked first. | HARD_REJECT(KILL_SWITCH_ACTIVE) short-circuits all budget evaluation. |
Emits to (downstream consumers)
External services
| Service | Endpoint | SLA assumed | On failure |
|---|
| CLOB API (positions) | https://clob.polymarket.com | 99.95% / 200ms p99 | HARD_REJECT(CAPITAL_ALLOCATOR_DATA_UNAVAILABLE) if position data unavailable. |
23. Security Surfaces
Abuse vectors considered
- Submitting concurrent intents to bypass per-strategy idempotency and double-count budget
- Poisoning the position cache to show lower exposure than actual
Mitigations
- Per-intent_id deduplication within 24h window prevents double-count
- Position cache is validated against CLOB on each refresh; stale data triggers fail-closed
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 | All exposure values are denominated in pUSD. Uses V2 CLOB position endpoints; no order signing. |
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 when within strategy and portfolio budget | strategy_exposure=500, portfolio_exposure=3000, intent.size_usd=300, caps=2000/10000 | APPROVE |
| Reshape when strategy budget partially exceeded | strategy_exposure=1800, intent.size_usd=400, cap=2000 | RESHAPE_REQUIRED(max_size_usd=200) |
| Reject when strategy budget fully exhausted | strategy_exposure=2000, intent.size_usd=100, cap=2000 | HARD_REJECT(CAPITAL_ALLOCATOR_STRATEGY_BUDGET_EXCEEDED) |
| Reject when portfolio cap exceeded | total_exposure=9800, intent.size_usd=300, portfolio_cap=10000, buffer=0.05 | HARD_REJECT(CAPITAL_ALLOCATOR_PORTFOLIO_BUDGET_EXCEEDED) |
Integration Tests
| Test | Expected result |
|---|
| Reshape flows through to ExecutionPlan with reduced size | ExecutionPlan receives constraints.max_size_usd and does not exceed it |
| KillSwitch bypasses all budget checks | HARD_REJECT(KILL_SWITCH_ACTIVE) without reading position data |
Property Tests
| Property | Required behaviour |
|---|
| Approved order never causes strategy exposure to exceed per_strategy_max_usd | Always true |
| Reshape size is always strictly <= requested order size | Always true |
27. Operational Runbook
Incidents typically involve a position cache sync delay causing false budget exhaustion, or a genuine portfolio cap breach requiring position reduction.
On-call actions
| Alert | First step | Diagnosis | Mitigation | Escalate to |
|---|
CapitalAllocatorPortfolioNearCap | Check current portfolio exposure in Grafana; confirm positions are accurate and no cache sync issue. | | | Risk pod lead if genuine cap breach confirmed. |
CapitalAllocatorDataUnavailable | Check CLOB API status and Redis connectivity; manually refresh position cache if possible. | | | Risk pod lead if sustained > 2 minutes. |
Manual overrides
polytraders risk refresh-positions --user-id <id> — After a known CLOB sync delay; forces immediate position cache refresh.
Healthcheck
GET /internal/health/capitalallocator → green: Position cache age < 30s, CLOB reachable, no DATA_UNAVAILABLE rejections in last 5m; red: Position cache age > 60s or CLOB unreachable
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 |