Skip to Content
Smart ContractsPublic records

Public records

The contract’s public registry is permissionless. Any wallet can write to it directly — no Monolith account, no API key, no application, no fee.

It exists because the curated registry is written by Monolith on behalf of Artifacts. The public registry is the open counterpart: a place for anyone to anchor a fingerprint-to-manifest binding on the same chain, readable by the same tooling, without going through Monolith at all.

Everything here is on the same contract described in the overview — address 0xfE9daFC133eD3e9726D4B2c24b8cC3c3AaDD8225 on chain 101010.

Registering an entry

function registerPublicManifest( bytes32 fingerprint, bytes32 manifestHash, string calldata userId, string[] calldata endpoints, string calldata manifestId ) external;
ArgumentTypeNotes
fingerprintbytes32Required, non-zero. SHA-256 of the asset’s exact bytes
manifestHashbytes32Required, non-zero. A hash committing to the manifest or metadata you’re binding
userIdstringOptional here — may be empty. Free-form; not verified by anyone
endpointsstring[]Soft Binding Resolution API URLs. Leave empty to fall back to the contract’s getDefaultEndpoints()
manifestIdstringThe manifest identifier this fingerprint resolves to

The sender’s address is recorded as registeredBy. One fingerprint can be registered once — a second write for the same fingerprint reverts with PublicManifestAlreadyExists.

import { readFile } from 'node:fs/promises' import { createHash } from 'node:crypto' import { JsonRpcProvider, Wallet, Contract } from 'ethers' const RPC = 'https://rpc.stabilityprotocol.com/zgt/try-it-out' const { address, abi } = await fetch( 'https://api.joinmonolith.com/c2pa/contract' ).then(r => r.json()) const bytes = await readFile('my-art.png') const fingerprint = '0x' + createHash('sha256').update(bytes).digest('hex') const provider = new JsonRpcProvider(RPC, 101010) const wallet = new Wallet(process.env.PRIVATE_KEY, provider) const registry = new Contract(address, abi, wallet) const tx = await registry.registerPublicManifest( fingerprint, manifestHash, // your 0x-prefixed bytes32 '', // userId — optional ['https://example.com/c2pa'], // resolution endpoints 'urn:uuid:0f9a…', // manifestId { gasPrice: 0 } // the chain is gasless ) const receipt = await tx.wait() console.log(receipt.hash)

The write emits two events — PublicManifestRegistered and PublicSoftBindingRegistered. The second is the one soft-binding resolvers watch; see Soft binding.

Reading it back

FunctionReturns
publicManifestExists(bytes32)bool
getPublicManifestByBinding(bytes32)(string[] endpoints, string manifestId)
getPublicManifestRecord(bytes32)(bytes32 manifestHash, string userId, string[] endpoints, string manifestId, uint256 timestamp)
getPublicManifestRegisteredBy(bytes32)address
getTotalPublicManifests()uint256
const [manifestHash, userId, endpoints, manifestId, timestamp] = await registry.getPublicManifestRecord(fingerprint) const registrant = await registry.getPublicManifestRegisteredBy(fingerprint)

Reads revert with PublicManifestNotFound when the fingerprint has no public entry. Check publicManifestExists first, or catch the revert.

Resolution precedence

Most of the time you don’t care which registry an answer came from — you just want to resolve a fingerprint. resolveByBindingDetailed searches both and tells you which one answered:

function resolveByBindingDetailed(bytes32 fingerprint) external view returns ( uint8 source, string[] memory endpoints, string memory manifestId, bytes32 manifestHash, address registeredBy, uint256 timestamp );
sourceMeaning
1Curated — the entry Monolith wrote for an Artifact
2Public — an entry written directly by a wallet

The curated registry always wins. If a fingerprint exists in both, you get the curated entry and source is 1. If it exists in neither, the call reverts with ManifestNotFound.

resolveByBinding is the same lookup without the provenance fields, for when you only need the endpoints and manifest id.

Removing an entry

function deletePublicManifest(bytes32 fingerprint, string calldata reason) external;

Callable by the address that registered the entry, or by Monolith. The record is removed from storage outright and PublicManifestDeleted is emitted with the supplied reason. The transaction itself stays in the chain’s history, but the registry no longer resolves that fingerprint, and it becomes available for registration again.

What a public entry proves

A public entry proves that this address wrote this binding at this time. It does not prove authorship, ownership, or that the registrant has ever seen the underlying file.

There is no fee, no rate limit, no quota, and no identity check on the public registry. Anyone can register any fingerprint they can compute — including one for a file they had no part in making — and the first write for a fingerprint is the one that sticks. Treat a public entry as a signed assertion by a wallet, and judge it on whether you trust that wallet.

Two things follow for anyone consuming these records:

  • Check registeredBy. It is the only identity in a public entry. Everything else, userId included, is free-form text chosen by whoever sent the transaction.
  • Treat endpoints as untrusted input. They are arbitrary strings supplied by the registrant and can point anywhere. Validate and sandbox them exactly as you would any third-party URL before fetching.

Curated-first resolution is what keeps this from affecting Monolith Artifacts: a public entry can never shadow the curated record for the same fingerprint.

When writes revert

ErrorCause
PublicManifestAlreadyExists(bytes32)The fingerprint already has a public entry
PublicManifestNotFound(bytes32)Read or delete for a fingerprint with no public entry
ZeroFingerprint()fingerprint was bytes32(0)
ZeroManifestHash()manifestHash was bytes32(0)
ArrayTooLong(uint256,uint256)Too many endpoints — the limit is on-chain
EmptyEndpoint()One of the endpoints strings was empty
NotAuthorizedToDelete()Delete attempted by an address that didn’t register the entry
PublicRegistryPaused()The public registry is not currently accepting writes
ContractPaused()The contract as a whole is not currently accepting writes

isPublicRegistryPaused() is a free read, so you can check before sending a write rather than discovering it from a revert.

Where to go next

  • Soft binding — what these bindings are for and how resolution works end-to-end
  • Interface — every function, event, and revert
Last updated on