Back to skill

Security audit

Polymarket Twitter Cadence Model Trader

Security checks for vulnerabilities and agentic risk

Overview

This skill is upfront that it can paper trade by default and place real Polymarket trades with --live, but several documented trading safeguards are not actually enforced in the code.

Install only if you are comfortable giving this skill a Simmer API key and keeping it in paper mode unless you have independently reviewed the code and dependency. Do not provide a live-capable key or run with --live until the market-volume check, account-wide position limit, order-size bounds, and live/paper client handling 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:266
Finding
Minimum Market-Volume Safeguard Is Not Enforced<![CDATA[ ## Vulnerability Details **File Location**: `trader.py:45-51`, `trader.py:79-85`, and `trader.py:266-280` **Vulnerability Type**: Declared financial-risk control is not enforced **Risk Level**: High The `SIMMER_MIN_VOLUME` parameter is loaded into `MIN_VOLUME`: ```python # Risk parameters MAX_POSITION = float(os.environ.get("SIMMER_MAX_POSITION", "40")) MIN_VOLUME = float(os.environ.get("SIMMER_MIN_VOLUME", "1000")) MAX_SPREAD = float(os.environ.get("SIMMER_MAX_SPREAD", "0.10")) MIN_DAYS = int(os.environ.get( "SIMMER_MIN_DAYS", "0")) MAX_POSITIONS = int(os.environ.get( "SIMMER_MAX_POSITIONS", "8")) YES_THRESHOLD = float(os.environ.get("SIMMER_YES_THRESHOLD", "0.38")) NO_THRESHOLD = float(os.environ.get("SIMMER_NO_THRESHOLD", "0.62")) MIN_TRADE = float(os.environ.get("SIMMER_MIN_TRADE", "5")) ``` It is loaded again after applying the Skill configuration: ```python MAX_POSITION = float(os.environ.get("SIMMER_MAX_POSITION", str(MAX_POSITION))) MIN_VOLUME = float(os.environ.get("SIMMER_MIN_VOLUME", str(MIN_VOLUME))) MAX_SPREAD = float(os.environ.get("SIMMER_MAX_SPREAD", str(MAX_SPREAD))) MIN_DAYS = int(os.environ.get( "SIMMER_MIN_DAYS", str(MIN_DAYS))) MAX_POSITIONS = int(os.environ.get( "SIMMER_MAX_POSITIONS", str(MAX_POSITIONS))) YES_THRESHOLD = float(os.environ.get("SIMMER_YES_THRESHOLD", str(YES_THRESHOLD))) NO_THRESHOLD = float(os.environ.get("SIMMER_NO_THRESHOLD", str(NO_THRESHOLD))) MIN_TRADE = float(os.environ.get("SIMMER_MIN_TRADE", str(MIN_TRADE))) ``` However, the trading loop proceeds directly from signal generation to context validation and order submission without checking market volume: ```python placed = 0 for m in markets: if placed >= MAX_POSITIONS: break side, size, reasoning = compute_signal(m) if not side: safe_print(f" [skip] {reasoning}") continue ok, why = context_ok(client, m.id) if not ok: ...[truncated 1549 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Retrieve the market's authoritative volume value before computing or executing a trade. - Reject markets whose volume is missing, malformed, non-finite, or below `MIN_VOLUME`. - Use a fail-closed policy when volume data cannot be obtained. - Ensure the volume field and `MIN_VOLUME` use the same currency and measurement period. - Add a second validation immediately before `client.trade()` to reduce time-of-check/time-of-use risk. - Add automated tests proving that zero-volume, missing-volume, and sub-threshold markets cannot reach the trading API. - Update logging to record the observed volume and applicable threshold for every rejection. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
trader.py:266
Finding
Maximum Open-Position Limit Only Counts Orders Placed During the Current Run<![CDATA[ ## Vulnerability Details **File Location**: `trader.py:266-287` **Vulnerability Type**: Incomplete enforcement of account-wide exposure limit **Risk Level**: High ```python placed = 0 for m in markets: if placed >= MAX_POSITIONS: break side, size, reasoning = compute_signal(m) if not side: safe_print(f" [skip] {reasoning}") continue ok, why = context_ok(client, m.id) if not ok: safe_print(f" [skip] {why}") continue try: r = client.trade( market_id=m.id, side=side, amount=size, source=TRADE_SOURCE, skill_slug=SKILL_SLUG, reasoning=reasoning, ) tag = "(sim)" if r.simulated else "(live)" status = "OK" if r.success else f"FAIL:{r.error}" safe_print(f" [trade] {side.upper()} ${size} {tag} {status} -- {reasoning[:70]}") if r.success: placed += 1 ``` ### Technical Analysis `MAX_POSITIONS` is documented as the maximum number of concurrent open positions. The implementation instead initializes a local counter to zero on every invocation and increments it only for successful orders placed during that invocation. The code never retrieves existing account positions, does not count unresolved exposure from earlier runs, and does not determine whether the account already has a position in the selected market. Therefore, `MAX_POSITIONS` is a per-run order limit rather than an account-wide open-position limit. Repeated invocations can continuously add exposure while each individual run remains within the local counter. ### Attack Path 1. The account already holds `MAX_POSITIONS` unresolved positions. 2. The operator, automation, or another process invokes the Skill again. 3. The local `placed` counter is reset to zero. 4. Market discovery identifies additional qualifying markets. 5. The Skill submits up to `MAX_POSITIONS` further successful orders. 6. Repeating the process causes the actual number of open positio ...[truncated 460 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Query all current unresolved/open positions before entering the trading loop. - Initialize the limit from the number of existing unique open positions rather than zero. - Treat pending orders as exposure where the API permits them to be queried. - Skip markets in which the account already has exposure unless an explicitly defined position-adjustment policy permits another order. - Enforce an account-wide invariant such as `existing_open + newly_opened < MAX_POSITIONS`. - Re-query position state immediately before submitting an order, or use an atomic server-side account limit, to address concurrent Skill runs. - Add tests covering pre-existing positions, duplicate markets, pending orders, repeated runs, and concurrent invocations. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
trader.py:204
Finding
Minimum Trade Size Can Override the Configured Maximum Position Size<![CDATA[ ## Vulnerability Details **File Location**: `trader.py:204-228` **Vulnerability Type**: Unsafe financial-limit calculation **Risk Level**: Medium The same unsafe calculation is used for both YES and NO orders: ```python if p <= YES_THRESHOLD: # We'd buy YES — does our model agree this bin is underpriced? if model_p <= p * 0.5: return None, 0, f"Model disagrees: model={model_p:.1%} < mkt={p:.1%}, skip YES" bias = min(2.0, max(0.5, model_p / max(p, 0.01))) conviction = min(1.0, (YES_THRESHOLD - p) / YES_THRESHOLD * bias) size = max(MIN_TRADE, round(conviction * MAX_POSITION, 2)) edge = model_p - p return "yes", size, ( f"YES model={model_p:.0%} mkt={p:.0%} edge={edge:+.0%} " f"lam={lam:.0f} bias={bias:.1f}x ${size} -- {q[:55]}" ) if p >= NO_THRESHOLD: # We'd sell NO — does our model agree this bin is overpriced? if model_p >= p * 1.5: return None, 0, f"Model disagrees: model={model_p:.1%} > mkt={p:.1%}, skip NO" bias = min(2.0, max(0.5, max(p, 0.01) / max(model_p, 0.01))) conviction = min(1.0, (p - NO_THRESHOLD) / (1 - NO_THRESHOLD) * bias) size = max(MIN_TRADE, round(conviction * MAX_POSITION, 2)) edge = p - model_p ``` The allowed configuration ranges overlap unsafely: `SIMMER_MAX_POSITION` can be as low as `1`, while `SIMMER_MIN_TRADE` can be as high as `100`. ### Technical Analysis The calculation uses `max(MIN_TRADE, calculated_size)`. If `MIN_TRADE` is greater than `MAX_POSITION`, the resulting order size exceeds the configured maximum position. For example, a valid configuration of `MAX_POSITION=10` and `MIN_TRADE=100` results in a `$100` order. A maximum exposure control must be an absolute upper bound. A minimum execution amount should cause undersized signals to be skipped, not cause an order to be increased beyond the maximum. ### Attack Path 1. Skill configuration or environment variables set `SIMMER_MIN_TRADE` to a value greater than `SIMMER_MAX_ ...[truncated 733 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Validate configuration at startup and reject `MIN_TRADE > MAX_POSITION`. - Treat `MAX_POSITION` as an unconditional upper bound. - Calculate the conviction amount first; if it is below `MIN_TRADE`, skip the trade rather than increasing it. - Apply a final bound before submission, for example: ```python calculated_size = round(conviction * MAX_POSITION, 2) if calculated_size < MIN_TRADE: return None, 0, "Calculated order is below the minimum trade size" size = min(calculated_size, MAX_POSITION) ``` - Validate all numeric settings as finite and non-negative. - Add tests for equal limits, inverted limits, boundary values, zero conviction, and both YES and NO paths. ]]>

T08 · Insecure Dependencies

Note
Location
clawhub.json:5
Finding
Trading Dependency Is Installed Without a Version or Integrity Pin<![CDATA[ ## Vulnerability Details **File Location**: `clawhub.json:5-10` **Vulnerability Type**: Unpinned security-sensitive third-party dependency **Risk Level**: Low ```json "requires": { "env": [ "SIMMER_API_KEY" ], "pip": [ "simmer-sdk" ] }, ``` The imported dependency is given the API credential and performs market searches and trades: ```python _client = SimmerClient( api_key=os.environ["SIMMER_API_KEY"], venue=venue, ) ``` ### Technical Analysis The package requirement specifies only `simmer-sdk`, without an exact reviewed version or an integrity hash. Each installation may therefore resolve to a different release. This dependency is security-sensitive because it executes in the same Python process, receives `SIMMER_API_KEY`, performs network operations, and submits trades. A compromised upstream release, package-index account compromise, or unsafe future update could change behavior without any modification to this repository. No evidence was found that the currently referenced package is malicious. The finding concerns the absence of dependency reproducibility and supply-chain controls. ### Attack Path 1. The upstream package publishing account or distribution channel is compromised, or a future release contains malicious behavior. 2. A user installs the Skill after that release is available. 3. The unconstrained requirement resolves to the affected package version. 4. `trader.py` imports and executes the package in-process. 5. The package receives `SIMMER_API_KEY` during `SimmerClient` construction. 6. Malicious dependency code could access the process environment, transmit the credential, alter market data, or submit unauthorized trades with the user's available authority. ### Impact Assessment A compromised dependency would execute with the same operating-system permissions as the Skill process. It could access environment variables and files available to that process and could abuse the trading authority represent ...[truncated 124 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Pin `simmer-sdk` to an exact version that has been reviewed and tested. - Use a lock file and require package hashes where the deployment system supports them. - Obtain packages only from the intended authenticated package index. - Re-audit dependency changes before updating the pin. - Use automated dependency monitoring and verify publisher provenance. - Restrict the runtime credential to the minimum necessary trading permissions and balance. - Run the Skill in an isolated environment with limited filesystem and network access. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (5)

Tp4

High
Category
MCP Tool Poisoning
Confidence
92% confidence
Finding
The skill is presented as a Twitter cadence modeling template, but the content also describes trading Truth Social markets and executing real Polymarket trades when `--live` is supplied. This mismatch can mislead users or orchestration systems about the skill's real operational scope, increasing the chance of enabling live financial actions or unsupported market coverage without informed consent.

Lp3

Medium
Category
MCP Least Privilege
Confidence
90% confidence
Finding
The skill declares access to an environment variable containing trading authority (`SIMMER_API_KEY`) but does not define any explicit tool scope such as `permissions` or `allowed-tools`. In an agent ecosystem, missing scope declarations weaken least-privilege controls and can let the skill access sensitive data or capabilities without clear operator visibility.

Description-Behavior Mismatch

Medium
Confidence
96% confidence
Finding
The manifest description limits the skill to pricing and trading Twitter post-count bins, but the implementation explicitly searches for and models both Twitter/X and Truth Social markets. This broadens the operational scope beyond the manifest's stated purpose in a way that is semantically significant, not just an implementation detail.

Intent-Code Divergence

Medium
Confidence
98% confidence
Finding
The global cached client in get_client() ignores subsequent live/paper mode changes after the first initialization. If the process first creates a live client, later calls to run(live=False) may still reuse the live client and place real-money trades despite the code and docstring claiming paper mode by default, which is dangerous in an automated trading skill.

Missing User Warnings

Low
Confidence
78% confidence
Finding
This manifest declares a required environment variable named `SIMMER_API_KEY`, which indicates the skill depends on sensitive credentials. In this file there is no accompanying user-facing warning or explanatory text about credential handling, which can leave users unaware that they must supply and trust the skill with an API key.

Static analysis

No suspicious patterns detected.