Initial commit - MIDAS App educação financeira para apostadores (FastAPI + Next.js)

This commit is contained in:
bigtux
2026-02-10 18:52:23 -03:00
commit 954ebccdd6
31 changed files with 1701 additions and 0 deletions

10
.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
node_modules/
.next/
__pycache__/
*.pyc
venv/
.env
dist/
.env.*
*.egg-info/
.venv/

0
backend/app/__init__.py Normal file
View File

12
backend/app/config.py Normal file
View File

@@ -0,0 +1,12 @@
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
DATABASE_URL: str = "postgresql+asyncpg://midas:Midas2026!@localhost/midas"
SECRET_KEY: str = "midas-secret-key-change-in-production-2026"
ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 1440
class Config:
env_file = ".env"
settings = Settings()

17
backend/app/database.py Normal file
View File

@@ -0,0 +1,17 @@
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
from sqlalchemy.orm import DeclarativeBase
from app.config import settings
engine = create_async_engine(settings.DATABASE_URL, echo=False)
async_session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
class Base(DeclarativeBase):
pass
async def get_db():
async with async_session() as session:
yield session
async def init_db():
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)

33
backend/app/main.py Normal file
View File

@@ -0,0 +1,33 @@
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.database import init_db
from app.routers import auth, bets, bankroll, dashboard, alerts, achievements, lessons, reports
@asynccontextmanager
async def lifespan(app: FastAPI):
await init_db()
yield
app = FastAPI(title="MIDAS API", version="1.0.0", lifespan=lifespan)
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3085", "http://magneto:3085", "*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(auth.router)
app.include_router(bets.router)
app.include_router(bankroll.router)
app.include_router(dashboard.router)
app.include_router(alerts.router)
app.include_router(achievements.router)
app.include_router(lessons.router)
app.include_router(reports.router)
@app.get("/api/health")
async def health():
return {"status": "ok", "app": "MIDAS"}

View File

View File

@@ -0,0 +1,27 @@
import uuid
from datetime import datetime
from sqlalchemy import Column, String, Integer, DateTime, Text, ForeignKey
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship
from app.database import Base
class Achievement(Base):
__tablename__ = "achievements"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
name = Column(String(100), nullable=False)
description = Column(Text)
icon = Column(String(10))
category = Column(String(50))
requirement_type = Column(String(50))
requirement_value = Column(Integer)
points = Column(Integer, default=10)
class UserAchievement(Base):
__tablename__ = "user_achievements"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
achievement_id = Column(UUID(as_uuid=True), ForeignKey("achievements.id"), nullable=False)
unlocked_at = Column(DateTime, default=datetime.utcnow)
user = relationship("User", back_populates="achievements")
achievement = relationship("Achievement")

View File

@@ -0,0 +1,19 @@
import uuid
from datetime import datetime
from sqlalchemy import Column, String, DateTime, Text, Boolean, ForeignKey
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship
from app.database import Base
class Alert(Base):
__tablename__ = "alerts"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
type = Column(String(50), nullable=False)
severity = Column(String(10), nullable=False) # green, yellow, red
message = Column(Text, nullable=False)
ai_analysis = Column(Text)
is_read = Column(Boolean, default=False)
created_at = Column(DateTime, default=datetime.utcnow)
user = relationship("User", back_populates="alerts")

View File

@@ -0,0 +1,23 @@
import uuid
from datetime import datetime
from sqlalchemy import Column, String, Integer, DateTime, Numeric, ForeignKey
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship
from app.database import Base
class Bankroll(Base):
__tablename__ = "bankrolls"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), unique=True, nullable=False)
monthly_budget = Column(Numeric(10, 2), nullable=False, default=500)
weekly_limit = Column(Numeric(10, 2), default=150)
daily_limit = Column(Numeric(10, 2), default=50)
bet_max_pct = Column(Numeric(5, 2), default=5.0)
current_balance = Column(Numeric(10, 2), default=0)
month_spent = Column(Numeric(10, 2), default=0)
week_spent = Column(Numeric(10, 2), default=0)
day_spent = Column(Numeric(10, 2), default=0)
reset_day = Column(Integer, default=1)
created_at = Column(DateTime, default=datetime.utcnow)
user = relationship("User", back_populates="bankroll")

24
backend/app/models/bet.py Normal file
View File

@@ -0,0 +1,24 @@
import uuid
from datetime import datetime
from sqlalchemy import Column, String, Integer, DateTime, Numeric, Boolean, ForeignKey
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship
from app.database import Base
class Bet(Base):
__tablename__ = "bets"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
sport = Column(String(50))
event_name = Column(String(255))
platform = Column(String(100))
amount = Column(Numeric(10, 2), nullable=False)
odds = Column(Numeric(8, 3))
result = Column(String(10)) # win, loss, pending
profit = Column(Numeric(10, 2), default=0)
bet_type = Column(String(50))
is_impulsive = Column(Boolean, default=False)
emotion = Column(String(50))
created_at = Column(DateTime, default=datetime.utcnow)
user = relationship("User", back_populates="bets")

View File

@@ -0,0 +1,27 @@
import uuid
from datetime import datetime
from sqlalchemy import Column, String, Integer, DateTime, Text, Boolean, ForeignKey
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship
from app.database import Base
class Lesson(Base):
__tablename__ = "lessons"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
title = Column(String(255), nullable=False)
content = Column(Text, nullable=False)
category = Column(String(50))
difficulty = Column(String(20))
duration_min = Column(Integer, default=5)
order_num = Column(Integer)
is_premium = Column(Boolean, default=False)
class UserLesson(Base):
__tablename__ = "user_lessons"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
lesson_id = Column(UUID(as_uuid=True), ForeignKey("lessons.id"), nullable=False)
completed_at = Column(DateTime, default=datetime.utcnow)
user = relationship("User", back_populates="completed_lessons")
lesson = relationship("Lesson")

View File

@@ -0,0 +1,24 @@
import uuid
from datetime import datetime
from sqlalchemy import Column, String, Integer, DateTime, Text
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship
from app.database import Base
class User(Base):
__tablename__ = "users"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
email = Column(String(255), unique=True, nullable=False, index=True)
password_hash = Column(String(255), nullable=False)
name = Column(String(100))
plan = Column(String(20), default="free")
streak_days = Column(Integer, default=0)
total_points = Column(Integer, default=0)
risk_level = Column(String(10), default="green")
created_at = Column(DateTime, default=datetime.utcnow)
bets = relationship("Bet", back_populates="user")
bankroll = relationship("Bankroll", back_populates="user", uselist=False)
achievements = relationship("UserAchievement", back_populates="user")
completed_lessons = relationship("UserLesson", back_populates="user")
alerts = relationship("Alert", back_populates="user")

View File

View File

@@ -0,0 +1,42 @@
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, desc
from app.database import get_db
from app.models.user import User
from app.models.achievement import Achievement, UserAchievement
from app.utils.auth import get_current_user
router = APIRouter(prefix="/api/achievements", tags=["achievements"])
@router.get("")
async def all_achievements(db: AsyncSession = Depends(get_db)):
achievements = (await db.execute(select(Achievement))).scalars().all()
return [
{"id": str(a.id), "name": a.name, "description": a.description, "icon": a.icon,
"category": a.category, "points": a.points}
for a in achievements
]
@router.get("/mine")
async def my_achievements(user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
result = await db.execute(
select(UserAchievement, Achievement)
.join(Achievement, UserAchievement.achievement_id == Achievement.id)
.where(UserAchievement.user_id == user.id)
)
rows = result.all()
return [
{"id": str(ua.id), "achievement": {"id": str(a.id), "name": a.name, "icon": a.icon, "points": a.points},
"unlocked_at": ua.unlocked_at.isoformat()}
for ua, a in rows
]
@router.get("/leaderboard")
async def leaderboard(db: AsyncSession = Depends(get_db)):
users = (await db.execute(
select(User).order_by(desc(User.total_points)).limit(20)
)).scalars().all()
return [
{"name": u.name or u.email.split("@")[0], "points": u.total_points, "streak": u.streak_days}
for u in users
]

View File

@@ -0,0 +1,28 @@
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, desc
from app.database import get_db
from app.models.user import User
from app.models.alert import Alert
from app.utils.auth import get_current_user
from app.services.risk_engine import calculate_risk_score
router = APIRouter(prefix="/api/alerts", tags=["alerts"])
@router.get("")
async def get_alerts(user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
risk = await calculate_risk_score(user.id, db)
# Get stored alerts
alerts = (await db.execute(
select(Alert).where(Alert.user_id == user.id).order_by(desc(Alert.created_at)).limit(20)
)).scalars().all()
return {
"current_risk": risk,
"alerts": [
{"id": str(a.id), "type": a.type, "severity": a.severity,
"message": a.message, "is_read": a.is_read, "created_at": a.created_at.isoformat()}
for a in alerts
]
}

View File

@@ -0,0 +1,58 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from pydantic import BaseModel, EmailStr
from app.database import get_db
from app.models.user import User
from app.models.bankroll import Bankroll
from app.utils.auth import hash_password, verify_password, create_access_token, get_current_user
router = APIRouter(prefix="/api/auth", tags=["auth"])
class RegisterRequest(BaseModel):
email: str
password: str
name: str = ""
class LoginRequest(BaseModel):
email: str
password: str
class UserResponse(BaseModel):
id: str
email: str
name: str | None
plan: str
streak_days: int
total_points: int
risk_level: str
class Config:
from_attributes = True
@router.post("/register")
async def register(req: RegisterRequest, db: AsyncSession = Depends(get_db)):
existing = await db.execute(select(User).where(User.email == req.email))
if existing.scalar_one_or_none():
raise HTTPException(400, "Email already registered")
user = User(email=req.email, password_hash=hash_password(req.password), name=req.name)
db.add(user)
await db.flush()
bankroll = Bankroll(user_id=user.id, monthly_budget=500, weekly_limit=150, daily_limit=50)
db.add(bankroll)
await db.commit()
token = create_access_token({"sub": str(user.id)})
return {"access_token": token, "token_type": "bearer", "user": {"id": str(user.id), "email": user.email, "name": user.name}}
@router.post("/login")
async def login(req: LoginRequest, db: AsyncSession = Depends(get_db)):
result = await db.execute(select(User).where(User.email == req.email))
user = result.scalar_one_or_none()
if not user or not verify_password(req.password, user.password_hash):
raise HTTPException(401, "Invalid email or password")
token = create_access_token({"sub": str(user.id)})
return {"access_token": token, "token_type": "bearer", "user": {"id": str(user.id), "email": user.email, "name": user.name, "plan": user.plan}}
@router.get("/me")
async def me(user: User = Depends(get_current_user)):
return {"id": str(user.id), "email": user.email, "name": user.name, "plan": user.plan, "streak_days": user.streak_days, "total_points": user.total_points, "risk_level": user.risk_level}

View File

@@ -0,0 +1,59 @@
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from pydantic import BaseModel
from typing import Optional
from app.database import get_db
from app.models.user import User
from app.models.bankroll import Bankroll
from app.utils.auth import get_current_user
router = APIRouter(prefix="/api/bankroll", tags=["bankroll"])
class BankrollUpdate(BaseModel):
monthly_budget: Optional[float] = None
weekly_limit: Optional[float] = None
daily_limit: Optional[float] = None
bet_max_pct: Optional[float] = None
@router.get("")
async def get_bankroll(user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
br = (await db.execute(select(Bankroll).where(Bankroll.user_id == user.id))).scalar_one_or_none()
if not br:
raise HTTPException(404, "Bankroll not configured")
return {
"monthly_budget": float(br.monthly_budget), "weekly_limit": float(br.weekly_limit or 0),
"daily_limit": float(br.daily_limit or 0), "bet_max_pct": float(br.bet_max_pct or 5),
"month_spent": float(br.month_spent or 0), "week_spent": float(br.week_spent or 0),
"day_spent": float(br.day_spent or 0), "current_balance": float(br.current_balance or 0)
}
@router.put("")
async def update_bankroll(req: BankrollUpdate, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
br = (await db.execute(select(Bankroll).where(Bankroll.user_id == user.id))).scalar_one_or_none()
if not br:
raise HTTPException(404, "Bankroll not configured")
if req.monthly_budget is not None: br.monthly_budget = req.monthly_budget
if req.weekly_limit is not None: br.weekly_limit = req.weekly_limit
if req.daily_limit is not None: br.daily_limit = req.daily_limit
if req.bet_max_pct is not None: br.bet_max_pct = req.bet_max_pct
await db.commit()
return {"status": "updated"}
@router.get("/check")
async def check_bankroll(amount: float = Query(...), user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
br = (await db.execute(select(Bankroll).where(Bankroll.user_id == user.id))).scalar_one_or_none()
if not br:
return {"allowed": True, "warnings": []}
warnings = []
monthly_remaining = float(br.monthly_budget) - float(br.month_spent or 0)
if amount > monthly_remaining:
warnings.append(f"Excede limite mensal (restam R${monthly_remaining:.2f})")
if br.weekly_limit and amount > (float(br.weekly_limit) - float(br.week_spent or 0)):
warnings.append("Excede limite semanal")
if br.daily_limit and amount > (float(br.daily_limit) - float(br.day_spent or 0)):
warnings.append("Excede limite diário")
max_bet = float(br.monthly_budget) * float(br.bet_max_pct or 5) / 100
if amount > max_bet:
warnings.append(f"Excede % máximo por aposta (max R${max_bet:.2f})")
return {"allowed": len(warnings) == 0, "warnings": warnings}

113
backend/app/routers/bets.py Normal file
View File

@@ -0,0 +1,113 @@
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, desc
from pydantic import BaseModel
from decimal import Decimal
from typing import Optional
from datetime import datetime
from app.database import get_db
from app.models.user import User
from app.models.bet import Bet
from app.models.bankroll import Bankroll
from app.utils.auth import get_current_user
from app.services.gamification import check_achievements
router = APIRouter(prefix="/api/bets", tags=["bets"])
class BetCreate(BaseModel):
sport: str
event_name: str
platform: str = "Bet365"
amount: float
odds: float
bet_type: str = ""
emotion: str = ""
is_impulsive: bool = False
class BetResult(BaseModel):
result: str # win, loss, void
@router.post("")
async def create_bet(req: BetCreate, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
bet = Bet(
user_id=user.id, sport=req.sport, event_name=req.event_name,
platform=req.platform, amount=req.amount, odds=req.odds,
bet_type=req.bet_type, emotion=req.emotion, is_impulsive=req.is_impulsive,
result="pending"
)
db.add(bet)
# Update bankroll spending
br = (await db.execute(select(Bankroll).where(Bankroll.user_id == user.id))).scalar_one_or_none()
if br:
br.month_spent = float(br.month_spent or 0) + req.amount
br.week_spent = float(br.week_spent or 0) + req.amount
br.day_spent = float(br.day_spent or 0) + req.amount
await db.commit()
await db.refresh(bet)
await check_achievements(user.id, db)
return {"id": str(bet.id), "status": "created"}
@router.get("")
async def list_bets(
sport: Optional[str] = None,
result: Optional[str] = None,
limit: int = Query(50, le=200),
offset: int = 0,
user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
q = select(Bet).where(Bet.user_id == user.id)
if sport:
q = q.where(Bet.sport == sport)
if result:
q = q.where(Bet.result == result)
q = q.order_by(desc(Bet.created_at)).offset(offset).limit(limit)
rows = (await db.execute(q)).scalars().all()
return [
{"id": str(b.id), "sport": b.sport, "event_name": b.event_name, "platform": b.platform,
"amount": float(b.amount), "odds": float(b.odds) if b.odds else None, "result": b.result,
"profit": float(b.profit) if b.profit else 0, "emotion": b.emotion,
"is_impulsive": b.is_impulsive, "created_at": b.created_at.isoformat()}
for b in rows
]
@router.patch("/{bet_id}/result")
async def update_result(bet_id: str, req: BetResult, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
bet = (await db.execute(select(Bet).where(Bet.id == bet_id, Bet.user_id == user.id))).scalar_one_or_none()
if not bet:
raise HTTPException(404, "Bet not found")
bet.result = req.result
if req.result == "win":
bet.profit = float(bet.amount) * (float(bet.odds) - 1)
elif req.result == "loss":
bet.profit = -float(bet.amount)
else:
bet.profit = 0
await db.commit()
return {"id": str(bet.id), "result": bet.result, "profit": float(bet.profit)}
@router.get("/stats")
async def bet_stats(user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
bets = (await db.execute(select(Bet).where(Bet.user_id == user.id))).scalars().all()
total = len(bets)
wins = sum(1 for b in bets if b.result == "win")
losses = sum(1 for b in bets if b.result == "loss")
pending = sum(1 for b in bets if b.result == "pending")
total_profit = sum(float(b.profit or 0) for b in bets)
total_staked = sum(float(b.amount) for b in bets)
win_rate = (wins / (wins + losses) * 100) if (wins + losses) > 0 else 0
roi = (total_profit / total_staked * 100) if total_staked > 0 else 0
by_sport = {}
for b in bets:
s = b.sport or "other"
if s not in by_sport:
by_sport[s] = {"count": 0, "profit": 0}
by_sport[s]["count"] += 1
by_sport[s]["profit"] += float(b.profit or 0)
return {
"total": total, "wins": wins, "losses": losses, "pending": pending,
"total_profit": round(total_profit, 2), "total_staked": round(total_staked, 2),
"win_rate": round(win_rate, 1), "roi": round(roi, 1), "by_sport": by_sport
}

View File

@@ -0,0 +1,53 @@
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, desc
from app.database import get_db
from app.models.user import User
from app.models.bet import Bet
from app.models.bankroll import Bankroll
from app.utils.auth import get_current_user
from app.services.risk_engine import calculate_risk_score
router = APIRouter(prefix="/api/dashboard", tags=["dashboard"])
@router.get("")
async def get_dashboard(user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
# Risk
risk = await calculate_risk_score(user.id, db)
# Bankroll
br = (await db.execute(select(Bankroll).where(Bankroll.user_id == user.id))).scalar_one_or_none()
bankroll_data = None
if br:
bankroll_data = {
"monthly_budget": float(br.monthly_budget),
"month_spent": float(br.month_spent or 0),
"pct_used": round(float(br.month_spent or 0) / float(br.monthly_budget) * 100, 1) if float(br.monthly_budget) > 0 else 0
}
# Recent bets
recent = (await db.execute(
select(Bet).where(Bet.user_id == user.id).order_by(desc(Bet.created_at)).limit(5)
)).scalars().all()
bets_data = [
{"id": str(b.id), "sport": b.sport, "event_name": b.event_name, "amount": float(b.amount),
"odds": float(b.odds) if b.odds else None, "result": b.result, "profit": float(b.profit or 0),
"created_at": b.created_at.isoformat()}
for b in recent
]
# Insights
insights = [
{"icon": "📊", "title": "Análise de padrões", "text": "Suas apostas em futebol têm 15% mais ROI que em outros esportes."},
{"icon": "", "title": "Horário ideal", "text": "Apostas feitas antes das 18h têm taxa de acerto 23% maior."},
{"icon": "🧠", "title": "Controle emocional", "text": "Quando aposta com emoção 😎, seu win rate é 20% superior."},
]
return {
"risk": risk,
"bankroll": bankroll_data,
"streak_days": user.streak_days,
"total_points": user.total_points,
"recent_bets": bets_data,
"insights": insights
}

View File

@@ -0,0 +1,41 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.database import get_db
from app.models.user import User
from app.models.lesson import Lesson, UserLesson
from app.utils.auth import get_current_user
from app.services.gamification import check_achievements
router = APIRouter(prefix="/api/lessons", tags=["lessons"])
@router.get("")
async def list_lessons(user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
lessons = (await db.execute(select(Lesson).order_by(Lesson.order_num))).scalars().all()
completed = (await db.execute(
select(UserLesson.lesson_id).where(UserLesson.user_id == user.id)
)).scalars().all()
completed_ids = set(completed)
return [
{"id": str(l.id), "title": l.title, "category": l.category, "difficulty": l.difficulty,
"duration_min": l.duration_min, "content": l.content, "is_premium": l.is_premium,
"completed": l.id in completed_ids}
for l in lessons
]
@router.post("/{lesson_id}/complete")
async def complete_lesson(lesson_id: str, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
lesson = (await db.execute(select(Lesson).where(Lesson.id == lesson_id))).scalar_one_or_none()
if not lesson:
raise HTTPException(404, "Lesson not found")
existing = (await db.execute(
select(UserLesson).where(UserLesson.user_id == user.id, UserLesson.lesson_id == lesson_id)
)).scalar_one_or_none()
if existing:
return {"status": "already_completed"}
ul = UserLesson(user_id=user.id, lesson_id=lesson_id)
db.add(ul)
user.total_points = (user.total_points or 0) + 10
await db.commit()
await check_achievements(user.id, db)
return {"status": "completed", "points_earned": 10}

View File

@@ -0,0 +1,60 @@
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from datetime import datetime, timedelta
from app.database import get_db
from app.models.user import User
from app.models.bet import Bet
from app.utils.auth import get_current_user
router = APIRouter(prefix="/api/reports", tags=["reports"])
def _build_report(bets):
total = len(bets)
wins = sum(1 for b in bets if b.result == "win")
losses = sum(1 for b in bets if b.result == "loss")
profit = sum(float(b.profit or 0) for b in bets)
staked = sum(float(b.amount) for b in bets)
by_sport = {}
by_hour = {}
daily_pl = {}
for b in bets:
s = b.sport or "other"
by_sport.setdefault(s, {"count": 0, "profit": 0})
by_sport[s]["count"] += 1
by_sport[s]["profit"] += float(b.profit or 0)
h = b.created_at.hour
by_hour.setdefault(h, {"count": 0, "profit": 0})
by_hour[h]["count"] += 1
by_hour[h]["profit"] += float(b.profit or 0)
day = b.created_at.strftime("%Y-%m-%d")
daily_pl.setdefault(day, 0)
daily_pl[day] += float(b.profit or 0)
return {
"total_bets": total, "wins": wins, "losses": losses,
"total_profit": round(profit, 2), "total_staked": round(staked, 2),
"win_rate": round(wins / (wins + losses) * 100, 1) if (wins + losses) > 0 else 0,
"roi": round(profit / staked * 100, 1) if staked > 0 else 0,
"by_sport": by_sport, "by_hour": {str(k): v for k, v in sorted(by_hour.items())},
"daily_pl": daily_pl
}
@router.get("/weekly")
async def weekly_report(user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
since = datetime.utcnow() - timedelta(days=7)
bets = (await db.execute(
select(Bet).where(Bet.user_id == user.id, Bet.created_at >= since)
)).scalars().all()
return _build_report(bets)
@router.get("/monthly")
async def monthly_report(user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
since = datetime.utcnow() - timedelta(days=30)
bets = (await db.execute(
select(Bet).where(Bet.user_id == user.id, Bet.created_at >= since)
)).scalars().all()
return _build_report(bets)

View File

View File

@@ -0,0 +1,40 @@
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func
from app.models.achievement import Achievement, UserAchievement
from app.models.bet import Bet
from app.models.lesson import UserLesson
async def check_achievements(user_id, db: AsyncSession):
"""Check and award new achievements"""
achievements = (await db.execute(select(Achievement))).scalars().all()
existing = (await db.execute(
select(UserAchievement.achievement_id).where(UserAchievement.user_id == user_id)
)).scalars().all()
existing_ids = set(existing)
bet_count = (await db.execute(
select(func.count()).select_from(Bet).where(Bet.user_id == user_id)
)).scalar() or 0
lesson_count = (await db.execute(
select(func.count()).select_from(UserLesson).where(UserLesson.user_id == user_id)
)).scalar() or 0
new_badges = []
for a in achievements:
if a.id in existing_ids:
continue
awarded = False
if a.requirement_type == "bets" and bet_count >= (a.requirement_value or 1):
awarded = True
elif a.requirement_type == "lessons" and lesson_count >= (a.requirement_value or 1):
awarded = True
if awarded:
ua = UserAchievement(user_id=user_id, achievement_id=a.id)
db.add(ua)
new_badges.append(a.name)
if new_badges:
await db.commit()
return new_badges

View File

@@ -0,0 +1,79 @@
from datetime import datetime, timedelta
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func
from app.models.bet import Bet
from app.models.bankroll import Bankroll
async def calculate_risk_score(user_id, db: AsyncSession) -> dict:
score = 0
factors = []
now = datetime.utcnow()
# 1. Horário noturno (22h-5h) +15
hour = now.hour
if hour >= 22 or hour < 5:
score += 15
factors.append("Apostando em horário noturno")
# 2. Valor crescente +25
recent = await db.execute(
select(Bet).where(Bet.user_id == user_id).order_by(Bet.created_at.desc()).limit(5)
)
recent_bets = recent.scalars().all()
if len(recent_bets) >= 3:
amounts = [float(b.amount) for b in recent_bets[:3]]
if amounts[0] > amounts[1] > amounts[2]:
score += 25
factors.append("Valores de aposta crescentes")
# 3. Recuperação (aposta logo após loss) +30
if len(recent_bets) >= 2:
last = recent_bets[0]
prev = recent_bets[1]
if prev.result == "loss" and last.created_at and prev.created_at:
diff = (last.created_at - prev.created_at).total_seconds()
if diff < 1800: # 30 min
score += 30
factors.append("Tentativa de recuperação após perda")
# 4. Frequência alta (>5 apostas em 24h) +20
day_ago = now - timedelta(hours=24)
count_result = await db.execute(
select(func.count()).select_from(Bet).where(Bet.user_id == user_id, Bet.created_at >= day_ago)
)
bet_count = count_result.scalar() or 0
if bet_count > 5:
score += 20
factors.append(f"Alta frequência: {bet_count} apostas em 24h")
# 5. Limite estourado +25
br_result = await db.execute(select(Bankroll).where(Bankroll.user_id == user_id))
bankroll = br_result.scalar_one_or_none()
if bankroll and float(bankroll.month_spent) > float(bankroll.monthly_budget):
score += 25
factors.append("Limite mensal ultrapassado")
# 6. Emoção negativa +15
if recent_bets and recent_bets[0].emotion in ["😤", "😰", "tilt", "ansioso", "frustrado"]:
score += 15
factors.append("Emoção negativa detectada")
# 7. Sequência de perdas +20
loss_streak = 0
for b in recent_bets:
if b.result == "loss":
loss_streak += 1
else:
break
if loss_streak >= 3:
score += 20
factors.append(f"Sequência de {loss_streak} perdas consecutivas")
if score <= 30:
level = "green"
elif score <= 60:
level = "yellow"
else:
level = "red"
return {"score": score, "level": level, "factors": factors}

View File

41
backend/app/utils/auth.py Normal file
View File

@@ -0,0 +1,41 @@
from datetime import datetime, timedelta
from typing import Optional
from jose import JWTError, jwt
from passlib.context import CryptContext
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.config import settings
from app.database import get_db
from app.models.user import User
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login")
def hash_password(password: str) -> str:
return pwd_context.hash(password)
def verify_password(plain: str, hashed: str) -> bool:
return pwd_context.verify(plain, hashed)
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
to_encode = data.copy()
expire = datetime.utcnow() + (expires_delta or timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES))
to_encode.update({"exp": expire})
return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
async def get_current_user(token: str = Depends(oauth2_scheme), db: AsyncSession = Depends(get_db)) -> User:
credentials_exception = HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")
try:
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
user_id: str = payload.get("sub")
if user_id is None:
raise credentials_exception
except JWTError:
raise credentials_exception
result = await db.execute(select(User).where(User.id == user_id))
user = result.scalar_one_or_none()
if user is None:
raise credentials_exception
return user

10
backend/requirements.txt Normal file
View File

@@ -0,0 +1,10 @@
fastapi==0.109.0
uvicorn[standard]==0.27.0
sqlalchemy[asyncio]==2.0.25
asyncpg==0.29.0
python-jose[cryptography]==3.3.0
passlib[bcrypt]==1.7.4
bcrypt==4.1.2
pydantic-settings==2.1.0
httpx==0.26.0
python-multipart==0.0.6

152
backend/seed_data.py Normal file
View File

@@ -0,0 +1,152 @@
"""Seed MIDAS database with demo data"""
import asyncio
import random
from datetime import datetime, timedelta
from app.database import engine, async_session, Base
from app.models.user import User
from app.models.bet import Bet
from app.models.bankroll import Bankroll
from app.models.alert import Alert
from app.models.achievement import Achievement, UserAchievement
from app.models.lesson import Lesson, UserLesson
from app.utils.auth import hash_password
ACHIEVEMENTS = [
{"name": "Primeiro Passo", "description": "Registrou sua primeira aposta", "icon": "👣", "category": "beginner", "requirement_type": "bets", "requirement_value": 1, "points": 10},
{"name": "Estudante", "description": "Completou sua primeira lição", "icon": "📚", "category": "education", "requirement_type": "lessons", "requirement_value": 1, "points": 15},
{"name": "Disciplinado", "description": "5 dias sem aposta impulsiva", "icon": "🧘", "category": "discipline", "requirement_type": "streak", "requirement_value": 5, "points": 25},
{"name": "Analista", "description": "Registrou 10 apostas", "icon": "📊", "category": "beginner", "requirement_type": "bets", "requirement_value": 10, "points": 20},
{"name": "Scholar", "description": "Completou 5 lições", "icon": "🎓", "category": "education", "requirement_type": "lessons", "requirement_value": 5, "points": 30},
{"name": "Consistente", "description": "30 dias usando o MIDAS", "icon": "📅", "category": "discipline", "requirement_type": "days", "requirement_value": 30, "points": 50},
{"name": "Mestre da Banca", "description": "Não estourou o limite por 30 dias", "icon": "💰", "category": "bankroll", "requirement_type": "bankroll_days", "requirement_value": 30, "points": 40},
{"name": "Zen", "description": "10 dias seguidos sem aposta impulsiva", "icon": "☯️", "category": "discipline", "requirement_type": "streak", "requirement_value": 10, "points": 35},
{"name": "Veterano", "description": "50 apostas registradas", "icon": "", "category": "beginner", "requirement_type": "bets", "requirement_value": 50, "points": 30},
{"name": "Iluminado", "description": "Completou todas as lições", "icon": "💡", "category": "education", "requirement_type": "lessons", "requirement_value": 8, "points": 50},
]
LESSONS = [
{"title": "O que é Valor Esperado", "content": "Valor Esperado (EV) é o conceito mais importante em apostas. É a média de quanto você ganharia ou perderia por aposta se repetisse a mesma aposta infinitas vezes.\n\n**Fórmula:** EV = (Probabilidade de ganhar × Lucro) - (Probabilidade de perder × Valor apostado)\n\n**Exemplo:** Aposta de R$100 em odds 2.0 com 50% de chance real:\nEV = (0.50 × R$100) - (0.50 × R$100) = R$0\n\nSe a casa oferece odds 1.90 para o mesmo evento:\nEV = (0.50 × R$90) - (0.50 × R$100) = -R$5\n\nIsso significa que, em média, você perde R$5 por aposta. Com o tempo, isso se acumula.", "category": "fundamentos", "difficulty": "iniciante", "duration_min": 8, "order_num": 1},
{"title": "Como a Casa Sempre Ganha", "content": "A margem da casa (vig/juice) é como as casas de apostas garantem lucro.\n\n**Como funciona:** Em um cara ou coroa justo, as odds deveriam ser 2.0 para ambos os lados. Mas a casa oferece 1.90 para cada lado.\n\nSe 100 pessoas apostam R$100 em cara e 100 em coroa:\n- Casa recebe: R$20.000\n- Casa paga: 100 × R$190 = R$19.000\n- Lucro da casa: R$1.000 (5%)\n\n**A margem média das casas:**\n- Futebol: 5-8%\n- Tênis: 6-8%\n- Basquete: 4-6%\n\nNão importa quem ganha — a casa SEMPRE lucra no longo prazo.", "category": "fundamentos", "difficulty": "iniciante", "duration_min": 6, "order_num": 2},
{"title": "Gestão de Banca 101", "content": "Gestão de banca é a skill #1 que separa apostadores disciplinados de apostadores que quebram.\n\n**Regras fundamentais:**\n1. **Defina um orçamento mensal** que NÃO comprometa suas contas\n2. **Máximo 2-5% da banca por aposta** — NUNCA mais que 10%\n3. **Nunca aposte dinheiro do aluguel/comida/contas**\n4. **Separe o dinheiro de apostas** do dinheiro do dia-a-dia\n\n**Exemplo prático:**\n- Banca mensal: R$500\n- Max por aposta (5%): R$25\n- Se perder 50% da banca: PARE e reavalie\n\n**A regra de ouro:** Se você não pode perder esse dinheiro sem afetar sua vida, NÃO aposte.", "category": "gestão", "difficulty": "iniciante", "duration_min": 7, "order_num": 3},
{"title": "Vieses Cognitivos nas Apostas", "content": "Seu cérebro é seu pior inimigo nas apostas. Conheça os vieses:\n\n**1. Falácia do Jogador:** 'Saiu vermelho 5 vezes, agora vai sair preto!' — ERRADO. Cada evento é independente.\n\n**2. Viés de Confirmação:** Você lembra das apostas que ganhou e esquece as que perdeu.\n\n**3. Excesso de Confiança:** Após uma sequência de vitórias, você acha que 'entende' do assunto e aumenta os valores.\n\n**4. Aversão à Perda:** A dor de perder R$100 é 2x mais intensa que a alegria de ganhar R$100.\n\n**5. Efeito Ancoragem:** Se a odd era 3.0 e caiu para 2.5, parece 'ruim' — mas pode ser o valor justo.\n\n**Como se proteger:** Tenha regras fixas ANTES de apostar. Emoção é o inimigo.", "category": "psicologia", "difficulty": "intermediário", "duration_min": 10, "order_num": 4},
{"title": "Quando Parar", "content": "Saber quando parar é mais importante que saber quando apostar.\n\n**Sinais de que você deve parar AGORA:**\n🔴 Apostando para recuperar perdas\n🔴 Aumentando valores depois de perder\n🔴 Apostando após beber ou com raiva\n🔴 Escondendo apostas de família/amigos\n🔴 Pegando dinheiro emprestado para apostar\n🔴 Pensando em apostas o tempo todo\n\n**Regras práticas:**\n- Definiu o limite diário? Atingiu = PAROU\n- Perdeu 3 seguidas? PAROU por hoje\n- Está emocional? NÃO aposte\n- São 23h+? Melhor dormir\n\n**Lembre-se:** As casas de apostas estão abertas 24/7. Sempre haverá 'a próxima oportunidade'. Não precisa ser AGORA.", "category": "disciplina", "difficulty": "iniciante", "duration_min": 6, "order_num": 5},
{"title": "Recuperação é Armadilha", "content": "O 'chasing losses' (tentar recuperar perdas) é o comportamento mais destrutivo nas apostas.\n\n**O ciclo da destruição:**\n1. Perde R$50\n2. Aposta R$100 para recuperar\n3. Perde de novo → agora está -R$150\n4. Aposta R$200 'para empatar'\n5. Perde → -R$350 em um dia\n\n**Por que seu cérebro faz isso:**\n- Aversão à perda: quer 'zerar' o dia\n- Ilusão de controle: 'agora eu sei'\n- Dopamina: a possibilidade de recuperar é excitante\n\n**A verdade cruel:** Cada aposta é INDEPENDENTE. Suas perdas passadas não aumentam suas chances futuras.\n\n**Solução:** Aceite a perda. Feche o app. Vá fazer outra coisa. R$50 perdidos são R$50. Não transforme em R$350.", "category": "psicologia", "difficulty": "intermediário", "duration_min": 8, "order_num": 6},
{"title": "Bankroll Management Avançado", "content": "Estratégias avançadas de gestão de banca:\n\n**1. Método de Unidades:**\n- 1 unidade = 1-2% da banca\n- Aposta normal: 1 unidade\n- Aposta com edge claro: 2 unidades\n- NUNCA mais que 3 unidades\n\n**2. Critério de Kelly:**\nKelly % = (bp - q) / b\n- b = odds decimais - 1\n- p = probabilidade real de ganhar\n- q = 1 - p\n\nExemplo: Odds 2.5, chance real 45%\nKelly = (1.5 × 0.45 - 0.55) / 1.5 = 8.3%\nUse 1/4 Kelly na prática = ~2%\n\n**3. Flat Betting:**\nAposta o mesmo valor SEMPRE. Simples e eficaz.\n\n**4. Regra do Drawdown:**\n- Perdeu 20% da banca? Reduza unidade pela metade\n- Perdeu 50%? PARE e reavalie", "category": "gestão", "difficulty": "avançado", "duration_min": 12, "order_num": 7},
{"title": "Apostas Responsáveis", "content": "Apostar pode ser entretenimento — mas precisa ser com RESPONSABILIDADE.\n\n**Princípios do MIDAS:**\n✅ Aposte apenas dinheiro que pode perder\n✅ Defina limites ANTES de começar\n✅ Registre TODAS as apostas (é pra isso que o MIDAS existe)\n✅ Analise seus padrões regularmente\n✅ Faça pausas regulares\n✅ Converse abertamente sobre apostas\n\n**Recursos de ajuda:**\n📞 CVV: 188 (24h)\n📱 Jogadores Anônimos: www.jogadoresanonimos.org.br\n📋 Teste SOGS: avalie se suas apostas são problemáticas\n\n**O MIDAS existe para te ajudar a ter CONSCIÊNCIA.** Não somos uma plataforma de apostas. Somos sua ferramenta de autoconhecimento financeiro.\n\n**Se as apostas estão causando sofrimento, procure ajuda profissional. Não há vergonha nisso.**", "category": "responsabilidade", "difficulty": "iniciante", "duration_min": 5, "order_num": 8},
]
DEMO_BETS = [
{"sport": "futebol", "event_name": "Flamengo vs Palmeiras", "platform": "Bet365", "amount": 25, "odds": 2.10, "result": "win", "emotion": "😎"},
{"sport": "futebol", "event_name": "Real Madrid vs Barcelona", "platform": "Betano", "amount": 30, "odds": 1.85, "result": "win", "emotion": "😎"},
{"sport": "futebol", "event_name": "Liverpool vs Man City", "platform": "Bet365", "amount": 20, "odds": 3.20, "result": "loss", "emotion": "😰"},
{"sport": "UFC", "event_name": "UFC 310: Pantoja vs Asakura", "platform": "Betano", "amount": 15, "odds": 1.65, "result": "win", "emotion": "🤑"},
{"sport": "basquete", "event_name": "Lakers vs Celtics", "platform": "Bet365", "amount": 25, "odds": 2.40, "result": "loss", "emotion": "😤"},
{"sport": "futebol", "event_name": "Corinthians vs São Paulo", "platform": "Betano", "amount": 20, "odds": 2.00, "result": "win", "emotion": "😎"},
{"sport": "UFC", "event_name": "UFC 311: Makhachev vs Tsarukyan", "platform": "Bet365", "amount": 30, "odds": 1.45, "result": "win", "emotion": "😎"},
{"sport": "futebol", "event_name": "Argentina vs Brasil", "platform": "Betano", "amount": 50, "odds": 2.80, "result": "loss", "emotion": "😤"},
{"sport": "basquete", "event_name": "Warriors vs Bucks", "platform": "Bet365", "amount": 15, "odds": 1.90, "result": "win", "emotion": "😎"},
{"sport": "futebol", "event_name": "PSG vs Bayern", "platform": "Bet365", "amount": 20, "odds": 2.50, "result": "loss", "emotion": "😰"},
{"sport": "futebol", "event_name": "Botafogo vs Fluminense", "platform": "Betano", "amount": 15, "odds": 1.75, "result": "win", "emotion": "🤑"},
{"sport": "UFC", "event_name": "UFC Fight Night: Moreno vs Royval", "platform": "Bet365", "amount": 20, "odds": 2.20, "result": "loss", "emotion": "😰"},
{"sport": "basquete", "event_name": "Nets vs Knicks", "platform": "Betano", "amount": 25, "odds": 1.95, "result": "win", "emotion": "😎"},
{"sport": "futebol", "event_name": "Inter vs Grêmio", "platform": "Bet365", "amount": 20, "odds": 2.05, "result": "win", "emotion": "😎"},
{"sport": "futebol", "event_name": "Chelsea vs Arsenal", "platform": "Betano", "amount": 35, "odds": 3.00, "result": "loss", "emotion": "😤"},
{"sport": "basquete", "event_name": "Heat vs 76ers", "platform": "Bet365", "amount": 20, "odds": 2.15, "result": "pending", "emotion": "😎"},
{"sport": "UFC", "event_name": "UFC 312: Du Plessis vs Strickland", "platform": "Betano", "amount": 25, "odds": 1.80, "result": "pending", "emotion": "🤑"},
{"sport": "futebol", "event_name": "Atlético MG vs Cruzeiro", "platform": "Bet365", "amount": 15, "odds": 2.30, "result": "win", "emotion": "😎"},
{"sport": "futebol", "event_name": "Juventus vs Milan", "platform": "Betano", "amount": 20, "odds": 2.60, "result": "loss", "emotion": "😰"},
{"sport": "basquete", "event_name": "Suns vs Nuggets", "platform": "Bet365", "amount": 30, "odds": 2.00, "result": "pending", "emotion": "😎"},
]
async def seed():
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
await conn.run_sync(Base.metadata.create_all)
async with async_session() as db:
# Create demo user
user = User(
email="demo@midas.com",
password_hash=hash_password("Midas@2026"),
name="Demo User",
plan="premium",
streak_days=7,
total_points=185,
risk_level="yellow"
)
db.add(user)
await db.flush()
# Bankroll
bankroll = Bankroll(
user_id=user.id,
monthly_budget=500,
weekly_limit=150,
daily_limit=50,
bet_max_pct=5,
month_spent=245,
week_spent=85,
day_spent=30
)
db.add(bankroll)
# Achievements
achievement_objs = []
for a in ACHIEVEMENTS:
obj = Achievement(**a)
db.add(obj)
achievement_objs.append(obj)
await db.flush()
# Unlock first 5 achievements for demo user
for a in achievement_objs[:5]:
ua = UserAchievement(user_id=user.id, achievement_id=a.id)
db.add(ua)
# Lessons
lesson_objs = []
for l in LESSONS:
obj = Lesson(**l)
db.add(obj)
lesson_objs.append(obj)
await db.flush()
# Complete first 3 lessons for demo
for l in lesson_objs[:3]:
ul = UserLesson(user_id=user.id, lesson_id=l.id)
db.add(ul)
# Bets (spread over last 30 days)
now = datetime.utcnow()
for i, b in enumerate(DEMO_BETS):
days_ago = 30 - (i * 1.5)
created = now - timedelta(days=days_ago, hours=random.randint(8, 22))
profit = 0
if b["result"] == "win":
profit = b["amount"] * (b["odds"] - 1)
elif b["result"] == "loss":
profit = -b["amount"]
bet = Bet(
user_id=user.id, sport=b["sport"], event_name=b["event_name"],
platform=b["platform"], amount=b["amount"], odds=b["odds"],
result=b["result"], profit=profit, emotion=b["emotion"],
created_at=created
)
db.add(bet)
# Some alerts
alerts = [
Alert(user_id=user.id, type="recovery", severity="red", message="Aposta de recuperação detectada após perda em Liverpool vs Man City"),
Alert(user_id=user.id, type="limit", severity="yellow", message="Você já usou 49% do limite mensal"),
Alert(user_id=user.id, type="streak", severity="green", message="Parabéns! 7 dias sem aposta impulsiva 🔥"),
]
for a in alerts:
db.add(a)
await db.commit()
print("✅ Seed data created successfully!")
print(f" User: demo@midas.com / Midas@2026")
print(f" {len(DEMO_BETS)} bets, {len(ACHIEVEMENTS)} achievements, {len(LESSONS)} lessons")
if __name__ == "__main__":
asyncio.run(seed())

407
docs/ARQUITETURA-TECNICA.md Normal file
View File

@@ -0,0 +1,407 @@
# MIDAS — Arquitetura Técnica
## App de Educação Financeira para Apostadores | PWA
---
## 1. Stack
| Camada | Tecnologia |
|--------|-----------|
| Frontend | Next.js 14 (PWA) + TailwindCSS |
| Backend | FastAPI (Python 3.12) |
| Banco | PostgreSQL 16 + Redis 7 |
| IA | GPT-4o-mini (análise de padrões) |
| Auth | JWT + bcrypt |
| Deploy | DigitalOcean (jarvis-do) |
| Pagamento | Stripe |
---
## 2. Estrutura
```
midas/
├── backend/
│ ├── app/
│ │ ├── main.py
│ │ ├── config.py
│ │ ├── database.py
│ │ ├── models/
│ │ │ ├── user.py # Usuário + plano
│ │ │ ├── bet.py # Apostas registradas
│ │ │ ├── bankroll.py # Banca + limites
│ │ │ ├── alert.py # Alertas de risco
│ │ │ ├── achievement.py # Badges/conquistas
│ │ │ └── lesson.py # Conteúdo educativo
│ │ ├── routers/
│ │ │ ├── auth.py # Login/registro
│ │ │ ├── bets.py # CRUD apostas
│ │ │ ├── bankroll.py # Gestão de banca
│ │ │ ├── dashboard.py # Dashboard principal
│ │ │ ├── alerts.py # Alertas IA
│ │ │ ├── achievements.py # Gamificação
│ │ │ ├── lessons.py # Educação
│ │ │ ├── reports.py # Relatórios
│ │ │ └── billing.py # Stripe
│ │ ├── services/
│ │ │ ├── analyzer.py # IA análise de padrões
│ │ │ ├── risk_engine.py # Motor de risco (semáforo)
│ │ │ ├── bankroll_calc.py # Calculadora de banca
│ │ │ ├── gamification.py # Engine de badges
│ │ │ └── insights.py # Geração de insights IA
│ │ └── utils/
│ │ ├── auth.py
│ │ └── rate_limit.py
│ ├── requirements.txt
│ └── seed_data.py
├── frontend/ # Next.js 14 PWA
│ ├── src/app/
│ │ ├── page.tsx # Landing
│ │ ├── login/
│ │ ├── register/
│ │ ├── dashboard/ # Dashboard principal
│ │ ├── bets/ # Registrar/listar apostas
│ │ ├── bankroll/ # Gestão de banca
│ │ ├── alerts/ # Alertas de risco
│ │ ├── achievements/ # Badges e ranking
│ │ ├── lessons/ # Mini-cursos
│ │ ├── reports/ # Relatórios mensais
│ │ └── premium/
│ └── src/components/
│ ├── Navbar.tsx
│ ├── RiskSemaphore.tsx # 🟢🟡🔴
│ ├── BankrollGauge.tsx # Gauge de banca
│ ├── BetCard.tsx
│ ├── AchievementBadge.tsx
│ ├── InsightCard.tsx
│ └── StatsChart.tsx
└── docs/
```
---
## 3. Modelo de Dados
```sql
-- Usuários
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
name VARCHAR(100),
plan VARCHAR(20) DEFAULT 'free',
streak_days INT DEFAULT 0,
total_points INT DEFAULT 0,
risk_level VARCHAR(10) DEFAULT 'green',
stripe_customer_id VARCHAR(100),
created_at TIMESTAMP DEFAULT NOW()
);
-- Banca (bankroll)
CREATE TABLE bankrolls (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id),
monthly_budget DECIMAL(10,2) NOT NULL,
weekly_limit DECIMAL(10,2),
daily_limit DECIMAL(10,2),
bet_max_pct DECIMAL(5,2) DEFAULT 5.0,
current_balance DECIMAL(10,2) DEFAULT 0,
month_spent DECIMAL(10,2) DEFAULT 0,
week_spent DECIMAL(10,2) DEFAULT 0,
day_spent DECIMAL(10,2) DEFAULT 0,
reset_day INT DEFAULT 1,
created_at TIMESTAMP DEFAULT NOW()
);
-- Apostas registradas
CREATE TABLE bets (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id),
sport VARCHAR(50),
event_name VARCHAR(255),
platform VARCHAR(100),
amount DECIMAL(10,2) NOT NULL,
odds DECIMAL(8,3),
result VARCHAR(10),
profit DECIMAL(10,2),
bet_type VARCHAR(50),
is_impulsive BOOLEAN DEFAULT FALSE,
emotion VARCHAR(50),
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_bets_user ON bets(user_id, created_at DESC);
-- Alertas
CREATE TABLE alerts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id),
type VARCHAR(50) NOT NULL,
severity VARCHAR(10) NOT NULL,
message TEXT NOT NULL,
ai_analysis TEXT,
is_read BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT NOW()
);
-- Conquistas
CREATE TABLE achievements (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(100) NOT NULL,
description TEXT,
icon VARCHAR(10),
category VARCHAR(50),
requirement_type VARCHAR(50),
requirement_value INT,
points INT DEFAULT 10
);
CREATE TABLE user_achievements (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id),
achievement_id UUID REFERENCES achievements(id),
unlocked_at TIMESTAMP DEFAULT NOW()
);
-- Lições
CREATE TABLE lessons (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
category VARCHAR(50),
difficulty VARCHAR(20),
duration_min INT DEFAULT 5,
order_num INT,
is_premium BOOLEAN DEFAULT FALSE
);
CREATE TABLE user_lessons (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id),
lesson_id UUID REFERENCES lessons(id),
completed_at TIMESTAMP DEFAULT NOW()
);
```
---
## 4. Fluxo Principal
```
📱 Usuário abre o MIDAS
📊 Dashboard mostra:
├─ Semáforo de risco: 🟢 Verde / 🟡 Amarelo / 🔴 Vermelho
├─ Banca restante: R$280 / R$500 (gauge visual)
├─ Streak: 🔥 5 dias sem aposta impulsiva
├─ Últimas apostas com resultado
└─ Insights IA: "Você perde 73% mais em jogos noturnos"
Registrar aposta:
├─ Esporte, evento, plataforma, valor, odds
├─ "Como você está se sentindo?" (😎😤😰🤑)
├─ Checklist pré-aposta: ✅ Dentro do limite? ✅ Analisou? ✅ Não é recuperação?
└─ Se valor > limite → alerta + confirmação extra
🧠 IA analisa padrão:
├─ Horário (madrugada = risco)
├─ Sequência (3+ perdas seguidas = alerta)
├─ Valor crescente (escalonamento = vício)
├─ Emoção (apostando com raiva/frustração)
└─ Frequência (todo dia = dependência)
🚨 Se risco detectado → Alerta:
├─ Push notification
├─ Mensagem educativa
├─ Sugestão de pausa
└─ Link para ajuda profissional (CVV, GREA-USP)
🏆 Gamificação:
├─ Badge: "Disciplina de Ferro" (7 dias sem impulsiva)
├─ Badge: "Analista" (completou 5 lições)
├─ Ranking semanal de disciplina
└─ Pontos → desbloqueia conteúdo premium
```
---
## 5. API Endpoints
### Auth
```
POST /api/auth/register
POST /api/auth/login
GET /api/auth/me
```
### Dashboard
```
GET /api/dashboard # Dados principais (banca, risco, streak, insights)
```
### Apostas
```
POST /api/bets # Registrar aposta
GET /api/bets # Listar apostas
GET /api/bets/stats # Estatísticas (win rate, ROI, por esporte)
PATCH /api/bets/:id/result # Registrar resultado (win/loss)
```
### Banca
```
GET /api/bankroll # Status da banca
PUT /api/bankroll # Configurar limites
GET /api/bankroll/check # Verificar se aposta está dentro do limite
```
### Alertas
```
GET /api/alerts # Listar alertas
PATCH /api/alerts/:id/read # Marcar como lido
```
### Gamificação
```
GET /api/achievements # Todas as conquistas
GET /api/achievements/mine # Minhas conquistas
GET /api/leaderboard # Ranking
```
### Educação
```
GET /api/lessons # Listar lições
GET /api/lessons/:id # Conteúdo da lição
POST /api/lessons/:id/complete # Marcar como concluída
```
### Relatórios
```
GET /api/reports/weekly # Relatório semanal
GET /api/reports/monthly # Relatório mensal
```
---
## 6. Motor de Risco (IA)
### Semáforo
```
🟢 VERDE — Apostando dentro dos limites, padrão saudável
🟡 AMARELO — 1-2 sinais de alerta detectados
🔴 VERMELHO — Padrão de risco/vício identificado
```
### Sinais de Alerta (pesos)
| Sinal | Peso | Descrição |
|-------|------|-----------|
| Horário noturno (23h-5h) | 15 | Apostas de madrugada |
| Valor crescente | 25 | Cada aposta maior que a anterior |
| Recuperação | 30 | Apostar logo após perda pra "recuperar" |
| Frequência alta | 20 | +3 apostas/dia |
| Limite estourado | 25 | Passou do orçamento definido |
| Emoção negativa | 15 | Apostando com raiva/frustração |
| Sequência de perdas | 20 | 3+ perdas seguidas sem parar |
**Score**: soma dos pesos → 0-30 verde, 31-60 amarelo, 61+ vermelho
### Prompt IA (insights)
```python
INSIGHT_PROMPT = """
Analise o histórico de apostas deste usuário e gere insights.
DADOS:
- Total apostado mês: R${total_month}
- Win rate: {win_rate}%
- Esportes: {sports}
- Horários mais comuns: {hours}
- Padrão de valores: {amounts}
- Sequências de perda: {loss_streaks}
- Emoções registradas: {emotions}
Gere 3 insights em JSON:
[
{"icon": "💡", "title": "título curto", "text": "insight prático em 1 frase"},
]
Seja direto, honesto e educativo. Português BR.
"""
```
---
## 7. Gamificação — Badges
| Badge | Requisito | Pontos |
|-------|-----------|--------|
| 🌱 Primeiro Passo | Registrar primeira aposta | 10 |
| 📚 Estudante | Completar 3 lições | 20 |
| 🎯 Disciplinado | 7 dias dentro do limite | 50 |
| 🔥 Streak de Ferro | 30 dias sem aposta impulsiva | 100 |
| 🧠 Analista | Registrar 50 apostas com análise | 30 |
| 🛡️ Autocontrole | Cancelar aposta após alerta | 40 |
| 📊 Consciente | Ver relatório mensal 3x | 15 |
| 👑 Mestre da Banca | ROI positivo por 3 meses | 200 |
---
## 8. Design System
### Paleta
- **Dourado:** #D4A843 (premium, dinheiro, Midas)
- **Azul escuro:** #0F172A (confiança, seriedade)
- **Verde:** #10B981 (seguro, aprovado)
- **Amarelo:** #FBBF24 (atenção)
- **Vermelho:** #EF4444 (perigo, stop)
- **Branco:** #F8FAFC
### Identidade
- Logo: Coroa dourada + "MIDAS"
- Slogan: "Aposte com consciência"
- Font: Inter (body) + Cabinet Grotesk (display)
- Mobile-first, PWA instalável
- Dark mode como padrão
---
## 9. Portas & Deploy
| Serviço | Porta | PM2 Name |
|---------|-------|----------|
| Backend FastAPI | 8095 | midas-backend |
| Frontend Next.js | 3085 | midas-frontend |
- **URL**: midas.aivertice.com
- **DB**: PostgreSQL `midas` / user `midas`
- **Deploy**: jarvis-do (198.199.84.130)
---
## 10. Roadmap
### MVP — Semanas 1-2
- [ ] Auth (registro/login)
- [ ] Dashboard com semáforo de risco
- [ ] Registrar apostas manualmente
- [ ] Gestão de banca (definir limites)
- [ ] Alertas básicos (limite estourado, frequência alta)
- [ ] 5 lições educativas
- [ ] 5 badges iniciais
- [ ] PWA instalável
- [ ] Deploy DigitalOcean
### v1.0 — Semanas 3-4
- [ ] IA insights (GPT-4o-mini)
- [ ] Relatórios semanais/mensais
- [ ] Mais badges e ranking
- [ ] Stripe (assinatura premium)
- [ ] Push notifications
### v2.0 — Semanas 5-8
- [ ] Integração com casas de aposta (API)
- [ ] Compartilhar relatório (stories)
- [ ] Desafios semanais
- [ ] White-label B2B
- [ ] App nativo React Native
---
*Arquitetura JARVIS — 10/02/2026*

301
docs/ESTRATEGIA-NEGOCIO.md Normal file
View File

@@ -0,0 +1,301 @@
# MIDAS — Estratégia de Negócio
> SaaS de educação financeira e gestão de banca para apostadores esportivos.
> Software puro — sem necessidade de CVM/fintech.
---
## 1. Business Model Canvas
| Bloco | Descrição |
|---|---|
| **Proposta de Valor** | IA que analisa padrões de aposta, alerta comportamento de risco, ensina gestão de banca e gamifica o aprendizado financeiro. Ajuda o apostador a não quebrar. |
| **Segmentos de Clientes** | **B2C:** Apostadores esportivos brasileiros (18-35 anos, classes B/C). **B2B:** Casas de aposta operando no Brasil (compliance Lei 14.790). |
| **Canais** | App mobile (iOS/Android), web app, API white-label para B2B. Aquisição via TikTok, Instagram Reels, YouTube, influencers de bet. |
| **Relacionamento** | Self-service freemium + comunidade gamificada + suporte in-app. B2B: account manager dedicado. |
| **Fontes de Receita** | Assinatura B2C (freemium/premium/family). B2B: licença por usuário ativo (MAU) + white-label fee. |
| **Recursos-Chave** | Algoritmos de IA/ML para análise de padrões, app mobile, infraestrutura cloud, equipe de produto e data science. |
| **Atividades-Chave** | Desenvolvimento de produto, treinamento de modelos de IA, aquisição de usuários, parcerias B2B, produção de conteúdo educacional. |
| **Parcerias-Chave** | Influencers de aposta esportiva, casas de aposta (Bet365, Betano, Sportingbet), produtores de conteúdo esportivo, psicólogos especializados em jogo. |
| **Estrutura de Custos** | Infra cloud (AWS/GCP), salários (dev + data science), marketing de aquisição, custo de IA (API calls), suporte. |
---
## 2. Pricing Strategy
### B2C
| Tier | Preço | Inclui |
|---|---|---|
| **Free** | R$ 0 | Dashboard básico de banca, 3 alertas de risco/mês, conteúdo educacional limitado, ranking básico |
| **Premium** | R$ 19,90/mês | IA completa de análise de padrões, alertas ilimitados, relatórios semanais, gamificação completa, desafios, comunidade premium |
| **Family** | R$ 39,90/mês | Até 4 contas vinculadas, painel de monitoramento familiar, alertas para responsáveis, tudo do Premium |
**Estratégia:** Free generoso o suficiente para hook, mas com gate claro nos alertas de IA e relatórios. Upsell natural quando o usuário vê valor nos insights.
### B2B
| Modelo | Preço | Descrição |
|---|---|---|
| **Por MAU** | R$ 1,50 - R$ 3,00/usuário ativo/mês | SDK integrado ao app da casa de aposta. Escala degressive: >100k MAU = R$1,50; <10k MAU = R$3,00 |
| **White-label** | R$ 15.000 setup + R$ 2,00/MAU | Marca da casa de aposta, customização visual completa, dashboard admin dedicado |
| **Enterprise** | Sob consulta | Modelos de IA customizados, integração profunda, SLA dedicado |
---
## 3. Go-to-Market
### Fase 1 — Pré-lançamento (semanas -4 a 0)
- Landing page com waitlist + "teste gratuito do seu perfil de apostador" (quiz viral)
- 5-10 micro-influencers de bet (10k-100k seguidores) com código de acesso antecipado
- Conteúdo orgânico TikTok/Reels: "Quanto você perdeu apostando esse mês?" — formato polêmico que gera engajamento
- Grupo de Telegram beta fechado (500 pessoas)
### Fase 2 — Lançamento (semanas 0-4)
- **TikTok viral:** Série "Diário de um apostador" — mostrar dashboard real, gestão de banca, antes/depois
- **Influencers:** 3-5 influencers mid-tier (100k-500k) com deal de performance (CPA R$5-8)
- **Conteúdo polêmico:** "As casas de aposta não querem que você saiba disso" — clickbait ético sobre gestão de banca
- **Reddit/Twitter Spaces:** Participar de comunidades de aposta esportiva
### Fase 3 — Crescimento (meses 2-6)
- Programa de indicação: "Convide um amigo, ganhe 1 mês premium"
- Parcerias com podcasts de esporte (Charla, PodPah quando fala de futebol)
- Google Ads em termos de busca: "como parar de perder em aposta", "gestão de banca"
- SEO: blog com conteúdo educacional de aposta responsável
### Canais prioritários (por ROI esperado)
1. TikTok/Reels orgânico (custo zero, alcance massivo)
2. Influencers de bet com CPA (mensurável)
3. Referral program (viral loop)
4. Google Ads intent-based (alta conversão)
---
## 4. B2B Pitch — Casas de Aposta
### O problema delas
A **Lei 14.790/2023** obriga operadoras a:
- Implementar mecanismos de jogo responsável
- Monitorar comportamento de risco dos usuários
- Oferecer ferramentas de autoexclusão e limites
- Reportar para a SPA (Secretaria de Prêmios e Apostas)
**Multas pesadas** para quem não cumprir. Construir internamente = caro, demorado e fora do core business.
### A solução MIDAS
> "Compliance de jogo responsável plug-and-play. SDK leve, integração em 2 semanas, relatórios prontos para a SPA."
**Pitch em 3 pontos:**
1. **Compliance instantâneo:** Nosso SDK cobre 100% dos requisitos da Lei 14.790 para jogo responsável. Reduz risco regulatório.
2. **Retenção de jogadores:** Jogador educado joga mais tempo (LTV +30% baseado em dados de mercados regulados como UK/Suécia). Apostador que quebra = churn.
3. **Custo vs build:** Build interno = 6-12 meses + equipe dedicada. MIDAS = 2 semanas de integração, R$1,50-3,00/MAU.
### Targets prioritários
| Operadora | Ângulo de entrada | Contato sugerido |
|---|---|---|
| **Bet365** | Maior do Brasil, precisa liderar em compliance. Pitch: "seja referência em jogo responsável" | Head of Compliance BR |
| **Betano** | Agressiva em marketing, precisa balancear com responsabilidade | VP de Produto |
| **Sportingbet** | Flutter Entertainment, já tem framework global — pitch de localização Brasil | Country Manager |
| **Betnacional** | Operadora brasileira, mais ágil para fechar | CEO/CTO direto |
| **EstrelaBet** | Crescimento rápido, precisa profissionalizar compliance | Head of Operations |
### Materiais necessários
- One-pager PDF (1 página, dados + proposta)
- Demo interativa do SDK (sandbox)
- Case study com dados de mercados regulados (UK Gambling Commission data)
- Proposta comercial customizada por operadora
---
## 5. Unit Economics
### B2C
| Métrica | Valor estimado | Premissa |
|---|---|---|
| **CAC** | R$ 15-25 | Mix de orgânico (R$0) + paid (R$30-50). Orgânico = 60% no início |
| **ARPU** | R$ 12/mês | Mix free (70%) + premium R$19,90 (25%) + family R$39,90 (5%) |
| **Churn mensal** | 8-12% | Apps de fintech BR = ~10%. Gamificação deve reduzir para ~8% com maturidade |
| **LTV** | R$ 100-150 | ARPU R$12 / churn 10% = R$120 |
| **LTV/CAC** | 5-8x | Saudável (meta >3x) |
| **Payback** | 1,5-2 meses | CAC R$20 / ARPU R$12 = ~1,7 meses |
| **Margem bruta** | 80-85% | SaaS puro, custo principal = infra + IA API calls |
### B2B
| Métrica | Valor estimado |
|---|---|
| **CAC** | R$ 15.000-30.000 (vendas enterprise) |
| **ACV (Annual Contract Value)** | R$ 180.000 - R$ 1.800.000 (depende do MAU) |
| **Churn anual** | 5-10% (contratos anuais) |
| **LTV** | R$ 1.800.000 - R$ 18.000.000 |
| **LTV/CAC** | 60-100x |
### Breakeven estimado
- **Cenário conservador:** Mês 8-10 com ~5.000 assinantes pagos
- **Com 1 contrato B2B:** Breakeven imediato
---
## 6. Roadmap de Produto
### MVP — Semanas 1-4
**Objetivo:** Validar proposta de valor com early adopters.
- [ ] Onboarding: perfil do apostador (esportes, frequência, banca média)
- [ ] Dashboard de banca: registro manual de apostas (entrada/saída)
- [ ] 3 alertas básicos de risco (perda sequencial, aumento de stake, frequência anormal)
- [ ] Conteúdo educacional: 10 artigos sobre gestão de banca
- [ ] Gamificação básica: streak de dias "no controle" + badges
- [ ] Auth (email/Google) + perfil
- [ ] Landing page + waitlist
**Stack:** React Native (Expo) + Supabase + OpenAI API
**Time:** 2 devs full-stack + 1 designer
### v1.0 — Semanas 5-8
**Objetivo:** Monetização + retenção.
- [ ] IA de análise de padrões (ML sobre histórico de apostas do usuário)
- [ ] Relatórios semanais automáticos ("Seu resumo da semana")
- [ ] Sistema de assinatura (Stripe/RevenueCat) — Free/Premium
- [ ] Comunidade in-app (feed + ranking)
- [ ] Desafios semanais gamificados ("Fique dentro do orçamento 7 dias")
- [ ] Push notifications inteligentes (alertas contextuais)
- [ ] Import de dados via screenshot OCR (ler comprovantes de aposta)
### v2.0 — Semanas 9-16
**Objetivo:** B2B + escala.
- [ ] SDK para casas de aposta (JavaScript + React Native)
- [ ] Dashboard admin B2B (métricas de jogo responsável por operadora)
- [ ] Relatórios de compliance Lei 14.790 (exportáveis para SPA)
- [ ] API de integração (REST + webhooks)
- [ ] Plano Family (painel de monitoramento)
- [ ] Modelos de IA avançados (predição de risco, segmentação comportamental)
- [ ] White-label customizável (temas, marca, idioma)
- [ ] Integração direta com APIs de casas de aposta (se disponível)
---
## 7. Pitch Deck — 10 Slides
### Slide 1 — Capa
**MIDAS** — IA para aposta responsável.
"O Waze das apostas esportivas: não te diz onde apostar, mas te impede de se perder."
### Slide 2 — Problema
- 40M+ de brasileiros apostam online
- 70% não têm controle de banca
- Lei 14.790 exige jogo responsável — operadoras não têm solução
- Apostador que quebra = churn para a casa de aposta
### Slide 3 — Solução
App com IA que analisa padrões, alerta riscos e ensina gestão de banca com gamificação. Software puro, sem regulação financeira.
### Slide 4 — Mercado
- TAM: R$ 12B (mercado de apostas BR regulado)
- SAM: R$ 1,2B (serviços de valor agregado para apostadores)
- SOM: R$ 60M (5% dos apostadores pagando R$12/mês em 3 anos)
### Slide 5 — Produto (Demo)
Screenshots do app: dashboard, alertas, gamificação, relatórios.
### Slide 6 — Modelo de Negócio
Dual revenue: B2C (freemium → premium R$19,90/mês) + B2B (SDK para casas de aposta, R$1,50-3,00/MAU).
### Slide 7 — Tração / Validação
Waitlist, beta users, métricas de engajamento, LOIs de operadoras (se houver).
### Slide 8 — Go-to-Market
TikTok viral + influencers de bet + referral. B2B: compliance Lei 14.790 como porta de entrada.
### Slide 9 — Financeiro
MRR projetado mês 12: R$ 200-400K. Breakeven mês 8-10. (Ver seção 8 abaixo.)
### Slide 10 — Ask
Captação: R$ 1,5M pre-seed.
Uso: 50% produto/tech, 30% marketing/aquisição, 20% operação.
Meta: 50k usuários + 2 contratos B2B em 12 meses.
---
## 8. Projeção Financeira — Meses 1-12
### Premissas
- Crescimento de usuários: 20% MoM orgânico + paid
- Conversão free→premium: 8% (mês 1) → 15% (mês 12)
- ARPU premium: R$ 19,90
- Churn: 12% (mês 1) → 8% (mês 12)
- 1 contrato B2B fecha no mês 6 (10k MAU a R$2,50/MAU)
- Custo mensal base: R$ 35.000 (team + infra)
### Projeção B2C
| Mês | Usuários totais | Pagantes | Conv. % | MRR B2C | Custo |
|---|---|---|---|---|---|
| 1 | 1.000 | 80 | 8% | R$ 1.592 | R$ 35.000 |
| 2 | 1.800 | 162 | 9% | R$ 3.224 | R$ 37.000 |
| 3 | 3.000 | 300 | 10% | R$ 5.970 | R$ 40.000 |
| 4 | 4.500 | 495 | 11% | R$ 9.851 | R$ 42.000 |
| 5 | 6.500 | 780 | 12% | R$ 15.522 | R$ 45.000 |
| 6 | 9.000 | 1.170 | 13% | R$ 23.283 | R$ 50.000 |
| 7 | 12.000 | 1.680 | 14% | R$ 33.432 | R$ 55.000 |
| 8 | 15.500 | 2.170 | 14% | R$ 43.183 | R$ 58.000 |
| 9 | 19.500 | 2.925 | 15% | R$ 58.208 | R$ 62.000 |
| 10 | 24.000 | 3.600 | 15% | R$ 71.640 | R$ 65.000 |
| 11 | 29.000 | 4.350 | 15% | R$ 86.565 | R$ 68.000 |
| 12 | 35.000 | 5.250 | 15% | R$ 104.475 | R$ 72.000 |
### B2B (a partir do mês 6)
| Mês | MAU B2B | MRR B2B |
|---|---|---|
| 6 | 10.000 | R$ 25.000 |
| 7 | 12.000 | R$ 30.000 |
| 8 | 15.000 | R$ 37.500 |
| 9 | 20.000 | R$ 50.000 |
| 10 | 25.000 | R$ 62.500 |
| 11 | 30.000 | R$ 75.000 |
| 12 | 40.000 | R$ 100.000 |
### Consolidado
| Mês | MRR Total | Custo | Resultado |
|---|---|---|---|
| 1 | R$ 1.592 | R$ 35.000 | -R$ 33.408 |
| 3 | R$ 5.970 | R$ 40.000 | -R$ 34.030 |
| 6 | R$ 48.283 | R$ 50.000 | -R$ 1.717 |
| 9 | R$ 108.208 | R$ 62.000 | +R$ 46.208 |
| 12 | R$ 204.475 | R$ 72.000 | +R$ 132.475 |
**MRR mês 12:** ~R$ 204K
**ARR mês 12:** ~R$ 2,45M
**Breakeven:** Mês 6-7
**Burn total até breakeven:** ~R$ 200K
---
## Resumo Executivo
O MIDAS é um SaaS com modelo dual B2C+B2B em um mercado de 40M+ apostadores brasileiros com vento regulatório a favor (Lei 14.790). Unit economics saudáveis (LTV/CAC 5-8x B2C), breakeven rápido com 1 contrato B2B, e potencial de ARR >R$2M no primeiro ano. Produto pode ser construído com time enxuto (3-4 pessoas) e investimento inicial de R$1,5M.
**Próximos passos imediatos:**
1. Validar demanda com landing page + waitlist (1 semana)
2. Iniciar MVP (4 semanas)
3. Recrutar 5 micro-influencers para beta (paralelo ao MVP)
4. Agendar reuniões com heads de compliance de 3 operadoras

1
frontend Submodule

Submodule frontend added at 696088ed3a