Merit AC
Setup guide Working example, ships with the repo

Python setup

Employees don't call the Anthropic/OpenAI API directly with a shared org key — they call a thin internal proxy. The proxy issues each employee their own logical key, forwards the request to the real provider, reads the token usage back off the response, prices it, and fires a UsageEvent at Merit AC — all before the response reaches the caller. No code changes in the calling application; just point ANTHROPIC_BASE_URL at the proxy instead of the vendor.

The proxy, stripped to the part that matters

@app.post("/v1/messages")
async def proxy_anthropic_messages(request: Request, x_merit_proxy_key: str = Header(...)):
    external_id = PROXY_KEY_TO_EXTERNAL_ID[x_merit_proxy_key]
    body = await request.json()

    upstream_resp = await client.post(f"{ANTHROPIC_UPSTREAM}/v1/messages", json=body, ...)
    payload = upstream_resp.json()

    usage = payload["usage"]
    cost_usd = price(body["model"], usage["input_tokens"], usage["output_tokens"])

    await client.post(MERIT_INGEST_URL, json={
        "source_system": "anthropic_api",
        "external_id": external_id,
        "tool": "anthropic_api",
        "model": body["model"],
        "cost_usd": round(cost_usd, 6),
        "tokens_in": usage["input_tokens"],
        "tokens_out": usage["output_tokens"],
    }, headers={"Authorization": f"Bearer {MERIT_INGEST_TOKEN}"})

    return payload

The full, runnable reference lives at backend/proxy_example.py in the Merit AC repo — same pattern LiteLLM's proxy uses, just with a Merit AC ingest call added at the end. Fire the ingest call after the response streams back to the caller (or via a background task/queue), so a slow Merit AC call never adds latency to the employee's actual request.

Getting your token

Every organization has its own ingest_token, from GET /admin/org once you're signed in. Set it as MERIT_INGEST_TOKEN in the proxy's environment.

Mapping identities before you ingest

Before your first event, map the proxy key to a real person via POST /admin/identity-mapping:

requests.post(f"{MERIT_API_BASE}/admin/identity-mapping", json={
    "email": "priya@yourcompany.com",
    "source_system": "anthropic_api",
    "external_id": "proxy-key-priya-abc123",
}, headers={"Authorization": f"Bearer {DASHBOARD_JWT}"})

An unmapped external_id gets a 422 on ingest, on purpose — that's a shadow-AI candidate (spend nobody's accounted for), not something to silently drop.

Outcomes and quality signals

These don't flow through the proxy — they come from webhooks on the systems where the work actually lands. A GitHub PR merge:

requests.post(f"{MERIT_API_BASE}/ingest/outcome", json={
    "source_system": "anthropic_api",
    "external_id": "proxy-key-priya-abc123",
    "source": "github",
    "outcome_type": "pr_merged",
    "external_ref": pr_html_url,
}, headers={"Authorization": f"Bearer {MERIT_INGEST_TOKEN}"})

And a revert, which counts as a quality signal instead:

requests.post(f"{MERIT_API_BASE}/ingest/quality-signal", json={
    "source_system": "anthropic_api",
    "external_id": "proxy-key-priya-abc123",
    "signal_type": "pr_reverted",
    "external_ref": pr_html_url,
}, headers={"Authorization": f"Bearer {MERIT_INGEST_TOKEN}"})

See github_ingest.py in the repo for a working, whole-repo GitHub sync that does this automatically instead of a one-off webhook handler.

Verifying it worked

Usage events land in UsageEvent immediately, but the dashboard and /api/* only ever read PersonScore, which is written by the nightly scoring job. To see today's events reflected without waiting for the schedule, trigger it on demand:

requests.post(f"{MERIT_API_BASE}/admin/recompute-scores",
    headers={"Authorization": f"Bearer {DASHBOARD_JWT}"})
# then:
requests.get(f"{MERIT_API_BASE}/api/tool-breakdown",
    headers={"Authorization": f"Bearer {DASHBOARD_JWT}"}).json()

If your tool shows up with the right spend, the wiring is correct. If ingestion returned a 422 anywhere along the way, that identity mapping step above is the thing to check first.