Back to skill

Security audit

Polymarket Bundle Esports Tempo Trader

Security checks for vulnerabilities and agentic risk

Overview

This is a disclosed esports trading skill, but its live-trading safeguards do not fully match what it documents, so users should review it carefully before giving it trading authority.

Install only if you are comfortable reviewing and accepting live financial trading risk. Keep the API key limited to the smallest possible trading authority, leave the skill in paper mode until tested, pin and review simmer-sdk, and do not use --live until the market-identity fallback, volume filter, and true open-position limit are fixed.

Vulnerability Patterns
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
Findings (4)

T09 · Insecure Skill Coding Practices

Error
Location
trader.py:178
Finding

Unidentified Markets Are Grouped into Shared Bundles, Enabling Cross-Match Trading Signals

Content
View full analysis
str: """Extract a normalized match identifier.""" m = _MATCH_PREFIX.search(question) if m: raw = m.group(1).strip() raw = _GAME_NUM.sub("", raw) key = re.sub(r"\s+", " ", raw.lower()).strip(" -|:,") if len(key) >= 3: return key if market is not None: for attr in ("event_slug", "group_slug", "import_source", "event_id"): val = getattr(market, attr, None) if val and isinstance(val, str) and len(val) >= 3: return val.lower().strip() return "unknown_match" ``` ```python prop_type, line_value = prop match_key = parse_match_key(q, market=m) game_number = parse_game_number(q) key = f"{match_key}|game{game_number}" tm = TempoMarket(m, match_key, game_number, prop_type, line_value, float(p)) bundles.setdefault(key, []).append(tm) ``` ### Technical Analysis When both question parsing and metadata-based identity extraction fail, `parse_match_key()` returns the constant value `unknown_match`. `build_bundles()` then combines this shared value with the game number and uses it as the bundle key. Consequently, all unidentified markets with the same parsed game number are treated as propositions belonging to the same match. The subsequent inconsistency analysis assumes that every proposition in a bundle is correlated. It can therefore derive a trading signal from unrelated matches and pass that signal to the live trade execution path. This violates the integrity requirement for the strategy: bundle membership must be based on a verified common event, not a shared error value. ### Attack Path 1. Two or more qualifying esports markets are returned by the market API. 2. Their questions do not yi ...[truncated 1193 chars]
Remediation
View remediation
str | None: # Perform validated extraction... return None match_key = parse_match_key(q, market=m) if match_key is None: continue ``` ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
trader.py:35
Finding

Documented Minimum-Volume Safeguard Is Not Enforced

Content
View full analysis
tuple[str | None, float, str]: """ Returns (side, size, reasoning) or (None, 0, skip_reason). Conviction scales with the magnitude of the tempo inconsistency. Threshold gates (YES_THRESHOLD / NO_THRESHOLD) still apply as hard limits. """ p = getattr(market, "current_probability", None) if not isinstance(p, (int, float)): return None, 0, "missing probability" # Spread gate spread_cents = getattr(market, "spread_cents", None) if isinstance(spread_cents, (int, float)) and spread_cents / 100 > MAX_SPREAD: return None, 0, f"Spread {spread_cents/100:.1%} > {MAX_SPREAD:.1%}" # Days-to-resolution gate resolves_at = getattr(market, "resolves_at", None) if resolves_at: try: resolves = datetime.fromisoformat(resolves_at.replace("Z", "+00:00")) days = (resolves - datetime.now(timezone.utc)).days if days < MIN_DAYS: return None, 0, f"Only {days} days to resolve" except Exception: pass ``` ### Technical Analysis `SIMMER_MIN_VOLUME` is exposed as a risk parameter and documented as the minimum acceptable market volume, but it is not referenced by market discovery, signal generation, or trade execution after configuration is loaded. Low-volume markets are more susceptible to stale prices, price manipulation, poor fills, and difficult ...[truncated 1375 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
trader.py:599
Finding

Maximum Concurrent Position Limit Counts Only Orders Placed During the Current Run

Content
View full analysis
= MAX_POSITIONS: break ``` ```python if r.success: placed += 1 ``` ### Technical Analysis The setting named `MAX_POSITIONS` is documented as a limit on concurrent open positions. The implementation does not retrieve open positions or pending orders from the account. Instead, it initializes `placed` to zero on every process invocation and increments it only after a successful order in that invocation. As a result, the code implements a per-run successful-order limit rather than a concurrent-position limit. Existing positions, pending orders, partially filled orders, and exposure created by previous executions are omitted. This is a risk-control failure because repeatedly starting the trader can accumulate substantially more exposure than the configured maximum implies. ### Attack Path 1. The account already has one or more open positions created by an earlier run. 2. The trader starts again and resets `placed` to zero. 3. Market discovery produces additional qualifying opportunities. 4. The loop allows as many as `MAX_POSITIONS` successful orders without accounting for existing positions. 5. Subsequent invocations repeat the process and continue increasing concurrent exposure. No external attacker is required; manual restarts, automation, or repeated scheduled execution can trigger the condition. An attacker who can induce repeated execution or maintain qualifying market conditions could amplify the exposure. ### Impact Assessment The account can hold more concurrent positions than the operator intended, increasing aggregate USDC exposure and correlated-loss risk. The issue does not provide host privilege ...[truncated 252 chars]
Remediation
View remediation
= MAX_POSITIONS: return remaining = MAX_POSITIONS - used ``` ]]>

T08 · Insecure Dependencies

Warning
Location
clawhub.json:3
Finding

Security-Sensitive Runtime Dependency Is Unpinned

Content
View full analysis
Remediation
View remediation
Vulnerability Patterns
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
Findings (3)

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
90% confidence
Finding

The skill declares access to sensitive environment-backed credentials such as SIMMER_API_KEY but does not define any explicit tool or permission scope. In an agent framework, missing least-privilege boundaries can allow broader-than-necessary access to secrets or execution capabilities, increasing the blast radius if the skill is misused or composed with other tools.

Content

No source excerpt is available for this finding.

Autonomous Decision Making

Medium
Category
Excessive Agency
Confidence
75% confidence
Finding

Skill enables autonomous high-impact decisions without human-in-the-loop verification. Critical operations (destructive commands, financial transactions, data deletion) should require explicit user confirmation.

Content

Scanner excerpt · SKILL.md (reported line 92)May include surrounding context.

md
Dota2 game props form a "tempo bundle" where all indicators should tell a consistent story about how the game will play out. Traditional sportsbooks enforce this consistency through centralized line-setting. Polymarket has no such mechanism -- each prop is its own independent order book. Esports tempo props are especially vulnerable because:

- First blood, kills, daytime, and multi-kill props are deeply correlated but priced independently
- Retail traders specialize in one prop type (e.g., kills markets) without checking adjacent tempo indicators
- Game-specific knowledge (Dota2 day/night cycle mechanics, teamfight timing) is niche and not widely understood by general prediction market participants
- Multi-game series (BO3/BO5) create separate tempo bundles per game, multiplying the surface area for inconsistencies
- New props added mid-series don't inherit tempo context from earlier games

Dynamic attribute access via getattr()

Low
Category
Dangerous Code Execution
Confidence
50% confidence
Finding

Dynamic getattr() with a non-literal attribute name can access arbitrary object attributes, potentially bypassing access controls.

Content

Scanner excerpt · trader.py (reported line 174)May include surrounding context.

python
if market is not None:
        for attr in ("event_slug", "group_slug", "import_source", "event_id"):
            val = getattr(market, attr, None)
            if val and isinstance(val, str) and len(val) >= 3:
                return val.lower().strip()

Static analysis

No suspicious patterns detected.