On-Chain Attestations
How a Hatch Score gets published to BNB Chain, what the attestation guarantees, and how to verify one independently.
See also: ADR 0006 for the canonicalization + ABI shape rationale.
What is a Hatch attestation?
An entry in the HatchAttest contract that records:
subject— the token contract address being attested about.schemaId—hatch.score.v1(bytes32).data— ABI-encoded score payload.attestationId— contract-assigned unique id.attester— our hot wallet address (rotatable, multisig post-mainnet).
The digest (keccak256 of canonicalized data) is the idempotency key —
same inputs always produce the same digest, so a re-publish is a no-op.
Payload schema — hatch.score.v1
(uint16 aggregate, // 0..100
uint8 band, // 0 = red, 1 = amber, 2 = green
bytes32 digest, // keccak256(canonical score)
string promptVersion,
uint16[6] signals, // [meme, creator, image, name, social, risk]
bool hasStubs) // always false — publisher refuses true
Signal order is fixed: meme, creator, image, name, social, risk.
Changing order or adding a signal bumps the schema to v2.
When an attestation gets published
creator hits "Publish on-chain" on /score/:id
│
▼
POST /v1/score/:id/publish ──▶ apps/api
│
├─ refuse hasStubs: true ◀─── trust rule
├─ refuse not_configured ◀─── fail-closed env
├─ canonicalize(score) → digest
├─ findExisting(digest)
│ └─ if exists, return existing attestation (idempotent)
├─ ABI-encode payload
├─ viem writeContract(HatchAttest.attest, subject, schemaId, data)
└─ waitForTransactionReceipt
│
▼
extract attestationId from Attested event
persist { scoreRequestId, attestationId, digest, txHash, subject, schemaId }
emit 'attestation.published' webhook
│
▼
return 200 { attestationId, txHash, digest }
Trust rules
Non-negotiable rules baked into the publisher:
- Refuse preliminary rows.
hasStubs: true→ 409has_stubs. Regardless of env configuration. Regardless of who calls it. - Fail-closed on env. Missing
BSC_RPC_URL/ATTESTER_PRIVATE_KEY/HATCH_ATTEST_ADDRESS→ 503not_configured. No silent no-op writes. - Idempotent by digest. Same canonical score = same digest = same attestation. A re-publish is a read, not a new tx.
- Validate subject.
subjectmust be a valid 0x-address; publisher refuses malformed addresses before touching RPC. - Require tx receipt + Attested event. If the tx lands but the event
doesn't fire (unexpected contract state), roll back and return
upstream.
See ADR 0006 for alternatives considered and why viem+keccak beat EAS or raw JSON blobs.
Verifying an attestation
Via block explorer (BscScan)
- Open the tx hash you got back from
/score/:id/publishin BscScan. - Decode the input data — first arg is
subject, second isschemaId, third is the ABI-encoded payload. - Paste the payload bytes into a Solidity decoder (or use abi.hashex.org) with the schema above.
- Compute
keccak256of the canonicalized score (you need the original row — fetch from/api/v1/score/<token>). Must equal the digest in the payload.
Via SDK (planned)
import { HatchClient } from '@hatch/sdk';
const hatch = new HatchClient();
const record = await hatch.getAttestation(tokenAddress);
const valid = await hatch.verifyAttestation(record);
Ships with a future SDK minor; call it planned for now.
Manually in code
import { keccak256, toBytes } from 'viem';
import canonicalize from 'canonicalize';
const canonical = canonicalize(scoreResult); // sorted keys, no transient fields
const digest = keccak256(toBytes(canonical));
// digest must equal attestation.digest
What an attestation guarantees
- ✅ Every signal was live. No stubs.
- ✅ The prompt version is recorded (
meme@1.0.0, etc.). Replayable. - ✅ The digest is deterministic — same inputs produce the same hash.
- ✅ Publishing is idempotent — republishing doesn't duplicate.
What it does NOT guarantee
- ❌ That the token will graduate. Scoring is forecasting, not truth.
- ❌ That the creator will actually launch (or launch on time).
- ❌ That subsequent tokens from this creator will perform similarly.
An attestation is a timestamped, verifiable claim about the inputs at the moment of scoring. Nothing more.
Attestation lifecycle
| Event | What triggers it |
|---|---|
| Created | POST /v1/score/:id/publish succeeds |
| Re-returned | Same digest requested again (idempotent) |
| (Future) Revoked | Admin action if legal disclosure demands |
| (Future) Superseded | Same subject, newer hatch.score.v2 schema |
Today: attestations are immutable. Revocation + supersession land when we have enough production data to justify them.
Pricing
- Publishing:
~0.00005 BNBper attestation at current BNB gas prices. - Anyone can read attestations for free via BSC RPC.
- Hatch pays gas; creators don't.
Env setup (operators)
BSC_RPC_URL=https://bsc-dataseed.binance.org/
BSC_CHAIN_ID=56 # 97 for testnet
HATCH_ATTEST_ADDRESS=0x... # deployed contract
ATTESTER_PRIVATE_KEY=0x... # 32-byte hex, gitignored
Rotation policy: rotate the attester key quarterly; revoke on-chain via
HatchRegistry.revokeAttester() if the key is ever exposed.
Mainnet deployment gated on Sprint C.7 (audit + legal).