{"openapi":"3.0.0","info":{"title":"AlgoVictory API","version":"1.0.0","description":"HTTP API for AlgoVictory. The service is a Django 5 + Django REST Framework application\nthat issues JWTs via SimpleJWT, supports Google and Twitter sign-in through django-allauth.\n\n## Authentication\nAll endpoints require a valid JWT Bearer token by default unless marked with `security: []`.\n\n## Rate Limiting\nAll endpoints are subject to global throttling: **10 req/s** for unauthenticated requests,\n**20 req/s** for authenticated requests. Certain endpoints have stricter scoped limits noted\nin their descriptions. Exceeded limits return `429 Too Many Requests`.\n"},"servers":[{"url":"http://127.0.0.1:8000","description":"Local development"},{"url":"https://algovictory.com","description":"production deployment"}],"tags":[{"name":"Auth","description":"Account creation and JWT lifecycle"},{"name":"Users","description":"Authenticated user profile access"},{"name":"Allauth Providers","description":"Social login flow via django-allauth and the subsequent session-to-JWT exchange"},{"name":"Sports","description":"UFC events and fighter data"},{"name":"Payments","description":"Stripe checkout and webhook for premium membership"}],"paths":{"/api/user/register/":{"post":{"tags":["Auth"],"summary":"Register a new user","description":"Creates a user account using a username, email, and password.\n\nRate limited to **10 requests/hour** per IP (`register` scope).\n","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserRegistrationRequest"},"examples":{"default":{"summary":"Basic registration","value":{"username":"janedoe","email":"jane@example.com","password":"Passw0rd!"}}}}}},"responses":{"201":{"description":"User created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded (10/hour)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/token/":{"post":{"tags":["Auth"],"summary":"Login end point","description":"Exchanges username and password for a token pair using SimpleJWT.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenObtainRequest"},"examples":{"password-login":{"summary":"Password login","value":{"username":"janedoe","password":"Passw0rd!"}}}}}},"responses":{"200":{"description":"Token pair issued","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenPair"}}}},"401":{"description":"Invalid credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/token/refresh/":{"post":{"tags":["Auth"],"summary":"Refresh an access token","description":"Exchanges a valid refresh token for a new access token. Refresh tokens expire after 1 day.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenRefreshRequest"},"examples":{"refresh":{"value":{"refresh":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}}}}}},"responses":{"200":{"description":"New access token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenRefreshResponse"}}}},"401":{"description":"Invalid or expired refresh token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/user-info/":{"get":{"tags":["Users"],"summary":"Get the authenticated user's profile","description":"Returns profile data for the currently authenticated user.","responses":{"200":{"description":"User profile","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserInfo"}}}},"401":{"description":"Missing or invalid JWT","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/social-token/":{"get":{"tags":["Auth","Allauth Providers"],"summary":"Exchange a social session for JWTs","description":"`LOGIN_REDIRECT_URL` in Django settings points to this endpoint (`/api/social-token/`).\nAfter django-allauth finishes the provider login and sets the user session, the\nbrowser is redirected here. The view requires the authenticated session (no\ndirect API clients) and converts it into SimpleJWT access and refresh tokens,\nthen redirects to the configured frontend URL (`FRONTEND_URL`) with `access`\nand `refresh` tokens in the query string.\n\nIf the session is missing or expired, a 401 JSON error is returned. This endpoint\nis intended only as an internal redirect target; callers should not hit it\ndirectly without first completing the social login.\n","responses":{"302":{"description":"Redirect to the frontend with `access` and `refresh` query params"},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}}}}},"/accounts/google/login/":{"get":{"tags":["Auth","Allauth Providers"],"summary":"Start Google OAuth2 login (redirect)","description":"Frontend initiates Google sign-in by calling this django-allauth endpoint.\nIt responds with a 302 redirect to Google's OAuth consent screen. After a\nsuccessful grant, allauth returns the browser to `LOGIN_REDIRECT_URL`\n(`/api/social-token/`) where the session is exchanged for JWTs.\n","security":[],"responses":{"302":{"description":"Redirects the browser to Google OAuth consent"}}}},"/accounts/twitter/login/":{"get":{"tags":["Auth","Allauth Providers"],"summary":"Start Twitter OAuth1 login (redirect)","description":"Frontend initiates Twitter (OAuth1) sign-in by calling this django-allauth\nendpoint. It responds with a 302 redirect to Twitter's OAuth authorization screen.\nAfter authorization, allauth returns the browser to `/api/social-token/` to\nconvert the authenticated session into JWTs.\n","security":[],"responses":{"302":{"description":"Redirects the browser to Twitter OAuth1 authorization"}}}},"/api/reset-password/":{"post":{"tags":["Auth"],"summary":"Request a password reset email","description":"Validates the email against existing accounts and triggers allauth's\npassword reset flow, which sends an email containing a uid and token.\nThe email links to `FRONTEND_URL/reset-password/{uid}/{key}`.\n\nRate limited to **4 requests/hour** per IP (`password_reset` scope).\n","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResetPasswordRequest"},"examples":{"default":{"summary":"Request password reset","value":{"email":"jane@example.com"}}}}}},"responses":{"200":{"description":"Reset email sent (always returns 200 to avoid email enumeration)","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"400":{"description":"Validation error — invalid email format, or no account found with that email","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded (4/hour)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/set-password/":{"post":{"tags":["Auth"],"summary":"Set a new password using a reset token","description":"Accepts the base36-encoded uid and token from the password reset email\nalong with a new password. The uid is decoded to look up the user, and\nthe token is verified using allauth's `PASSWORD_RESET_TOKEN_GENERATOR`.\n\nRate limited to **4 requests/hour** per IP (`password_reset` scope).\n","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetPasswordRequest"},"examples":{"default":{"summary":"Set new password","value":{"uid":"c","key":"d5abr1-9c71a70ebbe989ddcf059bfb7417b628","password":"NewPassw0rd!"}}}}}},"responses":{"200":{"description":"Password changed successfully","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"string"}},"required":["ok"]}}}},"400":{"description":"Invalid or expired token, or validation error (password too short)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded (4/hour)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/sports/show-events/":{"get":{"tags":["Sports"],"summary":"List all UFC events","description":"Returns all stored upcoming UFC events.","security":[],"responses":{"200":{"description":"List of events","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UpcomingEvent"}}}}}}}},"/sports/fight-card/":{"get":{"tags":["Sports"],"summary":"Get the fight card for the next event","description":"Returns all fights for the first upcoming event, including both fighters and their career stats per fight.","security":[],"responses":{"200":{"description":"Fight card","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/FightCard"}}}}}}}},"/sports/ai/":{"get":{"tags":["Sports"],"summary":"Get AI analysis for the upcoming fight card","description":"Returns the stored AI analysis record for the current upcoming event.\nFour LLM models each provide a per-fighter breakdown and a winner prediction:\n- `qwen/qwen3-32b`\n- `llama-3.1-8b-instant`\n- `llama-3.3-70b-versatile`\n- `meta-llama/llama-4-scout-17b-16e-instruct`\n\n**Requires premium membership.** The request must carry a valid JWT for an\naccount where `premium = true`. A valid JWT from a non-premium account\nreturns `403`.\n","responses":{"200":{"description":"AI analysis record for the upcoming event","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AisResponse"}}}},"401":{"description":"Missing or invalid JWT","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Authenticated user does not have premium membership","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No AI analysis record found for the current event","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/payments/stripe-pay/":{"post":{"tags":["Payments"],"summary":"Create a Stripe checkout session","description":"Creates a Stripe Checkout Session for a one-time $5.00 USD membership\npayment. On success, Stripe redirects the user to\n`FRONTEND_URL/payments/success/`; on cancel, to\n`FRONTEND_URL/payments/cancel/`. The session `metadata` carries the\nauthenticated user's ID so the webhook can upgrade the account.\n","responses":{"200":{"description":"Checkout session URL","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri","description":"Stripe-hosted checkout URL the client should redirect to"}},"required":["url"]},"examples":{"default":{"value":{"url":"https://checkout.stripe.com/pay/cs_test_..."}}}}}},"401":{"description":"Missing or invalid JWT","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"200 (error)":{"description":"Stripe authentication failure (API key missing/invalid)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}}}}},"/payments/stripe-webhook/":{"post":{"tags":["Payments"],"summary":"Stripe webhook receiver","description":"Receives signed webhook events from Stripe. The signature is verified\nusing the `Stripe-Signature` header and `STRIPE_WEBHOOK_SECRET`.\n\nOn a `checkout.session.completed` event the user identified by\n`session.metadata.user_id` is upgraded to premium:\n- `premium` → `true`\n- `payment_date` → current UTC timestamp\n- `payment_expires` → `payment_date + 1 month`\n\nAny other event type is silently acknowledged with 200.\n","security":[],"parameters":[{"name":"Stripe-Signature","in":"header","required":true,"schema":{"type":"string"},"description":"HMAC signature provided by Stripe for payload verification"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"Raw Stripe event object (see Stripe docs)"}}}},"responses":{"200":{"description":"Event acknowledged"},"400":{"description":"Invalid payload or signature verification failed"}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"UserRegistrationRequest":{"type":"object","required":["username","email","password"],"properties":{"username":{"type":"string"},"email":{"type":"string","format":"email"},"password":{"type":"string","format":"password"}}},"User":{"type":"object","properties":{"id":{"type":"integer"},"username":{"type":"string"},"email":{"type":"string","format":"email"}},"required":["id","username","email"]},"UserInfo":{"allOf":[{"$ref":"#/components/schemas/User"},{"type":"object","properties":{"first_name":{"type":"string"},"last_name":{"type":"string"},"provider":{"type":"string","description":"Authentication provider identifier (`google`, `twitter`, or `local`)."},"avatar":{"type":"string","format":"uri","nullable":true},"premium":{"type":"boolean","default":false},"payment_date":{"type":"string","nullable":true,"example":"12:00PM Apr 20, 2026","description":"Formatted as `%I:%M%p %b %d, %Y`"},"payment_expires":{"type":"string","nullable":true,"example":"12:00PM May 20, 2026","description":"Formatted as `%I:%M%p %b %d, %Y`"}}}]},"TokenObtainRequest":{"type":"object","required":["username","password"],"properties":{"username":{"type":"string"},"password":{"type":"string","format":"password"}}},"TokenPair":{"type":"object","required":["refresh","access"],"properties":{"refresh":{"type":"string"},"access":{"type":"string"}}},"TokenRefreshRequest":{"type":"object","required":["refresh"],"properties":{"refresh":{"type":"string"}}},"TokenRefreshResponse":{"type":"object","required":["access"],"properties":{"access":{"type":"string"}}},"ResetPasswordRequest":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email"}}},"SetPasswordRequest":{"type":"object","required":["uid","key","password"],"properties":{"uid":{"type":"string","description":"Base36-encoded user primary key"},"key":{"type":"string","description":"Password reset token generated by allauth"},"password":{"type":"string","format":"password","minLength":4,"description":"New password (must be longer than 3 characters)"}}},"UpcomingEvent":{"type":"object","properties":{"id":{"type":"integer"},"active":{"type":"boolean"},"dateTime":{"type":"string","example":"April 12, 2026"},"day":{"type":"string","example":"April 12, 2026"},"eventId":{"type":"integer"},"leagueId":{"type":"integer"},"name":{"type":"string"},"season":{"type":"integer"},"shortName":{"type":"string"},"status":{"type":"string"}},"required":["id","active","dateTime","day","eventId","leagueId","name","season","shortName","status"]},"Fighter":{"type":"object","properties":{"fighter_id":{"type":"integer"},"first_name":{"type":"string"},"last_name":{"type":"string"},"nickname":{"type":"string","nullable":true},"weight_class":{"type":"string"},"birth_date":{"type":"string","format":"date-time","nullable":true},"height":{"type":"number","nullable":true},"weight":{"type":"number","nullable":true},"reach":{"type":"number","nullable":true},"wins":{"type":"integer"},"losses":{"type":"integer"},"draws":{"type":"integer"},"no_contests":{"type":"integer"},"technical_knockouts":{"type":"integer"},"technical_knockout_losses":{"type":"integer"},"submissions":{"type":"integer"},"submission_losses":{"type":"integer"},"title_wins":{"type":"integer"},"title_losses":{"type":"integer"},"title_draws":{"type":"integer"},"sig_strikes_landed_per_minute":{"type":"number"},"sig_strike_accuracy":{"type":"number"},"takedown_average":{"type":"number"},"submission_average":{"type":"number"},"knockout_percentage":{"type":"number"},"technical_knockout_percentage":{"type":"number"},"decision_percentage":{"type":"number"},"imageURL":{"type":"string","format":"uri","nullable":true}},"required":["fighter_id","first_name","last_name"]},"FightCardFighter":{"type":"object","properties":{"fighter":{"$ref":"#/components/schemas/Fighter"}},"required":["fighter"]},"FightCard":{"type":"object","properties":{"fight_id":{"type":"integer"},"weight_class":{"type":"string"},"status":{"type":"string"},"event":{"type":"integer","description":"FK to UpcomingEventsModel (eventId)"},"day":{"type":"string","nullable":true,"example":"April 12, 2026","description":"Formatted as `%B %d, %Y` from the event's `day` field"},"fighters":{"type":"array","items":{"$ref":"#/components/schemas/FightCardFighter"}}},"required":["fight_id","weight_class","status","event","day","fighters"]},"AiFighterAnalysis":{"type":"object","properties":{"name":{"type":"string","description":"Fighter's full name"},"Advantages":{"type":"string","description":"Model's assessment of the fighter's strengths"},"Disadvantages":{"type":"string","description":"Model's assessment of the fighter's weaknesses"},"Performance Index":{"type":"integer","minimum":0,"maximum":100,"description":"Model-assigned overall performance score"}},"required":["name","Advantages","Disadvantages","Performance Index"]},"AiWinner":{"type":"object","properties":{"name":{"type":"string","description":"Full name of the predicted winner"},"factor":{"type":"string","description":"Predicted finishing method (e.g. \"KO\", \"TKO round 2\", \"points\")"},"comment":{"type":"string","description":"Model's reasoning for the prediction"},"points":{"type":"integer","nullable":true,"description":"Optional scoring margin when factor is points-based"}},"required":["name","factor","comment"]},"AiModelAnalysis":{"type":"object","properties":{"fighter_1":{"$ref":"#/components/schemas/AiFighterAnalysis"},"fighter_2":{"$ref":"#/components/schemas/AiFighterAnalysis"},"winner":{"$ref":"#/components/schemas/AiWinner"}},"required":["fighter_1","fighter_2","winner"]},"AiModelResult":{"type":"object","properties":{"analysis":{"$ref":"#/components/schemas/AiModelAnalysis"}},"required":["analysis"]},"AiChatter":{"type":"object","description":"Keyed by model identifier. All four keys are always present.\n","properties":{"qwen/qwen3-32b":{"$ref":"#/components/schemas/AiModelResult"},"llama-3.1-8b-instant":{"$ref":"#/components/schemas/AiModelResult"},"llama-3.3-70b-versatile":{"$ref":"#/components/schemas/AiModelResult"},"meta-llama/llama-4-scout-17b-16e-instruct":{"$ref":"#/components/schemas/AiModelResult"}},"required":["qwen/qwen3-32b","llama-3.1-8b-instant","llama-3.3-70b-versatile","meta-llama/llama-4-scout-17b-16e-instruct"]},"AisResponse":{"type":"object","properties":{"id":{"type":"integer"},"event":{"type":"integer","description":"FK to UpcomingEventsModel (eventId)"},"chatter":{"$ref":"#/components/schemas/AiChatter"}},"required":["id","event","chatter"]},"Error":{"type":"object","properties":{"detail":{"type":"string"}},"required":["detail"]}}},"security":[{"bearerAuth":[]}]}