Ask a chat model what the Vandal does to the body and you get a number. Ask it which patch that number is from and you get a shrug, or a confident wrong date. The model saw the value somewhere in its training data. It has no receipt.
This is the fix: give the model a tool that reads the Knowledge API and hand it the fact with its source, its tier, and its patch. Four reads, no scraping, one key. Every response below is the live file, trimmed.
0. Get a key
Every path under api/ needs a key in the x-api-key header. Create one in the developer console (the Free tier is enough for this) and put it in your environment as KB_API_KEY. That name is what the docs and the curl lines read; keep it out of the URL and out of your logs. The contract is that a request without one is refused: a 401 whose body links to the console.
{
"error": "unauthorized",
"message": "This path needs an API key in the x-api-key header.",
"link": "https://kb.play-ascend.com/portal"
}1. Read the index
index.json lists every game, its counts, the attribution line its licence requires, and the paths for its files. Read it once and cache it; it changes when the corpus publishes, so key the cache on published.json (below).
$ curl -s -H "x-api-key: $KB_API_KEY" https://kb.play-ascend.com/api/index.json{
"game": "valorant",
"title": "VALORANT",
"entities": 185,
"facts": 1767,
"paths": {
"entities": "valorant/entities.json",
"entity": "valorant/entity/{file}.json",
"fields": "valorant/fields.json",
"codenames": "valorant/codenames.json"
}
}2. Find the entity
A game's list is one line per entity: name, type, group, tags, and the file segment you need for the next URL. It is the only place that segment is stated, so do not build it from the name. Match on name first, then tags; the Vandal is tagged "AK" because that is what people call it.
{
"id": "vandal",
"file": "vandal",
"type": "weapon",
"name": "Vandal",
"group": "Assault Rifles",
"tags": ["Assault Rifles", "AK"],
"description": null,
"values": 15,
"facts": 16
}Cache the list for an hour per game. VALORANT's is 185 entries; Teamfight Tactics' is 5,708. Filtering it locally is cheaper than a request, and the Free tier's 60 requests per five minutes go further when the list is not one of them.
3. Read the facts
The entity file has two arrays. resolved[] is the current value per field and qualifier: the one an answer uses. facts[] is everything ever stated, superseded values included, each with the patch it stopped in. For a question about the game as it is now, read resolved[] only.
$ curl -s -H "x-api-key: $KB_API_KEY" https://kb.play-ascend.com/api/valorant/entity/vandal.json[
{ "field": "weapon.damage", "qualifier": "0-50m body", "value": 40, "unit": "hp",
"tier": "official_gamefiles", "patchFrom": "1.07",
"source": "https://valorant-api.com/v1/weapons (Vandal)" },
{ "field": "weapon.damage", "qualifier": "0-50m head", "value": 160, "unit": "hp",
"tier": "official_gamefiles", "patchFrom": null,
"source": "https://valorant-api.com/v1/weapons (Vandal)" },
{ "field": "weapon.damage", "qualifier": "0-50m leg", "value": 34, "unit": "hp",
"tier": "official_gamefiles", "patchFrom": null,
"source": "https://valorant-api.com/v1/weapons (Vandal)" }
]Three things to hand the model with every value. The tier, so it can say how well the figure is attested (game files here, the best there is). The source, so it can point at the page or file. And patchFrom, so it can date the value or say the source does not. A null patch is not "unknown"; it is "this source does not date it", and the model should say that rather than invent a patch.
4. Know what the field means
fields.json per game says what each field measures, its unit, and the bounds a value must fall inside. Field names are scoped to the game: weapon.damage in VALORANT and weapon.damage in Counter-Strike 2 are different quantities with different qualifiers. Read it once per game and give the model the unit with the value, so "9.75" comes out as "9.75 shots per second".
{ "field": "weapon.damage", "type": "number", "unit": "hp", "min": 1, "max": 500, "facts": 112, "appliesTo": "weapon" }
{ "field": "weapon.fire_rate_per_s", "type": "number", "unit": "per_s", "min": 0.1, "max": 30, "facts": 32, "appliesTo": "weapon" }The tool, in one shape
Whatever framework you use, the tool the model calls is the same: take a game and a name, find the entity in the cached list, fetch its file, return resolved[] with the attribution line. Keep it to one tool. A model with six tools for one corpus spends its turns choosing between them.
- Input.
game(a slug from the index) andquery(what the user typed). Optionally afieldto narrow the answer. - Lookup. Filter the game's cached list by name, then tags, then group. Return the top few matches if the query is ambiguous and let the model ask; do not pick one silently.
- Fetch.
api/<game>/entity/<file>.json, cached for a day, invalidated whenpublished.jsonchanges. - Output. The entity's name,
sourceUrl, the resolved facts (field, qualifier, value, unit, tier, patchFrom, source), and the game's attribution line from the index.
What a good answer reads like
User: "How much does a Vandal body shot do?" A model wired this way answers: "40 damage to the body at 0–50 m, per Riot's shipped weapon data (valorant-api.com), since patch 1.07. Head is 160, leg is 34." That is the whole point: the number, the range it holds at, the source, the patch. A reader can check every clause.
When the corpus has nothing, the model says so: "The corpus does not carry a figure for that." That sentence is worth more than a guess, and it is the one most bots never say.
Caching and limits
The files change when the corpus publishes, which is nightly when a source moved. published.json carries publishedAt, the commit, and a changed flag. Poll it hourly and drop your entity cache when the stamp moves. Between publishes, nothing changes, so there is no reason to re-read a file you already hold.
The Free tier is 60 requests per five minutes as the console states it today. With the index and the lists cached, that is 60 entity reads per five minutes, which covers a bot answering a busy channel. An assistant serving many servers all day wants Pro (1,500 per five minutes) or the bulk files, which give you every game as one download for a build step. A 429 carries Retry-After in seconds; honour it.
Print the attribution
Each game's index entry carries the line its source licence requires. Show it where the data is shown: a footer line in the bot reply, an about page, the overlay's settings. It names the source, the licence, and the trademark owner, and states that none of the publishers is affiliated with or endorses the project. That sentence protects you as much as it does us.
The full file shapes, the error bodies, and the limits are on the docs page. An MCP server that exposes these same reads as tools is next, so an agent in Claude or Cursor gets them without building URLs at all.
Questions
Can I skip the list and guess the file name?
No. The file segment comes from entities.json and is the only supported way to build an entity URL. Some are the display name lowercased (vandal), some are not (clove-ruse, ahri-orb-of-deception). Cache the list and look it up.
Should the model read facts[] or resolved[]?
resolved[] for the game as it is now: one current value per field and qualifier. facts[] for history: every value ever stated, with patchTo on the ones that stopped. A 'what changed in patch X' question reads facts[]; everything else reads resolved[].
What does a null value mean?
That the corpus does not state the figure. Nothing is inferred or filled in from another source. Tell the model to say so; the alternative is a guess wearing a citation.
Do I need Pro for a Discord bot?
Not to start. With the index and lists cached, the Free tier's 60 requests per five minutes covers a bot answering one community. Move to Pro when you serve many servers, or when you want the bulk files for an offline build.



