🔗 WHOOP OAuth: criar o app
Para o coach ler a sua WHOOP, primeiro você cria um app de desenvolvedor no portal da WHOOP. Esse app dá ao HealthOS uma identidade (client id + secret) e diz exatamente para onde a WHOOP deve devolver você depois que você autorizar. Três decisões definem tudo: o redirect URI, os escopos e o offline.
🟢 Novo aqui? O que é OAuth
OAuth é o jeito padrão de "dar permissão sem entregar a senha". Em vez de digitar seu login da WHOOP no HealthOS, você clica em Autorizar no site da WHOOP e ela devolve um token — uma chave temporária que só serve para ler o que você liberou. O redirect URI é o endereço para onde a WHOOP te manda de volta carregando esse token.
No portal do desenvolvedor, três passos:
Criar o app
No developer portal da WHOOP, crie um app. Você recebe um WHOOP_CLIENT_ID e um WHOOP_CLIENT_SECRET — vão para o ~/.env.
Registrar o redirect URI
Cole exatamente https://<seu-host>/whoop/callback. Tem que bater caractere por caractere com o que o seu callback usa — senão a WHOOP recusa a autorização.
Habilitar os escopos
Marque read:recovery read:sleep read:cycles offline. Os três read: liberam os dados; o offline libera o refresh token (sync sem você por perto).
📋 Cole no painel · objetivo: os dois campos exatos do app WHOOP
Redirect URI: https://<seu-host>/whoop/callback
Scopes: read:recovery read:sleep read:cycles offline
Como verificar: o painel salva sem reclamar e o offline aparece marcado. Sem o offline você teria que re-autorizar todo dia — o sync automático não rodaria.
📊 Como ler: da esquerda pra direita é o caminho da permissão até o dado — o app autoriza no callback, o callback grava o token, o sync usa o token e escreve em vitals. A linha tracejada de volta é a rotação do token (gotcha 1 do tópico 4): cada run troca o token pelo próximo.
Conceitos-chave
Dá id + secret ao HealthOS.
.../whoop/callback, exato.
recovery + sleep + cycles.
Libera o refresh token.
✅ Autorização one-time
Você autoriza a WHOOP uma única vez. Nessa autorização, o seu callback recebe o código, troca por tokens e grava o WHOOP_REFRESH_TOKEN no ~/.env. A partir daí, o sync diário se vira sozinho com esse refresh token — você não precisa abrir o navegador de novo.
Você clica em "Autorizar"
Abre a tela de consentimento da WHOOP com os escopos que você marcou. Você aceita.
A WHOOP redireciona
Ela te manda para https://<seu-host>/whoop/callback com um código de uso único.
O callback grava o token
O callback troca o código por tokens e escreve o WHOOP_REFRESH_TOKEN no ~/.env. Pronto: o one-time acabou.
▶ Rode você mesmo · objetivo: conferir que o refresh token foi gravado
grep WHOOP_REFRESH_TOKEN ~/.env
Como verificar: aparece uma linha WHOOP_REFRESH_TOKEN=... com um valor longo (não vazio). Se vier vazio, a autorização não fechou — refaça o passo de "Autorizar".
💾 O que ficou no ~/.env
Depois do one-time, seu ~/.env tem os três: WHOOP_CLIENT_ID, WHOOP_CLIENT_SECRET (do app, no tópico 1) e agora o WHOOP_REFRESH_TOKEN (gravado pelo callback, rotacionado a cada sync). Esse arquivo vive na sua home e nunca vai para o git.
Conceitos-chave
Autoriza uma vez só.
Troca código por tokens.
Gravado no ~/.env.
O sync se vira sozinho depois.
🔄 whoop-sync.py
O agent/scripts/whoop-sync.py é o coração da conexão. Cada vez que ele roda, puxa a recuperação e o sono da WHOOP e grava quatro campos na tabela vitals: recovery_pct, hrv_ms, resting_hr, sleep_hours. Ele é determinístico (mesmo dia, mesmo resultado) e idempotente (rodar de novo não cria linha duplicada — atualiza a do dia).
🟢 Novo aqui?
- Determinístico — não tem IA nem aleatório no meio: dado o mesmo dia na WHOOP, a saída é sempre a mesma. Dá pra confiar e re-rodar.
- Idempotente — rodar 1× ou 5× deixa o banco no mesmo estado: uma linha por dia em
vitals, não cinco. Por isso agendar repetições é seguro.
🗄️ Os 4 campos que caem em vitals
% de recuperação da noite.
Variabilidade cardíaca (ms).
Frequência de repouso.
Horas de sono da noite.
▶ Rode você mesmo · objetivo: sincronizar agora e ver os 4 campos em vitals
python3 agent/scripts/whoop-sync.py
python3 agent/scripts/db.py select vitals
Como verificar: a linha de hoje em vitals traz recovery_pct, hrv_ms, resting_hr e sleep_hours preenchidos. Rode os dois comandos de novo: continua uma linha para o dia (idempotente), não duas.
Conceitos-chave
A noite que a WHOOP pontuou.
4 campos por dia.
Mesmo dia, mesma saída.
Re-rodar não duplica.
🛡️ Os 2 gotchas de produção
Dois detalhes derrubam o sync se você não os tratar — e os dois só aparecem em produção, depois que o primeiro sync já funcionou. O whoop-sync.py já cuida dos dois; entenda por quê para não tropeçar ao reescrever.
✗ Se você ignorar
- ✗(a) Reusar o mesmo refresh token → no 2º run a WHOOP responde
invalid_grante o sync morre. - ✗(b) Usar o user-agent padrão do Python → o Cloudflare da WHOOP te bane com erro 1010.
✓ O que o sync faz
- ✓(a) A cada run, grava o novo refresh token que a WHOOP devolve por cima do antigo (rotação).
- ✓(b) Envia um user-agent de browser nas requisições, então o Cloudflare deixa passar.
⚠️ Como cada falha aparece
- •Gotcha (a) — rotação: o refresh token é de uso único. Quando você o usa, a WHOOP devolve um novo e invalida o velho. Se você não salvar o novo, o próximo run usa um token já queimado →
invalid_grant. Verifique rodando o sync duas vezes: a 2ª ainda deve ter sucesso. - •Gotcha (b) — user-agent: o Cloudflare na frente da API bloqueia clientes "robóticos". O user-agent padrão do
requests/urllibentrega o jogo → 1010. Mandar um header de browser resolve.
Conceitos-chave
Salvar o novo token a cada run.
Sintoma de token não-rotacionado.
Fingir browser passa no Cloudflare.
Cloudflare baniu o cliente.
⏰ Agendar o sync
A WHOOP não pontua a sua noite num horário fixo — às vezes às 7h, às vezes mais tarde. Por isso o sync roda três vezes pela manhã (7h, 10h, 13h): alguma dessas execuções pega a recuperação assim que ela ficar pronta. Como o sync é idempotente, repetir é de graça e seguro.
📊 Como ler: à esquerda, o cron dispara quatro coisas pela manhã — três syncs (azul: 7h/10h/13h) que escrevem em vitals, e o /checkin (âmbar, tópico 6) que lê a recuperação e manda a revisão no Telegram. As três horas garantem pegar a recovery assim que a WHOOP pontuar.
▶ Rode você mesmo · objetivo: agendar o sync 3× na manhã (Linux/cron)
# abra o crontab: crontab -e
# sync às 7h, 10h e 13h (idempotente, repetir é seguro)
0 7,10,13 * * * cd ~/<sua-pasta>/agent && /usr/bin/python3 scripts/whoop-sync.py >> ~/whoop-sync.log 2>&1
macOS: em vez do cron, use agent/setup/whoop-sync.plist.example (launchd). Como verificar: amanhã de manhã a tabela vitals ganha a linha do dia e ~/whoop-sync.log mostra execuções sem erro (sem invalid_grant, sem 1010).
Conceitos-chave
Pega a recovery quando ela sai.
Uma linha agenda os 3 horários.
.plist.example pronto no repo.
Idempotência protege.
🌅 Agendar o check-in da manhã
O sync só enche a tabela; quem te entrega a revisão é o check-in da manhã. Diferente do sync (que é só um script lendo a API), o check-in dispara um turno do agente — uma chamada de LLM que lê a recovery da noite e escreve a revisão no Telegram. Por isso ele é agendado à parte: você manda o prompt /checkin toda manhã pelo seu scheduler.
💡 Sync ≠ check-in
O sync é mecânico e barato: lê a WHOOP, grava 4 números. O check-in é um turno do agente (custa uma chamada de LLM): ele lê esses números + o resto do seu snapshot e conversa com você. Agende o sync para rodar antes do check-in, para a revisão já encontrar a recovery do dia na mesa.
▶ Rode você mesmo · objetivo: disparar o /checkin toda manhã (um turno do agente)
# crontab -e — dispara /checkin às 7h05 (logo após o 1º sync)
5 7 * * * cd ~/<sua-pasta>/agent && /usr/bin/python3 <seu-disparador> "/checkin" >> ~/checkin.log 2>&1
Atalho: plataformas de agente com scheduler embutido (ex.: ClaudeClaw) disparam o /checkin direto, sem cron. Como verificar: às 7h você recebe no Telegram a revisão da manhã abrindo com a recovery da noite e amarrando-a às escolhas de ontem.
🔁 A ordem da manhã
1) 07:00 o sync escreve a recovery em vitals. 2) 07:05 o /checkin dispara o agente, que lê essa recovery e te manda a revisão. Os syncs das 10h e 13h são a rede de segurança caso a WHOOP pontue a noite mais tarde.
Conceitos-chave
O prompt da revisão da manhã.
Uma chamada de LLM, não um script.
Recovery na mesa quando dispara.
Cron ou o do seu agente.
🔁 Outras wearables
A WHOOP é só o exemplo trabalhado. O mesmo padrão — OAuth uma vez + sync diário escrevendo em vitals — serve para Oura, Garmin, Fitbit, ou até registro manual. O coach e o dashboard não sabem nem se importam de onde veio o número: eles trabalham com o que cair na tabela vitals.
✓ Reaproveita o padrão
- ✓Oura / Garmin / Fitbit — trocar o app OAuth e o mapeamento JSON→
vitals; o resto fica igual. - ✓Registro manual — uma mensagem no Telegram ("dormi 7h, recovery boa") já vira linha em
vitals. - ✓O
/checkin, o snapshot e o dashboard continuam idênticos.
✗ O que não muda
- ✗Não reescreva o coach por fonte — ele lê
vitals, não a API da wearable. - ✗Não pule os 2 gotchas: qualquer OAuth tem rotação de token e antibot na frente.
- ✗Não precisa de WHOOP para começar — o curso roda com registro manual.
✅ Auto-checagem (opcional): por que o whoop-sync.py precisa enviar um user-agent de browser?
Conceitos-chave
OAuth + sync diário.
O coach lê a tabela, não a API.
Mensagem vira linha.
Rotação + antibot em toda fonte.
📋 Resumo do módulo
.../whoop/callback exato e marque read:recovery read:sleep read:cycles offline.WHOOP_REFRESH_TOKEN no ~/.env.invalid_grant) e mande user-agent de browser (senão Cloudflare 1010)./checkin de manhã; o padrão serve a qualquer wearable.Próximo módulo:
Trilha 3 — Como usar: a rotina diária, comida e treino por foto, memória/padrões e o dashboard. O passo a passo terminou; agora é viver o coach no dia a dia.