---
title: "Api Learning Mastery Badges"
method: GET
path: "/api/learning/mastery/badges"
tags: ["Education"]
---

# Api Learning Mastery Badges

`GET /api/learning/mastery/badges`

## Request

- Base URL: `https://www.oxfordledge.com` (the document declares no server; this is the origin it was published from)
- URL: `https://www.oxfordledge.com/api/learning/mastery/badges`
- Auth: none declared

LEARN Phase 11 — per-domain tier badges (self-only).

OWNER decision #4 (2026-04-17): badges are SELF-ONLY. No public
endpoint exposes another user's mastery grid. This endpoint reads
the authed user from the session and returns ONLY their own data.

Tier thresholds:
  - Journeyman: mean_score >= 60 (shown when attempted_count >= 3)
  - Analyst:    mean_score >= 80 (shown when attempted_count >= 5)
  - Oracle:     mean_score >= 95 AND first_seen_at <= NOW() - 60d
                (sustained-mastery proxy — we don't keep full score
                history, so "has been tagged with this domain for
                60+ days" is our best signal that the >=95% score
                isn't a flash-in-the-pan result)
                AND attempted_count >= 5 AND coverage_pct >= 50
                (#95, 2026-08-13 — the sustained-score proxy said
                nothing about BREADTH, so three concepts out of
                sixty earned the terminal badge. Oracle also had a
                lower evidence floor than Analyst, which is
                inconsistent independent of the coverage question;
                both are fixed. Threshold: _ORACLE_MIN_COVERAGE_PCT)

Response shape:
  {
    "domains": [
      {domain, title, mean_score, attempted_count, total_concepts,
       coverage_pct, tier, tier_label, next_tier, next_tier_target},
      ...
    ],
    "overall_tier": "journeyman"|"analyst"|"oracle"|"learner",
    "concepts_with_attempts": int,
    "total_concepts": int,
  }

Sorted by mean_score DESC. Domains with zero attempts still appear
(tier=null) so the grid is always 8 panels.

## Response `200`

Successful Response

- MasteryBadgesResponse: `GET /api/learning/mastery/badges` — per-domain tier grid. Self-only (OWNER decision #4). Returns 8 panels even when the user has no attempts (untouched domains carry tier=null) so the FE grid is layout-stable.
  - `domains` MasteryBadgeDomain[], required: One row per domain, sorted by mean_score DESC then attempted_count DESC.
    - `domain` string, required: Domain slug (valuation, fundamentals, credit, …).
    - `title` string, required: Display title for the domain panel.
    - `mean_score` number, required: EMA mean score across attempted concepts in this domain.
    - `attempted_count` integer, required: Concepts in this domain the user has attempted.
    - `total_concepts` integer, required: Total concepts in this domain (denominator).
    - `coverage_pct` number, required: 100 * attempted_count / total_concepts.
    - `tier` string, nullable: Earned tier slug: journeyman | analyst | oracle, or null when below threshold.
    - `tier_label` string, nullable: Capitalized tier label for display, or null.
    - `next_tier` string, nullable: Next tier slug to aim for; null at oracle.
    - `next_tier_target` integer, nullable: Mean-score target for the next tier; null at oracle.
  - `overall_tier` string, required: Overall cross-domain tier: oracle | analyst | journeyman | learner. Defaults to 'learner' until the user has earned a tier in ≥ ceil(N/2) domains.
  - `concepts_with_attempts` integer, required: Total attempted concepts across all domains.
  - `total_concepts` integer, required: Total concepts in the canonical taxonomy (denominator).

---

[API](https://skmtc.dev/oxfordledge/apis/oxford-ledge-api.md) · [All operations](https://skmtc.dev/oxfordledge/apis/oxford-ledge-api/llms.txt) · [OpenAPI document](https://skmtc.dev/oxfordledge/apis/oxford-ledge-api/revisions/e832a1260f0b?raw)
