Self-hosted API usage dashboard for Firecrawl, DeepSeek, OpenAI, Anthropic/Claude, OpenRouter, and custom HTTP usage endpoints. It stores provider credentials encrypted at rest, polls usage/balance APIs, renders a dark React/MUI dashboard, and exposes a flat Homepage Dashboard endpoint.
- Backend: FastAPI, SQLAlchemy async, asyncpg, Alembic, cryptography/Fernet
- Frontend: Vite, React, MUI, React Router
- Runtime: PostgreSQL, nginx-based frontend/proxy image, Docker Compose
cp .env.example .env
openssl rand -base64 32 | tr '+/' '-_'
# paste that value into ENCRYPTION_KEY in .env
# generate an admin token and paste it into ADMIN_TOKEN in .env
# optional if you only expose /api/v1/homepage through HOMEPAGE_ALLOWED_HOSTS
openssl rand -hex 32
docker compose pull
docker compose up -dOpen through the frontend container, which also proxies API traffic to the backend:
- Frontend: http://localhost:3000
- Backend health: http://localhost:3000/health
- Homepage payload: http://localhost:3000/api/v1/homepage (requires
Authorization: Bearer <ADMIN_TOKEN>)
Set NGINX_HTTP_PORT in .env to change the external HTTP port. PostgreSQL is intentionally internal-only and is not published on the host. The default Compose stack pulls published images from GitHub Container Registry; set IMAGE_TAG, BACKEND_IMAGE, or FRONTEND_IMAGE to pin or override them.
| Variable | Description |
|---|---|
DATABASE_URL |
Async SQLAlchemy URL. Defaults to the Compose PostgreSQL service. |
POSTGRES_IMAGE |
PostgreSQL image used by Compose. Defaults to postgres:18-alpine; the Compose volume is mounted at /var/lib/postgresql for the 18+ image layout. |
ENCRYPTION_KEY |
Required Fernet key used to encrypt API credentials at rest. |
ADMIN_TOKEN |
Optional bearer token for dashboard config, usage, polling, and homepage API routes. Keep it set for the admin UI; it may be blank only when HOMEPAGE_ALLOWED_HOSTS is used for homepage-only access behind an external auth proxy. |
HOMEPAGE_ALLOWED_HOSTS |
Comma-separated hostnames that may access GET /api/v1/homepage without ADMIN_TOKEN; ports are ignored. All other API routes still require admin auth. |
IMAGE_TAG |
Tag for the default GHCR backend/frontend images. Defaults to latest. |
BACKEND_IMAGE |
Optional full backend image override. Defaults to ghcr.io/skulldorom/usage-dashboard-backend:${IMAGE_TAG}. |
FRONTEND_IMAGE |
Optional full frontend image override. Defaults to ghcr.io/skulldorom/usage-dashboard-frontend:${IMAGE_TAG}. |
NGINX_HTTP_PORT |
Host port published by the frontend/proxy container. Defaults to 3000. |
BACKEND_CORS_ORIGINS |
Comma-separated allowed origins for the FastAPI API. |
VITE_API_BASE_URL |
Frontend API base path baked into the published frontend image. Defaults to /api. |
AUTO_POLL_ENABLED |
Enables background provider polling. Defaults to true. |
AUTO_POLL_INTERVAL_MINUTES |
Minutes between automatic provider polls. Defaults to 60. |
Uses GET https://api.firecrawl.dev/v2/team/credit-usage for current remaining/plan credits and GET https://api.firecrawl.dev/v2/team/credit-usage/historical for usage consumed in the billing period.
Uses GET https://api.deepseek.com/user/balance.
Uses GET https://api.openai.com/v1/organization/costs. This endpoint requires an organization admin key
Uses GET https://api.anthropic.com/v1/organizations/usage_report/messages for the last 24 hours. This requires an Anthropic Admin API key from Console → Settings → Organization → Admin API Keys, not a normal inference key.
Uses GET https://openrouter.ai/api/v1/key with the same bearer token used for inference and surfaces remaining, daily, weekly, and monthly credit usage.
Choose Custom HTTP in Settings, provide a base URL, relative path, auth header template, and one JSON path metric. The encrypted API key is inserted into the auth header template via {api_key}. Do not put secrets in the base URL or path; the backend rejects credential-looking URLs.
Example metric config generated by the form:
{
"method": "GET",
"path": "/v1/billing",
"auth_header_name": "Authorization",
"auth_header_template": "Bearer {api_key}",
"metrics": [
{"label": "remaining", "path": "$.credits.remaining", "unit": "credits", "maximum_path": "$.credits.limit"}
]
}The Compose stack no longer has a separate nginx service. The frontend service is the nginx-based static frontend and API proxy. Use the URL that matches where Homepage is running:
- Same Docker Compose project/network:
http://frontend/api/v1/homepage - Separate Compose project on a shared Docker network: connect Homepage to the Usage Dashboard network and use
http://frontend/api/v1/homepage, or add a network alias such asusage-dashboardand usehttp://usage-dashboard/api/v1/homepage - Host/LAN access through the published port:
http://<server-ip-or-dns>:${NGINX_HTTP_PORT:-3000}/api/v1/homepage - Public reverse-proxy access:
https://usage.example.com/api/v1/homepage
If you use HOMEPAGE_ALLOWED_HOSTS, include the hostname that reaches the frontend/proxy and is forwarded to the backend. For internal Docker calls that is usually frontend or your network alias; for public access it is your external hostname.
Two widget formats are supported. Pick one.
The default block display shows individual fields as labelled rows. Use this for a compact summary tile:
- API Usage:
icon: mdi-api
widget:
type: customapi
url: http://frontend/api/v1/homepage
# Optional when HOMEPAGE_ALLOWED_HOSTS includes frontend.
# headers:
# Authorization: Bearer ***
refreshInterval: 300000
mappings:
- field: summary
label: Providers
- field: configured_providers
label: Configured
- field: healthy_providers
label: Healthy
- field: degraded_providers
label: DegradedFlattened metrics keys (e.g. firecrawl_main_credits_remaining, deepseek_main_total_balance) are also available as extra field mappings.
Requires Homepage ≥ 1.1.0. Set display: dynamic-list and use the object-style mappings below. Each enabled provider config becomes a row with its label on the left and usage text on the right.
display: dynamic-list is mandatory -- omitting it causes TypeError: s.slice is not a function because Homepage tries to treat the object-style mappings as a block-display array.
- API Usage:
icon: mdi-api
widget:
type: customapi
url: http://frontend/api/v1/homepage
display: dynamic-list
# Optional when HOMEPAGE_ALLOWED_HOSTS includes frontend.
# headers:
# Authorization: Bearer ***
refreshInterval: 300000
mappings:
items: list
name: label
label: value
format: textThe list array contains one flat object per enabled provider config:
label→ left side (e.g.deepseek (main))value→ right side (prefers remaining credits/usage, then percent-used, then summary fallback)
The existing scalar fields (summary, configured_providers, healthy_providers, degraded_providers) and flattened metrics object remain in the response for use with Option A or extra mappings.
Set HOMEPAGE_ALLOWED_HOSTS when a trusted proxy such as Authentik protects the public hostname and you only want the flat homepage payload to be readable without sharing ADMIN_TOKEN:
ADMIN_TOKEN=
HOMEPAGE_ALLOWED_HOSTS=usage.example.com,status.localOnly GET /api/v1/homepage checks this allowlist. /configs, /poll, /usage, and history endpoints still return 401 without a valid admin bearer token. Hostnames are matched case-insensitively and any port suffix is ignored.
cd backend
python -m venv .venv
. .venv/bin/activate
pip install -r requirements.txt
export ENCRYPTION_KEY=$(python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())")
pytestcd frontend
npm install
npm run dev
npm run buildTo build local images instead of using GHCR, run explicit builds with the Dockerfiles:
docker build -t usage-dashboard-backend:local -f backend/Dockerfile .
docker build -t usage-dashboard-frontend:local -f frontend/Dockerfile .
BACKEND_IMAGE=usage-dashboard-backend:local FRONTEND_IMAGE=usage-dashboard-frontend:local docker compose up -d