Claim on-chain verification

How reinsurers and partner platforms verify claim digests anchored on-chain after Gooval approval.

Live getClaim

Call the public on-chain view via JSON-RPC eth_call. No Gooval account or private key required. Defaults to Base Sepolia testnet; switch to Custom for mainnet Proxy + RPC from your integration pack.

Overview

This guide is for reinsurers and partner insurance platforms. It explains how to verify claim digests that Gooval writes to the blockchain after platform approval.

After approval, Gooval anchors a claim digest to the on-chain contract. The read-only method getClaim is a public view: any node can read it via JSON-RPC eth_callno Gooval account and no write permission required.

Verification: hash the claim snapshot from the API with the shared algorithm, then compare it to on-chain dataHash. All partners use the same algorithm.

This is not asymmetric signature verification and there is no separate signing private key. Tamper-evidence comes from Keccak-256 over the canonicalized UTF-8 byte string, with the result stored on-chain.

Network & contract

ItemTestnet (current)
NetworkBase Sepolia
Chain ID84532
Contract (Proxy)0x9CeB85F44653974501cB694124b2825fbe194B1b
Public RPC examplehttps://sepolia.base.org

Always call getClaim on the Proxy address above. Mainnet addresses are provided in formal integration materials.

Contract interface (summary)

function getClaim(string calldata claimNo) external view returns (
    bytes32 dataHash,
    string claimNo,
    string currency,
    uint256 amount
);
  • amount: minor units of the original currency (fiat cents). Example: "12.34" USD → 1234
  • Not yet on-chain: claimNo is empty and dataHash is bytes32(0) — do not treat as anchored.

Verification steps

  1. Obtain the platform claimNo and the on-chain snapshot via the reinsurer query API (see “On-chain snapshot fields”).
  2. Compute local hash H from the snapshot using the hash algorithm below.
  3. Call on-chain getClaim(claimNo) and compare: dataHash == H; claimNo / currency match the snapshot; amount equals the snapshot amount converted to integer cents (half-up).

Hash algorithm

Identifiers in the API response:

FieldValueNotes
hashAlgorithmkeccak256Ethereum Keccak-256 (not NIST SHA3-256)
canonicalizationsorted_json_strip_url_querySee rules below

H = keccak256(UTF8(C))

Rules to build the canonical string C:

  1. If the field name is documentUrl / document_url and the value is a string: strip URL query and fragment; keep only scheme://host/path.
  2. Remove all downloadUrl / download_url (presigned URLs change and must not participate in the hash).
  3. Apply the rules recursively to objects and arrays.
  4. Serialize as compact JSON: object keys sorted lexicographically; separators , and : (no spaces); non-ASCII kept as UTF-8 (equivalent to Python json.dumps(..., sort_keys=True, separators=(",", ":"), ensure_ascii=False)).

Output format: 0x + 64 lowercase hex characters (32 bytes).

On-chain snapshot fields

Input to on-chain dataHash is the following 8 fields (digest at platform approval). You must use this snapshot to verify the on-chain hash — do not substitute other payload shapes.

FieldTypeDescription
claimNostringPlatform claim number, e.g. C2026082410000001
claimIdstringInternal platform claim ID
amountstringDecimal amount string, e.g. "12.34" (not cents)
currencystringCurrency uppercase, e.g. USD
payoutTargetstringPayout target, e.g. merchant / customer
statusstringStatus at approval time
storeNumberstringStore number
spNumberstringShipping-protection number; empty string "" if none

The same on-chain record also stores:

On-chain fieldMeaning
currencySame as snapshot
amountInteger cents: round half-up of amount × 100. Example: "12.34"1234; "10.005"1001

Pass the plaintext claimNo to getClaim.

Relation to query API claimData

The reinsurer query API may return claimData as:

ShapeAPI dataHashEquals on-chain dataHash?
Platform 8-field snapshotOn-chain hashYes — hash claimData with the algorithm above
Full insurer filing (attachments, etc.)Hash of that payload aloneUsually no — only proves that API JSON was not rewritten

For on-chain matching, always:

  • Compute H from the 8-field on-chain snapshot, or
  • Read getClaim(claimNo).dataHash directly

If claimData includes attachments: use downloadUrl to download; when hashing, remove downloadUrl and strip query from documentUrl.

Numeric example

On-chain snapshot:

{
  "claimNo": "C2026082410000001",
  "claimId": "c1",
  "amount": "12.34",
  "currency": "USD",
  "payoutTarget": "merchant",
  "status": "platform_merchant_approved",
  "storeNumber": "100001",
  "spNumber": ""
}

Unique canonical C (sorted keys, no spaces):

{"amount":"12.34","claimId":"c1","claimNo":"C2026082410000001","currency":"USD","payoutTarget":"merchant","spNumber":"","status":"platform_merchant_approved","storeNumber":"100001"}

Keccak-256 of that UTF-8 byte string:

H = 0x1b36d3febe4f24c59d5fbe3205b01a1b3223ba647ad9d302e449ee5b7ec69840

Matching on-chain fields:

On-chain fieldMeaning
claimNoC2026082410000001
currencyUSD
amount1234
dataHashSame H as above

documentUrl normalization example (illustrates URL rules — not the on-chain object):

Input fragment:

{
  "documentList": [
    {
      "documentUrl": "https://bucket.example.com/evidence/x.jpg?X-Amz-Signature=abc",
      "downloadUrl": "https://cdn.example.com/x.jpg?token=zz"
    }
  ]
}

Form that participates in the hash:

{"documentList":[{"documentUrl":"https://bucket.example.com/evidence/x.jpg"}]}

On-chain read

JSON-RPC

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "eth_call",
  "params": [
    {
      "to": "0x9CeB85F44653974501cB694124b2825fbe194B1b",
      "data": "<getClaim calldata>"
    },
    "latest"
  ]
}

data: function selector = first 4 bytes of keccak256("getClaim(string)"), then ABI-encoded string claimNo. Decode the return as (bytes32, string, string, uint256).

Python (build calldata)

from eth_abi import encode
from eth_utils import keccak

selector = keccak(text="getClaim(string)")[:4]
calldata = "0x" + (selector + encode(["string"], ["C2026082410000001"])).hex()

ethers v6

import { Contract, JsonRpcProvider } from "ethers";

const abi = [
  "function getClaim(string claimNo) view returns (tuple(bytes32 dataHash, string claimNo, string currency, uint256 amount))",
];

const provider = new JsonRpcProvider("https://sepolia.base.org");
const contract = new Contract(
  "0x9CeB85F44653974501cB694124b2825fbe194B1b",
  abi,
  provider
);

const row = await contract.getClaim("C2026082410000001");
console.log({
  dataHash: row.dataHash,
  claimNo: row.claimNo,
  currency: row.currency,
  amount: row.amount.toString(),
});

Code sample: compute dataHash

Python

Dependency: pycryptodome (or an equivalent Keccak-256). Do not use hashlib.sha3_256.

import json
from urllib.parse import urlsplit, urlunsplit
from Crypto.Hash import keccak


def strip_url_query(url: str) -> str:
    raw = (url or "").strip()
    if not raw:
        return ""
    parts = urlsplit(raw)
    return urlunsplit((parts.scheme, parts.netloc, parts.path, "", ""))


def strip_document_urls(value):
    if isinstance(value, dict):
        out = {}
        for key, item in value.items():
            if key in {"downloadUrl", "download_url"}:
                continue
            if key in {"documentUrl", "document_url"} and isinstance(item, str):
                out[key] = strip_url_query(item)
            else:
                out[key] = strip_document_urls(item)
        return out
    if isinstance(value, list):
        return [strip_document_urls(item) for item in value]
    return value


def claim_data_hash(payload: dict) -> str:
    cleaned = strip_document_urls(payload)
    canonical = json.dumps(
        cleaned, sort_keys=True, separators=(",", ":"), ensure_ascii=False
    )
    digest = keccak.new(digest_bits=256)
    digest.update(canonical.encode("utf-8"))
    return "0x" + digest.hexdigest()


snapshot = {
    "claimNo": "C2026082410000001",
    "claimId": "c1",
    "amount": "12.34",
    "currency": "USD",
    "payoutTarget": "merchant",
    "status": "platform_merchant_approved",
    "storeNumber": "100001",
    "spNumber": "",
}
assert claim_data_hash(snapshot) == (
    "0x1b36d3febe4f24c59d5fbe3205b01a1b3223ba647ad9d302e449ee5b7ec69840"
)

TypeScript (viem)

import { keccak256, stringToHex } from "viem";

function stripUrlQuery(url: string): string {
  try {
    const u = new URL(url);
    return `${u.protocol}//${u.host}${u.pathname}`;
  } catch {
    return url.split("?")[0].split("#")[0];
  }
}

function stripDocumentUrls(value: unknown): unknown {
  if (Array.isArray(value)) return value.map(stripDocumentUrls);
  if (value && typeof value === "object") {
    const out: Record<string, unknown> = {};
    for (const [key, item] of Object.entries(value as Record<string, unknown>)) {
      if (key === "downloadUrl" || key === "download_url") continue;
      if ((key === "documentUrl" || key === "document_url") && typeof item === "string") {
        out[key] = stripUrlQuery(item);
      } else {
        out[key] = stripDocumentUrls(item);
      }
    }
    return out;
  }
  return value;
}

function sortKeys(value: unknown): unknown {
  if (Array.isArray(value)) return value.map(sortKeys);
  if (value && typeof value === "object") {
    const out: Record<string, unknown> = {};
    for (const key of Object.keys(value as object).sort()) {
      out[key] = sortKeys((value as Record<string, unknown>)[key]);
    }
    return out;
  }
  return value;
}

function claimDataHash(payload: object): `0x${string}` {
  const canonical = JSON.stringify(sortKeys(stripDocumentUrls(payload)));
  return keccak256(stringToHex(canonical));
}

When converting amount to cents, use half-up decimal rounding consistent with “round to cents” — avoid floating-point error.

Reinsurer query API (fetch data, not chain read)

On-chain getClaim is publicly readable; business fields and attachment downloads require the reinsurer query API (partner key):

GET /reinsurer/v1/claims/{partnerClaimNo}
x-chain-query-key: <query key issued by Gooval>
x-chain-timestamp: <optional Unix seconds; must be within ±300s of server time>
  • {partnerClaimNo}: platform claimNo (starts with C…) or internal claim ID
  • Key must match the reinsurer for that claim; otherwise 403

Success response includes: claimNo, dataHash, hashAlgorithm, canonicalization, claimData, and chain.contractAddress / chain.txHash / chain.chainId.

Recommended flow:

  1. Call the query API for claimNo, the on-chain snapshot (or reconstructable 8 fields), and chain.contractAddress
  2. Compute H with this document’s algorithm
  3. eth_call getClaim(claimNo) against the Proxy; compare dataHash and cent amount
  4. If claimData is a full insurer filing: use the same algorithm to verify “API dataHash ↔ payload after stripping downloadUrl”; do not assume it equals on-chain dataHash

Field-level API details follow the integration docs.

Verification checklist

  • Use Ethereum Keccak-256, not SHA3-256 / SHA-256 / MD5
  • JSON: sorted keys, no spaces, UTF-8
  • Remove downloadUrl; strip query / fragment from documentUrl
  • For chain matching use the 8-field on-chain snapshot; on-chain amount is in cents
  • Call getClaim on the Proxy address (testnet above)
  • If getClaim returns empty claimNo or zero hash, treat as not yet anchored