Bet on yourself. Talk to the hand.
OffHand is a habit accountability app. You chat with The Challenger (a slightly cheeky hand persona), wager virtual "fingers" on a habit, and either follow through or lose them.
Chat‑only — no forms, no confirm buttons. Just talk.
- Conversational betting — the LLM understands "bet 100 on the run tomorrow" and places it via tool calls.
- Persistent chat memory keyed by session (MongoDB).
- Per‑user identity + fingers balance, updated automatically as bets are created and completed.
- Web app + PWA install (works offline shell, add to home screen).
- Voice input (Web Speech API) + text‑to‑speech reply with system voice picker.
- Markdown‑rendered replies, bullet lists for "show my bets".
Backend
- FastAPI + Uvicorn
- Beanie (async MongoDB ODM)
- LangChain + Gemini (
langchain-google-genai) langchain-mongodbchat message historyuvfor dependency management
Frontend
- Plain HTML / CSS / vanilla JS in
ui/— no build step - Google Fonts (Space Grotesk + Inter), Ionicons,
marked,DOMPurify - Service worker + web manifest for PWA install
- Python 3.13
uv- MongoDB (local or Atlas)
- Google AI Studio API key (Gemini)
-
Clone and install:
git clone https://github.com/<you>/OffHand cd OffHand uv sync
-
Create a
.envat the repo root:MODEL=gemini-2.5-flash API_KEY=your_google_ai_studio_key MONGODB_URL=mongodb://localhost:27017
-
Run:
make run
Serves on
http://localhost:8000.
make uiServes ui/ on http://localhost:5173 via Python's built‑in http.server. Open it in a browser. First visit → sign up.
To point the UI at a deployed backend, edit const API at the top of ui/app.html.
src/
main.py FastAPI app + CORS + router mounts
api/
chatbot_router.py POST /chatbot/
user_router.py POST /user/{signup,signin}, GET /user/{id}
ai/
llm.py tool loop
prompts.py SYSTEM_PROMPT
context.py current_user_id ContextVar
service/
bet_service.py LangChain @tool functions
user_service.py auth + user helpers
model/ Beanie Documents
config/ settings + Mongo init
ui/
index.html landing page (install PWA)
app.html chat UI
manifest.webmanifest PWA manifest — start_url = /app.html
sw.js network-first for HTML, cache-first for assets
hand.png, point.png mascot + favicon
- Frontend generates a
user_id(on signup) and asession_id(per browser session), stores both inlocalStorage, and sends them as headers on every/chatbot/call. session_idkeys theMongoDBChatMessageHistorycollection → per‑session chat memory.user_idis set into a PythonContextVarat the top of the chatbot route. Bet tools read it from there — the LLM never sees or handles user IDs, so it can't accidentally act on the wrong user.- The LLM is bound to four bet tools:
create_bet,get_all_bets,get_bet,update_bet. User CRUD lives in a plain FastAPI router — deliberately not exposed as LLM tools. - Balances are updated automatically inside
update_betwhen a bet is completed or reopened.
- Bet expiry (days) with auto‑forfeit on
expires_at - The Challenger's own finger balance / counter‑bet tracking
- Habit categories + streaks
- Push notifications for expiring bets
- Real password hashing (currently plaintext — hobby stage)
MIT (or whichever — add if needed).

