# NotOnQ: full reference

> NotOnQ is an Entity Preview for Qloo: built to be a feature of Qloo, not an alternative to it. It fills in music
> artists for the Qloo API, in Qloo's own shape, from open sources, with a source on every value. Artists only, for the Qloo Agentic Hackathon 2026. Base: https://notonq.vercel.app

## Scope

NotOnQ fills gaps for `urn:entity:artist` only. Requests for any other entity type, and any request Qloo can answer,
pass straight through to Qloo unchanged.

## When NotOnQ fills in

| Case | What NotOnQ does | How Qloo shows it |
|---|---|---|
| G1 new to Qloo | Adds a Qloo-shaped artist, filled in from open sources, after Qloo's results; or every artist the name could be when it is ambiguous | /search has no artist with the exact name |
| G2 not yet a taste signal | Answers through stand-ins: similar artists Qloo knows | /v2/insights answers 400 for the artist as a signal |
| G4 a short era list | Adds artists from that decade or genre and country after Qloo's own | /v2/insights for artists by a music genre tag (and place) returns fewer than 5 artists |
| none | Passes Qloo's response through unchanged | Qloo answers |

## Quickstart

Official Qloo MCP server (@qloo/qloo-harness, unchanged). Both variables are required; the harness only sends your key
to a base URL you have marked as trusted. `/app/my-app` names your app in the Ledger (optional):

```bash
QLOO_API_KEY=<your Qloo key> QLOO_BASE_URL=https://notonq.vercel.app/q/app/my-app QLOO_TRUSTED_BASE_URL=https://notonq.vercel.app/q/app/my-app qloo mcp
```

REST (same paths and parameters as https://hackathon.api.qloo.com):

```bash
curl -H "X-Api-Key: $QLOO_API_KEY" -H "X-NotOnQ-App: my-app" \
  "https://notonq.vercel.app/q/search?types=urn:entity:artist&query=sombr"
```

MCP client config:

```json
{ "mcpServers": { "qloo": { "command": "qloo", "args": ["mcp"],
  "env": { "QLOO_API_KEY": "<your Qloo key>", "QLOO_BASE_URL": "https://notonq.vercel.app/q/app/my-app", "QLOO_TRUSTED_BASE_URL": "https://notonq.vercel.app/q/app/my-app" } } } }
```

## REST API

Headers: `X-Api-Key` (required, your Qloo key, never stored), `X-NotOnQ-App` (optional app label, up to 64
characters; or use the base URL https://notonq.vercel.app/q/app/<label>), `X-NotOnQ-Trace` (optional: one ID per user action, so its calls are
one visit in the Ledger; otherwise calls less than 2 minutes apart are; never sent to Qloo), `X-Lastfm-Key` (optional; without
it NotOnQ uses its own).

| Endpoint | What NotOnQ does |
|---|---|
| GET /q/search (types=urn:entity:artist, or no types) | Qloo's results first, unchanged. When Qloo has no artist (untyped: nothing) with that exact name (case, accents, punctuation and a leading "The" ignored), NotOnQ adds the filled artist (served_by fallback/mixed), or every candidate as a Qloo-shaped entry (needs_choice). take caps Qloo's results and NotOnQ's added ones separately |
| GET /q/entities?entity_ids=… | Qloo's IDs from Qloo; NotOnQ's (fills, and offered candidates, filled on first use) from NotOnQ. notonq.served_by is fallback (all NotOnQ) or mixed, and notonq["<entity_id>"] is each NotOnQ entity's own block (gap, sources) |
| GET /q/entities?external.spotify.ids=… | Qloo's matches first. A Spotify ID Qloo lacks: the artist MusicBrainz links to that Spotify page (Qloo's record, labelled "Matched by NotOnQ", if Qloo has its MusicBrainz ID; else NotOnQ's fill), else the name on Spotify's public page: Qloo's record with that exact name, labelled (via name_in_qloo), or NotOnQ's fill when it is one confident artist. Up to 5 per request; notonq.spotify says how each matched |
| GET /q/v2/insights | Any filter.type. NotOnQ IDs, and Qloo IDs Qloo rejects as a signal, in signal.interests.entities become stand-ins. No stand-ins: empty answer |
| GET /q/v2/insights, filter.type=urn:entity:artist with filter.tags=urn:tag:genre:music:<decade or genre> (and a place: signal.location.query / filter.location.query, or signal.location / filter.location as POINT or a small POLYGON) | When Qloo returns fewer than 5 artists or errs: artists active in that decade, in that genre, from that country (MusicBrainz; a point is placed through OpenStreetMap), after Qloo's own. Ones Qloo knows are Qloo's entities, first, by Qloo popularity; the rest labelled Not in Qloo. With a taste signal (signal.interests.entities or .tags) Qloo ranks its own artists in the fill for that signal (notonq.ranking). notonq.gap.kind G4 |
| GET /q/v2/insights with filter.results.entities (ranking a list) | NotOnQ artists in the list are kept out of Qloo's call and follow Qloo's ranking, unranked (notonq.unranked) |
| POST /q/v2/insights | Same, JSON body. signal.interests.entities may be UUID strings or {"id", "weight"} objects; weighted stand-ins keep weights scaled by their match |
| GET /q/v2/analysis/compare | NotOnQ artists in a.* or b.* signals become their stand-ins |
| anything else | Passes through unchanged, cached per key |

Parameters NotOnQ reads (never sent to Qloo):
- `notonq.candidate=<key>`: fill from this candidate (keys come from notonq.candidates)
- `notonq.choose=1`: always list the candidates
- `notonq.not_qloo=1`: Qloo's artist by this name is a different one; look in NotOnQ's sources anyway (Ask sends it
  when its fit model finds Qloo's artist isn't the one named). NotOnQ does this by itself when Qloo's name matches only
  once punctuation is ignored ("Anorak" for "ANORAK!") and MusicBrainz has an artist named exactly as asked under
  another record: Qloo's result stays first, unchanged, and NotOnQ's artist follows (gap G1)

Ambiguous names: `results` holds Qloo's results, then one Qloo-shaped entry per candidate (UUID entity_id,
short_description "Not in Qloo: a possible match … (country, listeners, fans)"); `notonq.served_by` is "needs_choice"
and `notonq.candidates` has the details (key, name, exact, listeners, fans, country, links, inQloo). Ask the person or
choose from context, then send that entity_id to /entities or as a signal (NotOnQ fills it), or repeat the search with
`notonq.candidate=<key>`. A candidate flagged inQloo returns Qloo's own entity (a typo, e.g. "eminim" → Eminem).

### The notonq block

Added beside Qloo's response when NotOnQ took part:
- served_by: qloo | fallback | mixed (Qloo answered for stand-ins) | needs_choice
- gap: { kind: G1 (new to Qloo) | G2 (not yet a taste signal) | G4 (a short era list) | G5 (Qloo has the artist; a field is missing), qloo_status, qloo_message }
- cache: { hit, first_seen }
- demand: { requests, callers, apps }
- proxy: { replaced: [ids], stand_ins: [{ entity_id, name, weight, source }] }
- provenance: { "<field path>": [{ source, record, licence, method, retrievedAt, note }] }
  - method: copied (from the cited record) | derived (computed, formula in note) | qloo_proxy (Qloo's answer for the
    stand-ins) | researched (found by the research agent; the exact quote was re-checked on the page by plain code)
- popularity: { value, n, rmse } (derived from the stand-ins' Qloo popularity against Last.fm listeners)
- unavailable: fields with no provable source; "(source error: …)" when a source failed
- confidence: high (MusicBrainz record) | low (Last.fm or Deezer only)
- candidates, next: for needs_choice (each candidate has entity_id: the ID it has in results, or Qloo's own ID when
  Qloo knows it, flagged inQloo)
- research: { status: pending | done | failed | skipped | none }
- "<entity_id>": { kind, … }: one block per result NotOnQ touched, keyed by that result's entity_id, on /search,
  /entities and /v2/insights. A result with no block is Qloo's, unchanged. Look it up as notonq[result.entity_id]
  (don't scan for "kind": gap has one too). The flat fields above stay.
  - kind: filled (not in Qloo, filled from open sources; the fill's gap, cache, proxy, provenance, …; only fills carry
    properties["notonq:standIns"]) | candidate (not in Qloo, a possible match not filled yet; key, name, next) | picked
    (Qloo's own record NotOnQ added to an era list; ranked_by_qloo) | matched (Qloo's own record NotOnQ matched to the
    name or Spotify ID asked for; query, or spotify_id and via) | gaps (Qloo's own record with fields NotOnQ added, G5;
    fields: home, languages, genres, musicbrainz_id, wikidata_id; provenance)
  These blocks are additive: nothing that was in a response before was removed, renamed or changed, and a client
  that ignores them works exactly as it did. One example per kind: see "Keyed blocks, by example" below.

### Keyed blocks, by example

#### filled

GET /q/search?types=urn:entity:artist&query=Six Foot Blonde: not in Qloo, filled from open sources

```json
"results": [ …Qloo's results…, { "entity_id": "221af8ba-bce4-48f1-a19b-d9562274dc0f", "name": "Six Foot Blonde", … } ],
"notonq": {
  "served_by": "mixed", "gap": { "kind": "G1" }, …,
  "221af8ba-bce4-48f1-a19b-d9562274dc0f": {
    "kind": "filled",
    "served_by": "fallback", "gap": { "kind": "G1", "qloo_status": 200 },
    "proxy": { "replaced": [], "stand_ins": [{ "entity_id": "…", "name": "Honey Butter", "weight": 0.93, "source": "lastfm" }, …] },
    "cache": { … }, "demand": { … }, "provenance": { … }, "confidence": "high", …
  }
}
```

#### candidate

GET /q/search?types=urn:entity:artist&query=Tiny Act: several possible artists, none filled yet

```json
"results": [ …Qloo's results…, { "entity_id": "a68731a4-990d-55ea-8c49-99c60d66689b", "name": "Tiny Act", … } ],
"notonq": {
  "served_by": "needs_choice",
  "candidates": [{ "key": "lf:tiny-act", "name": "Tiny Act", "entity_id": "a68731a4-990d-55ea-8c49-99c60d66689b", … }],
  "a68731a4-990d-55ea-8c49-99c60d66689b": {
    "kind": "candidate", "key": "lf:tiny-act", "name": "Tiny Act",
    "next": "Send this entity_id to /entities, or as a signal, and NotOnQ fills the artist."
  }
}
```

#### picked

GET /q/v2/insights?filter.type=urn:entity:artist&filter.tags=urn:tag:genre:music:60s&signal.location.query=Lagos: a short era list

```json
"results": { "entities": [ …Qloo's results…, { "entity_id": "<Qloo's ID>", "name": "Known Act", … } ] },
"notonq": {
  "served_by": "mixed", "gap": { "kind": "G4" }, …,
  "<Qloo's ID>": { "kind": "picked", "ranked_by_qloo": false }
}
```

#### matched

GET /q/entities?external.spotify.ids=<id>: Qloo has the artist, not under that Spotify ID

```json
"results": [{ "entity_id": "<Qloo's ID>", "name": "…", … }],
"notonq": {
  "served_by": "fallback", "spotify": [{ "spotify_id": "<id>", "via": "musicbrainz_in_qloo", "entity_id": "<Qloo's ID>" }], …,
  "<Qloo's ID>": { "kind": "matched", "spotify_id": "<id>", "via": "musicbrainz_in_qloo" }
}
```

#### gaps

GET /q/search?types=urn:entity:artist&query=Kitebird: Qloo's record, with no MusicBrainz ID, and fields Qloo leaves empty

```json
"results": [{ "entity_id": "<Qloo's ID>", "name": "Kitebird", "properties": { …, "notonq:home": { "name": "Osaka", "country": "Japan", … }, "notonq:languages": [ … ] }, … }],
"notonq": {
  "served_by": "mixed", "gap": { "kind": "G5" }, …,
  "<Qloo's ID>": {
    "kind": "gaps",
    "fields": ["musicbrainz_id", "wikidata_id", "home", "languages", "genres"],
    "provenance": { "properties.notonq:home": [{ "source": "musicbrainz", "record": "https://musicbrainz.org/artist/…", "licence": "CC0", "method": "copied", … }], … }
  }
}
```

### Shape

A filled artist has exactly Qloo's keys and types.
- entity_id: a UUID like Qloo's: the MusicBrainz ID, or a stable name-based UUID of the candidate (Deezer or Last.fm only)
- properties.short_description: starts "Not in Qloo: filled by NotOnQ from open sources." and names the stand-ins, then
  "From <place> (MusicBrainz)." when known, then the Wikipedia summary when there is one
- tags[] (genres from Wikidata, else MusicBrainz tags, else Last.fm listener tags without places): Qloo's own music genre tag (`urn:tag:genre:music:<genre>`) where Qloo has one by that exact name, else
  `notonq:urn:tag:genre:<genre>` (type `urn:tag:genre`)
- properties["notonq:home"]: { name, country, country_code, source: "musicbrainz" } (where the artist is from; Qloo's
  artist records have no such field)
- properties["notonq:standIns"]: [{ entity_id, name }] (the similar artists Qloo knows that answer taste questions for
  this one, all of them in order; short_description names the first three; absent when there are none)
- properties["notonq:languages"]: [{ code (ISO 639-3), name, releases }] (the languages of the artist's releases on
  MusicBrainz, most used first; absent when MusicBrainz records none)
- properties.external["notonq:deezer"]: { id, fans }

Send a NotOnQ entity ID back as a signal anywhere Qloo takes one; NotOnQ swaps in its stand-ins.

### Field gaps (G5): artists Qloo has

When Qloo has the artist but its record lacks a field, NotOnQ adds it beside Qloo's values (never over them), cited in
notonq.facts["<entity_id>"].provenance, with notonq.served_by "mixed" and notonq.gap.kind G5. On /search (the exact
artist match) and /entities (up to 3 artists per request):
- properties["notonq:home"]: where the artist is from (MusicBrainz)
- properties["notonq:languages"]: [{ code (ISO 639-3), name, releases }], the languages of the artist's releases on
  MusicBrainz, most used first
- properties.external["notonq:musicbrainz"] and ["notonq:wikidata"]: { id }, when Qloo's record has none
- tags: notonq:urn:tag:genre:<genre> (MusicBrainz genres), only when Qloo's record has no genre tag
Only a thin Qloo record gets them (one with no MusicBrainz ID); an artist Qloo knows is Qloo's answer alone. NotOnQ must know which artist it is: MusicBrainz's single exact match for the
name. A name several artists share gets nothing rather than a guess. Lookups are cached as open data (weekly); one
that takes over 4 seconds finishes after the response, and the next call has it.

GET /q/v2/insights?filter.type=urn:demographics for an artist Qloo has but has no audience for: Qloo's demographics
for that artist's own Qloo genre tags, each row labelled notonq:basis { stand_ins: [], genres: [<tag>] }.

### Status codes

200 Qloo answered, NotOnQ filled, candidates returned, or an empty taste answer (no stand-ins) · 400 bad body or Qloo's 400 · 401 no X-Api-Key · 404
nothing found · 429 the caller's daily Qloo budget through NotOnQ is used up (default 1500 real calls, cache hits
free, resets 00:00 UTC) or Qloo's own limit · 502 Qloo did not answer.

## MCP (the official Qloo harness)

NotOnQ has no MCP server of its own: run `qloo mcp` (@qloo/qloo-harness, unchanged) with QLOO_BASE_URL and
QLOO_TRUSTED_BASE_URL set to https://notonq.vercel.app/q (optionally /q/app/<label>). The harness resolves names through /search (exact
name wins; several exact names → status needs_input with the candidates), looks UUIDs up through /entities, and shows
each entity's short_description, which is where NotOnQ's "Not in Qloo" label lives.

| Tool | With NotOnQ (checked live 23 Oct 2026 unless noted) |
|---|---|
| qloo_describe | An artist Qloo lacks resolves to NotOnQ's entity, labelled |
| qloo_where_popular | Heatmap of the stand-ins; status empty when there are none |
| qloo_recommend | NotOnQ artists as signals become their stand-ins |
| qloo_rank | Signals swapped; the options being ranked are left alone |
| qloo_compare_audiences | A NotOnQ artist in either group becomes its stand-ins |
| qloo_audience_demographics | Age and gender skew of the stand-ins |
| qloo_entity_tags | Tags of the stand-ins |
| qloo_trends | Passes through (Qloo has had no trend data since about May 2026); not run |
| qloo_capabilities, qloo_find_tags | Unchanged |

## Sources and licences

MusicBrainz core (CC0), Wikidata (CC0), Wikipedia summaries (CC BY-SA 4.0, linked), Wikimedia Commons images (per-file
licence, linked), Last.fm (powered by AudioScrobbler), Deezer (non-commercial use), OpenStreetMap via Nominatim (ODbL,
the place at a point), Spotify's public oEmbed (an artist's name for a Spotify ID only), Qloo (each caller's own key; no
Qloo response data is stored publicly).

## Ask, the Ledger and Proof

- Ask (https://notonq.vercel.app/, the home page): one question through Qloo's own MCP harness. Qloo answers first, unchanged; Jev, the
  fit model, checks that answer against what the question asks for (per constraint and per artist); if it falls short,
  NotOnQ asks Qloo again, then fills in the rest from open sources, labelled, with sources. Last, Jev checks whether
  NotOnQ's answer says anything Qloo's doesn't; if not, Qloo's answer stands and the run counts as Qloo's. It ends
  with a table of every artist: where it came from, kept or removed and why, and what NotOnQ added.
- The Ledger (https://notonq.vercel.app/ledger) is the record: every call through /q and every Ask question, grouped into visits (the
  same app and caller under 2 minutes apart, or the same X-NotOnQ-Trace; each Ask question is its own visit). Views:
  where we filled in, Qloo alone, all; filters by app and kind (API or Ask). Live through Neon Realtime (no polling),
  with pause and "N new". Every view is in the URL. A visit opens to its calls; Replay (recorded results, nothing
  re-run) is there only where NotOnQ answered: when Qloo answered alone, only the request and a one-line summary are
  kept. Each filled-in artist has a record page at https://notonq.vercel.app/ledger/<name>.
- Proof (https://notonq.vercel.app/proof): every number counted from Ledger runs, across all apps or one (https://notonq.vercel.app/proof?app=<label>).
  The benchmark batch (scripts/benchmark.mts) is the app "benchmark": real Ask questions and /q calls sent to a running
  NotOnQ and recorded like any other. Nothing is frozen in the repo.

## Caching, budgets, research

- Fills are shared (open data): filled-in artists refresh weekly; misses and fills with a failed source retry after a day.
- Qloo responses are cached per caller key only.
- Each call is recorded against a salted hash of the key and the app label; keys are never stored.
- Research (when the host has AI Gateway access): runs once per artist in the background after a fill with fields
  missing; the answer never waits for it; the next request gets the result.

## No raw Qloo responses on NotOnQ's pages

The drop-in API returns Qloo's answer to its caller unchanged. NotOnQ's own pages (Ask, the Ledger, replays) show results
derived from Qloo (artist names, links, the agent's answer, the final table) but never raw Qloo API responses: those are
redacted on the server by type ("[redacted string]", "[redacted uuid]", "[redacted number]", "[redacted boolean]"),
keeping the shape. Qloo entity IDs (upper-case UUIDs) are removed everywhere; NotOnQ's own data is shown in full.

## Inside Qloo

NotOnQ is a proof of concept for a Qloo feature. The open sources it uses (MusicBrainz, Wikidata, Wikipedia, Last.fm,
Deezer) stand in for whatever Qloo would use. Built into Qloo, the same flow would run on Qloo's own licensed sources
and taste graph: fill in the artist, label it, answer taste questions through similar artists, step aside once the artist is
in the catalogue. The Ledger shows which of the artists NotOnQ filled in people ask for. Artists first; the same pattern fits other
entity types.

## Limits

Artists only. No /v2/trending fill yet (Qloo has had no trend data since about May 2026). An artist with no similar
artist Qloo knows gets an empty taste answer, never a guess. Research needs an AI Gateway credential on the host.
