Capture, quantify, and communicate the value of AI-assisted engineering work at WYRE Technology.
Every time an engineer finishes a Claude Code session, a stop hook automatically logs the session to a central API. The dashboard then turns that raw data into metrics: hours saved, dollar value generated, leverage ratios -- broken down by client, engineer, and task type. Use it for QBRs, internal reviews, and proving ROI on AI tooling.
Engineer's Machine WYRE Infrastructure
+---------------------+ +------------------------+
| | | |
| Claude Code | | FastAPI (uvicorn) |
| | | HTTPS | | |
| +-- Stop Hook ---+------------->+ +-- PostgreSQL |
| (Python) | POST | | |
| | /api/v1/ | +-- Next.js Dashboard|
+---------------------+ sessions | (TBD) |
+------------------------+
Data flow:
- Claude Code session ends and fires the stop hook
- Hook uses Claude CLI to classify the work (task type, summary, manual-hours estimate)
- Hook POSTs a session record to the Impact API
- API stores it in PostgreSQL with the engineer's hourly rate
- Dashboard and metrics endpoints expose hours saved, dollar value, and leverage ratios
# 1. Clone and start everything
git clone https://github.com/wyre-technology/ai-impact-tracker.git
cd ai-impact-tracker
docker compose up -d
# 2. Verify the API is running
curl http://localhost:8000/healthz
# => {"status":"ok"}
# 3. Create an engineer (dev mode, no auth required beyond the header)
curl -X POST http://localhost:8000/api/v1/admin/engineers \
-H "Content-Type: application/json" \
-H "X-Dev-Engineer-OID: 00000000-0000-0000-0000-000000000001" \
-d '{
"entra_oid": "00000000-0000-0000-0000-000000000001",
"name": "Jane Doe",
"email": "jane@wyretechnology.com",
"is_admin": true
}'
# 4. Post a test session
curl -X POST http://localhost:8000/api/v1/sessions \
-H "Content-Type: application/json" \
-H "X-Dev-Engineer-OID: 00000000-0000-0000-0000-000000000001" \
-d '{
"engineer_entra_oid": "00000000-0000-0000-0000-000000000001",
"project": "ai-impact-tracker",
"task_summary": "Initial project setup",
"task_type": "development",
"duration_minutes": 45,
"files_created": 12,
"files_modified": 3,
"lines_added": 800,
"lines_removed": 50,
"estimated_manual_hours": 4.0
}'
# 5. Check metrics
curl http://localhost:8000/api/v1/metrics/summary \
-H "X-Dev-Engineer-OID: 00000000-0000-0000-0000-000000000001"Interactive API docs are available at http://localhost:8000/docs.
The stop hook runs on each engineer's machine and automatically logs Claude Code sessions to the API.
curl -fsSL https://raw.githubusercontent.com/wyre-technology/ai-impact-tracker/main/hooks/claude-code/install.sh | bashThe installer will:
- Download
stop_hook.pyto~/.claude/hooks/ - Register it in
~/.claude/settings.json - Prompt for your Entra ID Object ID and API URL
irm https://raw.githubusercontent.com/wyre-technology/ai-impact-tracker/main/hooks/claude-code/install.ps1 | iex- Copy
hooks/claude-code/stop_hook.pyto~/.claude/hooks/stop_hook.py - Add to
~/.claude/settings.json:{ "hooks": { "Stop": [ { "hooks": [ { "type": "command", "command": "python3 ~/.claude/hooks/stop_hook.py", "timeout": 30 } ] } ] } } - Set environment variables (see Configuration below)
Set these in your shell profile (~/.zshrc, ~/.bashrc, or Windows user env vars):
| Variable | Required | Description |
|---|---|---|
WYRE_ENGINEER_OID |
Yes | Your Entra ID Object ID (from Azure Portal > Users) |
WYRE_IMPACT_API_URL |
Yes | Base URL of the Impact API (e.g. https://impact-api.wyretechnology.com) |
WYRE_IMPACT_API_KEY |
Recommended | API key (wyre_ak_...) for authentication. Get one from an admin. |
WYRE_SESSION_CLIENT |
No | Client slug to attribute work to. Set per-project in .envrc or similar. |
WYRE_IMPACT_API_TOKEN |
No | Entra ID Bearer token (fallback if no API key). |
Example .zshrc:
export WYRE_ENGINEER_OID="ab12cd34-ef56-7890-abcd-ef1234567890"
export WYRE_IMPACT_API_URL="https://impact-api.wyretechnology.com"
export WYRE_IMPACT_API_KEY="wyre_ak_a1b2c3d4e5f6..."Per-project client attribution (e.g. in a project's .envrc):
export WYRE_SESSION_CLIENT="acme-corp"| Variable | Default | Description |
|---|---|---|
IMPACT_DATABASE_URL |
postgresql+asyncpg://impact:impact@localhost:5432/impact |
Async PostgreSQL connection string |
IMPACT_DEV_MODE |
true |
Enable dev auth bypass (X-Dev-Engineer-OID header) |
IMPACT_DEBUG |
false |
Enable debug logging |
IMPACT_ENTRA_TENANT_ID |
(empty) | Azure AD tenant ID for JWT validation |
IMPACT_ENTRA_CLIENT_ID |
(empty) | Azure AD app registration client ID |
IMPACT_ENTRA_AUTHORITY |
(empty) | Entra authority URL |
IMPACT_CORS_ORIGINS |
["http://localhost:3000"] |
JSON array of allowed CORS origins |
The API supports three authentication methods, checked in order:
Generate a key via the admin UI or API (see docs/api-keys.md), then pass it as a Bearer token:
curl -H "Authorization: Bearer wyre_ak_a1b2c3d4..." https://impact-api.wyretechnology.com/api/v1/sessionsAPI keys start with the wyre_ak_ prefix. They are hashed (SHA-256) before storage -- the raw key is shown only once at creation time.
For the dashboard or other apps integrated with Azure AD, pass a standard Bearer JWT:
curl -H "Authorization: Bearer eyJ0eXAi..." https://impact-api.wyretechnology.com/api/v1/sessionsThe API validates the token against your tenant's JWKS endpoint and extracts the oid claim.
When IMPACT_DEV_MODE=true (the default in docker-compose), pass the engineer's Entra OID directly:
curl -H "X-Dev-Engineer-OID: ab12cd34-ef56-..." http://localhost:8000/api/v1/sessionsThis mode must never be enabled in production.
Base URL: /api/v1
All endpoints except /healthz require authentication.
Ingest a session from the stop hook.
Request body:
{
"engineer_entra_oid": "ab12cd34-...",
"client_slug": "acme-corp",
"project": "infra-automation",
"task_summary": "Wrote Terraform modules for AKS cluster",
"task_type": "iac",
"duration_minutes": 45,
"tool_calls": {"Edit": 12, "Bash": 8, "Read": 20},
"files_created": 5,
"files_modified": 3,
"lines_added": 320,
"lines_removed": 40,
"estimated_manual_hours": 4.0,
"notes": null
}Task types: iac, documentation, scripting, troubleshooting, admin, development, analysis, other
Response: 201 Created with the full session object including id, created_at, and computed hourly_rate.
List sessions with pagination and filters.
Query parameters:
| Param | Type | Description |
|---|---|---|
page |
int | Page number (default: 1) |
page_size |
int | Items per page (default: 20, max: 100) |
client_id |
UUID | Filter by client |
engineer_id |
UUID | Filter by engineer |
task_type |
string | Filter by task type |
start |
datetime | Sessions after this time |
end |
datetime | Sessions before this time |
Response:
{
"items": [...],
"total": 142,
"page": 1,
"page_size": 20
}Aggregate metrics across all sessions. Supports start, end, client_id, and engineer_id query filters.
Response:
{
"total_sessions": 142,
"total_ai_hours": 71.5,
"total_estimated_manual_hours": 428.0,
"hours_saved": 356.5,
"dollar_value": 96300.00,
"ai_leverage_ratio": 5.99,
"by_task_type": {
"iac": {
"sessions": 45,
"ai_hours": 22.0,
"estimated_manual_hours": 180.0,
"hours_saved": 158.0,
"dollar_value": 40500.00
}
},
"by_month": [
{"month": "2026-01", "sessions": 30, "hours_saved": 75.0, "dollar_value": 18750.00}
]
}Per-client metric breakdown. Supports start and end query filters.
Response: Array of ClientMetrics objects with client_id, client_name, total_sessions, total_ai_hours, total_estimated_manual_hours, hours_saved, dollar_value, and ai_leverage_ratio.
Per-engineer metric breakdown. Supports start and end query filters.
Response: Array of EngineerMetrics objects with engineer_id, engineer_name, and the same metric fields as client metrics.
List all clients.
Get a single client by slug.
List all engineers.
Get a single engineer by UUID.
All admin endpoints require the authenticated user to have is_admin = true.
| Method | Path | Description |
|---|---|---|
POST |
/api/v1/admin/sessions |
Create a session manually (backfill). Accepts engineer_id (UUID) instead of entra_oid. Supports created_at for backdating. |
PUT |
/api/v1/admin/sessions/{id} |
Edit/correct a session. Partial updates supported. |
DELETE |
/api/v1/admin/sessions/{id} |
Delete a session. |
| Method | Path | Description |
|---|---|---|
POST |
/api/v1/admin/engineers |
Add an engineer. Fields: entra_oid, name, email, default_hourly_rate (default: 225.00), is_admin. |
PUT |
/api/v1/admin/engineers/{id} |
Update engineer fields (name, email, rate, active, is_admin). |
| Method | Path | Description |
|---|---|---|
POST |
/api/v1/admin/clients |
Add a client. Fields: name, slug, monthly_mrr. |
PUT |
/api/v1/admin/clients/{id} |
Update client fields (name, slug, MRR, active). |
| Method | Path | Description |
|---|---|---|
GET |
/api/v1/admin/settings |
Get global settings (currently: default_hourly_rate). |
PUT |
/api/v1/admin/settings |
Update global settings. |
| Method | Path | Description |
|---|---|---|
POST |
/api/v1/admin/api-keys |
Create an API key for an engineer. Returns the raw key once. |
GET |
/api/v1/admin/api-keys |
List all API keys (prefix only, never the full key). |
DELETE |
/api/v1/admin/api-keys/{id} |
Revoke an API key. |
See docs/api-keys.md for the full API key lifecycle guide.
| Method | Path | Description |
|---|---|---|
POST |
/api/v1/admin/export |
Export sessions as CSV. Body accepts start, end, client_id, engineer_id filters. |
Unauthenticated health check. Returns {"status": "ok"}.
The Next.js dashboard (in apps/web/, TBD) will provide:
- Summary page -- total hours saved, dollar value, leverage ratio, trend charts
- Client breakdown -- per-client metrics table and charts
- Engineer breakdown -- per-engineer metrics table
- Session log -- paginated, filterable list of all sessions
- Admin panel -- manage engineers, clients, settings, and API keys
Until the dashboard is built, use the interactive API docs at /docs or query the metrics endpoints directly.
- Start the API (
docker compose up -d) - Create your first engineer as an admin:
curl -X POST http://localhost:8000/api/v1/admin/engineers \ -H "Content-Type: application/json" \ -H "X-Dev-Engineer-OID: YOUR_ENTRA_OID" \ -d '{ "entra_oid": "YOUR_ENTRA_OID", "name": "Your Name", "email": "you@wyretechnology.com", "is_admin": true }'
- Create API keys for each engineer (see docs/api-keys.md)
- Create clients for work attribution:
curl -X POST http://localhost:8000/api/v1/admin/clients \ -H "Content-Type: application/json" \ -H "X-Dev-Engineer-OID: YOUR_ENTRA_OID" \ -d '{"name": "Acme Corp", "slug": "acme-corp", "monthly_mrr": 5000.00}'
- Distribute API keys and install instructions to your team
- Set
default_hourly_rateper engineer to calculate dollar value of time saved - Set
is_admin: truefor users who need access to admin endpoints - Deactivate engineers with
PUT /api/v1/admin/engineers/{id}and{"active": false}
Use POST /api/v1/admin/sessions to manually enter historical sessions. The created_at field accepts a datetime for backdating.
Use POST /api/v1/admin/export to download a CSV of all sessions, optionally filtered by date range, client, or engineer.
The infra/ directory contains OpenTofu configuration for deploying to Azure Container Apps.
Required environment variables in production:
IMPACT_DATABASE_URL=postgresql+asyncpg://user:pass@host:5432/impact
IMPACT_DEV_MODE=false
IMPACT_ENTRA_TENANT_ID=your-tenant-id
IMPACT_ENTRA_CLIENT_ID=your-client-id
IMPACT_CORS_ORIGINS=["https://impact.wyretechnology.com"]
Critical: Set IMPACT_DEV_MODE=false in production. This disables the X-Dev-Engineer-OID header bypass and requires real authentication (API key or Entra JWT).
# Start PostgreSQL and API with hot reload
docker compose up -d
# API: http://localhost:8000
# Docs: http://localhost:8000/docs
# Health: http://localhost:8000/healthzThe docker-compose setup runs with IMPACT_DEV_MODE=true, so you can authenticate with just the X-Dev-Engineer-OID header.
cd apps/api
alembic upgrade headapps/api/ FastAPI backend (Python, async SQLAlchemy, Pydantic v2)
apps/web/ Next.js dashboard (TBD)
hooks/claude-code/ Claude Code stop hook + installers
infra/ OpenTofu (Azure Container Apps)
docs/ Additional documentation
cd apps/api
pytestSee CONTRIBUTING.md.