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

# Independent verification

> Reproduce our hash and recover our signature yourself, without trusting or calling MicroCrop.

This is the core of what MicroCrop sells. A determination is not "our system says so" — it
is an artifact you, your regulator, your reinsurer or a hostile counterparty can check
**offline**, in any language, with our servers switched off.

This page tells you exactly how, and gives you a runnable reproducer and a pinned
conformance vector to test it against.

## What you are checking

Three claims, in order. Each is independently falsifiable:

<Steps>
  <Step title="The body is what was signed">
    Re-serialize the canonical body under the rules below and SHA-256 it. If your digest
    equals `canonical.hash`, the body has not been altered by so much as a byte since
    signing.
  </Step>

  <Step title="MicroCrop signed it">
    Recover the secp256k1 signer from `canonical.signature` over that digest. If it equals
    `canonical.signer` — and that address is the signer MicroCrop published to you out of
    band — the determination is ours.
  </Step>

  <Step title="It predates the dispute">
    If `notary` is present, confirm the anchor transaction on the notary ledger. It commits
    the same hash at a time nobody can move.
  </Step>
</Steps>

None of these require calling MicroCrop. Steps 1 and 2 require no network at all.

## Get the artifact

```bash theme={null}
curl "https://app.microcrop.app/api/determinations/$DET_ID/evidence?download=1" \
  -H "x-api-key: $KEY" -o evidence.json
```

```json theme={null}
{
  "success": true,
  "data": {
    "packageVersion": "1.0",
    "exportedAt": "2026-09-09T07:00:00.000Z",
    "determination": { "id": "b1f0…", "kind": "CROP_DROUGHT", "methodologyVersion": "crop-drought-rdi-1.0", "status": "…", "createdAt": "…" },
    "policy": { "policyNumber": "MC-KE-2026-000123", "organizationName": "Acme Insurance Ltd", "coverageType": "DROUGHT", "sumInsured": "50000.00", "currency": "KES", "durationDays": 120 },
    "provenance": { "sourceHash": "a1b2c3", "url": "https://climateserv.example/x", "paramsJson": "{\"franchiseBp\":3000}" },
    "canonical": {
      "schemaVersion": "1.0",
      "body": { "…the signed object…": "…" },
      "hash": "5e2151314da8b329de09fb0b9ed764e5b86a4fbc795d6b01c6e917a7c1c3299d",
      "signer": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
      "signature": "0xebb1…1b"
    },
    "notary": { "provider": "stellar", "network": "…", "reference": "…", "txHash": "…", "anchoredAt": "…" },
    "verification": {
      "method": "canonicalize body (RFC-8785/JCS subset) -> SHA-256 (lowercase hex, no 0x) -> ecrecover; off-chain, chain-neutral",
      "reproducer": "node src/scripts/verify-evidence-package.js <package.json>",
      "ok": true, "hash": "5e2151…", "signer": "0x7099…",
      "declaredHash": "5e2151…", "declaredSigner": "0x7099…"
    }
  }
}
```

<Warning>
  **Ignore `data.verification`.** It is MicroCrop's own claim about MicroCrop's own artifact.
  It is there for convenience; it is worth nothing as evidence. Compute your own.
</Warning>

If `canonical` is `null`, the record is a legacy EVM-only determination and cannot be
independently re-verified this way — `verification.available` will be `false` with a
reason. Any determination issued under the current schema carries a canonical block, and
the API's `determined.evidence.verifiable` flag tells you before you fetch.

## The canonical form

The body is serialized under a **deliberately strict, portable subset of RFC 8785 (JCS)**.
The subset exists so that two independent implementations cannot disagree:

| Rule                                                                                                                                                                | Why                                                                               |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| Allowed value types: **string, boolean, array, plain object**. Nothing else.                                                                                        | One encoding per value.                                                           |
| **Numbers are forbidden** as raw values. Every numeric quantity — minor units, basis points, unix seconds — is pre-encoded by the producer as a **decimal string**. | No IEEE-754 ever enters the hash, and "is `5` a number or a string" cannot arise. |
| **`null` and `undefined` are forbidden.** An absent field is **omitted**.                                                                                           | No `null` vs missing ambiguity.                                                   |
| Object keys are emitted in **ascending code-unit order** (a plain lexicographic sort of the key strings).                                                           | Deterministic ordering.                                                           |
| Arrays keep their order — order **is** significant in arrays.                                                                                                       |                                                                                   |
| Output is **minified UTF-8**: no insignificant whitespace, strings escaped per JSON.                                                                                |                                                                                   |
| The hash is **SHA-256** over those UTF-8 bytes, rendered **lowercase hex with no `0x` prefix**.                                                                     | It is a neutral content hash, not an EVM word.                                    |

The body additionally carries **no chain domain and no USDC amount** — `domain`,
`chainId`, `verifyingContract`, and any key ending in `usdc` are structurally forbidden
anywhere in the tree. Assert that yourself: a determination that names a chain or a
settlement currency is not the artifact you were sold.

### The signature

The signer signs the **32-byte SHA-256 digest as a raw digest** — there is no EIP-191
`\x19Ethereum Signed Message` prefix and no EIP-712 domain. So recovery is exactly:

```
recovered = ecrecover(digest = sha256(canonicalBytes), signature)
```

In ethers.js that is `recoverAddress("0x" + hash, signature)`. The result is a checksummed
address; compare case-insensitively.

## Conformance vector

Pin this in your own test suite. If your implementation reproduces it byte-for-byte, it
agrees with ours.

<CodeGroup>
  ```json body theme={null}
  {
    "schemaVersion": "1.0",
    "kind": "CROP_DROUGHT",
    "methodologyVersion": "crop-drought-rdi-1.0",
    "subject": { "policyId": "pol_123", "plotId": "plot_456" },
    "result": {
      "damagePercentBp": "4500",
      "thresholdBp": "3000",
      "sumInsured": { "amountMinor": "5000000", "currency": "KES" },
      "payoutAmount": { "amountMinor": "2250000", "currency": "KES" }
    },
    "evidence": {
      "latitude_e6": "1016700", "longitude_e6": "35006200", "seasonStartEpoch": "1678838400",
      "lgpDays": "180", "rainW1_e2": "12345", "rainRefW1_e2": "20000", "rainW2_e2": "8000",
      "rainRefW2_e2": "15000", "cddW2Days": "12"
    },
    "provenance": {
      "sourceHash": "a1b2c3",
      "url": "https://climateserv.example/x",
      "paramsJson": "{\"franchiseBp\":3000}"
    },
    "assessedAt": "1690000000"
  }
  ```

  ```text canonical bytes (665 bytes, one line) theme={null}
  {"assessedAt":"1690000000","evidence":{"cddW2Days":"12","latitude_e6":"1016700","lgpDays":"180","longitude_e6":"35006200","rainRefW1_e2":"20000","rainRefW2_e2":"15000","rainW1_e2":"12345","rainW2_e2":"8000","seasonStartEpoch":"1678838400"},"kind":"CROP_DROUGHT","methodologyVersion":"crop-drought-rdi-1.0","provenance":{"paramsJson":"{\"franchiseBp\":3000}","sourceHash":"a1b2c3","url":"https://climateserv.example/x"},"result":{"damagePercentBp":"4500","payoutAmount":{"amountMinor":"2250000","currency":"KES"},"sumInsured":{"amountMinor":"5000000","currency":"KES"},"thresholdBp":"3000"},"schemaVersion":"1.0","subject":{"plotId":"plot_456","policyId":"pol_123"}}
  ```

  ```text sha-256 theme={null}
  5e2151314da8b329de09fb0b9ed764e5b86a4fbc795d6b01c6e917a7c1c3299d
  ```
</CodeGroup>

Note what the canonical bytes show: `assessedAt` first and `subject` last, because the
keys are sorted — not the order they appear in the body above. Inside `provenance`, the
embedded JSON in `paramsJson` is a **string**, escaped as one; it is not re-canonicalized.

<Note>
  Key ordering is a plain lexicographic sort over the key strings. JavaScript sorts by
  UTF-16 code unit and Python by Unicode code point; these differ only for characters
  outside the Basic Multilingual Plane. Every key in the MicroCrop schema is ASCII, so the
  two agree — but if you extend the schema, keep keys ASCII.
</Note>

## Runnable reproducer

Node 18+ and `npm i ethers`. This is the whole verifier — about 50 lines, no MicroCrop
dependency, no network:

```js verify-determination.mjs theme={null}
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
import { recoverAddress } from "ethers";

/** Canonical JSON: RFC 8785 / JCS subset. Strings, booleans, arrays, plain objects ONLY. */
function canonicalJson(value, path = "$") {
  if (typeof value === "string") return JSON.stringify(value);
  if (typeof value === "boolean") return value ? "true" : "false";
  if (Array.isArray(value))
    return "[" + value.map((v, i) => canonicalJson(v, `${path}[${i}]`)).join(",") + "]";
  if (value !== null && typeof value === "object" && value.constructor === Object) {
    return (
      "{" +
      Object.keys(value)
        .sort()
        .map((k) => JSON.stringify(k) + ":" + canonicalJson(value[k], `${path}.${k}`))
        .join(",") +
      "}"
    );
  }
  throw new TypeError(`non-canonical value at ${path}: ${value === null ? "null" : typeof value}`);
}

const FORBIDDEN = new Set([
  "domain", "chainId", "verifyingContract", "payoutAmountUsdc", "sumInsuredUsdc", "usdc",
]);

/** The body must carry no chain domain and no USDC amount, anywhere in the tree. */
function assertNeutral(value, path = "$") {
  if (Array.isArray(value)) return value.forEach((v, i) => assertNeutral(v, `${path}[${i}]`));
  if (value !== null && typeof value === "object" && value.constructor === Object) {
    for (const k of Object.keys(value)) {
      const lk = k.toLowerCase();
      if (FORBIDDEN.has(k) || lk.endsWith("usdc") || lk === "chainid")
        throw new Error(`forbidden key "${k}" at ${path}`);
      assertNeutral(value[k], `${path}.${k}`);
    }
  }
}

const pkg = JSON.parse(readFileSync(process.argv[2], "utf8"));
const { body, hash, signature, signer } = pkg.data?.canonical ?? pkg.canonical;

assertNeutral(body);

const bytes = Buffer.from(canonicalJson(body), "utf8");
const recomputed = createHash("sha256").update(bytes).digest("hex");
if (recomputed !== hash) throw new Error(`hash mismatch: ${recomputed} != ${hash}`);

const recovered = recoverAddress("0x" + recomputed, signature);
if (signer && recovered.toLowerCase() !== signer.toLowerCase())
  throw new Error(`signer mismatch: ${recovered} != ${signer}`);

console.log("canonical bytes :", bytes.length);
console.log("sha-256         :", recomputed);
console.log("recovered signer:", recovered);
console.log("PASS — this determination is exactly what MicroCrop signed.");
```

```bash theme={null}
node verify-determination.mjs evidence.json
# canonical bytes : 665
# sha-256         : 5e2151314da8b329de09fb0b9ed764e5b86a4fbc795d6b01c6e917a7c1c3299d
# recovered signer: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
# PASS — this determination is exactly what MicroCrop signed.
```

### Test it against the vector

Save this as `vector.json` and run the reproducer on it. It is signed with a **public,
well-known test key** — it is a conformance fixture, not a MicroCrop determination, and
the signer address below is deliberately not one of ours:

```json vector.json theme={null}
{
  "canonical": {
    "body": {
      "schemaVersion": "1.0",
      "kind": "CROP_DROUGHT",
      "methodologyVersion": "crop-drought-rdi-1.0",
      "subject": { "policyId": "pol_123", "plotId": "plot_456" },
      "result": {
        "damagePercentBp": "4500",
        "thresholdBp": "3000",
        "sumInsured": { "amountMinor": "5000000", "currency": "KES" },
        "payoutAmount": { "amountMinor": "2250000", "currency": "KES" }
      },
      "evidence": {
        "latitude_e6": "1016700", "longitude_e6": "35006200", "seasonStartEpoch": "1678838400",
        "lgpDays": "180", "rainW1_e2": "12345", "rainRefW1_e2": "20000", "rainW2_e2": "8000",
        "rainRefW2_e2": "15000", "cddW2Days": "12"
      },
      "provenance": { "sourceHash": "a1b2c3", "url": "https://climateserv.example/x", "paramsJson": "{\"franchiseBp\":3000}" },
      "assessedAt": "1690000000"
    },
    "hash": "5e2151314da8b329de09fb0b9ed764e5b86a4fbc795d6b01c6e917a7c1c3299d",
    "signer": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
    "signature": "0xebb11792c6a5fda80eb46e729cb9e02ef192315a4c0eb61ae3254b5b5a694e6f78385957c63504e6505ae9dfa147c5e5f3dc85e61b0921d5271bd4a4ec4026fd1b"
  }
}
```

Then prove the check bites: change `"4500"` to `"4501"` and re-run. The hash comparison
must fail. If it passes, your canonicalizer is wrong.

## The hash check in other languages

Step 1 needs only a JSON parser and SHA-256 — no crypto library, no dependencies:

```python theme={null}
import hashlib, json, sys

def canonical(v, path="$"):
    if isinstance(v, str):
        return json.dumps(v, ensure_ascii=False, separators=(",", ":"))
    if isinstance(v, bool):
        return "true" if v else "false"
    if isinstance(v, list):
        return "[" + ",".join(canonical(x, f"{path}[{i}]") for i, x in enumerate(v)) + "]"
    if isinstance(v, dict):
        return "{" + ",".join(
            json.dumps(k, ensure_ascii=False, separators=(",", ":")) + ":" + canonical(v[k], f"{path}.{k}")
            for k in sorted(v.keys())
        ) + "}"
    raise TypeError(f"non-canonical value at {path}: {type(v).__name__}")

pkg = json.load(open(sys.argv[1]))
c = pkg.get("data", pkg)["canonical"]
digest = hashlib.sha256(canonical(c["body"]).encode("utf-8")).hexdigest()
print(digest, "MATCH" if digest == c["hash"] else "MISMATCH")
```

<Warning>
  Check `isinstance(v, bool)` **before** any numeric branch in Python — `bool` is a subclass
  of `int`. And note that Python's `json.load` will happily parse a raw number into the
  body; the canonicalizer above correctly refuses it, which is the behaviour you want. A
  body containing a raw number is not a valid canonical body.
</Warning>

For step 2 in Python, recover with `eth_keys` or `coincurve` over the same 32-byte digest —
again with no message prefix.

## Verifying the amount owed

The amount is **derived**, not asserted, so you can reproduce it from the policy you
yourself sold:

```
amountOwedMinor = (sumInsuredMinor × damagePercentBp) ÷ 10000
```

* `sumInsuredMinor` is `policy.sumInsured` in minor units of `policy.currency`
  (KES and GHS have exponent 2, so KES 100,000.00 → `10000000`).
* **Multiply before dividing**, and use integer arithmetic throughout — a `BigInt`, a
  `Decimal`, an `int`, never a float.
* **Truncate toward zero.** Never round half-up; there is no tie-break rule to get wrong.

```js theme={null}
const owed = (BigInt(sumInsuredMinor) * BigInt(damagePercentBp)) / 10000n;
// 10000000n * 1650n / 10000n === 1650000n  →  KES 16,500.00
```

The signed canonical body carries the oracle's own local-currency
`result.payoutAmount.amountMinor`. Your derivation, the API's `settlement.amountOwed` and
that signed figure should all agree. If the API's derived figure disagrees with the signed
one, the response carries `settlement.amountOwedDiscrepancy` and you should stop and
reconcile before paying anyone.

## What verification does and does not establish

**It establishes** that the determination body has not been altered since signing, that it
was signed by the key MicroCrop published, that the inputs and their source are named, and
— with the notary anchor — that it existed at a stated time.

**It does not establish** that the underlying weather data is correct, that the methodology
is appropriate for the risk, or that the amount is legally owed to the farmer. Those are
questions about the source, the product design, and your contract respectively.

The provenance block is what lets you attack the first of those on your own terms: re-fetch
`provenance.url`, apply `provenance.paramsJson`, recompute the index, and compare against
the `evidence` inputs in the signed body. If they disagree, you have found something real
and we want to hear about it.

**\[PLACEHOLDER — FOR COUNSEL]** Any statement about the evidentiary weight of a
determination in a given jurisdiction, or about its admissibility in a dispute with a
policyholder or a regulator, must be drafted by legal counsel. This page describes what
the artifact cryptographically is, not what it legally means.

<CardGroup cols={2}>
  <Card title="Determinations reference" icon="file-lines" href="/guides/determinations">
    Every field in the determination response.
  </Card>

  <Card title="Service tiers" icon="scale-balanced" href="/guides/service-tiers">
    Why the artifact, not the payout, is the product.
  </Card>
</CardGroup>
