Pre-launch. The protocol is not publicly live yet. This documentation describes the contracts as implemented; deployed addresses are published at launch.

Update Token Socials

Writing the on-chain metadata record. One packed bytes32 per token, owned by the deployer and transferred automatically on a successful CTO.

Interface

const SOCIALS_ABI = [
  "function set_socials(address token, bytes32 packed)",
  "function transfer_owner(address token, address new_owner)",
  "function get_socials(address token) view returns (bytes32)",
  "function get_owner(address token) view returns (address)",
  "function get_record(address token) view returns (bytes32 packed, address owner, uint256 updatedAt)",
  "function is_registered(address token) view returns (bool)"
];

const socials = new ethers.Contract(TOKEN_SOCIALS, SOCIALS_ABI, signer);
SOCIALS_ABI = json.load(open("TokenSocials.abi.json"))

socials = w3.eth.contract(address=TOKEN_SOCIALS, abi=SOCIALS_ABI)

Reading a record

const [packed, owner, updatedAt] = await socials.get_record(token);

if (packed === ethers.ZeroHash) {
    // Registered but never set — deployed with no socials
}
packed, owner, updated_at = socials.functions.get_record(token).call()

if packed == bytes(32):
    pass  # Registered but never set — deployed with no socials

get_record returns everything in one call: owner and timestamp share a storage slot, so a full read is two SLOADs.

Writing

const me = await signer.getAddress();
if ((await socials.get_owner(token)) !== me) throw new Error("not owner");

await (await socials.set_socials(token, packed)).wait();
me = acct.address
if socials.functions.get_owner(token).call() != me:
    raise RuntimeError("not owner")

send(socials.functions.set_socials(token, packed))

The write replaces the record wholesale and refreshes the timestamp. There is no partial update — build the full 32 bytes each time.

The contract does not interpret the bytes

It stores 32 opaque bytes and takes no view on their meaning. There is no schema, no validation, and no canonical encoding — the layout is an application-layer convention between whoever writes and whoever reads. Consumers should tolerate records that do not decode under their expected scheme, including records written by a different tool.

A worked encoding

Thirty-two bytes is tight, so records generally store identifiers rather than URLs. One workable layout, offered as an example rather than a standard:

// [0]     scheme version
// [1:12]  platform handle A, ascii, null-padded
// [12:23] platform handle B, ascii, null-padded
// [23:32] reserved

function packSocials(version, handleA, handleB) {
    const buf = new Uint8Array(32);
    buf[0] = version;
    const enc = new TextEncoder();
    buf.set(enc.encode(handleA.slice(0, 11)), 1);
    buf.set(enc.encode(handleB.slice(0, 11)), 12);
    return ethers.hexlify(buf);
}
# [0]     scheme version
# [1:12]  platform handle A, ascii, null-padded
# [12:23] platform handle B, ascii, null-padded
# [23:32] reserved

def pack_socials(version, handle_a, handle_b):
    buf = bytearray(32)
    buf[0] = version
    a = handle_a[:11].encode("ascii")
    b = handle_b[:11].encode("ascii")
    buf[1:1 + len(a)] = a
    buf[12:12 + len(b)] = b
    return bytes(buf)

For anything larger than a handle, store a content hash on-chain and resolve the payload off-chain.

Ownership

Socials ownership starts with the deployer and moves automatically when a CTO succeeds — the hook calls transfer_owner during finalization so metadata follows the deployer role. You can also transfer it manually:

await (await socials.transfer_owner(token, newOwner)).wait();
send(socials.functions.transfer_owner(token, new_owner))
Two independent roles

Socials ownership and the fee-earning deployer role are tracked in different contracts. A CTO moves both. A manual transfer_owner moves only the metadata — the fee share stays where it was. Read PrimaryHook.getTokenInfo(token).deployer for the fee role and get_owner(token) for metadata; do not assume they match.

Setting socials at deployment

The fourth argument to deploy_pair is the initial packed record, saving a second transaction. Pass ethers.ZeroHash to skip it and set it later.

Common reverts

RevertCause
Not ownerCaller does not own the record
Not registeredNo record — the protocol token path does not register
Unauthorizedregister_token called by anything but the factory