Inspiration

Every offseason, trade Twitter is full of hot takes with zero grounding: "this trade is a 10/10" from someone who hasn't looked at a box score in months. I wanted to see if an LLM agent, given real tools instead of just its training data, could actually do the job a front-office analyst does. Pull real rosters, check real stats, factor in injuries, read the actual news, and then form an opinion, live, with its reasoning visible the whole time instead of a black-box answer.

What it does

I describe a trade or signing in plain English and pick the team I'm evaluating it for. An agent built on LangGraph and Claude takes it from there:

  • Pulls both teams' current rosters
  • Looks up season stats and current injury status for the player(s) involved, plus at least one key incumbent whose role is affected. The agent decides who's relevant on its own; I never hand-pick anyone
  • Searches recent news for context and reactions
  • Reasons through all of it and produces a graded report: letter grade, win-probability delta, stat comparisons, a historical parallel when one genuinely fits, and cited sources

I can watch it happen in real time. A live terminal-style feed streams each tool call as the agent makes it, followed by a stat dashboard and the final report card, all wrapped in a custom-built dark UI with an interactive WebGL background.

How I built it

Backend is FastAPI wrapping a LangGraph agent with two nodes: a gather loop that calls Claude with tools (rosters, player stats, news search) until it has enough context, and a finalize step that forces a structured tool call so I always get clean JSON back, never freeform text I have to hope parses correctly. I split the model choice across the two nodes: Haiku for the fast tool-orchestration loop, Sonnet for the final analysis where quality actually matters. I also made every tool call that can run in parallel actually run in parallel via asyncio.gather, which cut a roughly 50 second run down to about 15.

Results stream to the frontend over Server-Sent Events, which I parse by hand on the frontend since the native EventSource API can't send a POST body. The frontend is React and Vite with no CSS framework: a custom interactive border-glow effect on every card, and a WebGL background built on OGL, both hand-tuned to a consistent dark navy and orange theme.

Data comes from balldontlie (rosters, stats, injuries) and Tavily (news search), deployed on Railway (backend) and Vercel (frontend).

Challenges I ran into

The sports data API turned out to be the hardest part of this project, harder than the agent itself. Its player search doesn't reliably match a full "First Last" query, and naively retrying on a partial name once returned a completely unrelated player who happened to share a common surname, a bug that only showed up because I built a diagnostic script and tested against the real API instead of trusting it blindly. I ended up writing a search function that only accepts a result if the full name matches exactly, trying progressively narrower terms until it finds one.

Season averages also turned out to be gated behind a paid tier I didn't have at first, which surfaced as a silent 401 deep in a working pipeline. Rather than just erroring out, I built a three-tier fallback: try the official season-averages endpoint, fall back to averaging individual game logs myself if that tier isn't available, fall back to bio data alone as a last resort. The app degrades gracefully instead of breaking, regardless of which API tier is active.

I also hit a subtler bug in my own SSE parsing. The spec allows \r\n\r\n as an event boundary, not just \n\n, and my parser only checked for the latter, so on some stacks every event silently vanished into a buffer that was thrown away when the stream closed. No error, no crash, just nothing rendering. That one took real diagnostic work to track down since it looked identical to a backend hang from the outside.

Accomplishments that I'm proud of

Getting the agent to genuinely reason instead of just cheerlead. In one test run it graded a hypothetical trade down specifically because the actual news search didn't support the premise, and said so in the verdict. That's the moment this stopped feeling like a template and started feeling like an actual analyst.

I'm also proud of how resilient the backend ended up being by the end: timeouts on every external call, automatic retry on rate limits, and a genuine multi-tier fallback for the sports data instead of a single point of failure. None of that was in the original plan; it's the result of actually testing against real APIs instead of assuming they'd behave.

What I learned

Third-party APIs lie by omission far more than they error out loudly. A 200 with an empty array, or a fuzzy search that returns the wrong entity, is a much sneakier bug than a clean 401. Building a standalone diagnostic script early, separate from the full agent, saved a huge amount of time once things stopped working as expected. I also learned to trust structured tool-forced output over parsed free text the moment stakes went up. Every JSON parsing headache in this project disappeared once I started forcing the schema at the API level instead of hoping the model formatted things correctly.

What's next for Run The Trade

Multi-team trades (three-way deals are common in the real NBA and the current schema only handles two sides), a shareable image export of the report card for social, and extending the same agent pattern beyond basketball. The tool-calling structure barely cares what sport it's reasoning about; it's really the rosters and stats underneath that are basketball-specific.

Built With

Share this project:

Updates

Submission history