1. Bot Identity
| Layer | Security Security |
|---|
| Bot class | Guardrail |
|---|
| Authority | Reject |
|---|
| Status | PLANNED |
|---|
| Readiness | Spec ready |
|---|
| Runs before | exec.smart_router |
|---|
| Runs after | exec.order_lifecycle_manager |
|---|
| Applies to | Per OrderIntent |
|---|
| Default mode | shadow |
|---|
| User-visible | Yes |
|---|
| Developer owner | Security pod |
|---|
Operational profile
| Ownership | Security pod · on-call sec-oncall · #polytraders-sec · escalates to Head of Security · P1 |
|---|
| Latency budget | p50: 8ms · p99: 60ms |
|---|
| Modes supported | offshadowadvisoryenforcedquarantine |
|---|
| Data freshness | max_market_data_age_ms=5000 · max_orderbook_age_ms=5000 · max_external_feed_age_ms=5000 · on stale → REJECT — never assume funding. |
|---|
| Human override | yes · by Security on-call · logs SEC_FUNDING_OVERRIDE · time-bound: Per intent · scope: Single intent_id · second approval required |
|---|
2. Purpose
Rejects any OrderIntent whose required pUSD collateral cannot be covered by the funded balance of the assigned wallet, including a configurable buffer. Prevents the system from submitting orders that would fail at the exchange for insufficient funds, which burns latency and creates noisy reject metrics.
3. Why This Bot Matters
Insufficient-funds rejects
Each one wastes a CLOB round-trip and pollutes monitoring with noise that hides real issues.
Funding-race conditions
Two strategies competing for the same wallet's collateral can both pass an unsynchronised check; the second order rejects on-chain.
Operational embarrassment
An incident where the system tried to trade on a wallet that was never funded looks worse than catching it cleanly.
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 |
|---|
| funding_buffer_usd | 25 | 25 | 5 | Buffer that must remain free after this order is accounted for. |
| balance_cache_ttl_ms | 5000 | 5000 | 15000 | How long an on-chain balance read can be cached before it must be re-fetched. |
7. Detailed Parameter Instructions
funding_buffer_usd
What it means
Buffer that must remain free after this order is accounted for.
Default
{ "funding_buffer_usd": 25 }
Why this default matters
$25 buffer covers worst-case fee + slippage on a normal Polymarket binary order.
Threshold logic
| Condition | Action |
|---|
| ≥ $25 | PASS |
| < $25 | REJECT |
Developer check
if (free - intent.size_usd < p.funding_buffer_usd) reject('SEC_FUNDING');
User-facing English
We did not place this order because the wallet does not have enough money to cover it safely.
balance_cache_ttl_ms
What it means
How long an on-chain balance read can be cached before it must be re-fetched.
Default
{ "balance_cache_ttl_ms": 5000 }
Why this default matters
5 seconds is a reasonable trade-off for an active trading session.
Threshold logic
| Condition | Action |
|---|
| 5000 | Default |
Developer check
if (now - balanceCachedAt > p.balance_cache_ttl_ms) refresh();
User-facing English
(Internal.)
8. Default Configuration
{
"funding_buffer_usd": 25,
"balance_cache_ttl_ms": 5000
}
9. Implementation Flow
— not yet authored —
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.
with wallet_lock(wallet):
bal = balance(wallet, ttl=p.balance_cache_ttl_ms)
free = bal - reserved(wallet)
if intent.size_usd > free - p.funding_buffer_usd: return reject('SEC_FUNDING')
reserve(wallet, intent.size_usd, intent.intent_id)
return pass_()
11. Wire Examples
Input — what arrives on the wire
{
"intent_id": "intent_005",
"size_usd": 90,
"wallet_address": "0xabc"
}
Output — what the bot emits
{
"vote": "REJECT",
"reason_code": "SEC_FUNDING",
"explain": "Wallet 0xabc has $80 free; order for $90 would breach $25 buffer."
}
12. Decision Logic
APPROVE
Compute free = balance - reserved. Reserve collateral on PASS.
RESHAPE_REQUIRED
This bot does not reshape orders.
REJECT
Reject if order would breach the funding buffer.
WARNING_ONLY
No warn-only path defined.
13. Standard Decision Output
This bot returns a RiskVote object. See RiskVote schema.
{
"vote": "REJECT",
"reason_code": "SEC_FUNDING",
"explain": "Wallet 0xabc has $80 free; order for $90 would breach $25 buffer."
}
14. Reason Codes
| Code | Severity | Meaning | Action | User-facing message |
|---|
SEC_FUNDING | P1 | Sec Funding | See decision output and developer log for context. | We did not place this order because the wallet does not have enough money to cover it safely. |
SEC_FUNDING_OK | P1 | Sec Funding Ok | See decision output and developer log for context. | We did not place this order because the wallet does not have enough money to cover it safely. |
SEC_FUNDING_RACE_LOST | P1 | Sec Funding Race Lost | See decision output and developer log for context. | We did not place this order because the wallet does not have enough money to cover it safely. |
15. Metrics & Logs
Metrics emitted
| Metric | Type | Unit | Labels | Meaning |
|---|
funding_rejects_total | counter | event | market_id, reason_code | Funding rejects total. |
funding_pass_total | counter | event | bot_id | Funding pass total. |
balance_refresh_total | counter | event | bot_id | Balance refresh total. |
reservation_race_total | counter | event | bot_id | Reservation race total. |
Dashboards
16. Developer Reporting
"Per decision: intent_id, wallet_address, balance_usd, reserved_usd, intent.size_usd, vote, reason_code."
17. Plain-English Reporting
| Situation | User-facing explanation |
|---|
| When this bot acts | We did not place this order because the wallet does not have enough money to cover it safely. |
18. Failure-Mode Block
| main_failure_mode | Stale balance cache misses a withdrawal that just settled. |
|---|
| false_positive_risk | Just-funded wallet read before the next cache refresh; mitigation: on-demand cache invalidation when an inbound transfer is observed. |
|---|
| false_negative_risk | Two intents racing on the same wallet both PASS because reservation lock is not held; mitigation: reservation is taken under a per-wallet mutex. |
|---|
| safe_fallback | If the balance cannot be fetched at all, REJECT — never assume funding. |
|---|
| required_dependencies | — |
|---|
19. Failure-Injection Recipes
| Scenario | How to inject | Expected behaviour | Recovery |
|---|
Drain a wallet during shadow and assert all subsequent orders REJECT | Drain a wallet during shadow and assert all subsequent orders REJECT. | Bot detects within its latency budget and emits the corresponding reason code. | Remove the injected fault; bot returns to healthy state within one debounce window. |
Disconnect the balance RPC and assert REJECT-on-stale fires | Disconnect the balance RPC and assert REJECT-on-stale fires. | Bot detects within its latency budget and emits the corresponding reason code. | Remove the injected fault; bot returns to healthy state within one debounce window. |
20. State & Persistence
Per-wallet reserved collateral map (durable). Balance cache (in-memory, TTL).
State stores
| Name | Kind | Key | Value shape | TTL | Durability |
|---|
wallet_funding_guard_state | in-memory + fast KV mirror | bot_id | Per-wallet reserved collateral map (durable). Balance cache (in-memory, TTL). | 24h | crash-safe via KV mirror |
Cold-start recovery
Cold-start hydrates from fast KV; missing keys default to safe fallback.
On restart
All in-flight decisions are re-evaluated; no bot decision is trusted across restart without re-emit.
21. Concurrency & Idempotency
| Aspect | Specification |
|---|
| Execution model | Per-wallet mutex. Read-modify-write on (balance, reserved). |
| Max in-flight | 32 |
| Idempotency key | order_intent_id |
| Replay-safe | True |
| Deduplication | By idempotency_key within a 60s window. |
| Ordering guarantees | Per-market_id FIFO; cross-market unordered. |
| Per-call timeout (ms) | 250 |
| Backpressure strategy | Bounded queue; oldest-dropped with metric increment when full. |
| Locking / mutual exclusion | Per-market_id mutex; no global locks. |
22. Dependencies
Depends on (must run first)
Emits to (downstream consumers)
Requires (graph.requires)
exec.order_lifecycle_manager
Required before (graph.required_before)
exec.smart_router
| Consumes | OrderIntent WalletBalance ReservationMap |
|---|
| Emits | RiskVote |
|---|
| Blocks orders | yes |
|---|
23. Security Surfaces
Read-only RPC for ERC-20 balance.
Signing surface
None — bot does not sign or submit.
Abuse vectors considered
- Reservation map is internal only and writes are mutex-guarded.
Mitigations
- Rate-limit per source
- Audit-log every override
- Require role-based authz on admin paths
24. Polymarket V2 Compatibility
| Aspect | Value |
|---|
| CLOB version | V2 |
| Collateral asset | pUSD |
| EIP-712 Exchange domain version | 2 |
| Aware of builderCode field | yes |
| Aware of negative-risk markets | yes |
| Multi-chain ready | yes |
| SDK used | Polymarket CLOB V2 SDK |
| Settlement contract | CTFExchangeV2 |
| Notes | Reads pUSD ERC-20 balance directly; no V1/V2 difference. |
25. Versioning & Migration
| Field | Value |
|---|
| current | 0.1.0 |
| contract_version | 1.0.0 |
| last_breaking_change | none |
| deprecation_window_days | 30 |
26. Acceptance Tests
Unit Tests
| Test | Setup | Expected result |
|---|
| Free = $25 + intent.size → PASS. | Synthetic fixture per template. | Behaviour matches the rule described in the test name. |
| Free = intent.size → REJECT. | Synthetic fixture per template. | Behaviour matches the rule described in the test name. |
| Race two intents on the same wallet → exactly one PASS, one REJECT. | Synthetic fixture per template. | Behaviour matches the rule described in the test name. |
Integration Tests
| Test | Expected result |
|---|
| End-to-end: drain a wallet to $24 and assert all OrderIntents are rejected. | End-to-end behaviour matches the spec without manual intervention. |
Property Tests
| Property | Required behaviour |
|---|
| Sum of accepted intent sizes never exceeds (balance - funding_buffer_usd). | Always true across all generated inputs. |
27. Operational Runbook
If reject rate spikes from this bot, check whether a wallet was drained or whether the balance cache is failing to refresh.
On-call actions
| Alert | First step | Diagnosis | Mitigation | Escalate to |
|---|
5.9_anomaly | Open the bot's reporting page and confirm the alert is real (not a metric hiccup). | Inspect developer log entries for the affected market_id over the last 30 minutes. | Force-clear via Admin UI if the rule is clearly stale; otherwise leave engaged and notify owner. | Security pod |
Manual overrides
polytraders bot pause 5.9 — Disables the bot's enforcement layer; downstream consumers fall back to safe defaults.
Healthcheck
GET /healthz/wallet_funding_guard → 200 if last successful evaluation < 60s ago.
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 |