Audits

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.

FixWhat changedWhy
getBettingTrendsAdded teamIds filter param + keep full threshold breakdownsRaw data for 92 teams = 616K chars. With filter: 6.7K. Users ask "over 3.5 goals at home?" — need per-threshold data
getTeamStatisticsAdded 9 more stats (fouls, offsides, saves, aerials, crosses, dribbles, duels, recoveries)"How many fouls this season?" would fail with only 22 stats
getEventPlayersFixed to match actual API shape: { players: {...}, player_statistics: {...} }Was reading wrong field names, getting all nulls
getTeamLastLineupUse isSubstitute, jerseyNo, substitutedIn/OutAPI uses different field names than assumed
getTeamLineupSame field name fixes as last lineupSame API shape
getMatchOddsPass through full bookmaker data instead of just best oddsUsers ask "what does bet365 have?"
getPlayerStatsWithOddsReturn all players with best odds, not just top 5Users ask about specific players who might not be top 5
getTeamUpcomingEventsAdded venue field"Where is the next game?" is a common question

Size comparison table (updated)

ToolBefore (raw)After (summary)SavingsNotes
getAllTeamsentire DBremoved100%
getTeamPerformance (10 matches)~21,000~3,00086%Includes possession, xG, shots, corners, fouls, cards, passes for both teams
getLeagueTable (20 teams)~34,000~2,20094%
getBettingTrends (1 team filtered)~616,000 unfiltered~6,700 per team99%Use teamIds param — MUST filter
getTeamLastLineup3,0122,9662%Minimal savings — data is already compact, just using correct field names
getTeamLineup (confirmed)20,1642,56687%Heatmap coordinates stripped (for widget only)
getTeamStatistics~2,000~80060%31 key stats from 50+
getPlayerHeatmap~5,000+~8098%
getEventPlayers (53 players)~8,000+~3,50056%Stats included only when API provides them
getTeamUpcomingEvents~6,000~1,80070%Now includes venue
getMatchOddsvariespass-through0%Data is already well-structured, users need full bookmaker details
getPlayerStatsWithOddsvariesall players + best odds~40%Widget carries full bookmaker breakdown

Tools with no changes needed:

  • getMatchDetails, getPlayerInfo, getPlayerSeasonStats, getRefereeInfo, getRefereeStatistics — small single-entity responses
  • getSeasons, getTournaments, getTeamFormations, getTeamLastFormation — small responses
  • search, 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.

On this page