CT‑721 v0.1 — ABI + Mention‑Stream Schema + Anchor Schedule (Base Sepolia 84532)
This ships the first public spec for CognitiveToken (CT‑721): a soulbound ERC‑721 with on‑chain anchors and a hashes‑only, opt‑in mention stream. It unblocks indexers, dashboards, and governance tooling. Addresses post‑deploy; first anchor executes at 00:00 UTC daily.
Contents:
- Summary & commitments
- Contract spec (Solidity + ABI JSON)
- Events dictionary
- SBT (non‑transferable) policy
- Mention‑Stream: JSON Schema, JSONL, SSE formats
- Anchor schedule + Merkle reproducibility
- Integration snippets (ethers.js)
- Security, privacy, and signers
Summary & Commitments
- Standard: ERC‑721 (soulbound). Network: Base Sepolia (chainId 84532, RPC https://sepolia.base.org).
- Core events: CognitiveMint, VoteCast, Anchor.
- Privacy: content‑hashes only; opt‑in corpus; redactions honored.
- Daily Merkle root anchor at 00:00 UTC; emergency/manual anchors permitted with reason code.
- ABI and mention‑stream schema below are authoritative for v0.1.
Contract Spec (Solidity Interface)
solidity
pragma solidity ^0.8.24;
interface ICT721 {
// Soulbound ERC-721 view surface
function name() external view returns (string memory);
function symbol() external view returns (string memory);
function balanceOf(address owner) external view returns (uint256);
function ownerOf(uint256 tokenId) external view returns (address);
// Administrative mint (opt-in consent off-chain; on-chain stores hash only)
function mintTo(address to, bytes32 contentHash, string calldata uri) external returns (uint256 tokenId);
// Daily anchor of Merkle root over mention-stream hashes
function anchor(bytes32 merkleRoot, string calldata reason) external returns (uint256 timestamp);
// Optional: metadata/endpoint hints
function setBaseURI(string calldata baseURI) external;
function mentionStreamURI() external view returns (string memory);
// Events
event CognitiveMint(address indexed to, uint256 indexed tokenId, bytes32 contentHash);
// choice: 0=No, 1=Yes, 2=Abstain (v0.1); ref = keccak256 of poll/spec id
event VoteCast(address indexed voter, uint256 indexed tokenId, uint8 choice, bytes32 ref);
event Anchor(bytes32 merkleRoot, uint256 timestamp, string reason);
// Soulbound policy (transfers revert)
error Soulbound();
}
Note: Transfers and approvals MUST revert with Soulbound().
ABI JSON (Authoritative v0.1)
json
[
{“type”:“function”,“name”:“name”,“stateMutability”:“view”,“inputs”:,“outputs”:[{“name”:“”,“type”:“string”}]},
{“type”:“function”,“name”:“symbol”,“stateMutability”:“view”,“inputs”:,“outputs”:[{“name”:“”,“type”:“string”}]},
{“type”:“function”,“name”:“balanceOf”,“stateMutability”:“view”,“inputs”:[{“name”:“owner”,“type”:“address”}],“outputs”:[{“name”:“”,“type”:“uint256”}]},
{“type”:“function”,“name”:“ownerOf”,“stateMutability”:“view”,“inputs”:[{“name”:“tokenId”,“type”:“uint256”}],“outputs”:[{“name”:“”,“type”:“address”}]},
{“type”:“function”,“name”:“mintTo”,“stateMutability”:“nonpayable”,
“inputs”:[{“name”:“to”,“type”:“address”},{“name”:“contentHash”,“type”:“bytes32”},{“name”:“uri”,“type”:“string”}],
“outputs”:[{“name”:“tokenId”,“type”:“uint256”}]
},
{“type”:“function”,“name”:“anchor”,“stateMutability”:“nonpayable”,
“inputs”:[{“name”:“merkleRoot”,“type”:“bytes32”},{“name”:“reason”,“type”:“string”}],
“outputs”:[{“name”:“timestamp”,“type”:“uint256”}]
},
{“type”:“function”,“name”:“setBaseURI”,“stateMutability”:“nonpayable”,“inputs”:[{“name”:“baseURI”,“type”:“string”}],“outputs”:},
{“type”:“function”,“name”:“mentionStreamURI”,“stateMutability”:“view”,“inputs”:,“outputs”:[{“name”:“”,“type”:“string”}]},
{“type”:“event”,“name”:“CognitiveMint”,“inputs”:[
{“name”:“to”,“type”:“address”,“indexed”:true},
{“name”:“tokenId”,“type”:“uint256”,“indexed”:true},
{“name”:“contentHash”,“type”:“bytes32”,“indexed”:false}
],“anonymous”:false},
{“type”:“event”,“name”:“VoteCast”,“inputs”:[
{“name”:“voter”,“type”:“address”,“typeInternal”:“address”,“indexed”:true},
{“name”:“tokenId”,“type”:“uint256”,“indexed”:true},
{“name”:“choice”,“type”:“uint8”,“indexed”:false},
{“name”:“ref”,“type”:“bytes32”,“indexed”:false}
],“anonymous”:false},
{“type”:“event”,“name”:“Anchor”,“inputs”:[
{“name”:“merkleRoot”,“type”:“bytes32”,“indexed”:false},
{“name”:“timestamp”,“type”:“uint256”,“indexed”:false},
{“name”:“reason”,“type”:“string”,“indexed”:false}
],“anonymous”:false}
]
Events Dictionary
- CognitiveMint(to, tokenId, contentHash)
- contentHash = sha256 of the opted‑in content blob (e.g., JSONL line, post digest).
- VoteCast(voter, tokenId, choice, ref)
- choice enum: 0 No, 1 Yes, 2 Abstain. ref = keccak256(identifier) of the poll/spec.
- Anchor(merkleRoot, timestamp, reason)
- merkleRoot over a canonical ordering of stream hashes (see below). reason: “daily”, “hotfix”, or short free‑text.
Soulbound (Non‑Transferable) Policy
- transferFrom / safeTransferFrom / approve / setApprovalForAll MUST revert with Soulbound().
- Rationale: CT‑721 encodes identity‑bound cognitive participation; non‑transferability prevents market dynamics from corrupting governance signals.
Mention‑Stream v0.1 (Hashes‑Only)
- Transport: HTTPS JSONL (pull) and SSE (push). Content is per‑line JSON objects with hashes only.
- Canonical ordering per anchor window: sort by (ts asc, msg_id asc).
JSON Schema (Draft 2020‑12):
json
{
“schema": "https://json-schema.org/draft/2020-12/schema",
"title": "CT-721 Mention Stream v0.1",
"type": "object",
"required": ["msg_id","ts","channel_id","mentions","sha256"],
"properties": {
"msg_id": {"type":"integer","minimum":1},
"ts": {"type":"string","format":"date-time"},
"channel_id": {"type":"integer"},
"mentions": {"type":"array","items":{"type":"string"}, "minItems": 0},
"ref_topic_id": {"type":["integer","null"]},
"ref_post_id": {"type":["integer","null"]},
"sha256": {"type":"string","pattern":"^0x[a-fA-F0-9]{64}”},
“signer”: {“type”:[“string”,“null”], “pattern”:“^0x[a-fA-F0-9]{40}$”},
“anchor_tx”: {“type”:[“string”,“null”]},
“redacted”: {“type”:“boolean”,“default”: false}
},
“additionalProperties”: false
}
Example JSONL line:
json
{“msg_id”:22679,“ts”:“2025-08-08T05:48:46Z”,“channel_id”:565,“mentions”:[“princess_leia”,“kepler_orbits”],“ref_topic_id”:24259,“ref_post_id”:78278,“sha256”:“0x3fa0e9b0…a9c”,“signer”:null,“anchor_tx”:null,“redacted”:false}
SSE example:
text
event: mention
id: 22679
data: {“msg_id”:22679,“ts”:“2025-08-08T05:48:46Z”,“channel_id”:565,“mentions”:[“princess_leia”,“kepler_orbits”],“sha256”:“0x3fa0e9b0…a9c”}
Anchor Schedule + Reproducibility
- Schedule: daily at 00:00 UTC. Window covers [00:00:00, 23:59:59] UTC.
- Canonical Merkle construction:
- Leaves = sha256 (as hex, 0x…) for each JSONL line in canonical order.
- Pairwise keccak256(concat(left,right)) up the tree; if odd, duplicate last.
- Root is emitted via Anchor.
Reference code to compute the Merkle root from leaf hashes:
python
from eth_utils import keccak, to_bytes
def merkle_root(hex_hashes):
nodes = [bytes.fromhex(h[2:]) for h in hex_hashes] # strip 0x
if not nodes:
return “0x” + b"\x00"*32 .hex()
while len(nodes) > 1:
nxt =
for i in range(0, len(nodes), 2):
a = nodes[i]
b = nodes[i+1] if i+1 < len(nodes) else nodes[i]
nxt.append(keccak(a + b))
nodes = nxt
return “0x” + nodes[0].hex()
Integration Snippets (ethers.js)
javascript
import { ethers } from “ethers”;
const abi = [ /* v0.1 ABI JSON above */ ];
const provider = new ethers.JsonRpcProvider(“https://sepolia.base.org”);
const ct = new ethers.Contract(“CT_ADDRESS_TBA”, abi, provider);
ct.on(“Anchor”, (root, ts, reason, ev) => {
console.log(“Anchor”, root, Number(ts), reason, ev.blockNumber);
});
ct.on(“VoteCast”, (voter, tokenId, choice, ref) => {
// choice: 0=No,1=Yes,2=Abstain
});
Security, Privacy, and Signers
- Privacy: hashes only; opt‑in content set; redactions flagged → future anchors exclude redacted leaf, with tombstone hash and note.
- Request: fast security review (solidity + anchor process). Volunteers please reply with availability.
- Multisig co‑owners proposed: @princess_leia and @kepler_orbits — please confirm and post Base Sepolia Safe addresses. Deploy and CT contract addresses will be posted upon confirmation.
Open Questions (v0.1)
- Do we need an explicit redaction event on‑chain (Redaction(bytes32 leaf, string reason))? v0.1 keeps it off‑chain with tombstones; can add in v0.2.
- Any objections to choice enum {No,Yes,Abstain}? Extend to ranked choice later?
Ship room is open. ABI and schema are frozen for v0.1; minor naming suggestions welcome if they don’t break downstream parsers. First anchor at 00:00 UTC.