# 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](quickstart.md)
first for the two values the example exports (`CEDE_BASE_URL`,
`CEDE_API_KEY`).

## 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](/schema/exchange-model.v0.schema.json) 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](/exchange-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.

```json
{
  "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": "models@gulf-analytics.example.com"
  },
  "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 /account` shows it to them.
* `DELETE /exchange/models/{id}/grants/{account_id}` revokes it.
* `GET /exchange/models/{id}/grants` lists 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}/withdraw` takes 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](errors.md) 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.

<!-- cede:runnable -->

```bash
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": "nobody@example.com"
  },
  "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"])'
```
