Skill: svenmedina07-ship-it/sports-acca-tracker (version 1.3.1)

--- name: acca-tracker description: Track accumulator (acca) betting slips across football, basketball, and tennis — parses slip photo or text, checks live scores every 15 minutes, and reports bet status with overall acca health and cash-out context. version: 1.3.1 author: Ruby (Hermes Agent) tags: [betting, football, basketball, tennis, accumulator, live-scores, sports, gambling, parlay, bet-tracking] license: MIT metadata: hermes: tags: [leisure, sports, betting] requires_toolsets: [web, terminal] --- # Acca Tracker — Multi-Sport Accumulator Monitor Track accumulator (parlay) bets across **football, basketball, and tennis** by monitoring live scores and reporting whether each leg is still alive. Works with any betting slip — photo, screenshot, or typed text. ## When to Use - User shares a photo/screenshot of a betting slip (accumulator/parlay/bet builder) - User wants live score updates for their bets - User says "track my acca", "monitor my bet", "is my bet alive", "check my slip" - Slips can mix sports (e.g., 2 football + 1 basketball + 1 tennis) ## How It Works ``` User sends slip photo → Agent parses legs (vision_analyze or text extraction) → Agent creates a cron job (*/15 * * * *) → Cron searches live scores every 15 min → Cron delivers report: per-leg status + acca health → Auto-cleans up after all matches finish ``` ## Step 1: Parse the Slip ### From a Photo Use `vision_analyze` with the image and this question: ``` Extract all betting legs from this slip. For each leg return: - Match (Team A vs Team B) or Event (Player A vs Player B for tennis) - Sport (football/soccer, basketball, tennis) - Competition/League - Date and kickoff time - Bet type (exact wording from slip) - Decimal odds Also extract: total odds, stake amount, max return, any bonus info. ``` ### Sport Detection Auto-detect sport per leg based on: - **Tennis:** single names/players, WTA/ATP/Grand Slam, "set" in bet type, no team names - **Basketball:** NBA, EuroLeague, BBL, "quarter", "points", "rebounds" - **Football/Soccer:** everything else (default) Tag each leg with its sport — the cron prompt uses sport-specific APIs. ### From Text If user types their slip, extract the same fields. Normalize bet type wording to match the standard types in `references/bet-types.md`. ## Step 2: Build the Slip Summary After parsing, confirm with the user before starting tracking: ``` 📋 PARSED SLIP — 5 legs 1. Arsenal vs PSG (⚽ UCL, 21:00) — Arsenal W (1.55) — 64.5% 2. Bayern vs Inter (⚽ UCL, 21:00) — Bayern W (1.40) — 71.4% 3. Lakers vs Celtics (🏀 NBA, 02:30) — Lakers W (1.80) — 55.6% 4. Djokovic vs Alcaraz (🎾 ATP, 14:00) — Djokovic W (2.10) — 47.6% 5. FSV Schöningen vs Lohne (⚽ RL, 18:00) — BTTS No (2.65) — 37.7% ⚠️ Stake: €10 | Combined odds: 30.12 | Max return: €301.20 Riskiest leg: #5 (37.7% implied) Start tracking? (yes/no) ``` Calculate implied probability per leg: `1 / odds * 100`. Flag any under 40% as ⚠️ gamble leg. ## Step 3: Create the Cron Job Use the `cronjob` tool: ``` action: create name: acca-tracker-{short-id} schedule: "*/15 * * * *" repeat: 48 deliver: origin prompt: {see template below} ``` ### Schedule Adjustments | Scenario | Schedule | Repeat | |----------|----------|--------| | Single match day | `*/15 * * * *` | 48 | | High-stakes acca | `*/10 * * * *` | 72 | | Multi-day (Tue + Wed) | `*/15 * * * *` | 192 | | Tournament bracket | `0 */2 * * *` | 84 | ### Cron Prompt Template ``` You are tracking a MULTI-SPORT accumulator bet. Check live scores NOW and report. SLIP DETAILS: {paste all legs with sport tags, bet types, odds, and win conditions} Total odds: {total} | Stake: {stake} | Max return: {max_return} INSTRUCTIONS: 1. For each leg, determine scores using the scores.sh helper script: - Run in terminal: bash scripts/scores.sh {DATE} Soccer - Run in terminal: bash scripts/scores.sh {DATE} Basketball - Run in terminal: bash scripts/scores.sh {DATE} Tennis - Output: league|home|away|home_score|away_score|status (or league|event|result|status for tennis) - Match results to legs by team/player names - If script returns NO_DATA, fall back to web_search as last resort 2. For each leg determine: - Current score - Match status: Not Started / Live (minute/set) / HT / FT / Postponed / Abandoned - Sport-specific score format (football: goals, basketball: points, tennis: sets+games) - Bet status: ✅ WON / ✅ WINNING / ⏳ PENDING / ❌ LOST / ❌ DEAD 3. Calculate overall acca status: - ALL SAFE ✅ = no legs lost - STILL ALIVE ⏳ = no legs lost, none confirmed won - ACCA DEAD ❌ = at least one leg LOST 4. If acca dead: name the killing leg, explain why 5. Calculate "legs alive / won / lost / pending / total" 6. If all FT: state "TRACKING COMPLETE" and summarize SPORT-SPECIFIC BET EVALUATION: - Football: standard 90min + stoppage (see references/bet-types.md) - Basketball: Match Winner = team with more points at FT. Overtime counts. Over/Under = total combined points of both teams. - Tennis: Match Winner = player who wins required sets (2/3 or 3/5). Set betting = exact set score. Games over/Under = total games in match. REPORT FORMAT (code blocks only): 🏟️ ACCA LIVE REPORT — {time} Leg | Match | Score | Status | Bet | Result ----|--------------------------------|--------------|--------|------------|---------- 1 | ⚽ Arsenal vs PSG | 2 - 0 (67') | 67' | Arsenal W | ✅ WINNING 2 | 🏀 Lakers vs Celtics | 98 - 102 | Q4 | Lakers W | ❌ DEAD 3 | 🎾 Djokovic vs Alcaraz | 6-4, 3-2 | Set 2 | Djokovic W | ✅ WINNING 📊 1 winning / 0 won / 1 dead / 1 pending (3 total) ❌ ACCA DEAD — Leg 2 killed it (Lakers trailing 98-102 Q4) DATA: If no score found, say so explicitly. Never guess scores. If 2+ hours past kickoff with no data: "unverified — likely finished" ``` ## Step 4: Report States ### All Alive ``` 🏟️ ACCA LIVE — 21:34 CET 1 | Arsenal vs PSG | 1 - 0 | 55' | Arsenal W | ✅ WINNING 2 | Bayern vs Inter | 0 - 0 | 55' | Bayern W | ⏳ PENDING 3 | Luton vs Northampton | 0 - 0 | 55' | BTTS No | ✅ WINNING 📊 1 winning / 0 won / 0 lost / 2 pending 💰 ⏳ STILL ALIVE ``` ### Acca Dead ``` 🏟️ ACCA LIVE — 22:18 CET 1 | Arsenal vs PSG | 2 - 1 | 78' | Arsenal W | ✅ WINNING 2 | Bayern vs Inter | 1 - 2 | 78' | Bayern W | ❌ DEAD 3 | Luton vs Northampton | 0 - 0 | 78' | BTTS No | ✅ WINNING 📊 1 winning / 0 won / 1 dead / 1 pending ❌ ACCA DEAD — Leg 2 killed it (Bayern losing 1-2) Remaining matches still playing — updating for interest. ``` ### Final — Won ``` 🏟️ ACCA FINAL — 23:45 CET — 🎉🎉🎉 1 | Arsenal vs PSG | 2 - 1 | FT | Arsenal W | ✅ WON 2 | Bayern vs Inter | 3 - 1 | FT | Bayern W | ✅ WON 3 | Luton vs Northampton | 0 - 0 | FT | BTTS No | ✅ WON 📊 3/3 LEGS WON 💰 ACCA WON — Stake: €10 | Return: €210.40 Tracking complete. ``` ## Cash-Out Context When some legs won and others pending, estimate cash-out value: ``` Won legs combined odds: 1.55 × 1.40 = 2.17 Remaining legs implied probability: 0.565 × 0.541 × 0.377 = 0.115 Estimated value: €10 × 2.17 × 0.115 = €2.49 Cash-out range: ~€2.00 – €3.50 (rough estimate) ``` Only include when: at least 1 leg WON, at least 1 PENDING, acca NOT dead. Always frame as "estimated". ## Data Source Strategy See `references/data-sources.md` for full API details. Quick reference: - **PRIMARY:** `scripts/scores.sh {date} {sport}` — handles all transport, all sports - **FALLBACK:** `execute_code` with Python/urllib (same API, different transport) - **WEB SEARCH:** last resort when script returns NO_DATA - **TENNIS NOTE:** Player names in `strEvent` (not strHomeTeam/strAwayTeam), coverage tournament-dependent ## Bet Types See `references/bet-types.md` for the full list of 18+ bet types with scoring logic, decision tree, and edge cases (void, ET, red cards, handicaps). ## Step 5: Cleanup - After all matches FT, deliver final summary then stop - Auto-ends after `repeat` count - User can say "stop tracking" to remove cron early - Multi-day accas: increase repeat to 192 ## Limitations - **15-min intervals** — goals/points missed between checks, not real-time - **Low-tier football leagues** — limited coverage, may only get HT/FT - **JS-rendered sites** — Flashscore/Sofascore can't be scraped directly - **Tennis player names** — TheSportsDB stores in `strEvent`, parsing required - **Bookmaker-specific rules** — some bet types vary by bookmaker (e.g., retirement handling) - **Telegram cron delivery** — if reports fail to arrive, check `last_delivery_error`. Try explicit `telegram:chat_id:thread_id` format. Reports still generate at `~/.hermes/cron/output/{job_id}/.md` ## Tips - Always confirm slip with user before starting - Include team nicknames in search queries - When acca dies, keep tracking for interest - Flag "near misses" (hit post, disallowed goal) if mentioned in results - Name each cron differently for multi-slip support - Tennis coverage on TheSportsDB is tournament-dependent — may return zero events between tournaments. Always have web_search as fallback. - Basketball events are rich on TheSportsDB — covers NBA, EuroLeague, BBL, LKL, LNB, Japanese B1, etc. ESPN is backup only. ## ClawHub Publishing - Current slug: `sports-acca-tracker` (old `football-acca-tracker` also exists, no delete cmd) - Publish command: `clawhub publish <path> --slug <slug> --name "Name" --version X.Y.Z --tags "tag1,tag2"` - After publish, skill is hidden until security scan completes (usually minutes) - Verify with: `clawhub inspect <slug>` - ClawHub has no unpublish/delete — slug collisions mean choosing a new slug, not overwriting

path: openclaw.plugin.json
content: {
  "name": "acca-tracker",
  "version": "1.0.0",
  "description": "Track football accumulator betting slips via live score monitoring",
  "type": "skill",
  "entry": "SKILL.md",
  "files": [
    "SKILL.md",
    "references/bet-types.md",
    "references/data-sources.md"
  ]
}


path: package.json
content: {
  "name": "acca-tracker",
  "displayName": "Acca Tracker",
  "version": "1.0.0",
  "description": "Track football accumulator betting slips — parses slip photo or text, checks live scores every 15 minutes, and reports bet status for each leg with overall acca health.",
  "keywords": ["betting", "football", "accumulator", "live-scores", "sports", "parlay"],
  "license": "MIT",
  "author": "Banozz",
  "repository": "svenmedina07-ship-it/skills",
  "openclawVersion": ">=2026.1.0",
  "pluginSdkVersion": ">=2026.1.0"
}


path: references/bet-types.md
content: # Bet Type Reference

All recognized bet types with win conditions, scoring logic, and edge cases.
Covers **football, basketball, and tennis**.

## Football Bet Types

```
BET TYPE                        | WIN CONDITION
────────────────────────────────|────────────────────────────────────────
Match Betting - [Team]          | That team must win (90 min + stoppage)
Match Betting - Draw            | Match must end in a draw
Both Teams To Score - No        | At least one team fails to score
Both Teams To Score - Yes       | Both teams must score (1-1 or higher)
Over/Under [X].5 Goals          | Total goals strictly over or under X.5
Over/Under [X].5 Goals - Team   | That team's goals over/under X.5
Over/Under [X] Goals            | Total goals over or under X (exact = push)
Double Chance - [Team/Draw]     | Team wins OR draws
Draw No Bet - [Team]            | Team wins (draw = void, stake returned)
Handicap [Team] +/-[X]          | Adjusted score: team's goals +/- handicap
Half-Time/Full-Time             | Correct at HT AND FT
First Goalscorer                | Named player scores first
Anytime Goalscorer              | Named player scores at any point
Correct Score                   | Exact final score required
Win To Nil - [Team]             | Team wins AND opponent scores 0
Clean Sheet - [Team]            | Team concedes 0 goals
Total Corners Over/Under        | Match corner count over/under line
Total Cards Over/Under          | Match card count over/under line
Team To Qualify                 | Team advances (aggregate, extra time counts)
To Lift The Trophy              | Team wins final (includes penalties if needed)
```

## Basketball Bet Types

```
BET TYPE                        | WIN CONDITION
────────────────────────────────|────────────────────────────────────────
Money Line - [Team]             | Team wins (OT counts)
Spread - [Team] +/-[X]          | Team score +/- spread covers opponent
Over/Under [X] Points           | Combined total points over/under line
Over/Under [X] Points - [Team]  | That team's points over/under line
First Half Winner               | Leading team at half time
First Quarter Winner            | Leading team at end of Q1
Team Total Over/Under           | Single team's point total
Winning Margin                  | Winning team's margin of victory
Race to [X] Points              | First team to reach X points
Player Points Over/Under        | Individual player's point total
Player Rebounds Over/Under      | Individual player's rebound total
Player Assists Over/Under       | Individual player's assist total
Double Double - [Player]        | Player records double-digit in 2 stats
Triple Double - [Player]        | Player records double-digit in 3 stats
```

### Basketball Scoring Rules
- **Overtime counts** for all standard bets (money line, spread, totals)
- **Quarter/Half bets**: only the specified period counts
- **Player props**: void if player doesn't play (DNP)
- **Push**: exact totals return stake (e.g., O/U 210 and game ends 210)

## Tennis Bet Types

```
BET TYPE                        | WIN CONDITION
────────────────────────────────|────────────────────────────────────────
Match Winner - [Player]         | Player wins the match (2/3 or 3/5 sets)
Set Betting - [Score]           | Exact set score (e.g., 2-1, 3-1)
Total Games Over/Under          | Total games in match over/under line
First Set Winner                | Player wins the first set
Set 1 Total Games Over/Under    | Games played in set 1 over/under
Player to Win a Set - [Player]  | Player wins at least one set
Match to Go to Deciding Set     | Final set played (3rd in Bo3, 5th in Bo5)
Tournament Winner               | Player wins the tournament
Handicap Games +/-[X]           | Player's games +/- handicap vs opponent
Ace Count Over/Under            | Total aces in match over/under
Double Faults Over/Under        | Total double faults over/under
```

### Tennis Scoring Rules
- **Set structure**: Bo3 (women/most men) or Bo5 (Grand Slam men's singles)
- **No draws** — every match has a winner
- **Retirement**: most bookies void bets if player retires before match completion
- **Super tiebreak**: counts as one set for set betting purposes
- **Game handicap**: apply to total games won by each player

## Decision Tree

```
What sport is this leg?
├── FOOTBALL/SOCCER
│   ├── Is there a team name?
│   │   ├── YES → Is it "Match Betting"?
│   │   │   ├── YES → That team must WIN
│   │   │   └── NO → Check sub-type (BTTS, Handicap, etc.)
│   │   └── NO → Is it goals-based? Apply over/under to match total
│   └── Check for void/postponed conditions
├── BASKETBALL
│   ├── Money Line → Team wins (OT counts)
│   ├── Spread → Team score + spread > opponent score
│   ├── Over/Under → Combined points vs line
│   └── Player props → Check individual stat totals
└── TENNIS
    ├── Match Winner → Player wins required sets
    ├── Set Betting → Exact set score
    ├── Games O/U → Total games vs line
    └── Player props → Aces, double faults, etc.
```

## Implied Probability

Always calculate: `implied_prob = 1 / decimal_odds * 100`

Example: odds 2.20 → 1/2.20 = 45.5% implied probability

Flag legs under 40% as "gamble legs" in the parsed slip confirmation.

## Scoring Logic — Per Bet Type

### Match Betting (Team to Win)
- Live: Team winning → ✅ WINNING | Draw/losing → ⏳ PENDING
- FT: Team won → ✅ WON | Drew/lost → ❌ LOST

### BTTS No
- Live: 0-0 → ⏳ PENDING | X-0 or 0-X → ✅ WINNING | X-X → ❌ DEAD
- FT: X-0 or 0-X → ✅ WON | X-X → ❌ LOST

### BTTS Yes
- Live: Both scored → ✅ WINNING | One scored → ⏳ PENDING
- FT: Both scored → ✅ WON | One/both on 0 → ❌ LOST

### Over/Under Goals
- Live: Already hit target → ✅ WINNING | Under → ⏳ PENDING
- FT: Over → ✅ WON | Under → ❌ LOST
- Note: Exact lines (Over 2.0) → total = 2 = VOID (push)

### Double Chance (Team/Draw)
- Live: Team winning/drawing → ✅ WINNING | Losing → ⏳ PENDING
- FT: Won/drew → ✅ WON | Lost → ❌ LOST

### Draw No Bet
- Live: Winning → ✅ WINNING | Drawing/losing → ⏳ PENDING
- FT: Won → ✅ WON | Drew → VOID | Lost → ❌ LOST

### Handicap [Team] -[X]
- Live: Winning by X+ → ✅ WINNING | Winning by <X → ⏳ PENDING
- FT: Won by X+ → ✅ WON | Won by <X or drew/lost → ❌ LOST

### Team To Qualify
- Checks aggregate score across two legs
- Extra time/penalties may count — note rules explicitly
- FT of 2nd leg: Ahead on aggregate → ✅ WON | Behind → ❌ LOST

### Basketball — Money Line
- Live: Team leading → ✅ WINNING | Trailing → ❌ DEAD | Tied → ⏳ PENDING
- FT: Team won → ✅ WON | Lost → ❌ LOST
- Overtime counts

### Basketball — Spread
- Live: Team score + spread > opponent → ✅ WINNING | Behind → ⏳ PENDING
- FT: Cover → ✅ WON | Don't cover → ❌ LOST | Exact → VOID (push)

### Basketball — Over/Under Points
- Live: Combined total already over line → ✅ WINNING | Under → ⏳ PENDING
- FT: Over → ✅ WON | Under → ❌ LOST | Exact → VOID

### Tennis — Match Winner
- Live: Player leading in sets → ✅ WINNING | Trailing → ❌ DEAD | Even → ⏳ PENDING
- FT: Player won → ✅ WON | Lost → ❌ LOST

### Tennis — Set Betting
- Live: Current set score matches predicted → ⏳ PENDING (can't confirm until FT)
- FT: Exact match → ✅ WON | Different → ❌ LOST

### Tennis — Games Over/Under
- Live: Total games already past line → ✅ WINNING | Under → ⏳ PENDING
- FT: Over → ✅ WON | Under → ❌ LOST | Exact → VOID

## Void/Push Handling

A void leg returns the stake. In accumulators:
- Void leg is removed from calculation
- Remaining legs still count
- Recalculate odds: original_total / void_leg_odds

Report void legs:
```
 3 | Team E vs Team F | — — — | POSTPONED | BTTS No | ⏸️ VOID (stake returned)
```

## Edge Cases

### Postponed/Abandoned
- Leg is VOID (stake returned)
- Acca continues with remaining legs

### Extra Time / Penalties (Football)
- "Match Betting" (90 min) → ET/penalties DON'T count
- "To Qualify" / "To Lift Trophy" → ET/penalties DO count

### Red Cards (Football)
- Mention in report: "⚠️ {Team} down to 10 men (red card {minute}')"
- Relevant for Match Winner, Over/Under goals

### Basketball — Overtime
- Standard bets (money line, spread, O/U) → OT ALWAYS counts
- Quarter bets → OT does NOT count (only the specified quarter)

### Tennis — Retirement
- Most bookies: bet void if player retires before match completion
- Some bookies: bet stands if first set completed
- Check user's bookmaker rules. Default to void.

### Tennis — Walkover
- Always VOID (stake returned)

### Same Match, Multiple Bets
- Track each bet independently
- Report each bet's status separately


path: references/data-sources.md
content: # Data Source Strategy

Free live score sources — no API keys needed.

## IMPORTANT: Cron Environment Limitation

Cron jobs run in fresh sessions with no persistent state. Two methods available:

1. **scripts/scores.sh** (RECOMMENDED) — shell script, works with `terminal` tool
2. **execute_code** (Python) — primary when available, falls back to scores.sh

Both hit the same TheSportsDB API. The script also includes ESPN NBA fallback.

## PRIMARY: scripts/scores.sh

The helper script handles all three sports with a single command.

### Usage

```bash
bash scripts/scores.sh <date> <sport> [team_filter]

# Examples:
bash scripts/scores.sh 2026-04-15 Soccer
bash scripts/scores.sh 2026-04-15 Basketball
bash scripts/scores.sh 2026-04-15 Tennis
bash scripts/scores.sh 2026-04-15 Soccer "Arsenal"
```

### Output Format

```
league|home|away|home_score|away_score|status
# Tennis uses different fields:
league|event|result|status
```

### Exit Codes

- `0` = success, data found
- `1` = no data for that date/sport
- `2` = bad arguments

### Cron Prompt Integration

In the cron prompt, instruct the agent:

```
To check scores, run in terminal:
  bash scripts/scores.sh {DATE} Soccer
  bash scripts/scores.sh {DATE} Basketball
  bash scripts/scores.sh {DATE} Tennis

The script returns pipe-delimited results. Parse each line:
  league|home|away|home_score|away_score|status
  league|event|result|status  (tennis)
```

## SECONDARY: execute_code (Python)

When execute_code is available, use Python directly:

```python
import json, urllib.request

headers = {"User-Agent": "Mozilla/5.0"}
today = "2026-04-15"
sport = "Soccer"  # or "Basketball", "Tennis"

url = f"https://www.thesportsdb.com/api/v1/json/3/eventsday.php?d={today}&s={sport}"
req = urllib.request.Request(url, headers=headers)
with urllib.request.urlopen(req, timeout=15) as resp:
    data = json.loads(resp.read())
    for e in data.get("events", []):
        if sport == "Tennis":
            print(f"{e.get('strLeague','?')}: {e.get('strEvent','?')} — {e.get('strResult','no result')} ({e.get('strStatus','?')})")
        else:
            print(f"{e.get('strLeague','?')}: {e.get('strHomeTeam','?')} {e.get('intHomeScore','?')}-{e.get('intAwayScore','?')} {e.get('strAwayTeam','?')} ({e.get('strStatus','?')})")
```

## TheSportsDB Status Values

| Status           | Meaning              |
|-----------------|----------------------|
| Not Started      | Pre-match            |
| 1H               | First half           |
| 2H               | Second half          |
| HT               | Half time            |
| FT / Match Finished | Full time         |
| Match Cancelled  | Cancelled            |
| Postponed        | Postponed            |

## Sport-Specific Notes

### Soccer
Standard fields: strHomeTeam, strAwayTeam, intHomeScore, intAwayScore, strStatus.
Coverage: ALL tiers — Champions League to Regionalliga.

### Basketball
Same fields as soccer. TheSportsDB covers NBA, EuroLeague, BBL, LKL, LNB, etc.
If TheSportsDB returns nothing, scores.sh falls back to ESPN for:
- **NBA** — `nba/scoreboard`
- **WNBA** — `wnba/scoreboard`
- **NCAA Men's** — `mens-college-basketball/scoreboard`
- **NCAA Women's** — `womens-college-basketball/scoreboard`

EuroLeague and other international leagues are covered by TheSportsDB directly.

### Tennis
DIFFERENT field structure on TheSportsDB:
- `strHomeTeam` / `strAwayTeam` = "None" (not useful)
- `strEvent` = "Tournament Player1 vs Player2" (USE THIS)
- `strResult` = "Winner beat Loser 2-0\nWinner : 6 6\nLoser : 2 2"
- `strStatus` = normal status values
- Coverage depends on active tournaments — may be empty between events

## TERTIARY: web_search

Use when both script and Python return nothing:
```
"{Team A} vs {Team B} live score"
"{Player A} vs {Player B} tennis score {date}"
```

## Handling No Data

1. Check if kickoff has passed (if not → PENDING)
2. 30+ min past kickoff, no data → "score unavailable — match should be live"
3. 2+ hours past kickoff, no data → "unverified — likely finished, no live data"
4. NEVER guess or fabricate scores

## Lessons Learned

- **Do NOT scrape Sofascore/ESPN/BBC/LiveScore/FotMob** — JS rendering, web_extract returns empty HTML.
- **TheSportsDB first** saves 10+ wasted tool calls on JS-rendered sites.
- **Tennis coverage is tournament-dependent** — may have zero events on some days.
- **Team name mismatches** (e.g. "Dynamo Berlin" vs "BFC Dynamo") — note the alias.
- **scripts/scores.sh** handles all transport complexity — use it in cron prompts.


path: scripts/scores.sh
content: #!/usr/bin/env bash
# scores.sh — Multi-sport live score fetcher for acca-tracker
# Usage: bash scores.sh <date> <sport> [team_filter]
#   date:  YYYY-MM-DD
#   sport: Soccer | Basketball | Tennis
#   team_filter: optional grep filter (case-insensitive)
#
# Output: one line per match, pipe-delimited fields
#   Soccer/Basketball: league|home|away|home_score|away_score|status
#   Tennis:            league|event|result|status
#
# Exit codes: 0=success, 1=no data, 2=bad args

set -euo pipefail

DATE="${1:-}"
SPORT="${2:-Soccer}"
FILTER="${3:-}"

if [[ -z "$DATE" ]]; then
  echo "Usage: bash scores.sh <YYYY-MM-DD> <Soccer|Basketball|Tennis> [team_filter]" >&2
  exit 2
fi

UA="Mozilla/5.0 (compatible; AccaTracker/1.3)"
BASE="https://www.thesportsdb.com/api/v1/json/3/eventsday.php"

# Fetch from TheSportsDB
fetch_thesportsdb() {
  local sport="$1"
  local url="${BASE}?d=${DATE}&s=${sport}"
  curl -sf --max-time 15 -H "User-Agent: ${UA}" "$url" 2>/dev/null || echo '{"events":null}'
}

# Fetch from ESPN basketball leagues
# Usage: fetch_espn <league_path> <league_label>
#   league_path: nba | wnba | mens-college-basketball | womens-college-basketball
#   league_label: display name for output (e.g. "NBA", "WNBA", "NCAA-M", "NCAA-W")
fetch_espn() {
  local path="$1"
  local label="$2"
  local url="https://site.api.espn.com/apis/site/v2/sports/basketball/${path}/scoreboard"
  local raw
  raw=$(curl -sf --max-time 15 -H "User-Agent: ${UA}" "$url" 2>/dev/null) || true
  if [[ -z "$raw" || "$raw" == "null" ]]; then
    return 1
  fi
  # Parse with league label injected
  echo "$raw" | python3 -c "
import sys, json
label = '${label}'
data = json.load(sys.stdin)
events = data.get('events') or []
if not events:
    sys.exit(1)
for e in events:
    comps = (e.get('competitions') or [{}])[0].get('competitors') or []
    if len(comps) < 2:
        continue
    home = next((c for c in comps if c.get('homeAway')=='home'), comps[0])
    away = next((c for c in comps if c.get('homeAway')=='away'), comps[1])
    h_name = home.get('team',{}).get('displayName','?')
    a_name = away.get('team',{}).get('displayName','?')
    h_score = home.get('score','?')
    a_score = away.get('score','?')
    status = e.get('status',{}).get('type',{}).get('description','?')
    print(f'{label}|{h_name}|{a_name}|{h_score}|{a_score}|{status}')
" 2>/dev/null
}

# Parse soccer/basketball events from TheSportsDB JSON
parse_standard() {
  python3 -c "
import sys, json
data = json.load(sys.stdin)
events = data.get('events') or []
if not events:
    sys.exit(1)
for e in events:
    league = e.get('strLeague', '?')
    home = e.get('strHomeTeam', '?')
    away = e.get('strAwayTeam', '?')
    hs = e.get('intHomeScore', '?')
    asc = e.get('intAwayScore', '?')
    status = e.get('strStatus', '?')
    print(f'{league}|{home}|{away}|{hs}|{asc}|{status}')
" 2>/dev/null
}

# Parse tennis events (different field layout)
parse_tennis() {
  python3 -c "
import sys, json
data = json.load(sys.stdin)
events = data.get('events') or []
if not events:
    sys.exit(1)
for e in events:
    league = e.get('strLeague', '?')
    event = e.get('strEvent', '?')
    result = e.get('strResult', 'no result')
    status = e.get('strStatus', '?')
    # Clean up multiline results
    result = result.replace(chr(10), ' | ').strip()
    print(f'{league}|{event}|{result}|{status}')
" 2>/dev/null
}

# Apply optional filter
apply_filter() {
  if [[ -n "$FILTER" ]]; then
    grep -i "$FILTER" || true
  else
    cat
  fi
}

# Main logic
RESULT=""
case "$SPORT" in
  Tennis)
    RESULT=$(fetch_thesportsdb "Tennis" | parse_tennis | apply_filter) || true
    ;;
  Basketball)
    RESULT=$(fetch_thesportsdb "Basketball" | parse_standard | apply_filter) || true
    # Fallback: try ESPN leagues (NBA, WNBA, NCAA Men, NCAA Women)
    # EuroLeague covered by TheSportsDB; skip if already got data
    if [[ -z "$RESULT" ]]; then
      for league_pair in "nba:NBA" "wnba:WNBA" "mens-college-basketball:NCAA-M" "womens-college-basketball:NCAA-W"; do
        path="${league_pair%%:*}"
        label="${league_pair##*:}"
        PARTIAL=$(fetch_espn "$path" "$label" | apply_filter) || true
        if [[ -n "$PARTIAL" ]]; then
          RESULT="${RESULT:+${RESULT}
}${PARTIAL}"
        fi
      done
    fi
    ;;
  Soccer|*)
    RESULT=$(fetch_thesportsdb "Soccer" | parse_standard | apply_filter) || true
    ;;
esac

if [[ -z "$RESULT" ]]; then
  echo "NO_DATA|${SPORT}|${DATE}|No events found"
  exit 1
fi

echo "$RESULT"
exit 0


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)