Multi-building rows: find the rows to split
POST /multi-building-flags takes the rows of a schedule of values as plain text and answers, for each row, whether it describes more than one separate building. A row such as "Bldgs A-D, garden apts" or "Main campus: 3 dorms, library, gym" puts every one of those buildings on one point, with one construction and one height, and a catastrophe model run on it inherits that. This route finds those rows so you can split them before you model.
Read the quickstart first for the two values you export (CEDE_BASE_URL, CEDE_API_KEY) and how to get a key. The same thing is on the website as a tool you can paste a schedule into: the multi-building row detector. Its sibling, exposure coding, codes rows into OED occupancy and construction; this route shares its switch, its limits and its provider.
On this page
What it is for, and what it is not
- A prompt to split, not a split. Nothing is written anywhere. You get a flag and a probability per row; splitting the row, and deciding how its value divides between buildings, is yours.
- Not a count. A flag says "more than one building". It does not say how many, and it does not read building counts out of the text for you.
- Not a chat. You send rows. The statement each row is asked about is fixed by Craton and published by digest in every answer; a request that carries anything but
rowsis refused. - Not a guess. A row is flagged only when the model's probability clears the gate. A row that leans several buildings below the gate is reported as
below_gate, for a person to read, never flagged silently.
Who does the checking
Each row is read by Jev, a decision model made by TypeSafe, reached through Vercel's AI Gateway (typesafe-ai/jev). Jev does not write text. Craton gives it one fixed statement, "this row covers more than one separate building", and Jev answers with the probability that the statement holds. Craton's own code then decides what is flagged. Every answer names the model, the provider that served it, and the digest of the question set, which is this route's own and separate from exposure coding's.
The request
{
"rows": [
"Bldgs A-D, garden apts, 2 sty wood frame, Charlotte NC",
"Apartment bldg, 24 units, 3 story, wood frame, Houston TX",
"Naves industriales 1 y 2, estructura metalica, Queretaro"
]
}| Member | What it is |
|---|---|
rows |
1 to 50 strings, one schedule row each, at most 1,000 characters after runs of spaces and line breaks are collapsed. The only member the route reads. |
More rows than that go in more requests. A request over a limit is refused with 413 payload_too_large, and a request that is not an object with a rows array of strings, or that carries any other member, with 422 invalid_request. Repeated rows in one request are sent once. A table row reads best sent the way the benchmark sent it: "Header: value; Header: value", money columns left out.
What comes back
{
"schema_version": "0.1.0",
"run_id": "multi-building-flags-v0:4b1c2d3e4f5a6b7c:8d9e0f1a2b3c4d5e",
"checked_at": "2026-09-27T18:02:11Z",
"model": {"name": "typesafe-ai/jev", "served_by": ["typesafe-ai"], "via": "vercel-ai-gateway"},
"question_set": {"id": "craton-jev-flags/1:...", "version": "2026-09-27T...", "digest": "sha256:..."},
"gate": {"several_buildings": 0.5},
"counts": {"rows": 2, "flagged": 1, "below_gate": 0, "not_flagged": 1, "unanswered": 0},
"rows": [
{"row": 1, "status": "flagged", "several_buildings": true, "probability": 0.97, "cleared": true, "generation_id": "gen_01M3CMP07REFE73BM5ZXHEK2MP"},
{"row": 2, "status": "not_flagged", "several_buildings": false, "probability": 0.03, "cleared": false, "generation_id": "gen_01M3CMP0RJ5B1TQ5X5Z9W4M0QK"}
]
}That shows the shape, not a recorded answer: the numbers are there to read the fields by, and the gate this environment uses is the one its answers report. The published schema is the whole contract.
| Field | What it is |
|---|---|
run_id |
Changes whenever any row is decided differently, or by another provider or question set. |
checked_at |
When, in UTC. The gateway reports no dated model version, so the date is part of the attribution. |
model |
The model as the gateway reported it, the providers that served this request, and the route. |
question_set |
Which fixed statement every row was asked, by digest. |
gate |
The probability a row needs to be flagged, never below an even chance. |
counts |
How many rows came back in each status. |
rows[].status |
flagged: the probability cleared the gate; split the row. below_gate: at least an even chance but under the gate; read it yourself. not_flagged: under an even chance. unanswered: no usable reply came back; send the row again. |
rows[].probability |
The model's probability that the row covers more than one separate building, 0 to 1, or null when unanswered. |
rows[].several_buildings |
That probability read at an even chance, or null. Whether a row is flagged is status, not this. |
rows[].cleared |
True exactly when the row is flagged. |
rows[].generation_id |
The AI gateway's id for the call that answered the row (gen_...), or null. |
The row text is not echoed back; rows are numbered from 1 in the order sent.
Limits, and when it is off
The limits are exposure coding's, and they are one set for both routes: a row checked here and a row coded there count against the same budgets, because the provider's limit is one account's.
- Per key: 20 requests a minute by default, on top of the API's own per-key limit. Per client address: 120 requests an hour. Over either,
429 rate_limitedwithRetry-After. - Pace: Craton spaces its calls at about 25 rows a minute across everyone using either route, so a request of 50 rows can come back with some rows
unanswered; ten rows a request is quickest. - Per day, across all of Craton: a budget of rows sent to the provider. Once it is used,
503 exposure_coding_unavailablewithRetry-Afteruntil midnight UTC. - Switched off: off unless the environment switches exposure coding on; one switch covers both routes. Off, every well-formed request answers
503 exposure_coding_unavailableand nothing leaves Craton. The same 503 comes back if the provider gave no usable answer for any row, withRetry-Afterwhen the provider named a wait.
Not metered: nothing is stored and no catastrophe model runs.
Where your rows go
Checking a row sends its text to Vercel's AI Gateway and on to the provider that serves Jev, exactly as exposure coding does: the gateway's listing says the providers do not train on what they are sent, and zero data retention is not available for this model, so a provider may keep the text for a time under its own terms. Craton does not store the rows, keep them in a cache or write them to its logs.
Send what describes a building or a site. Never send a person's name or anything else about a person.
Walk it
The first block checks three rows and prints each row's status and probability. If the environment you point it at has exposure coding switched off, or the provider did not answer, it prints the refusal instead, which is the other answer this route is documented to give.
set -euo pipefail
: "${CEDE_BASE_URL:?export CEDE_BASE_URL first, see the quickstart}"
: "${CEDE_API_KEY:?export CEDE_API_KEY first, see the quickstart}"
# 1: three schedule rows, US first.
cat > rows.json <<'JSON'
{"rows": [
"Bldgs A-D, garden apts, 2 sty wood frame, Charlotte NC",
"Apartment bldg, 24 units, 3 story, wood frame, Houston TX",
"Naves industriales 1 y 2, estructura metalica, Queretaro"
]}
JSON
status=$(curl -sS -o flags.json -w '%{http_code}' \
"$CEDE_BASE_URL/multi-building-flags" \
-H "Authorization: Bearer $CEDE_API_KEY" \
-H 'Content-Type: application/json' \
--data @rows.json)
if test "$status" = 503; then
# Off here, or the provider did not answer: the documented refusal.
python3 -c 'import json
error = json.load(open("flags.json"))["error"]
print(error["status"], error["code"])
print(error["message"])'
else
python3 -c 'import json
answer = json.load(open("flags.json"))
print("run :", answer["run_id"])
print("model :", answer["model"]["name"], "served by", ", ".join(answer["model"]["served_by"]))
print("gate :", answer["gate"]["several_buildings"])
for row in answer["rows"]:
print("row", row["row"], row["status"], row["probability"])'
fiThe second block writes the rows back out as CSV with a flag column, the way the website's download does: yes for a flagged row, check for one below the gate, no otherwise, and blank when no answer came back. It checks the same rows again, so it stands on its own.
set -euo pipefail
: "${CEDE_BASE_URL:?export CEDE_BASE_URL first, see the quickstart}"
: "${CEDE_API_KEY:?export CEDE_API_KEY first, see the quickstart}"
# 2: the same rows, with a flag column.
cat > rows.json <<'JSON'
{"rows": [
"Bldgs A-D, garden apts, 2 sty wood frame, Charlotte NC",
"Apartment bldg, 24 units, 3 story, wood frame, Houston TX",
"Naves industriales 1 y 2, estructura metalica, Queretaro"
]}
JSON
status=$(curl -sS -o flags.json -w '%{http_code}' \
"$CEDE_BASE_URL/multi-building-flags" \
-H "Authorization: Bearer $CEDE_API_KEY" \
-H 'Content-Type: application/json' \
--data @rows.json)
if test "$status" = 503; then
python3 -c 'import json
error = json.load(open("flags.json"))["error"]
print(error["status"], error["code"])'
else
python3 -c 'import csv, json, sys
rows = json.load(open("rows.json"))["rows"]
answer = json.load(open("flags.json"))
flag = {"flagged": "yes", "below_gate": "check", "not_flagged": "no"}
out = csv.writer(sys.stdout)
out.writerow(["LocNumber", "Text", "SeveralBuildings", "Probability", "Status"])
for checked, text in zip(answer["rows"], rows):
out.writerow([
checked["row"],
text,
flag.get(checked["status"], ""),
"" if checked["probability"] is None else checked["probability"],
checked["status"],
])'
fi