An AI assistant for sports bettors: chat with an agent that reasons over a live warehouse of games, player stats, betting odds, and player props, and answers questions like "which of tonight's strikeout props look soft given recent form?" — with every number grounded in real data, not model memory.
Status: sunset. Kleet ran in production March–August 2026 with paying users, then was deliberately wound down (solo side project; the economics didn't justify keeping the infrastructure live). The codebase is complete and runs locally. This repo is maintained as a case study.
- Conversational betting analysis — a LangGraph agent with SQL and Python tools over a Postgres warehouse of MLB/NBA data (games, box scores, season stats, standings, lineups, splits, pitch-type aggregates), streamed to the browser over SSE.
- Live odds & props — game odds and player props synced every 5 minutes and treated as a live mirror of currently-placeable bets: anything the sportsbook pulls is reaped from the DB, so the agent never recommends a bet that already closed.
- Pre-computed slate context — a daily job distills team form, hitter hot/cold streaks, and pitcher form for today's + tomorrow's slate, plus LLM-summarized news with hard guardrails (every item must cite a source URL; entity IDs constrained to a DB-provided roster; empty output is valid).
- Automated social pipeline — the agent drafts X posts, fact-checks every number in each draft against the database (fail-closed), and a human approves via Telegram before anything publishes.
- SaaS scaffolding — Auth0 login, Stripe subscriptions with tiered model/feature gating, per-user usage metering.
Three services, two databases:
flowchart LR
FE["frontend/<br/>Next.js + assistant-ui"] -- "SSE stream" --> BE["backend/<br/>FastAPI + LangGraph agent"]
BE --> UDB[("kleet_user<br/>users · threads · billing")]
BE -- "read-only" --> SDB[("kleet_data<br/>games · stats · odds · props")]
DS["data/<br/>cron sync jobs"] -- "writes" --> SDB
BDL["BallDontLie API"] --> DS
BE --> LLM["LLM providers<br/>(Anthropic · OpenRouter · Fireworks)"]
AUTH["Auth0"] -.-> BE
STRIPE["Stripe"] -.-> BE
TG["Telegram review bot"] -.-> BE
BE -.-> X["X API"]
backend/— FastAPI + LangGraph agent, SSE chat streaming, Auth0 JWT auth, Stripe webhooks, tier gating, the social-post pipeline.frontend/— Next.js + assistant-ui LocalRuntime, Tailwind CSS v4.data/— SQLAlchemy sync service run as cron jobs at different cadences (every minute for live game clocks → every 5 min for odds/stats → hourly for games/rosters/standings → daily for splits/context), sourcing from the BallDontLie API.
The decisions I'd point a reviewer at:
- Sandboxed Python tool, contained in three layers. The agent can execute model-written Python. Containment: an env-var allowlist (no secret can reach the subprocess), a shared SQL validator injected as source into the sandbox prelude so the two query paths can't drift, and — the hard wall — a
SELECT-only Postgres role, so writes fail at the database no matter what code runs. - Live-mirror odds with fail-safe reaping. Odds/props rows that the upstream API stops returning are deleted (the bet closed) — but reaping is skipped whenever a fetch fails, so an error-induced empty response is never misread as "all bets dropped." Destructive syncs get the most defensive code.
- A time-zone bug class, eliminated structurally. All sports data is Eastern Time; naive
datetime.now()is off by a calendar day every evening. Sharedet_now()/et_today()helpers, an agent prompt that pins the convention, and tests that cover the bug class rather than individual bugs. - LLM writes to the DB, treated as hostile input. The news summarizer is the one place a model writes to the database. Guardrails: IDs constrained to a known roster, mandatory verbatim source citations, unparseable output = write nothing, and the news job never deletes — destructive operations belong exclusively to the deterministic job that precedes it.
- Fact-checked content generation. Social drafts are verified number-by-number against the DB before a human ever sees them; verification is fail-closed (unparseable verdict = unverified), and a round where no draft passes is regenerated from scratch — bad numbers structurally cannot reach review.
- Graceful league degradation. A single
DISABLED_LEAGUESenv var makes a league invisible to the agent end-to-end (schema, prompts, query validation) without touching data — used in production when a data-feed subscription lapsed mid-season, because a betting assistant quietly serving stale data is worse than one that says "NBA is unavailable." - CI across three services — ruff + pyright + pytest on both Python services, lint + typecheck + build on the frontend; tests focus on high-blast-radius paths (billing webhooks, auth, the ET bug class, destructive reaping guards).
backend/ FastAPI app — agent, tools, routers, social pipeline, tests
frontend/ Next.js app — chat UI, settings, billing
data/ Sync service — cron scripts, SQLAlchemy models, API clients, tests
scripts/ Local database helpers and sandbox-role setup
Requires PostgreSQL 18, Python 3.12+ with uv, and Node 20+.
# databases
createdb kleet_user && createdb kleet_data
# backend
cd backend && cp .env.example .env # fill in what you have; agent needs an LLM key
uv sync && uv run uvicorn app.main:app --reload
# frontend
cd frontend && cp .env.example .env
npm install && npm run dev
# data service (populates kleet_data; needs a BallDontLie key)
cd data && cp .env.example .env
uv sync && uv run python -m app.sync.mlb_game_syncThe app expects Auth0 and Stripe credentials for the full login/billing flow; the sync service and agent tooling run without them.
Kleet is an archived software project provided for educational purposes. It is not betting or financial advice. Sports data and odds can be incomplete or stale; gamble responsibly.
Licensed under the MIT License.
Built solo, March–August 2026. Questions welcome — open an issue.