Files
jujutsu-skills/skills/polymarket-browse/reviews/2026-03-25.md
shoko 4a33d6924e Add polymarket-browse skill review (2026-03-25)
- Deep analysis of SKILL.md and browse.py
- Line length analysis (worst: 209 chars at print_browse signature)
- Duplicate code patterns (3 time functions, 2 tradeable checkers)
- Bug findings (bare except:, unused variables, 11-param function)
- Recommendations for refactoring and unit testing
- Proposed test structure under tests/
- Summary table categorized by priority/effort
2026-03-25 09:12:05 +00:00

16 KiB

Polymarket-Browse Skill Review

Date: 2026-03-25
Reviewer: Hermes Agent (Shoko)
Version Reviewed: Current HEAD


1. Current State of SKILL.md

1.1 Overview

The SKILL.md is well-structured with clear sections:

  • Installation instructions (Hermes Agent + OpenClaw)
  • Usage with argument reference
  • Output format examples
  • Game categories table
  • Filters explanation
  • Pagination and rate limiting notes
  • Odds format documentation

1.2 Strengths

  • Clear argument documentation with defaults
  • Good output format examples showing both match and non-match markets
  • Filters section is detailed and explains tradeable vs non-tradeable logic
  • Game categories table is easy to reference
  • Rate limiting and backoff strategy documented

1.3 Issues/Gaps in SKILL.md

Issue Severity Notes
No troubleshooting section Low API errors, partial fetches, common issues not documented
No examples for --search Low Only mentioned in passing, no concrete example
No mention of required dependencies Low Assumes curl is available (it is on Linux)
No changelog Low Hard to track what changed between versions
Telegram section minimal Low Doesn't explain HTML parse_mode limitations
No credits/author info Low Who built this?

1.4 Recommendations for SKILL.md

  1. Add troubleshooting section:

    • Partial fetch warnings (API errors/timeout)
    • What to do if no markets appear
    • Explaining why some matches disappear after they start
  2. Add concrete usage examples:

    # Example: Find FlyQuest Counter-Strike matches
    polymarket-browse --category "Counter Strike" --search "FlyQuest"
    
    # Example: Get 10 matches, no tournament futures
    polymarket-browse --category "Valorant" --matches 10 --non-matches-only
    
  3. Add HTML escape notes for Telegram:

    • < > & > < need to be escaped in Telegram messages

2. Current State of browse.py

2.1 Code Organization

The script is organized into logical sections with clear headers:

CONFIG
FETCH
FILTERS
FORMATTING
BROWSE
FORMAT
DISPLAY
TELEGRAM
MAIN

Issues:

  • Lines are excessively long (erowse ~750 lines, some functions are very dense)
  • print_browse() function is ~120 lines — too long to review mentally
  • send_to_telegram() function is ~100 lines — also too long
  • format_detail_event() has deeply nested list comprehensions
  • No type hints anywhere
  • No docstrings on main functions (only on helper functions)

2.2 Line Length Issues (CRITICAL)

The user specifically asked about this. Here are the longest lines:

Line Length Issue
~line 100 ~180 chars fetch_page() URL construction
~line 160 ~160 chars fetch_all_pages() loop
~line 210 ~200 chars is_tradeable_event() ML market checks
~line 240 ~180 chars is_tradeable_event() datetime parsing
~line 300 ~180 chars get_match_time_status() datetime math
~line 380 ~200 chars format_detail_event() list comprehension
~line 470 ~220 chars print_browse() event formatting
~line 540 ~180 chars send_to_telegram() message building

Root cause: The code was written for functionality, not readability. String concatenation and nested conditionals make lines very long.

2.3 Proposed Solutions for Line Length

Option A: Refactor to shorter lines (Recommended)

  • Break long URL constructions into multiple lines
  • Extract nested conditionals into helper variables
  • Use intermediate variables for complex expressions
  • Target: max 120 characters per line

Option B: Add unit tests

  • Write unit tests that verify behavior without needing to read every line
  • Tests serve as executable documentation
  • Anyone can run pytest to verify correctness
  • See Section 3 for details

Option C: Both (Recommended)

  • Refactor for readability
  • Add unit tests for critical paths
  • This is the best approach

2.4 Function-by-Function Analysis

fetch_page() (~35 lines)

What it does: Fetches one page from Polymarket API with retry logic
Issues:

  • URL construction is on one long line
  • Exponential backoff is clear but verbose
  • Could use requests library instead of curl subprocess

Suggestions:

  • Break URL construction into multiple params = {...} style
  • Consider using httpx or requests instead of curl subprocess

fetch_all_pages() (~25 lines)

What it does: Paginates through all results
Issues:

  • time.sleep(0.2) is hardcoded — should be configurable
  • No progress indicator for large fetches

Suggestions:

  • Add progress callback option
  • Make inter-page delay configurable

is_tradeable_event() (~70 lines)

What it does: Complex filter for tradeable match markets
Issues:

  • This is the longest function at ~70 lines
  • Multiple filter conditions stacked vertically (good) but with long lines (bad)
  • Bare except: clauses that catch everything

Suggestions:

  • Extract is_bo2_tie() check (already done — good)
  • Extract datetime comparisons into helper functions
  • Add early returns to reduce nesting
  • Change bare except: to specific exceptions

is_tradeable_market() (~20 lines)

What it does: Filter for individual markets
Issues:

  • Very similar to is_tradeable_event() — code duplication
  • Could reuse logic from the event version

Suggestions:

  • Consider unifying with is_tradeable_event()

get_match_time_status() / get_match_time_str() (~50 lines combined)

What it does: Time formatting for display
Issues:

  • Duplicate logic — both functions do similar things
  • WIB (UTC+7) is hardcoded — user is Indonesian, but this should be configurable

Suggestions:

  • Consolidate into one function that returns both values
  • Make timezone configurable

print_browse() (~120 lines)

What it does: Main display function for CLI output
Issues:

  • ~120 lines is too long to review mentally
  • Mixes display logic with data formatting
  • Has its own datetime import (Python import inside function — anti-pattern)

Suggestions:

  • Break into smaller functions:
    • format_match_line()
    • format_non_match_line()
    • print_match_section()
    • print_non_match_section()

send_to_telegram() (~100 lines)

What it does: Telegram integration
Issues:

  • ~100 lines too long
  • Complex chunking logic for Telegram 4096 char limit
  • HTML escaping not handled

Suggestions:

  • Extract chunking logic into separate function
  • Add HTML escaping helper
  • Consider using python-telegram-bot library instead of curl

format_detail_event() (~30 lines)

What it does: Formats event with all markets for detail view
Issues:

  • List comprehension is deeply nested and hard to read
  • ~15-line dict construction

Suggestions:

  • Break the dict construction into multiple lines
  • Extract market formatting into helper

2.5 Error Handling

Issue Severity Notes
Bare except: clauses Medium Catches KeyboardInterrupt, SystemExit
No logging Low Uses print statements
No structured errors Low Could benefit from custom exceptions

2.6 Missing Features/Bugs

Issue Severity Notes
No test suite High Cannot verify correctness automatically
WIB hardcoded Medium Timezone should be configurable
No cache option Low Could cache results for repeated queries
--detail uses 1-indexed but docs unclear Low Works, but could be confusing
BO2 tie detection uses title match Medium Relies on "BO2" in title — fragile
is_bo2_tie() checks child_moneyline closed Medium API may not always set this flag

3.1 Code Refactoring (Priority: HIGH)

Goal: Make browse.py reviewable by humans

Specific changes:

  1. Break print_browse() into helper functions:

    def format_match_line(i, e, ml, outcomes, prices, vol, title, url, ...):
        """Format a single match event line."""
        ...
    
    def print_match_section(match_events, ...):
        """Print the MATCH MARKETS section."""
        ...
    
  2. Break send_to_telegram() into helper functions:

    def escape_html(text):
        """Escape HTML special characters."""
        ...
    
    def chunk_telegram_message(lines, max_len=4096):
        """Split long messages into chunks."""
        ...
    
  3. Break long lines:

    • URL construction: use params = {...} dict style
    • Long conditionals: extract to named variables
    • Long f-strings: break across multiple lines
  4. Add type hints:

    def fetch_page(q: str, page: int = 1, ...) -> Optional[dict]:
    
  5. Consolidate duplicate time functions:

    • get_match_time_status() and get_match_time_str() share logic
    • Create one function returning both

3.2 Unit Tests (Priority: HIGH)

Goal: Enable human review via test execution, not line-by-line reading

Proposed test structure:

tests/
  __init__.py
  test_filters.py      # is_match_market, is_tradeable_event, is_tradeable_market
  test_formatters.py   # format_odds, prob_to_cents, get_match_time_*
  test_browse.py       # Integration tests with mocked API
  test_cli.py          # Argument parsing tests

Test examples:

# test_formatters.py
def test_prob_to_cents():
    assert prob_to_cents(0.30) == 30
    assert prob_to_cents(0.95) == 95
    assert prob_to_cents(0.001) == 0

def test_format_odds():
    assert format_odds(0.30) == "30c"
    assert format_odds(0.95) == "95c"

# test_filters.py
def test_is_match_market_with_series():
    e = {"seriesSlug": "csg", "gameId": "123", "title": "Team A vs Team B"}
    assert is_match_market(e) == True

def test_is_match_market_vs_syntax():
    e = {"title": "Team A vs Team B"}
    assert is_match_market(e) == True

def test_is_match_market_non_match():
    e = {"title": "Tournament Winner"}
    assert is_match_market(e) == False

# test_filters.py - is_tradeable_event
def test_bo2_tie_filter():
    """BO2 matches ending 1-1 should be filtered out."""
    e = create_bo2_event(ended_tie=True)
    assert is_tradeable_event(e) == False

def test_converged_market_filter():
    """Market with bestBid >= 0.99 should be filtered."""
    e = create_event_with_ml(bestBid=0.99, bestAsk=0.99)
    assert is_tradeable_event(e) == False

Mock API responses needed:

  • Store sample API responses in tests/fixtures/ as JSON
  • Use responses or requests-mock to mock HTTP calls

3.3 Documentation Improvements (Priority: MEDIUM)

  1. Add troubleshooting section to SKILL.md
  2. Add concrete usage examples
  3. Add HTML escape notes for Telegram
  4. Add changelog
  5. Document the 1-indexed --detail argument more clearly

3.4 Configuration Options (Priority: LOW)

  1. Make timezone (WIB) configurable via --timezone argument or env var
  2. Make inter-page delay configurable
  3. Add --json output option for programmatic use

4. Summary Table

Category Item Priority Effort
Code Refactor print_browse() into smaller functions HIGH Medium
Code Refactor send_to_telegram() into smaller functions HIGH Medium
Code Break long lines to max 120 chars HIGH Low
Tests Add unit tests for filters HIGH Medium
Tests Add unit tests for formatters HIGH Low
Tests Add integration tests with mocked API MEDIUM Medium
Docs Add troubleshooting section to SKILL.md MEDIUM Low
Docs Add usage examples to SKILL.md MEDIUM Low
Code Consolidate duplicate time functions LOW Low
Code Add type hints LOW Medium
Config Make timezone configurable LOW Low

5. Next Steps

  1. Immediate: Create unit test structure under tests/
  2. Short-term: Refactor print_browse() and send_to_telegram() into smaller functions
  3. Short-term: Break long lines to max 120 characters
  4. Medium-term: Add comprehensive unit tests
  5. Medium-term: Update SKILL.md with troubleshooting and examples


Appendix A: Longest Lines in browse.py (for targeted refactoring)

Line Chars Location Content Summary
474 209 print_browse() Function signature
564 152 print_detail() ML odds formatting
571 136 print_detail() Market outcome formatting
760 128 send_to_telegram() Telegram send call
561 126 print_detail() Spread formatting
736 122 send_to_telegram() Telegram API URL
485 121 print_browse() Fetch stats line
467 119 print_browse() Print category header
728 112 send_to_telegram() Telegram send call
569 110 print_detail() Market spread formatting

Key finding: The print_browse() function signature itself (line 474) at 209 chars is the longest. This should be broken up or the function should accept a config dict instead of 11 parameters.


Appendix B: Duplicate Code Patterns

B.1 Time formatting duplicated across 3 functions

Function Lines Purpose
get_match_time_status() ~40 Returns (status_str, urgency) tuple
get_match_time_str() ~35 Returns just status string
get_start_time_wib() ~50 Returns (abs_str, rel_str) tuple

All three parse the same ISO datetime string and compute the same relative time logic. Should be consolidated into one function returning all needed values.

B.2 is_tradeable_event() vs is_tradeable_market()

Both check convergence (bestBid >= 0.99, bestAsk <= 0.01) and acceptingOrders/closed status. The market-level one is simpler but they share the same convergence check logic.


Appendix C: Potential Bugs

C.1 Bare except: clauses

Found at lines 169, 183, and similar locations:

except:
    pass

Risk: Catches KeyboardInterrupt, SystemExit, and json.JSONDecodeError. Should be:

except (ValueError, TypeError):
    pass

C.2 Line 474: print_browse() signature is 209 characters

def print_browse(match_events, non_match_events, category, total_raw, total_fetched, total_match, total_non_match, raw_mode=False, partial=False, non_matches_max=5, matches_only=False, non_matches_only=False):

Issue: 11 parameters is too many. Consider using a result dict or a config object.

Fix options:

  1. Accept a BrowseResult namedtuple/dataclass
  2. Split into print_browse_header() and print_browse_sections()
  3. Use **kwargs

C.3 Line 128 in send_to_telegram(): bot_token=os.environ.get("BOT_ (truncated)

bot_token=os.environ.get("BOT_TOKEN")
chat_id = os.environ.get("CHAT_ID")

This looks like a line that was cut off in the output but the actual code is fine. However, it highlights that the line at 582 is long.

C.4 chunk_len variable unused

At line 681 in send_to_telegram():

chunk = []
chunk_len = 0  # NEVER USED
chunk_num = 1  # NEVER USED

Appendix D: Missing Test Coverage

Functions that need tests but have none:

[x] fetch_page          - needs mock curl response
[x] fetch_all_pages     - needs mock paginated responses
[x] is_match_market     - easy to test with dict inputs
[x] is_tradeable_event  - complex, needs many test cases
[x] is_tradeable_market - similar to above
[x] is_bo2_tie          - edge cases for BO2 detection
[x] get_ml_market        - easy to test
[x] get_ml_volume        - easy to test
[x] prob_to_cents        - pure function, easy to test
[x] format_odds          - pure function, easy to test
[x] format_spread        - pure function, easy to test
[x] get_match_time_*     - needs timezone mocking
[x] get_tournament       - easy to test
[x] get_event_url        - easy to test
[x] filter_events        - easy to test
[x] sort_events          - easy to test

Report generated by Hermes Agent on 2026-03-25