Locate once, read it many ways.

A ClassGrade knowledge space is a learned coordinate system for a subject. You locate a learner in it once — for your tenant, resolved from your key — then read that one position through whichever framework views you ask for. Three things stay separate: which subject (the knowledge space), how you read the position (the framework views), and whose private layer you see (the tenant). The position is computed once; the views are just different readings of it.

Early access

locate and neighbourhood are live — their “try it” panels call the real backend. field, the premium field-query tier, isn’t built yet and returns a canned sample. The knowledge space is early in calibration, so positions report wide, provisional uncertainty for now — it sharpens as real learner data trains the surface.

You locate a learner once, in a knowledge space, for your tenant — then read that one position through one or more framework views. Three axes, never conflated:

01

Knowledge space

Which subject?

Examples
English (live) · Maths, Science (next)
Set via
Request field “space”
Changes
The coordinate system itself — the axes you get back.
02

Framework view

Read the position how?

Examples
CEFR · school-grade
Set via
Request field “views” (a list)
Changes
Only the interpretation of the position — not the position.
03

Tenant

Whose private layer?

Examples
Blueberry · a client
Set via
Resolved from the API key — never in the body
Changes
Which proprietary layer overlays the shared seed; data isolation.
The measurement model

We don’t score children. We locate them.

What a position means, how the enrolled grade sets the prior, and why we read the same point two ways — band against grade, and depth within the band.

Base URLhttps://api.classgrade.ai/v1

POST/v1/locateBundled

Place a learner in a knowledge space from evidence, and read that one position through the framework views you ask for.

space picks the coordinate system — english is live; maths and science are accepted values, coming (requesting one now returns a 400). grade is required (1–10): the enrolled grade is the prior the measured position is read against. evidence is coordinates on the space’s axes; producing those from raw work is a separate upstream step, held out here. views default to the tenant’s default and are scoped per tenant. The position is decay-adjusted to call time, so as_of is stamped; an unsupplied axis is honestly null, never 0.

Request
{
  "space": "english",
  "grade": 6,
  "evidence": [
    {
      "axis": "accuracy",
      "value": -0.15
    },
    {
      "axis": "fluency",
      "value": -0.2
    },
    {
      "axis": "vocabulary",
      "value": -0.1
    }
  ],
  "views": [
    "cefr",
    "school"
  ]
}
Response
{
  "space": "english",
  "tenant": "blueberry",
  "position": {
    "axes": {
      "accuracy": {
        "value": -0.15,
        "uncertainty": 0.49
      },
      "pronunciation": {
        "value": null,
        "uncertainty": null
      },
      "fluency": {
        "value": -0.2,
        "uncertainty": 0.49
      },
      "vocabulary": {
        "value": -0.1,
        "uncertainty": 0.49
      },
      "content": {
        "value": null,
        "uncertainty": null
      }
    },
    "as_of": "2026-06-03T08:13:13Z"
  },
  "views": {
    "cefr": {
      "band": "B1",
      "detail": "around grade-expected (B1)"
    },
    "school": {
      "enrolled_grade": 6,
      "expected_level": "B1",
      "measured_level": "B1",
      "gap": 0,
      "detail": "on track",
      "within_band": -0.034,
      "within_band_state": "entry"
    }
  }
}

The same position, read two ways. Unsupplied axes are honestly null (here pronunciation and content), never 0. The school view is grade-relative: gap reads the measured band against the grade’s expectation (here on track), and within_band_state reads depth inside that band on a four-state scale — while the surface is early in calibration, uncertainty is wide and most reads sit near entry. Where the two views diverge, the gap is the signal.

Same model, same call, every time. Three Grade 6 learners — only the evidence differs. Press each and the live API answers differently: where they are, and the neighbourhood it opens.

Live response
POST /v1/locate · english · grade 6evidence · acc -0.15, flu -0.20, voc -0.10 ×6

The same low reads, but enough of them now. Still B1 — the right band for Grade 6, so a gradebook says on track and moves on. With the evidence to earn it, the depth resolves: struggling, at the floor of the band. Right band, wrong depth.

POST/v1/neighbourhoodBundled

For a located position, return the skills immediately behind and ahead — five back (what it rests on) and five forward (what comes next). The engine behind the home-page demo.

Centre on a position from /v1/locate, or a skill id. builds_on and comes_next are walked along prerequisite edges, not nearest-neighbour. Each neighbour carries its own per-view reading, so the same skill can read differently across views — that divergence is signal.

Request
{
  "space": "english",
  "position": "bm.cefr.c_l_vc.B1.01",
  "views": [
    "cefr",
    "school"
  ]
}
Response
{
  "space": "english",
  "tenant": "blueberry",
  "centre": {
    "id": "bm.cefr.c_l_vc.B1.01",
    "label": "Use simple vocabulary on familiar topics"
  },
  "builds_on": [
    {
      "id": "bm.cefr.c_l_vc.A2.01",
      "label": "Can use basic vocabulary for daily needs",
      "views": {
        "cefr": "A2",
        "school": "Class 5"
      }
    },
    {
      "id": "bm.cefr.c_l_vc.B1.02",
      "label": "Control elementary vocabulary",
      "views": {
        "cefr": "B1",
        "school": "Class 6"
      }
    }
  ],
  "comes_next": [
    {
      "id": "bm.cefr.c_l_ga.B1.01",
      "label": "Use common routines with reasonable accuracy",
      "views": {
        "cefr": "B1",
        "school": "Class 6"
      }
    },
    {
      "id": "bm.cefr.c_l_vc.B2.01",
      "label": "Use appropriate vocabulary in familiar contexts",
      "views": {
        "cefr": "B2",
        "school": "Class 9"
      }
    }
  ]
}

Abbreviated — each side returns up to five, walked along prerequisite edges. The live call returns the full set.

Live response
POST/v1/fieldPremium · meteredComing soon

Field queries over the space — divergence, Jacobian, entropy, influence. The premium, metered tier: called deliberately, not on every turn.

Genuinely not built yet, documented so the surface is complete. Shape is indicative; these reads land once enough trajectory data is flowing to compute them reliably.

Request
{
  "space": "english",
  "query": "influence",
  "position": "bm.cefr.c_l_vc.B1.01"
}
Response
{
  "space": "english",
  "query": "influence",
  "entropy": 1.84,
  "influence": [
    {
      "id": "bm.cefr.c_l_ga.B1.01",
      "weight": 0.41
    },
    {
      "id": "bm.cefr.c_l_vc.A2.01",
      "weight": 0.33
    },
    {
      "id": "bm.cefr.c_l_vc.B2.01",
      "weight": 0.27
    }
  ]
}
Preview · canned response

Auth

Every request carries an API key: Authorization: Bearer <api_key>. A key belongs to one tenant — the key is how the platform knows which proprietary layer to read. The tenant is never in the body.

Tenant isolation

A key sees two things: the shared seed — public and identical for everyone — and its own proprietary layer, read on top of the seed and private to that tenant. It never sees another tenant’s layer: not as a filtered field, not as a sibling key in the response. Isolation is structural — the response is assembled from your slice alone, and an unknown tenant gets a 404, never a fallback.

One locate, many views

Asking for several framework views is one locate read several ways — billed as one position lookup, not one per view. locate and neighbourhood are bundled; field is premium and metered. Adding a knowledge space (maths, science) is how the product line grows — same machinery, new axes.

This is an L2 English knowledge space. Need a different one — L1 English, another subject, another language? That’s what we build. Same machinery, new axes.

Talk to us