Node setup
Merit AC's ingestion API is plain JSON over HTTPS, so a Node proxy follows the exact same shape as the Python reference — issue each employee their own key, forward the call, read the token usage off the response, and fire a usage event at Merit AC before returning.
The request that matters
const usage = upstreamResponse.usage;
const costUsd = price(model, usage.input_tokens, usage.output_tokens);
await fetch(`${MERIT_API_BASE}/ingest/usage`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.MERIT_INGEST_TOKEN}`,
},
body: JSON.stringify({
source_system: "anthropic_api",
external_id: externalIdForKey(proxyKey),
tool: "anthropic_api",
model,
cost_usd: Number(costUsd.toFixed(6)),
tokens_in: usage.input_tokens,
tokens_out: usage.output_tokens,
}),
});Fire this after the response streams back to the caller (or via a queue) so a slow Merit AC ingest call never adds latency to the actual request — same note as the Python reference in backend/proxy_example.py.
Fields Merit AC expects
source_system, external_id, tool, and cost_usd are required; model, tokens_in, tokens_out, and occurred_at are optional. external_id must already exist in an IdentityMapping for your organization — map it once:
await fetch(`${MERIT_API_BASE}/admin/identity-mapping`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${dashboardJwt}`,
},
body: JSON.stringify({
email: "priya@yourcompany.com",
source_system: "anthropic_api",
external_id: "proxy-key-priya-abc123",
}),
});using the ingest_token from GET /admin/org. An unmapped external_id gets a 422 on purpose — that's a shadow-AI candidate, 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:
// PR merged
await fetch(`${MERIT_API_BASE}/ingest/outcome`, {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${MERIT_INGEST_TOKEN}` },
body: JSON.stringify({
source_system: "anthropic_api",
external_id: "proxy-key-priya-abc123",
source: "github",
outcome_type: "pr_merged",
external_ref: pullRequest.html_url,
}),
});
// PR reverted -- a quality signal, not an outcome
await fetch(`${MERIT_API_BASE}/ingest/quality-signal`, {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${MERIT_INGEST_TOKEN}` },
body: JSON.stringify({
source_system: "anthropic_api",
external_id: "proxy-key-priya-abc123",
signal_type: "pr_reverted",
external_ref: pullRequest.html_url,
}),
});Verifying it worked
Usage events land immediately, but the dashboard reads PersonScore, written by the nightly scoring job. Trigger it on demand to see today's events reflected without waiting:
await fetch(`${MERIT_API_BASE}/admin/recompute-scores`, {
method: "POST",
headers: { Authorization: `Bearer ${dashboardJwt}` },
});
const breakdown = await fetch(`${MERIT_API_BASE}/api/tool-breakdown`, {
headers: { Authorization: `Bearer ${dashboardJwt}` },
}).then((r) => r.json());If your tool shows up with the right spend, the wiring is correct.