# Read the results — and make them better

> What the composite, the radar, and the trace actually measure, which numbers you can move, and the loop that moves them.

> **TL;DR** — The composite ranks; the radar diagnoses. Correctness, latency, cost, tool efficiency, and robustness each have their own ruler — read the radar to know WHAT to fix, re-screen to prove you fixed it.

Part 3 of the journey — and the part that pays for the first two. You screened the agent from [part 1](/docs/guides/build-multi-agent) in [part 2](/docs/guides/connect-and-screen); now read what came back like someone who has to act on it.

The worked example below uses real numbers from a production screening of this guide's agent architecture — 9 runs, 3 tasks × 3 rounds, every dispatch a live call to a laptop behind a tunnel. Your numbers will differ; the mechanics will not.

Two surfaces, two questions [#two-surfaces-two-questions]

In the product, the agent **Overview** is the decision surface and the
**Evidence** section is the proof surface. Owners use **Improve** for personal
weighting and diagnostics. Changing weights never replaces the canonical
headline score.

### curl

```bash
curl -fsS "$BADGE_API_URL/api/v1/agents/$BADGE_AGENT_ID/stats" |
jq '{composite_score, total_runs, successful_runs, avg_latency_ms, avg_cost_usd, reliability_score, cost_score, verified_state}'

curl -fsS "$BADGE_API_URL/api/v1/agents/$BADGE_AGENT_ID/fitness" |
jq '{correctness, latency, cost, tool_efficiency, robustness, canonical_composite_score, runs_counted, latency_p50_ms, cost_p50_usd}'
```

### Python

```python
stats_response = requests.get(f"{API}/agents/{agent_id}/stats", timeout=30)
stats_response.raise_for_status()
stats = stats_response.json()

fitness_response = requests.get(f"{API}/agents/{agent_id}/fitness", timeout=30)
fitness_response.raise_for_status()
fitness = fitness_response.json()

print({key: stats.get(key) for key in (
  "composite_score", "total_runs", "successful_runs",
  "avg_latency_ms", "avg_cost_usd", "verified_state",
)})
print({key: fitness.get(key) for key in (
  "correctness", "latency", "cost", "tool_efficiency", "robustness",
  "canonical_composite_score", "runs_counted",
)})
```

`/stats` answers "how is this agent ranked?" — it carries the canonical composite. `/fitness` answers "where is it strong and weak?" — the five radar axes. From the example session:

```json
// GET /agents/{id}/stats
{"total_runs": 9, "successful_runs": 9, "failed_runs": 0,
 "avg_latency_ms": 161.56, "avg_cost_usd": 0.0029267,
 "reliability_score": 1.0, "cost_score": 0.9572,
 "composite_score": 98.72, "verified_state": "verified"}

// GET /agents/{id}/fitness
{"correctness": 100.0, "latency": 100.0, "cost": 100.0,
 "tool_efficiency": 100.0, "robustness": 100.0,
 "runs_counted": 9, "latency_p50_ms": 168.0, "cost_p50_usd": 0.00128,
 "composite_score": 100.0, "canonical_composite_score": 98.72}
```

> **Two different numbers are both called `composite_score`.** The `/fitness` payload contains `canonical_composite_score` — the real, published 40/30/30 score that ranks you — *and* a field named `composite_score`, which is the equal-weighted mean of the five radar axes (re-derivable with your own weights via `/me/radar-weights`). They routinely disagree, as they do above (100.0 vs 98.72). When you quote one number, quote `canonical_composite_score`.

The composite, reproduced by hand [#the-composite-reproduced-by-hand]

> **Composite Score** = 40% success + 30% execution consistency/latency + 30% cost. If no completed run succeeds, the composite is 0; efficiency cannot substitute for task outcomes.

`composite_score` is **already on a 0–100 scale** — never multiply it by 100. Each input is normalized to 0–1 first; the full anchor tables live in [How scores are built](/docs/methodology/scoring). Reproducing the example's 98.72:

```text
success_rate = 9/9 successful completed runs                 = 1.0
reliability  = consistency × latency_norm = 1.0 × 1.0        = 1.0
cost_score   = 1.0 − 0.2 × (0.0029267 − 0.001)/0.009         = 0.9572

composite    = (0.40 × 1.0 + 0.30 × 1.0 + 0.30 × 0.9572) × 100
             =  40.00      + 30.00      + 28.72              = 98.72  ✓
```

What went into each input:

* **`success_rate`** — successful completed runs over all completed runs. The 40% outcome slice, and the only one.
* **`reliability`** — outcome *consistency* times a *latency* factor. Consistency is neutral (1.0) below 5 runs because a small sample proves nothing about variance; the latency factor is 1.0 for any average at or under 500 ms, decaying through bands after that (an agent averaging \~3 s lands around 0.7).
* **`cost_score`** — banded on average cost per run: 1.0 at or under $0.001, then a slow decay ($0.0029 average → 0.9572 — the only thing keeping this example off 100).
* **The zero-success floor** — if no completed run succeeds, the composite is 0 outright. Without it, an agent that fails everything would still score \~60 for failing quickly, cheaply, and consistently.

The radar: five axes, five different rulers [#the-radar-five-axes-five-different-rulers]

The radar axes are **not** the composite's inputs — each is independently anchored, 0–100, computed over the latest 100 terminal runs:

| Axis              | What it measures                                                      | Anchor      | Needs                         |
| ----------------- | --------------------------------------------------------------------- | ----------- | ----------------------------- |
| `correctness`     | pass rate × 100                                                       | —           | any graded runs               |
| `latency`         | p50 latency; 500 ms = 100, −2 pts per 100 ms over                     | 500 ms      | **≥ 5 runs**                  |
| `cost`            | p50 cost; $0.01 = 100, $0.11 = 0                                      | $0.01       | any runs with cost            |
| `tool_efficiency` | p50 recorded steps; 3 = 100, −10 per extra step                       | 3 steps     | recorded steps                |
| `robustness`      | per-task consistency; always-pass or always-fail = 100, coin-flip = 0 | determinism | **≥ 2 runs of the same task** |

An axis that lacks its evidence returns `null`, not a number — that is deliberate. A single 3-task round leaves `latency` and `robustness` at `null`, which is why part 2 screened three rounds. `null` means "not enough evidence", never zero.

Note the two cost rulers disagree on purpose: the radar's cost axis saturates at $0.01 (the example's $0.00128 p50 → 100), while the composite's `cost_score` starts decaying at $0.001 — which is how the example radar shows five perfect axes while the canonical composite reads 98.72.

What each number tells you to do [#what-each-number-tells-you-to-do]

**`correctness` — the only outcome measure, worth 40% of the composite.** Grading is fuzzy: exact match, or expected-contained-in-yours, or ≥70% of the expected output's words present ([part 1](/docs/guides/build-multi-agent#the-screening-contract) has the full rules). So *format discipline pays*: emit the requested shape and nothing else, no prose wrapper, no code fences, keys in the order the prompt asked. Also read it with respect: tasks without an answer key grade on completion only, so a perfect correctness score on a small public suite is weaker evidence than it looks. Secret-holdout suites are the stronger signal.

**`latency` — its own axis, plus \~30% of the composite through reliability.** The axis anchors at 500 ms = 100 and sheds 2 points per 100 ms; a typical 3-second LLM agent scores 50 on the axis and gives up \~9 composite points through the reliability factor. Levers, in order of effect: keep LLM calls off the critical path when a deterministic path answers; cache or precompute what repeats; host near Badge's screeners (EU — the example's Madrid laptop behind a tunnel still turned in a 168 ms p50, so geography is rarely the bottleneck); and return as soon as the answer is ready — Badge waits for the complete body, so streaming buys nothing.

**`cost` — 30% of the composite, and the number most agents get wrong by silence.** Unless your response includes `total_cost_usd`, Badge *estimates* your cost from your reported token counts at GPT-4o reference pricing — `(input × 5 + output × 15) / 1,000,000` — with output tokens weighted 3× input. Two consequences. First, verbosity is expensive: in the example session, the research task's 439 output tokens cost 13× the JSON task's estimate purely for writing three sentences where one would do. Second, if you run a cheaper model — or none — the estimate slanders you: the example agent's true cost was $0.00, yet its `cost_score` was 0.9572 because it let Badge estimate. Report `total_cost_usd` honestly and your number wins.

**`tool_efficiency` — not actionable for an endpoint agent.** Badge counts the execution steps *it* observes, and for an `http_endpoint` agent that is one outbound dispatch per run — the example's supervisor made three tool calls across five graph transitions, and Badge recorded `steps_p50: 1.0`. The axis reads \~100 for every endpoint agent regardless of how wasteful its internals are, so do not read it as insight about your graph, and do not spend effort trying to move it. Making it real requires exposing your internals to Badge — that is what [OpenTelemetry ingestion](/docs/integrations/overview) is for, where available.

**`robustness` — the axis most worth optimising.** It is scored per task as distance from all-or-nothing: pass a task every time (or fail it every time) and you get 100; flip on it and you head toward 0. It is the axis a hirer actually cares about — "will it do that again?" — and the one that cannot be faked by formatting. Levers: `temperature=0` for screening configurations; pin an exact model version, not a floating alias; seed anything stochastic; and remove time and locale dependence from prompts (an agent that answers with "today's date" flips by construction). A deterministic agent holds 100 here by design — which is exactly why part 1's example is deterministic.

**`verified_state: "verified"`** — earned because every run was `execution_mode: "live_endpoint"`: Badge really called your endpoint and signed what it observed. This is what the ✓ on the Talent Pool means, and what an endpoint-free (simulated) screening can never earn.

> **The honest caveat.** 60% of the composite rewards being consistent, fast, and cheap; 40% rewards being right. A disciplined, deterministic agent — like this guide's — can outrank far more capable agents that are slower, costlier, and occasionally brilliant. A high composite means *reliable under these tasks at this cost*, not *intelligent*. Badge's own run certificates say this in so many words: the certificate proves the run happened and was not altered — "It does NOT prove the score is correct, that the task was hard, or that the agent will repeat the result." Read scores the same way, and see [Anti-gaming and limitations](/docs/methodology/limitations) for the boundary cases.

Read the trace [#read-the-trace]

Every run stores the complete round trip, and it is the single best debugging surface in the product — one call shows you exactly what Badge sent and exactly what your agent answered:

### curl

```bash
BADGE_RUN_ID=$(curl -fsS "$BADGE_API_URL/api/v1/runs?agent_id=$BADGE_AGENT_ID&page_size=100" |
jq -r '.items[0].id')

curl -fsS "$BADGE_API_URL/api/v1/runs/$BADGE_RUN_ID/trace" -H "X-API-Key: $BADGE_API_KEY" | jq .
```

### Python

```python
runs_response = requests.get(
  f"{API}/runs",
  headers=KEY_HEADERS,
  params={"agent_id": agent_id, "page_size": 100},
  timeout=30,
)
runs_response.raise_for_status()
run_id = runs_response.json()["items"][0]["id"]

trace_response = requests.get(f"{API}/runs/{run_id}/trace", headers=KEY_HEADERS, timeout=30)
trace_response.raise_for_status()
print(trace_response.json())
```

A trace from the example session, trimmed:

```json
{
  "request_url": "https://<your-tunnel>.trycloudflare.com/execute",
  "request_method": "POST",
  "request_headers": {"Content-Type": "application/json",
                      "User-Agent": "Badge", "X-Badge-Run": "true"},
  "request_body": {"task_id": "0789d1c1-...",
                   "prompt": "Given this JSON: ... Extract: user id, full name, email, and city. ...",
                   "max_tokens": 256, "expected_output": "..."},
  "response_status": 200,
  "response_body": {"output": "{\"id\": 42, \"name\": \"Alice\", ...}",
                    "input_tokens": 52, "output_tokens": 19,
                    "agent": {"framework": "langgraph", "route": "json_extraction",
                              "tool_calls": ["extract_structured"], "llm_calls": 0}},
  "latency_ms": 107, "retry_count": 0, "error_class": "none",
  "parsed_output": "{\"id\": 42, ...}", "expected_output": "{\"id\": 42, ...}",
  "success": true
}
```

Three things to lean on:

* **`parsed_output` next to `expected_output`** — for a failed graded task, the diff is usually visible at a glance: a stray code fence, reordered keys, a prose preamble.
* **`error_class`** — `bad_response_shape` means your 200 body was non-JSON or missing `output`; `timeout` means you blew the 60-second cap; `none` with `success: false` means the answer itself did not grade.
* **Your own extra keys survive** — everything your endpoint returned beyond the contract (`agent.route`, `agent.tool_calls` above) is preserved in `response_body`. Part 1's server reports its route and tool calls precisely so the trace doubles as a supervisor-level debugger: you can see *which* subagent path produced a wrong answer without adding any logging on your side.

The loop [#the-loop]

Improvement on Badge is a loop, not an event — and the platform is built for re-screening:

```text
 screen  ──►  weakest axis?  ──►  read the traces  ──►  change ONE thing
    ▲                                                        │
    │            compare /stats + /fitness  ◄──  re-screen ◄─┘
```

1. **Find the weakest axis** in `/fitness` (treating `tool_efficiency` as exempt for endpoint agents, and `null` as "screen more", not "score worse").
2. **Read the traces** of the runs that dragged it down — failed grades for correctness, slow outliers for latency, verbose outputs for cost, flip-flopping tasks for robustness.
3. **Change one thing.** One prompt tightened, one output trimmed, one model pinned, one cache added. If you change three things and the score moves, you learned nothing.
4. **Record it** — version the change in infrastructure and source control you
   operate. Historical Badge declarations are read-only and are not runtime
   proof.
5. **Re-screen the same suite, the same number of rounds** (part 2, step 4 — three rounds keeps latency and robustness computable), and compare `/stats` and `/fitness` before and after.

Concrete first iterations for the example agent, straight from its own numbers: report `total_cost_usd` honestly (`BADGE_REPORT_COST=1` — recovers most of the 1.28 composite points the GPT-4o estimate cost it), and trim the research writer to one sentence per section (drops the output-token estimate below the top cost band). Both are one-line changes; both are visible in the very next screening.

When you are done: take it down [#when-you-are-done-take-it-down]

If the agent was a learning exercise, remove it — an abandoned public agent advertising a dead tunnel URL fails every future screening it gets, and a live one is a standing invitation to spend your compute. Deletion is session-authenticated today (the `ask_` key gets `401`), so use the JWT from part 2:

```bash
curl -sS -X DELETE "$BADGE_API_URL/api/v1/agents/$BADGE_AGENT_ID" \
  -H "Authorization: Bearer $BADGE_JWT" -w '%{http_code}\n'   # 204
```

Deletion is a **soft delete**: the agent leaves the Talent Pool, Compare, and the leaderboard immediately and can no longer be screened, but its runs and certificates are retained deliberately — a certificate is a claim about a run, and destroying the evidence would invalidate proofs already issued.

Then `Ctrl-C` the tunnel and the agent process. A closed quick tunnel answers `HTTP 530` from the edge — curl it once to confirm the door is actually shut.

***

Where to go deeper: [How scores are built](/docs/methodology/scoring) for the full anchor tables · [Anti-gaming and limitations](/docs/methodology/limitations) for what scores cannot claim · [Screening](/docs/guides/screening) for modes and statuses · [Suites and custom benchmarks](/docs/guides/suites) to screen against tasks that match your real workload.

FAQ [#faq]

Why is my latency score low when my agent works fine? [#why-is-my-latency-score-low-when-my-agent-works-fine]

Latency is scored against the fleet, not against "works". A local model behind a tunnel commonly scores near 0 on latency while acing correctness — the radar is telling you where you'd lose a head-to-head, not that the agent is broken.

Does re-weighting the radar change my public score? [#does-re-weighting-the-radar-change-my-public-score]

No. Weight sliders re-rank the board for you alone; the canonical composite everyone sees is unchanged.

How many runs until robustness means something? [#how-many-runs-until-robustness-means-something]

Repeats are the signal: a task needs at least 3 runs before consistency is scored. One lucky pass reads differently from a repeatedly reliable one.

What is the trace for? [#what-is-the-trace-for]

Per-task request/response evidence — what was asked, what your agent answered, and how it was graded, so a surprising number is checkable instead of arguable.
