The model exchange: models other people run
The model exchange lists catastrophe models that their providers run on their own Oasis platform. You run a listed model from the model lab the same way you run one of Craton's own, and its results open on the comparison page beside every other run. Craton never holds the provider's model files and never takes or passes on money: a listing is either free to run, or run only by the accounts its provider grants.
This page has two halves. The first is for anyone who runs models; the second is for providers who want to list one. Read the quickstart first for the two values the example exports (CEDE_BASE_URL, CEDE_API_KEY).
On this page
For people who run models
What is listed
GET /exchange/models lists every model on the exchange, in name order. It needs no key. Nothing is ranked or scored: the list is in name order and the comparison page shows measurements, not opinions.
Each entry is a whole listing, the same document GET /exchange/models/{id} answers with:
| Field | What it is |
|---|---|
id |
The listing's id, xm_ and 16 hex digits. The model lab names the model exchange:<id>. |
provider |
The provider's account id and the name they list under. |
name, version |
The model, and its semantic version. A new version is checked again before it is listed. |
perils |
The perils it computes losses for, in the risk object's peril codes. A European windstorm model lists extratropical_cyclone, and a hurricane or typhoon model lists tropical_cyclone. |
coverage |
Where it answers: a description, and a box [min_lon, min_lat, max_lon, max_lat]. |
access |
free: any account may run it. by_grant: only the accounts the provider granted. |
access_url |
Where you ask the provider for access, outside Craton. Set on every listing by grant. |
card |
The provider's model card: summary, assumptions, limitations, licence, contact. |
connector |
Which platform runs it: the API kind, the platform's address, the model's id there, the number of samples each run asks for, and, when the provider names them, the OED attributes the model expects on each location (oed). The provider's login is never in it. |
status, review |
listed and approved on everything in the public list. |
The published schema is the whole contract.
Running one
In the model lab, a listed model appears with the lab's own models, labeled with its provider's name. A model by grant that your account may not run yet shows "Request access", which opens the provider's access_url. Any payment for access is between you and the provider; Craton is not part of it.
Running a model on the exchange sends your locations and values to its provider. The lab writes your locations as an OED location file, sends it to the provider's platform with the analysis settings, and reads the losses back. The provider sees what any Oasis platform sees of an analysis: the locations, their values and the settings. They do not see your key, your email address or anything else in your account. Craton asks the platform to delete the portfolio and the analysis afterwards; whether it does is the provider's platform's behavior.
For providers: listing an Oasis model
Any Craton account can list a model. You need an Oasis platform (the OasisPlatform API, version 2) on the public internet, over https, with your model on it and a user Craton can log in as. Listing a model means agreeing to the terms for model providers: above all, the places and values a run sends you are used only for that run and then deleted.
1. Create a draft
POST /exchange/models with the listing's members. Craton sets the id, the provider's account (the one your key belongs to), the status and the review, and refuses a body that tries to set them.
{
"provider": {"name": "Gulf Analytics"},
"name": "Gulf wind",
"version": "1.0.0",
"perils": ["tropical_cyclone"],
"coverage": {"description": "US Gulf coast", "bbox": [-98.0, 24.0, -80.5, 31.5]},
"access": "by_grant",
"access_url": "https://gulf-analytics.example.com/access",
"card": {
"summary": "Ground-up wind losses for the US Gulf coast.",
"assumptions": ["A stochastic catalog of 10,000 years."],
"limitations": ["No storm surge."],
"licence": "Gulf Analytics model terms",
"contact": "[email protected]"
},
"connector": {
"base_url": "https://oasis.gulf-analytics.example.com/api",
"model_pk": 3,
"samples": 10,
"oed": {"country_code": "US", "occupancy_code": 1050, "construction_code": 5000, "currency": "USD"},
"credentials": {"username": "craton", "password": "..."}
}
}The answer is 201 with the listing, status: draft. A draft is visible to you and to nobody else: GET /exchange/models/{id} with your key reads it, and anyone else gets 404. PATCH /exchange/models/{id} changes it. Each top-level member you send replaces that member whole, except connector, whose members are written one at a time, so you can change your credentials without sending the address again.
connector.samples defaults to 10, and connector.kind to oasis-platform-v2, the only kind there is.
connector.oed is optional: the OED location attributes your model expects on every location the lab sends it, where the user's own data does not say. It carries any of country_code (ISO 3166-1 alpha-2, upper case), occupancy_code and construction_code (OED codes, as integers) and currency (ISO 4217, upper case), for example {"country_code": "US", "occupancy_code": 1050, "construction_code": 5000, "currency": "USD"}. Leave it out, or send null to remove it, and the lab's own defaults apply.
2. Credentials: sent once, never shown again
connector.credentials is either {"username", "password"} for your platform's access_token/ endpoint, or {"access_token"} with an optional "refresh_token". Send them on create or with a PATCH.
They are stored encrypted, under a key held by the environment rather than the database, and no response ever returns them, to you or anyone. A listing says only connector.credentials_stored: true. They are decrypted for one reader only: the model lab, when it runs your model for an account that may run it. An environment that holds no encryption key refuses to store credentials at all (422 exchange_credentials_unavailable) rather than keep them in clear.
Give Craton a platform user that can do what a run needs (create a portfolio, upload a location file, create and run an analysis, read its outputs) and nothing else.
3. The platform address
connector.base_url must be https, with no user name, query or fragment in it, and its host must resolve to public addresses only. Craton refuses an address that resolves to a private, loopback, link-local or shared address (422 unsafe_platform_address), when the listing is stored and again before every connection, and every connection goes to the addresses it checked. A host that does not resolve yet may be stored on a draft; the check below and every run need it to resolve.
4. Check that Craton can reach it
POST /exchange/models/{id}/check tries four things in order and stops at the first that fails:
| Step | Passes when |
|---|---|
address |
the base URL is https and its host resolves to public addresses only |
health |
GET {base_url}/healthcheck/ answers 200 |
login |
POST {base_url}/access_token/ accepts your stored user name and password (skipped when you stored an access token) |
model |
GET {base_url}/v2/models/{model_pk}/ answers 200 with that model |
It answers 200 whatever it finds, with "ok": true only when no step failed, and each step says what happened in a sentence. The check sends nothing about anyone's locations, stores nothing, follows no redirect, and never shows your credentials or the token your platform issued.
5. Ask for review
POST /exchange/models/{id}/review sets review.state to pending. It needs stored credentials (422 exchange_not_ready without them). Asking again while a review is pending changes nothing.
A person at Craton then reads the listing and decides. They check that the provider is who the listing says, that the card says what the model does and does not do, and that the platform answers. Approval is the only thing that lists a model: status becomes listed and the model appears in GET /exchange/models and in the lab. A rejection leaves it a draft, with review.note saying why; fix what it names and ask again.
Your own listing runs for your own account in any state: Craton hands the model lab your connector for your own runs before anyone has reviewed it, so you can try the model on your own locations before you ask.
6. Grants, for a listing by grant
On a listing whose access is by_grant:
PUT /exchange/models/{id}/grants/{account_id}grants one account. Ask the person for their account id:GET /accountshows it to them.DELETE /exchange/models/{id}/grants/{account_id}revokes it.GET /exchange/models/{id}/grantslists every account you granted, oldest first.
Granting twice keeps the first grant, and revoking an account that was never granted changes nothing. You always run your own model without a grant. On a free listing, every account may run the model and a grant is refused (422 exchange_not_ready). Craton records who is granted and nothing about why.
7. After it is listed
- Changing anything a person checked at review takes the model off the exchange and asks for review again: the provider name, the model's name, version, perils, coverage, access, access link or card, or the connector's address, model id, samples or OED attributes.
- Changing only the credentials keeps it listed.
POST /exchange/models/{id}/withdrawtakes it off the exchange (status: withdrawn) and cancels a pending review. Listing it again means asking for review again.
What Craton sends your platform
| When | What |
|---|---|
| A check | GET healthcheck/, POST access_token/ with your stored login, GET v2/models/{model_pk}/ with the token. Nothing else. |
| A run | Your login; then a portfolio, the user's locations and values as an OED location file, an analysis of your model with ground-up settings and your samples, the run, the output download, and the requests to delete the portfolio and the analysis. |
Craton spends no compute on your model and keeps none of your files. Your platform's own terms decide what it keeps.
The lab's own route
GET /internal/exchange/models/{id}/connector is how the model lab gets what it needs to run an exchange model for one account. It answers only Craton's own services on the same machine, holding the environment's internal credential, and refuses every other request: 401 unauthenticated without that credential, 403 internal_only from anywhere else. You never call it.
Refusals
| Status and code | When |
|---|---|
404 not_found |
No such listing, or one that is neither listed nor yours. Also a key configured into the environment rather than issued to an account: a listing belongs to an account. |
422 invalid_exchange_model |
The body sets a member Craton sets, carries one a listing does not have, misses one, or has a value the schema refuses. details names every fault. |
422 unsafe_platform_address |
connector.base_url is not one Craton connects to; the message says why. |
422 exchange_credentials_unavailable |
Credentials sent to an environment that holds no key to encrypt them with. |
422 exchange_not_ready |
A review asked for without stored credentials or for a model already listed, or a grant on a free listing. |
The error reference lists every code.
Walk it
The block below is a provider's whole round trip on a draft: it signs up a provider account and a second account, creates a listing by grant, reads it, sends credentials, runs the check, asks for review, grants the second account and revokes it, withdraws the listing, and shows that the lab's route refuses a request without its credential. The platform address is a placeholder that does not resolve, so the check stops at its first step and says so. If the environment you point it at holds no encryption key, the credentials are refused and so is the review, and the block prints both refusals, which are the answers this page documents for that case.
set -euo pipefail
: "${CEDE_BASE_URL:?export CEDE_BASE_URL first, see the quickstart}"
# Read one field out of a JSON document on stdin: `field a.b.0.c`.
field() {
python3 -c 'import json, sys
document = json.load(sys.stdin)
for step in sys.argv[1].split("."):
document = document[int(step)] if step.isdigit() else document[step]
print(document)' "$1"
}
# 1. A provider account, and a second account to grant.
CEDE_API_KEY=$(curl -sS --fail-with-body -H "Content-Type: application/json" \
-d '{"label": "exchange provider walkthrough"}' \
"$CEDE_BASE_URL/signup" | field api_key.secret)
auth="Authorization: Bearer $CEDE_API_KEY"
friend=$(curl -sS --fail-with-body -H "Content-Type: application/json" \
-d '{"label": "exchange user walkthrough"}' "$CEDE_BASE_URL/signup" | field id)
# 2. What is listed now. No key needed.
curl -sS --fail-with-body "$CEDE_BASE_URL/exchange/models" \
| python3 -c 'import json, sys; print("listed now:", json.load(sys.stdin)["count"])'
# 3. A draft listing, by grant, with no credentials yet.
cat > listing.json <<'JSON'
{
"provider": {"name": "Walkthrough Analytics"},
"name": "Walkthrough wind",
"version": "1.0.0",
"perils": ["tropical_cyclone"],
"coverage": {"description": "US Gulf coast", "bbox": [-98.0, 24.0, -80.5, 31.5]},
"access": "by_grant",
"access_url": "https://walkthrough.example.com/access",
"card": {
"summary": "A placeholder listing that shows the provider's round trip.",
"assumptions": ["None: this listing runs nothing."],
"limitations": ["Its platform address does not resolve."],
"licence": "None",
"contact": "[email protected]"
},
"connector": {"base_url": "https://oasis.example.com/api", "model_pk": 3}
}
JSON
model=$(curl -sS --fail-with-body -H "$auth" -H "Content-Type: application/json" \
-d @listing.json "$CEDE_BASE_URL/exchange/models" | field id)
echo "draft: $model"
# 4. Your own draft reads back with your key.
curl -sS --fail-with-body -H "$auth" "$CEDE_BASE_URL/exchange/models/$model" \
| python3 -c 'import json, sys
listing = json.load(sys.stdin)
print("status:", listing["status"], "| review:", listing["review"]["state"])
print("credentials stored:", listing["connector"]["credentials_stored"])'
# 5. Credentials, sent once. Refused where the environment holds no key.
status=$(curl -sS -o patched.json -w '%{http_code}' -X PATCH -H "$auth" \
-H "Content-Type: application/json" \
-d '{"connector": {"credentials": {"username": "craton", "password": "walkthrough"}}}' \
"$CEDE_BASE_URL/exchange/models/$model")
if test "$status" = 422; then
python3 -c 'import json; e = json.load(open("patched.json"))["error"]; print(e["status"], e["code"])'
else
python3 -c 'import json
listing = json.load(open("patched.json"))
assert "password" not in json.dumps(listing)
print("credentials stored:", listing["connector"]["credentials_stored"])'
fi
# 6. The check: four steps, stopping at the first that fails.
curl -sS --fail-with-body -X POST -H "$auth" \
"$CEDE_BASE_URL/exchange/models/$model/check" \
| python3 -c 'import json, sys
check = json.load(sys.stdin)
print("ok:", check["ok"])
for step in check["steps"]:
print(" ", step["step"].ljust(7), step["outcome"].ljust(7), step["detail"])'
# 7. Ask for review. It needs stored credentials.
status=$(curl -sS -o review.json -w '%{http_code}' -X POST -H "$auth" \
"$CEDE_BASE_URL/exchange/models/$model/review")
if test "$status" = 422; then
python3 -c 'import json; e = json.load(open("review.json"))["error"]; print(e["status"], e["code"])'
else
python3 -c 'import json; print("review:", json.load(open("review.json"))["review"]["state"])'
fi
# 8. Grant the second account, read the grants, revoke it.
curl -sS --fail-with-body -X PUT -H "$auth" \
"$CEDE_BASE_URL/exchange/models/$model/grants/$friend" | field count
curl -sS --fail-with-body -H "$auth" "$CEDE_BASE_URL/exchange/models/$model/grants" \
| field grants.0.account_id
curl -sS --fail-with-body -X DELETE -H "$auth" \
"$CEDE_BASE_URL/exchange/models/$model/grants/$friend" | field count
# 9. Withdraw it.
curl -sS --fail-with-body -X POST -H "$auth" \
"$CEDE_BASE_URL/exchange/models/$model/withdraw" | field status
# 10. The lab's own route refuses a request without its credential.
status=$(curl -sS -o internal.json -w '%{http_code}' \
"$CEDE_BASE_URL/internal/exchange/models/$model/connector?account_id=$friend")
test "$status" = 401
python3 -c 'import json; e = json.load(open("internal.json"))["error"]; print(e["status"], e["code"])'