| name | cfo-dashboard |
| version | 1.1.0 |
| description | Relatório mensal de custos de infra/IA do projeto: Claude API, Gemini (imagens), Meta Ads (gasto real via Marketing API ou budget planejado), Supabase, Vercel e Apple Developer. Real para o passado, projetado para o próximo mês. |
cfo-dashboard
Gera dashboard de custos mensais reais e projetados para o projeto Lado a Lado.
Uso
/cfo-dashboard [mês]
Exemplos:
/cfo-dashboard — mês atual
/cfo-dashboard abril 2026 — mês específico
O que fazer ao executar
1. Carregar configuração
cat ~/.claude/cfo-config.json 2>/dev/null || echo "{}"
Se o arquivo não existir, criar com os defaults abaixo e pedir ao usuário os valores faltantes:
{
"anthropic_admin_key_env": "ANTHROPIC_ADMIN_KEY",
"claude_code_plan": "pro",
"claude_code_monthly_brl": 110.00,
"claude_code_note": "Cobrado em BRL. USD = brl / brl_to_usd_rate.",
"google_cloud_project": "",
"google_billing_account": "",
"gemini_cost_per_image_usd": 0.067,
"brl_to_usd_rate": 5.85,
"supabase_access_token_env": "SUPABASE_ACCESS_TOKEN",
"supabase_project_ref": "",
"supabase_plan": "free",
"supabase_monthly_cost_usd": 0.00,
"vercel_token_env": "VERCEL_TOKEN",
"vercel_team_id": "",
"vercel_plan": "hobby",
"vercel_monthly_cost_usd": 20.00,
"apple_developer_annual_cost_usd": 99.00,
"apple_developer_renewal_date": "",
"meta_ads_default_monthly_budget_brl": 150.00
}
Calcular datas do período:
python3 -c "
from datetime import date, timedelta
import calendar
today = date.today()
start = today.replace(day=1)
end = today.replace(day=calendar.monthrange(today.year, today.month)[1])
print(f'START={start.isoformat()} END={end.isoformat()} YEAR={today.year} MONTH={today.month:02d}')
"
2. Buscar custos Claude (Anthropic Admin API)
Requer Admin API key (sk-ant-admin...). Diferente da chave de API normal.
source ~/.zshrc 2>/dev/null
ADMIN_KEY=$ANTHROPIC_ADMIN_KEY
curl -s "https://api.anthropic.com/v1/organizations/cost_report?start_date=${START}&end_date=${END}" \
-H "x-api-key: $ADMIN_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "Content-Type: application/json" \
| python3 -c "
import sys, json
data = json.load(sys.stdin)
# total em centavos (lowest unit) — converter para USD
total_cents = float(data.get('total_cost', 0))
total_usd = total_cents / 100
print(f'claude_total_usd={total_usd:.4f}')
print(f'claude_breakdown={json.dumps(data.get(\"breakdown\", {}))}')
" 2>/dev/null || echo "claude_total_usd=MANUAL"
Se retornar MANUAL ou erro de autenticação:
⚠️ Chave Admin não configurada. Para buscar custos do Claude automaticamente:
- Acesse console.anthropic.com → Settings → API Keys
- Crie uma Admin API Key (começa com
sk-ant-admin)
- Adicione ao
.zshrc: export ANTHROPIC_ADMIN_KEY="sk-ant-admin..."
- Ou informe o custo do mês manualmente (disponível em console.anthropic.com → Billing)
3. Buscar custos Gemini via Billing Page (por mês calendário)
A página de billing exibe custos por mês calendário (não rolling 28 dias). A seção "Transações" fica dentro de um iframe cross-origin (payments.google.com), inacessível via JavaScript. A abordagem é: abrir a página, rolar até a seção, tirar screenshot e ler os valores via visão.
Regra de cache:
- Meses fechados (anteriores ao atual): verificar cache no
historico do config. Se já houver gemini_brl, usar direto sem abrir o Chrome.
- Mês atual: sempre buscar valor atualizado.
python3 << 'PYEOF'
import subprocess, time, json, datetime, re
from PIL import Image
cfg = json.load(open('/Users/fipacheco/.claude/cfo-config.json'))
rate = cfg.get('brl_to_usd_rate', 5.85)
BILLING_URL = "https://aistudio.google.com/billing?billing=01458E-C82CA8-2430CB"
today = datetime.date.today()
mes_key = today.strftime('%Y-%m')
historico = cfg.get('historico', {})
TARGET_MES_KEY = mes_key
is_current_month = (TARGET_MES_KEY == mes_key)
if not is_current_month and TARGET_MES_KEY in historico and historico[TARGET_MES_KEY].get('gemini_brl') is not None:
cached = historico[TARGET_MES_KEY]['gemini_brl']
print(f'gemini_brl={cached:.2f} (cache)')
print(f'gemini_total_usd={cached/rate:.4f} (cache)')
else:
open_script = f'''tell application "Google Chrome"
activate
set newTab to make new tab at end of tabs of front window
set URL of newTab to "{BILLING_URL}"
set active tab index of front window to (count tabs of front window)
end tell'''
subprocess.run(['osascript', '-e', open_script], capture_output=True)
print("Aguardando billing page (30s)...")
time.sleep(30)
subprocess.run(['osascript', '-e', '''tell application "Google Chrome"
execute front window\'s active tab javascript "const m = document.querySelector(\'.layout-main\'); if(m) m.scrollTop = 800;"
end tell'''], capture_output=True)
time.sleep(3)
# Clicar em qualquer link de transação para garantir que os valores sejam exibidos
# Coordenadas de tela calibradas: link "1 – 30 de abr." ≈ (242, 334) logical
subprocess.run(['osascript', '-e', '''tell application "Google Chrome" to activate
delay 0.5
tell application "System Events"
tell process "Google Chrome"
click at {242, 334}
end tell
end tell'''], capture_output=True)
time.sleep(5)
# Screenshot e crop da área de Transações
subprocess.run(['screencapture', '-x', '/tmp/billing_gemini.png'], capture_output=True)
img = Image.open('/tmp/billing_gemini.png')
w, h = img.size
cropped = img.crop((0, int(h * 0.05), int(w * 0.60), int(h * 0.50)))
cropped.resize((cropped.width * 2, cropped.height * 2), Image.LANCZOS).save('/tmp/billing_gemini_crop.png')
subprocess.run(['osascript', '-e', 'tell application "Google Chrome" to close active tab of front window'], capture_output=True)
print("Screenshot salvo: /tmp/billing_gemini_crop.png")
print("PRÓXIMO PASSO: usar Read tool em /tmp/billing_gemini_crop.png para extrair os valores")
PYEOF
Após rodar o script, usar o Read tool para ler /tmp/billing_gemini_crop.png e extrair:
# Exemplo de leitura esperada:
# Transações
# 1 – 5 de mai. de 2026 R$ 1,05
# 1 – 30 de abr. de 2026 R$ 14,89
# 1 – 31 de mar. de 2026 R$ 0,00
Depois de ler os valores:
import json, re
def parse_brl(s):
s = s.strip().replace('R$', '').replace(' ', '')
if ',' in s and '.' in s:
return float(s.replace('.', '').replace(',', '.'))
elif ',' in s:
return float(s.replace(',', '.'))
return float(s)
GEMINI_CURRENT_BRL = 1.05
GEMINI_PREV_BRL = 14.89
GEMINI_PREV_KEY = "2026-04"
cfg = json.load(open('/Users/fipacheco/.claude/cfo-config.json'))
rate = cfg.get('brl_to_usd_rate', 5.85)
historico = cfg.get('historico', {})
if GEMINI_PREV_KEY not in historico:
historico[GEMINI_PREV_KEY] = {}
if historico[GEMINI_PREV_KEY].get('gemini_brl') is None:
historico[GEMINI_PREV_KEY]['gemini_brl'] = GEMINI_PREV_BRL
historico[GEMINI_PREV_KEY]['gemini_usd'] = GEMINI_PREV_BRL / rate
cfg['historico'] = historico
with open('/Users/fipacheco/.claude/cfo-config.json', 'w') as f:
json.dump(cfg, f, indent=2, ensure_ascii=False)
print(f"Cache atualizado: {GEMINI_PREV_KEY} = R${GEMINI_PREV_BRL}")
print(f"gemini_brl={GEMINI_CURRENT_BRL:.2f}")
print(f"gemini_total_usd={GEMINI_CURRENT_BRL/rate:.4f}")
⚠️ Se o click não revelar os valores: as coordenadas de clique {242, 334} são calibradas para a resolução atual (Retina 3360×2100). Se a tela mudar, recalibrar abrindo o billing page manualmente, rolando 800px e tirando screenshot para identificar a nova posição dos links.
4. Buscar custos Vercel
source ~/.zshrc 2>/dev/null
VERCEL_TOKEN_VAL=$VERCEL_TOKEN
curl -s "https://api.vercel.com/v1/billing/charges?\
from=${START}T00:00:00Z&\
to=${END}T23:59:59Z" \
-H "Authorization: Bearer $VERCEL_TOKEN_VAL" \
| python3 -c "
import sys
total = 0
for line in sys.stdin:
line = line.strip()
if not line:
continue
import json
try:
item = json.loads(line)
# FOCUS v1.3 — campo BilledCost em USD
total += float(item.get('BilledCost', 0))
except:
pass
print(f'vercel_total_usd={total:.4f}')
" 2>/dev/null || echo "vercel_total_usd=PLAN_FIXED"
Se retornar PLAN_FIXED ou zero (plano fixo sem uso variável):
- Usar
vercel_monthly_cost_usd do config (default: $20.00)
5. Verificar plano Supabase
source ~/.zshrc 2>/dev/null
SUPA_TOKEN=$SUPABASE_ACCESS_TOKEN
PROJECT_REF=$(python3 -c "import json; print(json.load(open('/Users/fipacheco/.claude/cfo-config.json')).get('supabase_project_ref',''))" 2>/dev/null)
curl -s "https://api.supabase.com/v1/projects/${PROJECT_REF}/subscription" \
-H "Authorization: Bearer $SUPA_TOKEN" \
| python3 -c "
import sys, json
data = json.load(sys.stdin)
plan = data.get('plan', {})
tier = plan.get('id', 'unknown')
print(f'supabase_plan={tier}')
# preço fixo por tier
prices = {'free': 0, 'pro': 25, 'team': 599, 'enterprise': 0}
print(f'supabase_usd={prices.get(tier, 25):.2f}')
" 2>/dev/null || echo "supabase_usd=PLAN_FIXED"
Se retornar PLAN_FIXED: usar supabase_monthly_cost_usd do config (default: $25.00).
6. Buscar gastos Meta Ads (Instagram Boost)
Prioridade: valor real via Marketing API. Fallback: budget planejado do tracking.
import json, os, urllib.request, urllib.parse, datetime
cfg = json.load(open('/Users/fipacheco/.claude/cfo-config.json'))
ig_cfg_path = os.path.expanduser("~/.claude/instagram-planner-config.json")
tracking_path = os.path.expanduser("~/.claude/instagram-boost-tracking.json")
rate = cfg.get('brl_to_usd_rate', 5.85)
META_ADS_BRL = 0.0
META_ADS_FONTE = "sem_campanhas"
try:
ig_cfg = json.load(open(ig_cfg_path))
token = ig_cfg.get("meta_access_token_cached", "")
ad_account = ig_cfg.get("ad_account_id", "")
if token and ad_account:
today = datetime.date.today()
url = (
f"https://graph.facebook.com/v21.0/{ad_account}/insights"
f"?fields=spend&date_preset=this_month&access_token={token}"
)
resp = json.loads(urllib.request.urlopen(url).read())
dados = resp.get("data", [])
if dados:
META_ADS_BRL = float(dados[0].get("spend", 0))
META_ADS_FONTE = "marketing_api"
print(f"meta_ads_brl={META_ADS_BRL:.2f} (Marketing API — mês corrente)")
else:
print("meta_ads_brl=0.00 (API OK, sem gastos ainda)")
META_ADS_FONTE = "marketing_api_zero"
except Exception as e:
print(f"Marketing API indisponível: {e}")
if META_ADS_FONTE not in ("marketing_api", "marketing_api_zero"):
try:
tracking = json.load(open(tracking_path))
today = datetime.date.today()
mes_key = today.strftime("%Y-%m")[:7]
for boost in reversed(tracking.get("boosts", [])):
criado = boost.get("criado_em", "")[:7]
mes_boost = boost.get("mes", "").replace("_", "-")[:7]
if criado == mes_key or mes_boost.replace("maio","05").replace("junho","06") == mes_key:
META_ADS_BRL = float(boost.get("budget_total", 0))
META_ADS_FONTE = "tracking_budget_planejado"
print(f"meta_ads_brl={META_ADS_BRL:.2f} (budget planejado do tracking — valor fixo)")
break
except Exception as e:
print(f"Tracking indisponível: {e}")
if META_ADS_BRL == 0.0 and META_ADS_FONTE not in ("marketing_api", "marketing_api_zero"):
META_ADS_BRL = cfg.get("meta_ads_default_monthly_budget_brl", 0.0)
META_ADS_FONTE = "config_default"
print(f"meta_ads_brl={META_ADS_BRL:.2f} (config_default)")
META_ADS_USD = META_ADS_BRL / rate
print(f"meta_ads_usd={META_ADS_USD:.4f} fonte={META_ADS_FONTE}")
Projeção Meta Ads para o próximo mês:
try:
tracking = json.load(open(tracking_path))
boosts = tracking.get("boosts", [])
if boosts:
PROJ_META_BRL = float(boosts[-1].get("budget_total", 0))
else:
PROJ_META_BRL = cfg.get("meta_ads_default_monthly_budget_brl", 0.0)
except:
PROJ_META_BRL = cfg.get("meta_ads_default_monthly_budget_brl", 0.0)
PROJ_META_USD = PROJ_META_BRL / rate
print(f"proj_meta_ads_brl={PROJ_META_BRL:.2f} proj_meta_ads_usd={PROJ_META_USD:.4f}")
7. Calcular Apple Developer (custo fixo anual)
python3 -c "
import json, datetime
cfg = json.load(open('/Users/fipacheco/.claude/cfo-config.json'))
annual = cfg.get('apple_developer_annual_cost_usd', 99)
monthly = annual / 12
renewal = cfg.get('apple_developer_renewal_date', 'não configurado')
print(f'apple_monthly_usd={monthly:.2f}')
print(f'apple_renewal={renewal}')
print(f'apple_annual_usd={annual:.2f}')
"
7. Projetar custos Gemini para o próximo mês
Ler o plano do Instagram mais recente e contar imagens a gerar.
LATEST_PLAN=$(python3 -c "
import os, re, glob
dirs = sorted(glob.glob(os.path.expanduser('~/instagram-planner/*/')), reverse=True)
for d in dirs:
files = glob.glob(os.path.join(d, 'plano*.md'))
if not files:
continue
def version_key(f):
m = re.search(r'plano-v(\d+)\.md$', f)
return int(m.group(1)) if m else 1
best = sorted(files, key=version_key, reverse=True)[0]
print(best)
break
" 2>/dev/null)
if [ -z "$LATEST_PLAN" ]; then
echo "gemini_proximo_images=0 gemini_proximo_usd=0"
else
python3 -c "
import re, json
with open('$LATEST_PLAN') as f:
content = f.read()
# Contar posts
posts = re.findall(r'^### Post \d+', content, re.MULTILINE)
total_posts = len(posts)
# Contar slides de carrosséis
slides = re.findall(r'\*\*Formato:\*\* carrossel \((\d+) slides?\)', content)
total_slides = sum(int(s) for s in slides)
# Posts de imagem única (não carrossel)
single = re.findall(r'\*\*Formato:\*\* imagem.nica', content)
total_single = len(single)
# Total de imagens: slides dos carrosséis + imagens únicas
# Carrossel: cada slide gera 1 imagem (exceto slide CTA que pode ser sem imagem)
# Estimativa conservadora: slides - 1 por carrossel (último slide = fundo sólido)
carousel_count = len(slides)
total_images = total_slides - carousel_count + total_single # deduz slide CTA de cada carrossel
cost_per_image = $GEMINI_COST_PER_IMAGE
total_cost = total_images * cost_per_image
print(f'gemini_proximo_posts={total_posts}')
print(f'gemini_proximo_images={total_images}')
print(f'gemini_proximo_usd={total_cost:.4f}')
print(f'gemini_plano_fonte={\"$LATEST_PLAN\"}')
" 2>/dev/null || echo "gemini_proximo_usd=MANUAL"
fi
8. Montar e exibir dashboard
Consolidar todos os valores coletados e exibir:
╔══════════════════════════════════════════════════════════════╗
║ CFO Dashboard — <Mês Ano> ║
╚══════════════════════════════════════════════════════════════╝
CUSTOS REAIS — <dd/mm/yyyy> a <dd/mm/yyyy>
──────────────────────────────────────────────────────────────
Claude API (Anthropic) $XX.XX
Gemini — geração de imagens $XX.XX
Meta Ads (Instagram Boost) R$XXX.XX / $XX.XX [fonte]
Supabase Pro $25.00
Vercel Pro $20.00
Apple Developer (1/12 do anual) $8.25
──────────────────────────────────────────────────────────────
TOTAL MÊS ATUAL $XXX.XX
PROJEÇÃO — próximo mês
──────────────────────────────────────────────────────────────
Claude API (média últimos 3 meses) $XX.XX
Gemini — N posts × M imagens $XX.XX
Fonte: <caminho do plano>
Meta Ads (budget planejado) R$XXX.XX / $XX.XX
Supabase Pro $25.00
Vercel Pro $20.00
Apple Developer (1/12 do anual) $8.25
──────────────────────────────────────────────────────────────
TOTAL PROJETADO $XXX.XX
REFERÊNCIA ANUAL
──────────────────────────────────────────────────────────────
Apple Developer (renovação em <data>) $99.00/ano
Projeção anualizada (×12) $XXX.XX/ano
⚠️ AVISOS
──────────────────────────────────────────────────────────────
(listar aqui qualquer valor que precisou ser inserido
manualmente, incluindo fonte dos Meta Ads se não foi API)
9. Atualizar cache no config
Salvar os valores reais do mês no config para histórico e cálculo de médias futuras:
python3 -c "
import json, datetime
with open('/Users/fipacheco/.claude/cfo-config.json') as f:
cfg = json.load(f)
mes_key = datetime.date.today().strftime('%Y-%m')
historico = cfg.get('historico', {})
historico[mes_key] = {
'claude_usd': CLAUDE_TOTAL,
'gemini_usd': GEMINI_TOTAL,
'meta_ads_brl': META_ADS_BRL,
'meta_ads_usd': META_ADS_USD,
'meta_ads_fonte': META_ADS_FONTE,
'supabase_usd': SUPABASE_TOTAL,
'vercel_usd': VERCEL_TOTAL,
'apple_usd': APPLE_MONTHLY,
'total_usd': GRAND_TOTAL
}
cfg['historico'] = historico
with open('/Users/fipacheco/.claude/cfo-config.json', 'w') as f:
json.dump(cfg, f, indent=2, ensure_ascii=False)
print('Histórico atualizado.')
"
10. Salvar relatório mensal em arquivo
Todo relatório tem duas seções: ## Projetado e ## Realizado.
Ciclo de vida:
- Ao rodar no mês corrente → cria o arquivo com
## Projetado preenchido e ## Realizado em branco (_pendente — preencher ao final do mês_)
- Ao rodar para um mês já fechado → cria com
## Projetado em branco (_relatório criado retroativamente_) e ## Realizado preenchido
- Atualizar o Realizado: somente quando o usuário pedir explicitamente (ex: "atualiza o realizado de maio"). Nunca sobrescrever automaticamente.
Verificar existência antes de salvar:
ls ~/.claude/cfo-reports/YYYY-MM.md 2>/dev/null && echo "EXISTS" || echo "NEW"
Função auxiliar Python para gerar o conteúdo do bloco de tabela (reutilizar nas duas seções):
def render_tabela(CLAUDE_USD, GEMINI_USD, GEMINI_BRL, GEMINI_PERIOD,
META_ADS_BRL, META_ADS_USD, META_ADS_FONTE,
SUPABASE_USD, VERCEL_USD, APPLE_USD, TOTAL_USD,
CLAUDE_PLAN, SUPABASE_PLAN, VERCEL_PLAN, rate, today_str):
def pct(v, t): return f"{v/t*100:.1f}%" if t > 0 else "—"
gemini_brl_display = GEMINI_BRL if GEMINI_BRL else GEMINI_USD * rate
itens = [
("Claude Code", CLAUDE_PLAN, CLAUDE_USD),
("Gemini API", "AI Studio", GEMINI_USD),
("Meta Ads", "Instagram Boost", META_ADS_USD),
("Supabase", SUPABASE_PLAN, SUPABASE_USD),
("Vercel", VERCEL_PLAN, VERCEL_USD),
("Apple Developer", "1/12 de $99/ano", APPLE_USD),
]
ranking = "\n".join(
f"{i+1}. {n} — {pct(v, TOTAL_USD)} do total"
for i, (n, _, v) in enumerate(sorted(itens, key=lambda x: x[2], reverse=True)) if v > 0
)
gemini_note = ""
if GEMINI_PERIOD:
gemini_note = f"\n- **Gemini:** ciclo AI Studio: {GEMINI_PERIOD} (28 dias, não mês calendário)."
if GEMINI_BRL > 0:
gemini_note += f" Original: R${GEMINI_BRL:.2f} (câmbio {rate})."
fontes_map = {
"marketing_api": "valor real via Marketing API",
"marketing_api_zero": "R$0 confirmado via Marketing API (sem gastos ainda)",
"tracking_budget_planejado": "budget planejado — gasto real ainda não disponível",
"config_default": "budget padrão do config (nenhuma campanha ativa encontrada)",
"sem_campanhas": "nenhuma campanha ativa este mês",
}
meta_note = f"\n- **Meta Ads:** {fontes_map.get(META_ADS_FONTE, META_ADS_FONTE)}."
return f"""**Fechado em:** {today_str}
| Serviço | Plano | Valor (USD) | Valor (BRL) |
|---|---|---|---|
| Claude Code | {CLAUDE_PLAN} | ${CLAUDE_USD:.2f} | R${CLAUDE_USD*rate:.2f} |
| Gemini API | AI Studio | ${GEMINI_USD:.2f} | R${gemini_brl_display:.2f} |
| Meta Ads | Instagram Boost | ${META_ADS_USD:.2f} | R${META_ADS_BRL:.2f} |
| Supabase | {SUPABASE_PLAN} | ${SUPABASE_USD:.2f} | R${SUPABASE_USD*rate:.2f} |
| Vercel | {VERCEL_PLAN} | ${VERCEL_USD:.2f} | R${VERCEL_USD*rate:.2f} |
| Apple Developer | 1/12 de $99/ano | ${APPLE_USD:.2f} | R${APPLE_USD*rate:.2f} |
| **TOTAL** | | **${TOTAL_USD:.2f}** | **R${TOTAL_USD*rate:.2f}** |
### Notas
{gemini_note}{meta_note}
- **Claude:** plano Pro fixo. Admin API key não configurada — sem detalhe por modelo.
- **Apple Developer:** rateio mensal ($99/ano ÷ 12). Data de renovação não configurada.
### Maiores custos
{ranking}"""
Criar relatório novo (preencher as variáveis com os valores coletados):
import json, os, calendar, datetime
cfg = json.load(open('/Users/fipacheco/.claude/cfo-config.json'))
rate = cfg.get('brl_to_usd_rate', 5.85)
today = datetime.date.today()
MESES = ['Janeiro','Fevereiro','Março','Abril','Maio','Junho',
'Julho','Agosto','Setembro','Outubro','Novembro','Dezembro']
MES_KEY = "YYYY-MM"
CLAUDE_USD = 0.0
GEMINI_USD = 0.0
GEMINI_BRL = 0.0
GEMINI_PERIOD = ""
SUPABASE_USD = 0.0
VERCEL_USD = 0.0
APPLE_USD = 0.0
TOTAL_USD = 0.0
PROJETADO_TOTAL_USD = 0.0
CLAUDE_PLAN = "Pro (fixo)"
SUPABASE_PLAN = "Free"
VERCEL_PLAN = "Hobby"
report_path = os.path.expanduser(f'~/.claude/cfo-reports/{MES_KEY}.md')
year, mon = MES_KEY.split('-')
mes_nome = MESES[int(mon)-1]
ultimo_dia = calendar.monthrange(int(year), int(mon))[1]
is_current = (int(year) == today.year and int(mon) == today.month)
if os.path.exists(report_path):
print(f"Relatório já existe: {report_path} — para atualizar, peça explicitamente.")
else:
if is_current:
proj_block = render_tabela(
CLAUDE_USD, GEMINI_USD, GEMINI_BRL, GEMINI_PERIOD,
SUPABASE_USD, VERCEL_USD, APPLE_USD, PROJETADO_TOTAL_USD,
CLAUDE_PLAN, SUPABASE_PLAN, VERCEL_PLAN, rate, f"projeção de {today.isoformat()}"
)
real_block = "_pendente — preencher ao final do mês_"
else:
proj_block = "_relatório criado retroativamente — projeção não disponível_"
real_block = render_tabela(
CLAUDE_USD, GEMINI_USD, GEMINI_BRL, GEMINI_PERIOD,
SUPABASE_USD, VERCEL_USD, APPLE_USD, TOTAL_USD,
CLAUDE_PLAN, SUPABASE_PLAN, VERCEL_PLAN, rate, today.isoformat()
)
content = f"""# CFO Report — {mes_nome} {year}
**Período:** 01/{mon}/{year} – {ultimo_dia:02d}/{mon}/{year}
**Câmbio:** 1 USD = R${rate:.2f}
---
## Projetado
{proj_block}
---
## Realizado
{real_block}
"""
os.makedirs(os.path.dirname(report_path), exist_ok=True)
with open(report_path, 'w') as f:
f.write(content)
print(f"Relatório salvo: {report_path}")
Atualizar o Realizado (somente quando o usuário pedir explicitamente):
import re
with open(report_path) as f:
existing = f.read()
real_block = render_tabela(
CLAUDE_USD, GEMINI_USD, GEMINI_BRL, GEMINI_PERIOD,
SUPABASE_USD, VERCEL_USD, APPLE_USD, TOTAL_USD,
CLAUDE_PLAN, SUPABASE_PLAN, VERCEL_PLAN, rate, datetime.date.today().isoformat()
)
updated = re.sub(r'(## Realizado\n\n).*', r'\1' + real_block + '\n', existing, flags=re.DOTALL)
with open(report_path, 'w') as f:
f.write(updated)
print(f"Realizado atualizado: {report_path}")
11. Atualizar relatório anual
O arquivo ~/.claude/cfo-reports/YYYY-anual.md é sempre regravado ao rodar a skill — é um relatório vivo que reflete o estado atual do histórico. Não há seção "projetado/realizado" separada aqui: cada linha da tabela já indica seu status (✅/🔄/🔮/⚠️).
Regra de atualização do histórico de atualizações: ao regravar, adicionar uma linha na tabela do final com a data e uma descrição do que mudou (ex: "Realizado de Maio adicionado").
import json, os, datetime, calendar
cfg = json.load(open('/Users/fipacheco/.claude/cfo-config.json'))
rate = cfg.get('brl_to_usd_rate', 5.85)
hist = cfg.get('historico', {})
today = datetime.date.today()
year = today.year
PROJ_CLAUDE = cfg.get('claude_code_monthly_usd', 100.00)
PROJ_GEMINI = (sum(v['gemini_usd'] for v in hist.values()) / max(len(hist), 1)) if hist else 2.54
PROJ_SUPA = cfg.get('supabase_monthly_cost_usd', 0.00)
PROJ_VERCEL = cfg.get('vercel_monthly_cost_usd', 0.00)
PROJ_APPLE = cfg.get('apple_developer_annual_cost_usd', 99.00) / 12
rate = cfg.get('brl_to_usd_rate', 5.85)
meta_hist = [v['meta_ads_usd'] for v in hist.values() if v.get('meta_ads_usd', 0) > 0]
if meta_hist:
PROJ_META = sum(meta_hist) / len(meta_hist)
else:
PROJ_META = cfg.get('meta_ads_default_monthly_budget_brl', 0.0) / rate
PROJ_TOTAL = PROJ_CLAUDE + PROJ_GEMINI + PROJ_META + PROJ_SUPA + PROJ_VERCEL + PROJ_APPLE
MESES_PT = ['Jan','Fev','Mar','Abr','Mai','Jun','Jul','Ago','Set','Out','Nov','Dez']
MESES_FULL = ['Janeiro','Fevereiro','Março','Abril','Maio','Junho',
'Julho','Agosto','Setembro','Outubro','Novembro','Dezembro']
CLAUDE_PLAN = cfg.get('claude_code_plan', 'pro').capitalize() + ' (fixo)'
SUPA_PLAN = cfg.get('supabase_plan', 'free').capitalize()
VERCEL_PLAN = cfg.get('vercel_plan', 'hobby').capitalize()
rows = []
total_real = 0.0
total_proj = 0.0
for m in range(1, 13):
key = f"{year}-{m:02d}"
mes = MESES_PT[m - 1]
d = hist.get(key)
mes_date = datetime.date(year, m, 1)
is_current = (m == today.month and year == today.year)
if d:
status = "✅ Realizado"
total_real += d['total_usd']
rows.append((mes, status,
d['claude_usd'], d['gemini_usd'], d.get('meta_ads_usd', 0),
d['supabase_usd'], d['vercel_usd'], d['apple_usd'],
d['total_usd'], False))
elif mes_date < today.replace(day=1):
status = "⚠️ Sem dados"
rows.append((mes, status, None, None, None, None, None, None, None, False))
elif is_current:
status = "🔄 Em curso"
total_proj += PROJ_TOTAL
rows.append((mes, status,
PROJ_CLAUDE, PROJ_GEMINI, PROJ_META, PROJ_SUPA, PROJ_VERCEL, PROJ_APPLE,
PROJ_TOTAL, True))
else:
status = "🔮 Projetado"
total_proj += PROJ_TOTAL
rows.append((mes, status,
PROJ_CLAUDE, PROJ_GEMINI, PROJ_META, PROJ_SUPA, PROJ_VERCEL, PROJ_APPLE,
PROJ_TOTAL, True))
def fmt(v): return "—" if v is None else f"${v:,.2f}"
hist_count = len([v for v in hist.values()])
tabela_linhas = []
for mes, status, claude, gemini, meta, supa, vercel, apple, total, is_proj in rows:
mark = " *" if is_proj else ""
tabela_linhas.append(
f"| {mes} | {status} | {fmt(claude)} | {fmt(gemini)} | {fmt(meta)} | {fmt(supa)} | {fmt(vercel)} | {fmt(apple)} | **{fmt(total)}**{mark} |"
)
tabela = "\n".join(tabela_linhas)
total_rastreado = total_real + total_proj
total_anual_full = PROJ_TOTAL * 12
annual_path = os.path.expanduser(f'~/.claude/cfo-reports/{year}-anual.md')
historico_updates = []
if os.path.exists(annual_path):
with open(annual_path) as f:
content = f.read()
import re
m_hist = re.search(r'## Histórico de atualizações\n\n(.*)', content, re.DOTALL)
if m_hist:
historico_updates = [l for l in m_hist.group(1).strip().splitlines() if l.startswith('|') and '---' not in l and 'Data' not in l]
hoje_str = today.isoformat()
if not any(hoje_str in l for l in historico_updates):
historico_updates.append(f"| {hoje_str} | Relatório atualizado — {hist_count} mês(es) com dados reais |")
historico_tabela = "\n".join(historico_updates)
content = f"""# CFO Report — Anual {year}
**Gerado em:** {hoje_str}
**Câmbio referência:** 1 USD = R${rate:.2f}
**Base da projeção:** média de {hist_count} mês(es) de histórico
---
## Resumo do Ano
| Mês | Status | Claude | Gemini | Meta Ads | Supabase | Vercel | Apple | **Total** |
|---|---|---:|---:|---:|---:|---:|---:|---:|
{tabela}
_\\* projeção baseada na média de {hist_count} mês(es) de histórico_
---
## Totais
| | USD | BRL (R${rate:.2f}) |
|---|---:|---:|
| ✅ Realizado | ${total_real:,.2f} | R${total_real*rate:,.2f} |
| 🔄 + 🔮 Projetado (meses restantes) | ${total_proj:,.2f} | R${total_proj*rate:,.2f} |
| **Total rastreado ({year})** | **${total_rastreado:,.2f}** | **R${total_rastreado*rate:,.2f}** |
| **Projeção anual completa (×12)** | **${total_anual_full:,.2f}** | **R${total_anual_full*rate:,.2f}** |
---
## Composição dos Custos (projeção mensal)
| Serviço | Mensal | % do total | Anual |
|---|---:|---:|---:|
| Claude Code ({CLAUDE_PLAN}) | ${PROJ_CLAUDE:,.2f} | {PROJ_CLAUDE/PROJ_TOTAL*100:.1f}% | ${PROJ_CLAUDE*12:,.2f} |
| Meta Ads Instagram | ${PROJ_META:,.2f} | {PROJ_META/PROJ_TOTAL*100:.1f}% | ${PROJ_META*12:,.2f} |
| Apple Developer (rateio) | ${PROJ_APPLE:,.2f} | {PROJ_APPLE/PROJ_TOTAL*100:.1f}% | ${PROJ_APPLE*12:,.2f} |
| Gemini API | ${PROJ_GEMINI:,.2f} | {PROJ_GEMINI/PROJ_TOTAL*100:.1f}% | ${PROJ_GEMINI*12:,.2f} |
| Supabase ({SUPA_PLAN}) | ${PROJ_SUPA:,.2f} | {PROJ_SUPA/PROJ_TOTAL*100:.1f}% | ${PROJ_SUPA*12:,.2f} |
| Vercel ({VERCEL_PLAN}) | ${PROJ_VERCEL:,.2f} | {PROJ_VERCEL/PROJ_TOTAL*100:.1f}% | ${PROJ_VERCEL*12:,.2f} |
| **Total** | **${PROJ_TOTAL:,.2f}** | **100%** | **${PROJ_TOTAL*12:,.2f}** |
---
## Notas
- **Projeção Gemini:** baseada em média de {hist_count} mês(es) — quanto mais histórico, mais precisa.
- **Projeção Meta Ads:** média dos meses com histórico real; fallback = budget padrão do config (R${cfg.get('meta_ads_default_monthly_budget_brl',0):.0f}/mês).
- **Claude:** plano Pro fixo. Custo real de uso de API não rastreado (sem Admin API key).
- **Apple Developer:** $99/ano rateado. Renovação paga de uma vez, não mensalmente.
---
## Histórico de atualizações
| Data | Descrição |
|---|---|
{historico_tabela}
"""
with open(annual_path, 'w') as f:
f.write(content)
print(f"Relatório anual atualizado: {annual_path}")
Configuração inicial obrigatória
Na primeira execução, a skill vai identificar o que está faltando e pedir ao usuário:
| Campo | Como obter |
|---|
ANTHROPIC_ADMIN_KEY | console.anthropic.com → Settings → API Keys → Create Admin Key |
google_billing_account | gcloud billing accounts list |
google_cloud_project | gcloud config get-value project |
supabase_project_ref | URL do projeto: https://app.supabase.com/project/<REF> |
VERCEL_TOKEN | vercel.com → Account Settings → Tokens |
vercel_team_id | vercel.com → Team Settings → General (Team ID) |
apple_developer_renewal_date | appleid.apple.com → Assinaturas |
Salvar tudo em ~/.claude/cfo-config.json para não precisar repetir.
Preços de referência (atualizar se mudarem)
| Serviço | Modelo | Preço unitário |
|---|
| Gemini 3.1 Flash | Imagem 1K px | $0.067/imagem |
| Gemini 3 Pro | Imagem 1K-2K px | $0.134/imagem |
| Imagen 4 Fast | Qualquer | $0.02/imagem |
| Supabase | Pro | $25.00/mês |
| Vercel | Pro | $20.00/membro/mês |
| Apple Developer | Anual | $99.00/ano |
Para atualizar os preços do Gemini, editar gemini_cost_per_image_usd no config.