> ## Documentation Index
> Fetch the complete documentation index at: https://docs.routeur.app/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Prices are US dollars per contract that pays $1: 0.46 means 46 cents. Spread is the Yes ask plus the No ask minus $1. Liquidity grades are deep, tradeable and thin; lists hide thin markets unless include_thin=true. Relations are proven from contract terms, never inferred from wording; a lead marked informational is never executable. Every figure carries a graph_run; read /agents/reading-order first, then /concepts/relations and /concepts/liquidity.

# Rule-change watch

> List a market's contract versions and diffs, follow rule changes live, and know what to do when the settlement source changes.

## What you'll get

Every recorded version of one market's contract terms with what changed between them, the rulebooks in force above it with their settlement sources, and a live feed of rule changes across every market. Then a checklist for the case that matters most: the terms a position or a proof rests on changed underneath it. On 2026-09-17 a Polymarket election market's resolution metadata changed at 12:01 UTC, and a Kalshi tennis market's strike and close time changed at 12:03.

## Prerequisites

* An API key in `ROUTEUR_API_KEY`
* Python 3 with `requests`

<Steps>
  <Step title="Read a market's rules and their history">
    `GET /v1/markets/{venue}/{id}/rules` works for any market ever recorded, listed or not.

    ```bash theme={null}
    curl "https://api.routeur.app/v1/markets/polymarket/sweden-parliamentary-election-v-over-under-7-percent-20260812172023903/rules" \
      -H "X-API-Key: $ROUTEUR_API_KEY"
    ```

    Trimmed, observed 2026-09-17 12:47 UTC (the rule text itself is in `rules`, left out here):

    ```json theme={null}
    {
      "data": {
        "venue": "polymarket", "id": "sweden-parliamentary-election-v-over-under-7-percent-20260812172023903",
        "title": "Sweden Parliamentary Election: V Over/Under 7%?",
        "rules_hash": "3a3cd2fab54c0bf52fe24ebf9096efebb94ef000fcb93ff7d73a5e190ac2da5e",
        "rulebooks": [
          { "venue": "polymarket", "kind": "market", "id": "sweden-parliamentary-election-v-over-under-7-percent-20260812172023903",
            "content_hash": "369353b2d57ac1d826bf510ec22828a8e7cd8aaa3cf96b5fabdda04dd6ea7a00",
            "first_seen_at": "2026-09-17T12:01:36.414238Z", "last_seen_at": "2026-09-17T12:31:32.131815Z",
            "fields": { "negRisk": false, "negRiskOther": false, "resolutionSource": "", "umaResolutionStatuses": "[\"proposed\", \"proposed\"]" },
            "settlement_sources": [],
            "changed_fields": ["umaResolutionStatuses"],
            "diff": "--- market@4238e5c38e80\n+++ market@369353b2d57a\n@@ -2,5 +2,5 @@\n   \"negRisk\": false,\n   \"negRiskOther\": false,\n   \"resolutionSource\": \"\",\n-  \"umaResolutionStatuses\": \"[]\"\n+  \"umaResolutionStatuses\": \"[\\\"proposed\\\", \\\"proposed\\\"]\"\n }\n" }
        ],
        "versions": [
          { "rules_hash": "d441a7ef9a056aeff1dcf9e574e77af0de907aede7749195c312355cd856ec4b",
            "first_seen_at": "2026-09-14T17:30:29.428735Z", "last_seen_at": "2026-09-17T11:31:32.813899Z",
            "changed_fields": [], "rulebooks_changed": [] },
          { "rules_hash": "3a3cd2fab54c0bf52fe24ebf9096efebb94ef000fcb93ff7d73a5e190ac2da5e",
            "first_seen_at": "2026-09-17T12:01:36.414238Z", "last_seen_at": "2026-09-17T12:31:32.131815Z",
            "changed_fields": [],
            "summary": "The venue changed a term the recorded fields do not show, such as an expiration or settlement timestamp.",
            "rulebooks_changed": [ { "kind": "market", "id": "sweden-parliamentary-election-v-over-under-7-percent-20260812172023903",
                                     "content_hash": "369353b2d57ac1d826bf510ec22828a8e7cd8aaa3cf96b5fabdda04dd6ea7a00",
                                     "from_hash": "4238e5c38e800fd21f2df8066cb1a685926d8135bb7724b2ff5c34c7aaf14111" } ] }
        ],
        "changes": [
          { "scope": "contract", "from_hash": "d441a7ef9a05…", "to_hash": "3a3cd2fab54c…", "changed_at": "2026-09-17T12:01:36.414238Z",
            "changed_fields": [], "summary": "The venue changed a term the recorded fields do not show, such as an expiration or settlement timestamp." }
        ]
      }
    }
    ```

    The market's own text did not change; its rulebook did. `umaResolutionStatuses` went from `[]` to `["proposed", "proposed"]`: a resolution has been proposed on UMA, and the market is about to settle.
  </Step>

  <Step title="Print the versions, diffs and settlement sources">
    This script lists every contract version with its diff, the rulebooks in force with where each says the market settles, the ledger, and then reads each rulebook's own history to say whether a settlement source changed.

    ```python Python theme={null}
    import os
    import sys
    import requests

    API = "https://api.routeur.app"
    session = requests.Session()
    session.headers["X-API-Key"] = os.environ["ROUTEUR_API_KEY"]
    venue, market_id = os.environ.get("VENUE", "kalshi"), os.environ["MARKET_ID"]


    def sources(rulebook):
        return sorted(s["name"] for s in rulebook.get("settlement_sources", []))


    # 1. Every version of the contract, with what changed.
    res = session.get(f"{API}/v1/markets/{venue}/{market_id}/rules", timeout=30)
    if res.status_code == 404:
        sys.exit("No contract terms recorded for this market.")
    res.raise_for_status()
    rules = res.json()["data"]

    print(f"{rules['title']} · current version {rules['rules_hash'][:12]} · {len(rules['versions'])} version(s), {len(rules['changes'])} recorded change(s)")
    for i, v in enumerate(rules["versions"], 1):
        print(f"\nversion {i} · {v['rules_hash'][:12]} · seen {v['first_seen_at']} → {v['last_seen_at']}")
        if v.get("changed_fields"):
            print(f"  changed: {', '.join(v['changed_fields'])}")
        if v.get("summary"):
            print(f"  {v['summary']}")
        if v.get("diff"):
            print("  " + v["diff"].replace("\n", "\n  "))
        for rb in v.get("rulebooks_changed", []):
            print(f"  rulebook {rb['kind']} {rb['id']} moved to {rb['content_hash'][:12]} (was {rb.get('from_hash', '?')[:12]})")

    # 2. The rulebooks in force now, and where each one says the market settles.
    print("\nrulebooks in force:")
    for rb in rules["rulebooks"]:
        print(f"  {rb['kind']} {rb['id']} · {rb['content_hash'][:12]} · settles by {', '.join(sources(rb)) or 'no named source'}")
        if rb.get("changed_fields"):
            print(f"    changed from the previous version: {', '.join(rb['changed_fields'])}")
            if rb.get("diff"):
                print("    " + rb["diff"].replace("\n", "\n    "))
        doc = rb.get("document")
        if doc:
            print(f"    terms document {doc['url']} ({doc.get('content_hash', '?')[:12]}), fetched {doc.get('fetched_at')}")

    # 3. The ledger: every change, whatever its scope.
    if rules["changes"]:
        print("\nledger:")
        for c in rules["changes"]:
            print(f"  {c['changed_at']} · {c['scope']}{' ' + c['rulebook_id'] if c.get('rulebook_id') else ''} · {c['summary']}")

    # 4. Did the settlement source change between rulebook versions? Read the rulebook's own history.
    for rb in rules["rulebooks"]:
        detail = session.get(f"{API}/v1/rulebooks/{rb['venue']}/{rb['kind']}/{rb['id']}", timeout=30)
        if detail.status_code == 404:
            continue
        versions = detail.json()["data"]["versions"]
        for prev, cur in zip(versions, versions[1:]):
            if sources(prev) != sources(cur):
                print(f"\nSETTLEMENT SOURCE CHANGED on {rb['kind']} {rb['id']} at {cur['first_seen_at']}: "
                      f"{sources(prev) or 'none'} → {sources(cur) or 'none'}")
            elif cur.get("changed_fields"):
                print(f"\n{rb['kind']} {rb['id']} changed {cur['changed_fields']} at {cur['first_seen_at']} (sources unchanged)")
    ```

    Output for the Polymarket market above, observed 2026-09-17 12:48 UTC:

    ```text theme={null}
    Sweden Parliamentary Election: V Over/Under 7%? · current version 3a3cd2fab54c · 2 version(s), 1 recorded change(s)

    version 1 · d441a7ef9a05 · seen 2026-09-14T17:30:29.428735Z → 2026-09-17T11:31:32.813899Z

    version 2 · 3a3cd2fab54c · seen 2026-09-17T12:01:36.414238Z → 2026-09-17T12:31:32.131815Z
      The venue changed a term the recorded fields do not show, such as an expiration or settlement timestamp.
      rulebook market sweden-parliamentary-election-v-over-under-7-percent-20260812172023903 moved to 369353b2d57a (was 4238e5c38e80)

    rulebooks in force:
      market sweden-parliamentary-election-v-over-under-7-percent-20260812172023903 · 369353b2d57a · settles by no named source
        changed from the previous version: umaResolutionStatuses
        --- market@4238e5c38e80
        +++ market@369353b2d57a
        @@ -2,5 +2,5 @@
           "negRisk": false,
           "negRiskOther": false,
           "resolutionSource": "",
        -  "umaResolutionStatuses": "[]"
        +  "umaResolutionStatuses": "[\"proposed\", \"proposed\"]"
         }

    ledger:
      2026-09-17T12:01:36.414238Z · contract · The venue changed a term the recorded fields do not show, such as an expiration or settlement timestamp.

    market sweden-parliamentary-election-v-over-under-7-percent-20260812172023903 changed ['umaResolutionStatuses'] at 2026-09-17T12:01:36.414238Z (sources unchanged)
    ```

    And for a Kalshi market whose own contract changed, the same script:

    ```text theme={null}
    $ VENUE=kalshi MARKET_ID=KXITFMATCH-26SEP17LAZMIK-LAZ python3 rules.py
    George Lazarov wins · current version 57e5efec374c · 2 version(s), 1 recorded change(s)

    version 1 · 121d560b329e · seen 2026-09-16T18:17:36.704272Z → 2026-09-17T11:48:20.072932Z

    version 2 · 57e5efec374c · seen 2026-09-17T12:03:22.640221Z → 2026-09-17T12:03:22.640221Z
      changed: strike, closes_at
      Structured strike changed; close time moved from 2026-10-01T10:00:00Z to 2026-09-17T12:00:40Z.

    rulebooks in force:
      event KXITFMATCH-26SEP17LAZMIK · 7b5a933a00d6 · settles by ESPN, Flashscore, Fox Sports, ITF

    ledger:
      2026-09-17T12:03:22.640221Z · contract · Structured strike changed; close time moved from 2026-10-01T10:00:00Z to 2026-09-17T12:00:40Z.
    ```
  </Step>

  <Step title="Follow rule changes as they happen">
    The `rules` topic of `/v1/stream` sends one `rule_change` event per change, for every market or only the ones you name in `markets`.

    ```bash theme={null}
    curl -N "https://api.routeur.app/v1/stream?topics=rules" -H "X-API-Key: $ROUTEUR_API_KEY"
    ```

    An event observed 2026-09-17 12:03 UTC (replayed with a cursor from earlier in the hour):

    ```text theme={null}
    event: rule_change
    data: {"venue":"kalshi","market_id":"KXITFMATCH-26SEP17LAZMIK-LAZ","title":"George Lazarov wins","scope":"contract","from_hash":"121d560b329e513f30cbb4590a911734da12d10835b46e99092e9d8bd88e13b2","to_hash":"57e5efec374c0d67a34952b7fdf6e5b321be23422c3f10f7a0209cfdc04d5534","changed_fields":["strike","closes_at"],"summary":"Structured strike changed; close time moved from 2026-10-01T10:00:00Z to 2026-09-17T12:00:40Z.","rules_path":"/v1/markets/kalshi/KXITFMATCH-26SEP17LAZMIK-LAZ/rules","changed_at":"2026-09-17T12:03:22.640221Z","recorded_at":"2026-09-17T12:03:22.640221Z"}
    ```

    Set `TOPICS = "rules"` in the [Stream consumer](/cookbook/stream-consumer) and replace its handler with one that keys on what changed:

    ```python Python theme={null}
    WATCHED = {("kalshi", "KXMLBTOTAL-26SEP171235MILPIT-9"), ("polymarket", "mlb-mil-pit-2026-09-17-total-8pt5")}


    def handle(name, payload):
        if name != "rule_change":
            return
        key = (payload["venue"], payload["market_id"])
        fields = payload["changed_fields"]
        if payload["scope"] != "contract":
            level = "SETTLEMENT"      # a rulebook or terms document above the contract changed
        elif "rules" in fields or "strike" in fields:
            level = "TERMS"           # the payoff condition itself
        elif fields:
            level = "TIMING"          # closes_at, game_starts_at, title, labels
        else:
            level = "OTHER"           # a term the recorded fields do not show
        mark = " <-- WATCHED" if key in WATCHED else ""
        print(f"{level:<10} {payload['venue']} {payload['market_id']} · {payload['summary']} · read {payload['rules_path']}{mark}")
    ```

    Between 12:01 and 12:36 UTC on 2026-09-17, 699 `rule_change` events arrived, 695 of them on Polymarket. 574 had `changed_fields: []` (a term the recorded fields do not show, mostly resolution timestamps moving as games ended), 46 changed only `game_starts_at`, and 79 changed `strike` or `closes_at`. The last group is the one a position cares about; the handler above marks it `TERMS` or `TIMING`.
  </Step>

  <Step title="When the settlement source changes">
    A settlement source is where the venue says it will read the result. It lives in the rulebooks above the contract: a Kalshi event's `settlement_sources`, a Polymarket market's `resolutionSource` and `umaResolutionStatuses`, and the venue's contract terms document. A change there does not change the market's `rules_hash`, so it is easy to miss. When the stream sends a `rule_change` with `scope` other than `contract`, or the rulebook's `changed_fields` names a source, do this:

    1. **Read the diff.** `GET /v1/rulebooks/{venue}/{kind}/{id}` has every version with `changed_fields`, `diff`, and for Kalshi the terms `document` with its own `diff`. Decide whether the new source can disagree with the old one.
    2. **Re-check every proof the market is in.** A proof holds for a contract version, and a relation's evidence names what both contracts settle on. `GET /v1/relations/{a}/{b}` returns `reproduced: false`, or `404 relation_not_found`, when the graph can no longer relate the current versions; a relation that survives is safe only if its `evidence` still describes the settlement you read in step 1.
    3. **Treat leads on the pair as void until the next graph run confirms them.** Runs finish hourly (`GET /v1/status`), and a lead's `depth.limited_by` becomes `informational` when the venues could settle differently.
    4. **If you hold a position, compare the sources, not the hashes.** The script's last section prints `SETTLEMENT SOURCE CHANGED` with the names before and after. A change from a named agency to none, or to a different one, is the case to act on.

    A Polymarket `umaResolutionStatuses` going from `[]` to `["proposed", ...]`, as above, is not a source change: it means resolution has started on the same source. Expect the market to close and the price to go to 0 or 100¢.
  </Step>
</Steps>

## Read the result

| Field                        | Meaning                                                                                                                                                                                                                                                                                                               |
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `rules_hash`                 | The current contract version. Every proof and lead names the version it holds for.                                                                                                                                                                                                                                    |
| `versions[]`                 | Every contract version, oldest first, with `first_seen_at`, `last_seen_at`, `changed_fields` (rules, title, labels, event, strike, open, close, game start, outcome tokens), a `summary`, a unified `diff` of the rule text, and the rulebooks that changed with it.                                                  |
| `rulebooks[]`                | The rulebook versions in force for the current contract: a Kalshi `event` (settlement sources, exclusivity, strike period) or `series` (contract terms document, prohibitions), or a Polymarket `market` (resolution metadata). `fields` is the venue's data verbatim; `settlement_sources` is what was read from it. |
| `rulebooks[].document`       | The venue's contract terms document, with its text, hash, fetch time and a `diff` from the previous version.                                                                                                                                                                                                          |
| `changes[]`                  | The ledger: every change to the market's terms, whatever its `scope`: `contract`, `series`, `event`, `market` or `document`.                                                                                                                                                                                          |
| `rule_change.scope` (stream) | Which layer changed. `contract` is the market's own terms; anything else changed above it under an unchanged contract. `rules_path` is where to read it.                                                                                                                                                              |

## Pitfalls

* **`changed_fields: []` with a new hash is normal.** It means the venue changed something the recorded fields do not show, such as an expiration or settlement timestamp. The summary says so. Do not treat every new version as a rules rewrite; do read the ledger when one lands on a market you hold.
* **Kalshi series rulebooks were not recorded on 2026-09-17.** `GET /v1/rulebooks/kalshi/series/KXBTCD` (and `KXMLBTOTAL`, `KXSB`, `KXITFMATCH`) returned `404 rulebook_not_found`; only `event` rulebooks were present. The script skips a missing rulebook; the series' contract terms document is then not available through this endpoint.
* **A close time moving earlier is a settlement.** The Kalshi tennis market's `closes_at` moved from 2026-10-01 to 2026-09-17 12:00:40 because the match ended. Treat it as the market resolving, not as a rule change to argue with.
* **Hashes are 64 hex characters.** The script prints 12 for readability; compare full values in code.
* **The stream replays one hour.** A watcher that was down longer must reread `/rules` for the markets it cares about after the `reset` event.

## Related

* [Proof trail](/cookbook/proof-trail): the proof names the contract versions it holds for.
* [Stream consumer](/cookbook/stream-consumer): a resumable consumer for the `rules` topic.
* [Venues](/concepts/venues#contract-versions): how each venue publishes its terms.
* `GET /v1/rulebooks/{venue}/{kind}/{id}`: every version of a rulebook and the markets under it.
