The Integrations MCP is a read-only, per-org, PAT-authenticated JSON-RPC surface for sibling products in the Cirrus AI suite (RAIA, FinOps, custom in-house tools) to pull traces, metrics, and usage data from Argus without going through the admin-session UI. It runs at POST /v1/integrations/mcp.
Argus exposes four orthogonal surfaces. Pick this one when the caller is another product, not a user, an operator, or an LLM client.
| Surface | Caller | Auth | Shape |
|---|---|---|---|
| /v1/chat/completions | LLM client (Claude Code, Cursor, custom SDK) | Virtual key | OpenAI-compatible |
| /v1/admin/* | Human operator via UI, or automation | Session cookie or admin-scope PAT | REST |
| /v1/argus/mcp | Operator tooling / agent acting as an operator | PAT only (never a session cookie) | JSON-RPC 2.0 / MCP |
| /v1/integrations/mcp | Sibling product (RAIA, FinOps) | Service-account PAT, integration scope | JSON-RPC 2.0 / MCP |
A cold-start walkthrough — go from "no token" to a first useful call. Every following section assumes you have NEXUS_PAT and NEXUS_URL set.
# Once you have the token, put it in your caller's secret store.
# For quick local tests:
export NEXUS_PAT="nxat_...paste-token-here..."
export NEXUS_URL="https://nexus.cirruslabs.technology"curl -sS -X POST "$NEXUS_URL/v1/integrations/mcp" \
-H "Authorization: Bearer $NEXUS_PAT" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": { "name": "integration.ping", "arguments": {} }
}' | jqExpected response:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [
{
"type": "text",
"text": "{\"pong\":true,\"org_id\":\"org_abc\",\"product_slug\":\"raia\",\"scope\":\"integration\",\"server_time\":\"2026-07-07T04:12:00.000Z\"}"
}
],
"isError": false
}
}"error":"unauthorized" — the token is wrong or was revoked. If you see "error":"suite_product_disabled" — the org needs the product enabled (Settings → Suite Products).result.content[0].text. Get comfortable with the double-parse:# The content is a JSON string inside a JSON envelope.
# Extract the inner payload once and reuse the shape for every tool call.
RESULT=$(curl -sS -X POST "$NEXUS_URL/v1/integrations/mcp" \
-H "Authorization: Bearer $NEXUS_PAT" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0", "id": 1, "method": "tools/call",
"params": { "name": "integration.metrics.overview", "arguments": {} }
}')
# Pull the inner text and re-parse
echo "$RESULT" | jq -r '.result.content[0].text' | jqintegration.metrics.overview for a one-shot KPI snapshot, or integration.traces.list to pull the last hour of activity. Full reference below.json.loads(resp.json()["result"]["content"][0]["text"]).Callers hold a service-account Personal Access Token with two required bindings. Read-only here is a property of the integration scope, not of service accounts as such — a service account minted with admin or write scope can perform admin writes, it just cannot call this surface.
integrationDedicated scope mapping to a fixed permission set: integration:traces:read, integration:metrics:read, integration:usage:read. Those three are the only permissions the integration scope carries, so a token scoped this way can read but never mutate.
raia | finops | <custom>The sibling product the token represents. Emitted on every audit row and gated by the org's Suite Products entitlement (see below). Metadata-only today; a future release will make this binding load-bearing.
admin scope and the integration:* permissions are disjoint by design, and this trips people up. An admin-scoped PAT is not a superset of an integration one: it deliberately cannot reach this surface and gets permission_denied on every tool here. Instance-admin status does not help either — the integration:* permissions are excluded from the admin short-circuit on purpose. Nor is the reverse true: an integration token cannot touch /v1/admin/* or /v1/argus/mcp. integration is a separate identity domain for sibling-product attribution, not a lower rung on a ladder. A human PAT cannot even be minted with it — only an org-owned service account can hold integration scope. If you are getting permission_denied here with a working admin token, this is why: you need a second, separately minted service-account token.suite_product_disabled. If Redis is unreachable, calls still proceed — the gate never brings the surface down.Every non-trivial tool takes at least one resource id. Here's how to look them up.
From the Control Plane: Keys page — the id is the first column, displayed as vk_.... Click the row to open the detail page; the URL carries the id.
Programmatically:
# List keys for your org (admin session or admin PAT — this endpoint
# is NOT part of the integration surface; it's how a human operator
# looks up IDs to hand off to the integration caller).
curl -sS "$NEXUS_URL/v1/admin/keys" \
-H "Authorization: Bearer $ADMIN_PAT" | jq '.keys[] | { id, name, keyType }'
# Or from a running trace: the trace summary includes virtual_key_id.
# Grab a recent trace's key id in one call:
curl -sS -X POST "$NEXUS_URL/v1/integrations/mcp" \
-H "Authorization: Bearer $NEXUS_PAT" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"integration.traces.list","arguments":{"limit":1}}}' \
| jq -r '.result.content[0].text' | jq '.traces[0].virtual_key_id'$ADMIN_PAT is an admin-scoped PAT — different from $NEXUS_PAT. The admin endpoints are outside the integration surface; they're what your operators use to hand off IDs to you.From the Control Plane: Teams page — click any team; the URL becomes /teams/<team_id>. Copy from the address bar.
Programmatically:
# List teams for your org
curl -sS "$NEXUS_URL/v1/admin/teams" \
-H "Authorization: Bearer $ADMIN_PAT" | jq '.teams[] | { id, name, slug }'
# You can also read team IDs off cost_attribution — every by_team row
# includes team_id + team_name so you can build a lookup on the fly.
curl -sS -X POST "$NEXUS_URL/v1/integrations/mcp" \
-H "Authorization: Bearer $NEXUS_PAT" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"integration.metrics.cost_attribution","arguments":{}}}' \
| jq -r '.result.content[0].text' | jq '.by_team[] | { team_id, team_name }'cost_attribution returns team_id + team_name together, so you can render your report without a separate lookup.Trace IDs come from two places:
integration.traces.list — every summary carries trace_id./traces/<trace_id>.# Trace IDs come from two places:
#
# 1. integration.traces.list — every summary carries trace_id.
# 2. The Control Plane Traces page — click any row, the URL is
# /traces/<trace_id>. Copy from the address bar.
#
# Once you have a trace_id, fetch spans or the RAIA transcript:
curl -sS -X POST "$NEXUS_URL/v1/integrations/mcp" \
-H "Authorization: Bearer $NEXUS_PAT" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0", "id": 1, "method": "tools/call",
"params": {
"name": "integration.traces.get",
"arguments": { "trace_id": "01J1Z...", "format": "raia_transcript" }
}
}' | jq -r '.result.content[0].text' | jqEvery tool that queries analytics accepts start and end as ISO 8601 timestamps. Omit both to get the trailing 24h — Argus fills in the defaults on the server. Buckets snap to calendar boundaries (UTC), so two callers asking for the same window get the same bucket edges.
You don't have to hunt through the Traces UI for the row you care about — the gateway accepts three different headers that let the caller pre-stamp requests with an identifier or label. Pick whichever fits your architecture; you can combine traceparent/x-argus-trace-id with x-argus-trace-tag if you want both a stable id and a structured tag.
traceparent — W3C standard trace idMost OpenTelemetry-instrumented SDKs emit this header by default. Argus adopts the 32-hex trace-id portion as the row's trace_id, so the same id appears on both the caller's side and in Argus. Recommended when the caller already runs an OTel pipeline.
# W3C traceparent — most OTel-instrumented SDKs emit this by default.
# Argus adopts it as the row's trace_id, so the same id appears on both
# sides of the call.
curl -sS -X POST "$NEXUS_URL/v1/chat/completions" \
-H "Authorization: Bearer $NEXUS_VK" \
-H "traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01" \
-H "Content-Type: application/json" \
-d '{"model":"claude-sonnet-4-6","messages":[{"role":"user","content":"hi"}]}'
# Later, pull that exact conversation:
curl -sS -X POST "$NEXUS_URL/v1/integrations/mcp" \
-H "Authorization: Bearer $NEXUS_PAT" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"integration.traces.get",
"arguments":{
"trace_id":"0af7651916cd43dd8448eb211c80319c",
"format":"raia_transcript"
}}}'x-argus-trace-id — explicit overrideFor clients that can't emit W3C traceparent. Whatever string you send becomes the trace's canonical id. Handy for evaluation frameworks that want to pin a deterministic id to each item in a batch.
# For clients that can't emit W3C traceparent, Argus honors an override.
# Same effect — your string becomes the trace's canonical id.
curl -sS -X POST "$NEXUS_URL/v1/chat/completions" \
-H "Authorization: Bearer $NEXUS_VK" \
-H "x-argus-trace-id: eval-run-2026-07-07-batch-42-item-018" \
-H "Content-Type: application/json" \
-d '{"model":"claude-sonnet-4-6","messages":[...]}'x-argus-trace-tag — structured labelA caller-provided label up to 128 characters, stored alongside the trace and echoed back on every traces.list row. Use this when you don't care about picking the trace_id yourself but do want to filter your own traffic later — feature flag, cohort, evaluation batch, canary experiment, anything.
feature=summarize|user=u123|exp=v2. That gives you a substring you can search for later.# x-argus-trace-tag is a 128-char label. Use it when you don't want to
# hand-pick the trace_id but DO want to filter your own traffic later
# (feature flag, cohort, evaluation batch, ...).
curl -sS -X POST "$NEXUS_URL/v1/chat/completions" \
-H "Authorization: Bearer $NEXUS_VK" \
-H "x-argus-trace-tag: feature=summarize|user=u123|exp=v2" \
-H "Content-Type: application/json" \
-d '{"model":"claude-sonnet-4-6","messages":[...]}'
# Every traces.list row echoes the tag back:
curl -sS -X POST "$NEXUS_URL/v1/integrations/mcp" \
-H "Authorization: Bearer $NEXUS_PAT" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"integration.traces.list","arguments":{"limit":100}}}' \
| jq -r '.result.content[0].text' \
| jq '.traces[] | select(.trace_tag | contains("feature=summarize"))'integration.traces.list returns trace_tag on every row but does not yet accept it as a filter server- side. Pull recent traces and filter client-side (as in the jq example above) until the filter lands. Filed as a followup ticket./v1/chat/completions, /v1/embeddings, etc.) — set them once at the caller's HTTP client and every subsequent tool call inherits them. The gateway also honors traceparent on /v1/integrations/mcp itself, so RAIA/FinOps can pin their own trace id to the audit-log row too.Standard JSON-RPC 2.0 with an MCP-shaped tools/call method:
POST /v1/integrations/mcp HTTP/1.1
Host: nexus.cirruslabs.technology
Authorization: Bearer <PAT>
Content-Type: application/json
{
"jsonrpc": "2.0",
"id": "req-1",
"method": "tools/call",
"params": {
"name": "integration.ping",
"arguments": {}
}
}Successful calls return { content: [{type:"text", text:"..."}], isError: false }. The tool serialises its response as a single JSON string inside that text block — hence the double-parse in the walkthrough. Errors use the same envelope with isError: true; the inner JSON carries an error plus a code:
{
"jsonrpc": "2.0",
"id": "req-1",
"result": {
"content": [
{ "type": "text", "text": "{\"error\":\"permission_denied\",\"required\":\"integration:usage:read\"}" }
],
"isError": true
}
}| code | meaning | what to do |
|---|---|---|
| unauthorized | Missing/invalid PAT or the token was revoked. | Re-mint the token; check for typos in the header. |
| permission_denied | Token scope does not grant the tool's required permission. | The response includes the missing permission. Check the token scope; integration PATs should carry the integration scope. |
| suite_product_disabled | Token's productSlug is not enabled for the org. | Ask a Argus admin to enable the product under Settings → Suite Products. |
| not_found | Resource id (trace, key, team) either doesn't exist or belongs to a different org. Always returned for cross-tenant lookups — never leaks existence. | Double-check you're using an id from your own org. |
| invalid_request | Schema violation (bad window, missing required arg). | The error message names the offending field. |
| unknown_tool | Tool name is not registered in this release. | Refresh your tool inventory with tools/list. |
All tools are read-only and org-scoped. Org id is derived from the calling PAT — any orgId passed in arguments is silently ignored.
Smoke test. Confirms auth and dispatch are healthy; echoes back the org and product slug the token resolved to. Use this from a health-check to catch entitlement regressions early.
Arguments: none.
List recent trace summaries grouped by trace_id. Cursor-paginated; supports a time window and rich filters.
Arguments: start, end, limit (1–200, default 50), cursor, filters.virtual_key_id, filters.model, filters.provider, filters.has_error, filters.include_internal.
# All errored traces in the last 24h, newest first
curl -sS -X POST "$NEXUS_URL/v1/integrations/mcp" \
-H "Authorization: Bearer $NEXUS_PAT" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0", "id": 1, "method": "tools/call",
"params": {
"name": "integration.traces.list",
"arguments": {
"limit": 100,
"filters": { "has_error": true, "include_internal": false }
}
}
}' | jq -r '.result.content[0].text' | jq '.traces[] | { trace_id, model, cost_usd }'# Recent traces for a single virtual key — useful when a customer
# reports "my key is behaving oddly, can you look?"
curl -sS -X POST "$NEXUS_URL/v1/integrations/mcp" \
-H "Authorization: Bearer $NEXUS_PAT" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0", "id": 1, "method": "tools/call",
"params": {
"name": "integration.traces.list",
"arguments": {
"start": "2026-07-06T00:00:00Z",
"limit": 50,
"filters": { "virtual_key_id": "vk_abc" }
}
}
}'The response carries traces[] (summaries with trace_id, model, provider, cost_usd, duration_ms, has_error, ...), next_cursor for pagination, and total (approximate count in the window).
Return a single trace by id. format="spans" (default) yields the raw span array — good for debugging. format="raia_transcript" yields the RAIA-compatible envelope (messages, model, provider, cost, tokens) that RAIA's eval pipeline expects.
Arguments: trace_id (required), format ∈ spans | raia_transcript. Foreign-org trace ids return not_found.
{
"trace_id": "01J1Z...",
"conversation_id": "conv_abc",
"started_at": "2026-07-06T18:14:22.104Z",
"messages": [
{ "role": "user", "content": "Summarise this incident report.", "timestamp": "..." },
{ "role": "assistant", "content": "The report describes...", "timestamp": "..." },
{ "role": "tool", "name": "lookup_ticket", "arguments": {}, "result": {} }
],
"model": "claude-sonnet-4-6",
"provider": "anthropic",
"cost_usd": 0.0423,
"total_tokens": 3187
}Org-level KPI snapshot for a window: total requests, total cost, p95 latency, cache hit rate, error rate. Same query the Argus dashboard uses. Ideal for a nightly "how'd we do yesterday?" digest.
Arguments: start, end (both default to trailing 24h).
{
"start": "2026-07-01T00:00:00.000Z",
"end": "2026-07-07T00:00:00.000Z",
"total_requests": 128432,
"total_cost_usd": 1284.19,
"p95_latency_ms": 1820,
"cache_hit_rate": 0.34,
"error_rate": 0.007
}Time-series values for a single metric, optionally grouped by a dimension. Buckets snap to calendar boundaries in UTC so multiple callers see identical alignment.
Arguments: metric (required), group_by ∈ none (default) | model | provider | team | virtual_key, bucket ∈ hour | day (default) | week, start, end, filters.* (same set as traces.list).
# Daily spend broken down by model — the primary series RAIA
# plots on its "cost trend" widget.
curl -sS -X POST "$NEXUS_URL/v1/integrations/mcp" \
-H "Authorization: Bearer $NEXUS_PAT" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0", "id": 1, "method": "tools/call",
"params": {
"name": "integration.metrics.timeseries",
"arguments": {
"metric": "cost",
"group_by": "model",
"bucket": "day",
"start": "2026-07-01T00:00:00Z",
"end": "2026-07-07T00:00:00Z"
}
}
}'{
"metric": "cost",
"bucket": "day",
"buckets": ["2026-07-01", "2026-07-02", "2026-07-03", "..."],
"series": [
{ "key": "claude-sonnet-4-6", "values": [12.4, 18.1, 22.9, "..."] },
{ "key": "gpt-4.1", "values": [ 8.2, 9.7, 10.4, "..."] }
]
}The primary chargeback query. Cost + request count grouped three ways in a single round-trip: by team, by (model + provider), and by caller class (client vs internal). One call renders a full chargeback report.
Arguments: start, end, top_models_limit (1–100, default 20).
# The one-call chargeback query — spend split three ways
# (team, model+provider, caller class) for a billing month.
curl -sS -X POST "$NEXUS_URL/v1/integrations/mcp" \
-H "Authorization: Bearer $NEXUS_PAT" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0", "id": 1, "method": "tools/call",
"params": {
"name": "integration.metrics.cost_attribution",
"arguments": {
"start": "2026-06-01T00:00:00Z",
"end": "2026-07-01T00:00:00Z",
"top_models_limit": 20
}
}
}'{
"by_team": [
{ "team_id": "team_platform", "team_name": "Platform", "cost_usd": 412.10, "requests": 41230 },
{ "team_id": "team_ml", "team_name": "ML", "cost_usd": 318.45, "requests": 22004 }
],
"by_model": [
{ "model": "claude-sonnet-4-6", "provider": "anthropic", "cost_usd": 612.87, "requests": 45120 },
{ "model": "gpt-4.1", "provider": "openai", "cost_usd": 401.22, "requests": 33044 }
],
"by_caller_class": [
{ "caller_class": "client", "cost_usd": 1102.44, "requests": 118032 },
{ "caller_class": "internal", "cost_usd": 181.75, "requests": 10400 }
]
}End-to-end latency bands (p50 / p75 / p90 / p95 / p99 / p99.9), time-to-first-token (TTFT) bands, and average tokens/sec per (provider, model) tuple. Backs RAIA's "model performance" view; useful for tuning model choice or catching a provider regression.
Arguments: start, end.
{
"series": [
{
"provider": "anthropic",
"model": "claude-sonnet-4-6",
"requests": 41230,
"latency_ms": { "p50": 720, "p75": 940, "p90": 1220, "p95": 1810, "p99": 3200, "p999": 6100 },
"ttft_ms": { "p50": 120, "p75": 165, "p90": 210, "p95": 290, "p99": 480, "p999": 812 },
"tokens_per_second": 92.4
}
]
}Usage totals + bucketed series for one virtual key. Optional grouping by model or provider. Returns not_found if the key doesn't belong to your org (checked before the ClickHouse query fires — no timing side-channel).
Arguments: virtual_key_id (required), start, end, bucket ∈ hour | day (default) | week, group_by ∈ model | provider (optional).
# Weekly usage for one key, bucketed daily, split by model
curl -sS -X POST "$NEXUS_URL/v1/integrations/mcp" \
-H "Authorization: Bearer $NEXUS_PAT" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0", "id": 1, "method": "tools/call",
"params": {
"name": "integration.usage.by_key",
"arguments": {
"virtual_key_id": "vk_abc",
"start": "2026-07-01T00:00:00Z",
"end": "2026-07-08T00:00:00Z",
"bucket": "day",
"group_by": "model"
}
}
}'{
"virtual_key_id": "vk_abc",
"start": "2026-07-01T00:00:00.000Z",
"end": "2026-07-08T00:00:00.000Z",
"totals": {
"request_count": 41230,
"prompt_tokens": 18234000,
"completion_tokens": 8100200,
"cached_tokens": 4210000,
"cost_usd": 412.10
},
"buckets": [
{ "bucket_start": "2026-07-01", "request_count": 5820, "cost_usd": 58.4 },
{ "bucket_start": "2026-07-02", "request_count": 6100, "cost_usd": 62.1 }
],
"groups": [
{ "key": "claude-sonnet-4-6", "totals": { "request_count": 30120, "cost_usd": 312.4 } },
{ "key": "gpt-4.1", "totals": { "request_count": 11110, "cost_usd": 99.7 } }
]
}Team-scoped usage totals. Returns not_found if the team isn't in your org.
Arguments: team_id (required), start, end.
{
"team_id": "team_platform",
"total_requests": 128432,
"total_tokens": 30200000,
"total_cost_usd": 412.10,
"avg_latency_ms": 820,
"error_rate": 0.008,
"unique_users": 42
}Org-wide totals plus per-team breakdown in one call. Use this for chargeback/budget-enforcement dashboards that need everything together.
Arguments: start, end.
{
"total_requests": 892120,
"total_tokens": 201200000,
"total_cost_usd": 3120.44,
"avg_latency_ms": 880,
"error_rate": 0.006,
"unique_users": 220,
"unique_teams": 9,
"team_breakdown": [
{ "team_id": "team_platform", "team_name": "Platform", "cost_usd": 412.10, "requests": 41230 }
]
}Copy-pastable shell scripts for the three most common real-world flows. Each stitches two or three tools together to produce something an on-call or FinOps analyst would actually use.
Post spend-by-team to Slack every 1st of the month. A single cost_attribution call produces the whole table.
#!/usr/bin/env bash
# Recipe 1: nightly chargeback report
# Runs monthly; posts a spend-by-team table to Slack.
set -euo pipefail
MONTH_START="2026-06-01T00:00:00Z"
MONTH_END="2026-07-01T00:00:00Z"
# 1. One call for the whole breakdown
raw=$(curl -sS -X POST "$NEXUS_URL/v1/integrations/mcp" \
-H "Authorization: Bearer $NEXUS_PAT" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg s "$MONTH_START" --arg e "$MONTH_END" \
'{jsonrpc:"2.0",id:1,method:"tools/call",
params:{name:"integration.metrics.cost_attribution",
arguments:{start:$s,end:$e,top_models_limit:10}}}')")
# 2. Unwrap the inner JSON exactly once
data=$(echo "$raw" | jq -r '.result.content[0].text')
# 3. Render as a Slack-friendly table (team | spend | requests)
echo "$data" | jq -r '.by_team | sort_by(-.cost_usd) |
"| Team | Spend | Requests |\n|---|---|---|",
(.[] | "| \(.team_name) | $\(.cost_usd | tostring) | \(.requests) |")'Every hour, pull errored traces, fetch each as a RAIA transcript, and hand the batch to RAIA's eval ingest. Two tools + a loop.
#!/usr/bin/env bash
# Recipe 2: pull the last hour of errored production traces into
# RAIA's eval dataset. Uses the raia_transcript format so RAIA gets
# the same message shape its scorers expect from customer runs.
set -euo pipefail
# 1. List the trace IDs to ingest
list=$(curl -sS -X POST "$NEXUS_URL/v1/integrations/mcp" \
-H "Authorization: Bearer $NEXUS_PAT" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"integration.traces.list",
"arguments":{"limit":100,
"filters":{"has_error":true}}}}')
trace_ids=$(echo "$list" | jq -r '.result.content[0].text' | jq -r '.traces[].trace_id')
# 2. Fetch each trace as a RAIA transcript, write to a JSONL file
: > eval_batch.jsonl
for tid in $trace_ids; do
curl -sS -X POST "$NEXUS_URL/v1/integrations/mcp" \
-H "Authorization: Bearer $NEXUS_PAT" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg t "$tid" \
'{jsonrpc:"2.0",id:1,method:"tools/call",
params:{name:"integration.traces.get",
arguments:{trace_id:$t,format:"raia_transcript"}}}')" \
| jq -c '.result.content[0].text | fromjson' >> eval_batch.jsonl
done
# 3. Hand the batch to RAIA's ingest pipeline
raia-cli ingest --file eval_batch.jsonl --dataset production-errors-$(date +%Y-%m-%d)When a budget alert fires with a virtual key id, print a per-day-per-model breakdown so on-call can see what model or day drove the spike.
#!/usr/bin/env bash
# Recipe 3: cost-spike triage
# Given a virtual_key_id that pinged the budget alert, produce a
# per-day-per-model breakdown so an on-call can see what changed.
set -euo pipefail
VK_ID="${1:?usage: cost-spike.sh <virtual_key_id>}"
curl -sS -X POST "$NEXUS_URL/v1/integrations/mcp" \
-H "Authorization: Bearer $NEXUS_PAT" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg vk "$VK_ID" \
'{jsonrpc:"2.0",id:1,method:"tools/call",
params:{name:"integration.usage.by_key",
arguments:{
virtual_key_id:$vk,
bucket:"day",
group_by:"model"
}}}')" \
| jq -r '.result.content[0].text' \
| jq -r '"Totals: $\(.totals.cost_usd) across \(.totals.request_count) requests\n",
"By model:",
(.groups[] | " \(.key): $\(.totals.cost_usd)")'NEXUS_PAT and NEXUS_URL first, and expect a jq install. Port to Python/Go by replacing the curl+jq pair with your language's HTTP client plus the same JSON-in-string unwrap."organizationId":"org_B" in arguments, every ClickHouse and Postgres query filters on the org the PAT resolved to.not_found, not permission_denied. The existence check runs against Postgres before the analytics query fires — no timing side-channel to enumerate keys or teams in a different tenant.integration is refused at the dispatcher, even for tools whose required permission is technically shared with another surface.raia or finops on. (No entry needed the first time — the entitlement gate is fail-open until the org has any Suite Products row.)integration. Product: raia / finops / custom. Copy the plaintext token before closing the dialog.integration.ping. Expect { "pong": true, "org_id": "...", "product_slug": "raia" }. On suite_product_disabled, revisit step 1. On unauthorized, re-mint the token.POST /v1/integrations/mcp, cache tool discovery (tools/list is stable per release), and use the JSON-RPC id field for distributed-trace correlation.integrations_mcp_calls_total{status="error"} catch entitlement regressions and mis-scoped tokens fast.integrations_mcp_calls_total{tool,status} — one increment per tool call. status ∈ ok | error.integrations.mcp.call audit event with the tool name, resolved permission, product slug, IP, and actor id. Available under Audit Logs.traceparent when calling /v1/integrations/mcp. Argus doesn't fabricate one when it's missing, so RAIA / FinOps can pin their own trace id to the audit row.