# A technical price, from nothing

One terminal, no key in advance. By the end you will have an account of your
own, a complete parametric object, and a **technical price** for it with every
assumption behind the number attached to the same response.

Two blocks, because there are two ways to get to a priceable object: write the
whole thing yourself, or send Craton a location schedule and finish the object it
produces with `PATCH /objects/{id}`. Run either on its own; the second is the
one to read if the exposure came out of a file.

Read [the quickstart](quickstart.md) first if you have not: it explains
`CEDE_BASE_URL`, the `Authorization: Bearer` header, and the job pattern every
Craton verb answers with. This page repeats none of that and assumes all of it.

---

## What you need

* `curl` 7.76 or newer (this page uses `--fail-with-body`) and `python3`.
* `CEDE_BASE_URL` — the base URL of the Craton environment you are pointed at,
  with no trailing slash. That is the only thing this page needs from you.

```sh
export CEDE_BASE_URL="https://altier.ridgehead-hamlet.ts.net:8472"   # no trailing slash
```

**No `CEDE_API_KEY` here, on purpose.** The block below signs up for an account
of its own every time it runs, so it works from a standing start and shows the
two account routes doing their job:

* **`POST /signup`** — unauthenticated, self-service, nobody in the loop. It
  creates an account on the free sandbox plan and hands back that account's
  first key. The secret is in that response and in no other: Craton keeps only its
  digest, so the block prints it the moment it arrives — the same
  shown-once convention [the quickstart](quickstart.md) uses. Save what it
  prints if you want the account again; a run that scrolls past is a run whose
  account you keep nothing of.
* **`GET /account`** — the account the presented key belongs to: its id, its
  plan and the label you chose at signup. It never returns the secret, because
  Craton does not have it.

Already holding a key from somewhere else? Then skip step 1 and send yours
instead — everything after it is the same. Each run of the block as written is
a separate tenant, and objects belong to the account that made them.

---

## Before you copy the object: the feed is not yours to invent

Everything in the object below is a decision you make — the limit, the
attachment, the box, the ladder — with exactly one exception. A parametric
trigger names the data it is measured against, an `id` and a `version` in
`trigger.data_sources[0]`, and **both must already be pinned in the deployment
you are pointed at.** Craton prices from committed snapshots and makes no network
call while it runs, which is what lets a result replay byte for byte a year
later; the same rule read from the other end is that a vintage nobody committed
cannot be fetched for you on demand.

Both blocks on this page name `usgs-eq-kanto` at version `2026-08-10` — the
pinned USGS catalogue for the Kanto box the peril block describes — and that
pair is why they price. Write a plausible-looking id of your own instead (a
country-wide quake catalogue, say, or yesterday's vintage of a real feed) and
nothing refuses you while you are writing: the object still validates, `POST
/objects` still stores it and `PATCH /objects/{id}` still accepts it. The
refusal arrives one call later, on the **price job**, as `422
unpriceable_object` saying `no pinned snapshot for feed …` and listing every
`id@version` that is pinned. It is determinism doing its job rather than a typo
the engine will forgive, so the fix is always to name a feed that exists.

Two ways to read that list *before* you write the block rather than after:

* **[The pinned data feeds](feeds.md)** — every pinned `id@version` with the
  kind, the coverage and the licence of each, and [what each feed
  publishes](feeds.md#the-measurement-variable-each-feed-publishes) for the
  `measurement.variable` that goes beside it.
* **`GET /primitives`** — the same list from the deployment you are pointed at,
  which is the one that actually decides. Step 3 of each block below calls it,
  prints what it found, and stops there if the feed this page names is not on
  it — so a stale page fails while you are still reading it, not inside a job.

A feed id is a place and a source, not a peril: there is no all-of-the-world
catalogue to fall back on, and coverage is checked on every call.

---

## Which model prices here: Craton's, not one you composed

`POST /objects/{id}/price` prices with the **platform pricing model**. Craton
picks one of its own two pricers from the shape of the object's trigger — the
pair tabulated under [what is in a technical
price](#what-is-in-a-technical-price) further down — and labels the response
with *that* model and its version. The verb reads your object; it never reaches
for a model in your account, so the name on this page's answer is a Craton model
whatever you have composed in the builder. The label is the response telling
you truthfully what computed the number.

A technical price labelled with a model **you** composed comes from a different
route. **`POST /models/{id}/runs`** measures your model's index over a risk
object's cover period and answers with a `model_run` whose `model.id`,
`model.name` and `model.version` are yours, beside its own `technical_price`.
**[Composing a model](model-builder.md)** is that road end to end: compose from
the four primitives, read the automatic validation pass and its forty-year
backtest, then run the model against an object. This page walks the other one —
everything below prices an object with Craton's own pricer.

---

## The whole thing, in one block

Run this in an empty directory.

<!-- cede:runnable -->

```bash
set -euo pipefail

: "${CEDE_BASE_URL:?export CEDE_BASE_URL first — see 'What you need'}"

# 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 — An account, from nothing. /signup takes no credential and answers with
#     the account and its first key. The secret is in this response and in no
#     other, so it is printed here: Craton stores only its digest.
account=$(curl -sS --fail-with-body \
  -H "Content-Type: application/json" \
  -d '{"label": "technical price walkthrough"}' \
  "$CEDE_BASE_URL/signup")
CEDE_API_KEY=$(printf '%s' "$account" | field api_key.secret)
export CEDE_API_KEY
auth="Authorization: Bearer $CEDE_API_KEY"
printf 'your key (shown once): %s\n' "$CEDE_API_KEY"

# 2 — Who that key belongs to. /account is the first call that needs it.
curl -sS --fail-with-body -H "$auth" "$CEDE_BASE_URL/account" > account.json
printf 'account %s on the %s plan, labelled %s\n' \
  "$(field id < account.json)" \
  "$(field plan < account.json)" \
  "$(field label < account.json)"

# 3 — What this deployment pins. The trigger below names one feed by id and
#     version, and a price reads committed snapshots and never the network, so
#     a pair that is not on this list cannot be priced against. Reading it here
#     turns a stale page into a failure now rather than a failed job later.
curl -sS --fail-with-body -H "$auth" "$CEDE_BASE_URL/primitives" > primitives.json
python3 -c '
import json, sys
pinned = [(feed["id"], feed["version"]) for feed in json.load(open("primitives.json"))["feeds"]]
for feed_id, version in pinned:
    print("pinned feed:", feed_id + "@" + version)
wanted = ("usgs-eq-kanto", "2026-08-10")
if wanted not in pinned:
    sys.exit("this page names %s@%s, which this deployment does not pin — "
             "write one of the pairs above into trigger.data_sources instead."
             % wanted)
print("the feed this page prices against is pinned here")
'

# 4 — Something to price. Ingest emits exposure and peril; a price needs the
#     rest — a limit, an attachment and the index that decides the payout — so
#     the whole object is written out here and sent as JSON.
cat > priceable-object.json <<'JSON'
{
  "schema_version": "0.1.0",
  "status": "analysed",
  "exposure": {
    "kind": "location_schedule",
    "currency": "JPY",
    "locations": [
      { "ref": "LOC-0001",
        "address_as_given": "2-16-1 Konan, Minato-ku, Tokyo 108-0075",
        "latitude": 35.6284, "longitude": 139.7387,
        "geocode": { "resolution": "rooftop", "confidence": 0.96 },
        "occupancy": "warehouse", "construction": "reinforced_concrete",
        "year_built": 2011,
        "values": { "building": { "amount": 8400000000, "currency": "JPY" } } }
    ],
    "source_fidelity": { "unmapped_columns": [], "guessed_units": [], "ambiguous_rows": [] }
  },
  "peril": {
    "code": "earthquake",
    "region": {
      "description": "Kanto cat-in-a-box: 34.9-36.2N, 139.0-140.6E.",
      "bounding_geometry": {
        "type": "Polygon",
        "coordinates": [[[139.0, 34.9], [140.6, 34.9], [140.6, 36.2],
                         [139.0, 36.2], [139.0, 34.9]]]
      }
    }
  },
  "financial_structure": {
    "limit": { "amount": 1000000000, "currency": "JPY" },
    "attachment": { "value": 6.0, "unit": "M", "index_ref": "kanto-eq-box-magnitude" }
  },
  "trigger": {
    "type": "parametric_cat_in_a_box",
    "index": {
      "name": "kanto-eq-box-magnitude",
      "version": "1.0.0",
      "description": "Largest catalogue magnitude inside the Kanto box, shallower than the depth threshold.",
      "measurement": { "variable": "catalogue_preferred_magnitude", "unit": "M", "statistic": "max" },
      "aggregation_window": { "duration": "PT72H", "alignment": "event" },
      "thresholds": [
        { "label": "attachment", "level": 6.0, "unit": "M" },
        { "label": "exhaustion", "level": 7.0, "unit": "M" },
        { "label": "max_focal_depth", "level": 100, "unit": "km" }
      ],
      "payout_function": {
        "type": "step",
        "points": [ { "level": 6.0, "payout_ratio": 0.25 },
                    { "level": 6.5, "payout_ratio": 0.5 },
                    { "level": 7.0, "payout_ratio": 1 } ],
        "maximum_payout_ratio": 1
      }
    },
    "data_sources": [
      { "id": "usgs-eq-kanto",
        "name": "USGS ANSS Comprehensive Earthquake Catalog - Kanto extract",
        "kind": "quake_catalogue",
        "version": "2026-08-10", "vintage": "2026-08-10" }
    ]
  },
  "period": {
    "inception": "2026-04-01T00:00:00+09:00",
    "expiry": "2027-04-01T00:00:00+09:00",
    "timezone": "Asia/Tokyo"
  }
}
JSON

object_id=$(curl -sS --fail-with-body -H "$auth" \
  -H "Content-Type: application/json" \
  --data-binary @priceable-object.json \
  "$CEDE_BASE_URL/objects" | field id)
echo "object: $object_id"

# 5 — Price it. A job, like every Craton verb: 202 and an id, never the answer
#     in the same breath as the request.
job_id=$(curl -sS --fail-with-body -X POST -H "$auth" \
  "$CEDE_BASE_URL/objects/$object_id/price" | field id)
echo "price job: $job_id"

# 6 — Poll it until it stops moving.
state=unknown
for _ in $(seq 1 300); do
  curl -sS --fail-with-body -H "$auth" "$CEDE_BASE_URL/jobs/$job_id" > job.json
  state=$(field status < job.json)
  case "$state" in succeeded|failed) break ;; esac
  sleep 0.2
done
if [ "$state" != succeeded ]; then
  printf 'the price job did not succeed: %s\n' "$(cat job.json)" >&2
  exit 1
fi

# 7 — The number, and the working behind it. Everything printed below came out
#     of the one response: the price is four lines of arithmetic, and the rest
#     of the document is what it was computed from.
python3 -c '
import json
price = json.load(open("job.json"))["result"]["price"]
assumptions = price["assumptions"]
premium, burn = price["technical_price"], assumptions["burn"]
record = assumptions["period_of_record"]

print("technical price:", premium["amount"], premium["currency"],
      "- rate on line", premium["rate_on_line"])
print("expected loss:  ", price["expected_loss"]["amount"],
      price["expected_loss"]["currency"])
print("method:", assumptions["method"]["name"], "over",
      record["years"], "years", record["start"][:4], "-", record["end"][:4])
print("burn rate:", burn["burn_rate"], "-", burn["triggering_years"],
      "of", burn["years"], "years would have paid")
for loading in assumptions["loadings"]:
    print("  loading:", loading["name"], "at", loading["rate"], "=",
          loading["amount"]["amount"], loading["amount"]["currency"])
    print("    base: ", loading["base"]["arithmetic"])
    print("    then: ", loading["arithmetic"])
build_up = assumptions["price_build_up"]
print("the whole ladder, from the expected loss up:")
print("  expected loss:", build_up["expected_loss"]["base"]["arithmetic"])
for step in build_up["steps"]:
    print("  +", step["component"], step["amount"]["amount"],
          "-> running total", step["running_total"]["amount"],
          build_up["currency"])
print("  = technical price", build_up["technical_price"]["amount"],
      build_up["technical_price"]["currency"])
assert build_up["steps"][-1]["running_total"] == build_up["technical_price"]
for source in assumptions["data_sources"]:
    print("  data:", source["id"], "vintage", source["vintage"],
          "sha256", source["sha256"][:16], "-", source["licence"])
print("model:", price["model"]["name"], price["model"]["version"])
print("run id:", price["run_id"], "- content-addressed, so a replay is this id")
print("limitations stated on the result:", len(assumptions["limitations"]))
for limitation in assumptions["limitations"]:
    print("  -", limitation)
print(premium["disclosure"])
'
echo "done — the priced object is at /objects/$object_id"
```

If the last line printed, you have a technical price and the whole assumption
set that produced it.

---

## The other way in: a file, then the parts a file cannot carry

The block above writes the whole object by hand, which is the right shape when
the exposure is an index or a single site you already have in front of you. The
other way — the usual way — is to send Craton the schedule you were given and
finish the object it produces.

A location schedule carries buildings and values. It does not carry the peril
you are covering, the limit, the attachment or the trigger: those are decisions
you make after reading it, and Ingest does not invent them (SPEC §3.1 is
normalisation, no enrichment). [The trigger object](trigger.md) is the
field-by-field reference for the hardest of the four — every member of the
block, what each model requires of it, and the smallest trigger of each shape
that prices. `PATCH /objects/{id}` is where you record them,
**on the object the file became** — same id, same exposure, same source file in
its provenance — rather than on a hand-copied replica of it.

Run this in an empty directory too. It signs up for its own account, ingests a
two-row schedule that names no peril at all, writes the peril and the structure
onto the object that comes back, and prices it.

<!-- cede:runnable -->

```bash
set -euo pipefail

: "${CEDE_BASE_URL:?export CEDE_BASE_URL first — see 'What you need'}"

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 — An account of this block's own.
CEDE_API_KEY=$(curl -sS --fail-with-body \
  -H "Content-Type: application/json" \
  -d '{"label": "schedule to price walkthrough"}' \
  "$CEDE_BASE_URL/signup" | field api_key.secret)
export CEDE_API_KEY
auth="Authorization: Bearer $CEDE_API_KEY"
printf 'your key (shown once): %s\n' "$CEDE_API_KEY"

# 2 — A schedule with no peril column: buildings, values, and nothing that
#     says what the cover is against. This is the ordinary case.
cat > schedule.csv <<'CSV'
Location ID,Address,Latitude,Longitude,Occupancy,Building Value,Currency
LOC-0001,"2-16-1 Konan, Minato-ku, Tokyo",35.6284,139.7387,Warehouse,8400000000,JPY
LOC-0002,"1-1 Soga, Chuo-ku, Chiba",35.5687,140.1247,Light Industrial,5100000000,JPY
CSV

ingest_job=$(curl -sS --fail-with-body -H "$auth" \
  -F "file=@schedule.csv;type=text/csv" "$CEDE_BASE_URL/ingest" | field id)

state=unknown
for _ in $(seq 1 150); do
  curl -sS --fail-with-body -H "$auth" "$CEDE_BASE_URL/jobs/$ingest_job" > job.json
  state=$(field status < job.json)
  case "$state" in succeeded|failed) break ;; esac
  sleep 0.2
done
if [ "$state" != succeeded ]; then
  printf 'ingest did not succeed: %s\n' "$(cat job.json)" >&2
  exit 1
fi
object_id=$(field result.objects.0.id < job.json)
echo "object out of the file: $object_id"

# 3 — What this deployment pins, before writing a trigger that names a feed.
#     The id and the version below have to be on this list: a snapshot that was
#     never committed cannot be fetched at price time, and the object the PATCH
#     writes would store cleanly and fail in the price job instead.
curl -sS --fail-with-body -H "$auth" "$CEDE_BASE_URL/primitives" > primitives.json
python3 -c '
import json, sys
pinned = [(feed["id"], feed["version"]) for feed in json.load(open("primitives.json"))["feeds"]]
for feed_id, version in pinned:
    print("pinned feed:", feed_id + "@" + version)
wanted = ("usgs-eq-kanto", "2026-08-10")
if wanted not in pinned:
    sys.exit("this page names %s@%s, which this deployment does not pin — "
             "write one of the pairs above into trigger.data_sources instead."
             % wanted)
print("the feed this page prices against is pinned here")
'

# 4 — The parts the file could not carry. Each block replaces the block of
#     that name whole; nothing else on the object is touched.
cat > completion.json <<'JSON'
{
  "peril": {
    "code": "earthquake",
    "region": {
      "description": "Kanto cat-in-a-box: 34.9-36.2N, 139.0-140.6E.",
      "bounding_geometry": {
        "type": "Polygon",
        "coordinates": [[[139.0, 34.9], [140.6, 34.9], [140.6, 36.2],
                         [139.0, 36.2], [139.0, 34.9]]]
      }
    }
  },
  "financial_structure": {
    "limit": { "amount": 1000000000, "currency": "JPY" },
    "attachment": { "value": 6.0, "unit": "M", "index_ref": "kanto-eq-box-magnitude" }
  },
  "trigger": {
    "type": "parametric_cat_in_a_box",
    "index": {
      "name": "kanto-eq-box-magnitude",
      "version": "1.0.0",
      "description": "Largest catalogue magnitude inside the Kanto box, shallower than the depth threshold.",
      "measurement": { "variable": "catalogue_preferred_magnitude", "unit": "M", "statistic": "max" },
      "aggregation_window": { "duration": "PT72H", "alignment": "event" },
      "thresholds": [
        { "label": "attachment", "level": 6.0, "unit": "M" },
        { "label": "exhaustion", "level": 7.0, "unit": "M" },
        { "label": "max_focal_depth", "level": 100, "unit": "km" }
      ],
      "payout_function": {
        "type": "step",
        "points": [ { "level": 6.0, "payout_ratio": 0.25 },
                    { "level": 6.5, "payout_ratio": 0.5 },
                    { "level": 7.0, "payout_ratio": 1 } ],
        "maximum_payout_ratio": 1
      }
    },
    "data_sources": [
      { "id": "usgs-eq-kanto",
        "name": "USGS ANSS Comprehensive Earthquake Catalog - Kanto extract",
        "kind": "quake_catalogue",
        "version": "2026-08-10", "vintage": "2026-08-10" }
    ]
  },
  "period": {
    "inception": "2026-04-01T00:00:00+09:00",
    "expiry": "2027-04-01T00:00:00+09:00",
    "timezone": "Asia/Tokyo"
  }
}
JSON

curl -sS --fail-with-body -X PATCH -H "$auth" \
  -H "Content-Type: application/json" \
  --data-binary @completion.json \
  "$CEDE_BASE_URL/objects/$object_id" > completed.json
printf 'peril now: %s - and the exposure is still the file'"'"'s\n' \
  "$(field peril.code < completed.json)"

# 5 — Price the object the file became.
price_job=$(curl -sS --fail-with-body -X POST -H "$auth" \
  "$CEDE_BASE_URL/objects/$object_id/price" | field id)

state=unknown
for _ in $(seq 1 300); do
  curl -sS --fail-with-body -H "$auth" "$CEDE_BASE_URL/jobs/$price_job" > priced.json
  state=$(field status < priced.json)
  case "$state" in succeeded|failed) break ;; esac
  sleep 0.2
done
if [ "$state" != succeeded ]; then
  printf 'the price job did not succeed: %s\n' "$(cat priced.json)" >&2
  exit 1
fi

python3 -c '
import json
price = json.load(open("priced.json"))["result"]["price"]
premium = price["technical_price"]
print("technical price:", premium["amount"], premium["currency"],
      "- rate on line", premium["rate_on_line"])
print("priced object:", price["subject"]["object_id"], "- the one the file became")
print(premium["disclosure"])
'
echo "done — the priced object is at /objects/$object_id"
```

Read the object back afterwards and everything is in one place: the locations
as Ingest normalised them, `exposure.source_fidelity` recording how the file
was read, the blocks you wrote, the premium Price computed and the run that
produced it in `provenance`.

Change your mind about the limit and send another `PATCH`: the old premium
comes off and the object returns to `draft`, because a premium computed for a
different limit is a wrong number that reads exactly like a right one. Price it
again — it is one call. The premium itself is never something you send; it is
what Price writes, and the route refuses a hand-written one.

---

## What `PATCH /objects/{id}` writes, and what it does not

Six blocks, and those six only. Each one replaces the block of that name
whole — there is no deep merge, so a `financial_structure` arrives with its
limit *and* its attachment or it arrives half-written:

| Block | What you are recording |
|---|---|
| `peril` | what the cover is against, and the region it is scoped to |
| `financial_structure` | the limit and the attachment (`premium` is Price's output, not an input) |
| `trigger` | the index or the wording reference — [the trigger object](trigger.md) is the field-by-field reference |
| `period` | inception, expiry, and the zone day boundaries are read in |
| `jurisdiction` | governing law and territories |
| `counterparties` | who is on each side, as a record |

The other four blocks of a canonical object are read-only on this route, and
each has exactly one place it comes from. Sending one is a `422
invalid_request` whose `details` say the same thing this table says, one line
per block — but the table is here so it can be read before the round trip
rather than after it:

| Block | Why not here | Where it comes from |
|---|---|---|
| `exposure` | It is Ingest's record of a file it read, and `exposure.source_fidelity` is its account of how that file was read. Edit the exposure by hand and that account describes a file nobody can re-read. | `POST /ingest`, from the file. To state an exposure yourself, `POST /objects` — a different exposure is a different object, with its own provenance. |
| `id`, `schema_version` | The identity of an object is the server's, and a document conforms to the schema version it was written against rather than to one a caller renames it to. | The server, when the object is created. |
| `status` | It is what the object has, not what a caller asserts about it. | The verbs. A price run sets `priced` when it has priced; another `PATCH` to a pricing input sets it back to `draft`. |
| `provenance` | Append-only: it is the record of what has happened to this object. | Every verb, as it runs — the ingest run, each price, analysis and backtest. |

### The file did not say the currency — now what?

The common case behind this, and worth walking because the answer is not a
`PATCH`. A schedule whose money columns carry no currency anywhere produces an
object with the amounts on it, no `exposure.currency`, and a line in
`exposure.source_fidelity.ambiguous_rows` saying no currency is stated
anywhere in the file. That line is the honest reading of what arrived, so
overwriting the exposure to add `"currency": "USD"` would leave the object
saying two things at once: that the file did not say, and that it did.

Two routes, and both keep the object saying one thing:

* **State it in the file and ingest it again.** Ingest reads a currency from
  the money column's own header — `Building Value (USD)` — or from a banner
  cell sitting over the money columns. A column headed `Currency` on its own
  is recorded in `source_fidelity.unmapped_columns` instead, with the values
  it held, so check the object you get back rather than assuming. When the
  header carries it, `exposure.currency` and the per-value `currency` tags
  come back populated and `source_fidelity` has nothing to record.
* **Write the object yourself.** `POST /objects` takes a complete canonical
  object, exposure included, and stores it under a new id. Its provenance is
  yours to state and should say what is true: the exposure came from you, not
  from a file Ingest read.

What is *not* ambiguous either way is the money on the structure.
`financial_structure.limit` carries its own currency and you write it with
`PATCH`, so a limit in USD over an exposure whose file named no currency is
not a contradiction Craton is hiding — it is the file's silence, recorded where
you can see it.

---

## What just happened

**`POST /signup`** — an account on the free sandbox plan and its first key,
handed to a stranger holding no credential, live for the very next request. The
`api_key.secret` member of that response is the value to export as
`CEDE_API_KEY`; it is shown once because Craton holds only its digest, and a
second signup is a second tenant that cannot see the first one's work.

**`GET /account`** — the account behind the key you presented, with its plan
and label. Two things it deliberately does not carry: the secret, which Craton
does not hold, and anyone else's account, which is not yours to read. A key
that was configured into a deployment by whoever runs it — rather than handed
out by signup — belongs to no account and gets a `404` here while continuing to
work everywhere else.

**`GET /primitives`** — what this deployment pins, read before a trigger is
written rather than after one is refused: every feed by `id` and `version`,
with the measurements, transforms and payout functions the model builder
composes from. Both blocks stop there if the feed this page names is not on the
list they got back. **[The pinned data feeds](feeds.md)** is the same list as a
page, with each feed's coverage and licence.

**`POST /objects`** — a complete canonical object, sent as JSON. The one the
quickstart's ingest run produced carries exposure and peril, which is what
Ingest promises and no more; a price needs a limit, an attachment and a trigger
as well, so this page writes the whole document out and posts it.

**`PATCH /objects/{id}`** — the second block's way in, and the one to reach for
when the exposure came out of a file. It writes six blocks onto an object that
already exists — `peril`, `financial_structure`, `trigger`, `period`,
`jurisdiction`, `counterparties` — and refuses everything else. [What `PATCH
/objects/{id}` writes, and what it does
not](#what-patch-objectsid-writes-and-what-it-does-not) is the whole table:
the six, the four that are read-only here, and where each of those four comes
from instead.

**`POST /objects/{id}/price`** — the Price verb. `202`, a job id, and the job's
own address in the `Location` header. Poll `GET /jobs/{id}` until it reads
`succeeded` and the price is on `result.price`.

---

## What is in a technical price

The number is the small part of the response. SPEC's requirement is that the
**complete assumption set** travels with it as first-class fields, so nothing
downstream has to guess what produced it:

```
technical_price       amount, currency, rate on line, and the disclosure
                      that travels on every copy
expected_loss         the burn rate times the limit times the period factor
assumptions.burn      every year of the record with the payout ratio it would
                      have had, the burn rate, and how many years triggered
assumptions.loadings  each loading itemised: its rate, the money that rate
                      multiplies and the terms that money is made of, the
                      resulting amount, and why it is there — parameter
                      uncertainty, volatility, expense
assumptions.price_build_up
                      the ladder from the expected loss to the technical
                      price as a running total, one rung per loading, so the
                      itemisation can be checked without re-deriving anything
assumptions.event_selection
                      what the model measured, in the shape of the thing it
                      measured: the box, the catalogue events that fell in it
                      and each qualifying occurrence by the publisher's own
                      event id — or, for a station-measured index, the
                      measurement, the transform and every year's window
assumptions.data_sources
                      publisher, licence, vintage and the sha256 of the exact
                      snapshot the number was computed from
assumptions.method    the method by name, its clustering rule, and what it
                      does not do
assumptions.limitations
                      what this price is weak at, in the response rather than
                      in a footnote a reader never sees
run_id                content-addressed from the object, the model version and
                      the snapshot digest: the same inputs replay to the same
                      id and the same bytes
```

`technical_price.basis` is the constant `technical`, and the disclosure
sentence beside it is part of the published schema rather than a convention of
this page. A technical price is an actuarial estimate produced by analytics
software from the assumptions stated next to it. Nobody stands ready to
transact at it, it carries no capacity and no acceptance, and Craton never moves
money or touches paper.

### How the premium is built, and how to check it

Four numbers, in one order, and every one of them is a field. The expected loss
is the burn rate times the limit times the cover period factor. Each loading is
a rate times a base. The premium is the four added up:

```
expected_loss              =  burn_rate x limit x cover_period.factor
+ parameter_uncertainty    =  0.15 x expected_loss
+ volatility               =  0.20 x (standard_deviation x limit x factor)
+ expense                  =  0.05 x (expected_loss + the two loadings above)
-------------------------------------------------------------------------
= technical_price
```

Read that ladder off the response rather than from this page. Every loading in
`assumptions.loadings` carries:

* `rate` — the rate, as a number;
* `base` — **the money that rate multiplies**, as an amount in the response's
  currency, together with `terms`: the named values it is the product or the
  sum of, each of them a figure published elsewhere in the same response;
* `amount` — the resulting money;
* `arithmetic` — the same multiplication written out in the digits this
  response prints, for a reader rather than a parser.

So the three loadings do **not** compound one on top of another in a chain.
Parameter uncertainty and volatility both sit on quantities derived from the
expected loss and the limit; expense is the only one that loads the loadings,
and it says so in its own `base.terms`.

`assumptions.price_build_up` is the same walk as a running total —
`steps[0]` is the expected loss, one rung per loading in the order they are
applied, and the last `running_total` is the technical price. Nothing in it is
new information; it exists so that two readers checking the same response
reconstruct the same ladder.

**The published numbers are the arithmetic.** Each rate multiplies its base as
published, rounded to the currency's minor unit, so `rate x base` reproduces
the loading exactly rather than to within a rounding step, and the four
components add to the premium with nothing left over. A reader holding only the
response can check the whole of it with a pocket calculator, and
`assumptions.currency_basis` states the rounding rule the response was written
under.

**What Price reads.** Two models, one per index shape, and the object is
priced by the one that reads what it declares. Both read a **pinned** feed
vintage — the `id` and `version` in `trigger.data_sources[0]` must already be
committed to the build, because a result that replays byte for byte cannot
fetch anything at price time. **[The pinned data feeds](feeds.md)** is the list
to write that block from, and `GET /primitives` is the same list from the
deployment you are pointed at:

* `cede/parametric-burn-eq-box` — peril `earthquake`, trigger
  `parametric_cat_in_a_box`, the catalogue's preferred magnitude inside the
  box, over a pinned quake catalogue. The block at the top of this page.
* `cede/parametric-burn-station-index` — trigger `parametric_index`, a
  station-measured daily series aggregated over a rolling window, over a
  pinned daily-series vintage. [Backtest a structure](backtest.md) walks one
  end to end. Which peril it prices depends on **which feed the trigger
  names**, because the feed is what decides whether an index level says
  anything about that peril:

  | Feed kind | Measurement it publishes | Perils it prices |
  | --- | --- | --- |
  | `rainfall_daily` | `daily_precipitation_mm` (mm) | `weather_station` |
  | `wind_daily` | `daily_maximum_wind_speed_mph` (mph) | `weather_station`, `tropical_cyclone` |

  Those variable names are copied into `trigger.index.measurement.variable`
  exactly as written, with the unit beside them, and they are neither the name
  the publisher uses nor the English in the feed's description — **[what each
  pinned feed publishes](feeds.md#the-measurement-variable-each-feed-publishes)**
  is the per-feed list, including `catalogue_preferred_magnitude` for the quake
  catalogue the block at the top of this page prices against. A variable this
  build does not read is refused by name rather than approximated.

  So a named-storm (`tropical_cyclone`) structure prices when it attaches on a
  wind index and is refused when it attaches on a rainfall one — this build
  cannot attribute rain to a named storm — and the refusal names the feed
  rather than your peril.

  A wind level is not a storm category. Every pinned wind feed is a cell
  average of hourly means from a public reanalysis, so it reads systematically
  lower than an anemometer in the eyewall and lower again than a gust; a
  Saffir-Simpson number written as an attachment is a level of *that feed* and
  nothing else. The price says so in `assumptions.limitations`, and you should
  read it before writing the ladder. **[The pinned data
  feeds](feeds.md#the-wind-series-and-what-a-named-storm-structure-can-ask-of-them)**
  tabulates the highest day each wind record holds over its forty years and how
  many of those years reach 40, 50, 60 and 74 mph, which is what decides
  whether your bands are measurable: a band no year reaches earns no expected
  loss, and a ladder written *entirely* above the record is refused rather than
  priced — [the section below](#a-structure-the-record-never-paid-on-is-refused-not-priced-at-zero)
  is that refusal. Only one of the four pinned wind series has any year
  reaching 74 mph at all, so a Saffir-Simpson attachment is the refused case on
  most of them; read the table before you pick a level, not after.

  Nor is a wind feed a storm track archive. It knows no storm names, tracks or
  landfall points, so a trigger on it pays on wind speed alone whether or not a
  system was named, and cannot exclude a wind day no cyclone caused.

Both take a `step` payout function and both use the same method — empirical
annual burning cost over forty years — with the same three loadings, and both
name themselves in `model` on the result.

An object outside both is refused by name — a job that ends `failed` carrying
`422 unpriceable_object` and a message saying which part it could not read —
because a plausible number from a model that does not fit the structure is
worse than none: nothing downstream can tell the difference. That covers an
object with no limit, no attachment or no trigger, and it covers the sharper
case: an object whose declarations disagree with each other. The two index
shapes look identical in JSON, so each model checks the measurement and the
data-source kind against what it actually computes before reading a byte — a
rainfall index can never be evaluated as a magnitude one, and the refusal says
which declaration disagreed with which.

The burn rate underneath this price is the same burn rate
[Backtest a structure](backtest.md) shows year by year, read through the same
code — not a second number that happens to look similar.

### A structure the record never paid on is refused, not priced at zero

Both models price the years counted. If **no year** of the pinned record would
have paid — the attachment sits above every level the feed ever recorded, or
an index threshold excluded every event — then the burn rate is zero, its
standard deviation is zero, all three loadings are zero, and the technical
premium is exactly 0.00. That number is not a measurement that the structure
is cheap. It is the absence of any observation in its trigger range, and the
two would read identically in every field of the response.

So it is refused. The job ends `failed` with `422 unpriceable_object`, and the
message carries the number you actually need:

```
no year of the pinned record would have paid: this structure first pays at
90 mph and the highest 1-day rolling_maximum of daily_maximum_wind_speed_mph
in era5-wind-miami@2026-08-14 over 1986-2025 is 77.1 mph, in 2005 (window
2005-10-24 to 2005-10-24). No year in the period of record reaches 90 mph, so
the empirical burn assigns those bands no expected loss at all — every level
of this ladder, not only its attachment, sits above the record. […] Attach at
or below 77.1 mph to price against the observed record, or run POST
/structures/{id}/backtest to see the same 40 years with no payout in them,
which is a description of history rather than a price.
```

Two consequences worth knowing before you write a ladder:

* **A remote layer has no price here.** A burning-cost model over forty years
  fits no distribution to the tail — that is stated in `method.description` on
  every response — so it has nothing to say about an attachment above
  everything in the record, and this build will not say zero instead. Moving
  the attachment into the observed range is what produces a price.
* **Higher bands may still be unobserved, and those still price.** Only a
  structure with *nothing* in range is refused. A ladder that attaches at 74
  mph and exhausts at 111 mph prices off the years that reach 74; the bands
  nothing reached carry `occurrences: 0`, a `null` return period, and a
  sentence in `assumptions.limitations` saying the price understates them.

The decision behind this refusal — including the alternative it rejected,
publishing the zero alongside a data-gap field, and what refusing costs you —
is recorded in the build's own decision log as ADR-0048.

---

## When it does not work

| What you see | What it means |
| --- | --- |
| `422 invalid_request` from `/signup` | A signup body reads one optional member, `label`, and refuses anything else. Send `{"label": "…"}`, or no body at all. |
| `404` from `GET /account` | The key you sent was configured into that deployment by whoever runs it rather than handed out at sign-up, so it belongs to no account. Sign up for one of your own. |
| `422 invalid_risk_object` from `POST /objects` | The document did not validate against the published schema. The response names the member and what it expected. |
| Job `failed` with `422 unpriceable_object` | The object is outside what Price v0 reads, or it carries no financial structure and no trigger. The message says which; **[the trigger object](trigger.md)** lists every message this refusal arrives with and what to change for each. |
| Job `failed` with `422 unpriceable_object` saying `prices earthquake only` about a peril the table above prices | Your object names no trigger the station-index model reads — usually none at all — so it fell through to the model that answers for whatever nothing else claimed, and that model refused on the peril. It is a fact about that model and not about this build: the message goes on to name the model that *does* price your peril and what that model reads. Write the `parametric_index` trigger over the feed for your peril, as above, and price again. |
| Job `failed` with `422 unpriceable_object` saying `no year of the pinned record would have paid` | Your attachment is above every level the pinned feed recorded in forty years, so the only price available would be 0.00 — which reads as a cheap structure rather than an untested one. The message names the highest level the record reached and when. Attach at or below it, or backtest the structure to see the same forty years with no payout in them. |
| Job `failed` with `422 unpriceable_object` saying `no pinned snapshot` | The `id` or the `version` in `trigger.data_sources[0]` is not one this build pins, and a feed version has to pre-exist to be priced against. The message lists every pinned `id@version`; **[the pinned data feeds](feeds.md)** is the same list with each feed's coverage, and `GET /primitives` is that list from the deployment itself — [read it before you write the block](#before-you-copy-the-object-the-feed-is-not-yours-to-invent) and this refusal never reaches a job. |
| `422 invalid_request` from `PATCH /objects/{id}` | The body named a block this route does not write — the message lists the six it does — or sent a `financial_structure.premium`, which is Price's output rather than an input. |
| `422 invalid_risk_object` from `PATCH /objects/{id}` | The blocks are the caller's to write, but the object they would make does not validate. The response names each violation. |
| `401 unauthenticated` on any call after step 1 | The header is exactly `Authorization: Bearer <key>`. Staging keeps accounts in memory and redeploys whenever the code moves, so a key from yesterday is a key from a previous life — sign up again, it takes one call. |

Every refusal has the same shape and validates against the published error
schema; **[the error reference](errors.md)** is the full list.

---

*Both blocks on this page are extracted verbatim and executed against a live
environment on every change to Craton. If one stopped working, the build stops
too.*
