Attestations

A verdict published on Base, so the record survives outside Firsthand. There is no separate attestation endpoint: an attestation is a fact about a counterparty, so it comes back on the dossier.

GET /v1/dossier/{address}
"attestation": {
  "tx_hash": "0x...",
  "explorer_url": "https://basescan.org/tx/0x...",
  "contract": "0x...",
  "chain_id": 8453
}

attestation is null until a verdict has actually been published. Nothing is shown for a transaction that was never sent.

What goes on chain

attest(
  address counterparty,
  bytes32 standing,       keccak of grounded, thin, suspect or dormant
  uint16  confidenceBps,  0 to 10000, so nothing is lost to floats
  uint32  basisCount,
  bytes32 basisHash,      keccak over the sorted observation ids
  uint64  evaluatedAt
)

The basis hash is taken over sorted observation ids, so a reader holding the dossier can recompute it and prove the published verdict is the one Firsthand held. Sorting matters: the hash must not depend on the order rows came back from the database.

What will not be published

The contract reverts a grounded verdict with a zero basis count, and the encoder refuses the same thing one layer earlier. Publishing an unevidenced claim about a named third party to a public chain cannot be taken back, so the rule is enforced twice.

Publishing one

python scripts/publish_attestation.py --dry-run --agent 0x01f9...84d3
python scripts/publish_attestation.py --deploy
python scripts/publish_attestation.py --agent 0x01f9...84d3 --contract 0x...

The dry run prints exactly what would be sent without sending it.

Why not the reputation registry

ERC-8004's own reputation registry has an unverified implementation, and the selector observed on the wire does not match any signature we could reconstruct. Calling an unverified contract with a guessed signature to publish a judgment about a real third party is not a risk worth taking, so Firsthand publishes into a contract small enough to read in a minute instead.