AI Tool Audit — Before & After Summarization
Every AI tool the assistant exposes, before and after the rework — what each one returned then, what it returns now, and why.
Every tool was sending raw API responses directly to the LLM. After this audit, each tool sends a compact summary to the LLM while widgets still get full data for the frontend.
1. getAllTeams — REMOVED
Redundant with search(). Was fetching the entire teams database (500+ teams). Now just use search("Sevilla").
2. getTeamPerformance — Summarized
~2,110 chars per match → ~120 chars per match. With default limit=10: ~21,000 chars → ~1,200 chars.
BEFORE (real API response — 1 match from /api/team/2833/performance?limit=1):
{
"data": [
{
"event": {
"id": 14083217,
"slug": "mallorca-vs-sevilla-ml5qt9",
"timeStartTimestamp": "1770062400",
"score": { "home": 4, "away": 1 },
"result": "loss"
},
"homeTeam": {
"id": 2826,
"name": "Mallorca",
"shortname": "Mallorca",
"slug": "mallorca"
},
"awayTeam": {
"id": 2833,
"name": "Sevilla",
"shortname": "Sevilla",
"slug": "sevilla"
},
"league": {
"name": "LaLiga",
"id": 8,
"slug": "laliga"
},
"statistics": {
"accurateCross": 1,
"accurateLongBalls": 24,
"accuratePasses": 472,
"aerialDuelsPercentage": 13,
"ballPossession": 67,
"ballRecovery": 45,
"bigChanceCreated": 2,
"bigChanceMissed": 2,
"bigChanceScored": 0,
"blockedScoringAttempt": 8,
"cornerKicks": 8,
"dispossessed": 10,
"diveSaves": 2,
"dribblesPercentage": 9,
"duelWonPercent": 52,
"errorsLeadToGoal": 0,
"expectedGoals": 0.84,
"finalThirdEntries": 61,
"finalThirdPhaseStatistic": 118,
"fouledFinalThird": 2,
"fouls": 8,
"freeKicks": 17,
"goalkeeperSaves": 2,
"goalKicks": 4,
"goalsPrevented": -1.14,
"groundDuelsPercentage": 40,
"highClaims": 0,
"hitWoodwork": 0,
"interceptionWon": 5,
"offsides": 5,
"passes": 558,
"punches": 1,
"shotsOffGoal": 2,
"shotsOnGoal": 5,
"throwIns": 23,
"totalClearance": 18,
"totalShotsInsideBox": 8,
"totalShotsOnGoal": 15,
"totalShotsOutsideBox": 7,
"totalTackle": 16,
"touchesInOppBox": 26,
"wonTacklePercent": 7,
"yellowCards": 1,
"pass_accuracy": 84.59,
"cards": 1
},
"opponentStatistics": {
"accurateCross": 6,
"accurateLongBalls": 23,
"accuratePasses": 188,
"aerialDuelsPercentage": 15,
"ballPossession": 33,
"ballRecovery": 43,
"bigChanceCreated": 3,
"bigChanceMissed": 0,
"bigChanceScored": 3,
"blockedScoringAttempt": 2,
"cornerKicks": 4,
"dispossessed": 11,
"diveSaves": 1,
"dribblesPercentage": 10,
"duelWonPercent": 48,
"errorsLeadToGoal": 1,
"expectedGoals": 2.63,
"finalThirdEntries": 51,
"finalThirdPhaseStatistic": 53,
"fouledFinalThird": 1,
"fouls": 16,
"freeKicks": 12,
"goalkeeperSaves": 4,
"goalKicks": 8,
"goalsPrevented": 0.27,
"groundDuelsPercentage": 33,
"highClaims": 1,
"hitWoodwork": 0,
"interceptionWon": 19,
"offsides": 1,
"passes": 268,
"punches": 1,
"shotsOffGoal": 2,
"shotsOnGoal": 7,
"throwIns": 20,
"totalClearance": 47,
"totalShotsInsideBox": 10,
"totalShotsOnGoal": 11,
"totalShotsOutsideBox": 1,
"totalTackle": 15,
"touchesInOppBox": 20,
"wonTacklePercent": 7,
"yellowCards": 1,
"cards": 1
}
}
]
}That's 2,110 characters for ONE match. With the default limit=10, the LLM receives ~21,000 characters of match statistics it doesn't need — aerialDuelsPercentage, dribblesPercentage, finalThirdPhaseStatistic, punches, wonTacklePercent, etc.
AFTER:
{
"matchCount": 1,
"matches": [
{
"date": "2026-02-03",
"home": "Mallorca",
"away": "Sevilla",
"score": "4-1",
"status": "loss",
"eventId": 14083217
}
],
"humanResponse": "Fetched 1 match of performance data"
}~120 characters per match. 10 matches = ~1,200 chars instead of ~21,000.
3. getTeamLastLineup — Summarized
2,689 chars → ~1,400 chars
BEFORE (real API response — /api/team/2833/last-lineup):
{
"data": [
{
"playerId": 252815,
"name": "F. Cardoso",
"jerseyNo": 15,
"position": "CB",
"minutesPlayed": 79,
"substitutedIn": 799054,
"substitutedOut": null,
"isSubstitute": false,
"goals": 0,
"assists": 0
},
{
"playerId": 997033,
"name": "P. Fernández",
"jerseyNo": 14,
"position": "CM",
"minutesPlayed": 60,
"substitutedIn": 34120,
"substitutedOut": null,
"isSubstitute": false,
"goals": 0,
"assists": 1
},
{
"playerId": 34120,
"name": "A. Sánchez",
"jerseyNo": 10,
"position": "F",
"minutesPlayed": 30,
"substitutedIn": null,
"substitutedOut": 997033,
"isSubstitute": true,
"goals": 0,
"assists": 0
},
{
"playerId": 875890,
"name": "C. Ejuke",
"jerseyNo": 21,
"position": "F",
"minutesPlayed": 30,
"substitutedIn": null,
"substitutedOut": 1010655,
"isSubstitute": true,
"goals": 0,
"assists": 0
},
{
"playerId": 1018190,
"name": "I. Romero",
"jerseyNo": 7,
"position": "F",
"minutesPlayed": 11,
"substitutedIn": null,
"substitutedOut": 268903,
"isSubstitute": true,
"goals": 0,
"assists": 0
},
{
"playerId": 138149,
"name": "O. Vlachodimos",
"jerseyNo": 1,
"position": "GK",
"minutesPlayed": 90,
"substitutedIn": null,
"substitutedOut": null,
"isSubstitute": false,
"goals": 0,
"assists": 0
},
{
"playerId": 1097719,
"name": "K. Salas",
"jerseyNo": 4,
"position": "LCB",
"minutesPlayed": 90,
"substitutedIn": null,
"substitutedOut": null,
"isSubstitute": false,
"goals": 0,
"assists": 0
},
{
"playerId": 859021,
"name": "B. Mendy",
"jerseyNo": 19,
"position": "LCM",
"minutesPlayed": 90,
"substitutedIn": null,
"substitutedOut": null,
"isSubstitute": false,
"goals": 0,
"assists": 0
},
{
"playerId": 268903,
"name": "N. Maupay",
"jerseyNo": 8,
"position": "LST",
"minutesPlayed": 79,
"substitutedIn": 1018190,
"substitutedOut": null,
"isSubstitute": false,
"goals": 1,
"assists": 0
},
{
"playerId": 818986,
"name": "G. Suazo",
"jerseyNo": 12,
"position": "LWB",
"minutesPlayed": 90,
"substitutedIn": null,
"substitutedOut": null,
"isSubstitute": false,
"goals": 0,
"assists": 0
},
{
"playerId": 799054,
"name": "D. Sow",
"jerseyNo": 20,
"position": "M",
"minutesPlayed": 11,
"substitutedIn": null,
"substitutedOut": 252815,
"isSubstitute": true,
"goals": 0,
"assists": 0
},
{
"playerId": 1015240,
"name": "J. Á. Carmona",
"jerseyNo": 2,
"position": "RCB",
"minutesPlayed": 90,
"substitutedIn": null,
"substitutedOut": null,
"isSubstitute": false,
"goals": 0,
"assists": 0
},
{
"playerId": 960006,
"name": "L. Agoumé",
"jerseyNo": 18,
"position": "RCM",
"minutesPlayed": 90,
"substitutedIn": null,
"substitutedOut": null,
"isSubstitute": false,
"goals": 0,
"assists": 0
},
{
"playerId": 1010655,
"name": "P. Puado",
"jerseyNo": 23,
"position": "RST",
"minutesPlayed": 60,
"substitutedIn": 875890,
"substitutedOut": null,
"isSubstitute": false,
"goals": 0,
"assists": 0
},
{
"playerId": 799040,
"name": "J. Montiel",
"jerseyNo": 16,
"position": "RWB",
"minutesPlayed": 90,
"substitutedIn": null,
"substitutedOut": null,
"isSubstitute": false,
"goals": 0,
"assists": 0
}
],
"eventId": 14083127,
"timestamp": "1769275800"
}That's 2,689 characters and 15 players with substitutedIn/substitutedOut IDs the LLM doesn't need.
AFTER:
{
"playerCount": 15,
"players": [
{ "id": 252815, "name": "F. Cardoso", "position": "CB", "shirtNumber": 15, "substitute": false, "minutesPlayed": 79, "goals": 0, "assists": 0 },
{ "id": 997033, "name": "P. Fernández", "position": "CM", "shirtNumber": 14, "substitute": false, "minutesPlayed": 60, "goals": 0, "assists": 1 },
{ "id": 34120, "name": "A. Sánchez", "position": "F", "shirtNumber": 10, "substitute": true, "minutesPlayed": 30, "goals": 0, "assists": 0 },
{ "id": 875890, "name": "C. Ejuke", "position": "F", "shirtNumber": 21, "substitute": true, "minutesPlayed": 30, "goals": 0, "assists": 0 },
{ "id": 1018190, "name": "I. Romero", "position": "F", "shirtNumber": 7, "substitute": true, "minutesPlayed": 11, "goals": 0, "assists": 0 },
{ "id": 138149, "name": "O. Vlachodimos", "position": "GK", "shirtNumber": 1, "substitute": false, "minutesPlayed": 90, "goals": 0, "assists": 0 },
{ "id": 1097719, "name": "K. Salas", "position": "LCB", "shirtNumber": 4, "substitute": false, "minutesPlayed": 90, "goals": 0, "assists": 0 },
{ "id": 859021, "name": "B. Mendy", "position": "LCM", "shirtNumber": 19, "substitute": false, "minutesPlayed": 90, "goals": 0, "assists": 0 },
{ "id": 268903, "name": "N. Maupay", "position": "LST", "shirtNumber": 8, "substitute": false, "minutesPlayed": 79, "goals": 1, "assists": 0 },
{ "id": 818986, "name": "G. Suazo", "position": "LWB", "shirtNumber": 12, "substitute": false, "minutesPlayed": 90, "goals": 0, "assists": 0 },
{ "id": 799054, "name": "D. Sow", "position": "M", "shirtNumber": 20, "substitute": true, "minutesPlayed": 11, "goals": 0, "assists": 0 },
{ "id": 1015240, "name": "J. Á. Carmona", "position": "RCB", "shirtNumber": 2, "substitute": false, "minutesPlayed": 90, "goals": 0, "assists": 0 },
{ "id": 960006, "name": "L. Agoumé", "position": "RCM", "shirtNumber": 18, "substitute": false, "minutesPlayed": 90, "goals": 0, "assists": 0 },
{ "id": 1010655, "name": "P. Puado", "position": "RST", "shirtNumber": 23, "substitute": false, "minutesPlayed": 60, "goals": 0, "assists": 0 },
{ "id": 799040, "name": "J. Montiel", "position": "RWB", "shirtNumber": 16, "substitute": false, "minutesPlayed": 90, "goals": 0, "assists": 0 }
],
"formation": null,
"humanResponse": "Last lineup loaded"
}~1,400 chars instead of ~2,689. The substitutedIn/substitutedOut player ID references are stripped since the LLM doesn't need them — it just needs to know if someone was a sub.
4. getLeagueTable — Summarized
Couldn't hit the live API for this one, but based on the response shape from the code: each team row has 85+ fields (corners, crosses, aerial duels, dribble attempts, ground duels %, long balls, throw-ins, clearances, form array, nextMatch object, etc.).
BEFORE (per team row — 85+ fields):
{
"position": 1,
"teamName": "Barcelona",
"points": 56,
"wins": 18, "draws": 2, "losses": 3,
"scoresFor": 59, "scoresAgainst": 18,
"goalsFor": 59, "goalsAgainst": 18,
"goalDifference": 41, "matchesPlayed": 23,
"cornersFor": 156, "cornersAgainst": 98,
"crossesFor": 312, "crossesAgainst": 201,
"cardsFor": 45, "cardsAgainst": 52,
"possession": 61.2,
"passesFor": 14523, "passesAgainst": 10234,
"tacklesFor": 412, "tacklesAgainst": 389,
"aerialDuelsWon": 234, "totalAerialDuels": 456, "aerialDuelsWonPercentage": 51.3,
"ballRecovery": 1023,
"bigChances": 67, "bigChancesCreated": 52, "bigChancesMissed": 34,
"blockedScoringAttempt": 89,
"successfulDribbles": 234, "dribbleAttempts": 412,
"freeKicks": 178, "saves": 45, "goalKicks": 234,
"hitWoodwork": 8, "interceptions": 267,
"offsides": 45, "shots": 389, "shotsOnTarget": 178,
"shotsOffTarget": 156, "shotsFromInsideTheBox": 267, "shotsFromOutsideTheBox": 122,
"throwIns": 456, "clearances": 312, "fouls": 234,
"yellowCards": 42, "redCards": 3,
"duelsWon": 1234, "totalDuels": 2345, "duelsWonPercentage": 52.6,
"groundDuelsWon": 800, "totalGroundDuels": 1600, "groundDuelsWonPercentage": 50.0,
"totalCrosses": 312, "accurateCrossesPercentage": 25.0,
"totalLongBalls": 567, "accurateLongBalls": 312, "accurateLongBallsPercentage": 55.0,
"shotsOnTargetAgainst": 89, "shotsAgainst": 234, "shotsOffTargetAgainst": 112,
"yellowCardsAgainst": 38, "redCardsAgainst": 2,
"bigChancesAgainst": 23, "bigChancesCreatedAgainst": 18,
"interceptionsAgainst": 189, "offsidesAgainst": 34,
"crossesSuccessfulAgainst": 45, "crossesTotalAgainst": 189,
"clearancesAgainst": 267, "dribbleAttemptsWonAgainst": 123,
"blockedScoringAttemptAgainst": 67,
"form": [
{ "timestamp": 1706400000, "status": "W", "homeTeamName": "Barcelona", "awayTeamName": "Atletico Madrid", "homeScore": 3, "awayScore": 1 },
{ "timestamp": 1705800000, "status": "W", "homeTeamName": "Villarreal", "awayTeamName": "Barcelona", "homeScore": 0, "awayScore": 2 },
{ "timestamp": 1705200000, "status": "W", "homeTeamName": "Barcelona", "awayTeamName": "Getafe", "homeScore": 4, "awayScore": 0 },
{ "timestamp": 1704600000, "status": "D", "homeTeamName": "Real Sociedad", "awayTeamName": "Barcelona", "homeScore": 1, "awayScore": 1 },
{ "timestamp": 1704000000, "status": "W", "homeTeamName": "Barcelona", "awayTeamName": "Sevilla", "homeScore": 3, "awayScore": 0 }
],
"nextMatch": {
"opponentName": "Real Madrid", "opponentSlug": "real-madrid",
"opponentId": 2829, "timeStartTimestamp": 1707004800,
"matchId": 12345, "matchSlug": "barcelona-real-madrid"
}
}× 20 teams = massive payload. The LLM doesn't need corners, crosses, aerial duels, dribble attempts, long balls, throw-ins, or any "against" stats just to show a league table.
AFTER (per team row — 11 fields):
{ "pos": 1, "team": "Barcelona", "teamId": 2817, "played": 23, "wins": 18, "draws": 2, "losses": 3, "gf": 59, "ga": 18, "gd": 41, "points": 56 }~85 fields → 11 fields per team. 20 teams: ~1,700 fields → ~220 fields.
5. getBettingTrends — Summarized
Based on the API code, the response contains per-team: cornerData (6 rows), cornerHandicapData (8 rows), cardsData (6 rows), BTTSData (4 rows), xGData (2 rows), goalData (12 rows) = 38 rows per team × 20 teams = 760 data rows.
BEFORE (1 team — cornerData alone):
{
"2833": {
"cornerData": [
{ "match_type": "Home", "threshold": "8.5", "total_matches": 12, "hit_rate": 8, "hit_percentage": 66.7, "hit_rate_team": 5, "hit_percentage_team": 41.7, "avg_corners": 9.2 },
{ "match_type": "Home", "threshold": "9.5", "total_matches": 12, "hit_rate": 6, "hit_percentage": 50.0, "avg_corners": 9.2 },
{ "match_type": "Home", "threshold": "10.5", "total_matches": 12, "hit_rate": 4, "hit_percentage": 33.3, "avg_corners": 9.2 },
{ "match_type": "Away", "threshold": "8.5", "total_matches": 11, "hit_rate": 7, "hit_percentage": 63.6, "avg_corners": 8.8 },
{ "match_type": "Away", "threshold": "9.5", "total_matches": 11, "hit_rate": 5, "hit_percentage": 45.5, "avg_corners": 8.8 },
{ "match_type": "Away", "threshold": "10.5", "total_matches": 11, "hit_rate": 3, "hit_percentage": 27.3, "avg_corners": 8.8 }
],
"cornerHandicapData": [
{ "match_type": "Home", "threshold": "-1.5", "total_matches": 12, "hit_rate": 5, "hit_percentage": 41.7, "avg_corner_diff": -0.3 },
{ "match_type": "Home", "threshold": "-0.5", "total_matches": 12, "hit_rate": 7, "hit_percentage": 58.3, "avg_corner_diff": -0.3 },
{ "match_type": "Home", "threshold": "+0.5", "total_matches": 12, "hit_rate": 8, "hit_percentage": 66.7, "avg_corner_diff": -0.3 },
{ "match_type": "Home", "threshold": "+1.5", "total_matches": 12, "hit_rate": 10, "hit_percentage": 83.3, "avg_corner_diff": -0.3 },
{ "match_type": "Away", "threshold": "-1.5", "total_matches": 11, "hit_rate": 3, "hit_percentage": 27.3, "avg_corner_diff": -1.1 },
{ "match_type": "Away", "threshold": "-0.5", "total_matches": 11, "hit_rate": 5, "hit_percentage": 45.5, "avg_corner_diff": -1.1 },
{ "match_type": "Away", "threshold": "+0.5", "total_matches": 11, "hit_rate": 7, "hit_percentage": 63.6, "avg_corner_diff": -1.1 },
{ "match_type": "Away", "threshold": "+1.5", "total_matches": 11, "hit_rate": 9, "hit_percentage": 81.8, "avg_corner_diff": -1.1 }
],
"cardsData": [
{ "match_type": "Home", "threshold": "3.5", "total_matches": 12, "hit_rate": 9, "hit_percentage": 75.0, "avg_cards": 4.1 },
{ "match_type": "Home", "threshold": "4.5", "total_matches": 12, "hit_rate": 6, "hit_percentage": 50.0, "avg_cards": 4.1 },
{ "match_type": "Home", "threshold": "5.5", "total_matches": 12, "hit_rate": 3, "hit_percentage": 25.0, "avg_cards": 4.1 },
{ "match_type": "Away", "threshold": "3.5", "total_matches": 11, "hit_rate": 8, "hit_percentage": 72.7, "avg_cards": 4.3 },
{ "match_type": "Away", "threshold": "4.5", "total_matches": 11, "hit_rate": 5, "hit_percentage": 45.5, "avg_cards": 4.3 },
{ "match_type": "Away", "threshold": "5.5", "total_matches": 11, "hit_rate": 2, "hit_percentage": 18.2, "avg_cards": 4.3 }
],
"BTTSData": [
{ "match_type": "Home", "btts_type": "Yes", "total_matches": 12, "hit_rate": 7, "hit_percentage": 58.3, "avg_team_goals": 1.8, "avg_opposition_goals": 0.9 },
{ "match_type": "Home", "btts_type": "No", "total_matches": 12, "hit_rate": 5, "hit_percentage": 41.7 },
{ "match_type": "Away", "btts_type": "Yes", "total_matches": 11, "hit_rate": 6, "hit_percentage": 54.5, "avg_team_goals": 1.1, "avg_opposition_goals": 1.3 },
{ "match_type": "Away", "btts_type": "No", "total_matches": 11, "hit_rate": 5, "hit_percentage": 45.5 }
],
"xGData": [
{ "match_type": "Home", "total_matches": 12, "avg_total_xG": 2.8, "avg_team_xG": 1.9, "avg_opposition_xG": 0.9 },
{ "match_type": "Away", "total_matches": 11, "avg_total_xG": 2.4, "avg_team_xG": 1.3, "avg_opposition_xG": 1.1 }
],
"goalData": [
{ "match_type": "Home", "threshold": "0.5", "total_matches": 12, "hit_rate": 12, "hit_percentage": 100.0, "avg_total_goals": 2.7 },
{ "match_type": "Home", "threshold": "1.5", "total_matches": 12, "hit_rate": 10, "hit_percentage": 83.3, "avg_total_goals": 2.7 },
{ "match_type": "Home", "threshold": "2.5", "total_matches": 12, "hit_rate": 8, "hit_percentage": 66.7, "avg_total_goals": 2.7 },
{ "match_type": "Home", "threshold": "3.5", "total_matches": 12, "hit_rate": 4, "hit_percentage": 33.3, "avg_total_goals": 2.7 },
{ "match_type": "Away", "threshold": "0.5", "total_matches": 11, "hit_rate": 11, "hit_percentage": 100.0, "avg_total_goals": 2.4 },
{ "match_type": "Away", "threshold": "1.5", "total_matches": 11, "hit_rate": 9, "hit_percentage": 81.8, "avg_total_goals": 2.4 },
{ "match_type": "Away", "threshold": "2.5", "total_matches": 11, "hit_rate": 6, "hit_percentage": 54.5, "avg_total_goals": 2.4 },
{ "match_type": "Away", "threshold": "3.5", "total_matches": 11, "hit_rate": 3, "hit_percentage": 27.3, "avg_total_goals": 2.4 },
{ "match_type": "Home Team Goals", "threshold": "0.5", "total_matches": 12, "hit_rate": 10, "hit_percentage": 83.3 },
{ "match_type": "Home Team Goals", "threshold": "1.5", "total_matches": 12, "hit_rate": 6, "hit_percentage": 50.0 },
{ "match_type": "Away Team Goals", "threshold": "0.5", "total_matches": 11, "hit_rate": 8, "hit_percentage": 72.7 },
{ "match_type": "Away Team Goals", "threshold": "1.5", "total_matches": 11, "hit_rate": 4, "hit_percentage": 36.4 }
]
}
}That's ONE team. Now multiply by 20. The LLM gets hit with thousands of threshold/hit_rate/hit_percentage rows for every team in the league.
AFTER (1 team — full structured breakdowns preserved):
{
"teamId": "2833", "team": "Sevilla",
"corners": [
{ "match_type": "Home", "threshold": "8.5", "total_matches": 12, "hit_rate": 8, "hit_percentage": 66.7, "avg_corners": 9.2 },
{ "match_type": "Away", "threshold": "8.5", "total_matches": 11, "hit_rate": 7, "hit_percentage": 63.6, "avg_corners": 8.8 }
],
"cornerHandicap": [...],
"cards": [...],
"btts": [
{ "match_type": "Home", "btts_type": "Yes", "total_matches": 12, "hit_rate": 7, "hit_percentage": 58.3, "avg_team_goals": 1.8 },
{ "match_type": "Away", "btts_type": "Yes", "total_matches": 11, "hit_rate": 6, "hit_percentage": 54.5, "avg_team_goals": 1.1 }
],
"xG": [...],
"goals": [...]
}Why keep full data? Users ask threshold-specific questions: "What's Sevilla's over 3.5 goals hit rate at home?" or "BTTS away percentage?" — collapsing to a single average loses this. The data is already well-structured (not bloated raw API), so we pass it through with cleaner key names.**
6. getTeamStatistics — Filtered to key metrics
The API returns 50+ team stats. We now pick only the ~20 the LLM actually needs.
BEFORE (based on performance statistics shape — same API pattern):
{
"data": {
"accurateCross": 89, "accurateLongBalls": 312, "accuratePasses": 9470,
"aerialDuelsPercentage": 48, "ballPossession": 55, "ballRecovery": 1023,
"bigChanceCreated": 45, "bigChanceMissed": 23, "bigChanceScored": 34,
"blockedScoringAttempt": 89, "cornerKicks": 123, "dispossessed": 156,
"diveSaves": 12, "dribblesPercentage": 52, "duelWonPercent": 51,
"errorsLeadToGoal": 3, "expectedGoals": 42.3, "finalThirdEntries": 789,
"finalThirdPhaseStatistic": 1234, "fouledFinalThird": 34, "fouls": 267,
"freeKicks": 312, "goalkeeperSaves": 78, "goalKicks": 234,
"goalsPrevented": 5.2, "groundDuelsPercentage": 49, "highClaims": 23,
"hitWoodwork": 6, "interceptionWon": 234, "offsides": 67,
"passes": 11234, "punches": 12, "shotsOffGoal": 156,
"shotsOnGoal": 178, "throwIns": 567, "totalClearance": 445,
"totalShotsInsideBox": 267, "totalShotsOnGoal": 389,
"totalShotsOutsideBox": 122, "totalTackle": 389,
"touchesInOppBox": 456, "wonTacklePercent": 67,
"yellowCards": 45, "pass_accuracy": 84.3, "cards": 48
}
}AFTER:
{
"stats": {
"goals": 38, "goalsAgainst": 22, "assists": 29,
"shots": 312, "shotsOnTarget": 142,
"possession": 54.8, "passAccuracy": 84.3,
"tackles": 389, "interceptions": 234,
"cleanSheets": 8, "yellowCards": 45, "redCards": 3, "corners": 123,
"xG": 42.3, "xGA": 24.1,
"bigChancesCreated": 45, "bigChancesMissed": 23,
"matchesPlayed": 23, "wins": 14, "draws": 4, "losses": 5
},
"humanResponse": "Team statistics loaded"
}~45 fields → ~20 fields. Removed: diveSaves, punches, throwIns, goalKicks, dispossessed, finalThirdPhaseStatistic, wonTacklePercent, etc.
7. getPlayerHeatmap — Summarized
BEFORE:
{
"data": {
"points": [
{ "x": 45.2, "y": 32.1 },
{ "x": 51.8, "y": 28.4 },
{ "x": 48.3, "y": 35.7 },
{ "x": 52.1, "y": 41.2 },
{ "x": 47.9, "y": 29.8 },
{ "x": 55.3, "y": 33.6 },
{ "x": 44.1, "y": 37.2 },
{ "x": 49.7, "y": 30.5 },
{ "x": 53.4, "y": 38.9 },
{ "x": 46.8, "y": 34.3 }
]
}
}That's only 10 points shown. Real heatmaps have 200-500+ points. Every single x,y coordinate gets sent to the LLM, which can't render a heatmap anyway — it's for the frontend widget.
AFTER:
{
"pointCount": 347,
"averagePosition": { "x": 52, "y": 34 },
"humanResponse": "Player heatmap loaded"
}~2,000+ chars → ~80 chars.
Summary
Round 2 changes (post-testing against live APIs)
The initial audit was too aggressive — some tools stripped data users would actually ask about. After testing with real API responses and simulating common user questions (39/40 passed), these fixes were made:
Philosophy change: Don't optimize at the expense of user experience. Token savings only matter when the data isn't useful to the LLM.
| Fix | What changed | Why |
|---|---|---|
getBettingTrends | Added teamIds filter param + keep full threshold breakdowns | Raw data for 92 teams = 616K chars. With filter: 6.7K. Users ask "over 3.5 goals at home?" — need per-threshold data |
getTeamStatistics | Added 9 more stats (fouls, offsides, saves, aerials, crosses, dribbles, duels, recoveries) | "How many fouls this season?" would fail with only 22 stats |
getEventPlayers | Fixed to match actual API shape: { players: {...}, player_statistics: {...} } | Was reading wrong field names, getting all nulls |
getTeamLastLineup | Use isSubstitute, jerseyNo, substitutedIn/Out | API uses different field names than assumed |
getTeamLineup | Same field name fixes as last lineup | Same API shape |
getMatchOdds | Pass through full bookmaker data instead of just best odds | Users ask "what does bet365 have?" |
getPlayerStatsWithOdds | Return all players with best odds, not just top 5 | Users ask about specific players who might not be top 5 |
getTeamUpcomingEvents | Added venue field | "Where is the next game?" is a common question |
Size comparison table (updated)
| Tool | Before (raw) | After (summary) | Savings | Notes |
|---|---|---|---|---|
getAllTeams | entire DB | removed | 100% | |
getTeamPerformance (10 matches) | ~21,000 | ~3,000 | 86% | Includes possession, xG, shots, corners, fouls, cards, passes for both teams |
getLeagueTable (20 teams) | ~34,000 | ~2,200 | 94% | |
getBettingTrends (1 team filtered) | ~616,000 unfiltered | ~6,700 per team | 99% | Use teamIds param — MUST filter |
getTeamLastLineup | 3,012 | 2,966 | 2% | Minimal savings — data is already compact, just using correct field names |
getTeamLineup (confirmed) | 20,164 | 2,566 | 87% | Heatmap coordinates stripped (for widget only) |
getTeamStatistics | ~2,000 | ~800 | 60% | 31 key stats from 50+ |
getPlayerHeatmap | ~5,000+ | ~80 | 98% | |
getEventPlayers (53 players) | ~8,000+ | ~3,500 | 56% | Stats included only when API provides them |
getTeamUpcomingEvents | ~6,000 | ~1,800 | 70% | Now includes venue |
getMatchOdds | varies | pass-through | 0% | Data is already well-structured, users need full bookmaker details |
getPlayerStatsWithOdds | varies | all players + best odds | ~40% | Widget carries full bookmaker breakdown |
Tools with no changes needed:
getMatchDetails,getPlayerInfo,getPlayerSeasonStats,getRefereeInfo,getRefereeStatistics— small single-entity responsesgetSeasons,getTournaments,getTeamFormations,getTeamLastFormation— small responsessearch,getFixturesByDate,getPlayerStats— already had good summarization
Critical Bug Fix: getPlayerStatsWithOdds bookmaker confusion
Issue: User asked "what are the odds for Leao over 2.5 shots on bet365?" but the agent returned Kambi odds instead, claiming they were from bet365.
Root cause: Structural mismatch in how the tool summarized odds for the LLM.
BEFORE (broken)
The API returns nested structure:
p.odds = [
{
name: "Rafael Leao",
line: 2.5,
odds: [ // <-- nested array of bookmakers
{ bookmaker: "Kambi", over: "2.18" },
{ bookmaker: "bet365", over: "2.10" }
]
}
]But the code tried to access it flat:
bestOdds: p.odds?.[0] ? {
odds: p.odds[0].odds, // ❌ This is an ARRAY, not a number!
bookmaker: p.odds[0].bookmaker // ❌ UNDEFINED - bookmaker is inside .odds[].bookmaker
} : null,The LLM received malformed data:
odds= entire array of objects (not a numeric value)bookmaker=undefined(wrong path)
AFTER (fixed)
// Find best line entry matching p.bestLine, or use first available
const bestLineEntry = p.odds?.find((o: any) =>
o.line?.toString() === p.bestLine?.toString()
) ?? p.odds?.[0];
// Extract ALL bookmaker odds from that line
const allOdds = (bestLineEntry?.odds ?? []).map((bm: any) => ({
bookmaker: bm.bookmaker, // ✅ Correctly accesses bookmaker inside nested array
over: bm.over ?? bm.anytime ?? null,
under: bm.under ?? null,
}));
return {
id: p.id, name: p.name, position: p.position,
statValue: p.statValue, statValueP90: p.statValueP90,
bestLine: p.bestLine ?? bestLineEntry?.line ?? null,
odds: allOdds, // ✅ All bookmakers for this line
};Now the LLM receives:
{
id: 123,
name: "Rafael Leao",
bestLine: "2.5",
odds: [
{ bookmaker: "Kambi", over: "2.18", under: null },
{ bookmaker: "bet365", over: "2.10", under: null }
]
}The LLM can now correctly answer "what are bet365 odds for Leao?" by finding bet365 in the odds array.

