Wildfire, by the acre
A wildfire trigger priced off the fires that were mapped. This page uses the same model as the storm-report box, a parametric_cat_in_a_box trigger over a pinned extract, with the box written in peril.region. Here the extract is MTBS's mapped wildfires and the ladder is in burned acres. By the end you will have a wildfire object for the country around Paradise, California, its forty-one-year history, and a technical price for it, all from one block.
Read the quickstart first if you have not: it explains CEDE_BASE_URL, the Authorization: Bearer header and the job pattern every Craton verb answers with. The trigger object is the field-by-field reference for the block this page writes.
On this page
What a wildfire box is
Monitoring Trends in Burn Severity (MTBS), run by the USGS and the USDA Forest Service, maps every large US wildfire from Landsat imagery after it burns: fires reported at 1,000 acres or more in the west and 500 in the east, back to
- Craton pins extracts of its national table for five western regions,
one row per fire: the single point MTBS publishes for the fire's burn boundary, the ignition date, and the burned acres inside that boundary. A wildfire box asks: what was the largest fire whose point lies inside this box, year by year?
Three declarations route a structure to this model, and each is re-checked against the pinned bytes before any of them is read:
| Member | What a wildfire box declares | Why |
|---|---|---|
trigger.type |
parametric_cat_in_a_box |
A box and a ladder. |
trigger.data_sources[0].kind |
storm_report_set |
The member that routes to the report model. A mapped fire is the same shape of record as a storm report: a place, a time, a value. |
peril.code |
wildfire |
It must match the feed. A wildfire structure over a hail feed, or a hail structure over a wildfire feed, is refused by name. |
The index is four decisions, the same four as the storm-report box:
- The measurement is
burned_area_acresinacres, withstatistic: "max". - The window is the year, written
P1Y, and a fire belongs to the year it was ignited. - The ladder is a
stepfunction in acres, with a threshold labeledattachmentat its lowest level. - The box must sit inside the extract. A box reaching outside it is refused with the edge it crossed.
For each year, the largest fire inside the box sets the year's level, the ladder turns it into a payout ratio, and the burn rate is the mean ratio over every year, the years with no large fire included at zero.
Paradise and the Feather River country, end to end
One object: a box over Butte County and its neighbors (39.4N to 40.3N, 122.2W to 121.0W), paying half the limit when a fire of 100,000 acres or more has its point inside the box and all of it at 250,000 acres. The block signs up for an account of its own, stores the object, replays it over every year of the extract, and prices the same terms.
Run this in an empty directory.
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"
}
# Poll a job until it stops moving, leaving its last document in a file.
wait_for() {
local job_id="$1" into="$2" state=unknown
for _ in $(seq 1 300); do
curl -sS --fail-with-body -H "$auth" "$CEDE_BASE_URL/jobs/$job_id" > "$into"
state=$(field status < "$into")
case "$state" in succeeded|failed) break ;; esac
sleep 0.2
done
if [ "$state" != succeeded ]; then
printf 'the job did not succeed: %s\n' "$(cat "$into")" >&2
return 1
fi
}
# 1. An account of this block's own.
CEDE_API_KEY=$(curl -sS --fail-with-body \
-H "Content-Type: application/json" \
-d '{"label": "wildfire walkthrough"}' \
"$CEDE_BASE_URL/signup" | field api_key.secret)
export CEDE_API_KEY
auth="Authorization: Bearer $CEDE_API_KEY"
# 2. The wildfire-box object. The box is the peril region; the ladder is in
# acres, the unit the wildfire feed publishes.
cat > butte-wildfire.json <<'JSON'
{
"schema_version": "0.1.0",
"status": "draft",
"exposure": {
"kind": "index",
"source_fidelity": { "unmapped_columns": [], "guessed_units": [], "ambiguous_rows": [] }
},
"peril": {
"code": "wildfire",
"region": {
"description": "Butte County wildfire box: 39.4-40.3N, 122.2-121.0W.",
"bounding_geometry": {
"type": "Polygon",
"coordinates": [[[-122.2, 39.4], [-121.0, 39.4], [-121.0, 40.3],
[-122.2, 40.3], [-122.2, 39.4]]]
}
}
},
"financial_structure": {
"limit": { "amount": 10000000, "currency": "USD" },
"attachment": { "value": 100000, "unit": "acres", "index_ref": "butte-wildfire-box" }
},
"trigger": {
"type": "parametric_cat_in_a_box",
"index": {
"name": "butte-wildfire-box",
"version": "1.0.0",
"description": "Largest mapped wildfire inside the Butte County box, per year, by burned acres.",
"measurement": { "variable": "burned_area_acres", "unit": "acres", "statistic": "max" },
"aggregation_window": { "duration": "P1Y" },
"thresholds": [
{ "label": "attachment", "level": 100000, "unit": "acres" },
{ "label": "exhaustion", "level": 250000, "unit": "acres" }
],
"payout_function": {
"type": "step",
"points": [ { "level": 100000, "payout_ratio": 0.5 },
{ "level": 250000, "payout_ratio": 1 } ],
"maximum_payout_ratio": 1
}
},
"data_sources": [
{ "id": "mtbs-wildfire-northern-california",
"name": "MTBS mapped wildfires, Northern California extract",
"kind": "storm_report_set",
"version": "2026-09-27", "vintage": "2026-09-27" }
]
},
"period": {
"inception": "2027-01-01T00:00:00-08:00",
"expiry": "2028-01-01T00:00:00-08:00",
"timezone": "America/Los_Angeles"
}
}
JSON
object_id=$(curl -sS --fail-with-body -H "$auth" \
-H "Content-Type: application/json" \
--data-binary @butte-wildfire.json "$CEDE_BASE_URL/objects" | field id)
structure=$(curl -sS --fail-with-body -X POST -H "$auth" \
"$CEDE_BASE_URL/objects/$object_id/structures")
backtest_path=$(printf '%s' "$structure" | field links.backtest)
# 3. Replay it over every year of the extract.
backtest_job=$(curl -sS --fail-with-body -X POST -H "$auth" \
"$CEDE_BASE_URL$backtest_path" | field id)
wait_for "$backtest_job" backtest-job.json
artifact_path=$(field result.backtest.links.self < backtest-job.json)
curl -sS --fail-with-body -H "$auth" "$CEDE_BASE_URL$artifact_path" > backtest.json
python3 -c '
import json
artifact = json.load(open("backtest.json"))
statistics = artifact["statistics"]
print("paying years:", statistics["triggering_years"], "of", statistics["years"])
for row in artifact["years"]:
if row["triggered"]:
peak = row["peak_report"]
print(" ", row["year"], row["max_value"], row["index_unit"],
"fire", peak["event_id"], "->", row["payout_ratio"])
print("replayed by", artifact["model"]["name"], artifact["model"]["version"])
'
# 4. Price the same terms.
price_job=$(curl -sS --fail-with-body -X POST -H "$auth" \
"$CEDE_BASE_URL/objects/$object_id/price" | field id)
wait_for "$price_job" price-job.json
python3 -c '
import json
price = json.load(open("price-job.json"))["result"]["price"]
premium, assumptions = price["technical_price"], price["assumptions"]
selection = assumptions["event_selection"]
print("technical price:", premium["amount"], premium["currency"],
"- rate on line", premium["rate_on_line"])
print("fires inside the box:", selection["reports_in_box"],
"of", selection["reports_in_extract"], "in the extract")
print("priced by", price["model"]["name"], price["model"]["version"])
'
echo "done: the priced object is at /objects/$object_id"If the last line printed, every paying year above names the fire that set it by its MTBS event id, which you can look up on mtbs.gov.
Five of the forty-one years pay: Campbell (1990) and Camp (2018) at half the limit, and North Complex (2020), Dixie (2021) and Park (2024) at all of it. Three of the five are in the last five years of the record, and the burn is an average over all forty-one. Whether the next five years look more like the last five or like the whole record is a judgment this price does not make for you; the backtest above is where to see it.
The pinned extracts
Five feeds, one per western region, each wide enough to hold the boxes you would price inside it:
| Region | Box | Feed | Fires | Largest |
|---|---|---|---|---|
| Northern California | 37.0 to 42.0N, 124.5 to 119.0W | mtbs-wildfire-northern-california |
972 | August Complex, 2020 |
| Southern California | 32.5 to 37.0N, 121.0 to 114.1W | mtbs-wildfire-southern-california |
908 | Thomas, 2017 |
| Pacific Northwest (Oregon and Washington) | 42.0 to 49.0N, 124.8 to 116.5W | mtbs-wildfire-pacific-northwest |
1698 | Long Draw, 2012 |
| Colorado | 37.0 to 41.0N, 109.1 to 102.0W | mtbs-wildfire-colorado |
391 | Cameron Peak, 2020 |
| Southwest (Arizona and New Mexico) | 31.3 to 37.0N, 114.1 to 103.0W | mtbs-wildfire-southwest |
1786 | Wallow, 2011 |
All are version 2026-09-27, fires ignited 1984 to 2024, public domain, with MTBS credited as its data asks. The pinned data feeds lists each one's measurement. The record stops at 2024 because MTBS maps with a lag, and the later years were still incomplete when these extracts were cut.
The point lookup answers the same feeds: a GET on /hazard?peril=wildfire&lat=39.76&lon=-121.62 counts the fires around Paradise at 1,000, 10,000, 50,000, 100,000 and 250,000 acres and names the largest.
What a mapped-fire burn is not
Everything below ships in assumptions.limitations on every result.
Only large fires are in the record. MTBS maps fires reported at 1,000 acres or more in the west. A fire of a few hundred acres that took a neighborhood with it is not in these feeds, so a trigger that should respond to small, destructive fires cannot be written on them.
A fire is a point. Each fire sits at the one point MTBS publishes for its burn boundary. A fire that burned into the box from a point outside it is not counted, and a fire counted in the box may have burned mostly outside it. The larger the fire relative to the box, the larger that basis risk: Dixie burned nearly a million acres, and its point is one corner of that.
Acres are the mapped boundary. The acreage is the area inside the burn boundary, unburned islands included, so it is larger than the ground that actually burned.
A year pays once. The largest fire of the year is the one the year pays on; a second fire adds nothing, and reinstatements are not priced.