Our dynasty fantasy football league has a problem. We run a custom salary cap structure with multi-year contracts, extension fees, FAAB budgets, and a rules document that nobody reads. Every week, our commissioner gets bombarded with the same questions in the group chat. "What's the extension deadline again?" "How much cap space do I have?" "Why can't I keep Brock Bowers AND James Cook?" "Can I afford to keep Ja'Marr Chase?" "How many 2-year contracts do I have left?"
So I built him a replacement.
Not for his job, just for the FAQ duty. An AI agent called "The Commish" that knows everything about our league and can answer questions instantly. Here's how I built it using the Convex Agent SDK.
Why Build an AI Commissioner?
I've been building a custom fantasy football management platform for our league. We outgrew Sleeper and ESPN years ago—our contract and salary cap rules are too complex for off-the-shelf platforms. The app handles rosters, contracts, trades, and all the league administration.
But I knew that even with a dedicated app, our commissioner would still get questions. League members will always prefer asking a human over navigating UI and doing cap math in their heads. That's just friction.
So I decided to build an AI assistant into the platform from the start. Not a generic chatbot that hallucinates stats, but an agent with direct access to real league data—rosters, contracts, rules, and settings.
The requirements were clear:
- Answer rule questions instantly without anyone digging through documents
- Pull up any team's roster, contracts, and cap situation on demand
- Remember context about each user across conversations
- Have some personality—because fantasy football should be fun
I'd been using Convex for the league management app already, so when I discovered their Agent SDK, the pieces clicked together.
Giving the Agent a Personality
The first decision was the agent's identity. I didn't want a sterile assistant that responds like a database query tool. Fantasy football leagues have culture—trash talk, inside jokes, rivalries. The agent needed to fit that culture.
I named it "The Commish" and wrote a system prompt that captures the personality of a commissioner who's seen it all. Fair but firm on rules. Helpful but willing to dish out some friendly trash talk. Knowledgeable about every player, contract, and trade in league history.
export const commishAgent = new Agent(components.agent, {
name: "The Commish",
languageModel: openrouter.chat("google/gemini-3-flash-preview"),
instructions: `You are "The Commish" — the all-knowing, slightly sarcastic
but helpful commissioner of a fantasy football league...`,
maxSteps: 12,
callSettings: { temperature: 0.5 },
});
The maxSteps: 12 setting is important. It allows the agent to make multiple tool calls before responding, which enables complex multi-step reasoning. If someone asks "Can I afford to extend Ja'Marr Chase?", the agent might need to look up the current contract, check the extension rules, calculate the new salary, and verify cap space—all before giving an answer.
Setting temperature: 0.5 gives responses enough variability to feel natural without becoming unpredictable. The agent stays accurate on facts but has room for personality in how it delivers them.
Connecting the Agent to Real Data
An AI agent is only as useful as the data it can access. The Convex Agent SDK provides a createTool function that lets you define what the agent can do. Each tool has a description (so the agent knows when to use it), arguments (what parameters it accepts), and a handler (the actual database query).
I created tools for everything the agent might need: league rules, team rosters, cap situations, recent transactions, and a league-wide overview for comparison questions.
The key insight was pre-resolving context. Instead of passing league ID and user ID through every tool call, I bake them into the tool creation itself. When a user starts a conversation, I know which league they're in and who they are. The tools are created with that context already embedded.
function createCommishTools(leagueId: Id<"leagues">, clerkUserId: string) {
return {
getTeamRoster: createTool({
description: "Get a team's roster with contracts and salaries.",
args: z.object({ teamName: z.string() }),
handler: async (ctx, { teamName }) => {
return await ctx.runQuery(internal.commishAgent.getTeamRosterData, {
leagueId, teamName,
});
},
}),
// ... other tools
};
}
This pattern keeps the agent's reasoning clean. It doesn't need to think about authentication or which league to query. It just decides "I need this team's roster" and calls the tool. The context is already handled.
Teaching the Agent to Remember
One of the most powerful features is persistent memory. Without it, every conversation starts from scratch. The agent wouldn't know that you manage "The Dynasty Destroyers" or that you've been asking about wide receivers all week.
I created a simple memory table that stores a summary for each user in each league. The agent has a saveMemory tool that it can use to update this summary whenever it learns something worth remembering.
The magic happens in the context handler. Before generating each response, I fetch the user's memory and inject it into the conversation. The agent sees something like "[User Memory] This user manages The Dynasty Destroyers. They've been interested in trading for a tight end. Their cap situation is tight due to the Ja'Marr Chase contract."
This changes everything about how natural the conversations feel. Ask "What's my cap situation?" and the agent already knows which team to look up. Mention you're thinking about a trade and it remembers that context in future messages.
Real-Time Streaming
Nobody wants to wait 10 seconds staring at a loading spinner while an AI thinks. The Convex Agent SDK supports streaming responses, which means tokens appear on the client as they're generated.
The implementation uses streamText with saveStreamDeltas: true. As the model generates each token, it's saved to the database and pushed to any subscribed clients through Convex's real-time sync. The UI updates character by character, making the agent feel responsive even when it's doing complex multi-tool reasoning behind the scenes.
I also use Convex's scheduler for non-blocking response generation. When a user sends a message, the mutation returns immediately with a message ID. The actual AI response is scheduled as a background action. This keeps the UI snappy—you see your message appear instantly, then watch the agent's response stream in.
The Thread Lifecycle
Conversations are organized into threads. Each thread belongs to a specific user in a specific league, which lets me resolve all the context I need for tools and memory.
Creating a thread is straightforward—generate a thread ID, store the mapping to league and user, and return. Sending a message saves it to the thread and schedules the response generation. The client subscribes to the thread and receives streaming updates.
export const sendMessage = mutation({
args: { threadId: v.string(), prompt: v.string() },
handler: async (ctx, { threadId, prompt }) => {
const { messageId } = await saveMessage(ctx, components.agent, { threadId, prompt });
await ctx.scheduler.runAfter(0, internal.commishAgent.generateResponse, {
threadId, promptMessageId: messageId
});
return messageId;
},
});
The scheduler.runAfter(0, ...) pattern is worth highlighting. It schedules the action to run immediately but doesn't block the mutation from returning. The user sees their message appear instantly while the AI processing happens in the background.
Tracking Usage
AI API calls cost money, and I wanted visibility into how much the agent was being used. The SDK provides a usage handler that gets called after each API request with full token breakdowns—prompt tokens, completion tokens, reasoning tokens, cached tokens.
I log this to a usage table with the league ID attached. This lets me see usage patterns per league, set limits if needed, and understand costs. It's simple but essential for any production agent.
I also plan on using this data in a separate "Year in review" wrapped feature at the end of the season. That way we can poke fun at the people who relied on The Commish a little too much.
Patterns Worth Stealing
Building this agent taught me a few patterns that generalize well:
Pre-resolve context in tools. Don't make the agent pass authentication or tenant context through every tool call. Bake it in when you create the tools. This keeps the agent's reasoning focused on what data to fetch, not how to fetch it.
Fetch in parallel. When you need multiple pieces of context (user team, memory, settings), use Promise.all to fetch them simultaneously. Every millisecond of latency compounds when you're trying to feel real-time.
Inject context through handlers. The context handler pattern lets you add information to every request without polluting the tool definitions. User memory, team context, current date—anything the agent should know goes here.
Schedule, don't await. For non-blocking operations, use the scheduler to run actions in the background. Return immediately so the UI stays responsive.
The Result
The Commish now handles questions like:
- "What's my cap situation?" (knows which team from memory)
- "Can I extend Ja'Marr Chase for 3 years?" (checks contract rules and calculates costs)
- "What trades happened this week?" (queries transaction history)
- "Compare my roster to Dynasty Destroyers" (fetches both teams and analyzes)
- "Explain the luxury tax rules" (pulls from league settings)
The personality makes it fun. Ask a silly question and you'll get some friendly trash talk back. Ask about a rival team and the agent might note that they're "still recovering from that draft disaster in 2023."
What's Next
I'm working on a few additions:
Proactive notifications. The Commish could alert users about upcoming deadlines, expiring contracts, or cap crunches before they become problems. Also have plans for The Commish to proactively generaete and post weekly newsletters and tier lists during the season for fun weekly recaps.
Trade analysis. When someone proposes a trade, the agent could evaluate it against cap implications, positional needs, and contract timelines and then provide guidance to the human commissioner on whether or not the trade is fair and should be approved.
RAG for league history. Right now the agent can only query structured data. I want to add retrieval over our league's historical records—past draft recaps, trade announcements, and commissioner rulings. This would let the agent answer questions like "What did we decide about taxi squad rules last year?"
Observability. Tracing, error tracking, and correctness monitoring. I want to see exactly which tools the agent called, catch failures before users report them, and track whether answers are actually accurate over time.
Each feature follows the same pattern: define the tools, add the context, let the agent reason.
The Convex Agent SDK abstracts the genuinely hard parts of building AI agents. Thread management, message persistence, streaming, tool execution—it's all handled. I focused on what makes this agent unique: the personality, the data access, the memory system.
If you're building with Convex and want to add AI capabilities, the Agent SDK is worth exploring. The abstractions are thoughtful, and you can get a working agent surprisingly fast.
Read more: Convex vs Supabase: What database to vibe code with
interested in creating a league? Check out more here and sign email me to join the waitlist.