On-chain Verification
Hoodly anchors proofs without any smart contract. This page explains the exact format so you can verify every receipt yourself — no Hoodly account, no trust in our servers.

Why no smart contract?
A proof anchor needs three properties: immutability, a trusted timestamp, and public readability. A plain transaction on Robinhood Chain provides all three. Skipping the contract removes deployed code, upgrade keys and audit surface — there is simply nothing on-chain that could be exploited or misconfigured.
The anchor transaction
When a robot completes a task, Hoodly sends a 0-ETH transaction from the anchor wallet to itself. The transaction's input (calldata) field carries a versioned UTF-8 payload:
HOODLY:v1:<robot_id>:<task_id>:<proof_hash> example: HOODLY:v1:c3d2e1f0-7a6b-4c5d-8e9f-0a1b2c3d4e5f:8f14e45f-ea3c-4c1a-9b6e-2f7c0a1d3e5b:0x9c22ff5f21f0b81b113e63f7db6da94fedef11b2119b4088b89664fb9a3cb658
Open any anchor transaction in the Blockscout explorer, switch the input data view to UTF-8, and you will see this payload in plain text.
How the hash is computed
The proof hash is keccak256 over a canonical JSON encoding of the submitted proof object: keys sorted lexicographically at every nesting level, no whitespace, undefined values dropped. This makes the hash deterministic regardless of key order in the original request.
// submitted proof
{ "b": 2, "a": { "y": true, "x": null } }
// canonical form (this exact string is hashed)
{"a":{"x":null,"y":true},"b":2}Verify a proof yourself
Given the original proof JSON and the anchor transaction hash, you can verify independently with a few lines of JavaScript:
import { createPublicClient, http, keccak256, toBytes, hexToString } from "viem";
const client = createPublicClient({
transport: http("https://rpc.mainnet.chain.robinhood.com"),
});
function canonicalize(value) {
if (value === null || typeof value !== "object") return JSON.stringify(value);
if (Array.isArray(value)) return `[${value.map(canonicalize).join(",")}]`;
const entries = Object.entries(value)
.filter(([, v]) => v !== undefined)
.sort(([a], [b]) => (a < b ? -1 : 1))
.map(([k, v]) => `${JSON.stringify(k)}:${canonicalize(v)}`);
return `{${entries.join(",")}}`;
}
const proof = { dock: 7, payload_kg: 12.5, duration_s: 340 }; // the original proof
const txHash = "0x4a8b17e2..."; // from the verification receipt
const tx = await client.getTransaction({ hash: txHash });
const payload = hexToString(tx.input); // HOODLY:v1:<robot>:<task>:<hash>
const anchoredHash = payload.split(":")[4];
const localHash = keccak256(toBytes(canonicalize(proof)));
console.log(localHash === anchoredHash ? "VERIFIED" : "MISMATCH");Or skip the code entirely and use the public endpoint: GET /api/v1/verify/:proofHash — it performs the same chain read on every call.
The verify endpoint response
The endpoint never asks you to take verified on faith. Every individual check is reported separately, the evidence is returned in its canonical form so you can re-hash it, and the raw calldata read back from the block is included verbatim.
{
"verified": true,
"proof_hash": "0x9c22ff5f…",
"task": {
"id": "8f14e45f-ea3c-4c1a-9b6e-2f7c0a1d3e5b",
"title": "Dock inspection, bay 7",
"robot": "atlas-01",
"robot_id": "c3d2e1f0-7a6b-4c5d-8e9f-0a1b2c3d4e5f",
"status": "verified",
"claimed_at": "2026-02-11T09:14:02.113Z",
"completed_at": "2026-02-11T09:16:44.882Z"
},
"labels_published": true,
"share_url": "/p/0x9c22ff5f…",
"proof": { "dock": 7, "payload_kg": 12.5, "duration_s": 340 },
"canonical_proof": "{\"dock\":7,\"duration_s\":340,\"payload_kg\":12.5}",
"anchor": {
"tx_hash": "0x4a8b17e2…",
"block_number": "1284471",
"anchored_at": "2026-02-11T09:16:47.000Z",
"explorer_url": "https://robinhoodchain.blockscout.com/tx/0x4a8b17e2…",
"on_chain_payload_found": true,
"payload": "HOODLY:v1:c3d2e1f0-…:8f14e45f-…:0x9c22ff5f…",
"from": "0x5f2c…",
"expected_from": "0x5f2c…"
},
"checks": {
"proof_rehashes_to_hash": true,
"calldata_proof_hash_matches": true,
"calldata_task_id_matches": true,
"calldata_robot_id_matches": true,
"sender_is_anchor_wallet": true
}
}verified is only true when all four hard checks pass. proof_rehashes_to_hash is the one that catches tampering after the fact: it re-hashes the stored evidence and compares it to the hash written into the block, so an edited record can never report as verified. sender_is_anchor_wallet may be null when the verifier runs without a configured anchor key — that alone does not invalidate a proof, but a false does.
A hash that was never anchored returns 404 with verified: false. A malformed hash returns 400.
title and robot are null unless the owning account switched on publishing, in which case labels_published is false. Withholding a label costs nothing in verifiability — the hash, the evidence and every on-chain check are unaffected. It only means a customer's internal task naming is not published to whoever happens to hold the hash.
Sharing a proof
Every proof has a public page at /p/:proofHash. It runs the same checks as the endpoint, renders the receipt, and carries a per-proof preview image so the link expands into a card on X, Slack or Discord instead of a bare URL.
https://hoodly.fun/p/0x9c22ff5f21f0b81b113e63f7db6da94fedef11b2119b4088b89664fb9a3cb658
Send that to an auditor, a customer or an insurer. They need no account, no wallet and no token — and every anchor Hoodly has ever written is browsable at the proof explorer.
What this proves — and what it doesn't
An anchor proves that this exact proof payload existed at this block time and was never altered afterwards, and that it was claimed by a specific registered robot before completion. It does not physically observe the robot — the quality of the evidence inside the proof (sensor data, signatures from the robot itself, third-party attestations) is up to your integration. Hoodly makes the evidence immutable; you decide how strong the evidence is.
