Partially live — signed attestations are public; splitting, settlement and the on-chain registry are notRead an attestation
Cleaton

Verify a signature

Recover the attester yourself — the shipped EIP-191 scheme in twenty lines, and the EIP-712 registry check that replaces it.

Argued in full in the whitepaper at §7.2.

Verification is a pure function of the payload and the signature. No oracle call, no API call, no external dependency — that is the whole point of publishing a signature rather than a number.

What is signed today#

secp256k1 over an EIP-191 message — personal_sign — not EIP-712. The message is canonical JSON: keys sorted alphabetically, every value a string.

canonical.json
{"chainId":"4663","confidence":"0.65","expiresAt":"1787411823","horizonDays":"90","issuedAt":"1787325423","pool":"0xbeeff033f34c046626b8d0a041844c5d1a5409dd","v":"1"}

Key order is alphabetical, so serialisation never depends on insertion order. The signature covers exactly these seven fields — symbol, protocol, outcome and the rest of the API response are convenience, not claims.

Off chain#

verify.ts
import { recoverMessageAddress } from "viem";

function canonical(a) {
  const f = {
    chainId:     String(a.chainId),
    confidence:  Number(a.confidence).toFixed(2),
    expiresAt:   String(a.expiry),
    horizonDays: String(a.horizon),
    issuedAt:    String(a.issuedAt),
    pool:        a.pool.toLowerCase(),
    v:           "1",
  };
  return `{${Object.keys(f).sort().map((k) => `"${k}":"${f[k]}"`).join(",")}}`;
}

const a = await (await fetch(`https://dapp.cleaton.xyz/api/v1/pool/${pool}`)).json();

const signer = await recoverMessageAddress({
  message:   canonical(a),
  signature: a.signature,
});

// The attester Cleaton publishes, from GET /api/v1/attester.
const expected = "0xb6486340a930e21b7ee505111188e929120d9eda";

const authentic = signer.toLowerCase() === expected;
const fresh     = a.expiry > Math.floor(Date.now() / 1000);

That is the whole check. Do not compare against the attester field in the response — an attestation that carries its own idea of who signed it proves nothing. Take the expected address from GET /api/v1/attester, or pin it.

Or let us do it#

Convenience, not authority — it is the same recovery, run on our side, and worth exactly as much as trusting the host. Use it to check your own implementation against, then stop using it.

terminal
curl -s https://dapp.cleaton.xyz/api/v1/pool/$POOL \
  | curl -s -X POST https://dapp.cleaton.xyz/api/v1/verify \
      -H "Content-Type: application/json" --data-binary @-

# {"valid":true,"attester":"0xb648...9eda","expired":false,"canonical":"{...}"}

valid and expired are separate answers on purpose. An expired attestation is still authentically signed, and a consumer checking a historical claim — or backtesting the record — needs the first without the second.

On chain, when the registry ships#

Registry.sol
function verify(Attestation calldata a, bytes calldata sig)
    external view returns (bool valid, address attester)
{
    bytes32 digest = _hashTypedDataV4(_structHash(a));
    attester = ECDSA.recover(digest, sig);
    valid = registeredAttester[attester]
         && block.timestamp <= a.validUntil
         && bondOf[attester] >= minBond
         && !revoked[digest];
}

About 30k gas — a deliberate target. A deposit guard that costs more than the deposit it protects will not be adopted, so verification had to be cheap enough to sit in the hot path rather than in a periodic job.

Note what the check covers beyond the signature itself:

  • registeredAttester — the key is one the registry knows
  • validUntil — the attestation is not stale
  • bondOf >= minBond — the attester is still economically backed
  • !revoked — the attestation was not withdrawn early

Both schemes are secp256k1, so the on-chain move is a change of digest, not of primitive — and it is the same primitive the wallet sign-in uses, so the system carries one crypto scheme rather than two.

What verification does not tell you#

That the claim is right. A valid signature says Cleaton published this number and can be held to it. Whether the number was any good is the accuracy record’s job, and today that record is empty — no attestation has reached its horizon and been judged.

The three registry checks above are also absent off chain. There is no bond backing a published attestation yet, and nothing revokes one.

Cleaton Documentation — partially live: attestations over REST, no settlement or registry yet.