Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,19 @@
POLYGON_WALLET_PRIVATE_KEY=""

# LLM provider — set ONE of these:
OPENAI_API_KEY=""
OPENAI_MODEL="gpt-4-1106-preview"
# or
XAI_API_KEY=""
XAI_MODEL="grok-3-mini"

TAVILY_API_KEY=""
NEWSAPI_API_KEY=""
TELEGRAM_BOT_TOKEN=""
TELEGRAM_ALLOWED_USERS=""

# Auto-Trading Settings
AUTOTRADE_INTERVAL_MIN="30" # Minutes between trade cycles
AUTOTRADE_MAX_AMOUNT="25" # Max USDC per single trade
AUTOTRADE_MAX_TRADES="3" # Max trades per cycle
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Config defaults in .env.example don't match code fallbacks

Low Severity

.env.example documents AUTOTRADE_INTERVAL_MIN="30" and AUTOTRADE_MAX_TRADES="3", but the code fallback defaults are "10" and "5" respectively. A user who doesn't copy these values into their .env gets a 3x more frequent trading interval and 67% more trades per cycle than the documented behavior suggests, leading to more aggressive trading than expected in a financial application.

Additional Locations (1)

Fix in Cursor Fix in Web

AUTOTRADE_DRY_RUN="true" # Start in dry run mode (no real trades)
50 changes: 38 additions & 12 deletions agents/application/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
from agents.connectors.chroma import PolymarketRAG as Chroma
from agents.utils.objects import SimpleEvent, SimpleMarket
from agents.application.prompts import Prompter
from agents.polymarket.polymarket import Polymarket

def retain_keys(data, keys_to_retain):
if isinstance(data, dict):
Expand All @@ -28,20 +27,46 @@ def retain_keys(data, keys_to_retain):
else:
return data


def _build_executor_llm():
"""Build LLM for Executor. Supports xAI or OpenAI."""
load_dotenv()
xai_key = os.getenv("XAI_API_KEY")
openai_key = os.getenv("OPENAI_API_KEY")

if xai_key:
model = os.getenv("XAI_MODEL", "grok-3-mini")
return ChatOpenAI(
model=model,
temperature=0,
api_key=xai_key,
base_url="https://api.x.ai/v1",
), 95000
elif openai_key:
model = os.getenv("OPENAI_MODEL", "gpt-3.5-turbo-16k")
max_token_model = {'gpt-3.5-turbo-16k': 15000, 'gpt-4-1106-preview': 95000}
token_limit = max_token_model.get(model, 15000)
return ChatOpenAI(model=model, temperature=0), token_limit
else:
raise ValueError("No LLM API key. Set XAI_API_KEY or OPENAI_API_KEY in .env")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Executor missing Anthropic provider unlike other LLM builders

Medium Severity

_build_executor_llm only supports xAI and OpenAI, while _build_llm in both agent.py and auto_trader.py prioritizes Anthropic first. A user who sets only ANTHROPIC_API_KEY (following the documented priority) will get a ValueError from the Executor, causing it to silently fail during agent initialization.

Fix in Cursor Fix in Web



class Executor:
def __init__(self, default_model='gpt-3.5-turbo-16k') -> None:
def __init__(self) -> None:
load_dotenv()
max_token_model = {'gpt-3.5-turbo-16k':15000, 'gpt-4-1106-preview':95000}
self.token_limit = max_token_model.get(default_model)
self.llm, self.token_limit = _build_executor_llm()
self.prompter = Prompter()
self.openai_api_key = os.getenv("OPENAI_API_KEY")
self.llm = ChatOpenAI(
model=default_model, #gpt-3.5-turbo"
temperature=0,
)
self.gamma = Gamma()
self.chroma = Chroma()
self.polymarket = Polymarket()

# Polymarket CLOB — optional, only if wallet key is set
self.polymarket = None
if os.getenv("POLYGON_WALLET_PRIVATE_KEY"):
try:
from agents.polymarket.polymarket import Polymarket
self.polymarket = Polymarket()
except Exception:
pass

def get_llm_response(self, user_input: str) -> str:
system_message = SystemMessage(content=str(self.prompter.market_analyst()))
Expand Down Expand Up @@ -143,8 +168,9 @@ def map_filtered_events_to_markets(
market_ids = data["metadata"]["markets"].split(",")
for market_id in market_ids:
market_data = self.gamma.get_market(market_id)
formatted_market_data = self.polymarket.map_api_to_market(market_data)
markets.append(formatted_market_data)
if self.polymarket:
formatted_market_data = self.polymarket.map_api_to_market(market_data)
markets.append(formatted_market_data)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unguarded self.polymarket access causes NoneType crash

High Severity

format_trade_prompt_for_execution calls self.polymarket.get_usdc_balance() without a None check. The commit changed self.polymarket from always-initialized to conditionally None (when POLYGON_WALLET_PRIVATE_KEY is unset), but this method wasn't updated. When called via trade.py's one_best_trade, it will crash with an AttributeError on NoneType.

Fix in Cursor Fix in Web

return markets

def filter_markets(self, markets) -> "list[tuple]":
Expand Down
42 changes: 39 additions & 3 deletions agents/polymarket/gamma.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import httpx
import json

from agents.polymarket.polymarket import Polymarket
from agents.utils.objects import Market, PolymarketEvent, ClobReward, Tag


Expand Down Expand Up @@ -76,7 +75,7 @@ def get_markets(
'Cannot use "parse_pydantic" and "local_file" params simultaneously.'
)

response = httpx.get(self.gamma_markets_endpoint, params=querystring_params)
response = httpx.get(self.gamma_markets_endpoint, params=querystring_params, timeout=10)
if response.status_code == 200:
data = response.json()
if local_file_path is not None:
Expand Down Expand Up @@ -174,6 +173,42 @@ def get_clob_tradable_markets(self, limit=2) -> "list[Market]":
}
)

def search_markets(self, query: str, limit: int = 20) -> list:
"""Search active markets by keyword in question/description.

Fetches top 500 markets by volume and filters locally,
because the Gamma API text_query parameter doesn't work.
"""
response = httpx.get(self.gamma_markets_endpoint, params={
"active": "true",
"closed": "false",
"limit": 500,
"order": "volume",
"ascending": "false",
}, timeout=30)

if response.status_code != 200:
return []

all_markets = response.json()
keywords = [kw.strip().lower() for kw in query.split() if len(kw.strip()) >= 2]

if not keywords:
return all_markets[:limit]

scored = []
for m in all_markets:
question = m.get("question", "").lower()
description = (m.get("description", "") or "").lower()
text = question + " " + description

score = sum(1 for kw in keywords if kw in text)
if score > 0:
scored.append((score, m))

scored.sort(key=lambda x: (x[0], float(x[1].get("volume", 0) or 0)), reverse=True)
return [m for _, m in scored[:limit]]

def get_market(self, market_id: int) -> dict():
url = self.gamma_markets_endpoint + "/" + str(market_id)
print(url)
Expand All @@ -184,5 +219,6 @@ def get_market(self, market_id: int) -> dict():
if __name__ == "__main__":
gamma = GammaMarketClient()
market = gamma.get_market("253123")
from agents.polymarket.polymarket import Polymarket
poly = Polymarket()
object = poly.map_api_to_market(market)
obj = poly.map_api_to_market(market)
127 changes: 115 additions & 12 deletions agents/polymarket/polymarket.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@

from web3 import Web3
from web3.constants import MAX_INT
from web3.middleware import geth_poa_middleware

# web3 v7 renamed the middleware
try:
from web3.middleware import ExtraDataToPOAMiddleware as poa_middleware
except ImportError:
from web3.middleware import geth_poa_middleware as poa_middleware

import httpx
from py_clob_client.client import ClobClient
Expand Down Expand Up @@ -44,7 +49,7 @@ def __init__(self) -> None:

self.chain_id = 137 # POLYGON
self.private_key = os.getenv("POLYGON_WALLET_PRIVATE_KEY")
self.polygon_rpc = "https://polygon-rpc.com"
self.polygon_rpc = "https://polygon-bor-rpc.publicnode.com"
self.w3 = Web3(Web3.HTTPProvider(self.polygon_rpc))

self.exchange_address = "0x4bfb41d5b3570defd03c39a9a4d8de6bd8b8982e"
Expand All @@ -57,7 +62,8 @@ def __init__(self) -> None:
self.ctf_address = "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045"

self.web3 = Web3(Web3.HTTPProvider(self.polygon_rpc))
self.web3.middleware_onion.inject(geth_poa_middleware, layer=0)
if poa_middleware:
self.web3.middleware_onion.inject(poa_middleware, layer=0)

self.usdc = self.web3.eth.contract(
address=self.usdc_address, abi=self.erc20_approve
Expand All @@ -70,12 +76,21 @@ def __init__(self) -> None:
self._init_approvals(False)

def _init_api_keys(self) -> None:
# Check if using a proxy wallet (funder)
proxy_address = os.getenv("POLYMARKET_WALLET_ADDRESS", "")
funder = proxy_address if proxy_address else None
# signature_type=1 (POLY_PROXY) when using proxy wallet, 0 (EOA) otherwise
sig_type = 1 if funder else 0

self.client = ClobClient(
self.clob_url, key=self.private_key, chain_id=self.chain_id
self.clob_url,
key=self.private_key,
chain_id=self.chain_id,
funder=funder,
signature_type=sig_type,
)
self.credentials = self.client.create_or_derive_api_creds()
self.client.set_api_creds(self.credentials)
# print(self.credentials)

def _init_approvals(self, run: bool = False) -> None:
if not run:
Expand Down Expand Up @@ -334,28 +349,116 @@ def build_order(
return order

def execute_order(self, price, size, side, token_id) -> str:
"""Place a GTC limit order."""
return self.client.create_and_post_order(
OrderArgs(price=price, size=size, side=side, token_id=token_id)
)

def execute_market_order(self, market, amount) -> str:
token_id = ast.literal_eval(market[0].dict()["metadata"]["clob_token_ids"])[1]
def execute_market_buy(self, token_id: str, amount: float) -> str:
"""Execute a FOK (Fill or Kill) market order - fills immediately or cancels.

Args:
token_id: The CLOB token ID
amount: Amount in USDC to spend
"""
order_args = MarketOrderArgs(
token_id=token_id,
amount=amount,
)
signed_order = self.client.create_market_order(order_args)
print("Execute market order... signed_order ", signed_order)
resp = self.client.post_order(signed_order, orderType=OrderType.FOK)
print(resp)
print("Done!")
return resp

def execute_aggressive_order(self, token_id: str, side: str, amount: float) -> str:
"""Place an aggressive limit order that crosses the spread for fast fill.

Reads the orderbook and places the order at the best available price
to maximize chance of immediate fill.

Args:
token_id: The CLOB token ID
side: 'BUY' or 'SELL'
amount: Amount in USDC to spend (for BUY) or shares to sell (for SELL)
"""
ob = self.get_orderbook(token_id)

if side.upper() == "BUY":
# Buy at the best ask price (cross the spread)
if ob.asks and len(ob.asks) > 0:
best_ask = float(ob.asks[0].price)
# Use the ask price to guarantee fill
price = best_ask
else:
# No asks, use 0.99 as max
price = 0.99

size = round(amount / price, 2)
if size < 5:
size = 5
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minimum order size bypasses balance-based safety caps

Medium Severity

execute_aggressive_order enforces a minimum of 5 shares (if size < 5: size = 5), which can silently cause the actual USDC spent to far exceed the amount the auto-trader carefully computed via _adjust_amount_for_balance. For instance, with a $5 balance capped to a $2 bet on a $0.95 outcome, the size (2.1 shares) gets forced to 5 shares, spending $4.75 — more than double the intended cap and 95% of the balance.

Additional Locations (1)

Fix in Cursor Fix in Web

else:
# Sell at the best bid price (cross the spread)
if ob.bids and len(ob.bids) > 0:
best_bid = float(ob.bids[0].price)
price = best_bid
else:
price = 0.01

size = amount # For sells, amount IS the number of shares

return self.client.create_and_post_order(
OrderArgs(price=price, size=size, side=side.upper(), token_id=token_id)
)

def get_usdc_balance(self) -> float:
"""Get USDC balance. Checks proxy wallet first, then EOA."""
proxy_address = os.getenv("POLYMARKET_WALLET_ADDRESS", "")
if proxy_address:
try:
proxy_checksum = Web3.to_checksum_address(proxy_address)
balance_res = self.usdc.functions.balanceOf(proxy_checksum).call()
return float(balance_res / 1e6)
except Exception:
pass
# Fallback to EOA
balance_res = self.usdc.functions.balanceOf(
self.get_address_for_private_key()
).call()
return float(balance_res / 10e5)
return float(balance_res / 1e6)

def get_positions(self, limit: int = 100, sort_by: str = "CURRENT") -> list:
"""Fetch open positions from the Polymarket Data API."""
address = self.get_address_for_private_key()
params = {
"user": address,
"sizeThreshold": 0,
"limit": limit,
"sortBy": sort_by,
"sortDirection": "DESC",
}
res = httpx.get("https://data-api.polymarket.com/positions", params=params)
if res.status_code == 200:
return res.json()
return []

def get_open_orders(self, market: str = None, asset_id: str = None) -> list:
"""Fetch open orders from the CLOB."""
params = {}
if market:
params["market"] = market
if asset_id:
params["asset_id"] = asset_id
try:
return self.client.get_orders(**params) if params else self.client.get_orders()
except Exception:
return []

def cancel_order(self, order_id: str) -> dict:
"""Cancel a single order."""
return self.client.cancel(order_id)

def cancel_all_orders(self) -> dict:
"""Cancel all open orders."""
return self.client.cancel_all()


def test():
Expand Down Expand Up @@ -457,7 +560,7 @@ def main():

"""

# https://polygon-rpc.com
# https://polygon-bor-rpc.publicnode.com

test_market_token_id = (
"101669189743438912873361127612589311253202068943959811456820079057046819967115"
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ pysha3==1.0.2
pytest==8.3.2
python-dateutil==2.9.0.post0
python-dotenv==1.0.1
python-telegram-bot==21.3
python-multipart==0.0.9
pyunormalize==15.1.0
PyYAML==6.0.1
Expand Down
Loading